name: Deploy runner Worker

# THE single deploy path for the nucleic-runner control plane (cloud/nucleic-runner). Three triggers:
#   • Worker-code changes on main/dev — the control plane itself changed.
#   • Manual dispatch — e.g. to re-roll the container image (see below).
#   • workflow_call — runner-image.yml calls this after pushing a new image, passing the digest
#     it just pushed. That call site used to be a copy of this whole job; the two copies had
#     already drifted (this one warns-and-continues when the digest lookup fails, the copy
#     hard-failed), which is why there is now exactly one.
#
# Container image handling: `wrangler deploy` diffs the container application by its image
# REFERENCE STRING — deploying the checked-in mutable `:v1` verbatim would either report
# "no changes" (string unchanged → a re-pushed tag never rolls out; the 2026-07 stale-runner
# incident) or, worse, replace a digest-pinned production reference with whatever `:v1`
# resolves to. So the wrangler.jsonc reference is always pinned to a digest before deploying:
#   • called with `digest` (from runner-image.yml) → pin to exactly that, and FAIL if it can't
#     be applied; the caller just pushed it and a silent miss would ship a stale image.
#   • otherwise (code-only deploy) → resolve the registry's CURRENT digest for the committed
#     tag, so the deploy keeps (or converges on) the newest pushed image.
#
# Secrets (RUNNER_TOKEN_SECRET / RUNNER_ADMIN_SECRET / RUNNER_WAKE_SECRET) live on the Worker
# and survive deploys — this workflow never touches them. Uses the same CLOUDFLARE_API_TOKEN
# repo secret as runner-image.yml (Workers Scripts:Edit).

