From 62ab5535f365a414d3160f04cbec3f97c725c10c Mon Sep 17 00:00:00 2001 From: ArchonVII Date: Sat, 13 Jun 2026 18:56:07 -0500 Subject: [PATCH] fix(required-gate): decouple language lanes from pr-contract MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A failing PR body must not suppress real CI. The seven downstream lanes were gated on pr-contract success, so a contract failure skipped every lane and the decision job reported a misleading "node ci ... skipped" while no CI actually ran. Lanes now depend on detect alone; the decision job remains the single aggregator that requires both the contract and the lanes, so a malformed body still blocks merge — with real signal. Invert the workflow-structure tests that encoded the coupling as the spec, and add a test asserting the decision job still enforces the contract after decoupling. Closes #72 Refs ArchonVII/archon#200, ArchonVII/archon#202 Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/repo-required-gate.yml | 29 ++++++++++---------- scripts/workflow-structure.test.mjs | 34 +++++++++++------------- 2 files changed, 31 insertions(+), 32 deletions(-) diff --git a/.github/workflows/repo-required-gate.yml b/.github/workflows/repo-required-gate.yml index 67ac85e..cf33534 100644 --- a/.github/workflows/repo-required-gate.yml +++ b/.github/workflows/repo-required-gate.yml @@ -523,12 +523,19 @@ jobs: core.warning(`pr-template: ${report}`); } + # The validation/language lanes below are intentionally DECOUPLED from + # pr-contract: they run on detect classification alone and are never gated on + # contract success. A failing PR body must not suppress real CI. Coupling them + # caused every lane to skip on a body failure, after which the decision job + # reported a misleading "node ci ... required but result was skipped" while no + # CI had actually run (ArchonVII/archon#200). The decision job still requires + # BOTH the contract and the lanes, so a malformed body still blocks merge — + # now with real lane signal instead of a phantom skip. workflow-validation: name: workflow and hook validation needs: - detect - - pr-contract - if: always() && inputs.run-workflow-validation && needs.detect.outputs.ok == 'true' && needs.detect.outputs.run-workflow-validation == 'true' && (github.event_name != 'pull_request' || inputs.run-pr-contract == false || needs.pr-contract.result == 'success') + if: always() && inputs.run-workflow-validation && needs.detect.outputs.ok == 'true' && needs.detect.outputs.run-workflow-validation == 'true' runs-on: ubuntu-latest permissions: contents: read @@ -559,8 +566,7 @@ jobs: name: policy validation needs: - detect - - pr-contract - if: always() && inputs.run-policy-validation && needs.detect.outputs.ok == 'true' && needs.detect.outputs.run-policy-validation == 'true' && (github.event_name != 'pull_request' || inputs.run-pr-contract == false || needs.pr-contract.result == 'success') + if: always() && inputs.run-policy-validation && needs.detect.outputs.ok == 'true' && needs.detect.outputs.run-policy-validation == 'true' runs-on: ubuntu-latest permissions: contents: read @@ -579,8 +585,7 @@ jobs: name: dependency review needs: - detect - - pr-contract - if: always() && inputs.run-dependency-review && github.event_name == 'pull_request' && needs.detect.outputs.ok == 'true' && needs.detect.outputs.run-dependency-review == 'true' && (github.event_name != 'pull_request' || inputs.run-pr-contract == false || needs.pr-contract.result == 'success') + if: always() && inputs.run-dependency-review && github.event_name == 'pull_request' && needs.detect.outputs.ok == 'true' && needs.detect.outputs.run-dependency-review == 'true' runs-on: ubuntu-latest permissions: contents: read @@ -598,8 +603,7 @@ jobs: name: node ci needs: - detect - - pr-contract - if: always() && needs.detect.outputs.ok == 'true' && needs.detect.outputs.run-node-ci == 'true' && (github.event_name != 'pull_request' || inputs.run-pr-contract == false || needs.pr-contract.result == 'success') + if: always() && needs.detect.outputs.ok == 'true' && needs.detect.outputs.run-node-ci == 'true' uses: ArchonVII/github-workflows/.github/workflows/node-ci.yml@v1 with: node-versions: ${{ inputs.node-versions }} @@ -615,8 +619,7 @@ jobs: name: python ci needs: - detect - - pr-contract - if: always() && needs.detect.outputs.ok == 'true' && needs.detect.outputs.run-python-ci == 'true' && (github.event_name != 'pull_request' || inputs.run-pr-contract == false || needs.pr-contract.result == 'success') + if: always() && needs.detect.outputs.ok == 'true' && needs.detect.outputs.run-python-ci == 'true' uses: ArchonVII/github-workflows/.github/workflows/python-ci.yml@v1 with: python-versions: ${{ inputs.python-versions }} @@ -632,8 +635,7 @@ jobs: name: go ci needs: - detect - - pr-contract - if: always() && needs.detect.outputs.ok == 'true' && needs.detect.outputs.run-go-ci == 'true' && (github.event_name != 'pull_request' || inputs.run-pr-contract == false || needs.pr-contract.result == 'success') + if: always() && needs.detect.outputs.ok == 'true' && needs.detect.outputs.run-go-ci == 'true' uses: ArchonVII/github-workflows/.github/workflows/go-ci.yml@v1 with: go-versions: ${{ inputs.go-versions }} @@ -651,8 +653,7 @@ jobs: name: snapshot validation needs: - detect - - pr-contract - if: always() && needs.detect.outputs.ok == 'true' && needs.detect.outputs.run-snapshot-validation == 'true' && (github.event_name != 'pull_request' || inputs.run-pr-contract == false || needs.pr-contract.result == 'success') + if: always() && needs.detect.outputs.ok == 'true' && needs.detect.outputs.run-snapshot-validation == 'true' runs-on: ubuntu-latest permissions: contents: read diff --git a/scripts/workflow-structure.test.mjs b/scripts/workflow-structure.test.mjs index 992a9cd..1b252eb 100644 --- a/scripts/workflow-structure.test.mjs +++ b/scripts/workflow-structure.test.mjs @@ -49,44 +49,42 @@ describe('repo-required-gate workflow node delegation', () => { expect(body).not.toContain('cache: "npm"'); }); - it('gates expensive PR jobs on the cheap PR contract', () => { + it('decouples the language and validation lanes from the PR contract', () => { const body = readWorkflow('repo-required-gate'); + // A failing PR body must NEVER skip real CI: every lane depends on `detect` + // alone and is never gated on contract success. Coupling them skipped all + // CI on a body failure and surfaced a misleading "node ci ... skipped" from + // the decision job (ArchonVII/archon#200). Contract enforcement lives in + // the decision job, asserted in the next test. for (const job of [ 'workflow-validation', 'policy-validation', 'dependency-review', 'node-ci', 'python-ci', + 'go-ci', 'snapshot-validation', ]) { const block = workflowJobBlock(body, job); expect(block, `${job} job exists`).not.toBe(''); - expect(block, `${job} waits for pr-contract`).toContain('pr-contract'); - expect(block, `${job} skips when pull-request contract failed`).toContain( + expect(block, `${job} does not wait for pr-contract`).not.toContain('pr-contract'); + expect(block, `${job} is not gated on contract success`).not.toContain( "needs.pr-contract.result == 'success'", ); } }); - it('lets explicit pr-contract opt-outs continue to downstream jobs', () => { + it('still enforces the PR contract in the decision job after decoupling', () => { const body = readWorkflow('repo-required-gate'); + const decision = workflowJobBlock(body, 'decision'); - for (const job of [ - 'workflow-validation', - 'policy-validation', - 'dependency-review', - 'node-ci', - 'python-ci', - 'snapshot-validation', - ]) { - const block = workflowJobBlock(body, job); - - expect(block, `${job} respects run-pr-contract=false`).toContain( - "github.event_name != 'pull_request' || inputs.run-pr-contract == false || needs.pr-contract.result == 'success'", - ); - } + // Decoupling the lanes must not drop contract enforcement — the decision + // job stays the single aggregator that requires the contract. + expect(decision, 'decision waits for pr-contract').toContain('- pr-contract'); + expect(decision).toContain("CONTRACT_RESULT: ${{ needs.pr-contract.result }}"); + expect(decision).toContain('require_success "pr contract" "$CONTRACT_RESULT"'); }); it('honors optional validation inputs in the decision job', () => {