From d9f611e47ad7961dfdf7e569ced2500f8a5d16c8 Mon Sep 17 00:00:00 2001 From: ArchonVII Date: Tue, 9 Jun 2026 17:45:07 -0500 Subject: [PATCH] fix(required-gate): guard label reruns to ci full --- .../unreleased/59-ci-full-label-guard.md | 4 +++ README.md | 13 ++++++---- examples/repo-required-gate.yml | 25 +++++++++++++++---- scripts/workflow-structure.test.mjs | 19 +++++++++++--- 4 files changed, 47 insertions(+), 14 deletions(-) create mode 100644 .changelog/unreleased/59-ci-full-label-guard.md diff --git a/.changelog/unreleased/59-ci-full-label-guard.md b/.changelog/unreleased/59-ci-full-label-guard.md new file mode 100644 index 0000000..fb0c557 --- /dev/null +++ b/.changelog/unreleased/59-ci-full-label-guard.md @@ -0,0 +1,4 @@ +### Fixed + +- Preserved the `ci:full` label escape hatch without letting arbitrary PR label + changes such as `no-changelog` invoke or cancel the required gate. (#59) diff --git a/README.md b/README.md index 515530c..66f44b3 100644 --- a/README.md +++ b/README.md @@ -72,11 +72,14 @@ repo-required-gate / decision Keep targeted checks inside the gate or leave them non-required. Do not make branch protection depend on workflows that can be skipped by path filters; GitHub can leave those required checks pending. -The required-gate caller intentionally does not listen for `labeled` or -`unlabeled` events. Labels such as `no-changelog` should not cancel and restart -the required branch-protection gate. The `ci:full` label is still honored by the -classifier when the gate runs; after applying it to an already-open PR, rerun the -workflow, push a new commit, or use another normal PR event to refresh the gate. +The required-gate caller listens for `labeled` and `unlabeled` events only to +preserve the `ci:full` escape hatch. The caller job and concurrency expression +are guarded so `ci:full` is the only label change that can run or cancel the +required branch-protection gate. Other labels such as `no-changelog` may create +a skipped caller run, but they do not start `repo-required-gate / decision` and +do not cancel an in-flight required-gate run. If a repo overrides the +`force-full-ci-label` input, update the caller guard and concurrency label name +to match. For agent closeout, use the shared local preflight instead of direct promotion: diff --git a/examples/repo-required-gate.yml b/examples/repo-required-gate.yml index e12edcb..80b5710 100644 --- a/examples/repo-required-gate.yml +++ b/examples/repo-required-gate.yml @@ -7,7 +7,7 @@ name: Repo Required Gate on: pull_request: branches: [main] - types: [opened, edited, synchronize, reopened, ready_for_review] + types: [opened, edited, synchronize, reopened, ready_for_review, labeled, unlabeled] merge_group: permissions: @@ -16,7 +16,14 @@ permissions: concurrency: group: repo-required-gate-${{ github.event.pull_request.number || github.ref }} - cancel-in-progress: ${{ github.event_name == 'pull_request' }} + cancel-in-progress: >- + ${{ + github.event_name == 'pull_request' && + ( + (github.event.action != 'labeled' && github.event.action != 'unlabeled') || + github.event.label.name == 'ci:full' + ) + }} jobs: # ------------------------------------------------------------------------- @@ -26,6 +33,12 @@ jobs: # code (node|python|go|polyglot) / forced-full (via the `ci:full` label). # ------------------------------------------------------------------------- repo-required-gate: + if: >- + ${{ + github.event_name != 'pull_request' || + (github.event.action != 'labeled' && github.event.action != 'unlabeled') || + github.event.label.name == 'ci:full' + }} uses: ArchonVII/github-workflows/.github/workflows/repo-required-gate.yml@v1 with: # ------ Minimal (docs / config repos with no language CI) ----------- @@ -105,8 +118,10 @@ jobs: # ------ Force-full escape hatch (any stack) ------------------------- # Default label is `ci:full`. Apply it to a PR to bypass path-based # routing and run the full language CI surface — e.g. a workflow-only - # change that semantically affects CI. - # If you add the label after the latest gate run, rerun the workflow, - # push a new commit, or use another normal PR event to refresh the gate. + # change that semantically affects CI. The caller guard above allows + # only this label to run/cancel the gate on label changes; other labels + # such as `no-changelog` skip without replacing the required decision. + # If you override the label input, update the guard/concurrency label + # name above to match. # # force-full-ci-label: ci:full diff --git a/scripts/workflow-structure.test.mjs b/scripts/workflow-structure.test.mjs index 6844011..91c30bf 100644 --- a/scripts/workflow-structure.test.mjs +++ b/scripts/workflow-structure.test.mjs @@ -115,12 +115,23 @@ describe('repo-required-gate workflow node delegation', () => { }); describe('repo-required-gate caller example', () => { - it('does not rerun the required gate for arbitrary label changes', () => { + it('runs the required gate only for ci:full label changes', () => { const body = readExample('repo-required-gate'); - expect(body).toContain('types: [opened, edited, synchronize, reopened, ready_for_review]'); - expect(body).not.toMatch(/^\s*labeled,?\s*$/m); - expect(body).not.toMatch(/^\s*unlabeled,?\s*$/m); + expect(body).toContain( + 'types: [opened, edited, synchronize, reopened, ready_for_review, labeled, unlabeled]', + ); + expect(body).toContain("github.event.action != 'labeled'"); + expect(body).toContain("github.event.action != 'unlabeled'"); + expect(body).toContain("github.event.label.name == 'ci:full'"); + + const jobBlock = workflowJobBlock(body, 'repo-required-gate'); + expect(jobBlock).toContain('if: >-'); + expect(jobBlock).toContain("github.event.label.name == 'ci:full'"); + + const concurrencyBlock = body.slice(body.indexOf('concurrency:'), body.indexOf('jobs:')); + expect(concurrencyBlock).toContain('cancel-in-progress: >-'); + expect(concurrencyBlock).toContain("github.event.label.name == 'ci:full'"); }); });