Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
120 changes: 120 additions & 0 deletions .github/workflows/pr-labels-ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
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
151 changes: 151 additions & 0 deletions .github/workflows/pr-labels.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
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"
remove_if_present "Ready for QA Signoff"
;;
"QA Approved")
remove_if_present "Ready for QA Signoff"
;;
esac
30 changes: 30 additions & 0 deletions .github/workflows/qa-gate.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
name: QA Gate

on:
pull_request:
branches: [main]
types: [opened, synchronize, reopened, labeled, unlabeled]

permissions:
statuses: write

jobs:
qa-approved:
runs-on: ubuntu-latest
steps:
- name: Set QA status
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
if echo '${{ toJSON(github.event.pull_request.labels.*.name) }}' | grep -q '"QA Approved"'; then
STATE="success"
DESC="QA testing completed"
else
STATE="pending"
DESC="Waiting for QA — add 'QA Approved' label when testing is complete"
fi
gh api repos/${{ github.repository }}/statuses/${{ github.event.pull_request.head.sha }} \
--method POST \
-f state="$STATE" \
-f description="$DESC" \
-f context="QA Gate"
Loading