@@ -34,135 +34,9 @@ permissions:
3434 contents : read
3535
3636jobs :
37- # Promotion PRs (staging→main etc.) double-run the test suite: every commit
38- # on staging/main already gets a push-event run of the exact same test-build,
39- # and the pull_request "synchronize" run for the open release PR re-runs it on
40- # the same sha seconds later (~39 duplicate runs / 5 days measured Jul 2026).
41- # This gate skips the PR run's test-build ONLY when it can prove the identical
42- # work already passed elsewhere:
43- # 1. the PR base adds no file changes over the merge base with the head sha
44- # (compare head...base has an empty diff), so the merge result's tree is
45- # identical to the head tree the push run tested. Plain ancestry is not
46- # enough of a check here: main's merge-only ruleset leaves merge commits
47- # on main that staging lacks, so main...staging is permanently
48- # "diverged" — but those merge commits carry no tree delta. A real
49- # hotfix landed directly on the base makes the diff non-empty and we
50- # run tests here;
51- # 2. the push-event CI run at the same head sha finished its test jobs with
52- # conclusion success (polled, since push + PR runs start simultaneously).
53- # Fail-open by construction: any API error, timeout, missing run, or push-run
54- # failure leaves covered=false and the PR run tests normally, so the PR check
55- # is green only if tests passed either here or on the identical tree. Not a
56- # workflow-level filter on purpose — a job-level skip still reports a
57- # (successful) check context. NOTE: when test-build is skipped, its nested
58- # "Test and Build / ..." contexts are not created; verified 2026-07-22 that
59- # no required status checks are configured on main/staging (rulesets contain
60- # only pull_request/deletion/non_fast_forward). If required checks are ever
61- # added, require the caller "Test and Build" context, not the nested ones.
62- # dev is excluded: push runs on dev skip test-build, so dev-headed PRs have
63- # no push-run coverage to reuse.
64- dedup-promotion :
65- name : Dedup Promotion PR
66- runs-on : ${{ (vars.CI_PROVIDER == '' || vars.CI_PROVIDER == 'blacksmith') && 'blacksmith-2vcpu-ubuntu-2404' || 'ubuntu-latest' }}
67- timeout-minutes : 15
68- if : >-
69- github.event_name == 'pull_request' &&
70- github.event.pull_request.head.repo.full_name == github.repository &&
71- contains(fromJSON('["main", "staging"]'), github.event.pull_request.head.ref)
72- permissions :
73- contents : read
74- actions : read
75- outputs :
76- covered : ${{ steps.probe.outputs.covered }}
77- steps :
78- - name : Probe for a passing push run at the same sha
79- id : probe
80- env :
81- GH_TOKEN : ${{ github.token }}
82- REPO : ${{ github.repository }}
83- HEAD_SHA : ${{ github.event.pull_request.head.sha }}
84- BASE_SHA : ${{ github.event.pull_request.base.sha }}
85- BASE_REF : ${{ github.event.pull_request.base.ref }}
86- run : |
87- COVERED=false
88-
89- # (1) Merge-tree equivalence: compare head...base diffs the merge
90- # base against the base tip. An empty diff means the base contributes
91- # nothing beyond what head already contains (merge commits only), so
92- # the PR merge tree equals the head tree the push run tested. Any
93- # error yields "unknown" and we run the tests.
94- BASE_DELTA="$(gh api "repos/${REPO}/compare/${HEAD_SHA}...${BASE_SHA}" --jq '.files | length' 2>/dev/null || echo unknown)"
95- if [ "$BASE_DELTA" != "0" ]; then
96- echo "Base tip changes ${BASE_DELTA} file(s) over the merge base; merge tree differs from head — running tests in this PR run."
97- echo "covered=false" >> "$GITHUB_OUTPUT"
98- exit 0
99- fi
100-
101- # (2) Poll the push-event CI run's test jobs (they start seconds after
102- # this run and take ~4 min). Any conclusion other than success, or
103- # deadline expiry, falls through to covered=false.
104- DEADLINE=$((SECONDS + 600))
105- while [ "$SECONDS" -lt "$DEADLINE" ]; do
106- RUN_JSON="$(gh api "repos/${REPO}/actions/workflows/ci.yml/runs?event=push&head_sha=${HEAD_SHA}&per_page=5" 2>/dev/null || echo '')"
107- RUN_ID="$(printf '%s' "$RUN_JSON" | jq -r '.workflow_runs[0].id // empty' 2>/dev/null || echo '')"
108- if [ -n "$RUN_ID" ]; then
109- # Jobs from the reusable test-build workflow are prefixed
110- # "Test and Build /". If that name ever changes, fall back to the
111- # overall run conclusion (stricter, still correct).
112- STATE="$(gh api "repos/${REPO}/actions/runs/${RUN_ID}/jobs?per_page=100" --jq '
113- [.jobs[] | select(.name | startswith("Test and Build /"))] as $t |
114- if ($t | length) == 0 then "nojobs"
115- elif all($t[]; .conclusion == "success") then "success"
116- elif any($t[]; .conclusion != null and .conclusion != "success") then "failed"
117- else "pending" end' 2>/dev/null || echo pending)"
118- if [ "$STATE" = "nojobs" ]; then
119- # Nested reusable-workflow jobs appear only after the caller
120- # starts, so an in-progress run with no "Test and Build /"
121- # jobs yet just needs another poll. Once the run has
122- # COMPLETED without them, fail closed: the job was renamed or
123- # tests were skipped — never infer coverage from the overall
124- # run conclusion. Update the prefix here on a rename.
125- RUN_STATUS="$(printf '%s' "$RUN_JSON" | jq -r '.workflow_runs[0].status // "unknown"' 2>/dev/null || echo unknown)"
126- if [ "$RUN_STATUS" = "completed" ]; then
127- echo "Push run ${RUN_ID} completed with no 'Test and Build /' jobs — running tests in this PR run (update the prefix if the job was renamed)."
128- break
129- fi
130- STATE="pending"
131- fi
132- if [ "$STATE" = "success" ]; then
133- # (3) Re-verify merge-tree equivalence against the LIVE base
134- # tip at decision time: the base branch may have gained real
135- # commits during the poll, in which case the frozen BASE_SHA
136- # check from step (1) is stale and the skip would be unsound.
137- # Any error yields "unknown" and we run the tests.
138- LIVE_DELTA="$(gh api "repos/${REPO}/compare/${HEAD_SHA}...heads/${BASE_REF}" --jq '.files | length' 2>/dev/null || echo unknown)"
139- if [ "$LIVE_DELTA" = "0" ]; then
140- COVERED=true
141- echo "Push run ${RUN_ID} passed its test jobs for ${HEAD_SHA} and the live base tip still adds no file changes — skipping duplicate test-build."
142- else
143- echo "Base branch moved during the poll (live delta: ${LIVE_DELTA}) — running tests in this PR run."
144- fi
145- break
146- elif [ "$STATE" = "failed" ]; then
147- echo "Push run ${RUN_ID} did not pass (state: ${STATE}) — running tests in this PR run."
148- break
149- fi
150- fi
151- sleep 20
152- done
153-
154- echo "covered=${COVERED}" >> "$GITHUB_OUTPUT"
155-
15637 test-build :
15738 name : Test and Build
158- needs : [dedup-promotion]
159- # !cancelled(): dedup-promotion is skipped on every non-promotion event and
160- # a skipped need would otherwise skip this job too. covered != 'true' is
161- # fail-open — empty (skipped/failed probe) means run the tests.
162- if : >-
163- !cancelled() &&
164- (github.ref != 'refs/heads/dev' || github.event_name == 'pull_request') &&
165- needs.dedup-promotion.outputs.covered != 'true'
39+ if : github.ref != 'refs/heads/dev' || github.event_name == 'pull_request'
16640 uses : ./.github/workflows/test-build.yml
16741 secrets : inherit
16842
@@ -202,10 +76,10 @@ jobs:
20276 migrate :
20377 name : Migrate DB
20478 needs : [test-build]
205- # !cancelled() + explicit result check: test-build's needs chain contains
206- # the (push-skipped) dedup-promotion job, and a skipped transitive ancestor
207- # fails the implicit success() and would cascade-skip the whole deploy
208- # chain (migrate -> promote-images -> CodeDeploy) .
79+ # Explicit need results instead of the implicit success(): a skipped job
80+ # anywhere in the transitive needs chain silently fails implicit success()
81+ # and cascade-skips the deploy chain (migrate -> promote-images ->
82+ # CodeDeploy) — this bit us on 2026-07-23. State requirements explicitly .
20983 if : >-
21084 !cancelled() &&
21185 needs.test-build.result == 'success' &&
@@ -436,8 +310,7 @@ jobs:
436310 promote-images :
437311 name : Promote Images
438312 needs : [migrate, build-amd64]
439- # Explicit results: see migrate's comment — the implicit success() fails
440- # on the skipped dedup-promotion ancestor.
313+ # Explicit results: see migrate's comment.
441314 if : >-
442315 !cancelled() &&
443316 needs.migrate.result == 'success' &&
0 commit comments