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', () => {