@@ -231,13 +231,108 @@ jobs:
231231 echo "::notice::Release dry-run predicts v${RELEASE_VERSION} (${BUMP_TYPE})"
232232 fi
233233
234- # ── Step 2: Build once ──
234+ # ── Step 2: Exact-SHA release qualification ──
235+ #
236+ # A release owns an immutable candidate (github.sha), unlike Main Guard's
237+ # push monitoring, which is intentionally latest-wins. Reuse terminal green
238+ # Main Guard evidence for this SHA when it exists; otherwise dispatch a
239+ # non-cancellable qualification and await that exact proof.
240+ release-qualification :
241+ name : Release qualification
242+ needs : check
243+ if : needs.check.outputs.should-release == 'true' && needs.check.outputs.recovery-release != 'true'
244+ runs-on : ubuntu-latest
245+ timeout-minutes : 45
246+ outputs :
247+ candidate-sha : ${{ steps.proof.outputs.candidate-sha }}
248+ proof-sha : ${{ steps.proof.outputs.proof-sha }}
249+ proof-run : ${{ steps.proof.outputs.proof-run }}
250+ proof-status : ${{ steps.proof.outputs.proof-status }}
251+ next-action : ${{ steps.proof.outputs.next-action }}
252+ steps :
253+ - name : Reuse or await exact Main Guard proof
254+ id : proof
255+ env :
256+ GH_TOKEN : ${{ secrets.GITHUB_TOKEN }}
257+ CANDIDATE_SHA : ${{ github.sha }}
258+ DRY_RUN : ${{ inputs.dry-run || 'false' }}
259+ run : |
260+ set -euo pipefail
261+
262+ title="Release Qualification ${CANDIDATE_SHA}"
263+ run_json="$(gh run list --workflow audit-debt.yml --limit 100 --json databaseId,displayTitle,status,conclusion,url | jq -c --arg title "$title" 'map(select(.displayTitle == $title)) | first // empty')"
264+ action="reused exact terminal proof"
265+
266+ # A push-triggered Main Guard has the candidate as its workflow SHA,
267+ # so it is equally valid evidence and avoids an unnecessary dispatch.
268+ if [ -z "$run_json" ]; then
269+ run_json="$(gh run list --workflow audit-debt.yml --commit "$CANDIDATE_SHA" --status completed --limit 100 --json databaseId,status,conclusion,url | jq -c 'map(select(.conclusion == "success")) | first // empty')"
270+ fi
271+
272+ if [ -z "$run_json" ]; then
273+ gh workflow run audit-debt.yml --ref main -f qualification_sha="$CANDIDATE_SHA"
274+ action="started immutable qualification"
275+ for _ in $(seq 1 30); do
276+ run_json="$(gh run list --workflow audit-debt.yml --limit 100 --json databaseId,displayTitle,status,conclusion,url | jq -c --arg title "$title" 'map(select(.displayTitle == $title)) | first // empty')"
277+ [ -n "$run_json" ] && break
278+ sleep 2
279+ done
280+ fi
281+
282+ if [ -z "$run_json" ]; then
283+ echo "::error::Main Guard qualification did not appear for ${CANDIDATE_SHA}"
284+ exit 1
285+ fi
286+
287+ run_id="$(jq -r '.databaseId' <<<"$run_json")"
288+ while :; do
289+ run_json="$(gh run view "$run_id" --json status,conclusion,url)"
290+ status="$(jq -r '.status' <<<"$run_json")"
291+ [ "$status" = "completed" ] && break
292+ sleep 30
293+ done
294+
295+ conclusion="$(jq -r '.conclusion // "cancelled"' <<<"$run_json")"
296+ url="$(jq -r '.url' <<<"$run_json")"
297+ if [ "$conclusion" = "success" ]; then
298+ next_action="release immutable candidate"
299+ else
300+ next_action="repair or rerun the exact qualification"
301+ fi
302+
303+ {
304+ echo "candidate-sha=${CANDIDATE_SHA}"
305+ echo "proof-sha=${CANDIDATE_SHA}"
306+ echo "proof-run=${url}"
307+ echo "proof-status=${conclusion}"
308+ echo "next-action=${next_action}"
309+ } >> "$GITHUB_OUTPUT"
310+
311+ {
312+ echo "### Release qualification"
313+ echo
314+ echo "- Selected SHA: \`${CANDIDATE_SHA}\`"
315+ echo "- Exact gate proof SHA: \`${CANDIDATE_SHA}\`"
316+ echo "- Exact gate proof run: ${url}"
317+ echo "- Proof status: \`${conclusion}\`"
318+ echo "- Next action: ${next_action}"
319+ echo "- Proof source: ${action}"
320+ if [ "$DRY_RUN" = "true" ]; then
321+ echo "- Dry run: release mutation remains disabled"
322+ fi
323+ } >> "$GITHUB_STEP_SUMMARY"
324+
325+ [ "$conclusion" = "success" ]
326+
327+ # ── Step 3: Build once ──
235328 # Compile homeboy from source once and share the binary with all
236329 # quality gate jobs. Eliminates 3× redundant cargo builds.
237330 gate-build :
238331 name : Build
239- needs : check
240- if : needs.check.outputs.should-release == 'true'
332+ needs :
333+ - check
334+ - release-qualification
335+ if : needs.check.outputs.should-release == 'true' && needs.check.outputs.recovery-release != 'true' && needs.release-qualification.outputs.proof-status != 'success'
241336 # This binary is executed by the ubuntu-22.04 publication jobs. Build on
242337 # the oldest consumer runtime so recovery finalizers cannot require a newer GLIBC.
243338 runs-on : ubuntu-22.04
@@ -280,8 +375,9 @@ jobs:
280375 name : Audit
281376 needs :
282377 - check
378+ - release-qualification
283379 - gate-build
284- if : needs.check.outputs.should-release == 'true' && needs.check.outputs.recovery-release != 'true'
380+ if : needs.check.outputs.should-release == 'true' && needs.check.outputs.recovery-release != 'true' && needs.release-qualification.outputs.proof-status != 'success'
285381 runs-on : ubuntu-latest
286382 outputs :
287383 audit-result : ${{ steps.audit.outcome }}
@@ -329,8 +425,9 @@ jobs:
329425 name : Lint
330426 needs :
331427 - check
428+ - release-qualification
332429 - gate-build
333- if : needs.check.outputs.should-release == 'true' && needs.check.outputs.recovery-release != 'true'
430+ if : needs.check.outputs.should-release == 'true' && needs.check.outputs.recovery-release != 'true' && needs.release-qualification.outputs.proof-status != 'success'
334431 runs-on : ubuntu-latest
335432 steps :
336433 - uses : actions/checkout@v4
@@ -373,8 +470,9 @@ jobs:
373470 name : Test
374471 needs :
375472 - check
473+ - release-qualification
376474 - gate-build
377- if : needs.check.outputs.should-release == 'true' && needs.check.outputs.recovery-release != 'true'
475+ if : needs.check.outputs.should-release == 'true' && needs.check.outputs.recovery-release != 'true' && needs.release-qualification.outputs.proof-status != 'success'
378476 runs-on : ubuntu-latest
379477 steps :
380478 - uses : actions/checkout@v4
@@ -430,11 +528,12 @@ jobs:
430528 name : Release Quality Policy
431529 needs :
432530 - check
531+ - release-qualification
433532 - gate-build
434533 - gate-audit
435534 - gate-lint
436535 - gate-test
437- if : ${{ always() && needs.check.outputs.should-release == 'true' && needs.check.outputs.recovery-release != 'true' && needs.gate-build .result == 'success' }}
536+ if : ${{ always() && needs.check.outputs.should-release == 'true' && needs.check.outputs.recovery-release != 'true' && needs.release-qualification .result == 'success' }}
438537 runs-on : ubuntu-latest
439538 steps :
440539 - name : Checkout workflow event commit
@@ -445,20 +544,26 @@ jobs:
445544 - name : Enforce release-blocking commands
446545 env :
447546 BLOCKING_COMMANDS : ${{ env.RELEASE_BLOCKING_COMMANDS }}
547+ QUALIFICATION_RESULT : ${{ needs.release-qualification.outputs.proof-status }}
448548 AUDIT_RESULT : ${{ needs.gate-audit.outputs.audit-result || needs.gate-audit.result }}
449549 LINT_RESULT : ${{ needs.gate-lint.result }}
450550 TEST_RESULT : ${{ needs.gate-test.result }}
451551 run : |
552+ if [ "$QUALIFICATION_RESULT" != "success" ]; then
553+ echo "::error::Exact Main Guard qualification finished with result: $QUALIFICATION_RESULT"
554+ exit 1
555+ fi
452556 bash .github/release-quality-policy.sh
453557
454558 # ── Step 4: Version bump + changelog + tag ──
455559 prepare :
456560 name : Prepare Release
457561 needs :
458562 - check
563+ - release-qualification
459564 - gate-build
460565 - release-quality-policy
461- if : ${{ always() && needs.check.outputs.should-release == 'true' && needs.gate-build.result == 'success' && (needs.check.outputs.recovery-release == 'true' || inputs.release_tag != '' || needs.release-quality-policy.result == 'success') }}
566+ if : ${{ always() && needs.check.outputs.should-release == 'true' && ( needs.release-qualification.outputs.proof-status == 'success' || needs.check.outputs.recovery-release == 'true' || inputs.release_tag != '') && (needs.check.outputs.recovery-release == 'true' || inputs.release_tag != '' || needs.release-quality-policy.result == 'success') }}
462567 runs-on : ubuntu-latest
463568 outputs :
464569 release-version : ${{ steps.outputs.outputs['release-version'] }}
0 commit comments