Skip to content

Commit a06dc67

Browse files
bloveclaude
andcommitted
ci: gate deploys on the last promoted commit, not the last push
The deploy job decides what to redeploy by diffing `github.event.before..sha` — "what changed in THIS push". That silently drops work whenever a run does not reach the deploy steps, and nothing ever re-examines the range: - Pushes to main do not cancel in-progress runs, but GitHub keeps only the newest QUEUED run per concurrency group and cancels the rest. - A run can fail on a job unrelated to what it changed. Either way the next push diffs only its own range, so the skipped change stays undeployed until some later commit happens to touch a gated path. Seen live on 2026-08-31. #902 fixed the subagents agent URL under cockpit/ag-ui/subagents/angular/ and merged green, but its run failed on an unrelated website e2e regression before reaching the deploy job. The two green runs after it touched no gated path, so the fix sat on main, undeployed, next to the cockpit/runtimes examples stranded by two culled runs. All three shipped ~2.5h later only because #907 incidentally touched scripts/assemble-examples.ts. The failure mode is silent: main is green, the deploy job is green, and production is stale. Resolve the baseline once from refs/deploy/last-promoted, which advances only after a fully successful, non-stale deploy job, and feed it to all three gates. The range then covers everything not yet promoted rather than one push. Fail-safe by construction: with no marker (the first run after this lands) or an unresolvable one, it falls back to the previous behaviour, and the marker is never advanced by a stale, failed, or partial run — advancing past unshipped work is precisely the bug being fixed. Note: ag-ui-demo-deploy and posthog-sync-plan gate on github.event.before the same way and are left alone here; each deploys a different target and would need its own marker. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent fc65736 commit a06dc67

1 file changed

Lines changed: 56 additions & 11 deletions

File tree

.github/workflows/ci.yml

