Repository navigation
fix(deploy): compare the lists that route requests, not only key path… #3
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Release images | |
| on: | |
| push: | |
| tags: | |
| - "v*" | |
| permissions: | |
| contents: read | |
| packages: write | |
| env: | |
| REGISTRY: ghcr.io | |
| jobs: | |
| images: | |
| name: Build and push images | |
| runs-on: ubuntu-latest | |
| outputs: | |
| box-digest: ${{ steps.box.outputs.digest }} | |
| broker-digest: ${{ steps.broker.outputs.digest }} | |
| image-owner: ${{ steps.namespace.outputs.owner }} | |
| steps: | |
| - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 | |
| - uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0 | |
| - uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0 | |
| - uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0 | |
| with: | |
| registry: ${{ env.REGISTRY }} | |
| username: ${{ github.actor }} | |
| password: ${{ secrets.GITHUB_TOKEN }} | |
| # GHCR image names must be lowercase; the repository owner may not be. | |
| # Computed once and published as a job output, so the release notes name | |
| # the same images this job actually pushed. | |
| - name: Compute lowercase image namespace | |
| id: namespace | |
| env: | |
| OWNER: ${{ github.repository_owner }} | |
| run: | | |
| owner="$(printf '%s' "$OWNER" | tr '[:upper:]' '[:lower:]')" | |
| echo "IMAGE_OWNER=$owner" >> "$GITHUB_ENV" | |
| echo "owner=$owner" >> "$GITHUB_OUTPUT" | |
| - name: Build and push box | |
| id: box | |
| uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2 | |
| with: | |
| context: . | |
| file: packages/box/Dockerfile | |
| platforms: linux/amd64,linux/arm64 | |
| push: true | |
| provenance: true | |
| tags: | | |
| ghcr.io/${{ env.IMAGE_OWNER }}/blitz-box:${{ github.ref_name }} | |
| ghcr.io/${{ env.IMAGE_OWNER }}/blitz-box:latest | |
| - name: Build and push broker | |
| id: broker | |
| uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2 | |
| with: | |
| context: . | |
| file: packages/broker/Dockerfile | |
| platforms: linux/amd64,linux/arm64 | |
| push: true | |
| provenance: true | |
| tags: | | |
| ghcr.io/${{ env.IMAGE_OWNER }}/blitz-broker:${{ github.ref_name }} | |
| ghcr.io/${{ env.IMAGE_OWNER }}/blitz-broker:latest | |
| # Deploys the control plane pinned to the box image this run just pushed, so | |
| # a version tag is a complete release with no manual publish step. Opt-in via | |
| # two repository secrets (docs/BOX-IMAGE.md § Automatic releases); without | |
| # them the job skips with a notice, so forks and CI-only checkouts are | |
| # unaffected. Secrets stay in Cloudflare: the deploy only checks their | |
| # presence with `wrangler secret list`. | |
| deploy-control-plane: | |
| name: Deploy control plane with the released box image | |
| needs: images | |
| runs-on: ubuntu-latest | |
| # Client prod. Add a required reviewer to this environment in repository | |
| # settings and the tag still triggers the release, but a person approves | |
| # the deploy. The images above are already built and pushed by then, so an | |
| # approval that never comes costs nothing but an unused image tag. | |
| environment: production | |
| steps: | |
| - name: Check for deploy configuration | |
| id: gate | |
| env: | |
| PROD_WRANGLER_TOML: ${{ secrets.PROD_WRANGLER_TOML }} | |
| CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} | |
| run: | | |
| if [ -z "$PROD_WRANGLER_TOML" ] || [ -z "$CLOUDFLARE_API_TOKEN" ]; then | |
| echo "configured=false" >> "$GITHUB_OUTPUT" | |
| echo "::notice::Control-plane deploy skipped: set the PROD_WRANGLER_TOML and CLOUDFLARE_API_TOKEN repository secrets to deploy on tag pushes (docs/BOX-IMAGE.md)." | |
| else | |
| echo "configured=true" >> "$GITHUB_OUTPUT" | |
| fi | |
| - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 | |
| if: steps.gate.outputs.configured == 'true' | |
| - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 | |
| if: steps.gate.outputs.configured == 'true' | |
| with: | |
| node-version-file: package.json | |
| cache: npm | |
| - run: npm ci | |
| if: steps.gate.outputs.configured == 'true' | |
| - name: Write the deployment wrangler.toml | |
| if: steps.gate.outputs.configured == 'true' | |
| env: | |
| PROD_WRANGLER_TOML: ${{ secrets.PROD_WRANGLER_TOML }} | |
| run: printf '%s\n' "$PROD_WRANGLER_TOML" > packages/control-plane/wrangler.toml | |
| - name: Pin the released box image | |
| if: steps.gate.outputs.configured == 'true' | |
| env: | |
| IMAGE_OWNER: ${{ needs.images.outputs.image-owner }} | |
| BOX_DIGEST: ${{ needs.images.outputs.box-digest }} | |
| run: node packages/control-plane/scripts/set-box-image-ref.mjs --ref "ghcr.io/$IMAGE_OWNER/blitz-box@$BOX_DIGEST" | |
| # VM bootstraps pull with no registry login, and GHCR packages are | |
| # born private, so deploying an unpullable digest would break every new | |
| # workspace. Failing here leaves the deployment on its previous image — | |
| # flip the package public (one-time) and re-run this job. | |
| - name: Verify the image is publicly pullable | |
| if: steps.gate.outputs.configured == 'true' | |
| env: | |
| IMAGE_OWNER: ${{ needs.images.outputs.image-owner }} | |
| BOX_DIGEST: ${{ needs.images.outputs.box-digest }} | |
| run: | | |
| token="$(curl -fsS "https://ghcr.io/token?service=ghcr.io&scope=repository:${IMAGE_OWNER}/blitz-box:pull" | jq -r '.token // empty')" | |
| code="$(curl -s -o /dev/null -w '%{http_code}' \ | |
| -H "Authorization: Bearer $token" \ | |
| -H 'Accept: application/vnd.oci.image.index.v1+json, application/vnd.docker.distribution.manifest.list.v2+json' \ | |
| "https://ghcr.io/v2/${IMAGE_OWNER}/blitz-box/manifests/${BOX_DIGEST}")" | |
| if [ "$code" != "200" ]; then | |
| echo "::error::ghcr.io/${IMAGE_OWNER}/blitz-box is not publicly pullable (manifest HTTP $code). Make the package public (Package settings → Danger zone → Change visibility), then re-run this job. Not deploying an image workspaces cannot pull." | |
| exit 1 | |
| fi | |
| - name: Deploy | |
| if: steps.gate.outputs.configured == 'true' | |
| env: | |
| CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} | |
| run: npm run deploy -w packages/control-plane | |
| # A green deploy command proves upload, not content. Ask the deployment | |
| # which commit it is, and fail when it disagrees. | |
| - name: Verify the deployment reports this commit | |
| if: steps.gate.outputs.configured == 'true' | |
| run: | | |
| origin="$(node -e 'const fs=require("node:fs");import("smol-toml").then(({parse})=>{const v=parse(fs.readFileSync("packages/control-plane/wrangler.toml","utf8")).vars||{};process.stdout.write(v.APP_URL||"")})')" | |
| if [ -z "$origin" ]; then | |
| echo "::warning::PROD_WRANGLER_TOML sets no APP_URL, so the deploy cannot be verified from here." | |
| exit 0 | |
| fi | |
| for attempt in 1 2 3 4 5; do | |
| reported="$(curl -fsS "$origin/version" | jq -r '.commit // empty' || true)" | |
| [ "$reported" = "$GITHUB_SHA" ] && echo "$origin reports $reported" && exit 0 | |
| echo "attempt $attempt: reported '${reported:-none}', expected $GITHUB_SHA" | |
| sleep 6 | |
| done | |
| echo "::error::$origin/version reports '${reported:-none}', not $GITHUB_SHA." | |
| exit 1 | |
| release-notes: | |
| name: Add image digests to GitHub Release | |
| needs: images | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: write | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| BOX_DIGEST: ${{ needs.images.outputs.box-digest }} | |
| BROKER_DIGEST: ${{ needs.images.outputs.broker-digest }} | |
| IMAGE_OWNER: ${{ needs.images.outputs.image-owner }} | |
| steps: | |
| - name: Create or update release notes | |
| shell: bash | |
| run: | | |
| tag="${GITHUB_REF_NAME}" | |
| if ! gh release view "$tag" --repo "$GITHUB_REPOSITORY" >/dev/null 2>&1; then | |
| gh release create "$tag" --repo "$GITHUB_REPOSITORY" --generate-notes | |
| fi | |
| # Re-running a release must replace the digest section, not append a | |
| # second one, so strip any section this step wrote before and rebuild | |
| # it from the digests this run pushed. | |
| # Command substitution drops trailing newlines, so the separator below | |
| # is the only one and the output is byte-identical on every re-run. | |
| kept="$(gh release view "$tag" --repo "$GITHUB_REPOSITORY" --json body --jq .body \ | |
| | awk '/^## Container image digests$/ { exit } { print }')" | |
| { | |
| printf '%s\n\n' "$kept" | |
| printf '## Container image digests\n\n' | |
| printf -- '- `ghcr.io/%s/blitz-box:%s` — `%s`\n' "$IMAGE_OWNER" "$tag" "$BOX_DIGEST" | |
| printf -- '- `ghcr.io/%s/blitz-broker:%s` — `%s`\n' "$IMAGE_OWNER" "$tag" "$BROKER_DIGEST" | |
| } >"$RUNNER_TEMP/release-notes.md" | |
| gh release edit "$tag" --repo "$GITHUB_REPOSITORY" --notes-file "$RUNNER_TEMP/release-notes.md" |