on:
  push:
    branches: [main, dev]
    paths:
      - cloud/nucleic-runner/**
      - .github/workflows/runner-deploy.yml
  workflow_dispatch:
  workflow_call:
    inputs:
      digest:
        description: >
          Exact image manifest digest (sha256:…) to pin the containers block to. Passed by
          runner-image.yml with the digest it just pushed. Empty → resolve the tag's current
          digest from the registry instead.
        type: string
        required: false
        default: ""
    secrets:
      CLOUDFLARE_API_TOKEN:
        required: true

# One deploy at a time — this workflow and runner-image.yml's deploy job share the group so a
# code deploy and an image-rollout deploy can't interleave.
concurrency:
  group: runner-worker-deploy
  cancel-in-progress: false

jobs:
  deploy:
    name: deploy Worker (digest-pinned containers block)
    runs-on: ubuntu-latest
    permissions:
      contents: read
    defaults:
      run:
        working-directory: cloud/nucleic-runner
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 22
      # No lockfile in cloud/nucleic-runner (dev deps only, pinned exactly in package.json).
      - run: npm install
      - name: Pin the containers image to the caller's pushed digest
        # runner-image.yml just pushed this exact manifest, so there is nothing to resolve and
        # nothing to fall back to: if the rewrite does not land, the deploy would ship whatever
        # the committed tag happens to mean. Strict — the grep asserts the sed matched.
        if: ${{ inputs.digest != '' }}
        env:
          DIGEST: ${{ inputs.digest }}
        run: |
          # Shape-check before it reaches sed: anything but a bare sha256:<64 hex> would either
          # corrupt the image reference or smuggle regex/shell metacharacters into the rewrite.
          case "$DIGEST" in
            sha256:*) ;;
            *) echo "::error::caller passed a malformed image digest: ${DIGEST}"; exit 1 ;;
          esac
          printf '%s' "$DIGEST" | grep -qE '^sha256:[0-9a-f]{64}$'
          sed -i -E "s#(\"image\": \"registry\\.cloudflare\\.com/[^\"]+/nucleic-runner)[:@][^\"]+#\\1@${DIGEST}#" wrangler.jsonc
          grep -F "@${DIGEST}" wrangler.jsonc

      - name: Pin the containers image to the registry's current digest
        # Code-only deploy: nobody handed us a digest, so resolve what the tag in wrangler.jsonc
        # currently points at and pin to that (see the header). `wrangler containers images list`
        # NEVER prints digests
        # (hardcoded off in 4.105 — it broke the first version of this step), so resolve it the
        # way wrangler does internally: mint a short-lived pull credential from the containers
        # API, then read `Docker-Content-Digest` off the registry's standard v2 manifest
        # endpoint. The LOOKUP is best-effort: every failure routes to the tag fallback (each
        # command is `|| var=""`-guarded — a bare failing $(…) assignment aborts the whole step
        # under bash -e before the fallback can run, which is exactly how v1 of this step died).
        # Falling back to the committed tag is safe but churny: the reference string still
        # differs from the live digest-pinned one, so wrangler rolls out — to the tag's newest
        # image (runner-image.yml re-pushes the tag on every build) — restarting containers.
        # Once a digest IS found, the sed+grep pin is strict: a rewrite that stops matching
        # must fail the deploy, not silently ship.
        if: ${{ inputs.digest == '' }}
        env:
          CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
          # Account that owns the managed registry. NOT secret — committed in the wrangler.jsonc
          # image reference; keep in sync with runner-image.yml.
          CLOUDFLARE_ACCOUNT_ID: c0b8e2a01e80cd7106e0e98e96f5415f
        run: |
          set -o pipefail
          tag=$(sed -nE 's#.*"image": "registry\.cloudflare\.com/[^"]+/nucleic-runner:([^"]+)".*#\1#p' wrangler.jsonc)
          test -n "$tag"  # a committed digest reference would be a mistake — the tag is the source of truth
          password=$(curl -sfS -X POST \
            -H "Authorization: Bearer ${CLOUDFLARE_API_TOKEN}" -H "Content-Type: application/json" \
            "https://api.cloudflare.com/client/v4/accounts/${CLOUDFLARE_ACCOUNT_ID}/containers/registries/registry.cloudflare.com/credentials" \
            -d '{"expiration_minutes":5,"permissions":["pull"]}' \
            | jq -r '.result.password // .password // empty') || password=""
          digest=""
          if [ -n "$password" ]; then
            digest=$(curl -sfSI -u "v1:${password}" \
              -H "Accept: application/vnd.oci.image.index.v1+json, application/vnd.docker.distribution.manifest.list.v2+json, application/vnd.oci.image.manifest.v1+json, application/vnd.docker.distribution.manifest.v2+json" \
              "https://registry.cloudflare.com/v2/${CLOUDFLARE_ACCOUNT_ID}/nucleic-runner/manifests/${tag}" \
              | tr -d '\r' | awk 'tolower($1) == "docker-content-digest:" { print $2 }') || digest=""
          fi
          if [ -z "$digest" ]; then
            echo "::warning::could not resolve the current digest of nucleic-runner:${tag} from the managed registry — deploying the tag reference as committed (wrangler still rolls out to the tag's newest image; containers restart). Fix the digest lookup to avoid the churn."
            exit 0
          fi
          echo "resolved nucleic-runner:${tag} → ${digest}"
          sed -i -E "s#(\"image\": \"registry\\.cloudflare\\.com/[^\"]+/nucleic-runner)[:@][^\"]+#\\1@${digest}#" wrangler.jsonc
          grep -F "@${digest}" wrangler.jsonc
      - name: Test + typecheck + dry-run
        run: npm test && npm run typecheck && npm run check
      - name: Deploy (rolls out the pinned digest)
        env:
          CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
        run: |
          set -o pipefail
          npm run deploy 2>&1 | tee /tmp/deploy.log
          # Staleness guard: the deploy must have considered the container application at all.
          # With a digest-pinned reference, "no changes" is only possible when production
          # already runs exactly this digest — so either outcome (rollout started, or
          # already-current) is success; the failure mode is the app never being mentioned.
          if ! grep -q "nucleic-runner-runnercontainer" /tmp/deploy.log; then
            echo "::error::wrangler deploy never mentioned the container application — the containers block may have been dropped or renamed; the image was NOT rolled out"
            exit 1
          fi
          if grep -q "no changes" /tmp/deploy.log; then
            echo "container application already runs the pinned digest (no rollout needed)"
          else
            echo "rollout created for the pinned digest"
          fi
      - name: Live health check
        run: |
          code=$(curl -sS -m 15 -o /dev/null -w '%{http_code}' https://runner.nucleic.blakeslee.xyz/health)
          echo "health: HTTP $code"
          test "$code" = 200