Lines changed: 56 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -650,6 +650,10 @@ jobs:
650650
runs-on: ubuntu-latest
651651
# Only deploy on pushes to main, not on pull requests
652652
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
653+
permissions:
654+
# Needed to advance refs/deploy/last-promoted (see "Resolve deploy
655+
# baseline"). Nothing else in this job writes to the repo.
656+
contents: write
653657
steps:
654658
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
655659
with:
@@ -675,14 +679,53 @@ jobs:
675679
else
676680
echo "stale=false" >> "$GITHUB_OUTPUT"
677681
fi
678-
- name: Detect deploy-relevant changes
679-
id: deploy_preflight
682+
- name: Resolve deploy baseline
683+
id: deploy_baseline
680684
run: |
681-
base_sha="${{ github.event.before }}"
685+
# Gating on `github.event.before..sha` asks "what changed in THIS
686+
# push", which silently drops work whenever a run does not reach the
687+
# deploy steps. Pushes to main do not cancel in-progress runs, but
688+
# GitHub keeps only the newest QUEUED run per concurrency group and
689+
# cancels the rest, and a run can also fail on an unrelated job — in
690+
# both cases that push's diff range is never re-examined. The next
691+
# push only diffs its own range, so the skipped change stays
692+
# undeployed until some later commit happens to touch a gated path.
693+
#
694+
# Seen live 2026-08-31: #902 (cockpit/ag-ui/subagents/angular) failed
695+
# on an unrelated website e2e regression, and the two green runs after
696+
# it touched no gated path — so the fix sat on main, undeployed,
697+
# alongside the cockpit/runtimes examples from two culled runs. All
698+
# three shipped only when #907 incidentally touched a gated file.
699+
#
700+
# refs/deploy/last-promoted advances only after a fully successful
701+
# deploy job, so this range covers everything not yet promoted.
702+
# Falls back to the old behaviour when the marker is absent
703+
# (first run after this lands) or no longer resolves (history rewrite).
704+
base_sha=""
705+
marker="$(git ls-remote origin refs/deploy/last-promoted 2>/dev/null | cut -f1)"
706+
if [ -n "$marker" ] && git cat-file -e "${marker}^{commit}" 2>/dev/null; then
707+
base_sha="$marker"
708+
echo "::notice::Deploy baseline: last promoted commit ${marker}."
709+
else
710+
base_sha="${{ github.event.before }}"
711+
echo "::notice::No usable refs/deploy/last-promoted marker; falling back to github.event.before (${base_sha})."
712+
fi
682713
head_sha="${{ github.sha }}"
683714
if [ -z "$base_sha" ] || [ "$base_sha" = "0000000000000000000000000000000000000000" ]; then
684715
base_sha="$(git rev-parse "$head_sha^")"
685716
fi
717+
# Guarantee every consumer gets a resolvable commit: the fallback can
718+
# name a commit this clone does not have (force-push, deleted branch).
719+
if ! git cat-file -e "${base_sha}^{commit}" 2>/dev/null; then
720+
echo "::warning::Deploy baseline ${base_sha} does not resolve; using ${head_sha}^ instead."
721+
base_sha="$(git rev-parse "$head_sha^")"
722+
fi
723+
echo "base=$base_sha" >> "$GITHUB_OUTPUT"
724+
- name: Detect deploy-relevant changes
725+
id: deploy_preflight
726+
run: |
727+
base_sha="${{ steps.deploy_baseline.outputs.base }}"
728+
head_sha="${{ github.sha }}"
686729
687730
if ! git cat-file -e "$base_sha^{commit}" 2>/dev/null; then
688731
git fetch --no-tags origin "$base_sha"
@@ -702,11 +745,8 @@ jobs:
702745
- name: Check if examples changed
703746
id: examples_changed
704747
run: |
705-
base_sha="${{ github.event.before }}"
748+
base_sha="${{ steps.deploy_baseline.outputs.base }}"
706749
head_sha="${{ github.sha }}"
707-
if [ -z "$base_sha" ] || [ "$base_sha" = "0000000000000000000000000000000000000000" ]; then
708-
base_sha="$(git rev-parse "$head_sha^")"
709-
fi
710750
changed_files="$(git diff --name-only "$base_sha" "$head_sha")"
711751
examples_changed=false
712752
if printf '%s\n' "$changed_files" | grep -E '^cockpit/.*/angular/' >/dev/null; then
@@ -739,11 +779,8 @@ jobs:
739779
if: steps.deploy_preflight.outputs.relevant == 'true'
740780
id: affected
741781
run: |
742-
base_sha="${{ github.event.before }}"
782+
base_sha="${{ steps.deploy_baseline.outputs.base }}"
743783
head_sha="${{ github.sha }}"
744-
if [ -z "$base_sha" ] || [ "$base_sha" = "0000000000000000000000000000000000000000" ]; then
745-
base_sha="$(git rev-parse "$head_sha^")"
746-
fi
747784
748785
if ! git cat-file -e "$base_sha^{commit}" 2>/dev/null; then
749786
git fetch --no-tags origin "$base_sha"
@@ -840,6 +877,14 @@ jobs:
840877
npx vercel pull --yes --environment=production --token=${{ secrets.VERCEL_TOKEN }}
841878
npx vercel deploy --prebuilt --prod --yes --token=${{ secrets.VERCEL_TOKEN }}
842879
880+
# Advance only when the whole job succeeded and actually promoted. On a
881+
# stale run, a failure, or a partial deploy the marker stays put, so the
882+
# next run's range still covers whatever did not ship. Never `always()`:
883+
# advancing past unshipped work is the bug this step exists to prevent.
884+
- name: Record this commit as promoted
885+
if: success() && steps.freshness.outputs.stale != 'true'
886+
run: git push origin --force "${{ github.sha }}:refs/deploy/last-promoted"
887+
843888
demo-deploy:
844889
name: Canonical demo → Vercel
845890
timeout-minutes: 30 # fail fast instead of blocking the main concurrency group on a hang

0 commit comments

Comments
 (0)