Skip to content

fix(workflows): pr-labels-ci.yml bidirectional CI ↔ label transitions (#378)#410

Merged
cmeans-claude-dev[bot] merged 1 commit into
mainfrom
fix/pr-labels-ci-bidirectional-transitions
Apr 28, 2026
Merged

fix(workflows): pr-labels-ci.yml bidirectional CI ↔ label transitions (#378)#410
cmeans-claude-dev[bot] merged 1 commit into
mainfrom
fix/pr-labels-ci-bidirectional-transitions

Conversation

@cmeans-claude-dev
Copy link
Copy Markdown
Contributor

@cmeans-claude-dev cmeans-claude-dev Bot commented Apr 28, 2026

Summary

Closes #378. Two stale-label traps in pr-labels-ci.yml fixed symmetrically; both rooted in narrow outer guards that only fired on Awaiting CI, missing the post-CI Failed recovery arc and the Ready for QA → CI Failed regression arc.

Job Today Now
on-ci-pass Promotes only when Awaiting CI is present Promotes when Awaiting CI OR CI Failed is present
on-ci-fail Adds CI Failed only when Awaiting CI is present Adds CI Failed when Awaiting CI OR Ready for QA is present

Bug 1 — CI Failed → CI pass silently no-ops (issue #378)

Reproduction trail in #377 (2026-04-22): a lint-failing push moved labels to CI Failed; the fix-up push made CI go green; on-ci-pass fired and ran, but its outer if echo "$LABELS" | grep -q "^Awaiting CI$" was false (only CI Failed was present), so it silently no-op'd. PR sat at CI Failed while CI was actually green. Required a manual gh pr edit --remove-label "CI Failed" --add-label "Ready for QA" to unstick.

Bug 2 — Ready for QA → CI re-fail keeps the green label (symmetric)

Mirror trap on on-ci-fail: a CI re-run on a PR sitting at Ready for QA (e.g., manual re-trigger after a flake, or a workflow change forcing a re-run) that turns red leaves the PR labelled Ready for QA because the outer if echo "$LABELS" | grep -q "^Awaiting CI$" is false. The status check goes red but the label still says ready — QA might pick it up assuming CI is green.

Review-state preservation

Broadening the triggers introduces a new risk: if a QA Active / Ready for QA Signoff / QA Approved label coexists with a CI label (race, or manual mistake), the broader trigger could overwrite review-machine state with Ready for QA (on pass) or CI Failed (on fail). To prevent that, both jobs now short-circuit explicitly when any of those three labels is present:

for QA_STATE in "QA Active" "Ready for QA Signoff" "QA Approved"; do
  if echo "$LABELS" | grep -q "^$QA_STATE$"; then
    echo "$QA_STATE present — skipping (review in progress)"
    exit 0
  fi
done

Rationale: review state advances independently of CI re-runs. A passing or failing CI re-run on a PR that's already in QA review is visible via the check itself; the label transition would be redundant on success and destructive on failure. Dev Active short-circuit preserved unchanged.

Safety

State-machine trace (full)

Pre-state → CI conclusion → resulting transition (✓ = covered, ✗ = no-op, * = new):

Pre-state CI = success CI = failure
Awaiting CI Ready for QA CI Failed
CI Failed Ready for QA ✓* stays CI Failed
Ready for QA stays Ready for QA CI Failed ✓*
Dev Active no-op (skip) ✓ no-op (skip) ✓
QA Active no-op (skip) ✓* no-op (skip) ✓*
Ready for QA Signoff no-op (skip) ✓* no-op (skip) ✓*
QA Approved no-op (skip) ✓* no-op (skip) ✓*

The * entries are new in this PR. The Dev Active and "no pre-state" cases were already correct.

Test plan

Workflow YAML only. No tests to add.

QA

Prerequisites

  • None — pure workflow YAML change.

Manual tests

    • Workflow YAML parses cleanly. Confirm the Actions tab on this PR shows no parse-error annotations on pr-labels-ci.yml.
    • Diff matches the state-machine trace table above. Read .github/workflows/pr-labels-ci.yml head-to-toe; for each row of the trace, confirm the corresponding code path emits the expected transition (or skip).
    • Verification of the bug-fix itself is post-merge. workflow_run triggers always run from the default branch (per the LIMITATION comment at the top of pr-labels-ci.yml), so this PR's changes do not run on this PR. The natural validation is the next CI-fail-then-pass PR after this lands — when that happens, the PR should auto-promote CI Failed → Ready for QA without manual intervention. Reviewer should add a follow-up note here (or in the awareness milestone for this PR) once that natural validation occurs.

Out-of-scope follow-ups (not for this PR)

  • The dismiss_stale_reviews_on_push setting interacts with these transitions in subtle ways (review approvals get auto-dismissed on push, then CI re-runs). No change proposed; just flagging for awareness.
  • A future enhancement could add a QA Invalidated style label for the case where CI re-fails on a PR in QA review, but doing so requires designing the QA recovery path. Out of scope for pr-labels-ci.yml: handle CI Failed → CI pass transition (stale label trap) #378.

Closes the stale-label trap where a PR sitting at `CI Failed` whose
follow-up push made CI go green silently no-op'd and stayed at
`CI Failed`. Root cause: `on-ci-pass`'s outer guard
`if echo "$LABELS" | grep -q "^Awaiting CI$"` only fired on
`Awaiting CI`, so the `CI Failed → Ready for QA` arc was missing.

Symmetric fix on `on-ci-fail`: a PR at `Ready for QA` whose CI was
re-run (manual re-trigger after a flake) and turned red kept the green
label, hiding the regression from QA. `on-ci-fail` now also fires when
`Ready for QA` is the current label, removing it and applying
`CI Failed`.

Both jobs gain an explicit short-circuit on the in-flight review labels
`QA Active` / `Ready for QA Signoff` / `QA Approved` so the broadened
triggers do not disrupt review-machine state — review advances
independently of CI re-runs, and a re-run's success or failure on those
states is visible via the check itself rather than a label transition.
`Dev Active` short-circuit preserved unchanged.

No new contributor-controlled inputs introduced. Label list still read
via `gh pr view --json labels` (repo-owned strings, not fork-controlled).
Anchored grep patterns (`^Label$`) stay anchored. Existing env-routing
of `HEAD_BRANCH` / `RUN_ID` / `PR` / `REPO` (hardened in #332/#333)
unchanged.

Closes #378.

Verification is intrinsically post-merge — `workflow_run` triggers
always run from the default branch, so changes to `pr-labels-ci.yml`
cannot be exercised on the PR that introduces them.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@cmeans-claude-dev cmeans-claude-dev Bot requested a review from cmeans as a code owner April 28, 2026 19:56
@cmeans-claude-dev cmeans-claude-dev Bot added the Dev Active Developer is actively working on this PR; QA should not start label Apr 28, 2026
@github-actions github-actions Bot added the Awaiting CI Dev complete, waiting for CI/Codecov to pass before QA label Apr 28, 2026
@cmeans-claude-dev cmeans-claude-dev Bot removed the Dev Active Developer is actively working on this PR; QA should not start label Apr 28, 2026
@github-actions github-actions Bot added Ready for QA Dev work complete — QA can begin review and removed Awaiting CI Dev complete, waiting for CI/Codecov to pass before QA labels Apr 28, 2026
@codecov
Copy link
Copy Markdown

codecov Bot commented Apr 28, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

Copy link
Copy Markdown
Owner

@cmeans cmeans left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@cmeans cmeans added the QA Active QA is actively reviewing; Dev should not push changes label Apr 28, 2026
@github-actions github-actions Bot removed the Ready for QA Dev work complete — QA can begin review label Apr 28, 2026
Copy link
Copy Markdown
Owner

@cmeans cmeans left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[QA] Round 1 — Ready for QA Signoff. Zero findings.

Workflow-YAML-only change closing #378. 2 files / +55/-9. All 17 status checks green.

State-machine verification

I walked the pr-labels-ci.yml head-to-toe against the PR-body trace table — every row is correct. Summary:

Pre-state CI=success CI=failure
Awaiting CI → Ready for QA (REMOVE="Awaiting CI") → CI Failed (REMOVE="Awaiting CI")
CI Failed → Ready for QA (REMOVE="CI Failed") NEW — closes #378 stays CI Failed (REMOVE empty, no-op)
Ready for QA stays Ready for QA (REMOVE empty, no-op) → CI Failed (REMOVE="Ready for QA") NEW — symmetric fix
Awaiting CI + CI Failed → Ready for QA (REMOVE="Awaiting CI,CI Failed") (n/a)
Awaiting CI + Ready for QA (n/a) → CI Failed (REMOVE="Awaiting CI,Ready for QA")
Dev Active exits at first guard exits at first guard
QA Active / Ready for QA Signoff / QA Approved exits at new for-loop exits at new for-loop
no labels / unrelated only REMOVE empty → no-op REMOVE empty → no-op

The if [ -n "$REMOVE" ] wrapper correctly ties the remove + add together — no spurious Ready for QA/CI Failed adds when there's nothing to transition from.

Other checks

Area Verdict
Anchored grep (^Label$) preserved everywhere
Env-routing of HEAD_BRANCH / RUN_ID / PR / REPO unchanged
No new contributor-controlled inputs interpolated into shell
Workflow trigger remains workflow_run (base-branch context)
Dev Active short-circuit preserved unchanged
CHANGELOG order: Changed → Fixed → Security (KaC v1.1.0)
Closes #378 linked correctly in both PR body and CHANGELOG entry
Idempotency on already-present Ready for QA (when Awaiting CI + Ready for QA co-exist on success) ✓ — gh pr edit --add-label is idempotent

Manual tests

  1. Workflow YAML parses cleanly. No parse-error annotations on pr-labels-ci.yml. ✓
  2. Diff matches state-machine trace. Verified end-to-end above. ✓
  3. #409 migration live-validation — verified directly on this PR's status rollup:
    • pr-labels.yml on-push SUCCESS at 19:56 (run 25074527724) — Awaiting CI auto-applied to this PR on opening, no manual intervention.
    • pr-labels.yml on-label SUCCESS — fired when Dev Active removed.
    • pr-labels.yml on-unlabel SUCCESS at 20:01 — fired in this current label transition cycle.
    • qa-gate.yml qa-approved SUCCESS at 19:56 (3 invocations as labels evolved); QA Gate status context = PENDING (correct — current label is Ready for QA, not QA Approved).
    • These end-to-end exercise the pull_request_target triggers from main, confirming #409's migration works on a non-bootstrap PR. ✓
  4. Post-merge bug-fix validation — deferred. workflow_run always runs from default branch, so this PR's changes cannot exercise themselves. Natural validation is the next CI-fail-then-pass cycle after merge.

Applying Ready for QA Signoff as the final act.

@cmeans
Copy link
Copy Markdown
Owner

cmeans commented Apr 28, 2026

[QA] Applying Ready for QA Signoff — all 17 checks pass, zero findings on Round 1. State-machine trace walked end-to-end against pr-labels-ci.yml; all rows match. Bonus: #409's pull_request_target migration is visibly working on this PR (pr-labels.yml auto-applied Awaiting CI on open, qa-gate.yml posted QA Gate status), satisfying the deferred test plan #4 from #409. Post-merge bug-fix validation (test plan #4 here) will land naturally on the next CI-fail-then-pass PR — workflow_run triggers always run from default branch.

@cmeans cmeans added Ready for QA Signoff QA passed — ready for maintainer final review and merge QA Approved Manual QA testing completed and passed and removed QA Active QA is actively reviewing; Dev should not push changes Ready for QA Signoff QA passed — ready for maintainer final review and merge labels Apr 28, 2026
@cmeans-claude-dev cmeans-claude-dev Bot merged commit d74c684 into main Apr 28, 2026
50 checks passed
@cmeans-claude-dev cmeans-claude-dev Bot deleted the fix/pr-labels-ci-bidirectional-transitions branch April 28, 2026 20:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

QA Approved Manual QA testing completed and passed

Projects

None yet

Development

Successfully merging this pull request may close these issues.

pr-labels-ci.yml: handle CI Failed → CI pass transition (stale label trap)

1 participant