Skip to content

Rolling canary build #50

Rolling canary build

Rolling canary build #50

name: Rolling canary build
# Triggered after the "Code tests & eval" workflow succeeds on main — so
# :canary always reflects the latest *tested* commit, never an untested or
# red-on-main one. Publishes the `:canary` Docker image; the Pages canary
# subpath at web.octi.darken.eu/canary/ is deployed by the separate
# deploy-pages.yml (workflow_run on this workflow's success).
#
# Naming follows the cross-vendor convention (Chrome Canary, GitHub canary
# deploys). The companion in-app banner says "Canary build" + "This is a
# bleeding edge build" — see src/ui/CanaryBanner.svelte.
on:
workflow_dispatch:
workflow_run:
workflows: ["Code tests & eval"]
types: [completed]
permissions:
contents: read
# Per-job concurrency (no workflow-level cancel) so publish can't be killed
# mid-run by a fresh push. Stale-SHA guard inside the publish step aborts if
# main has moved past us during the build window.
jobs:
compute-version:
name: Compute canary version
# workflow_dispatch always proceeds; workflow_run only when the upstream
# code-checks completed successfully on a push to main (skip PR runs and
# failures). Mirrors deploy-pages.yml's filter pattern.
if: |
github.event_name == 'workflow_dispatch' ||
(github.event.workflow_run.conclusion == 'success' &&
github.event.workflow_run.event == 'push' &&
github.event.workflow_run.head_branch == 'main')
runs-on: ubuntu-22.04
concurrency:
group: release-canary-build-${{ github.repository }}
cancel-in-progress: true
outputs:
version: ${{ steps.derive.outputs.version }}
short_sha: ${{ steps.derive.outputs.short_sha }}
should_run: ${{ steps.skip-check.outputs.should_run }}
steps:
- name: Guard ref must be main
env:
REF: ${{ github.ref }}
run: |
set -euo pipefail
if [[ "$REF" != "refs/heads/main" ]]; then
echo "::error::canary must run on main (got '$REF')"
exit 1
fi
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
# Pin to the SHA that code-checks tested. With workflow_run trigger,
# github.sha is the default-branch tip at trigger time (which may
# have moved past the tested SHA); workflow_run.head_sha is the SHA
# the upstream actually ran on. Falls back to github.sha for
# workflow_dispatch.
ref: ${{ github.event.workflow_run.head_sha || github.sha }}
fetch-depth: 2
persist-credentials: false
- name: Skip release-bump commits
id: skip-check
run: |
set -euo pipefail
msg=$(git log -1 --pretty=%s)
# The release-prepare workflow pushes commits titled "Release: vX.Y.Z".
# Canary must NOT publish those — the release-tag workflow is the
# only mover for stable, and a canary publish from the same commit
# could race ahead of release-tag's E2E gate.
if [[ "$msg" =~ ^Release: ]]; then
echo "::notice::skipping canary for release-bump commit: $msg"
echo "should_run=false" >> "$GITHUB_OUTPUT"
else
echo "should_run=true" >> "$GITHUB_OUTPUT"
fi
- name: Derive canary version
id: derive
if: steps.skip-check.outputs.should_run == 'true'
run: |
set -euo pipefail
base=$(node -p "require('./package.json').version")
# Strip any -rc/-beta/-canary suffix from the package.json base. We want
# the X.Y.Z numeric core; -canary.<sha8> is appended below.
base_numeric=$(echo "$base" | sed -E 's/-(rc[0-9]+|beta[0-9]+|canary\.[0-9a-zA-Z]+)$//')
short=$(git rev-parse --short=8 HEAD)
echo "version=${base_numeric}-canary.${short}" >> "$GITHUB_OUTPUT"
echo "short_sha=${short}" >> "$GITHUB_OUTPUT"
build-and-publish-docker:
needs: [compute-version]
if: needs.compute-version.outputs.should_run == 'true'
name: Publish :canary Docker image (ghcr.io)
concurrency:
# Separate group from build so a fresh build cancelling an older build
# doesn't take this job down mid-publish.
group: release-canary-publish-${{ github.repository }}
cancel-in-progress: false
runs-on: ubuntu-22.04
permissions:
contents: read
packages: write
steps:
- name: Checkout main
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ref: main
fetch-depth: 1
persist-credentials: false
- name: Stale-SHA guard
# If main moved past the tested SHA during the build window, abort. A
# newer code-checks run will trigger a newer canary publish and we
# shouldn't move `:canary` backwards. EXPECTED_SHA is the SHA that
# code-checks ran on (workflow_run.head_sha); falls back to github.sha
# for workflow_dispatch.
env:
EXPECTED_SHA: ${{ github.event.workflow_run.head_sha || github.sha }}
run: |
set -euo pipefail
actual=$(git rev-parse HEAD)
if [[ "$actual" != "$EXPECTED_SHA" ]]; then
echo "::warning::main moved during build window (was $EXPECTED_SHA, now $actual). Aborting canary publish — a newer run will replace this."
exit 1
fi
- name: Log in to ghcr.io
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
- name: Build and push image
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
with:
context: .
push: true
tags: |
ghcr.io/${{ github.repository }}:canary
ghcr.io/${{ github.repository }}:canary-${{ needs.compute-version.outputs.short_sha }}
platforms: linux/amd64,linux/arm64
# Canary Docker images serve the SPA at root path (/) — the
# subpath-mounting only applies to GitHub Pages. Self-hosters who
# pull :canary get a fully-functional standalone build.
build-args: |
VITE_BASE=/
VITE_CHANNEL=canary
VITE_COMMIT_SHA=${{ github.event.workflow_run.head_sha || github.sha }}
VITE_APP_VERSION=${{ needs.compute-version.outputs.version }}