From 64c94fbe1d7a5d783bef98b65fd474dc939b479c Mon Sep 17 00:00:00 2001 From: "cmeans-claude-dev[bot]" <3223881+cmeans-claude-dev[bot]@users.noreply.github.com> Date: Mon, 30 Mar 2026 11:40:44 -0500 Subject: [PATCH 1/9] fix: split PR label automation to eliminate workflow_run failures Split the workflow_run trigger (CI-completion promotion) into its own file (pr-labels-ci.yml), separate from the pull_request-triggered jobs (pr-labels.yml). This fixes two issues: 1. workflow_run fires on every CI completion including merges to main, where there is no associated PR. The API call to look up the PR returned a 404 whose body leaked into the $PR variable, causing "invalid qualified head ref" errors and failure notification emails. 2. Having both triggers in one file meant every event spawned all jobs, with most being skipped. Splitting reduces Actions UI clutter. Fixes applied: - Added head_branch != default_branch filter to skip main-branch CI runs - Fixed 404 body leak: capture API output only on success - Removed redundant github.event_name checks from pr-labels.yml (now single-trigger, so event_name is always pull_request) - Simplified if conditions in on-push, on-unlabel, on-label jobs Co-Authored-By: Claude Opus 4.6 (1M context) --- .github/workflows/pr-labels-ci.yml | 88 ++++++++++++++++++++++++++++++ .github/workflows/pr-labels.yml | 69 ++--------------------- 2 files changed, 93 insertions(+), 64 deletions(-) create mode 100644 .github/workflows/pr-labels-ci.yml diff --git a/.github/workflows/pr-labels-ci.yml b/.github/workflows/pr-labels-ci.yml new file mode 100644 index 0000000..634881e --- /dev/null +++ b/.github/workflows/pr-labels-ci.yml @@ -0,0 +1,88 @@ +# mcp-awareness — ambient system awareness for AI agents +# Copyright (C) 2026 Chris Means +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +name: PR Label Automation (CI) + +# Promotes PRs from Awaiting CI to Ready for QA when CI passes. +# Split from pr-labels.yml because workflow_run fires on every CI +# completion (including merges to main), which caused noisy failures +# when the job couldn't find an associated PR. +# +# NOTE: References the CI workflow by name ("CI"). +# If the CI workflow in ci.yml is ever renamed, update the name here too. +on: + workflow_run: + workflows: [CI] + types: [completed] + +permissions: + pull-requests: write + checks: read + +jobs: + on-ci-pass: + # Only run for successful CI on PR branches (not merges to main). + if: >- + github.event.workflow_run.conclusion == 'success' + && github.event.workflow_run.event == 'pull_request' + && github.event.workflow_run.head_branch != github.event.repository.default_branch + runs-on: ubuntu-latest + steps: + - name: Promote to Ready for QA + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + REPO=${{ github.repository }} + + # Get the PR number from the workflow run. + # This can 404 after a force-push (the old commit's run is orphaned). + # Capture exit code separately so a 404 body doesn't leak into $PR. + PR="" + API_OUT=$(gh api repos/$REPO/actions/runs/${{ github.event.workflow_run.id }}/pull_requests \ + --jq '.[0].number // empty' 2>&1) && PR="$API_OUT" || true + + # Fallback: pull_requests array is empty for dependabot and other + # non-default-branch PRs. Search by head branch instead. + if [ -z "$PR" ]; then + HEAD_BRANCH=${{ github.event.workflow_run.head_branch }} + PR=$(gh pr list --repo "$REPO" --head "$HEAD_BRANCH" --state open \ + --json number --jq '.[0].number // empty' 2>/dev/null) || true + fi + + if [ -z "$PR" ]; then + echo "No PR associated with this workflow run (may be a fork PR or force-pushed away)" + exit 0 + fi + + LABELS=$(gh pr view "$PR" --repo "$REPO" --json labels --jq '.labels[].name') + + # Don't promote if Dev Active is present — dev isn't done yet. + # When they remove Dev Active, on-unlabel will check CI and promote. + if echo "$LABELS" | grep -q "^Dev Active$"; then + echo "Dev Active present — skipping promotion (dev still working)" + exit 0 + fi + + # Only promote if Awaiting CI is present + if echo "$LABELS" | grep -q "^Awaiting CI$"; then + # Also clean up QA Invalidated if it lingered from a previous push + REMOVE="Awaiting CI" + if echo "$LABELS" | grep -q "^QA Invalidated$"; then + REMOVE="Awaiting CI,QA Invalidated" + fi + gh pr edit "$PR" --repo "$REPO" --remove-label "$REMOVE" + gh pr edit "$PR" --repo "$REPO" --add-label "Ready for QA" + fi diff --git a/.github/workflows/pr-labels.yml b/.github/workflows/pr-labels.yml index cca90c3..1b25f1a 100644 --- a/.github/workflows/pr-labels.yml +++ b/.github/workflows/pr-labels.yml @@ -16,15 +16,12 @@ name: PR Label Automation -# NOTE: The workflow_run trigger references the CI workflow by name ("CI"). -# If the CI workflow in ci.yml is ever renamed, update the name here too. +# Handles label transitions triggered by PR events (push, label, unlabel). +# CI-completion promotion lives in pr-labels-ci.yml (separate workflow_run trigger). on: pull_request: branches: [main] types: [synchronize, labeled, unlabeled] - workflow_run: - workflows: [CI] - types: [completed] permissions: pull-requests: write @@ -34,7 +31,7 @@ jobs: # When new commits are pushed to a PR, reset to Awaiting CI. # Removes any QA/review labels that are now stale. on-push: - if: github.event_name == 'pull_request' && github.event.action == 'synchronize' + if: github.event.action == 'synchronize' runs-on: ubuntu-latest steps: - name: Reset labels on new push @@ -81,67 +78,12 @@ jobs: --body "New commits pushed while QA was active. QA review invalidated — resetting to Awaiting CI." fi - # When CI completes successfully, move from Awaiting CI to Ready for QA. - # Only works for same-origin PRs — fork PRs must be promoted manually - # because the workflow_run pull_requests array is empty for forks. - on-ci-pass: - if: >- - github.event_name == 'workflow_run' - && github.event.workflow_run.conclusion == 'success' - && github.event.workflow_run.event == 'pull_request' - runs-on: ubuntu-latest - steps: - - name: Promote to Ready for QA - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - REPO=${{ github.repository }} - - # Get the PR number from the workflow run. - # This can 404 after a force-push (the old commit's run is orphaned). - PR=$(gh api repos/$REPO/actions/runs/${{ github.event.workflow_run.id }}/pull_requests \ - --jq '.[0].number // empty' 2>/dev/null) || true - - # Fallback: pull_requests array is empty for dependabot and other - # non-default-branch PRs. Search by head branch instead. - if [ -z "$PR" ]; then - HEAD_BRANCH=${{ github.event.workflow_run.head_branch }} - PR=$(gh pr list --repo "$REPO" --head "$HEAD_BRANCH" --state open \ - --json number --jq '.[0].number // empty' 2>/dev/null) || true - fi - - if [ -z "$PR" ]; then - echo "No PR associated with this workflow run (may be a fork PR or force-pushed away)" - exit 0 - fi - - LABELS=$(gh pr view "$PR" --repo "$REPO" --json labels --jq '.labels[].name') - - # Don't promote if Dev Active is present — dev isn't done yet. - # When they remove Dev Active, on-unlabel will check CI and promote. - if echo "$LABELS" | grep -q "^Dev Active$"; then - echo "Dev Active present — skipping promotion (dev still working)" - exit 0 - fi - - # Only promote if Awaiting CI is present - if echo "$LABELS" | grep -q "^Awaiting CI$"; then - # Also clean up QA Invalidated if it lingered from a previous push - REMOVE="Awaiting CI" - if echo "$LABELS" | grep -q "^QA Invalidated$"; then - REMOVE="Awaiting CI,QA Invalidated" - fi - gh pr edit "$PR" --repo "$REPO" --remove-label "$REMOVE" - gh pr edit "$PR" --repo "$REPO" --add-label "Ready for QA" - fi - # When Dev Active is removed, transition to Awaiting CI — or straight # to Ready for QA if CI already passed (avoids stuck state when CI # finishes before the label is removed). on-unlabel: if: >- - github.event_name == 'pull_request' - && github.event.action == 'unlabeled' + github.event.action == 'unlabeled' && github.event.label.name == 'Dev Active' runs-on: ubuntu-latest steps: @@ -183,8 +125,7 @@ jobs: # to avoid wasted runner invocations. on-label: if: >- - github.event_name == 'pull_request' - && github.event.action == 'labeled' + github.event.action == 'labeled' && contains(fromJSON('["QA Active","Dev Active","Ready for QA Signoff","QA Failed","QA Approved"]'), github.event.label.name) runs-on: ubuntu-latest steps: From 5d35059f27a4bae4bd622e038191b1412b45a3c8 Mon Sep 17 00:00:00 2001 From: "cmeans-claude-dev[bot]" <3223881+cmeans-claude-dev[bot]@users.noreply.github.com> Date: Mon, 30 Mar 2026 11:45:36 -0500 Subject: [PATCH 2/9] fix: add 'opened' event type so new PRs get Awaiting CI label Without this, brand-new PRs never received the Awaiting CI label because only 'synchronize' (subsequent pushes) was in the trigger list. The on-push job now fires on both 'opened' and 'synchronize'. Co-Authored-By: Claude Opus 4.6 (1M context) --- .github/workflows/pr-labels.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/pr-labels.yml b/.github/workflows/pr-labels.yml index 1b25f1a..d64724e 100644 --- a/.github/workflows/pr-labels.yml +++ b/.github/workflows/pr-labels.yml @@ -21,7 +21,7 @@ name: PR Label Automation on: pull_request: branches: [main] - types: [synchronize, labeled, unlabeled] + types: [opened, synchronize, labeled, unlabeled] permissions: pull-requests: write @@ -31,7 +31,7 @@ jobs: # When new commits are pushed to a PR, reset to Awaiting CI. # Removes any QA/review labels that are now stale. on-push: - if: github.event.action == 'synchronize' + if: github.event.action == 'opened' || github.event.action == 'synchronize' runs-on: ubuntu-latest steps: - name: Reset labels on new push From f90f296a87900b9385ce7b8c9c302c2ab511def3 Mon Sep 17 00:00:00 2001 From: "cmeans-claude-dev[bot]" <3223881+cmeans-claude-dev[bot]@users.noreply.github.com> Date: Mon, 30 Mar 2026 11:49:13 -0500 Subject: [PATCH 3/9] =?UTF-8?q?fix:=20auto-transition=20Dev=20Active=20?= =?UTF-8?q?=E2=86=92=20Awaiting=20CI=20on=20push,=20restore=20on=20CI=20fa?= =?UTF-8?q?il?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Push means development is done — you're waiting, not actively developing. The previous flow kept Dev Active through pushes, creating a dead end where Awaiting CI was never added and on-ci-pass couldn't promote. New flow: - Push → on-push removes Dev Active, adds Awaiting CI - CI passes → on-ci-pass promotes to Ready for QA - CI fails → on-ci-fail restores Dev Active (dev needs to fix) Co-Authored-By: Claude Opus 4.6 (1M context) --- .github/workflows/pr-labels-ci.yml | 41 ++++++++++++++++++++++++++++-- .github/workflows/pr-labels.yml | 12 +++++---- 2 files changed, 46 insertions(+), 7 deletions(-) diff --git a/.github/workflows/pr-labels-ci.yml b/.github/workflows/pr-labels-ci.yml index 634881e..e69f56e 100644 --- a/.github/workflows/pr-labels-ci.yml +++ b/.github/workflows/pr-labels-ci.yml @@ -16,7 +16,7 @@ name: PR Label Automation (CI) -# Promotes PRs from Awaiting CI to Ready for QA when CI passes. +# Handles label transitions when CI completes (pass or fail). # Split from pr-labels.yml because workflow_run fires on every CI # completion (including merges to main), which caused noisy failures # when the job couldn't find an associated PR. @@ -33,8 +33,8 @@ permissions: checks: read jobs: + # When CI passes, promote from Awaiting CI to Ready for QA. on-ci-pass: - # Only run for successful CI on PR branches (not merges to main). if: >- github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.event == 'pull_request' @@ -86,3 +86,40 @@ jobs: gh pr edit "$PR" --repo "$REPO" --remove-label "$REMOVE" gh pr edit "$PR" --repo "$REPO" --add-label "Ready for QA" fi + + # When CI fails, move from Awaiting CI back to Dev Active. + on-ci-fail: + if: >- + github.event.workflow_run.conclusion == 'failure' + && github.event.workflow_run.event == 'pull_request' + && github.event.workflow_run.head_branch != github.event.repository.default_branch + runs-on: ubuntu-latest + steps: + - name: Restore Dev Active on CI failure + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + REPO=${{ github.repository }} + + PR="" + API_OUT=$(gh api repos/$REPO/actions/runs/${{ github.event.workflow_run.id }}/pull_requests \ + --jq '.[0].number // empty' 2>&1) && PR="$API_OUT" || true + + if [ -z "$PR" ]; then + HEAD_BRANCH=${{ github.event.workflow_run.head_branch }} + PR=$(gh pr list --repo "$REPO" --head "$HEAD_BRANCH" --state open \ + --json number --jq '.[0].number // empty' 2>/dev/null) || true + fi + + if [ -z "$PR" ]; then + echo "No PR associated with this workflow run" + exit 0 + fi + + LABELS=$(gh pr view "$PR" --repo "$REPO" --json labels --jq '.labels[].name') + + # Only restore Dev Active if Awaiting CI is present (normal flow) + if echo "$LABELS" | grep -q "^Awaiting CI$"; then + gh pr edit "$PR" --repo "$REPO" --remove-label "Awaiting CI" + gh pr edit "$PR" --repo "$REPO" --add-label "Dev Active" + fi diff --git a/.github/workflows/pr-labels.yml b/.github/workflows/pr-labels.yml index d64724e..fce489f 100644 --- a/.github/workflows/pr-labels.yml +++ b/.github/workflows/pr-labels.yml @@ -62,12 +62,14 @@ jobs: gh pr edit "$PR" --repo "$REPO" --remove-label "$REMOVE" fi - # Only add Awaiting CI if Dev Active is NOT present. - # If Dev Active is on, the dev isn't done yet — on-unlabel will - # handle the transition when they remove it. + # Remove Dev Active if present — pushing means dev work is done, + # now waiting for CI. Add to the removal batch above if needed. if echo "$PR_LABELS" | grep -q '"Dev Active"'; then - echo "Dev Active present — skipping Awaiting CI (dev still working)" - elif ! echo "$PR_LABELS" | grep -q '"Awaiting CI"'; then + gh pr edit "$PR" --repo "$REPO" --remove-label "Dev Active" + fi + + # Always add Awaiting CI (unless already present) + if ! echo "$PR_LABELS" | grep -q '"Awaiting CI"'; then gh pr edit "$PR" --repo "$REPO" --add-label "Awaiting CI" fi From b1d238cef4f0ab7d6eb8a3378de110361d22f0c9 Mon Sep 17 00:00:00 2001 From: "cmeans-claude-dev[bot]" <3223881+cmeans-claude-dev[bot]@users.noreply.github.com> Date: Mon, 30 Mar 2026 11:53:20 -0500 Subject: [PATCH 4/9] =?UTF-8?q?fix:=20add=20Ready=20for=20QA=20=E2=86=92?= =?UTF-8?q?=20removes=20Awaiting=20CI=20cleanup=20rule?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When Ready for QA is added (by on-ci-pass or manually), Awaiting CI should be removed automatically. Without this, both labels can coexist if the removal in on-ci-pass races or if a previous workflow version handled the transition. Co-Authored-By: Claude Opus 4.6 (1M context) --- .github/workflows/pr-labels.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/pr-labels.yml b/.github/workflows/pr-labels.yml index fce489f..ece2031 100644 --- a/.github/workflows/pr-labels.yml +++ b/.github/workflows/pr-labels.yml @@ -128,7 +128,7 @@ jobs: on-label: if: >- github.event.action == 'labeled' - && contains(fromJSON('["QA Active","Dev Active","Ready for QA Signoff","QA Failed","QA Approved"]'), github.event.label.name) + && contains(fromJSON('["Ready for QA","QA Active","Dev Active","Ready for QA Signoff","QA Failed","QA Approved"]'), github.event.label.name) runs-on: ubuntu-latest steps: - name: Clean up stale labels @@ -147,6 +147,9 @@ jobs: } case "$ADDED_LABEL" in + "Ready for QA") + remove_if_present "Awaiting CI" + ;; "QA Active") remove_if_present "Ready for QA" ;; From 25cbbaeb49bddce8bdc5d2249137beb6e8c477a3 Mon Sep 17 00:00:00 2001 From: "cmeans-claude-dev[bot]" <3223881+cmeans-claude-dev[bot]@users.noreply.github.com> Date: Mon, 30 Mar 2026 12:20:02 -0500 Subject: [PATCH 5/9] refactor: rewrite pr-labels.yml as clean state machine Complete rewrite based on the state machine spec. Key changes: - Dev Active coexists with Awaiting CI (no longer blocks it) - on-push keeps Dev Active, removes all other workflow labels - on-unlabel checks CI status and promotes if already passed - on-label cleanup includes Ready for QA removing Awaiting CI + CI Failed - QA Invalidated label removed entirely (comment is sufficient) - CI Failed added to cleanup rules for Dev Active and Ready for QA Co-Authored-By: Claude Opus 4.6 (1M context) --- .github/workflows/pr-labels.yml | 43 +++++++++++++++------------------ 1 file changed, 19 insertions(+), 24 deletions(-) diff --git a/.github/workflows/pr-labels.yml b/.github/workflows/pr-labels.yml index ece2031..f3afc5d 100644 --- a/.github/workflows/pr-labels.yml +++ b/.github/workflows/pr-labels.yml @@ -28,8 +28,9 @@ permissions: checks: read jobs: - # When new commits are pushed to a PR, reset to Awaiting CI. - # Removes any QA/review labels that are now stale. + # When a PR is opened or new commits are pushed, reset to Awaiting CI. + # Removes stale QA/workflow labels. Keeps Dev Active if present (coexists + # with Awaiting CI — dev is still working, CI runs but won't promote). on-push: if: github.event.action == 'opened' || github.event.action == 'synchronize' runs-on: ubuntu-latest @@ -47,9 +48,9 @@ jobs: HAD_QA_ACTIVE=true fi - # Remove stale workflow labels (coalesced where possible) + # Remove stale workflow labels (coalesced into one API call) REMOVE="" - for LABEL in "Ready for QA" "Ready for QA Signoff" "QA Approved" "QA Active" "QA Failed" "QA Invalidated"; do + for LABEL in "Ready for QA" "Ready for QA Signoff" "QA Approved" "QA Active" "QA Failed" "CI Failed"; do if echo "$PR_LABELS" | grep -q "\"$LABEL\""; then if [ -n "$REMOVE" ]; then REMOVE="$REMOVE,$LABEL" @@ -62,27 +63,21 @@ jobs: gh pr edit "$PR" --repo "$REPO" --remove-label "$REMOVE" fi - # Remove Dev Active if present — pushing means dev work is done, - # now waiting for CI. Add to the removal batch above if needed. - if echo "$PR_LABELS" | grep -q '"Dev Active"'; then - gh pr edit "$PR" --repo "$REPO" --remove-label "Dev Active" - fi - - # Always add Awaiting CI (unless already present) + # Add Awaiting CI (unless already present). + # Dev Active is NOT removed — it coexists with Awaiting CI. if ! echo "$PR_LABELS" | grep -q '"Awaiting CI"'; then gh pr edit "$PR" --repo "$REPO" --add-label "Awaiting CI" fi - # If QA was actively reviewing, flag the invalidation + # If QA was actively reviewing, notify via comment if [ "$HAD_QA_ACTIVE" = true ]; then - gh pr edit "$PR" --repo "$REPO" --add-label "QA Invalidated" gh pr comment "$PR" --repo "$REPO" \ --body "New commits pushed while QA was active. QA review invalidated — resetting to Awaiting CI." fi - # When Dev Active is removed, transition to Awaiting CI — or straight - # to Ready for QA if CI already passed (avoids stuck state when CI - # finishes before the label is removed). + # When Dev Active is removed manually, check if CI already passed. + # If yes, promote straight to Ready for QA. If no, Awaiting CI is + # already present and on-ci-pass will promote when CI completes. on-unlabel: if: >- github.event.action == 'unlabeled' @@ -97,34 +92,33 @@ jobs: PR=${{ github.event.pull_request.number }} REPO=${{ github.repository }} - # Check if CI already passed for the head commit (job-name-agnostic). + # Check if CI already passed for the head commit HEAD_SHA=${{ github.event.pull_request.head.sha }} CI_CONCLUSION=$(gh api "repos/$REPO/actions/workflows/ci.yml/runs?head_sha=$HEAD_SHA" \ --jq '.workflow_runs[0].conclusion // empty' 2>/dev/null) || true if [ "$CI_CONCLUSION" = "success" ]; then - # CI passed — go straight to Ready for QA + # CI passed — promote to Ready for QA REMOVE="" if echo "$PR_LABELS" | grep -q '"Awaiting CI"'; then REMOVE="Awaiting CI" fi - if echo "$PR_LABELS" | grep -q '"QA Invalidated"'; then - REMOVE="${REMOVE:+$REMOVE,}QA Invalidated" + if echo "$PR_LABELS" | grep -q '"CI Failed"'; then + REMOVE="${REMOVE:+$REMOVE,}CI Failed" fi if [ -n "$REMOVE" ]; then gh pr edit "$PR" --repo "$REPO" --remove-label "$REMOVE" fi gh pr edit "$PR" --repo "$REPO" --add-label "Ready for QA" else - # CI hasn't passed yet — ensure Awaiting CI is present + # CI hasn't passed — Awaiting CI should already be present if ! echo "$PR_LABELS" | grep -q '"Awaiting CI"'; then gh pr edit "$PR" --repo "$REPO" --add-label "Awaiting CI" fi fi # When a workflow label is added, clean up labels that no longer apply. - # Only runs for labels that have cleanup rules — skips irrelevant labels - # to avoid wasted runner invocations. + # Only runs for labels that have cleanup rules. on-label: if: >- github.event.action == 'labeled' @@ -149,13 +143,14 @@ jobs: case "$ADDED_LABEL" in "Ready for QA") remove_if_present "Awaiting CI" + remove_if_present "CI Failed" ;; "QA Active") remove_if_present "Ready for QA" ;; "Dev Active") remove_if_present "QA Failed" - remove_if_present "QA Invalidated" + remove_if_present "CI Failed" remove_if_present "Awaiting CI" remove_if_present "Ready for QA" ;; From 94b4a66cf3826126f44c640a3caf7cb9dbd8d377 Mon Sep 17 00:00:00 2001 From: "cmeans-claude-dev[bot]" <3223881+cmeans-claude-dev[bot]@users.noreply.github.com> Date: Mon, 30 Mar 2026 12:21:49 -0500 Subject: [PATCH 6/9] refactor: rewrite pr-labels-ci.yml with CI Failed state and safe PR lookup Complete rewrite based on the state machine spec. Key changes: - on-ci-fail adds CI Failed label (not Dev Active) when CI fails - Dev Active suppresses both pass and fail transitions - Safe 404 handling: API output captured only on success - head_branch filter skips CI runs on main (merge commits) - CI Failed cleaned up on next CI pass Co-Authored-By: Claude Opus 4.6 (1M context) --- .github/workflows/pr-labels-ci.yml | 55 ++++++++++++++++++------------ 1 file changed, 33 insertions(+), 22 deletions(-) diff --git a/.github/workflows/pr-labels-ci.yml b/.github/workflows/pr-labels-ci.yml index e69f56e..e1d2ee0 100644 --- a/.github/workflows/pr-labels-ci.yml +++ b/.github/workflows/pr-labels-ci.yml @@ -19,10 +19,14 @@ name: PR Label Automation (CI) # Handles label transitions when CI completes (pass or fail). # Split from pr-labels.yml because workflow_run fires on every CI # completion (including merges to main), which caused noisy failures -# when the job couldn't find an associated PR. +# when there was no associated PR. # # NOTE: References the CI workflow by name ("CI"). # If the CI workflow in ci.yml is ever renamed, update the name here too. +# +# LIMITATION: workflow_run triggers always run from the default branch +# (main), not the PR branch. Changes to this file cannot be tested +# on a PR — they take effect only after merge. on: workflow_run: workflows: [CI] @@ -33,7 +37,8 @@ permissions: checks: read jobs: - # When CI passes, promote from Awaiting CI to Ready for QA. + # When CI passes on a PR branch, promote from Awaiting CI to Ready for QA. + # Skips if Dev Active is present (dev isn't done yet). on-ci-pass: if: >- github.event.workflow_run.conclusion == 'success' @@ -47,15 +52,15 @@ jobs: run: | REPO=${{ github.repository }} - # Get the PR number from the workflow run. - # This can 404 after a force-push (the old commit's run is orphaned). - # Capture exit code separately so a 404 body doesn't leak into $PR. + # Look up the associated PR. + # The API can 404 after a force-push (orphaned run). + # Capture output only on success so 404 body doesn't leak into $PR. PR="" - API_OUT=$(gh api repos/$REPO/actions/runs/${{ github.event.workflow_run.id }}/pull_requests \ + API_OUT=$(gh api "repos/$REPO/actions/runs/${{ github.event.workflow_run.id }}/pull_requests" \ --jq '.[0].number // empty' 2>&1) && PR="$API_OUT" || true - # Fallback: pull_requests array is empty for dependabot and other - # non-default-branch PRs. Search by head branch instead. + # Fallback: pull_requests array is empty for dependabot PRs. + # Search by head branch instead. if [ -z "$PR" ]; then HEAD_BRANCH=${{ github.event.workflow_run.head_branch }} PR=$(gh pr list --repo "$REPO" --head "$HEAD_BRANCH" --state open \ @@ -63,31 +68,31 @@ jobs: fi if [ -z "$PR" ]; then - echo "No PR associated with this workflow run (may be a fork PR or force-pushed away)" + echo "No PR associated with this workflow run — exiting" exit 0 fi LABELS=$(gh pr view "$PR" --repo "$REPO" --json labels --jq '.labels[].name') - # Don't promote if Dev Active is present — dev isn't done yet. - # When they remove Dev Active, on-unlabel will check CI and promote. + # Dev Active means dev isn't done — don't promote. + # on-unlabel will handle promotion when Dev Active is removed. if echo "$LABELS" | grep -q "^Dev Active$"; then - echo "Dev Active present — skipping promotion (dev still working)" + echo "Dev Active present — skipping promotion" exit 0 fi - # Only promote if Awaiting CI is present + # Promote if Awaiting CI is present if echo "$LABELS" | grep -q "^Awaiting CI$"; then - # Also clean up QA Invalidated if it lingered from a previous push REMOVE="Awaiting CI" - if echo "$LABELS" | grep -q "^QA Invalidated$"; then - REMOVE="Awaiting CI,QA Invalidated" + if echo "$LABELS" | grep -q "^CI Failed$"; then + REMOVE="Awaiting CI,CI Failed" fi gh pr edit "$PR" --repo "$REPO" --remove-label "$REMOVE" gh pr edit "$PR" --repo "$REPO" --add-label "Ready for QA" fi - # When CI fails, move from Awaiting CI back to Dev Active. + # When CI fails on a PR branch, set CI Failed. + # Skips if Dev Active is present (dev is already working). on-ci-fail: if: >- github.event.workflow_run.conclusion == 'failure' @@ -95,14 +100,14 @@ jobs: && github.event.workflow_run.head_branch != github.event.repository.default_branch runs-on: ubuntu-latest steps: - - name: Restore Dev Active on CI failure + - name: Set CI Failed env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | REPO=${{ github.repository }} PR="" - API_OUT=$(gh api repos/$REPO/actions/runs/${{ github.event.workflow_run.id }}/pull_requests \ + API_OUT=$(gh api "repos/$REPO/actions/runs/${{ github.event.workflow_run.id }}/pull_requests" \ --jq '.[0].number // empty' 2>&1) && PR="$API_OUT" || true if [ -z "$PR" ]; then @@ -112,14 +117,20 @@ jobs: fi if [ -z "$PR" ]; then - echo "No PR associated with this workflow run" + echo "No PR associated with this workflow run — exiting" exit 0 fi LABELS=$(gh pr view "$PR" --repo "$REPO" --json labels --jq '.labels[].name') - # Only restore Dev Active if Awaiting CI is present (normal flow) + # Dev Active means dev is already working — don't add CI Failed + if echo "$LABELS" | grep -q "^Dev Active$"; then + echo "Dev Active present — skipping CI Failed" + exit 0 + fi + + # Set CI Failed if Awaiting CI is present (normal flow) if echo "$LABELS" | grep -q "^Awaiting CI$"; then gh pr edit "$PR" --repo "$REPO" --remove-label "Awaiting CI" - gh pr edit "$PR" --repo "$REPO" --add-label "Dev Active" + gh pr edit "$PR" --repo "$REPO" --add-label "CI Failed" fi From fc320c835c970032ca52b9d66766789b149143c9 Mon Sep 17 00:00:00 2001 From: "cmeans-claude-dev[bot]" <3223881+cmeans-claude-dev[bot]@users.noreply.github.com> Date: Mon, 30 Mar 2026 12:22:33 -0500 Subject: [PATCH 7/9] docs: add PR label state machine design spec and implementation plan Co-Authored-By: Claude Opus 4.6 (1M context) --- .../2026-03-30-pr-label-state-machine.md | 508 ++++++++++++++++++ ...026-03-30-pr-label-state-machine-design.md | 173 ++++++ 2 files changed, 681 insertions(+) create mode 100644 docs/superpowers/plans/2026-03-30-pr-label-state-machine.md create mode 100644 docs/superpowers/specs/2026-03-30-pr-label-state-machine-design.md diff --git a/docs/superpowers/plans/2026-03-30-pr-label-state-machine.md b/docs/superpowers/plans/2026-03-30-pr-label-state-machine.md new file mode 100644 index 0000000..8639760 --- /dev/null +++ b/docs/superpowers/plans/2026-03-30-pr-label-state-machine.md @@ -0,0 +1,508 @@ +# PR Label State Machine Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Rewrite the PR label automation as a clean state machine with proper transitions, replacing the current buggy incremental code on the `fix/pr-labels-workflow-split` branch. + +**Architecture:** Two workflow files split by trigger type: `pr-labels.yml` (pull_request events) and `pr-labels-ci.yml` (workflow_run events). A shared PR-lookup pattern in the CI file handles 404s safely. Label cleanup rules in on-label provide defense-in-depth for every transition. + +**Tech Stack:** GitHub Actions YAML, bash shell scripts, `gh` CLI + +**Spec:** `docs/superpowers/specs/2026-03-30-pr-label-state-machine-design.md` + +**Branch:** `fix/pr-labels-workflow-split` (already exists, will be rewritten) + +--- + +### Task 1: Rewrite pr-labels.yml (pull_request triggers) + +**Files:** +- Rewrite: `.github/workflows/pr-labels.yml` + +This file handles three jobs triggered by `pull_request` events: `on-push`, `on-unlabel`, and `on-label`. + +- [ ] **Step 1: Write the complete pr-labels.yml file** + +Replace the entire contents of `.github/workflows/pr-labels.yml` with: + +```yaml +# mcp-awareness — ambient system awareness for AI agents +# Copyright (C) 2026 Chris Means +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +name: PR Label Automation + +# Handles label transitions triggered by PR events (push, label, unlabel). +# CI-completion promotion lives in pr-labels-ci.yml (separate workflow_run trigger). +on: + pull_request: + branches: [main] + types: [opened, synchronize, labeled, unlabeled] + +permissions: + pull-requests: write + checks: read + +jobs: + # When a PR is opened or new commits are pushed, reset to Awaiting CI. + # Removes stale QA/workflow labels. Keeps Dev Active if present (coexists + # with Awaiting CI — dev is still working, CI runs but won't promote). + on-push: + if: github.event.action == 'opened' || github.event.action == 'synchronize' + runs-on: ubuntu-latest + steps: + - name: Reset labels on new push + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + PR_LABELS: ${{ toJSON(github.event.pull_request.labels.*.name) }} + run: | + PR=${{ github.event.pull_request.number }} + REPO=${{ github.repository }} + + HAD_QA_ACTIVE=false + if echo "$PR_LABELS" | grep -q '"QA Active"'; then + HAD_QA_ACTIVE=true + fi + + # Remove stale workflow labels (coalesced into one API call) + REMOVE="" + for LABEL in "Ready for QA" "Ready for QA Signoff" "QA Approved" "QA Active" "QA Failed" "CI Failed"; do + if echo "$PR_LABELS" | grep -q "\"$LABEL\""; then + if [ -n "$REMOVE" ]; then + REMOVE="$REMOVE,$LABEL" + else + REMOVE="$LABEL" + fi + fi + done + if [ -n "$REMOVE" ]; then + gh pr edit "$PR" --repo "$REPO" --remove-label "$REMOVE" + fi + + # Add Awaiting CI (unless already present). + # Dev Active is NOT removed — it coexists with Awaiting CI. + if ! echo "$PR_LABELS" | grep -q '"Awaiting CI"'; then + gh pr edit "$PR" --repo "$REPO" --add-label "Awaiting CI" + fi + + # If QA was actively reviewing, notify via comment + if [ "$HAD_QA_ACTIVE" = true ]; then + gh pr comment "$PR" --repo "$REPO" \ + --body "New commits pushed while QA was active. QA review invalidated — resetting to Awaiting CI." + fi + + # When Dev Active is removed manually, check if CI already passed. + # If yes, promote straight to Ready for QA. If no, Awaiting CI is + # already present and on-ci-pass will promote when CI completes. + on-unlabel: + if: >- + github.event.action == 'unlabeled' + && github.event.label.name == 'Dev Active' + runs-on: ubuntu-latest + steps: + - name: Transition after Dev Active removed + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + PR_LABELS: ${{ toJSON(github.event.pull_request.labels.*.name) }} + run: | + PR=${{ github.event.pull_request.number }} + REPO=${{ github.repository }} + + # Check if CI already passed for the head commit + HEAD_SHA=${{ github.event.pull_request.head.sha }} + CI_CONCLUSION=$(gh api "repos/$REPO/actions/workflows/ci.yml/runs?head_sha=$HEAD_SHA" \ + --jq '.workflow_runs[0].conclusion // empty' 2>/dev/null) || true + + if [ "$CI_CONCLUSION" = "success" ]; then + # CI passed — promote to Ready for QA + REMOVE="" + if echo "$PR_LABELS" | grep -q '"Awaiting CI"'; then + REMOVE="Awaiting CI" + fi + if echo "$PR_LABELS" | grep -q '"CI Failed"'; then + REMOVE="${REMOVE:+$REMOVE,}CI Failed" + fi + if [ -n "$REMOVE" ]; then + gh pr edit "$PR" --repo "$REPO" --remove-label "$REMOVE" + fi + gh pr edit "$PR" --repo "$REPO" --add-label "Ready for QA" + else + # CI hasn't passed — Awaiting CI should already be present + if ! echo "$PR_LABELS" | grep -q '"Awaiting CI"'; then + gh pr edit "$PR" --repo "$REPO" --add-label "Awaiting CI" + fi + fi + + # When a workflow label is added, clean up labels that no longer apply. + # Only runs for labels that have cleanup rules. + on-label: + if: >- + github.event.action == 'labeled' + && contains(fromJSON('["Ready for QA","QA Active","Dev Active","Ready for QA Signoff","QA Failed","QA Approved"]'), github.event.label.name) + runs-on: ubuntu-latest + steps: + - name: Clean up stale labels + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + ADDED_LABEL: ${{ github.event.label.name }} + PR_LABELS: ${{ toJSON(github.event.pull_request.labels.*.name) }} + run: | + PR=${{ github.event.pull_request.number }} + REPO=${{ github.repository }} + + remove_if_present() { + if echo "$PR_LABELS" | grep -q "\"$1\""; then + gh pr edit "$PR" --repo "$REPO" --remove-label "$1" + fi + } + + case "$ADDED_LABEL" in + "Ready for QA") + remove_if_present "Awaiting CI" + remove_if_present "CI Failed" + ;; + "QA Active") + remove_if_present "Ready for QA" + ;; + "Dev Active") + remove_if_present "QA Failed" + remove_if_present "CI Failed" + remove_if_present "Awaiting CI" + remove_if_present "Ready for QA" + ;; + "Ready for QA Signoff") + remove_if_present "QA Active" + ;; + "QA Failed") + remove_if_present "QA Active" + ;; + "QA Approved") + remove_if_present "Ready for QA Signoff" + ;; + esac +``` + +- [ ] **Step 2: Verify YAML syntax** + +Run: `python -c "import yaml; yaml.safe_load(open('.github/workflows/pr-labels.yml'))"` +Expected: No output (valid YAML) + +- [ ] **Step 3: Commit** + +```bash +git add .github/workflows/pr-labels.yml +git commit -m "refactor: rewrite pr-labels.yml as clean state machine + +Complete rewrite based on the state machine spec. Key changes: +- Dev Active coexists with Awaiting CI (no longer blocks it) +- on-push keeps Dev Active, removes all other workflow labels +- on-unlabel checks CI status and promotes if already passed +- on-label cleanup includes Ready for QA removing Awaiting CI + CI Failed +- QA Invalidated label removed entirely (comment is sufficient) +- CI Failed added to cleanup rules for Dev Active and Ready for QA" +``` + +--- + +### Task 2: Rewrite pr-labels-ci.yml (workflow_run triggers) + +**Files:** +- Rewrite: `.github/workflows/pr-labels-ci.yml` + +This file handles two jobs triggered by CI completion: `on-ci-pass` and `on-ci-fail`. + +- [ ] **Step 1: Write the complete pr-labels-ci.yml file** + +Replace the entire contents of `.github/workflows/pr-labels-ci.yml` with: + +```yaml +# mcp-awareness — ambient system awareness for AI agents +# Copyright (C) 2026 Chris Means +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +name: PR Label Automation (CI) + +# Handles label transitions when CI completes (pass or fail). +# Split from pr-labels.yml because workflow_run fires on every CI +# completion (including merges to main), which caused noisy failures +# when there was no associated PR. +# +# NOTE: References the CI workflow by name ("CI"). +# If the CI workflow in ci.yml is ever renamed, update the name here too. +# +# LIMITATION: workflow_run triggers always run from the default branch +# (main), not the PR branch. Changes to this file cannot be tested +# on a PR — they take effect only after merge. +on: + workflow_run: + workflows: [CI] + types: [completed] + +permissions: + pull-requests: write + checks: read + +jobs: + # When CI passes on a PR branch, promote from Awaiting CI to Ready for QA. + # Skips if Dev Active is present (dev isn't done yet). + on-ci-pass: + if: >- + github.event.workflow_run.conclusion == 'success' + && github.event.workflow_run.event == 'pull_request' + && github.event.workflow_run.head_branch != github.event.repository.default_branch + runs-on: ubuntu-latest + steps: + - name: Promote to Ready for QA + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + REPO=${{ github.repository }} + + # Look up the associated PR. + # The API can 404 after a force-push (orphaned run). + # Capture output only on success so 404 body doesn't leak into $PR. + PR="" + API_OUT=$(gh api "repos/$REPO/actions/runs/${{ github.event.workflow_run.id }}/pull_requests" \ + --jq '.[0].number // empty' 2>&1) && PR="$API_OUT" || true + + # Fallback: pull_requests array is empty for dependabot PRs. + # Search by head branch instead. + if [ -z "$PR" ]; then + HEAD_BRANCH=${{ github.event.workflow_run.head_branch }} + PR=$(gh pr list --repo "$REPO" --head "$HEAD_BRANCH" --state open \ + --json number --jq '.[0].number // empty' 2>/dev/null) || true + fi + + if [ -z "$PR" ]; then + echo "No PR associated with this workflow run — exiting" + exit 0 + fi + + LABELS=$(gh pr view "$PR" --repo "$REPO" --json labels --jq '.labels[].name') + + # Dev Active means dev isn't done — don't promote. + # on-unlabel will handle promotion when Dev Active is removed. + if echo "$LABELS" | grep -q "^Dev Active$"; then + echo "Dev Active present — skipping promotion" + exit 0 + fi + + # Promote if Awaiting CI is present + if echo "$LABELS" | grep -q "^Awaiting CI$"; then + REMOVE="Awaiting CI" + if echo "$LABELS" | grep -q "^CI Failed$"; then + REMOVE="Awaiting CI,CI Failed" + fi + gh pr edit "$PR" --repo "$REPO" --remove-label "$REMOVE" + gh pr edit "$PR" --repo "$REPO" --add-label "Ready for QA" + fi + + # When CI fails on a PR branch, set CI Failed. + # Skips if Dev Active is present (dev is already working). + on-ci-fail: + if: >- + github.event.workflow_run.conclusion == 'failure' + && github.event.workflow_run.event == 'pull_request' + && github.event.workflow_run.head_branch != github.event.repository.default_branch + runs-on: ubuntu-latest + steps: + - name: Set CI Failed + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + REPO=${{ github.repository }} + + PR="" + API_OUT=$(gh api "repos/$REPO/actions/runs/${{ github.event.workflow_run.id }}/pull_requests" \ + --jq '.[0].number // empty' 2>&1) && PR="$API_OUT" || true + + if [ -z "$PR" ]; then + HEAD_BRANCH=${{ github.event.workflow_run.head_branch }} + PR=$(gh pr list --repo "$REPO" --head "$HEAD_BRANCH" --state open \ + --json number --jq '.[0].number // empty' 2>/dev/null) || true + fi + + if [ -z "$PR" ]; then + echo "No PR associated with this workflow run — exiting" + exit 0 + fi + + LABELS=$(gh pr view "$PR" --repo "$REPO" --json labels --jq '.labels[].name') + + # Dev Active means dev is already working — don't add CI Failed + if echo "$LABELS" | grep -q "^Dev Active$"; then + echo "Dev Active present — skipping CI Failed" + exit 0 + fi + + # Set CI Failed if Awaiting CI is present (normal flow) + if echo "$LABELS" | grep -q "^Awaiting CI$"; then + gh pr edit "$PR" --repo "$REPO" --remove-label "Awaiting CI" + gh pr edit "$PR" --repo "$REPO" --add-label "CI Failed" + fi +``` + +- [ ] **Step 2: Verify YAML syntax** + +Run: `python -c "import yaml; yaml.safe_load(open('.github/workflows/pr-labels-ci.yml'))"` +Expected: No output (valid YAML) + +- [ ] **Step 3: Commit** + +```bash +git add .github/workflows/pr-labels-ci.yml +git commit -m "refactor: rewrite pr-labels-ci.yml with CI Failed state and safe PR lookup + +Complete rewrite based on the state machine spec. Key changes: +- on-ci-fail adds CI Failed label (not Dev Active) when CI fails +- Dev Active suppresses both pass and fail transitions +- Safe 404 handling: API output captured only on success +- head_branch filter skips CI runs on main (merge commits) +- CI Failed cleaned up on next CI pass" +``` + +--- + +### Task 3: Create CI Failed label and clean up QA Invalidated + +**Files:** None (GitHub API only) + +- [ ] **Step 1: Create the CI Failed label** + +```bash +gh label create "CI Failed" --repo cmeans/mcp-awareness --color "B60205" --description "CI failed — dev needs to fix" --force +``` + +Expected: Label created (red color matches QA Failed) + +- [ ] **Step 2: Delete QA Invalidated label if it exists** + +```bash +gh label delete "QA Invalidated" --repo cmeans/mcp-awareness --yes 2>/dev/null || echo "Label does not exist" +``` + +Expected: Label deleted or "does not exist" + +- [ ] **Step 3: Verify label set** + +```bash +gh label list --repo cmeans/mcp-awareness | grep -E "Active|Awaiting|Failed|Ready|QA|CI" +``` + +Expected: Shows `Dev Active`, `Awaiting CI`, `CI Failed`, `Ready for QA`, `QA Active`, `Ready for QA Signoff`, `QA Failed`, `QA Approved`. No `QA Invalidated`. + +- [ ] **Step 4: Commit** (no file changes — label operations are API-only, but commit the spec) + +```bash +git add docs/superpowers/specs/2026-03-30-pr-label-state-machine-design.md +git commit -m "docs: add PR label state machine design spec" +``` + +--- + +### Task 4: Update PR description and push + +- [ ] **Step 1: Update the PR #108 description to reflect the full redesign** + +```bash +gh pr edit 108 --title "refactor: PR label automation as a proper state machine" --body "$(cat <<'PREOF' +## Summary + +- Rewrites the PR label automation as a clean state machine with defined states, transitions, and cleanup rules +- Splits workflow_run trigger into separate file to eliminate failure emails on merges to main +- Adds `CI Failed` label for CI failures (covers both hard failures and Codecov) +- Removes `QA Invalidated` label (push comment is sufficient) +- `Dev Active` now coexists with `Awaiting CI` instead of blocking it + +## State Machine + +```mermaid +stateDiagram-v2 + [*] --> Awaiting_CI : PR opened / push + Dev_Active --> Awaiting_CI : push (coexist) + Awaiting_CI --> Ready_for_QA : CI pass (no Dev Active) + Awaiting_CI --> CI_Failed : CI fail (no Dev Active) + CI_Failed --> Awaiting_CI : push (fix) + Ready_for_QA --> QA_Active : QA starts + QA_Active --> Ready_for_QA_Signoff : signoff + QA_Active --> QA_Failed : fail + Ready_for_QA_Signoff --> QA_Approved : approved + QA_Approved --> [*] : merge + QA_Failed --> Dev_Active : dev picks up +``` + +## Design spec + +`docs/superpowers/specs/2026-03-30-pr-label-state-machine-design.md` + +## Changes + +| File | What | +|------|------| +| `pr-labels.yml` | Rewritten: on-push keeps Dev Active, removes all others, adds Awaiting CI. on-unlabel promotes if CI passed. on-label has full cleanup matrix. | +| `pr-labels-ci.yml` | Rewritten: on-ci-pass promotes (skips if Dev Active). on-ci-fail adds CI Failed (skips if Dev Active). Safe 404 handling. | +| Labels | Created `CI Failed`. Deleted `QA Invalidated`. | + +## QA + +### Prerequisites +- Review both workflow files against the spec + +### Manual tests +1. - [ ] **New PR without Dev Active** — create a test PR, verify it gets `Awaiting CI` on open, `Ready for QA` after CI passes (note: on-ci-pass runs from main, so this tests the OLD code until merge) +2. - [ ] **Push with Dev Active** — add `Dev Active` to a PR, push a commit, verify both `Dev Active` and `Awaiting CI` are present +3. - [ ] **Remove Dev Active after CI passed** — remove `Dev Active`, verify `Ready for QA` appears within 15s +4. - [ ] **CI failure without Dev Active** — push a commit that breaks lint, verify `CI Failed` appears (post-merge test only — on-ci-fail runs from main) +5. - [ ] **Label cleanup** — add `Ready for QA` manually, verify `Awaiting CI` is removed +6. - [ ] **No failure emails on merge** — merge a PR, verify no "PR Label Automation (CI)" failure email + +Note: Tests 1, 4, and 6 depend on the workflow_run trigger which runs from main. These can only be fully verified after merge. +PREOF +)" +``` + +- [ ] **Step 2: Push all commits** + +```bash +git push +``` + +- [ ] **Step 3: Verify labels on PR #108** + +```bash +gh pr view 108 --json labels --jq '[.labels[].name]' +``` + +Expected: `["Ready for QA"]` (or the automation sets it after push) + +- [ ] **Step 4: If automation doesn't set Ready for QA within 30 seconds, set it manually** + +The on-push job will reset to `Awaiting CI`, but since this is a `pull_request`-triggered workflow it runs from the PR branch with the new code. After CI passes, the old `on-ci-pass` on main will try to promote. If it fails (chicken-and-egg), manually set: + +```bash +gh pr edit 108 --remove-label "Awaiting CI" --add-label "Ready for QA" +``` diff --git a/docs/superpowers/specs/2026-03-30-pr-label-state-machine-design.md b/docs/superpowers/specs/2026-03-30-pr-label-state-machine-design.md new file mode 100644 index 0000000..c99a75a --- /dev/null +++ b/docs/superpowers/specs/2026-03-30-pr-label-state-machine-design.md @@ -0,0 +1,173 @@ +# PR Label State Machine Design + +**Date:** 2026-03-30 +**Status:** Approved +**Scope:** `.github/workflows/pr-labels.yml`, `.github/workflows/pr-labels-ci.yml` + +## Problem + +The PR label automation grew organically and had several bugs: +- `workflow_run` trigger fired on merges to main (no associated PR), causing failure emails +- `Dev Active` blocked `Awaiting CI` from being added, creating a dead end +- `opened` event was missing, so new PRs never received `Awaiting CI` +- `Ready for QA` didn't clean up `Awaiting CI`, allowing label coexistence +- No CI failure label — GitHub's soft failures (Codecov) aren't always blocking, but the team applies stricter standards +- `QA Invalidated` was added without user direction and adds complexity without value + +These were discovered reactively across 4 incremental commits instead of being caught upfront. + +## Design + +### States + +Exactly one workflow label is active at a time, except `Dev Active` which coexists with `Awaiting CI`. + +| Label | Meaning | +|-------|---------| +| `Dev Active` | Dev is working on this PR. Don't promote even if CI passes. Set manually by the dev. | +| `Awaiting CI` | CI is running. Waiting for result. | +| `CI Failed` | CI failed (hard failure or soft/Codecov). Dev needs to fix. | +| `Ready for QA` | CI passed, QA can begin testing. | +| `QA Active` | QA is actively reviewing. | +| `Ready for QA Signoff` | QA finished review, waiting for sign-off decision. | +| `QA Failed` | QA found issues. Back to dev. | +| `QA Approved` | QA passed. Merge is unblocked (satisfies QA Gate check). | + +### Removed + +- `QA Invalidated` — removed entirely. Push resets labels and the PR comment is sufficient notification that QA's review was invalidated. + +### State Diagram + +```mermaid +stateDiagram-v2 + [*] --> Awaiting_CI : PR opened + Dev_Active --> Awaiting_CI : push (coexist) + Awaiting_CI --> Ready_for_QA : CI pass (no Dev Active) + Awaiting_CI --> CI_Failed : CI fail (no Dev Active) + CI_Failed --> Awaiting_CI : push (fix) + Ready_for_QA --> QA_Active : QA starts + QA_Active --> Ready_for_QA_Signoff : signoff + QA_Active --> QA_Failed : fail + Ready_for_QA_Signoff --> QA_Approved : approved + QA_Approved --> [*] : merge + QA_Failed --> Dev_Active : dev picks up + + state Dev_Active { + direction LR + [*] --> holding : manual set + holding --> holding : push (CI runs, no promotion) + } + + note right of Dev_Active + Coexists with Awaiting CI. + CI runs but does not promote. + Removing Dev Active triggers + promotion if CI already passed. + end note + + note right of CI_Failed + Covers hard failures (lint, tests) + and soft failures (Codecov). + Cleaned up on next push or CI pass. + end note +``` + +### Dev Active Semantics + +`Dev Active` is a **manual hold** that coexists with `Awaiting CI`: + +- Dev sets `Dev Active` before pushing to signal they're not done +- Push adds `Awaiting CI` alongside `Dev Active` (both present) +- CI runs normally. If CI passes, `on-ci-pass` sees `Dev Active` and **does not promote** +- If CI fails with `Dev Active` present, **nothing happens** (dev is already working) +- When dev removes `Dev Active`, `on-unlabel` checks CI status: + - CI already passed -> promote to `Ready for QA` + - CI not passed -> `Awaiting CI` is already present, `on-ci-pass` will promote when CI passes + +### Transition Rules + +#### on-push (PR opened or synchronized) + +Trigger: `pull_request: [opened, synchronize]` +File: `pr-labels.yml` + +1. Remove all QA/workflow labels: `Ready for QA`, `Ready for QA Signoff`, `QA Approved`, `QA Active`, `QA Failed`, `CI Failed` +2. **Keep `Dev Active` if present** — don't remove it +3. Add `Awaiting CI` (unless already present) +4. If `QA Active` was present, add comment: "New commits pushed while QA was active." + +#### on-ci-pass (CI completes successfully) + +Trigger: `workflow_run: completed` (success, PR branch, not default branch) +File: `pr-labels-ci.yml` + +1. Look up associated PR (API with 404 safety, fallback to branch search) +2. If no PR found, exit silently +3. If `Dev Active` present, exit (don't promote) +4. If `Awaiting CI` present: remove `Awaiting CI`, remove `CI Failed` if present, add `Ready for QA` + +#### on-ci-fail (CI fails) + +Trigger: `workflow_run: completed` (failure, PR branch, not default branch) +File: `pr-labels-ci.yml` + +1. Look up associated PR (same pattern as on-ci-pass) +2. If no PR found, exit silently +3. If `Dev Active` present, exit (dev is already working) +4. If `Awaiting CI` present: remove `Awaiting CI`, add `CI Failed` + +#### on-unlabel (Dev Active removed) + +Trigger: `pull_request: unlabeled` where label is `Dev Active` +File: `pr-labels.yml` + +1. Check if CI already passed for head commit +2. If CI passed: remove `Awaiting CI` if present, add `Ready for QA` +3. If CI not passed: ensure `Awaiting CI` is present (should already be) + +#### on-label (cleanup rules) + +Trigger: `pull_request: labeled` for specific labels +File: `pr-labels.yml` + +| Label Added | Remove | +|-------------|--------| +| `Ready for QA` | `Awaiting CI`, `CI Failed` | +| `QA Active` | `Ready for QA` | +| `Dev Active` | `QA Failed`, `CI Failed`, `Awaiting CI`, `Ready for QA` | +| `Ready for QA Signoff` | `QA Active` | +| `QA Failed` | `QA Active` | +| `QA Approved` | `Ready for QA Signoff` | + +### Edge Cases + +| Scenario | Behavior | +|----------|----------| +| Dev sets `Dev Active`, pushes 3 times | Each push resets `Awaiting CI`, `Dev Active` persists, CI runs but doesn't promote | +| Dev removes `Dev Active` after CI passed | `on-unlabel` promotes to `Ready for QA` | +| Dev removes `Dev Active` before CI passed | `Awaiting CI` already present, `on-ci-pass` promotes later | +| CI fails without `Dev Active` | `CI Failed` added, dev pushes fix -> `Awaiting CI` | +| CI fails with `Dev Active` | Nothing, dev is already working | +| QA fails, dev adds `Dev Active`, pushes fix | `Dev Active` cleans up `QA Failed`, push adds `Awaiting CI`, flow restarts | +| New PR opened without `Dev Active` | Straight to `Awaiting CI` | +| Codecov fails but GitHub doesn't block | `CI Failed` label makes stricter standard visible | + +### File Structure + +Two workflow files, split by trigger type: + +- **`pr-labels.yml`** — `on: pull_request [opened, synchronize, labeled, unlabeled]` + - Jobs: `on-push`, `on-unlabel`, `on-label` +- **`pr-labels-ci.yml`** — `on: workflow_run [completed]` + - Jobs: `on-ci-pass`, `on-ci-fail` + +Rationale: `workflow_run` fires on every CI completion (including merges to main). Separating it prevents noisy failures when there's no associated PR. The `head_branch != default_branch` filter plus safe 404 handling provide defense in depth. + +### Known Limitation + +`workflow_run`-triggered workflows always run from the default branch (main). Changes to `pr-labels-ci.yml` cannot be tested on a PR branch — they take effect only after merge. The `pull_request`-triggered jobs in `pr-labels.yml` DO run from the PR branch and can be tested normally. + +### QA Gate + +`qa-gate.yml` is unchanged. It sets a GitHub status check based on the `QA Approved` label. This is the merge gate — independent of the label automation. From ab650d82264b2aec364cbc38783a10f8f9979710 Mon Sep 17 00:00:00 2001 From: "cmeans-claude-dev[bot]" <3223881+cmeans-claude-dev[bot]@users.noreply.github.com> Date: Mon, 30 Mar 2026 12:28:08 -0500 Subject: [PATCH 8/9] fix: QA Failed at signoff stage cleans up Ready for QA Signoff QA can fail a PR during signoff review, not just during active QA. Added Ready for QA Signoff to the QA Failed cleanup rule so both labels don't coexist. Updated spec diagram and edge cases. Co-Authored-By: Claude Opus 4.6 (1M context) --- .github/workflows/pr-labels.yml | 1 + .../specs/2026-03-30-pr-label-state-machine-design.md | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/pr-labels.yml b/.github/workflows/pr-labels.yml index f3afc5d..0109c18 100644 --- a/.github/workflows/pr-labels.yml +++ b/.github/workflows/pr-labels.yml @@ -159,6 +159,7 @@ jobs: ;; "QA Failed") remove_if_present "QA Active" + remove_if_present "Ready for QA Signoff" ;; "QA Approved") remove_if_present "Ready for QA Signoff" diff --git a/docs/superpowers/specs/2026-03-30-pr-label-state-machine-design.md b/docs/superpowers/specs/2026-03-30-pr-label-state-machine-design.md index c99a75a..a815e6d 100644 --- a/docs/superpowers/specs/2026-03-30-pr-label-state-machine-design.md +++ b/docs/superpowers/specs/2026-03-30-pr-label-state-machine-design.md @@ -49,6 +49,7 @@ stateDiagram-v2 Ready_for_QA --> QA_Active : QA starts QA_Active --> Ready_for_QA_Signoff : signoff QA_Active --> QA_Failed : fail + Ready_for_QA_Signoff --> QA_Failed : fail at signoff Ready_for_QA_Signoff --> QA_Approved : approved QA_Approved --> [*] : merge QA_Failed --> Dev_Active : dev picks up @@ -137,7 +138,7 @@ File: `pr-labels.yml` | `QA Active` | `Ready for QA` | | `Dev Active` | `QA Failed`, `CI Failed`, `Awaiting CI`, `Ready for QA` | | `Ready for QA Signoff` | `QA Active` | -| `QA Failed` | `QA Active` | +| `QA Failed` | `QA Active`, `Ready for QA Signoff` | | `QA Approved` | `Ready for QA Signoff` | ### Edge Cases @@ -152,6 +153,7 @@ File: `pr-labels.yml` | QA fails, dev adds `Dev Active`, pushes fix | `Dev Active` cleans up `QA Failed`, push adds `Awaiting CI`, flow restarts | | New PR opened without `Dev Active` | Straight to `Awaiting CI` | | Codecov fails but GitHub doesn't block | `CI Failed` label makes stricter standard visible | +| QA fails at signoff stage | `QA Failed` removes `Ready for QA Signoff`, back to dev | ### File Structure From 8eb3b96f0d553ea1750c2cd6e2c2cb1d11e2330d Mon Sep 17 00:00:00 2001 From: Chris Means Date: Mon, 30 Mar 2026 12:56:13 -0500 Subject: [PATCH 9/9] test: QA push to verify Dev Active coexistence with Awaiting CI Co-Authored-By: Claude Opus 4.6 (1M context) --- .../specs/2026-03-30-pr-label-state-machine-design.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/superpowers/specs/2026-03-30-pr-label-state-machine-design.md b/docs/superpowers/specs/2026-03-30-pr-label-state-machine-design.md index a815e6d..18a73a4 100644 --- a/docs/superpowers/specs/2026-03-30-pr-label-state-machine-design.md +++ b/docs/superpowers/specs/2026-03-30-pr-label-state-machine-design.md @@ -173,3 +173,4 @@ Rationale: `workflow_run` fires on every CI completion (including merges to main ### QA Gate `qa-gate.yml` is unchanged. It sets a GitHub status check based on the `QA Approved` label. This is the merge gate — independent of the label automation. +