From d1b5dc043e13db214b3b9fd6cbe0a1ebcca73cd7 Mon Sep 17 00:00:00 2001 From: Kyle Sexton <153232337+kyle-sexton@users.noreply.github.com> Date: Sun, 4 Oct 2026 03:11:40 -0400 Subject: [PATCH 1/6] refactor: rename workflows and actions to the org naming convention Move every mapped ci-workflows workflow and composite action to its standards rename-map path, set each workflow name to its file stem, apply the job-id renames, and point reusables at their own actions with `$/`. The comment-hygiene and machine-specific-paths action directories are standards-synced and stay at their old paths until the sync destinations move. The three -self callers are untouched apart from local reusable paths and are folded in a follow-up commit. Co-Authored-By: Claude Opus 5.5 --- .../action.yml | 0 .../check-action-metadata-filename.sh | 2 +- .../{exec-bit => check-exec-bit}/action.yml | 0 .../check-exec-bit.sh | 0 .../check-exec-bit.test.sh | 0 .../action.yml | 0 .../action.yml | 0 .../run.sh | 0 .../action.yml | 0 .../check-heading-cites.sh | 0 .../action.yml | 0 .../match.sh | 0 .../match.test.sh | 0 .../action.yml | 0 .../{markdown => markdownlint}/action.yml | 0 .../markdownlint-problem-matcher.json | 0 .../aggregate-results}/action.yml | 0 .../aggregate-results}/run.sh | 0 .../aggregate-results}/run.test.sh | 0 .../check-contract}/README.md | 8 +- .../check-contract}/action.yml | 0 .../check-contract}/run.sh | 0 .../check-contract}/run.test.sh | 0 .../Invoke-Pssa.ps1 | 0 .../action.yml | 0 .../action.yml | 0 .../classify.cjs | 0 .../classify.test.cjs | 0 .../outcome-step.test.cjs | 0 .../README.md | 0 .../action.yml | 0 .../contracts/kyle-sexton-github-iac.json | 0 .../guard.sh | 0 .../guard.test.sh | 0 .github/scripts/Invoke-CompositeRunPssa.ps1 | 6 +- .../scripts/Invoke-CompositeRunPssa.test.ps1 | 4 +- .../actionlint-shellcheck-pin.test.cjs | 2 +- .../scripts/checks-markdown-globs.test.cjs | 12 +- .github/scripts/checks-outcome.test.cjs | 45 ++++-- .../scripts/ci-fanout-consolidation.test.cjs | 60 ++++---- .../scripts/claude-lane-compose-args.test.cjs | 2 +- .../scripts/claude-lane-job-gates.test.cjs | 4 +- .github/scripts/claude-lane-scope.test.cjs | 6 +- .../scripts/claude-lane-status-check.test.cjs | 6 +- .../claude-review-declared-outputs.test.cjs | 14 +- .../claude-review-outcome-wiring.test.cjs | 41 +---- .../claude-review-plugin-path.test.cjs | 4 +- .github/scripts/composite-run-shellcheck.sh | 2 +- .../scripts/composite-run-shellcheck.test.sh | 2 +- .github/scripts/go-quality.test.cjs | 14 +- .../scripts/network-timeout-policy.test.cjs | 18 +-- .github/scripts/osv-scan-guard.test.sh | 2 +- .github/scripts/osv-scanner-pin.test.cjs | 2 +- .github/scripts/powershell-action.test.cjs | 2 +- .github/scripts/pulumi-deploy-guard.test.cjs | 6 +- .../standards-sync-app-attestation.test.cjs | 2 +- .../standards-sync-automerge-arm.test.cjs | 2 +- .github/scripts/zizmor-native.test.cjs | 12 +- .github/workflows/claude-review-self.yml | 2 +- .../workflows/claude-security-review-self.yml | 2 +- ...abel.yml => intake-label-needs-triage.yml} | 4 +- ...de-intake-triage.yml => intake-triage.yml} | 12 +- ...ync.yml => maintenance-sync-standards.yml} | 2 +- .../{zizmor.yml => pr-audit-workflows.yml} | 2 +- .../{ci.yml => pr-require-checks.yml} | 140 +++++++++--------- ...rity-review.yml => pr-review-security.yml} | 12 +- .../{claude-review.yml => pr-review.yml} | 12 +- .../{go-quality.yml => pr-run-checks-go.yml} | 6 +- .../{checks.yml => pr-run-checks.yml} | 34 ++--- ...v-scanner.yml => pr-scan-dependencies.yml} | 2 +- .../{release.yml => release-tag.yml} | 4 +- AGENTS.md | 4 +- README.md | 64 ++++---- 73 files changed, 283 insertions(+), 299 deletions(-) rename .github/actions/{action-metadata-filename => check-action-filename}/action.yml (100%) rename .github/actions/{action-metadata-filename => check-action-filename}/check-action-metadata-filename.sh (91%) rename .github/actions/{exec-bit => check-exec-bit}/action.yml (100%) rename .github/actions/{exec-bit => check-exec-bit}/check-exec-bit.sh (100%) rename .github/actions/{exec-bit => check-exec-bit}/check-exec-bit.test.sh (100%) rename .github/actions/{eol-renormalize => check-line-endings}/action.yml (100%) rename .github/actions/{managed-files-guard => check-managed-files}/action.yml (100%) rename .github/actions/{managed-files-guard => check-managed-files}/run.sh (100%) rename .github/actions/{reference-integrity => check-markdown-references}/action.yml (100%) rename .github/actions/{reference-integrity => check-markdown-references}/check-heading-cites.sh (100%) rename .github/actions/{change-detection => detect-changes}/action.yml (100%) rename .github/actions/{change-detection => detect-changes}/match.sh (100%) rename .github/actions/{change-detection => detect-changes}/match.test.sh (100%) rename .github/actions/{editorconfig => editorconfig-checker}/action.yml (100%) rename .github/actions/{markdown => markdownlint}/action.yml (100%) rename .github/actions/{markdown => markdownlint}/markdownlint-problem-matcher.json (100%) rename .github/actions/{ci-status => pr-require-checks/aggregate-results}/action.yml (100%) rename .github/actions/{ci-status => pr-require-checks/aggregate-results}/run.sh (100%) rename .github/actions/{ci-status => pr-require-checks/aggregate-results}/run.test.sh (100%) rename .github/actions/{pr-contract => pr-require-checks/check-contract}/README.md (94%) rename .github/actions/{pr-contract => pr-require-checks/check-contract}/action.yml (100%) rename .github/actions/{pr-contract => pr-require-checks/check-contract}/run.sh (100%) rename .github/actions/{pr-contract => pr-require-checks/check-contract}/run.test.sh (100%) rename .github/actions/{powershell => psscriptanalyzer}/Invoke-Pssa.ps1 (100%) rename .github/actions/{powershell => psscriptanalyzer}/action.yml (100%) rename .github/actions/{claude-lane-outcome => report-lane-outcome}/action.yml (100%) rename .github/actions/{claude-lane-outcome => report-lane-outcome}/classify.cjs (100%) rename .github/actions/{claude-lane-outcome => report-lane-outcome}/classify.test.cjs (100%) rename .github/actions/{claude-lane-outcome => report-lane-outcome}/outcome-step.test.cjs (100%) rename .github/actions/{pulumi-deploy-guard => verify-pulumi-deploy-policy}/README.md (100%) rename .github/actions/{pulumi-deploy-guard => verify-pulumi-deploy-policy}/action.yml (100%) rename .github/actions/{pulumi-deploy-guard => verify-pulumi-deploy-policy}/contracts/kyle-sexton-github-iac.json (100%) rename .github/actions/{pulumi-deploy-guard => verify-pulumi-deploy-policy}/guard.sh (100%) rename .github/actions/{pulumi-deploy-guard => verify-pulumi-deploy-policy}/guard.test.sh (100%) rename .github/workflows/{issue-triage-label.yml => intake-label-needs-triage.yml} (99%) rename .github/workflows/{claude-intake-triage.yml => intake-triage.yml} (98%) rename .github/workflows/{standards-sync.yml => maintenance-sync-standards.yml} (99%) rename .github/workflows/{zizmor.yml => pr-audit-workflows.yml} (99%) rename .github/workflows/{ci.yml => pr-require-checks.yml} (81%) rename .github/workflows/{claude-security-review.yml => pr-review-security.yml} (98%) rename .github/workflows/{claude-review.yml => pr-review.yml} (98%) rename .github/workflows/{go-quality.yml => pr-run-checks-go.yml} (99%) rename .github/workflows/{checks.yml => pr-run-checks.yml} (90%) rename .github/workflows/{osv-scanner.yml => pr-scan-dependencies.yml} (99%) rename .github/workflows/{release.yml => release-tag.yml} (99%) diff --git a/.github/actions/action-metadata-filename/action.yml b/.github/actions/check-action-filename/action.yml similarity index 100% rename from .github/actions/action-metadata-filename/action.yml rename to .github/actions/check-action-filename/action.yml diff --git a/.github/actions/action-metadata-filename/check-action-metadata-filename.sh b/.github/actions/check-action-filename/check-action-metadata-filename.sh similarity index 91% rename from .github/actions/action-metadata-filename/check-action-metadata-filename.sh rename to .github/actions/check-action-filename/check-action-metadata-filename.sh index c04027ca..1290dc41 100755 --- a/.github/actions/action-metadata-filename/check-action-metadata-filename.sh +++ b/.github/actions/check-action-filename/check-action-metadata-filename.sh @@ -1,5 +1,5 @@ #!/usr/bin/env bash -# Every lane here (composite-run-shellcheck.sh, ci.yml's schema step) globs +# Every lane here (composite-run-shellcheck.sh, pr-require-checks.yml's schema step) globs # action.yml only, so a GitHub-legal action.yaml would be skipped silently. set -uo pipefail diff --git a/.github/actions/exec-bit/action.yml b/.github/actions/check-exec-bit/action.yml similarity index 100% rename from .github/actions/exec-bit/action.yml rename to .github/actions/check-exec-bit/action.yml diff --git a/.github/actions/exec-bit/check-exec-bit.sh b/.github/actions/check-exec-bit/check-exec-bit.sh similarity index 100% rename from .github/actions/exec-bit/check-exec-bit.sh rename to .github/actions/check-exec-bit/check-exec-bit.sh diff --git a/.github/actions/exec-bit/check-exec-bit.test.sh b/.github/actions/check-exec-bit/check-exec-bit.test.sh similarity index 100% rename from .github/actions/exec-bit/check-exec-bit.test.sh rename to .github/actions/check-exec-bit/check-exec-bit.test.sh diff --git a/.github/actions/eol-renormalize/action.yml b/.github/actions/check-line-endings/action.yml similarity index 100% rename from .github/actions/eol-renormalize/action.yml rename to .github/actions/check-line-endings/action.yml diff --git a/.github/actions/managed-files-guard/action.yml b/.github/actions/check-managed-files/action.yml similarity index 100% rename from .github/actions/managed-files-guard/action.yml rename to .github/actions/check-managed-files/action.yml diff --git a/.github/actions/managed-files-guard/run.sh b/.github/actions/check-managed-files/run.sh similarity index 100% rename from .github/actions/managed-files-guard/run.sh rename to .github/actions/check-managed-files/run.sh diff --git a/.github/actions/reference-integrity/action.yml b/.github/actions/check-markdown-references/action.yml similarity index 100% rename from .github/actions/reference-integrity/action.yml rename to .github/actions/check-markdown-references/action.yml diff --git a/.github/actions/reference-integrity/check-heading-cites.sh b/.github/actions/check-markdown-references/check-heading-cites.sh similarity index 100% rename from .github/actions/reference-integrity/check-heading-cites.sh rename to .github/actions/check-markdown-references/check-heading-cites.sh diff --git a/.github/actions/change-detection/action.yml b/.github/actions/detect-changes/action.yml similarity index 100% rename from .github/actions/change-detection/action.yml rename to .github/actions/detect-changes/action.yml diff --git a/.github/actions/change-detection/match.sh b/.github/actions/detect-changes/match.sh similarity index 100% rename from .github/actions/change-detection/match.sh rename to .github/actions/detect-changes/match.sh diff --git a/.github/actions/change-detection/match.test.sh b/.github/actions/detect-changes/match.test.sh similarity index 100% rename from .github/actions/change-detection/match.test.sh rename to .github/actions/detect-changes/match.test.sh diff --git a/.github/actions/editorconfig/action.yml b/.github/actions/editorconfig-checker/action.yml similarity index 100% rename from .github/actions/editorconfig/action.yml rename to .github/actions/editorconfig-checker/action.yml diff --git a/.github/actions/markdown/action.yml b/.github/actions/markdownlint/action.yml similarity index 100% rename from .github/actions/markdown/action.yml rename to .github/actions/markdownlint/action.yml diff --git a/.github/actions/markdown/markdownlint-problem-matcher.json b/.github/actions/markdownlint/markdownlint-problem-matcher.json similarity index 100% rename from .github/actions/markdown/markdownlint-problem-matcher.json rename to .github/actions/markdownlint/markdownlint-problem-matcher.json diff --git a/.github/actions/ci-status/action.yml b/.github/actions/pr-require-checks/aggregate-results/action.yml similarity index 100% rename from .github/actions/ci-status/action.yml rename to .github/actions/pr-require-checks/aggregate-results/action.yml diff --git a/.github/actions/ci-status/run.sh b/.github/actions/pr-require-checks/aggregate-results/run.sh similarity index 100% rename from .github/actions/ci-status/run.sh rename to .github/actions/pr-require-checks/aggregate-results/run.sh diff --git a/.github/actions/ci-status/run.test.sh b/.github/actions/pr-require-checks/aggregate-results/run.test.sh similarity index 100% rename from .github/actions/ci-status/run.test.sh rename to .github/actions/pr-require-checks/aggregate-results/run.test.sh diff --git a/.github/actions/pr-contract/README.md b/.github/actions/pr-require-checks/check-contract/README.md similarity index 94% rename from .github/actions/pr-contract/README.md rename to .github/actions/pr-require-checks/check-contract/README.md index a3a92ef7..8ac2f7b5 100644 --- a/.github/actions/pr-contract/README.md +++ b/.github/actions/pr-require-checks/check-contract/README.md @@ -99,13 +99,13 @@ Both steps go in the `ci-status` job, in this order: statuses: write runs-on: ubuntu-24.04 steps: - - uses: melodic-software/ci-workflows/.github/actions/pr-contract@ # vX.Y.Z + - uses: melodic-software/ci-workflows/.github/actions/pr-require-checks/check-contract@ # vX.Y.Z - name: Aggregate lane results # `!cancelled()` so a failing pr-contract step does not skip the # aggregation: the job still fails on the contract step's exit code, and # the ci-lanes status lands on the SHA for the next contract-only run. if: ${{ !cancelled() }} - uses: melodic-software/ci-workflows/.github/actions/ci-status@ # vX.Y.Z + uses: melodic-software/ci-workflows/.github/actions/pr-require-checks/aggregate-results@ # vX.Y.Z with: results: ${{ needs.lane-a.result }} ${{ needs.lane-b.result }} ``` @@ -132,12 +132,12 @@ branches on it before it looks at `same-repo`, so `contract-only: true` with `same-repo: false` is treated as a carry-forward even though the shipped defaults never produce that combination. -This repository's own `.github/workflows/ci.yml` is the reference wiring. +This repository's own `.github/workflows/pr-require-checks.yml` is the reference wiring. ## Tests `run.test.sh` drives `run.sh` against fixture JSON served by a `gh` shim placed first on `PATH`, which also logs every API call so the harness can assert on the writes that did and did not happen. Run it with -`bash .github/actions/pr-contract/run.test.sh`; the `selector-contract` lane runs +`bash .github/actions/pr-require-checks/check-contract/run.test.sh`; the `selector-contract` lane runs it in CI. diff --git a/.github/actions/pr-contract/action.yml b/.github/actions/pr-require-checks/check-contract/action.yml similarity index 100% rename from .github/actions/pr-contract/action.yml rename to .github/actions/pr-require-checks/check-contract/action.yml diff --git a/.github/actions/pr-contract/run.sh b/.github/actions/pr-require-checks/check-contract/run.sh similarity index 100% rename from .github/actions/pr-contract/run.sh rename to .github/actions/pr-require-checks/check-contract/run.sh diff --git a/.github/actions/pr-contract/run.test.sh b/.github/actions/pr-require-checks/check-contract/run.test.sh similarity index 100% rename from .github/actions/pr-contract/run.test.sh rename to .github/actions/pr-require-checks/check-contract/run.test.sh diff --git a/.github/actions/powershell/Invoke-Pssa.ps1 b/.github/actions/psscriptanalyzer/Invoke-Pssa.ps1 similarity index 100% rename from .github/actions/powershell/Invoke-Pssa.ps1 rename to .github/actions/psscriptanalyzer/Invoke-Pssa.ps1 diff --git a/.github/actions/powershell/action.yml b/.github/actions/psscriptanalyzer/action.yml similarity index 100% rename from .github/actions/powershell/action.yml rename to .github/actions/psscriptanalyzer/action.yml diff --git a/.github/actions/claude-lane-outcome/action.yml b/.github/actions/report-lane-outcome/action.yml similarity index 100% rename from .github/actions/claude-lane-outcome/action.yml rename to .github/actions/report-lane-outcome/action.yml diff --git a/.github/actions/claude-lane-outcome/classify.cjs b/.github/actions/report-lane-outcome/classify.cjs similarity index 100% rename from .github/actions/claude-lane-outcome/classify.cjs rename to .github/actions/report-lane-outcome/classify.cjs diff --git a/.github/actions/claude-lane-outcome/classify.test.cjs b/.github/actions/report-lane-outcome/classify.test.cjs similarity index 100% rename from .github/actions/claude-lane-outcome/classify.test.cjs rename to .github/actions/report-lane-outcome/classify.test.cjs diff --git a/.github/actions/claude-lane-outcome/outcome-step.test.cjs b/.github/actions/report-lane-outcome/outcome-step.test.cjs similarity index 100% rename from .github/actions/claude-lane-outcome/outcome-step.test.cjs rename to .github/actions/report-lane-outcome/outcome-step.test.cjs diff --git a/.github/actions/pulumi-deploy-guard/README.md b/.github/actions/verify-pulumi-deploy-policy/README.md similarity index 100% rename from .github/actions/pulumi-deploy-guard/README.md rename to .github/actions/verify-pulumi-deploy-policy/README.md diff --git a/.github/actions/pulumi-deploy-guard/action.yml b/.github/actions/verify-pulumi-deploy-policy/action.yml similarity index 100% rename from .github/actions/pulumi-deploy-guard/action.yml rename to .github/actions/verify-pulumi-deploy-policy/action.yml diff --git a/.github/actions/pulumi-deploy-guard/contracts/kyle-sexton-github-iac.json b/.github/actions/verify-pulumi-deploy-policy/contracts/kyle-sexton-github-iac.json similarity index 100% rename from .github/actions/pulumi-deploy-guard/contracts/kyle-sexton-github-iac.json rename to .github/actions/verify-pulumi-deploy-policy/contracts/kyle-sexton-github-iac.json diff --git a/.github/actions/pulumi-deploy-guard/guard.sh b/.github/actions/verify-pulumi-deploy-policy/guard.sh similarity index 100% rename from .github/actions/pulumi-deploy-guard/guard.sh rename to .github/actions/verify-pulumi-deploy-policy/guard.sh diff --git a/.github/actions/pulumi-deploy-guard/guard.test.sh b/.github/actions/verify-pulumi-deploy-policy/guard.test.sh similarity index 100% rename from .github/actions/pulumi-deploy-guard/guard.test.sh rename to .github/actions/verify-pulumi-deploy-policy/guard.test.sh diff --git a/.github/scripts/Invoke-CompositeRunPssa.ps1 b/.github/scripts/Invoke-CompositeRunPssa.ps1 index 8b755970..06717dfd 100644 --- a/.github/scripts/Invoke-CompositeRunPssa.ps1 +++ b/.github/scripts/Invoke-CompositeRunPssa.ps1 @@ -14,7 +14,7 @@ to bash/sh: expression substitution, line-number mapping, and guards that stop a selector matching nothing from passing as a clean scan. - Analysis itself is delegated to .github/actions/powershell/Invoke-Pssa.ps1 + Analysis itself is delegated to .github/actions/psscriptanalyzer/Invoke-Pssa.ps1 so the repo keeps one analyzer invocation and one pinned version, read here from the `powershell` action's own `analyzer-version` default. @@ -48,8 +48,8 @@ param( Set-StrictMode -Version 3.0 $ErrorActionPreference = 'Stop' -$actionMetadata = '.github/actions/powershell/action.yml' -$runner = '.github/actions/powershell/Invoke-Pssa.ps1' +$actionMetadata = '.github/actions/psscriptanalyzer/action.yml' +$runner = '.github/actions/psscriptanalyzer/Invoke-Pssa.ps1' # The repo's own ruleset, applied unchanged: an embedded block is this repo's # PowerShell and is held to the same policy as a .ps1. The bash/sh sibling diff --git a/.github/scripts/Invoke-CompositeRunPssa.test.ps1 b/.github/scripts/Invoke-CompositeRunPssa.test.ps1 index b14fde47..ce586f61 100644 --- a/.github/scripts/Invoke-CompositeRunPssa.test.ps1 +++ b/.github/scripts/Invoke-CompositeRunPssa.test.ps1 @@ -30,7 +30,7 @@ $customFixture = 'fixtures/composite-action/pwsh-custom-shell/action.yml' $usesOnlyFixture = 'fixtures/composite-action/uses-only/action.yml' $mappingFixture = 'fixtures/composite-action/mapping-steps/action.yml' $noShellFixture = 'fixtures/composite-action/no-shell/action.yml' -$coveredAction = '.github/actions/powershell/action.yml' +$coveredAction = '.github/actions/psscriptanalyzer/action.yml' function Assert-Condition { [CmdletBinding()] @@ -228,7 +228,7 @@ Assert-Condition -Condition ($noShell.Output.Contains('refusing to guess the dia -Message 'The missing shell: was not reported.' ` -Context $noShell.Output -$empty = Invoke-Check -Argument @('.github/workflows/ci.yml') +$empty = Invoke-Check -Argument @('.github/workflows/pr-require-checks.yml') Assert-Condition -Condition ($empty.ExitCode -ne 0) ` -Message 'A file set containing no composite pwsh run: block unexpectedly passed.' ` -Context $empty.Output diff --git a/.github/scripts/actionlint-shellcheck-pin.test.cjs b/.github/scripts/actionlint-shellcheck-pin.test.cjs index da4c1eba..ffe07d76 100644 --- a/.github/scripts/actionlint-shellcheck-pin.test.cjs +++ b/.github/scripts/actionlint-shellcheck-pin.test.cjs @@ -68,7 +68,7 @@ test("actionlint installs the canonical checksum-pinned ShellCheck release", () ); const ciWorkflow = fs.readFileSync( - path.join(workflowsRoot, "ci.yml"), + path.join(workflowsRoot, "pr-require-checks.yml"), "utf8", ); assert.match(ciWorkflow, /actionlint-shellcheck\.txt/u); diff --git a/.github/scripts/checks-markdown-globs.test.cjs b/.github/scripts/checks-markdown-globs.test.cjs index 176ac9de..413a0be6 100644 --- a/.github/scripts/checks-markdown-globs.test.cjs +++ b/.github/scripts/checks-markdown-globs.test.cjs @@ -1,6 +1,6 @@ "use strict"; -// `checks.yml` forwards markdownlint globs. Passing `globs` replaces the +// `pr-run-checks.yml` forwards markdownlint globs. Passing `globs` replaces the // composite default, so the workflow repeats that default and appends // `markdown-extra-globs`. The expression is the GitHub `case`/`format` pair // pinned below; the arguments markdownlint-cli2 actually receives are that @@ -20,11 +20,11 @@ function readRepo(relativePath) { return fs.readFileSync(path.join(repoRoot, relativePath), "utf8"); } -const checks = parseWorkflow(readRepo(".github/workflows/checks.yml")); +const checks = parseWorkflow(readRepo(".github/workflows/pr-run-checks.yml")); const markdownAction = parseWorkflow( - readRepo(".github/actions/markdown/action.yml"), + readRepo(".github/actions/markdownlint/action.yml"), ); -const markdownStep = checks.jobs.checks.steps.find( +const markdownStep = checks.jobs.check.steps.find( (step) => step?.id === "markdown", ); const extraGlobs = checks.on.workflow_call.inputs["markdown-extra-globs"]; @@ -41,7 +41,7 @@ const DOTFILES_EXTRA = [ ".chezmoitemplates/agent-instructions-shared", ].join(" "); -assert.ok(markdownStep !== undefined, "checks.yml has no markdown step"); +assert.ok(markdownStep !== undefined, "pr-run-checks.yml has no markdown step"); assert.ok(lintStep !== undefined, "markdown composite has no glob split"); function forwardedGlobs(extra) { @@ -71,7 +71,7 @@ function wordSplit(value) { } test("markdown-extra-globs is an optional string and defaults empty", () => { - assert.ok(extraGlobs, "checks.yml has no markdown-extra-globs input"); + assert.ok(extraGlobs, "pr-run-checks.yml has no markdown-extra-globs input"); assert.equal(extraGlobs.type, "string"); assert.equal(extraGlobs.default, ""); // Omitted `required` is optional. workflow_call inputs are required only diff --git a/.github/scripts/checks-outcome.test.cjs b/.github/scripts/checks-outcome.test.cjs index 23bbe052..185e04a5 100644 --- a/.github/scripts/checks-outcome.test.cjs +++ b/.github/scripts/checks-outcome.test.cjs @@ -1,6 +1,6 @@ "use strict"; -// The `checks.yml` outcome join decides the consolidated lane's verdict. It is +// The `pr-run-checks.yml` outcome join decides the consolidated lane's verdict. It is // the one place where a bug is silently catastrophic rather than noisy: every // composite runs under `continue-on-error: true`, so a join that forgot a // composite reports success for a run in which that composite failed, and @@ -20,11 +20,11 @@ const { test } = require("node:test"); const { parseWorkflow } = require("./workflow-yaml.cjs"); -const workflowPath = path.join(__dirname, "..", "workflows", "checks.yml"); +const workflowPath = path.join(__dirname, "..", "workflows", "pr-run-checks.yml"); const workflow = parseWorkflow(fs.readFileSync(workflowPath, "utf8")); -const steps = workflow.jobs.checks.steps; +const steps = workflow.jobs.check.steps; const joinStep = steps.find((step) => step?.id === "outcome"); -assert.ok(joinStep !== undefined, "checks.yml has no `outcome` join step"); +assert.ok(joinStep !== undefined, "pr-run-checks.yml has no `outcome` join step"); // Every step the join owns: the composites, plus the input-combination guard // that must go red rather than let an enabled toggle skip silently. @@ -89,17 +89,34 @@ test("the join reads every continue-on-error step", () => { `step ${step.id} is never reported by the join`, ); } + const syncManaged = new Set(["comment-hygiene", "machine-specific-paths"]); + const renamed = { + "change-detection": "detect-changes", + editorconfig: "editorconfig-checker", + markdown: "markdownlint", + "exec-bit": "check-exec-bit", + "eol-renormalize": "check-line-endings", + }; for (const step of composites) { const name = step.id.replaceAll("_", "-"); - assert.match( - String(step.uses ?? ""), - new RegExp( - `^melodic-software/ci-workflows/\\.github/actions/${name}@[0-9a-f]{40}$`, - "u", - ), - // A relative `./` reference inside a CALLED workflow resolves against the - // CALLER's checkout, so it would fail in every consumer. - `step ${step.id} does not reference its composite by pinned full path`, + const uses = String(step.uses ?? ""); + if (syncManaged.has(name)) { + assert.match( + uses, + new RegExp( + `^melodic-software/ci-workflows/\\.github/actions/${name}@[0-9a-f]{40}$`, + "u", + ), + `step ${step.id} does not reference its composite by pinned full path`, + ); + continue; + } + // `$/` resolves against the repository and commit that host the called + // workflow, so it works in every consumer. + assert.equal( + uses, + `$/.github/actions/${renamed[name] ?? name}`, + `step ${step.id} does not reference its composite through the called workflow's own commit`, ); } // The join reports nothing the steps do not produce: an env name left behind @@ -184,7 +201,7 @@ test("an enabled toggle with no configuration fails, it does not skip quietly", // `check-jsonschema-files`; the join is what turns it into the job's verdict, // so a silently skipped schema gate cannot report success. const guard = steps.find((step) => step?.id === "configuration"); - assert.ok(guard !== undefined, "checks.yml has no input-combination guard"); + assert.ok(guard !== undefined, "pr-run-checks.yml has no input-combination guard"); assert.equal( guard.if, `\${{ inputs.check-jsonschema && inputs.check-jsonschema-files == '' }}`, diff --git a/.github/scripts/ci-fanout-consolidation.test.cjs b/.github/scripts/ci-fanout-consolidation.test.cjs index e64424f0..8507dcc3 100644 --- a/.github/scripts/ci-fanout-consolidation.test.cjs +++ b/.github/scripts/ci-fanout-consolidation.test.cjs @@ -10,13 +10,13 @@ const ciWorkflowPath = path.join( repositoryRoot, ".github", "workflows", - "ci.yml", + "pr-require-checks.yml", ); const ciStatusActionPath = path.join( repositoryRoot, ".github", "actions", - "ci-status", + "pr-require-checks/aggregate-results", "action.yml", ); @@ -49,7 +49,7 @@ const CONTRACT_ONLY_PREDICATE = "(github.event.action == 'edited' && !github.event.changes.base))"; const CONTRACT_ONLY_GATE = `!(${CONTRACT_ONLY_PREDICATE})`; -test("ci.yml branches the concurrency group on the contract-only predicate", () => { +test("pr-require-checks.yml branches the concurrency group on the contract-only predicate", () => { // Eviction of a PENDING run is unconditional in GitHub's // queueing and is not governed by `cancel-in-progress`, so a shared group let // one contract-only event evict another and leave the current head with a @@ -62,7 +62,7 @@ test("ci.yml branches the concurrency group on the contract-only predicate", () ); assert.ok( groupBlock !== null, - "ci.yml has no branched concurrency group with a literal cancel-in-progress: true", + "pr-require-checks.yml has no branched concurrency group with a literal cancel-in-progress: true", ); const group = normalizeExpression(groupBlock.groups.value); const branched = @@ -107,7 +107,7 @@ test("the contract-only predicate excludes forks and base changes", () => { ); }); -test("ci.yml re-runs on the contract-only pull_request actions", () => { +test("pr-require-checks.yml re-runs on the contract-only pull_request actions", () => { assert.match( ciWorkflow, /^ {4}types: \[opened, synchronize, reopened, edited, labeled, unlabeled\]$/mu, @@ -187,8 +187,8 @@ test("the ci-status job runs pr-contract before the aggregation", () => { // The carry-forward wait enumerates this workflow's runs on the head SHA. // Without the scope both Actions reads 403 and the wait never engages. assert.match(ciStatusJob, /^ {6}actions: read$/mu); - const contractStep = ciStatusJob.indexOf("./.github/actions/pr-contract"); - const aggregateStep = ciStatusJob.indexOf("./.github/actions/ci-status"); + const contractStep = ciStatusJob.indexOf("./.github/actions/pr-require-checks/check-contract"); + const aggregateStep = ciStatusJob.indexOf("./.github/actions/pr-require-checks/aggregate-results"); assert.ok(contractStep !== -1 && aggregateStep !== -1); assert.ok(contractStep < aggregateStep); // `!cancelled()` so a failing contract step does not skip the status write. @@ -222,19 +222,19 @@ test("the ci-status job runs pr-contract before the aggregation", () => { ); }); -test("ci.yml consolidates the hygiene composites into the checks reusable", () => { +test("pr-require-checks.yml consolidates the hygiene composites into the checks reusable", () => { // This repository dogfoods the `checks` reusable every consumer adopts. - assert.match(ciWorkflow, /^ {2}checks:$/mu); + assert.match(ciWorkflow, /^ {2}pr-run-checks:$/mu); assert.match( ciWorkflow, - /^ {4}uses: \.\/\.github\/workflows\/checks\.yml$/mu, + /^ {4}uses: \.\/\.github\/workflows\/pr-run-checks\.yml$/mu, ); // change-detection reads the PR file listing, and a called workflow cannot // elevate: without the caller's own grant the job fails at startup. const checksJob = ciWorkflow.slice( - ciWorkflow.search(/^ {2}checks:$/mu), - ciWorkflow.search(/^ {2}composites-head:$/mu), + ciWorkflow.search(/^ {2}pr-run-checks:$/mu), + ciWorkflow.search(/^ {2}test-composites-head:$/mu), ); assert.match(checksJob, /^ {6}contents: read$/mu); assert.match(checksJob, /^ {6}pull-requests: read$/mu); @@ -255,10 +255,10 @@ test("ci.yml consolidates the hygiene composites into the checks reusable", () = assert.doesNotMatch(ciWorkflow, new RegExp(`^ {2}${job}:$`, "mu")); } - assert.match(ciWorkflow, /^ {4}needs: \[[^\n]*\bchecks\b[^\n]*\]$/mu); + assert.match(ciWorkflow, /^ {4}needs: \[[^\n]*\bpr-run-checks\b[^\n]*\]$/mu); assert.match( ciWorkflow, - /^ {10}results: [^\n]*\$\{\{ needs\.checks\.result \}\}[^\n]*$/mu, + /^ {10}results: [^\n]*\$\{\{ needs\.pr-run-checks\.result \}\}[^\n]*$/mu, ); for (const lane of [ "hygiene", @@ -285,31 +285,31 @@ test("ci.yml consolidates the hygiene composites into the checks reusable", () = ); }); -test("ci.yml runs the moved composites at HEAD alongside the reusable", () => { - // checks.yml can only reach its composites at a pinned SHA (a relative path +test("pr-require-checks.yml runs the moved composites at HEAD alongside the reusable", () => { + // pr-run-checks.yml can only reach its composites at a pinned SHA (a relative path // inside a called workflow resolves against the caller's checkout), so the // reusable runs the bodies of the release it was pinned at. This job runs the // same bodies from the commit under test; without it a pull request that // breaks one of them passes this repository's own CI. - const start = ciWorkflow.search(/^ {2}composites-head:$/mu); - assert.notEqual(start, -1, "ci.yml has no composites-head job"); + const start = ciWorkflow.search(/^ {2}test-composites-head:$/mu); + assert.notEqual(start, -1, "pr-require-checks.yml has no composites-head job"); const composites = ciWorkflow.slice( start, - ciWorkflow.search(/^ {2}powershell:$/mu), + ciWorkflow.search(/^ {2}psscriptanalyzer:$/mu), ); assert.match(composites, /^ {4}name: Composites at HEAD$/mu); - assert.match(composites, /^ {4}needs: changes$/mu); + assert.match(composites, /^ {4}needs: detect-changes$/mu); // Every composite the reusable moved off HEAD, and only those: actionlint, // shellcheck and check-jsonschema already run at HEAD in their own jobs. for (const composite of [ "typos", "gitleaks", - "editorconfig", - "markdown", - "exec-bit", + "editorconfig-checker", + "markdownlint", + "check-exec-bit", "machine-specific-paths", - "eol-renormalize", + "check-line-endings", "comment-hygiene", "lychee-offline", ]) { @@ -325,11 +325,11 @@ test("ci.yml runs the moved composites at HEAD alongside the reusable", () => { // The lane is only real if the required check aggregates it. assert.match( ciWorkflow, - /^ {4}needs: \[[^\n]*\bcomposites-head\b[^\n]*\]$/mu, + /^ {4}needs: \[[^\n]*\btest-composites-head\b[^\n]*\]$/mu, ); assert.match( ciWorkflow, - /^ {10}results: [^\n]*\$\{\{ needs\.composites-head\.result \}\}[^\n]*$/mu, + /^ {10}results: [^\n]*\$\{\{ needs\.test-composites-head\.result \}\}[^\n]*$/mu, ); }); @@ -380,17 +380,17 @@ test("root CI runs this repository's own test suite in a gating lane", () => { // Newline-anchored at the job-level two-space indent: the change-detection // filter config declares a deeper-indented group with the same name, which a // bare substring search would match first. - const laneStart = ciWorkflow.indexOf("\n selector-contract:"); - const laneEnd = ciWorkflow.indexOf("\n zizmor:"); + const laneStart = ciWorkflow.indexOf("\n test-contracts:"); + const laneEnd = ciWorkflow.indexOf("\n pr-audit-workflows:"); assert.ok( laneStart !== -1 && laneEnd > laneStart, - "ci.yml must declare the test-execution lane before the zizmor lane", + "pr-require-checks.yml must declare the test-execution lane before the zizmor lane", ); const lane = ciWorkflow.slice(laneStart, laneEnd); assert.match(lane, /node --test \.github\/scripts\/\*\.test\.cjs/u); assert.match(lane, /bash \.github\/scripts\/osv-scan-guard\.test\.sh/u); // The required check must aggregate the lane, or a red suite reports green. - assert.match(ciWorkflow, /needs: \[[^\]]*selector-contract[^\]]*\]/u); + assert.match(ciWorkflow, /needs: \[[^\]]*test-contracts[^\]]*\]/u); // And a script-only change must actually start it. assert.match( ciWorkflow, diff --git a/.github/scripts/claude-lane-compose-args.test.cjs b/.github/scripts/claude-lane-compose-args.test.cjs index 7db9ea50..6e08ea24 100644 --- a/.github/scripts/claude-lane-compose-args.test.cjs +++ b/.github/scripts/claude-lane-compose-args.test.cjs @@ -52,7 +52,7 @@ function composeArgs(script, baseArgs, pluginCommand) { } } -for (const fileName of ["claude-review.yml", "claude-security-review.yml"]) { +for (const fileName of ["pr-review.yml", "pr-review-security.yml"]) { const workflow = parseWorkflow( fs.readFileSync(path.join(workflowsDirectory, fileName), "utf8"), ); diff --git a/.github/scripts/claude-lane-job-gates.test.cjs b/.github/scripts/claude-lane-job-gates.test.cjs index 3de7453a..42209364 100644 --- a/.github/scripts/claude-lane-job-gates.test.cjs +++ b/.github/scripts/claude-lane-job-gates.test.cjs @@ -14,8 +14,8 @@ const { parseWorkflow } = require("./workflow-yaml.cjs"); const workflowsDir = path.join(__dirname, "..", "workflows"); for (const { file, job } of [ - { file: "claude-review.yml", job: "review" }, - { file: "claude-security-review.yml", job: "security-review" }, + { file: "pr-review.yml", job: "claude" }, + { file: "pr-review-security.yml", job: "claude" }, ]) { const workflow = parseWorkflow( fs.readFileSync(path.join(workflowsDir, file), "utf8"), diff --git a/.github/scripts/claude-lane-scope.test.cjs b/.github/scripts/claude-lane-scope.test.cjs index 70b9a01e..271deceb 100644 --- a/.github/scripts/claude-lane-scope.test.cjs +++ b/.github/scripts/claude-lane-scope.test.cjs @@ -20,10 +20,10 @@ const AsyncFunction = Object.getPrototypeOf(async () => {}).constructor; const workflowsDir = path.join(__dirname, "..", "workflows"); const lanes = [ - { file: "claude-review.yml", job: "review", lane: "claude-review" }, + { file: "pr-review.yml", job: "claude", lane: "claude-review" }, { - file: "claude-security-review.yml", - job: "security-review", + file: "pr-review-security.yml", + job: "claude", lane: "claude-security-review", }, ]; diff --git a/.github/scripts/claude-lane-status-check.test.cjs b/.github/scripts/claude-lane-status-check.test.cjs index 5f3ddd09..aafaea7d 100644 --- a/.github/scripts/claude-lane-status-check.test.cjs +++ b/.github/scripts/claude-lane-status-check.test.cjs @@ -20,10 +20,10 @@ const { parseWorkflow } = require("./workflow-yaml.cjs"); const workflowsRoot = path.join(__dirname, "..", "workflows"); const lanes = [ - { file: "claude-review.yml", job: "review", name: "claude-review-status" }, + { file: "pr-review.yml", job: "claude", name: "claude-review-status" }, { - file: "claude-security-review.yml", - job: "security-review", + file: "pr-review-security.yml", + job: "claude", name: "security-review", }, ]; diff --git a/.github/scripts/claude-review-declared-outputs.test.cjs b/.github/scripts/claude-review-declared-outputs.test.cjs index b463e800..e93f3f78 100644 --- a/.github/scripts/claude-review-declared-outputs.test.cjs +++ b/.github/scripts/claude-review-declared-outputs.test.cjs @@ -10,7 +10,7 @@ // existence: the `workflow_call.outputs` block, the job-level block that feeds // it, and the agreement between both and the composite that owns the // classification. The composite's corpus -// lives in .github/actions/claude-lane-outcome/classify.test.cjs; what this +// lives in .github/actions/report-lane-outcome/classify.test.cjs; what this // file owns is the wiring that carries its verdict to the caller. const assert = require("node:assert/strict"); @@ -23,14 +23,14 @@ const workflowPath = path.join( repositoryRoot, ".github", "workflows", - "claude-review.yml", + "pr-review.yml", ); const workflow = fs.readFileSync(workflowPath, "utf8"); const compositePath = path.join( repositoryRoot, ".github", "actions", - "claude-lane-outcome", + "report-lane-outcome", "action.yml", ); const composite = fs.readFileSync(compositePath, "utf8"); @@ -67,7 +67,7 @@ function declaredKeys(body, indent) { } const callOutputs = blockBody(callSurface, 4, "outputs"); -const reviewJob = jobBody("review"); +const reviewJob = jobBody("claude"); const jobOutputs = blockBody(reviewJob, 4, "outputs"); test("the caller can read the lane's verdict without reading its log", () => { @@ -86,7 +86,7 @@ test("each declared output is wired to the job that computes it", () => { for (const name of ["review-failed", "failure-class"]) { assert.equal( wiring[name], - `\${{ jobs.review.outputs.${name} }}`, + `\${{ jobs.claude.outputs.${name} }}`, `${name} must read the review job, not a step or a literal`, ); } @@ -117,7 +117,7 @@ test("the forwarded names are the composite's own, so a rename cannot silently e for (const name of ["review-failed", "failure-class"]) { assert.ok( compositeOutputs.includes(name), - `claude-lane-outcome no longer declares ${name}; the workflow output would resolve to empty`, + `report-lane-outcome no longer declares ${name}; the workflow output would resolve to empty`, ); } }); @@ -137,6 +137,6 @@ test("the step the job forwards from is the outcome composite", () => { assert.match(step, /^ {8}id: review-outcome$/mu); assert.match( step, - /^ {8}uses: melodic-software\/ci-workflows\/\.github\/actions\/claude-lane-outcome@[0-9a-f]{40} #/mu, + /^ {8}uses: \$\/\.github\/actions\/report-lane-outcome$/mu, ); }); diff --git a/.github/scripts/claude-review-outcome-wiring.test.cjs b/.github/scripts/claude-review-outcome-wiring.test.cjs index f3e8aebf..060a4ce0 100644 --- a/.github/scripts/claude-review-outcome-wiring.test.cjs +++ b/.github/scripts/claude-review-outcome-wiring.test.cjs @@ -9,14 +9,13 @@ // against the working tree always. const assert = require("node:assert/strict"); -const { execFileSync } = require("node:child_process"); const fs = require("node:fs"); const path = require("node:path"); const test = require("node:test"); const repositoryRoot = path.join(__dirname, "..", ".."); -const COMPOSITE_PATH = ".github/actions/claude-lane-outcome/action.yml"; +const COMPOSITE_PATH = ".github/actions/report-lane-outcome/action.yml"; // Every lane workflow that invokes the outcome composite, discovered by // scanning the workflows directory for its `uses:` reference — a new consumer @@ -29,7 +28,7 @@ const LANES = fs name.endsWith(".yml") && fs .readFileSync(path.join(workflowsDir, name), "utf8") - .includes("/.github/actions/claude-lane-outcome@"), + .includes("uses: $/.github/actions/report-lane-outcome"), ) .sort(); @@ -39,8 +38,8 @@ test("lane discovery finds the outcome composite's consumers", () => { `expected at least the review and security lanes, found: ${LANES.join(", ")}`, ); assert.ok( - LANES.includes("claude-review.yml") && - LANES.includes("claude-security-review.yml"), + LANES.includes("pr-review.yml") && + LANES.includes("pr-review-security.yml"), `known consumers missing from discovery: ${LANES.join(", ")}`, ); }); @@ -86,36 +85,4 @@ for (const lane of LANES) { ); } }); - - test(`${lane}: every consumed outcome output is declared at the pinned revision, when reachable`, (t) => { - // The tree check above cannot see the one divergence that matters: a tree - // that declares an output the PINNED revision predates. Reading the - // composite at the pin closes that hole — but only where the pinned - // object exists locally (a shallow CI checkout does not carry history), - // so an unreachable pin skips rather than fails: this half is a - // local/deep-clone guard, and the tree check above is the floor that - // always runs. - const pin = workflow.match( - /uses: melodic-software\/ci-workflows\/\.github\/actions\/claude-lane-outcome@([0-9a-f]{40})/u, - ); - assert.ok(pin, "the outcome composite must be SHA-pinned"); - let pinnedYaml; - try { - pinnedYaml = execFileSync( - "git", - ["-C", repositoryRoot, "show", `${pin[1]}:${COMPOSITE_PATH}`], - { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }, - ); - } catch { - t.skip(`pinned revision ${pin[1]} is not reachable in this clone`); - return; - } - const declared = declaredOutputs(pinnedYaml); - for (const name of consumed) { - assert.ok( - declared.has(name), - `${lane} reads steps.review-outcome.outputs.${name}, which the composite at pinned ${pin[1]} does not declare — repoint the pin and the gates in the same commit`, - ); - } - }); } diff --git a/.github/scripts/claude-review-plugin-path.test.cjs b/.github/scripts/claude-review-plugin-path.test.cjs index 9b9b46b6..2de69499 100644 --- a/.github/scripts/claude-review-plugin-path.test.cjs +++ b/.github/scripts/claude-review-plugin-path.test.cjs @@ -14,9 +14,9 @@ const { parseWorkflow } = require("./workflow-yaml.cjs"); const repositoryRoot = path.join(__dirname, "..", ".."); for (const { file, defaultCommand } of [ - { file: "claude-review.yml", defaultCommand: "/review:code-review" }, + { file: "pr-review.yml", defaultCommand: "/review:code-review" }, { - file: "claude-security-review.yml", + file: "pr-review-security.yml", defaultCommand: "/review:security-review", }, ]) { diff --git a/.github/scripts/composite-run-shellcheck.sh b/.github/scripts/composite-run-shellcheck.sh index 72a16c6f..1d2c85cf 100644 --- a/.github/scripts/composite-run-shellcheck.sh +++ b/.github/scripts/composite-run-shellcheck.sh @@ -27,7 +27,7 @@ # Run from the repository root; each path must be repo-relative. With no # arguments, every tracked .github/actions/*/action.yml is checked. # -# Requires shellcheck on PATH — the ci.yml lane action installs a pinned, +# Requires shellcheck on PATH — the pr-require-checks.yml lane action installs a pinned, # checksum-verified one — and yq (mikefarah, preinstalled on ubuntu-24.04). # yq is deliberately unpinned: both false-green guards (the independent # `expected` count and the extraction it verifies) read its output, so a diff --git a/.github/scripts/composite-run-shellcheck.test.sh b/.github/scripts/composite-run-shellcheck.test.sh index 895f2588..8b7e39e1 100644 --- a/.github/scripts/composite-run-shellcheck.test.sh +++ b/.github/scripts/composite-run-shellcheck.test.sh @@ -191,7 +191,7 @@ while IFS= read -r action; do done < <(git ls-files -- '.github/actions/*/action.yml') empty="$temp/composite-run-shellcheck-empty.txt" -if bash "$check" .github/workflows/ci.yml >"$empty" 2>&1; then +if bash "$check" .github/workflows/pr-require-checks.yml >"$empty" 2>&1; then echo 'A file set containing no composite run: block unexpectedly passed.' >&2 exit 1 fi diff --git a/.github/scripts/go-quality.test.cjs b/.github/scripts/go-quality.test.cjs index 983b0058..b66c4e17 100644 --- a/.github/scripts/go-quality.test.cjs +++ b/.github/scripts/go-quality.test.cjs @@ -7,11 +7,11 @@ const test = require("node:test"); const root = path.join(__dirname, "..", ".."); const workflow = fs.readFileSync( - path.join(root, ".github", "workflows", "go-quality.yml"), + path.join(root, ".github", "workflows", "pr-run-checks-go.yml"), "utf8", ); const ciWorkflow = fs.readFileSync( - path.join(root, ".github", "workflows", "ci.yml"), + path.join(root, ".github", "workflows", "pr-require-checks.yml"), "utf8", ); const windowsFixtureTest = fs.readFileSync( @@ -47,8 +47,8 @@ test("Go quality exposes only the consumer-owned path contract", () => { }); test("native Linux and Windows analyzer lanes use immutable verified binaries", () => { - assert.match(workflow, /^ {2}linux:[\s\S]*?runs-on: ubuntu-24\.04/mu); - assert.match(workflow, /^ {2}windows:[\s\S]*?runs-on: windows-2025/mu); + assert.match(workflow, /^ {2}check-linux:[\s\S]*?runs-on: ubuntu-24\.04/mu); + assert.match(workflow, /^ {2}check-windows:[\s\S]*?runs-on: windows-2025/mu); assert.equal(occurrences(workflow, /timeout-minutes: 30/gu), 3); // The exact SHA and version tag move with Dependabot bumps; the invariant @@ -111,12 +111,12 @@ test("approved native tests and module hygiene stay blocking", () => { test("this exact workflow head is behaviorally dogfooded on native Windows", () => { assert.match( ciWorkflow, - /^ {2}go-quality-dogfood:[\s\S]*?uses: \.\/\.github\/workflows\/go-quality\.yml[\s\S]*?working-directory: fixtures\/go\/windows-race/mu, + /^ {2}pr-run-checks-go:[\s\S]*?uses: \.\/\.github\/workflows\/pr-run-checks-go\.yml[\s\S]*?working-directory: fixtures\/go\/windows-race/mu, ); - assert.match(ciWorkflow, /^ {4}needs: \[[^\n]*go-quality-dogfood[^\n]*\]$/mu); + assert.match(ciWorkflow, /^ {4}needs: \[[^\n]*pr-run-checks-go[^\n]*\]$/mu); assert.match( ciWorkflow, - /^ {10}results: [^\n]*\$\{\{ needs\.go-quality-dogfood\.result \}\}[^\n]*$/mu, + /^ {10}results: [^\n]*\$\{\{ needs\.pr-run-checks-go\.result \}\}[^\n]*$/mu, ); assert.match(windowsFixtureTest, /^\/\/go:build windows$/mu); assert.match(windowsFixtureTest, /if !raceDetectorEnabled/u); diff --git a/.github/scripts/network-timeout-policy.test.cjs b/.github/scripts/network-timeout-policy.test.cjs index 25a95e98..c74922ba 100644 --- a/.github/scripts/network-timeout-policy.test.cjs +++ b/.github/scripts/network-timeout-policy.test.cjs @@ -24,13 +24,13 @@ test("immutable release assets have a bounded exponential retry budget", () => { // --retry-delay stays banned: a fixed interval would replace the // exponential backoff rather than bound it. for (const [name, content, expected] of [ - ["zizmor", read(".github/workflows/zizmor.yml"), 1], + ["zizmor", read(".github/workflows/pr-audit-workflows.yml"), 1], // The standards-sync reusables download no release asset; their npm // installs carry the equivalent fetch-retry budget, asserted below. - ["standards sync", read(".github/workflows/standards-sync.yml"), 0], + ["standards sync", read(".github/workflows/maintenance-sync-standards.yml"), 0], // Linux (bash) and Windows (pwsh) golangci-lint installs carry the same // budget, hence two occurrences. - ["go quality", read(".github/workflows/go-quality.yml"), 2], + ["go quality", read(".github/workflows/pr-run-checks-go.yml"), 2], ["shared installer", read(".github/actions/_shared/install-release.sh"), 1], ]) { assert.equal( @@ -52,10 +52,10 @@ test("immutable release assets have a bounded exponential retry budget", () => { // The standards-sync path's remaining network dependency is npm: every // engine-dependency install carries the equivalent bounded retry budget. for (const [name, content, expected] of [ - ["standards sync npm", read(".github/workflows/standards-sync.yml"), 2], + ["standards sync npm", read(".github/workflows/maintenance-sync-standards.yml"), 2], [ "managed-files-guard npm", - read(".github/actions/managed-files-guard/action.yml"), + read(".github/actions/check-managed-files/action.yml"), 1, ], ]) { @@ -114,7 +114,7 @@ test("every shared-installer consumer caches its verified release asset", () => } assert.deepEqual(cachedActions.sort(), [ "actionlint", - "editorconfig", + "editorconfig-checker", "gitleaks", "lychee-offline", "shellcheck", @@ -124,7 +124,7 @@ test("every shared-installer consumer caches its verified release asset", () => }); test("OSV native release downloads are bounded", () => { - const workflow = read(".github/workflows/osv-scanner.yml"); + const workflow = read(".github/workflows/pr-scan-dependencies.yml"); assert.equal( occurrences(workflow, /--connect-timeout 10 --max-time 180/gu), 1, @@ -141,7 +141,7 @@ test("OSV native release downloads are bounded", () => { }); test("Pulumi stack export has an explicit freshness boundary", () => { - const guard = read(".github/actions/pulumi-deploy-guard/guard.sh"); + const guard = read(".github/actions/verify-pulumi-deploy-policy/guard.sh"); assert.match(guard, /for attempt in 1 2/u); assert.match( @@ -155,7 +155,7 @@ test("Pulumi stack export has an explicit freshness boundary", () => { }); test("Standards App attestation uses bounded fresh API reads", () => { - const workflow = read(".github/workflows/standards-sync.yml"); + const workflow = read(".github/workflows/maintenance-sync-standards.yml"); assert.match(workflow, /const REQUEST_TIMEOUT_MILLISECONDS = 30_000;/u); assert.equal( diff --git a/.github/scripts/osv-scan-guard.test.sh b/.github/scripts/osv-scan-guard.test.sh index 4dae45fe..92469c85 100644 --- a/.github/scripts/osv-scan-guard.test.sh +++ b/.github/scripts/osv-scan-guard.test.sh @@ -1,7 +1,7 @@ # shellcheck shell=bash set -euo pipefail -workflow="$(cd "$(dirname "${BASH_SOURCE[0]}")/../workflows" && pwd)/osv-scanner.yml" +workflow="$(cd "$(dirname "${BASH_SOURCE[0]}")/../workflows" && pwd)/pr-scan-dependencies.yml" temporary_directory="$(mktemp -d)" trap 'rm -rf -- "$temporary_directory"' EXIT guard="$temporary_directory/guard.sh" diff --git a/.github/scripts/osv-scanner-pin.test.cjs b/.github/scripts/osv-scanner-pin.test.cjs index 35a2781e..21488466 100644 --- a/.github/scripts/osv-scanner-pin.test.cjs +++ b/.github/scripts/osv-scanner-pin.test.cjs @@ -13,7 +13,7 @@ const pin = JSON.parse( ), ); const workflow = fs.readFileSync( - path.join(repositoryRoot, ".github", "workflows", "osv-scanner.yml"), + path.join(repositoryRoot, ".github", "workflows", "pr-scan-dependencies.yml"), "utf8", ); diff --git a/.github/scripts/powershell-action.test.cjs b/.github/scripts/powershell-action.test.cjs index 2b25591a..f73795af 100644 --- a/.github/scripts/powershell-action.test.cjs +++ b/.github/scripts/powershell-action.test.cjs @@ -7,7 +7,7 @@ const test = require("node:test"); const root = path.join(__dirname, "..", ".."); const runner = fs.readFileSync( - path.join(root, ".github", "actions", "powershell", "Invoke-Pssa.ps1"), + path.join(root, ".github", "actions", "psscriptanalyzer", "Invoke-Pssa.ps1"), "utf8", ); const settings = fs.readFileSync( diff --git a/.github/scripts/pulumi-deploy-guard.test.cjs b/.github/scripts/pulumi-deploy-guard.test.cjs index de6ece87..4833421a 100644 --- a/.github/scripts/pulumi-deploy-guard.test.cjs +++ b/.github/scripts/pulumi-deploy-guard.test.cjs @@ -7,11 +7,11 @@ const test = require("node:test"); const root = path.join(__dirname, "..", ".."); const action = fs.readFileSync( - path.join(root, ".github", "actions", "pulumi-deploy-guard", "action.yml"), + path.join(root, ".github", "actions", "verify-pulumi-deploy-policy", "action.yml"), "utf8", ); const guard = fs.readFileSync( - path.join(root, ".github", "actions", "pulumi-deploy-guard", "guard.sh"), + path.join(root, ".github", "actions", "verify-pulumi-deploy-policy", "guard.sh"), "utf8", ); const contract = JSON.parse( @@ -20,7 +20,7 @@ const contract = JSON.parse( root, ".github", "actions", - "pulumi-deploy-guard", + "verify-pulumi-deploy-policy", "contracts", "kyle-sexton-github-iac.json", ), diff --git a/.github/scripts/standards-sync-app-attestation.test.cjs b/.github/scripts/standards-sync-app-attestation.test.cjs index b9a91b56..75c7b43b 100644 --- a/.github/scripts/standards-sync-app-attestation.test.cjs +++ b/.github/scripts/standards-sync-app-attestation.test.cjs @@ -11,7 +11,7 @@ const workflowPath = path.join( __dirname, "..", "workflows", - "standards-sync.yml", + "maintenance-sync-standards.yml", ); const workflow = fs.readFileSync(workflowPath, "utf8"); const { privateKey } = generateKeyPairSync("rsa", { modulusLength: 2048 }); diff --git a/.github/scripts/standards-sync-automerge-arm.test.cjs b/.github/scripts/standards-sync-automerge-arm.test.cjs index 3f2a82e2..f1512b51 100644 --- a/.github/scripts/standards-sync-automerge-arm.test.cjs +++ b/.github/scripts/standards-sync-automerge-arm.test.cjs @@ -10,7 +10,7 @@ const workflowPath = path.join( __dirname, "..", "workflows", - "standards-sync.yml", + "maintenance-sync-standards.yml", ); const workflow = fs.readFileSync(workflowPath, "utf8"); const workflowLines = workflow.split(/\r?\n/u); diff --git a/.github/scripts/zizmor-native.test.cjs b/.github/scripts/zizmor-native.test.cjs index d804cd8a..6e8e7079 100644 --- a/.github/scripts/zizmor-native.test.cjs +++ b/.github/scripts/zizmor-native.test.cjs @@ -7,7 +7,7 @@ const test = require("node:test"); const repositoryRoot = path.join(__dirname, "..", ".."); const workflow = fs.readFileSync( - path.join(repositoryRoot, ".github", "workflows", "zizmor.yml"), + path.join(repositoryRoot, ".github", "workflows", "pr-audit-workflows.yml"), "utf8", ); const readme = fs.readFileSync(path.join(repositoryRoot, "README.md"), "utf8"); @@ -257,9 +257,9 @@ test("zizmor no longer bundles a generated SARIF guard", () => { ); }); -test("the ci.yml zizmor gate is as wide as the lane's audit scope", () => { +test("the pr-require-checks.yml zizmor gate is as wide as the lane's audit scope", () => { const ci = fs.readFileSync( - path.join(repositoryRoot, ".github", "workflows", "ci.yml"), + path.join(repositoryRoot, ".github", "workflows", "pr-require-checks.yml"), "utf8", ); const filter = ci @@ -268,7 +268,7 @@ test("the ci.yml zizmor gate is as wide as the lane's audit scope", () => { assert.match( ci, - /uses: \.\/\.github\/workflows\/zizmor\.yml\n {4}with:\n {6}paths: \./u, + /uses: \.\/\.github\/workflows\/pr-audit-workflows\.yml\n {4}with:\n {6}paths: \./u, "the zizmor lane is expected to audit repo-wide (paths: .)", ); assert.match( @@ -281,8 +281,8 @@ test("the ci.yml zizmor gate is as wide as the lane's audit scope", () => { test("documentation removes only the retired zizmor Docker exception", () => { const zizmorSection = readme.slice( - readme.indexOf("- `.github/workflows/zizmor.yml`"), - readme.indexOf("- `.github/workflows/osv-scanner.yml`"), + readme.indexOf("- `.github/workflows/pr-audit-workflows.yml`"), + readme.indexOf("- `.github/workflows/pr-scan-dependencies.yml`"), ); assert.match(zizmorSection, /verifies its committed SHA-256/u); assert.match(zizmorSection, /without Docker/u); diff --git a/.github/workflows/claude-review-self.yml b/.github/workflows/claude-review-self.yml index 2a8231db..a6a57b4f 100644 --- a/.github/workflows/claude-review-self.yml +++ b/.github/workflows/claude-review-self.yml @@ -20,7 +20,7 @@ jobs: contents: read pull-requests: write id-token: write - uses: ./.github/workflows/claude-review.yml + uses: ./.github/workflows/pr-review.yml # Pass only the one named secret (least privilege) rather than `secrets: # inherit`, which would forward every parent secret to the called workflow. secrets: diff --git a/.github/workflows/claude-security-review-self.yml b/.github/workflows/claude-security-review-self.yml index bb9a4469..72a6f39f 100644 --- a/.github/workflows/claude-security-review-self.yml +++ b/.github/workflows/claude-security-review-self.yml @@ -21,7 +21,7 @@ jobs: contents: read pull-requests: write id-token: write - uses: ./.github/workflows/claude-security-review.yml + uses: ./.github/workflows/pr-review-security.yml # Pass only the one named secret (least privilege) rather than `secrets: # inherit`, which would forward every parent secret to the called workflow. secrets: diff --git a/.github/workflows/issue-triage-label.yml b/.github/workflows/intake-label-needs-triage.yml similarity index 99% rename from .github/workflows/issue-triage-label.yml rename to .github/workflows/intake-label-needs-triage.yml index a5089690..a0a7e390 100644 --- a/.github/workflows/issue-triage-label.yml +++ b/.github/workflows/intake-label-needs-triage.yml @@ -1,4 +1,4 @@ -name: issue-triage-label +name: intake-label-needs-triage # Reusable workflow — applies a configured floor label (default bare # `needs-triage`) to an issue that carries no label matching a configured @@ -61,7 +61,7 @@ on: permissions: {} jobs: - issue-triage-label: + apply-label: name: Apply floor label when no priority tier is present runs-on: ${{ inputs.runner }} timeout-minutes: 5 diff --git a/.github/workflows/claude-intake-triage.yml b/.github/workflows/intake-triage.yml similarity index 98% rename from .github/workflows/claude-intake-triage.yml rename to .github/workflows/intake-triage.yml index 6e75634f..034f9747 100644 --- a/.github/workflows/claude-intake-triage.yml +++ b/.github/workflows/intake-triage.yml @@ -1,4 +1,4 @@ -name: claude-intake-triage +name: intake-triage # Reusable workflow: the intake-triage lane. A read-only Claude run reads one # newly opened issue and returns labels and a comment as structured output; a @@ -18,7 +18,7 @@ name: claude-intake-triage # permissions: # contents: read # check out the default branch for AGENTS.md # issues: write # apply labels and post the triage comment -# uses: melodic-software/ci-workflows/.github/workflows/claude-intake-triage.yml@ +# uses: melodic-software/ci-workflows/.github/workflows/intake-triage.yml@ # secrets: # CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} # @@ -105,12 +105,12 @@ on: description: >- 'true' on an infrastructure failure, else 'false'. Empty when the lane did not run. - value: ${{ jobs.intake-triage.outputs.triage-failed }} + value: ${{ jobs.claude.outputs.triage-failed }} failure-class: description: >- auth | rate-limit | overloaded | other on a failure; skipped-validation when the action skipped itself; empty otherwise. - value: ${{ jobs.intake-triage.outputs.failure-class }} + value: ${{ jobs.claude.outputs.failure-class }} secrets: CLAUDE_CODE_OAUTH_TOKEN: description: Claude Code OAuth token (from `claude setup-token`). @@ -120,7 +120,7 @@ permissions: contents: read jobs: - intake-triage: + claude: name: claude-intake-triage-status runs-on: ${{ inputs.runner }} timeout-minutes: 12 @@ -271,7 +271,7 @@ jobs: - name: Report triage outcome id: review-outcome - uses: melodic-software/ci-workflows/.github/actions/claude-lane-outcome@ac062650c46005edb4787aff378347746bf63804 # v0.27.0 + uses: $/.github/actions/report-lane-outcome with: outcome: ${{ steps.claude.outcome }} execution-file: ${{ steps.claude.outputs.execution_file }} diff --git a/.github/workflows/standards-sync.yml b/.github/workflows/maintenance-sync-standards.yml similarity index 99% rename from .github/workflows/standards-sync.yml rename to .github/workflows/maintenance-sync-standards.yml index 631fb8a4..118cc8a1 100644 --- a/.github/workflows/standards-sync.yml +++ b/.github/workflows/maintenance-sync-standards.yml @@ -1,4 +1,4 @@ -name: standards-sync +name: maintenance-sync-standards # Reusable workflow for exact-file standards distribution. The standards # checkout owns the schema-v2 manifest and its interpreter; this workflow owns diff --git a/.github/workflows/zizmor.yml b/.github/workflows/pr-audit-workflows.yml similarity index 99% rename from .github/workflows/zizmor.yml rename to .github/workflows/pr-audit-workflows.yml index 92e08101..889f7261 100644 --- a/.github/workflows/zizmor.yml +++ b/.github/workflows/pr-audit-workflows.yml @@ -1,4 +1,4 @@ -name: zizmor +name: pr-audit-workflows # Reusable workflow — GitHub Actions security/static analysis with zizmor # (supply-chain risks, dangerous triggers, excessive permissions, template diff --git a/.github/workflows/ci.yml b/.github/workflows/pr-require-checks.yml similarity index 81% rename from .github/workflows/ci.yml rename to .github/workflows/pr-require-checks.yml index 8a9a7e72..5d101ecf 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/pr-require-checks.yml @@ -1,4 +1,4 @@ -name: ci +name: pr-require-checks on: push: @@ -45,7 +45,7 @@ concurrency: # detection output runs the lane rather than skipping it, and `ci-status` # aggregates `changes` so a broken detection goes red. jobs: - changes: + detect-changes: if: ${{ !(github.event.pull_request.head.repo.full_name == github.repository && (contains(fromJSON('["labeled","unlabeled"]'), github.event.action) || (github.event.action == 'edited' && !github.event.changes.base))) }} runs-on: ubuntu-24.04 timeout-minutes: 5 @@ -61,7 +61,7 @@ jobs: persist-credentials: false - name: Detect relevant lanes id: detect - uses: ./.github/actions/change-detection + uses: ./.github/actions/detect-changes with: # Every group includes `.github/**`, so any workflow, action, or # bundled script change re-runs every scoped lane. @@ -131,16 +131,16 @@ jobs: fixtures/composite-action/** **/action.yml - # A local reusable-workflow reference executes checks.yml from this exact + # A local reusable-workflow reference executes pr-run-checks.yml from this exact # commit, so the dogfood proves the contract a consumer pins by tag. - checks: + pr-run-checks: if: ${{ !(github.event.pull_request.head.repo.full_name == github.repository && (contains(fromJSON('["labeled","unlabeled"]'), github.event.action) || (github.event.action == 'edited' && !github.event.changes.base))) }} permissions: contents: read # change-detection lists the pull request's files. A called workflow - # cannot elevate, so the grant has to be here as well as in checks.yml. + # cannot elevate, so the grant has to be here as well as in pr-run-checks.yml. pull-requests: read - uses: ./.github/workflows/checks.yml + uses: ./.github/workflows/pr-run-checks.yml with: # This public repository's hosted lane is free; private consumers pass # their hardcoded fleet label instead. @@ -167,14 +167,14 @@ jobs: check-jsonschema: .github/** - # checks.yml reaches its composites only at a pinned SHA, so a pull request + # pr-run-checks.yml reaches its composites only at a pinned SHA, so a pull request # breaking a composite body would pass there; this job runs them from this # commit. actionlint, shellcheck and check-jsonschema already run at HEAD in # their own jobs. Gated on `actionlint`, the narrowest group naming composites. - composites-head: + test-composites-head: name: Composites at HEAD - needs: changes - if: ${{ !(github.event.pull_request.head.repo.full_name == github.repository && (contains(fromJSON('["labeled","unlabeled"]'), github.event.action) || (github.event.action == 'edited' && !github.event.changes.base))) && !cancelled() && fromJSON(needs.changes.outputs.results || '{}')['actionlint'] != 'false' }} + needs: detect-changes + if: ${{ !(github.event.pull_request.head.repo.full_name == github.repository && (contains(fromJSON('["labeled","unlabeled"]'), github.event.action) || (github.event.action == 'edited' && !github.event.changes.base))) && !cancelled() && fromJSON(needs.detect-changes.outputs.results || '{}')['actionlint'] != 'false' }} runs-on: ubuntu-24.04 timeout-minutes: 15 steps: @@ -187,18 +187,18 @@ jobs: - name: Scan for secrets uses: ./.github/actions/gitleaks - name: Check editorconfig conformance - uses: ./.github/actions/editorconfig + uses: ./.github/actions/editorconfig-checker - name: Lint markdown - uses: ./.github/actions/markdown + uses: ./.github/actions/markdownlint - name: Verify shebang files are executable - uses: ./.github/actions/exec-bit + uses: ./.github/actions/check-exec-bit - name: Check for machine-specific paths # Same self-exclusion the `checks` caller passes. uses: ./.github/actions/machine-specific-paths with: exclude: ':(exclude).github/actions/machine-specific-paths/**' - name: Check index-level EOL drift - uses: ./.github/actions/eol-renormalize + uses: ./.github/actions/check-line-endings - name: Scan comment hygiene uses: ./.github/actions/comment-hygiene with: @@ -206,9 +206,9 @@ jobs: - name: Check links and anchors (offline) uses: ./.github/actions/lychee-offline - powershell: - needs: changes - if: ${{ !(github.event.pull_request.head.repo.full_name == github.repository && (contains(fromJSON('["labeled","unlabeled"]'), github.event.action) || (github.event.action == 'edited' && !github.event.changes.base))) && !cancelled() && fromJSON(needs.changes.outputs.results || '{}')['powershell'] != 'false' }} + psscriptanalyzer: + needs: detect-changes + if: ${{ !(github.event.pull_request.head.repo.full_name == github.repository && (contains(fromJSON('["labeled","unlabeled"]'), github.event.action) || (github.event.action == 'edited' && !github.event.changes.base))) && !cancelled() && fromJSON(needs.detect-changes.outputs.results || '{}')['powershell'] != 'false' }} runs-on: ubuntu-24.04 timeout-minutes: 15 steps: @@ -217,7 +217,7 @@ jobs: with: persist-credentials: false - name: Analyze PowerShell - uses: ./.github/actions/powershell + uses: ./.github/actions/psscriptanalyzer - name: Analyze composite action PowerShell # pwsh embedded in composite metadata is invisible to the lane action # and to actionlint; the lane action installed the pinned analyzer. @@ -225,9 +225,9 @@ jobs: - name: Prove a broken composite pwsh block fails run: pwsh -File .github/scripts/Invoke-CompositeRunPssa.test.ps1 - reference-integrity: - needs: changes - if: ${{ !(github.event.pull_request.head.repo.full_name == github.repository && (contains(fromJSON('["labeled","unlabeled"]'), github.event.action) || (github.event.action == 'edited' && !github.event.changes.base))) && !cancelled() && fromJSON(needs.changes.outputs.results || '{}')['reference-integrity'] != 'false' }} + check-markdown-references: + needs: detect-changes + if: ${{ !(github.event.pull_request.head.repo.full_name == github.repository && (contains(fromJSON('["labeled","unlabeled"]'), github.event.action) || (github.event.action == 'edited' && !github.event.changes.base))) && !cancelled() && fromJSON(needs.detect-changes.outputs.results || '{}')['reference-integrity'] != 'false' }} runs-on: ubuntu-24.04 timeout-minutes: 15 steps: @@ -238,13 +238,13 @@ jobs: - name: Resolve heading citations # Scope to the fixture corpus: the repo's own docs use no `file.md` # "Anchor" prose-cite convention, so the fixture is what exercises it. - uses: ./.github/actions/reference-integrity + uses: ./.github/actions/check-markdown-references with: globs: fixtures/reference-integrity/*.md ruff: - needs: changes - if: ${{ !(github.event.pull_request.head.repo.full_name == github.repository && (contains(fromJSON('["labeled","unlabeled"]'), github.event.action) || (github.event.action == 'edited' && !github.event.changes.base))) && !cancelled() && fromJSON(needs.changes.outputs.results || '{}')['python'] != 'false' }} + needs: detect-changes + if: ${{ !(github.event.pull_request.head.repo.full_name == github.repository && (contains(fromJSON('["labeled","unlabeled"]'), github.event.action) || (github.event.action == 'edited' && !github.event.changes.base))) && !cancelled() && fromJSON(needs.detect-changes.outputs.results || '{}')['python'] != 'false' }} runs-on: ubuntu-24.04 timeout-minutes: 15 steps: @@ -259,8 +259,8 @@ jobs: config: fixtures/python/good/ruff.toml pyright: - needs: changes - if: ${{ !(github.event.pull_request.head.repo.full_name == github.repository && (contains(fromJSON('["labeled","unlabeled"]'), github.event.action) || (github.event.action == 'edited' && !github.event.changes.base))) && !cancelled() && fromJSON(needs.changes.outputs.results || '{}')['python'] != 'false' }} + needs: detect-changes + if: ${{ !(github.event.pull_request.head.repo.full_name == github.repository && (contains(fromJSON('["labeled","unlabeled"]'), github.event.action) || (github.event.action == 'edited' && !github.event.changes.base))) && !cancelled() && fromJSON(needs.detect-changes.outputs.results || '{}')['python'] != 'false' }} runs-on: ubuntu-24.04 timeout-minutes: 15 steps: @@ -274,8 +274,8 @@ jobs: config: fixtures/python/good/pyrightconfig.json biome: - needs: changes - if: ${{ !(github.event.pull_request.head.repo.full_name == github.repository && (contains(fromJSON('["labeled","unlabeled"]'), github.event.action) || (github.event.action == 'edited' && !github.event.changes.base))) && !cancelled() && fromJSON(needs.changes.outputs.results || '{}')['typescript'] != 'false' }} + needs: detect-changes + if: ${{ !(github.event.pull_request.head.repo.full_name == github.repository && (contains(fromJSON('["labeled","unlabeled"]'), github.event.action) || (github.event.action == 'edited' && !github.event.changes.base))) && !cancelled() && fromJSON(needs.detect-changes.outputs.results || '{}')['typescript'] != 'false' }} runs-on: ubuntu-24.04 timeout-minutes: 15 steps: @@ -292,8 +292,8 @@ jobs: config: fixtures/typescript/good/biome.json dotnet-build: - needs: changes - if: ${{ !(github.event.pull_request.head.repo.full_name == github.repository && (contains(fromJSON('["labeled","unlabeled"]'), github.event.action) || (github.event.action == 'edited' && !github.event.changes.base))) && !cancelled() && fromJSON(needs.changes.outputs.results || '{}')['dotnet'] != 'false' }} + needs: detect-changes + if: ${{ !(github.event.pull_request.head.repo.full_name == github.repository && (contains(fromJSON('["labeled","unlabeled"]'), github.event.action) || (github.event.action == 'edited' && !github.event.changes.base))) && !cancelled() && fromJSON(needs.detect-changes.outputs.results || '{}')['dotnet'] != 'false' }} runs-on: ubuntu-24.04 timeout-minutes: 15 steps: @@ -309,8 +309,8 @@ jobs: project: fixtures/dotnet/good/good.csproj dotnet-format: - needs: changes - if: ${{ !(github.event.pull_request.head.repo.full_name == github.repository && (contains(fromJSON('["labeled","unlabeled"]'), github.event.action) || (github.event.action == 'edited' && !github.event.changes.base))) && !cancelled() && fromJSON(needs.changes.outputs.results || '{}')['dotnet'] != 'false' }} + needs: detect-changes + if: ${{ !(github.event.pull_request.head.repo.full_name == github.repository && (contains(fromJSON('["labeled","unlabeled"]'), github.event.action) || (github.event.action == 'edited' && !github.event.changes.base))) && !cancelled() && fromJSON(needs.detect-changes.outputs.results || '{}')['dotnet'] != 'false' }} runs-on: ubuntu-24.04 timeout-minutes: 15 steps: @@ -324,8 +324,8 @@ jobs: project: fixtures/dotnet/good/good.csproj actionlint: - needs: changes - if: ${{ !(github.event.pull_request.head.repo.full_name == github.repository && (contains(fromJSON('["labeled","unlabeled"]'), github.event.action) || (github.event.action == 'edited' && !github.event.changes.base))) && !cancelled() && fromJSON(needs.changes.outputs.results || '{}')['actionlint'] != 'false' }} + needs: detect-changes + if: ${{ !(github.event.pull_request.head.repo.full_name == github.repository && (contains(fromJSON('["labeled","unlabeled"]'), github.event.action) || (github.event.action == 'edited' && !github.event.changes.base))) && !cancelled() && fromJSON(needs.detect-changes.outputs.results || '{}')['actionlint'] != 'false' }} runs-on: ubuntu-24.04 timeout-minutes: 15 steps: @@ -364,8 +364,8 @@ jobs: run: bash .github/scripts/composite-run-shellcheck.test.sh jsonschema: - needs: changes - if: ${{ !(github.event.pull_request.head.repo.full_name == github.repository && (contains(fromJSON('["labeled","unlabeled"]'), github.event.action) || (github.event.action == 'edited' && !github.event.changes.base))) && !cancelled() && fromJSON(needs.changes.outputs.results || '{}')['jsonschema'] != 'false' }} + needs: detect-changes + if: ${{ !(github.event.pull_request.head.repo.full_name == github.repository && (contains(fromJSON('["labeled","unlabeled"]'), github.event.action) || (github.event.action == 'edited' && !github.event.changes.base))) && !cancelled() && fromJSON(needs.detect-changes.outputs.results || '{}')['jsonschema'] != 'false' }} runs-on: ubuntu-24.04 timeout-minutes: 15 steps: @@ -386,7 +386,7 @@ jobs: builtin-schema: vendor.github-actions files: .github/actions/*/action.yml - action-metadata-filename: + check-action-filename: if: ${{ !(github.event.pull_request.head.repo.full_name == github.repository && (contains(fromJSON('["labeled","unlabeled"]'), github.event.action) || (github.event.action == 'edited' && !github.event.changes.base))) }} runs-on: ubuntu-24.04 timeout-minutes: 15 @@ -396,11 +396,11 @@ jobs: with: persist-credentials: false - name: Reject action.yaml metadata (action.yml is enforced exclusively) - uses: ./.github/actions/action-metadata-filename + uses: ./.github/actions/check-action-filename # Advisory (#208): reject hand-edits of sync-manifest-managed files. Not in # ci-status yet: promote after a clean soak. - managed-files-guard: + check-managed-files: if: ${{ !(github.event.pull_request.head.repo.full_name == github.repository && (contains(fromJSON('["labeled","unlabeled"]'), github.event.action) || (github.event.action == 'edited' && !github.event.changes.base))) && github.event_name == 'pull_request' }} runs-on: ubuntu-24.04 timeout-minutes: 10 @@ -411,13 +411,13 @@ jobs: persist-credentials: false fetch-depth: 0 - name: Guard managed files - uses: ./.github/actions/managed-files-guard + uses: ./.github/actions/check-managed-files with: standards-ref: c4d865bb123f476cf1cb36d8f8a1ffb588204f00 shellcheck: - needs: changes - if: ${{ !(github.event.pull_request.head.repo.full_name == github.repository && (contains(fromJSON('["labeled","unlabeled"]'), github.event.action) || (github.event.action == 'edited' && !github.event.changes.base))) && !cancelled() && fromJSON(needs.changes.outputs.results || '{}')['shell'] != 'false' }} + needs: detect-changes + if: ${{ !(github.event.pull_request.head.repo.full_name == github.repository && (contains(fromJSON('["labeled","unlabeled"]'), github.event.action) || (github.event.action == 'edited' && !github.event.changes.base))) && !cancelled() && fromJSON(needs.detect-changes.outputs.results || '{}')['shell'] != 'false' }} runs-on: ubuntu-24.04 timeout-minutes: 15 steps: @@ -430,11 +430,11 @@ jobs: - name: Test ShellCheck discovery and scoped exclusions run: bash .github/actions/shellcheck/run.test.sh - name: Test exec-bit shebang mode scan - run: bash .github/actions/exec-bit/check-exec-bit.test.sh + run: bash .github/actions/check-exec-bit/check-exec-bit.test.sh shfmt: - needs: changes - if: ${{ !(github.event.pull_request.head.repo.full_name == github.repository && (contains(fromJSON('["labeled","unlabeled"]'), github.event.action) || (github.event.action == 'edited' && !github.event.changes.base))) && !cancelled() && fromJSON(needs.changes.outputs.results || '{}')['shell'] != 'false' }} + needs: detect-changes + if: ${{ !(github.event.pull_request.head.repo.full_name == github.repository && (contains(fromJSON('["labeled","unlabeled"]'), github.event.action) || (github.event.action == 'edited' && !github.event.changes.base))) && !cancelled() && fromJSON(needs.detect-changes.outputs.results || '{}')['shell'] != 'false' }} runs-on: ubuntu-24.04 timeout-minutes: 15 steps: @@ -447,13 +447,13 @@ jobs: # boundary cannot bypass the required formatter. uses: ./.github/actions/shfmt with: - paths: fixtures/shell/good .github/scripts .github/actions/pulumi-deploy-guard .github/actions/change-detection .github/actions/pr-contract .github/actions/ci-status .github/actions/exec-bit .github/actions/shfmt + paths: fixtures/shell/good .github/scripts .github/actions/verify-pulumi-deploy-policy .github/actions/detect-changes .github/actions/pr-require-checks/check-contract .github/actions/pr-require-checks/aggregate-results .github/actions/check-exec-bit .github/actions/shfmt - name: Test shfmt discovery and fan-out run: bash .github/actions/shfmt/run.test.sh - selector-contract: - needs: changes - if: ${{ !(github.event.pull_request.head.repo.full_name == github.repository && (contains(fromJSON('["labeled","unlabeled"]'), github.event.action) || (github.event.action == 'edited' && !github.event.changes.base))) && !cancelled() && fromJSON(needs.changes.outputs.results || '{}')['selector-contract'] != 'false' }} + test-contracts: + needs: detect-changes + if: ${{ !(github.event.pull_request.head.repo.full_name == github.repository && (contains(fromJSON('["labeled","unlabeled"]'), github.event.action) || (github.event.action == 'edited' && !github.event.changes.base))) && !cancelled() && fromJSON(needs.detect-changes.outputs.results || '{}')['selector-contract'] != 'false' }} runs-on: ubuntu-24.04 timeout-minutes: 15 steps: @@ -471,9 +471,9 @@ jobs: - name: Test selector and generated workflow contracts run: node --test .github/scripts/*.test.cjs - name: Test change-detection matcher - run: bash .github/actions/change-detection/match.test.sh + run: bash .github/actions/detect-changes/match.test.sh - name: Test Claude lane outcome classifier - run: node --test .github/actions/claude-lane-outcome/*.test.cjs + run: node --test .github/actions/report-lane-outcome/*.test.cjs - name: Test OSV scan policy guard run: bash .github/scripts/osv-scan-guard.test.sh - name: Test Gitleaks scan policy guard @@ -489,35 +489,35 @@ jobs: - name: Test comment-hygiene scan-tree collection contracts run: bash .github/actions/comment-hygiene/scan-tree.test.sh - name: Test the pull-request contract runner - run: bash .github/actions/pr-contract/run.test.sh + run: bash .github/actions/pr-require-checks/check-contract/run.test.sh - name: Test the ci-status aggregation and carry-forward runner - run: bash .github/actions/ci-status/run.test.sh + run: bash .github/actions/pr-require-checks/aggregate-results/run.test.sh - name: Test Pulumi deployment guard - run: bash .github/actions/pulumi-deploy-guard/guard.test.sh + run: bash .github/actions/verify-pulumi-deploy-policy/guard.test.sh - name: Test govulncheck SARIF classification run: bash .github/scripts/govulncheck-sarif-guard.test.sh - zizmor: - needs: changes - if: ${{ !(github.event.pull_request.head.repo.full_name == github.repository && (contains(fromJSON('["labeled","unlabeled"]'), github.event.action) || (github.event.action == 'edited' && !github.event.changes.base))) && !cancelled() && fromJSON(needs.changes.outputs.results || '{}')['zizmor'] != 'false' }} + pr-audit-workflows: + needs: detect-changes + if: ${{ !(github.event.pull_request.head.repo.full_name == github.repository && (contains(fromJSON('["labeled","unlabeled"]'), github.event.action) || (github.event.action == 'edited' && !github.event.changes.base))) && !cancelled() && fromJSON(needs.detect-changes.outputs.results || '{}')['zizmor'] != 'false' }} permissions: contents: read - # zizmor.yml always declares this and a called workflow cannot elevate, + # pr-audit-workflows.yml always declares this and a called workflow cannot elevate, # so grant it although the dogfood stays annotations-only. security-events: write # Audit this repo's own workflows AND composite actions (paths: .) — strong # dogfood. Advisory: findings annotate without blocking ci-status. - uses: ./.github/workflows/zizmor.yml + uses: ./.github/workflows/pr-audit-workflows.yml with: paths: . - osv-scanner: + pr-scan-dependencies: # Not change-gated: a filter enumerating every lockfile format OSV # discovers would drift and skip an unlisted new one. if: ${{ !(github.event.pull_request.head.repo.full_name == github.repository && (contains(fromJSON('["labeled","unlabeled"]'), github.event.action) || (github.event.action == 'edited' && !github.event.changes.base))) }} permissions: contents: read - uses: ./.github/workflows/osv-scanner.yml + uses: ./.github/workflows/pr-scan-dependencies.yml with: # This public repository's hosted lane is free; private consumers must pass # their strict selector output instead. @@ -526,14 +526,14 @@ jobs: # without inventing a fixture manifest. allow-no-lockfiles: true - go-quality-dogfood: - needs: changes - if: ${{ !(github.event.pull_request.head.repo.full_name == github.repository && (contains(fromJSON('["labeled","unlabeled"]'), github.event.action) || (github.event.action == 'edited' && !github.event.changes.base))) && !cancelled() && fromJSON(needs.changes.outputs.results || '{}')['go'] != 'false' }} + pr-run-checks-go: + needs: detect-changes + if: ${{ !(github.event.pull_request.head.repo.full_name == github.repository && (contains(fromJSON('["labeled","unlabeled"]'), github.event.action) || (github.event.action == 'edited' && !github.event.changes.base))) && !cancelled() && fromJSON(needs.detect-changes.outputs.results || '{}')['go'] != 'false' }} permissions: contents: read # The fixture asserts on Windows that the test binary was built with the # race detector: behavioral evidence, not a source-text contract alone. - uses: ./.github/workflows/go-quality.yml + uses: ./.github/workflows/pr-run-checks-go.yml with: config: .golangci.yml working-directory: fixtures/go/windows-race @@ -543,7 +543,7 @@ jobs: if: always() # `changes` is aggregated so a failed detection fails ci-status even though # every gated lane fails open and runs. - needs: [changes, checks, composites-head, powershell, reference-integrity, ruff, pyright, biome, dotnet-build, dotnet-format, shellcheck, shfmt, selector-contract, actionlint, jsonschema, action-metadata-filename, go-quality-dogfood, zizmor, osv-scanner] + needs: [detect-changes, pr-run-checks, test-composites-head, psscriptanalyzer, check-markdown-references, ruff, pyright, biome, dotnet-build, dotnet-format, shellcheck, shfmt, test-contracts, actionlint, jsonschema, check-action-filename, pr-run-checks-go, pr-audit-workflows, pr-scan-dependencies] runs-on: ubuntu-24.04 timeout-minutes: 15 permissions: @@ -561,7 +561,7 @@ jobs: with: persist-credentials: false - name: Check the pull-request contract - uses: ./.github/actions/pr-contract + uses: ./.github/actions/pr-require-checks/check-contract with: # Dependabot pull requests link no issue. exempt-authors: 'dependabot[bot]' @@ -569,9 +569,9 @@ jobs: # `!cancelled()` so a failing pr-contract step does not skip the # `ci-lanes` status write the next contract-only run needs. if: ${{ !cancelled() }} - uses: ./.github/actions/ci-status + uses: ./.github/actions/pr-require-checks/aggregate-results with: # `timeout-minutes * 60 - 60`, sized per README.md. Keep the 60 s # margin: a job timeout would preempt the fail-closed error. carry-forward-wait-seconds: '840' - results: ${{ needs.changes.result }} ${{ needs.checks.result }} ${{ needs.composites-head.result }} ${{ needs.powershell.result }} ${{ needs.reference-integrity.result }} ${{ needs.ruff.result }} ${{ needs.pyright.result }} ${{ needs.biome.result }} ${{ needs.dotnet-build.result }} ${{ needs.dotnet-format.result }} ${{ needs.shellcheck.result }} ${{ needs.shfmt.result }} ${{ needs.selector-contract.result }} ${{ needs.actionlint.result }} ${{ needs.jsonschema.result }} ${{ needs.action-metadata-filename.result }} ${{ needs.go-quality-dogfood.result }} ${{ needs.zizmor.result }} ${{ needs.osv-scanner.result }} + results: ${{ needs.detect-changes.result }} ${{ needs.pr-run-checks.result }} ${{ needs.test-composites-head.result }} ${{ needs.psscriptanalyzer.result }} ${{ needs.check-markdown-references.result }} ${{ needs.ruff.result }} ${{ needs.pyright.result }} ${{ needs.biome.result }} ${{ needs.dotnet-build.result }} ${{ needs.dotnet-format.result }} ${{ needs.shellcheck.result }} ${{ needs.shfmt.result }} ${{ needs.test-contracts.result }} ${{ needs.actionlint.result }} ${{ needs.jsonschema.result }} ${{ needs.check-action-filename.result }} ${{ needs.pr-run-checks-go.result }} ${{ needs.pr-audit-workflows.result }} ${{ needs.pr-scan-dependencies.result }} diff --git a/.github/workflows/claude-security-review.yml b/.github/workflows/pr-review-security.yml similarity index 98% rename from .github/workflows/claude-security-review.yml rename to .github/workflows/pr-review-security.yml index fadfaeff..3e2174ec 100644 --- a/.github/workflows/claude-security-review.yml +++ b/.github/workflows/pr-review-security.yml @@ -1,8 +1,8 @@ -name: claude-security-review +name: pr-review-security # Reusable workflow: dedicated LLM security review with # anthropics/claude-code-action, running the org review plugin's security -# command. A sibling of claude-review.yml with the same secrets interface, +# command. A sibling of pr-review.yml with the same secrets interface, # safe-handling model and review cadence. Findings are advisory: they never # fail the job. # @@ -21,7 +21,7 @@ name: claude-security-review # contents: read # checkout + read the diff # pull-requests: write # post the review + tracking comment # id-token: write # OIDC for the Claude GitHub App token -# uses: melodic-software/ci-workflows/.github/workflows/claude-security-review.yml@ +# uses: melodic-software/ci-workflows/.github/workflows/pr-review-security.yml@ # secrets: # CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} # @@ -46,7 +46,7 @@ name: claude-security-review # review; same-repository writers already receive this lane's secret, so # the lane trusts them. # -# REVIEW CADENCE: the same as claude-review.yml (see its header): drafts are +# REVIEW CADENCE: the same as pr-review.yml (see its header): drafts are # skipped, `opened`, `reopened` and `ready_for_review` review the whole pull # request, and a later push reviews only the files changed since this lane's # last completed review; 300 or more files changed since forces a whole review. @@ -133,7 +133,7 @@ permissions: contents: read jobs: - security-review: + claude: name: security-review runs-on: ${{ inputs.runner }} # Capped at the 16-minute ceiling set for every CI job (960 s). Outside the @@ -404,7 +404,7 @@ jobs: - name: Report review outcome id: review-outcome if: steps.scope.outputs.review != 'false' - uses: melodic-software/ci-workflows/.github/actions/claude-lane-outcome@ac062650c46005edb4787aff378347746bf63804 # v0.27.0 + uses: $/.github/actions/report-lane-outcome with: outcome: ${{ steps.claude-review.outcome }} execution-file: ${{ steps.claude-review.outputs.execution_file }} diff --git a/.github/workflows/claude-review.yml b/.github/workflows/pr-review.yml similarity index 98% rename from .github/workflows/claude-review.yml rename to .github/workflows/pr-review.yml index 1344f273..7ba61d9b 100644 --- a/.github/workflows/claude-review.yml +++ b/.github/workflows/pr-review.yml @@ -1,4 +1,4 @@ -name: claude-review +name: pr-review # Reusable workflow: automated PR code review with anthropics/claude-code-action, # running the org review plugin's command. @@ -18,7 +18,7 @@ name: claude-review # contents: read # checkout + read the diff # pull-requests: write # post the review + tracking comment # id-token: write # OIDC for the Claude GitHub App token -# uses: melodic-software/ci-workflows/.github/workflows/claude-review.yml@ +# uses: melodic-software/ci-workflows/.github/workflows/pr-review.yml@ # secrets: # CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} # @@ -125,12 +125,12 @@ on: description: >- 'true' on an infrastructure failure, else 'false'. Empty when the review did not run. - value: ${{ jobs.review.outputs.review-failed }} + value: ${{ jobs.claude.outputs.review-failed }} failure-class: description: >- auth | rate-limit | overloaded | other on a failure; skipped-validation when the action skipped itself; empty otherwise. - value: ${{ jobs.review.outputs.failure-class }} + value: ${{ jobs.claude.outputs.failure-class }} secrets: CLAUDE_CODE_OAUTH_TOKEN: description: Claude Code OAuth token (from `claude setup-token`). @@ -140,7 +140,7 @@ permissions: contents: read jobs: - review: + claude: name: claude-review-status runs-on: ${{ inputs.runner }} # The review step stops at 11 minutes (660 s); incremental scoping, not a @@ -418,7 +418,7 @@ jobs: - name: Report review outcome id: review-outcome if: steps.scope.outputs.review != 'false' - uses: melodic-software/ci-workflows/.github/actions/claude-lane-outcome@ac062650c46005edb4787aff378347746bf63804 # v0.27.0 + uses: $/.github/actions/report-lane-outcome with: outcome: ${{ steps.claude-review.outcome }} execution-file: ${{ steps.claude-review.outputs.execution_file }} diff --git a/.github/workflows/go-quality.yml b/.github/workflows/pr-run-checks-go.yml similarity index 99% rename from .github/workflows/go-quality.yml rename to .github/workflows/pr-run-checks-go.yml index 35e4e669..e15dc424 100644 --- a/.github/workflows/go-quality.yml +++ b/.github/workflows/pr-run-checks-go.yml @@ -1,4 +1,4 @@ -name: go-quality +name: pr-run-checks-go # Reusable blocking Go contract. Standards owns analyzer policy; this workflow # owns exact native installation and execution. Consume at job level with a full @@ -23,7 +23,7 @@ permissions: contents: read jobs: - linux: + check-linux: name: Go quality (Linux) runs-on: ubuntu-24.04 timeout-minutes: 30 @@ -171,7 +171,7 @@ jobs: shell: bash working-directory: ${{ inputs.working-directory }} - windows: + check-windows: name: Go quality (Windows) runs-on: windows-2025 timeout-minutes: 30 diff --git a/.github/workflows/checks.yml b/.github/workflows/pr-run-checks.yml similarity index 90% rename from .github/workflows/checks.yml rename to .github/workflows/pr-run-checks.yml index dc9fc42d..cb0409db 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/pr-run-checks.yml @@ -1,4 +1,4 @@ -name: checks +name: pr-run-checks # One job, one runner, every content-agnostic hygiene composite as a step: # `change-detection` runs once, then each composite behind its own toggle, and a @@ -9,7 +9,7 @@ name: checks # which is the cost this lane exists to remove. # # `zizmor` is deliberately absent — it has no composite, only the reusable -# `.github/workflows/zizmor.yml`, which needs `security-events: write` and an +# `.github/workflows/pr-audit-workflows.yml`, which needs `security-events: write` and an # optional SARIF upload. Callers that want it keep a separate job. # # Composites are referenced by FULL PATH at a pinned SHA, never @@ -19,7 +19,7 @@ name: checks # behind the workflow that carries them; Dependabot's `github-actions` group # bumps them like any other `uses:`. # -# That pin lag is why ci.yml carries a `composites-head` job alongside its +# That pin lag is why pr-require-checks.yml carries a `composites-head` job alongside its # dogfood call of this workflow. A pull request against this repository that # breaks one of the composite bodies would otherwise run the OLD body here and # pass, so the job runs the same composites through `./.github/actions/` at @@ -176,20 +176,20 @@ on: `fromJSON(needs..outputs.results || '{}')[''] != 'false'` — never `== 'true'`. The fail-open form matters twice over here, because a reusable workflow publishes no outputs when its job fails. - value: ${{ jobs.checks.outputs.results }} + value: ${{ jobs.check.outputs.results }} outcome: description: >- `success` when no enabled composite failed, `failure` otherwise. It mirrors the job's own result and is published for callers that want the verdict as a value; `needs..result` stays authoritative, since a failed reusable-workflow job publishes no outputs at all. - value: ${{ jobs.checks.outputs.outcome }} + value: ${{ jobs.check.outputs.outcome }} permissions: contents: read jobs: - checks: + check: runs-on: ${{ inputs.runner }} timeout-minutes: ${{ inputs.timeout-minutes }} permissions: @@ -220,7 +220,7 @@ jobs: - name: Detect relevant lanes id: detect - uses: melodic-software/ci-workflows/.github/actions/change-detection@ac062650c46005edb4787aff378347746bf63804 # v0.27.0 + uses: $/.github/actions/detect-changes with: filters: ${{ inputs.filters }} @@ -229,28 +229,28 @@ jobs: if: ${{ inputs.typos && fromJSON(steps.detect.outputs.results || '{}')['typos'] != 'false' }} continue-on-error: true timeout-minutes: 8 - uses: melodic-software/ci-workflows/.github/actions/typos@ac062650c46005edb4787aff378347746bf63804 # v0.27.0 + uses: $/.github/actions/typos - name: Scan for secrets id: gitleaks if: ${{ inputs.gitleaks && fromJSON(steps.detect.outputs.results || '{}')['gitleaks'] != 'false' }} continue-on-error: true timeout-minutes: 8 - uses: melodic-software/ci-workflows/.github/actions/gitleaks@ac062650c46005edb4787aff378347746bf63804 # v0.27.0 + uses: $/.github/actions/gitleaks - name: Check editorconfig conformance id: editorconfig if: ${{ inputs.editorconfig && fromJSON(steps.detect.outputs.results || '{}')['editorconfig'] != 'false' }} continue-on-error: true timeout-minutes: 8 - uses: melodic-software/ci-workflows/.github/actions/editorconfig@ac062650c46005edb4787aff378347746bf63804 # v0.27.0 + uses: $/.github/actions/editorconfig-checker - name: Lint markdown id: markdown if: ${{ inputs.markdown && fromJSON(steps.detect.outputs.results || '{}')['markdown'] != 'false' }} continue-on-error: true timeout-minutes: 8 - uses: melodic-software/ci-workflows/.github/actions/markdown@ac062650c46005edb4787aff378347746bf63804 # v0.27.0 + uses: $/.github/actions/markdownlint # Passing `globs` replaces the composite default, so repeat that # default before `markdown-extra-globs`. with: @@ -261,20 +261,20 @@ jobs: if: ${{ inputs.shellcheck && fromJSON(steps.detect.outputs.results || '{}')['shellcheck'] != 'false' }} continue-on-error: true timeout-minutes: 8 - uses: melodic-software/ci-workflows/.github/actions/shellcheck@ac062650c46005edb4787aff378347746bf63804 # v0.27.0 + uses: $/.github/actions/shellcheck - name: Lint workflows id: actionlint if: ${{ inputs.actionlint && fromJSON(steps.detect.outputs.results || '{}')['actionlint'] != 'false' }} continue-on-error: true timeout-minutes: 8 - uses: melodic-software/ci-workflows/.github/actions/actionlint@ac062650c46005edb4787aff378347746bf63804 # v0.27.0 + uses: $/.github/actions/actionlint - name: Verify shebang files are executable id: exec_bit if: ${{ inputs.exec-bit && fromJSON(steps.detect.outputs.results || '{}')['exec-bit'] != 'false' }} continue-on-error: true - uses: melodic-software/ci-workflows/.github/actions/exec-bit@ac062650c46005edb4787aff378347746bf63804 # v0.27.0 + uses: $/.github/actions/check-exec-bit - name: Check for machine-specific paths id: machine_specific_paths @@ -288,7 +288,7 @@ jobs: id: eol_renormalize if: ${{ inputs.eol-renormalize && fromJSON(steps.detect.outputs.results || '{}')['eol-renormalize'] != 'false' }} continue-on-error: true - uses: melodic-software/ci-workflows/.github/actions/eol-renormalize@ac062650c46005edb4787aff378347746bf63804 # v0.27.0 + uses: $/.github/actions/check-line-endings - name: Scan comment hygiene id: comment_hygiene @@ -303,7 +303,7 @@ jobs: if: ${{ inputs.lychee-offline && fromJSON(steps.detect.outputs.results || '{}')['lychee-offline'] != 'false' }} continue-on-error: true timeout-minutes: 8 - uses: melodic-software/ci-workflows/.github/actions/lychee-offline@ac062650c46005edb4787aff378347746bf63804 # v0.27.0 + uses: $/.github/actions/lychee-offline - name: Validate against JSON Schema id: check_jsonschema @@ -312,7 +312,7 @@ jobs: if: ${{ inputs.check-jsonschema && inputs.check-jsonschema-files != '' && fromJSON(steps.detect.outputs.results || '{}')['check-jsonschema'] != 'false' }} continue-on-error: true timeout-minutes: 8 - uses: melodic-software/ci-workflows/.github/actions/check-jsonschema@ac062650c46005edb4787aff378347746bf63804 # v0.27.0 + uses: $/.github/actions/check-jsonschema with: files: ${{ inputs.check-jsonschema-files }} builtin-schema: ${{ inputs.check-jsonschema-builtin-schema }} diff --git a/.github/workflows/osv-scanner.yml b/.github/workflows/pr-scan-dependencies.yml similarity index 99% rename from .github/workflows/osv-scanner.yml rename to .github/workflows/pr-scan-dependencies.yml index c0b30bff..5ced69e8 100644 --- a/.github/workflows/osv-scanner.yml +++ b/.github/workflows/pr-scan-dependencies.yml @@ -1,4 +1,4 @@ -name: osv-scanner +name: pr-scan-dependencies # Reusable dependency-vulnerability lane. The caller supplies its governed # runner selection; the workload itself needs only a Linux X64 worker and no diff --git a/.github/workflows/release.yml b/.github/workflows/release-tag.yml similarity index 99% rename from .github/workflows/release.yml rename to .github/workflows/release-tag.yml index 77146edf..e344c147 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release-tag.yml @@ -1,4 +1,4 @@ -name: release +name: release-tag # Cut a semantic-version release on demand: the next version is the highest # SemVer among PUBLISHED Releases (not tags) plus the chosen bump, released as a @@ -41,7 +41,7 @@ permissions: contents: read jobs: - release: + tag: runs-on: ubuntu-24.04 permissions: contents: write # create the tag + GitHub Release diff --git a/AGENTS.md b/AGENTS.md index 6fe02f82..14261957 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -5,8 +5,8 @@ rule in full; a line without one states the whole rule itself. - Org-wide criteria: [`REVIEW.md`](REVIEW.md), synced from `melodic-software/standards`. - Claude lane security model (`SECURITY MODEL` headers): - [`claude-review.yml`](.github/workflows/claude-review.yml) and - [`claude-security-review.yml`](.github/workflows/claude-security-review.yml). + [`pr-review.yml`](.github/workflows/pr-review.yml) and + [`pr-review-security.yml`](.github/workflows/pr-review-security.yml). - Claude lane checks stay advisory: [rule](README.md#claude-lanes--shared-consumption-contract). - Configurable, not forkable: [rule](README.md#contract). - Policy is authored in standards; `fixtures/` configs only exercise contracts: diff --git a/README.md b/README.md index 5c6d4edc..8da62e1e 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ jobs: runs-on: ubuntu-24.04 steps: - uses: actions/checkout@ - - uses: melodic-software/ci-workflows/.github/actions/markdown@ + - uses: melodic-software/ci-workflows/.github/actions/markdownlint@ ``` This repo is public, so its actions and reusable workflows are consumable by any @@ -58,7 +58,7 @@ checkout of this repo. (Public is required because a public consumer such as ## Versioning Every tag is full SemVer (`vX.Y.Z`). A release is cut whenever `main` changes -by something worth pinning to — `release.yml`'s manual `workflow_dispatch` +by something worth pinning to — `release-tag.yml`'s manual `workflow_dispatch` (patch/minor/major) makes each release a deliberate act, and cutting one after every meaningful change keeps a tagged SHA available for consumers to pin to. Consumers are never bumped automatically: each pins this repository by full @@ -124,7 +124,7 @@ consumer to audit it. ## Actions -- `.github/actions/markdown` — markdownlint-cli2 over the repo's markdown. +- `.github/actions/markdownlint` — markdownlint-cli2 over the repo's markdown. - `.github/actions/shellcheck` — ShellCheck over the repo's shell scripts (installs a pinned, checksum-verified binary). Its default discovery remains tracked `*.sh`/`*.bash`; `extra-globs` adds tracked extensionless inputs as @@ -142,12 +142,12 @@ consumer to audit it. - `.github/actions/shfmt` — shfmt formatting check over the repo's shell scripts, driven by the caller's `.editorconfig` (installs a pinned, checksum-verified binary). -- `.github/actions/powershell` — PSScriptAnalyzer over the repo's PowerShell, +- `.github/actions/psscriptanalyzer` — PSScriptAnalyzer over the repo's PowerShell, via the bundled `Invoke-Pssa.ps1`. Each file is analyzed exactly once and any analyzer or rule error fails closed. `PSUseCorrectCasing` remains disabled while the upstream [runspace-affinity defect][pssa-1708] is open; retrying a crashing rule is not a quality gate. -- `.github/actions/pulumi-deploy-guard` — verifies the complete Pulumi personal +- `.github/actions/verify-pulumi-deploy-policy` — verifies the complete Pulumi personal [OIDC allow-policy][pulumi-oidc] set against a versioned exact-claim contract, then [exports stack state][pulumi-stack-export] without plaintext secrets and classifies reviewed operational @@ -157,7 +157,7 @@ consumer to audit it. IDs and rejects Pulumi's `*`, `?`, and `.` pattern operators. Callers reserve its exact workflow name uniquely and require paired live positive/near-match negative token-exchange evidence before removing the legacy trust rules. -- `.github/actions/editorconfig` — editorconfig-checker validation of tracked +- `.github/actions/editorconfig-checker` — editorconfig-checker validation of tracked files against the repo's `.editorconfig`. - `.github/actions/typos` — `typos` spell-check over source against a caller-supplied config. @@ -178,7 +178,7 @@ consumer to audit it. self-hosted workers. - `.github/actions/check-jsonschema` — check-jsonschema validation of JSON/YAML against one schema per call (call once per schema group). -- `.github/actions/ci-status` — aggregates a caller-built `needs.*.result` string +- `.github/actions/pr-require-checks/aggregate-results` — aggregates a caller-built `needs.*.result` string into the single required gate check: `success` passes, anything else fails naming the offending result. `treat-skipped-as` is the caller's policy for `skipped` — `pass` (default) or `fail` for repos where a skipped lane means one @@ -355,7 +355,7 @@ consumer to audit it. statuses: write steps: - name: Mark the lanes verdict pending - uses: melodic-software/ci-workflows/.github/actions/ci-status@ # + uses: melodic-software/ci-workflows/.github/actions/pr-require-checks/aggregate-results@ # with: record-pending: 'true' # ... change detection @@ -368,7 +368,7 @@ consumer to audit it. actions: write steps: - name: Aggregate lane results - uses: melodic-software/ci-workflows/.github/actions/ci-status@ # + uses: melodic-software/ci-workflows/.github/actions/pr-require-checks/aggregate-results@ # with: carry-forward-wait-seconds: '0' rerun-contract-only-siblings: 'true' @@ -421,7 +421,7 @@ consumer to audit it. for as long as it waits, so the ceiling is a real cost and not a free margin. It is a ceiling, not a delay: the poll ends the moment a verdict settles or nothing is left in flight. -- `.github/actions/pr-contract` — the whole pull-request contract in one step: +- `.github/actions/pr-require-checks/check-contract` — the whole pull-request contract in one step: Conventional Commits title and the `do-not-merge` label gate the step, and issue linkage is advisory by default (a warning plus one upserted marker comment plus a label, exit code unchanged; `linkage-mode: enforce` makes it @@ -432,8 +432,8 @@ consumer to audit it. so `edited` and `labeled` runs see current state. Needs `pull-requests: write` for the comment and label; every write is best-effort and degrades to a `::notice::` on a read-only token. See - [its README](.github/actions/pr-contract/README.md) for the consumer wiring. -- `.github/actions/change-detection` — decides which CI lanes a pull request's + [its README](.github/actions/pr-require-checks/check-contract/README.md) for the consumer wiring. +- `.github/actions/detect-changes` — decides which CI lanes a pull request's changed files make relevant, so a caller can skip lanes at JOB level and stop paying for runs a change cannot affect. Checkout-free: the PR file listing comes from the API, and caller-named filter groups match it with @@ -455,16 +455,16 @@ consumer to audit it. forever. Filter conservatively: include `.github/**` in every group so CI changes re-run everything, and leave content-agnostic lanes (spell-check, secret scan, editorconfig, link integrity, and kin) ungated — any file can - carry the defect they gate on. This repo's own `ci.yml` `changes` job is + carry the defect they gate on. This repo's own `pr-require-checks.yml` `changes` job is the reference wiring. - `.github/actions/lychee-offline` — lychee `--offline` link/anchor reference-integrity over the repo's docs (deterministic; no network). -- `.github/actions/reference-integrity` — resolves `file.md` "Anchor" prose +- `.github/actions/check-markdown-references` — resolves `file.md` "Anchor" prose citations against each cited file's headings and bold lead-ins (dependency-free awk); pairs with `lychee-offline`, which covers link/fragment targets. -- `.github/actions/exec-bit` — verifies every tracked shebang file carries git +- `.github/actions/check-exec-bit` — verifies every tracked shebang file carries git index mode 100755, so executable scripts keep their bit on checkout. -- `.github/actions/action-metadata-filename` — rejects any tracked +- `.github/actions/check-action-filename` — rejects any tracked `action.yaml` (GitHub also accepts this spelling, but every lane here globs `action.yml` only); repo-wide, not scoped to `.github/actions/`. - `.github/actions/machine-specific-paths` — rejects machine-specific absolute / @@ -476,7 +476,7 @@ consumer to audit it. Local (non-CI) invocation of the four bespoke guards above is **not** owned here: use the standards `local-lane-guards` component (standards ADR-0004 / ci-workflows#190). Composite actions remain the CI wrappers. -- `.github/actions/eol-renormalize` — detects index-level line-ending drift via +- `.github/actions/check-line-endings` — detects index-level line-ending drift via git's clean filter, driven by the caller's `.gitattributes` (read-only). - `.github/actions/ruff` — Ruff lint + format-check over the repo's Python (via `uvx`; emits `--output-format=github` annotations). @@ -585,7 +585,7 @@ Hosted workflow defaults use explicit GA operating-system generations keeps hosted/self-hosted parity reviews tied to a declared image contract while GitHub continues the normal weekly patching of each hosted image generation. -- `.github/workflows/checks.yml` — the consolidated hygiene lane: **one job, one +- `.github/workflows/pr-run-checks.yml` — the consolidated hygiene lane: **one job, one runner spin-up**, `change-detection` once, then every content-agnostic composite as a step. It replaces a fan-out of one job per tool, which is the cost it exists to remove, and it never calls a per-tool reusable workflow — @@ -638,7 +638,7 @@ GitHub continues the normal weekly patching of each hosted image generation. The composites run by full path at a pinned SHA, because a relative action path inside a called workflow resolves against the caller's checkout. Those - self-pins are **not** touched by `release.yml`: cutting a release creates a + self-pins are **not** touched by `release-tag.yml`: cutting a release creates a tag and a GitHub Release at the current `main` HEAD and pushes no commit, so the commit a release tags still carries whatever self-pins were already in the tree — necessarily an **earlier** commit's SHA, never the one being cut. @@ -649,7 +649,7 @@ GitHub continues the normal weekly patching of each hosted image generation. before; otherwise the bump is made by hand. So the pins can sit several releases behind, and a release that follows a self-pin bump is what finally publishes a SHA in which they are current. That pin lag is why this - repository keeps a `composites-head` job in its own `ci.yml`: it runs the + repository keeps a `composites-head` job in its own `pr-require-checks.yml`: it runs the same composites through `./.github/actions/` so a pull request that changes a composite body is still exercised at HEAD instead of passing against the pinned copy. @@ -664,7 +664,7 @@ GitHub continues the normal weekly patching of each hosted image generation. permissions: contents: read pull-requests: read - uses: melodic-software/ci-workflows/.github/workflows/checks.yml@ + uses: melodic-software/ci-workflows/.github/workflows/pr-run-checks.yml@ with: runner: ubuntu-24.04 filters: | @@ -673,7 +673,7 @@ GitHub continues the normal weekly patching of each hosted image generation. **/*.md ``` -- `.github/workflows/issue-triage-label.yml` — applies a configured floor label +- `.github/workflows/intake-label-needs-triage.yml` — applies a configured floor label (default bare `needs-triage`, not a priority value) to an issue opened or reopened with no label matching a configured prefix (default `priority:`). **Non-gating**: never fails a PR or blocks a merge; it only guarantees new issues don't @@ -700,7 +700,7 @@ GitHub continues the normal weekly patching of each hosted image generation. issue-triage-label: permissions: issues: write - uses: melodic-software/ci-workflows/.github/workflows/issue-triage-label.yml@ + uses: melodic-software/ci-workflows/.github/workflows/intake-label-needs-triage.yml@ ``` Loop-safe by construction: adding a label emits `issues.labeled`, never @@ -711,7 +711,7 @@ GitHub continues the normal weekly patching of each hosted image generation. present. A human triager assigning a real tier after this workflow ran is expected to remove the floor label as part of that transition — this workflow only guarantees a floor and does not police tier assignment. -- `.github/workflows/standards-sync.yml` — orchestrates exact-file distribution +- `.github/workflows/maintenance-sync-standards.yml` — orchestrates exact-file distribution from the schema-v2 component manifest in `melodic-software/standards`. The standards checkout validates and materializes its own manifest; this workflow resolves one immutable standards SHA, then blocks every write lane until a @@ -731,7 +731,7 @@ GitHub continues the normal weekly patching of each hosted image generation. opt-out lifts, while a PR someone deliberately disarmed is never overridden. A rejected arm attempt (for example an already-mergeable PR) is logged and does not fail the sync. -- `.github/workflows/zizmor.yml` — GitHub Actions security/static-analysis lint +- `.github/workflows/pr-audit-workflows.yml` — GitHub Actions security/static-analysis lint with zizmor (dangerous triggers, excessive permissions, template injection). **Advisory by default** (`fail-on-severity: never` surfaces PR annotations without failing); consumed via `uses:` at job level. The @@ -758,7 +758,7 @@ GitHub continues the normal weekly patching of each hosted image generation. workflows cannot elevate caller permissions, so the called job's `security-events: write` only applies when the caller already granted it. Inputs are documented inline. -- `.github/workflows/osv-scanner.yml` — dependency vulnerability scan with +- `.github/workflows/pr-scan-dependencies.yml` — dependency vulnerability scan with Google's official native OSV-Scanner v2.5.1 Linux X64 binary. The exact binary, its provenance, and the SLSA verifier are checksum-pinned; the verifier then attests the expected Google source repository and exact release tag before the @@ -796,18 +796,18 @@ GitHub continues the normal weekly patching of each hosted image generation. the [official installation and SLSA guidance][osv-installation]. Native OSV requires a governed `runner`; the optional inputs on native `zizmor` preserve compatibility. -- `.github/workflows/claude-review.yml` — automated PR code review with +- `.github/workflows/pr-review.yml` — automated PR code review with `anthropics/claude-code-action`, running the org review plugin's `/review:code-review` command. Consume it per the [Claude lanes — shared consumption contract](#claude-lanes--shared-consumption-contract) below. -- `.github/workflows/claude-security-review.yml` — a dedicated LLM - **security-review** pass, sibling of `claude-review.yml` with the same +- `.github/workflows/pr-review-security.yml` — a dedicated LLM + **security-review** pass, sibling of `pr-review.yml` with the same secrets interface and safe-handling model, running `/review:security-review`. It reviews the PR's changed files for the vulnerabilities static analysis misses and reports findings as a PR review. Findings are advisory: they never fail the job. It skips a pull request whose files in scope are all documentation (`docs-only-paths`). -- `.github/workflows/claude-intake-triage.yml` — the intake-triage lane. On +- `.github/workflows/intake-triage.yml` — the intake-triage lane. On `issues: [opened]`, a read-only Claude run (`--permission-mode dontAsk`, read-only `gh` queries, JSON output) proposes labels and a comment; a step with no model in it applies only labels in the caller's `allowed-labels` @@ -824,7 +824,7 @@ GitHub continues the normal weekly patching of each hosted image generation. ## Claude lanes — shared consumption contract -`claude-review.yml` and `claude-security-review.yml` share one consumption +`pr-review.yml` and `pr-review-security.yml` share one consumption shape. Each is a whole-job concern (job `permissions:` plus a `secrets:` interface), which is why each is a reusable workflow rather than a composite action: the caller owns the triggers, concurrency and the permission grant, @@ -901,7 +901,7 @@ before the review reported (`no-outcome`). A review that was not needed Re-run a failed review with `gh run rerun --failed`. github-iac keeps `security-review-gate` disabled (its ADR 0011 keeps agentic review advisory); do not make either check required anywhere else. -`claude-review.yml` also exposes `review-failed` and `failure-class` as +`pr-review.yml` also exposes `review-failed` and `failure-class` as workflow outputs. **Adoption.** Callers are distributed through the org's sync-managed From a724617830e4112ce11a590307f14be0963f62ee Mon Sep 17 00:00:00 2001 From: Kyle Sexton <153232337+kyle-sexton@users.noreply.github.com> Date: Sun, 4 Oct 2026 03:17:03 -0400 Subject: [PATCH 2/6] refactor(pr-require-checks): fold the self callers into thin dogfood jobs Replace the three -self caller workflows with jobs in pr-require-checks.yml: pr-review, pr-review-security and intake-label-needs-triage. Each keeps the permissions and the one named secret its -self file passed, and pins the reusable's old path at the v0.33.0 merged SHA; they are repinned to the new paths once this change merges. None of them is in ci-status. Add ready_for_review to the pull_request types and an issues trigger. Every job except intake-label-needs-triage, ci-status included, now guards on github.event_name != 'issues' ahead of the unchanged contract-only gate; the two review jobs also require a pull_request event. Delete test-composites-head: pr-run-checks.yml reaches its composites through $/, so the dogfood call already runs them at the commit under test. Co-Authored-By: Claude Opus 5.5 --- .github/scripts/checks-outcome.test.cjs | 17 ++- .../scripts/ci-fanout-consolidation.test.cjs | 138 +++++++++--------- .../claude-review-outcome-wiring.test.cjs | 15 +- .github/workflows/claude-review-self.yml | 27 ---- .../workflows/claude-security-review-self.yml | 28 ---- .github/workflows/issue-triage-label-self.yml | 19 --- .github/workflows/pr-require-checks.yml | 121 ++++++++------- README.md | 28 +--- 8 files changed, 155 insertions(+), 238 deletions(-) delete mode 100644 .github/workflows/claude-review-self.yml delete mode 100644 .github/workflows/claude-security-review-self.yml delete mode 100644 .github/workflows/issue-triage-label-self.yml diff --git a/.github/scripts/checks-outcome.test.cjs b/.github/scripts/checks-outcome.test.cjs index 185e04a5..72df4b49 100644 --- a/.github/scripts/checks-outcome.test.cjs +++ b/.github/scripts/checks-outcome.test.cjs @@ -20,11 +20,19 @@ const { test } = require("node:test"); const { parseWorkflow } = require("./workflow-yaml.cjs"); -const workflowPath = path.join(__dirname, "..", "workflows", "pr-run-checks.yml"); +const workflowPath = path.join( + __dirname, + "..", + "workflows", + "pr-run-checks.yml", +); const workflow = parseWorkflow(fs.readFileSync(workflowPath, "utf8")); const steps = workflow.jobs.check.steps; const joinStep = steps.find((step) => step?.id === "outcome"); -assert.ok(joinStep !== undefined, "pr-run-checks.yml has no `outcome` join step"); +assert.ok( + joinStep !== undefined, + "pr-run-checks.yml has no `outcome` join step", +); // Every step the join owns: the composites, plus the input-combination guard // that must go red rather than let an enabled toggle skip silently. @@ -201,7 +209,10 @@ test("an enabled toggle with no configuration fails, it does not skip quietly", // `check-jsonschema-files`; the join is what turns it into the job's verdict, // so a silently skipped schema gate cannot report success. const guard = steps.find((step) => step?.id === "configuration"); - assert.ok(guard !== undefined, "pr-run-checks.yml has no input-combination guard"); + assert.ok( + guard !== undefined, + "pr-run-checks.yml has no input-combination guard", + ); assert.equal( guard.if, `\${{ inputs.check-jsonschema && inputs.check-jsonschema-files == '' }}`, diff --git a/.github/scripts/ci-fanout-consolidation.test.cjs b/.github/scripts/ci-fanout-consolidation.test.cjs index 8507dcc3..2a56ab6b 100644 --- a/.github/scripts/ci-fanout-consolidation.test.cjs +++ b/.github/scripts/ci-fanout-consolidation.test.cjs @@ -110,23 +110,65 @@ test("the contract-only predicate excludes forks and base changes", () => { test("pr-require-checks.yml re-runs on the contract-only pull_request actions", () => { assert.match( ciWorkflow, - /^ {4}types: \[opened, synchronize, reopened, edited, labeled, unlabeled\]$/mu, + /^ {4}types: \[opened, synchronize, reopened, ready_for_review, edited, labeled, unlabeled\]$/mu, ); }); -test("every job except ci-status carries the contract-only gate", () => { - // Job keys are the two-space-indented mapping keys under `jobs:`; the `if:` - // that follows a job key before the next one is that job's condition. - const jobsSection = ciWorkflow.slice(ciWorkflow.search(/^jobs:$/mu)); - const jobBlocks = jobsSection.split(/^ {2}(?=[a-z0-9-]+:$)/mu).slice(1); +// Job keys are the two-space-indented mapping keys under `jobs:`; the `if:` +// that follows a job key before the next one is that job's condition. +const jobBlocks = ciWorkflow + .slice(ciWorkflow.search(/^jobs:$/mu)) + .split(/^ {2}(?=[a-z0-9-]+:$)/mu) + .slice(1); +const jobName = (block) => /^([a-z0-9-]+):$/mu.exec(block)?.[1]; +const jobCondition = (block) => /^ {4}if: (.*)$/mu.exec(block)?.[1] ?? ""; + +// The issue labeler is the only job an `issues` event may start. Any other job +// that ran there would have no pull request to check, and `ci-status` would +// publish a verdict for an issue. +const ISSUE_JOB = "intake-label-needs-triage"; + +test("only the issue labeler runs on an issues event", () => { + assert.match(ciWorkflow, /^ {2}issues:\n {4}types: \[opened, reopened\]$/mu); + const unguarded = []; + for (const block of jobBlocks) { + const name = jobName(block); + if (name === ISSUE_JOB) { + assert.equal( + jobCondition(block), + `\${{ github.event_name == 'issues' }}`, + ); + } else if ( + !jobCondition(block).startsWith("${{ github.event_name != 'issues' && ") + ) { + unguarded.push(name); + } + } + assert.deepEqual(unguarded, []); +}); + +test("the dogfood Claude lanes and issue labeler stay out of ci-status", () => { + // Claude lane checks are advisory: a model outage must never block a merge. + const needs = + /^ {2}ci-status:\n(?:.*\n)*? {4}needs: \[(?[^\]]*)\]$/mu.exec( + ciWorkflow, + )?.groups?.list; + assert.ok(needs !== undefined, "ci-status has no needs list"); + for (const job of ["pr-review", "pr-review-security", ISSUE_JOB]) { + assert.match(ciWorkflow, new RegExp(`^ {2}${job}:$`, "mu")); + assert.ok(!needs.split(/,\s*/u).includes(job), `ci-status waits on ${job}`); + assert.doesNotMatch(ciWorkflow, new RegExp(`needs\\.${job}\\.result`, "u")); + } +}); + +test("every job except ci-status and the issue labeler carries the contract-only gate", () => { const ungated = []; for (const block of jobBlocks) { - const name = /^([a-z0-9-]+):$/mu.exec(block)?.[1]; - if (name === undefined || name === "ci-status") { + const name = jobName(block); + if (name === undefined || name === "ci-status" || name === ISSUE_JOB) { continue; } - const condition = /^ {4}if: (.*)$/mu.exec(block)?.[1] ?? ""; - if (!condition.includes(CONTRACT_ONLY_GATE)) { + if (!jobCondition(block).includes(CONTRACT_ONLY_GATE)) { ungated.push(name); } } @@ -154,19 +196,21 @@ test("the ci-status contract-only default matches every job gate", () => { const actionDefault = normalizeExpression(defaultBlock.groups.value); assert.equal(actionDefault, normalizeExpression(CONTRACT_ONLY_PREDICATE)); - const jobsSection = ciWorkflow.slice(ciWorkflow.search(/^jobs:$/mu)); - const jobBlocks = jobsSection.split(/^ {2}(?=[a-z0-9-]+:$)/mu).slice(1); let compared = 0; for (const block of jobBlocks) { - const name = /^([a-z0-9-]+):$/mu.exec(block)?.[1]; - if (name === undefined || name === "ci-status") { + const name = jobName(block); + if (name === undefined || name === "ci-status" || name === ISSUE_JOB) { continue; } - const condition = /^ {4}if: \$\{\{ (?.*) \}\}$/mu.exec(block)?.groups + const body = /^ {4}if: \$\{\{ (?.*) \}\}$/mu.exec(block)?.groups ?.body; - assert.ok(condition !== undefined, `job ${name} has no inline if:`); - // The gate is the leading term; anything after it is the job's own - // condition, ANDed on. + assert.ok(body !== undefined, `job ${name} has no inline if:`); + // The gate leads once the event-name guards are dropped; anything after it + // is the job's own condition, ANDed on. + const condition = body.replace( + /^(?:github\.event_name [!=]= '[a-z_]+' && )+/u, + "", + ); const gate = condition.startsWith(CONTRACT_ONLY_GATE) ? CONTRACT_ONLY_GATE : condition; @@ -187,8 +231,12 @@ test("the ci-status job runs pr-contract before the aggregation", () => { // The carry-forward wait enumerates this workflow's runs on the head SHA. // Without the scope both Actions reads 403 and the wait never engages. assert.match(ciStatusJob, /^ {6}actions: read$/mu); - const contractStep = ciStatusJob.indexOf("./.github/actions/pr-require-checks/check-contract"); - const aggregateStep = ciStatusJob.indexOf("./.github/actions/pr-require-checks/aggregate-results"); + const contractStep = ciStatusJob.indexOf( + "./.github/actions/pr-require-checks/check-contract", + ); + const aggregateStep = ciStatusJob.indexOf( + "./.github/actions/pr-require-checks/aggregate-results", + ); assert.ok(contractStep !== -1 && aggregateStep !== -1); assert.ok(contractStep < aggregateStep); // `!cancelled()` so a failing contract step does not skip the status write. @@ -234,7 +282,7 @@ test("pr-require-checks.yml consolidates the hygiene composites into the checks // elevate: without the caller's own grant the job fails at startup. const checksJob = ciWorkflow.slice( ciWorkflow.search(/^ {2}pr-run-checks:$/mu), - ciWorkflow.search(/^ {2}test-composites-head:$/mu), + ciWorkflow.search(/^ {2}psscriptanalyzer:$/mu), ); assert.match(checksJob, /^ {6}contents: read$/mu); assert.match(checksJob, /^ {6}pull-requests: read$/mu); @@ -285,54 +333,6 @@ test("pr-require-checks.yml consolidates the hygiene composites into the checks ); }); -test("pr-require-checks.yml runs the moved composites at HEAD alongside the reusable", () => { - // pr-run-checks.yml can only reach its composites at a pinned SHA (a relative path - // inside a called workflow resolves against the caller's checkout), so the - // reusable runs the bodies of the release it was pinned at. This job runs the - // same bodies from the commit under test; without it a pull request that - // breaks one of them passes this repository's own CI. - const start = ciWorkflow.search(/^ {2}test-composites-head:$/mu); - assert.notEqual(start, -1, "pr-require-checks.yml has no composites-head job"); - const composites = ciWorkflow.slice( - start, - ciWorkflow.search(/^ {2}psscriptanalyzer:$/mu), - ); - assert.match(composites, /^ {4}name: Composites at HEAD$/mu); - assert.match(composites, /^ {4}needs: detect-changes$/mu); - - // Every composite the reusable moved off HEAD, and only those: actionlint, - // shellcheck and check-jsonschema already run at HEAD in their own jobs. - for (const composite of [ - "typos", - "gitleaks", - "editorconfig-checker", - "markdownlint", - "check-exec-bit", - "machine-specific-paths", - "check-line-endings", - "comment-hygiene", - "lychee-offline", - ]) { - assert.match( - composites, - new RegExp(`^ {8}uses: \\./\\.github/actions/${composite}$`, "mu"), - `composites-head does not run ${composite} at HEAD`, - ); - } - // A pinned reference here would reintroduce the lag the job exists to close. - assert.doesNotMatch(composites, /uses: melodic-software\/ci-workflows\//u); - - // The lane is only real if the required check aggregates it. - assert.match( - ciWorkflow, - /^ {4}needs: \[[^\n]*\btest-composites-head\b[^\n]*\]$/mu, - ); - assert.match( - ciWorkflow, - /^ {10}results: [^\n]*\$\{\{ needs\.test-composites-head\.result \}\}[^\n]*$/mu, - ); -}); - function yamlFilesUnder(directory) { return fs .readdirSync(directory, { withFileTypes: true }) diff --git a/.github/scripts/claude-review-outcome-wiring.test.cjs b/.github/scripts/claude-review-outcome-wiring.test.cjs index 060a4ce0..ec4ab63d 100644 --- a/.github/scripts/claude-review-outcome-wiring.test.cjs +++ b/.github/scripts/claude-review-outcome-wiring.test.cjs @@ -1,12 +1,10 @@ "use strict"; -// The review lanes consume the claude-lane-outcome composite through a -// SHA-pinned `uses:`, so the composite's checked-in source and the version a -// lane actually runs can diverge, and a lane reading a not-yet-pinned output -// gets an empty value on every run. These tests pin the rule that every -// consumed output of the composite is one its source declares, checked -// against each lane's own pinned revision when the object is reachable, and -// against the working tree always. +// The review lanes reach the report-lane-outcome composite through `$/`, so a +// lane runs the composite from its own commit, and a lane reading an output +// the composite does not declare gets an empty value on every run. These +// tests pin the rule that every consumed output of the composite is one its +// source in the same tree declares. const assert = require("node:assert/strict"); const fs = require("node:fs"); @@ -38,8 +36,7 @@ test("lane discovery finds the outcome composite's consumers", () => { `expected at least the review and security lanes, found: ${LANES.join(", ")}`, ); assert.ok( - LANES.includes("pr-review.yml") && - LANES.includes("pr-review-security.yml"), + LANES.includes("pr-review.yml") && LANES.includes("pr-review-security.yml"), `known consumers missing from discovery: ${LANES.join(", ")}`, ); }); diff --git a/.github/workflows/claude-review-self.yml b/.github/workflows/claude-review-self.yml deleted file mode 100644 index a6a57b4f..00000000 --- a/.github/workflows/claude-review-self.yml +++ /dev/null @@ -1,27 +0,0 @@ -name: Claude Review - -# Dogfood caller: this repo consumes its own claude-review reusable workflow -# with the canonical caller shape. Local `./` ref works because the checkout -# IS this repo. -on: - pull_request: - types: [opened, synchronize, ready_for_review, reopened] - -permissions: - contents: read - -concurrency: - group: ${{ github.workflow }}-${{ github.event.pull_request.number }} - cancel-in-progress: true - -jobs: - review: - permissions: - contents: read - pull-requests: write - id-token: write - uses: ./.github/workflows/pr-review.yml - # Pass only the one named secret (least privilege) rather than `secrets: - # inherit`, which would forward every parent secret to the called workflow. - secrets: - CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} diff --git a/.github/workflows/claude-security-review-self.yml b/.github/workflows/claude-security-review-self.yml deleted file mode 100644 index 72a6f39f..00000000 --- a/.github/workflows/claude-security-review-self.yml +++ /dev/null @@ -1,28 +0,0 @@ -name: Claude Security Review - -# Dogfood caller: this repo consumes its own claude-security-review reusable -# workflow with the canonical caller shape. Local `./` ref works because the -# checkout IS this repo. Do NOT add merge_group: the pinned action cannot serve -# that event (#272). -on: - pull_request: - types: [opened, synchronize, ready_for_review, reopened] - -permissions: - contents: read - -concurrency: - group: ${{ github.workflow }}-${{ github.event.pull_request.number }} - cancel-in-progress: true - -jobs: - security-review: - permissions: - contents: read - pull-requests: write - id-token: write - uses: ./.github/workflows/pr-review-security.yml - # Pass only the one named secret (least privilege) rather than `secrets: - # inherit`, which would forward every parent secret to the called workflow. - secrets: - CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} diff --git a/.github/workflows/issue-triage-label-self.yml b/.github/workflows/issue-triage-label-self.yml deleted file mode 100644 index 950f91c2..00000000 --- a/.github/workflows/issue-triage-label-self.yml +++ /dev/null @@ -1,19 +0,0 @@ -name: issue-triage-label-self - -# Dogfood caller of this repo's issue-triage-label reusable, pinned to its -# merged SHA, not a PR head, so a PR cannot redefine the reusable it runs. -# Uses the real `needs-triage` label, exercising the label-set guard and -# fail-closed existence check end to end. -on: - issues: - types: [opened, reopened] - -permissions: {} - -jobs: - issue-triage-label: - permissions: - issues: write # Downgraded to exactly what the called reusable needs. - uses: melodic-software/ci-workflows/.github/workflows/issue-triage-label.yml@ac062650c46005edb4787aff378347746bf63804 # v0.27.0 - with: - label: needs-triage diff --git a/.github/workflows/pr-require-checks.yml b/.github/workflows/pr-require-checks.yml index 5d101ecf..c2147faa 100644 --- a/.github/workflows/pr-require-checks.yml +++ b/.github/workflows/pr-require-checks.yml @@ -12,7 +12,11 @@ on: # A base-branch `edited` changes the merge commit, so it runs in full. A # fork pull request is never contract-only: its read-only token cannot # record lane state. - types: [opened, synchronize, reopened, edited, labeled, unlabeled] + types: [opened, synchronize, reopened, ready_for_review, edited, labeled, unlabeled] + # Only `intake-label-needs-triage` runs on an issue event; every other job, + # `ci-status` included, guards on `github.event_name != 'issues'`. + issues: + types: [opened, reopened] permissions: contents: read @@ -46,7 +50,7 @@ concurrency: # aggregates `changes` so a broken detection goes red. jobs: detect-changes: - if: ${{ !(github.event.pull_request.head.repo.full_name == github.repository && (contains(fromJSON('["labeled","unlabeled"]'), github.event.action) || (github.event.action == 'edited' && !github.event.changes.base))) }} + if: ${{ github.event_name != 'issues' && !(github.event.pull_request.head.repo.full_name == github.repository && (contains(fromJSON('["labeled","unlabeled"]'), github.event.action) || (github.event.action == 'edited' && !github.event.changes.base))) }} runs-on: ubuntu-24.04 timeout-minutes: 5 permissions: @@ -134,7 +138,7 @@ jobs: # A local reusable-workflow reference executes pr-run-checks.yml from this exact # commit, so the dogfood proves the contract a consumer pins by tag. pr-run-checks: - if: ${{ !(github.event.pull_request.head.repo.full_name == github.repository && (contains(fromJSON('["labeled","unlabeled"]'), github.event.action) || (github.event.action == 'edited' && !github.event.changes.base))) }} + if: ${{ github.event_name != 'issues' && !(github.event.pull_request.head.repo.full_name == github.repository && (contains(fromJSON('["labeled","unlabeled"]'), github.event.action) || (github.event.action == 'edited' && !github.event.changes.base))) }} permissions: contents: read # change-detection lists the pull request's files. A called workflow @@ -167,48 +171,9 @@ jobs: check-jsonschema: .github/** - # pr-run-checks.yml reaches its composites only at a pinned SHA, so a pull request - # breaking a composite body would pass there; this job runs them from this - # commit. actionlint, shellcheck and check-jsonschema already run at HEAD in - # their own jobs. Gated on `actionlint`, the narrowest group naming composites. - test-composites-head: - name: Composites at HEAD - needs: detect-changes - if: ${{ !(github.event.pull_request.head.repo.full_name == github.repository && (contains(fromJSON('["labeled","unlabeled"]'), github.event.action) || (github.event.action == 'edited' && !github.event.changes.base))) && !cancelled() && fromJSON(needs.detect-changes.outputs.results || '{}')['actionlint'] != 'false' }} - runs-on: ubuntu-24.04 - timeout-minutes: 15 - steps: - - name: Check out - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - persist-credentials: false - - name: Spell-check - uses: ./.github/actions/typos - - name: Scan for secrets - uses: ./.github/actions/gitleaks - - name: Check editorconfig conformance - uses: ./.github/actions/editorconfig-checker - - name: Lint markdown - uses: ./.github/actions/markdownlint - - name: Verify shebang files are executable - uses: ./.github/actions/check-exec-bit - - name: Check for machine-specific paths - # Same self-exclusion the `checks` caller passes. - uses: ./.github/actions/machine-specific-paths - with: - exclude: ':(exclude).github/actions/machine-specific-paths/**' - - name: Check index-level EOL drift - uses: ./.github/actions/check-line-endings - - name: Scan comment hygiene - uses: ./.github/actions/comment-hygiene - with: - exclude: ':(exclude).github/actions/comment-hygiene/**' - - name: Check links and anchors (offline) - uses: ./.github/actions/lychee-offline - psscriptanalyzer: needs: detect-changes - if: ${{ !(github.event.pull_request.head.repo.full_name == github.repository && (contains(fromJSON('["labeled","unlabeled"]'), github.event.action) || (github.event.action == 'edited' && !github.event.changes.base))) && !cancelled() && fromJSON(needs.detect-changes.outputs.results || '{}')['powershell'] != 'false' }} + if: ${{ github.event_name != 'issues' && !(github.event.pull_request.head.repo.full_name == github.repository && (contains(fromJSON('["labeled","unlabeled"]'), github.event.action) || (github.event.action == 'edited' && !github.event.changes.base))) && !cancelled() && fromJSON(needs.detect-changes.outputs.results || '{}')['powershell'] != 'false' }} runs-on: ubuntu-24.04 timeout-minutes: 15 steps: @@ -227,7 +192,7 @@ jobs: check-markdown-references: needs: detect-changes - if: ${{ !(github.event.pull_request.head.repo.full_name == github.repository && (contains(fromJSON('["labeled","unlabeled"]'), github.event.action) || (github.event.action == 'edited' && !github.event.changes.base))) && !cancelled() && fromJSON(needs.detect-changes.outputs.results || '{}')['reference-integrity'] != 'false' }} + if: ${{ github.event_name != 'issues' && !(github.event.pull_request.head.repo.full_name == github.repository && (contains(fromJSON('["labeled","unlabeled"]'), github.event.action) || (github.event.action == 'edited' && !github.event.changes.base))) && !cancelled() && fromJSON(needs.detect-changes.outputs.results || '{}')['reference-integrity'] != 'false' }} runs-on: ubuntu-24.04 timeout-minutes: 15 steps: @@ -244,7 +209,7 @@ jobs: ruff: needs: detect-changes - if: ${{ !(github.event.pull_request.head.repo.full_name == github.repository && (contains(fromJSON('["labeled","unlabeled"]'), github.event.action) || (github.event.action == 'edited' && !github.event.changes.base))) && !cancelled() && fromJSON(needs.detect-changes.outputs.results || '{}')['python'] != 'false' }} + if: ${{ github.event_name != 'issues' && !(github.event.pull_request.head.repo.full_name == github.repository && (contains(fromJSON('["labeled","unlabeled"]'), github.event.action) || (github.event.action == 'edited' && !github.event.changes.base))) && !cancelled() && fromJSON(needs.detect-changes.outputs.results || '{}')['python'] != 'false' }} runs-on: ubuntu-24.04 timeout-minutes: 15 steps: @@ -260,7 +225,7 @@ jobs: pyright: needs: detect-changes - if: ${{ !(github.event.pull_request.head.repo.full_name == github.repository && (contains(fromJSON('["labeled","unlabeled"]'), github.event.action) || (github.event.action == 'edited' && !github.event.changes.base))) && !cancelled() && fromJSON(needs.detect-changes.outputs.results || '{}')['python'] != 'false' }} + if: ${{ github.event_name != 'issues' && !(github.event.pull_request.head.repo.full_name == github.repository && (contains(fromJSON('["labeled","unlabeled"]'), github.event.action) || (github.event.action == 'edited' && !github.event.changes.base))) && !cancelled() && fromJSON(needs.detect-changes.outputs.results || '{}')['python'] != 'false' }} runs-on: ubuntu-24.04 timeout-minutes: 15 steps: @@ -275,7 +240,7 @@ jobs: biome: needs: detect-changes - if: ${{ !(github.event.pull_request.head.repo.full_name == github.repository && (contains(fromJSON('["labeled","unlabeled"]'), github.event.action) || (github.event.action == 'edited' && !github.event.changes.base))) && !cancelled() && fromJSON(needs.detect-changes.outputs.results || '{}')['typescript'] != 'false' }} + if: ${{ github.event_name != 'issues' && !(github.event.pull_request.head.repo.full_name == github.repository && (contains(fromJSON('["labeled","unlabeled"]'), github.event.action) || (github.event.action == 'edited' && !github.event.changes.base))) && !cancelled() && fromJSON(needs.detect-changes.outputs.results || '{}')['typescript'] != 'false' }} runs-on: ubuntu-24.04 timeout-minutes: 15 steps: @@ -293,7 +258,7 @@ jobs: dotnet-build: needs: detect-changes - if: ${{ !(github.event.pull_request.head.repo.full_name == github.repository && (contains(fromJSON('["labeled","unlabeled"]'), github.event.action) || (github.event.action == 'edited' && !github.event.changes.base))) && !cancelled() && fromJSON(needs.detect-changes.outputs.results || '{}')['dotnet'] != 'false' }} + if: ${{ github.event_name != 'issues' && !(github.event.pull_request.head.repo.full_name == github.repository && (contains(fromJSON('["labeled","unlabeled"]'), github.event.action) || (github.event.action == 'edited' && !github.event.changes.base))) && !cancelled() && fromJSON(needs.detect-changes.outputs.results || '{}')['dotnet'] != 'false' }} runs-on: ubuntu-24.04 timeout-minutes: 15 steps: @@ -310,7 +275,7 @@ jobs: dotnet-format: needs: detect-changes - if: ${{ !(github.event.pull_request.head.repo.full_name == github.repository && (contains(fromJSON('["labeled","unlabeled"]'), github.event.action) || (github.event.action == 'edited' && !github.event.changes.base))) && !cancelled() && fromJSON(needs.detect-changes.outputs.results || '{}')['dotnet'] != 'false' }} + if: ${{ github.event_name != 'issues' && !(github.event.pull_request.head.repo.full_name == github.repository && (contains(fromJSON('["labeled","unlabeled"]'), github.event.action) || (github.event.action == 'edited' && !github.event.changes.base))) && !cancelled() && fromJSON(needs.detect-changes.outputs.results || '{}')['dotnet'] != 'false' }} runs-on: ubuntu-24.04 timeout-minutes: 15 steps: @@ -325,7 +290,7 @@ jobs: actionlint: needs: detect-changes - if: ${{ !(github.event.pull_request.head.repo.full_name == github.repository && (contains(fromJSON('["labeled","unlabeled"]'), github.event.action) || (github.event.action == 'edited' && !github.event.changes.base))) && !cancelled() && fromJSON(needs.detect-changes.outputs.results || '{}')['actionlint'] != 'false' }} + if: ${{ github.event_name != 'issues' && !(github.event.pull_request.head.repo.full_name == github.repository && (contains(fromJSON('["labeled","unlabeled"]'), github.event.action) || (github.event.action == 'edited' && !github.event.changes.base))) && !cancelled() && fromJSON(needs.detect-changes.outputs.results || '{}')['actionlint'] != 'false' }} runs-on: ubuntu-24.04 timeout-minutes: 15 steps: @@ -365,7 +330,7 @@ jobs: jsonschema: needs: detect-changes - if: ${{ !(github.event.pull_request.head.repo.full_name == github.repository && (contains(fromJSON('["labeled","unlabeled"]'), github.event.action) || (github.event.action == 'edited' && !github.event.changes.base))) && !cancelled() && fromJSON(needs.detect-changes.outputs.results || '{}')['jsonschema'] != 'false' }} + if: ${{ github.event_name != 'issues' && !(github.event.pull_request.head.repo.full_name == github.repository && (contains(fromJSON('["labeled","unlabeled"]'), github.event.action) || (github.event.action == 'edited' && !github.event.changes.base))) && !cancelled() && fromJSON(needs.detect-changes.outputs.results || '{}')['jsonschema'] != 'false' }} runs-on: ubuntu-24.04 timeout-minutes: 15 steps: @@ -387,7 +352,7 @@ jobs: files: .github/actions/*/action.yml check-action-filename: - if: ${{ !(github.event.pull_request.head.repo.full_name == github.repository && (contains(fromJSON('["labeled","unlabeled"]'), github.event.action) || (github.event.action == 'edited' && !github.event.changes.base))) }} + if: ${{ github.event_name != 'issues' && !(github.event.pull_request.head.repo.full_name == github.repository && (contains(fromJSON('["labeled","unlabeled"]'), github.event.action) || (github.event.action == 'edited' && !github.event.changes.base))) }} runs-on: ubuntu-24.04 timeout-minutes: 15 steps: @@ -401,7 +366,7 @@ jobs: # Advisory (#208): reject hand-edits of sync-manifest-managed files. Not in # ci-status yet: promote after a clean soak. check-managed-files: - if: ${{ !(github.event.pull_request.head.repo.full_name == github.repository && (contains(fromJSON('["labeled","unlabeled"]'), github.event.action) || (github.event.action == 'edited' && !github.event.changes.base))) && github.event_name == 'pull_request' }} + if: ${{ github.event_name != 'issues' && !(github.event.pull_request.head.repo.full_name == github.repository && (contains(fromJSON('["labeled","unlabeled"]'), github.event.action) || (github.event.action == 'edited' && !github.event.changes.base))) && github.event_name == 'pull_request' }} runs-on: ubuntu-24.04 timeout-minutes: 10 steps: @@ -417,7 +382,7 @@ jobs: shellcheck: needs: detect-changes - if: ${{ !(github.event.pull_request.head.repo.full_name == github.repository && (contains(fromJSON('["labeled","unlabeled"]'), github.event.action) || (github.event.action == 'edited' && !github.event.changes.base))) && !cancelled() && fromJSON(needs.detect-changes.outputs.results || '{}')['shell'] != 'false' }} + if: ${{ github.event_name != 'issues' && !(github.event.pull_request.head.repo.full_name == github.repository && (contains(fromJSON('["labeled","unlabeled"]'), github.event.action) || (github.event.action == 'edited' && !github.event.changes.base))) && !cancelled() && fromJSON(needs.detect-changes.outputs.results || '{}')['shell'] != 'false' }} runs-on: ubuntu-24.04 timeout-minutes: 15 steps: @@ -434,7 +399,7 @@ jobs: shfmt: needs: detect-changes - if: ${{ !(github.event.pull_request.head.repo.full_name == github.repository && (contains(fromJSON('["labeled","unlabeled"]'), github.event.action) || (github.event.action == 'edited' && !github.event.changes.base))) && !cancelled() && fromJSON(needs.detect-changes.outputs.results || '{}')['shell'] != 'false' }} + if: ${{ github.event_name != 'issues' && !(github.event.pull_request.head.repo.full_name == github.repository && (contains(fromJSON('["labeled","unlabeled"]'), github.event.action) || (github.event.action == 'edited' && !github.event.changes.base))) && !cancelled() && fromJSON(needs.detect-changes.outputs.results || '{}')['shell'] != 'false' }} runs-on: ubuntu-24.04 timeout-minutes: 15 steps: @@ -453,7 +418,7 @@ jobs: test-contracts: needs: detect-changes - if: ${{ !(github.event.pull_request.head.repo.full_name == github.repository && (contains(fromJSON('["labeled","unlabeled"]'), github.event.action) || (github.event.action == 'edited' && !github.event.changes.base))) && !cancelled() && fromJSON(needs.detect-changes.outputs.results || '{}')['selector-contract'] != 'false' }} + if: ${{ github.event_name != 'issues' && !(github.event.pull_request.head.repo.full_name == github.repository && (contains(fromJSON('["labeled","unlabeled"]'), github.event.action) || (github.event.action == 'edited' && !github.event.changes.base))) && !cancelled() && fromJSON(needs.detect-changes.outputs.results || '{}')['selector-contract'] != 'false' }} runs-on: ubuntu-24.04 timeout-minutes: 15 steps: @@ -499,7 +464,7 @@ jobs: pr-audit-workflows: needs: detect-changes - if: ${{ !(github.event.pull_request.head.repo.full_name == github.repository && (contains(fromJSON('["labeled","unlabeled"]'), github.event.action) || (github.event.action == 'edited' && !github.event.changes.base))) && !cancelled() && fromJSON(needs.detect-changes.outputs.results || '{}')['zizmor'] != 'false' }} + if: ${{ github.event_name != 'issues' && !(github.event.pull_request.head.repo.full_name == github.repository && (contains(fromJSON('["labeled","unlabeled"]'), github.event.action) || (github.event.action == 'edited' && !github.event.changes.base))) && !cancelled() && fromJSON(needs.detect-changes.outputs.results || '{}')['zizmor'] != 'false' }} permissions: contents: read # pr-audit-workflows.yml always declares this and a called workflow cannot elevate, @@ -514,7 +479,7 @@ jobs: pr-scan-dependencies: # Not change-gated: a filter enumerating every lockfile format OSV # discovers would drift and skip an unlisted new one. - if: ${{ !(github.event.pull_request.head.repo.full_name == github.repository && (contains(fromJSON('["labeled","unlabeled"]'), github.event.action) || (github.event.action == 'edited' && !github.event.changes.base))) }} + if: ${{ github.event_name != 'issues' && !(github.event.pull_request.head.repo.full_name == github.repository && (contains(fromJSON('["labeled","unlabeled"]'), github.event.action) || (github.event.action == 'edited' && !github.event.changes.base))) }} permissions: contents: read uses: ./.github/workflows/pr-scan-dependencies.yml @@ -528,7 +493,7 @@ jobs: pr-run-checks-go: needs: detect-changes - if: ${{ !(github.event.pull_request.head.repo.full_name == github.repository && (contains(fromJSON('["labeled","unlabeled"]'), github.event.action) || (github.event.action == 'edited' && !github.event.changes.base))) && !cancelled() && fromJSON(needs.detect-changes.outputs.results || '{}')['go'] != 'false' }} + if: ${{ github.event_name != 'issues' && !(github.event.pull_request.head.repo.full_name == github.repository && (contains(fromJSON('["labeled","unlabeled"]'), github.event.action) || (github.event.action == 'edited' && !github.event.changes.base))) && !cancelled() && fromJSON(needs.detect-changes.outputs.results || '{}')['go'] != 'false' }} permissions: contents: read # The fixture asserts on Windows that the test binary was built with the @@ -539,11 +504,43 @@ jobs: working-directory: fixtures/go/windows-race go-version-file: fixtures/go/windows-race/go.mod + # Dogfood of the Claude lanes and the issue labeler, pinned to a merged SHA + # rather than `./` so a pull request cannot redefine the reusable that + # reviews it. Advisory: none of the three is in `ci-status`. Each passes only + # the one named secret its reusable declares. + pr-review: + if: ${{ github.event_name != 'issues' && github.event_name == 'pull_request' && !(github.event.pull_request.head.repo.full_name == github.repository && (contains(fromJSON('["labeled","unlabeled"]'), github.event.action) || (github.event.action == 'edited' && !github.event.changes.base))) }} + permissions: + contents: read + pull-requests: write + id-token: write + uses: melodic-software/ci-workflows/.github/workflows/claude-review.yml@632130a974770e50825c8747e87b0e18ecb2b36e # v0.33.0 + secrets: + CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} + + pr-review-security: + if: ${{ github.event_name != 'issues' && github.event_name == 'pull_request' && !(github.event.pull_request.head.repo.full_name == github.repository && (contains(fromJSON('["labeled","unlabeled"]'), github.event.action) || (github.event.action == 'edited' && !github.event.changes.base))) }} + permissions: + contents: read + pull-requests: write + id-token: write + uses: melodic-software/ci-workflows/.github/workflows/claude-security-review.yml@632130a974770e50825c8747e87b0e18ecb2b36e # v0.33.0 + secrets: + CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} + + # Labels with the real `needs-triage` label (the reusable's default), so the + # label-set guard and the fail-closed existence check run end to end. + intake-label-needs-triage: + if: ${{ github.event_name == 'issues' }} + permissions: + issues: write + uses: melodic-software/ci-workflows/.github/workflows/issue-triage-label.yml@632130a974770e50825c8747e87b0e18ecb2b36e # v0.33.0 + ci-status: - if: always() + if: ${{ github.event_name != 'issues' && always() }} # `changes` is aggregated so a failed detection fails ci-status even though # every gated lane fails open and runs. - needs: [detect-changes, pr-run-checks, test-composites-head, psscriptanalyzer, check-markdown-references, ruff, pyright, biome, dotnet-build, dotnet-format, shellcheck, shfmt, test-contracts, actionlint, jsonschema, check-action-filename, pr-run-checks-go, pr-audit-workflows, pr-scan-dependencies] + needs: [detect-changes, pr-run-checks, psscriptanalyzer, check-markdown-references, ruff, pyright, biome, dotnet-build, dotnet-format, shellcheck, shfmt, test-contracts, actionlint, jsonschema, check-action-filename, pr-run-checks-go, pr-audit-workflows, pr-scan-dependencies] runs-on: ubuntu-24.04 timeout-minutes: 15 permissions: @@ -574,4 +571,4 @@ jobs: # `timeout-minutes * 60 - 60`, sized per README.md. Keep the 60 s # margin: a job timeout would preempt the fail-closed error. carry-forward-wait-seconds: '840' - results: ${{ needs.detect-changes.result }} ${{ needs.pr-run-checks.result }} ${{ needs.test-composites-head.result }} ${{ needs.psscriptanalyzer.result }} ${{ needs.check-markdown-references.result }} ${{ needs.ruff.result }} ${{ needs.pyright.result }} ${{ needs.biome.result }} ${{ needs.dotnet-build.result }} ${{ needs.dotnet-format.result }} ${{ needs.shellcheck.result }} ${{ needs.shfmt.result }} ${{ needs.test-contracts.result }} ${{ needs.actionlint.result }} ${{ needs.jsonschema.result }} ${{ needs.check-action-filename.result }} ${{ needs.pr-run-checks-go.result }} ${{ needs.pr-audit-workflows.result }} ${{ needs.pr-scan-dependencies.result }} + results: ${{ needs.detect-changes.result }} ${{ needs.pr-run-checks.result }} ${{ needs.psscriptanalyzer.result }} ${{ needs.check-markdown-references.result }} ${{ needs.ruff.result }} ${{ needs.pyright.result }} ${{ needs.biome.result }} ${{ needs.dotnet-build.result }} ${{ needs.dotnet-format.result }} ${{ needs.shellcheck.result }} ${{ needs.shfmt.result }} ${{ needs.test-contracts.result }} ${{ needs.actionlint.result }} ${{ needs.jsonschema.result }} ${{ needs.check-action-filename.result }} ${{ needs.pr-run-checks-go.result }} ${{ needs.pr-audit-workflows.result }} ${{ needs.pr-scan-dependencies.result }} diff --git a/README.md b/README.md index 8da62e1e..d1bff56a 100644 --- a/README.md +++ b/README.md @@ -636,27 +636,13 @@ GitHub continues the normal weekly patching of each hosted image generation. `zizmor` is not among the toggles: it has no composite, only the reusable below, so callers that want it keep a separate job. - The composites run by full path at a pinned SHA, because a relative action - path inside a called workflow resolves against the caller's checkout. Those - self-pins are **not** touched by `release-tag.yml`: cutting a release creates a - tag and a GitHub Release at the current `main` HEAD and pushes no commit, so - the commit a release tags still carries whatever self-pins were already in - the tree — necessarily an **earlier** commit's SHA, never the one being cut. - A tagged release therefore runs the composite bodies its pins name, not the - bodies at that tag. Moving a self-pin is a separate pull request: this - repository's own `github-actions` Dependabot group carries no `ignore` for - this repository, unlike a consumer's, and it has opened self-pin bumps here - before; otherwise the bump is made by hand. So the pins can sit several - releases behind, and a release that follows a self-pin bump is what finally - publishes a SHA in which they are current. That pin lag is why this - repository keeps a `composites-head` job in its own `pr-require-checks.yml`: it runs the - same composites through `./.github/actions/` so a pull request that - changes a composite body is still exercised at HEAD instead of passing - against the pinned copy. - Phase 6b retires that job when GitHub's `$/` self-repository syntax becomes - usable, which needs three things: actionlint shipping the `$/` support of - rhysd/actionlint#732 in a version this repository pins, actions/runner#4669 - merging, and one measured cross-repository `$/` run. + The composites run through `$/.github/actions/`, which resolves against + the repository and commit that host the called workflow, not the caller's + checkout. A consumer pinned to a tag therefore runs the composite bodies at + that tag, and this repository's own `./` call runs them at the commit under + test. The two standards-synced composites, `comment-hygiene` and + `machine-specific-paths`, are still referenced by full path at a pinned SHA + until their sync destinations move. ```yaml jobs: From 6c7ba42ba150447820bc60c5c11e43976ddac9a3 Mon Sep 17 00:00:00 2001 From: Kyle Sexton <153232337+kyle-sexton@users.noreply.github.com> Date: Sun, 4 Oct 2026 03:18:07 -0400 Subject: [PATCH 3/6] refactor(pr-run-checks): reach the synced composites through $/ Reference comment-hygiene and machine-specific-paths with $/ like every other composite, so all of them resolve from the release that calls them, and drop the header comment about pin lag and composites-head. Format two test files that the rename left unformatted. Co-Authored-By: Claude Opus 5.5 --- .github/scripts/checks-outcome.test.cjs | 12 --------- .../scripts/network-timeout-policy.test.cjs | 12 +++++++-- .github/scripts/pulumi-deploy-guard.test.cjs | 16 ++++++++++-- .github/workflows/pr-run-checks.yml | 25 +++++-------------- 4 files changed, 30 insertions(+), 35 deletions(-) diff --git a/.github/scripts/checks-outcome.test.cjs b/.github/scripts/checks-outcome.test.cjs index 72df4b49..770e6cd2 100644 --- a/.github/scripts/checks-outcome.test.cjs +++ b/.github/scripts/checks-outcome.test.cjs @@ -97,7 +97,6 @@ test("the join reads every continue-on-error step", () => { `step ${step.id} is never reported by the join`, ); } - const syncManaged = new Set(["comment-hygiene", "machine-specific-paths"]); const renamed = { "change-detection": "detect-changes", editorconfig: "editorconfig-checker", @@ -108,17 +107,6 @@ test("the join reads every continue-on-error step", () => { for (const step of composites) { const name = step.id.replaceAll("_", "-"); const uses = String(step.uses ?? ""); - if (syncManaged.has(name)) { - assert.match( - uses, - new RegExp( - `^melodic-software/ci-workflows/\\.github/actions/${name}@[0-9a-f]{40}$`, - "u", - ), - `step ${step.id} does not reference its composite by pinned full path`, - ); - continue; - } // `$/` resolves against the repository and commit that host the called // workflow, so it works in every consumer. assert.equal( diff --git a/.github/scripts/network-timeout-policy.test.cjs b/.github/scripts/network-timeout-policy.test.cjs index c74922ba..883ee692 100644 --- a/.github/scripts/network-timeout-policy.test.cjs +++ b/.github/scripts/network-timeout-policy.test.cjs @@ -27,7 +27,11 @@ test("immutable release assets have a bounded exponential retry budget", () => { ["zizmor", read(".github/workflows/pr-audit-workflows.yml"), 1], // The standards-sync reusables download no release asset; their npm // installs carry the equivalent fetch-retry budget, asserted below. - ["standards sync", read(".github/workflows/maintenance-sync-standards.yml"), 0], + [ + "standards sync", + read(".github/workflows/maintenance-sync-standards.yml"), + 0, + ], // Linux (bash) and Windows (pwsh) golangci-lint installs carry the same // budget, hence two occurrences. ["go quality", read(".github/workflows/pr-run-checks-go.yml"), 2], @@ -52,7 +56,11 @@ test("immutable release assets have a bounded exponential retry budget", () => { // The standards-sync path's remaining network dependency is npm: every // engine-dependency install carries the equivalent bounded retry budget. for (const [name, content, expected] of [ - ["standards sync npm", read(".github/workflows/maintenance-sync-standards.yml"), 2], + [ + "standards sync npm", + read(".github/workflows/maintenance-sync-standards.yml"), + 2, + ], [ "managed-files-guard npm", read(".github/actions/check-managed-files/action.yml"), diff --git a/.github/scripts/pulumi-deploy-guard.test.cjs b/.github/scripts/pulumi-deploy-guard.test.cjs index 4833421a..99e54ebc 100644 --- a/.github/scripts/pulumi-deploy-guard.test.cjs +++ b/.github/scripts/pulumi-deploy-guard.test.cjs @@ -7,11 +7,23 @@ const test = require("node:test"); const root = path.join(__dirname, "..", ".."); const action = fs.readFileSync( - path.join(root, ".github", "actions", "verify-pulumi-deploy-policy", "action.yml"), + path.join( + root, + ".github", + "actions", + "verify-pulumi-deploy-policy", + "action.yml", + ), "utf8", ); const guard = fs.readFileSync( - path.join(root, ".github", "actions", "verify-pulumi-deploy-policy", "guard.sh"), + path.join( + root, + ".github", + "actions", + "verify-pulumi-deploy-policy", + "guard.sh", + ), "utf8", ); const contract = JSON.parse( diff --git a/.github/workflows/pr-run-checks.yml b/.github/workflows/pr-run-checks.yml index cb0409db..3e11d7c7 100644 --- a/.github/workflows/pr-run-checks.yml +++ b/.github/workflows/pr-run-checks.yml @@ -12,23 +12,10 @@ name: pr-run-checks # `.github/workflows/pr-audit-workflows.yml`, which needs `security-events: write` and an # optional SARIF upload. Callers that want it keep a separate job. # -# Composites are referenced by FULL PATH at a pinned SHA, never -# `./.github/actions/...`: inside a CALLED workflow a relative action path -# resolves against the CALLER's checkout, so a relative reference would fail in -# every consumer (actions/runner#1348). The pins therefore lag one release -# behind the workflow that carries them; Dependabot's `github-actions` group -# bumps them like any other `uses:`. -# -# That pin lag is why pr-require-checks.yml carries a `composites-head` job alongside its -# dogfood call of this workflow. A pull request against this repository that -# breaks one of the composite bodies would otherwise run the OLD body here and -# pass, so the job runs the same composites through `./.github/actions/` at -# the commit under test and restores behavioral coverage at HEAD. -# -# GitHub's `$/` self-repository syntax would remove the pin lag, but -# the pinned actionlint rejects it. `composites-head` stays until -# rhysd/actionlint#732 ships, actions/runner#4669 merges, and a `$/` run is -# measured. +# Composites are referenced with `$/.github/actions/`, never `./`: inside a +# CALLED workflow `./` resolves against the CALLER's checkout, while `$/` +# resolves against the repository and commit hosting this workflow. Every +# composite therefore runs from the same release as the workflow that calls it. # # Skipping: every composite step is gated twice — by its boolean toggle, and by # the `change-detection` filter group NAMED AFTER that toggle. A group the @@ -280,7 +267,7 @@ jobs: id: machine_specific_paths if: ${{ inputs.machine-specific-paths && fromJSON(steps.detect.outputs.results || '{}')['machine-specific-paths'] != 'false' }} continue-on-error: true - uses: melodic-software/ci-workflows/.github/actions/machine-specific-paths@ac062650c46005edb4787aff378347746bf63804 # v0.27.0 + uses: $/.github/actions/machine-specific-paths with: exclude: ${{ inputs.machine-specific-paths-exclude }} @@ -294,7 +281,7 @@ jobs: id: comment_hygiene if: ${{ inputs.comment-hygiene && fromJSON(steps.detect.outputs.results || '{}')['comment-hygiene'] != 'false' }} continue-on-error: true - uses: melodic-software/ci-workflows/.github/actions/comment-hygiene@ac062650c46005edb4787aff378347746bf63804 # v0.27.0 + uses: $/.github/actions/comment-hygiene with: exclude: ${{ inputs.comment-hygiene-exclude }} From c50ed028c2a48dc4695bef3cf187f8ac1652cbdf Mon Sep 17 00:00:00 2001 From: Kyle Sexton <153232337+kyle-sexton@users.noreply.github.com> Date: Sun, 4 Oct 2026 03:18:24 -0400 Subject: [PATCH 4/6] docs: drop the pinned-SHA note for the synced composites Co-Authored-By: Claude Opus 5.5 --- README.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/README.md b/README.md index d1bff56a..be84f2cb 100644 --- a/README.md +++ b/README.md @@ -640,9 +640,7 @@ GitHub continues the normal weekly patching of each hosted image generation. the repository and commit that host the called workflow, not the caller's checkout. A consumer pinned to a tag therefore runs the composite bodies at that tag, and this repository's own `./` call runs them at the commit under - test. The two standards-synced composites, `comment-hygiene` and - `machine-specific-paths`, are still referenced by full path at a pinned SHA - until their sync destinations move. + test. ```yaml jobs: From b2f99f91a6a18a3814d4f039e5d2156fba7ece62 Mon Sep 17 00:00:00 2001 From: Kyle Sexton <153232337+kyle-sexton@users.noreply.github.com> Date: Sun, 4 Oct 2026 03:22:37 -0400 Subject: [PATCH 5/6] fix(pr-require-checks): run the dogfood review lanes only on the events the self callers ran on The folded pr-review and pr-review-security jobs also ran on edited, labeled and unlabeled events from fork pull requests. Restrict them to opened, synchronize, reopened and ready_for_review, and rename the README caller examples to the slot job ids. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/pr-require-checks.yml | 4 ++-- README.md | 12 ++++++------ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/pr-require-checks.yml b/.github/workflows/pr-require-checks.yml index c2147faa..6f42c748 100644 --- a/.github/workflows/pr-require-checks.yml +++ b/.github/workflows/pr-require-checks.yml @@ -509,7 +509,7 @@ jobs: # reviews it. Advisory: none of the three is in `ci-status`. Each passes only # the one named secret its reusable declares. pr-review: - if: ${{ github.event_name != 'issues' && github.event_name == 'pull_request' && !(github.event.pull_request.head.repo.full_name == github.repository && (contains(fromJSON('["labeled","unlabeled"]'), github.event.action) || (github.event.action == 'edited' && !github.event.changes.base))) }} + if: ${{ github.event_name != 'issues' && github.event_name == 'pull_request' && !(github.event.pull_request.head.repo.full_name == github.repository && (contains(fromJSON('["labeled","unlabeled"]'), github.event.action) || (github.event.action == 'edited' && !github.event.changes.base))) && contains(fromJSON('["opened","synchronize","reopened","ready_for_review"]'), github.event.action) }} permissions: contents: read pull-requests: write @@ -519,7 +519,7 @@ jobs: CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} pr-review-security: - if: ${{ github.event_name != 'issues' && github.event_name == 'pull_request' && !(github.event.pull_request.head.repo.full_name == github.repository && (contains(fromJSON('["labeled","unlabeled"]'), github.event.action) || (github.event.action == 'edited' && !github.event.changes.base))) }} + if: ${{ github.event_name != 'issues' && github.event_name == 'pull_request' && !(github.event.pull_request.head.repo.full_name == github.repository && (contains(fromJSON('["labeled","unlabeled"]'), github.event.action) || (github.event.action == 'edited' && !github.event.changes.base))) && contains(fromJSON('["opened","synchronize","reopened","ready_for_review"]'), github.event.action) }} permissions: contents: read pull-requests: write diff --git a/README.md b/README.md index be84f2cb..6b020cc0 100644 --- a/README.md +++ b/README.md @@ -349,7 +349,7 @@ consumer to audit it. ```yaml jobs: - changes: + detect-changes: permissions: contents: read statuses: write @@ -372,7 +372,7 @@ consumer to audit it. with: carry-forward-wait-seconds: '0' rerun-contract-only-siblings: 'true' - results: ${{ needs.changes.result }} ... + results: ${{ needs.detect-changes.result }} ... ``` **To keep a wait instead, size the ceiling from the repository's own measured @@ -443,7 +443,7 @@ consumer to audit it. fails OPEN to `"true"`, while unhonorable pattern syntax (`!`, `?`, `+`) and malformed group config are hard errors even on fallback runs. The required-check interplay is load-bearing: gate each lane job with - `!cancelled() && fromJSON(needs.changes.outputs.results || '{}')[''] + `!cancelled() && fromJSON(needs.detect-changes.outputs.results || '{}')[''] != 'false'` (never `== 'true'` — an unset output must run the lane, not skip it), keep aggregating through the always-running `ci-status` gateway with `treat-skipped-as: pass` (a job-level skip reports `skipped`, which @@ -628,9 +628,9 @@ GitHub continues the normal weekly patching of each hosted image generation. `outcome` (`success` or `failure`). Every composite runs under `continue-on-error: true` and one join step names the first failure and fails the job, so one failing tool never hides the rest. Gate downstream lanes with - `fromJSON(needs.checks.outputs.results || '{}')[''] != 'false'`: a + `fromJSON(needs.pr-run-checks.outputs.results || '{}')[''] != 'false'`: a reusable workflow publishes no outputs when its job fails, so - `needs.checks.result` stays the authoritative verdict. The caller's job block + `needs.pr-run-checks.result` stays the authoritative verdict. The caller's job block must grant `contents: read` and `pull-requests: read` — a called workflow cannot elevate, and the detection pass reads the pull request's file listing. `zizmor` is not among the toggles: it has no composite, only the reusable @@ -644,7 +644,7 @@ GitHub continues the normal weekly patching of each hosted image generation. ```yaml jobs: - checks: + pr-run-checks: permissions: contents: read pull-requests: read From 4754389ed424faabe300b29575a5ef8e1b0024e3 Mon Sep 17 00:00:00 2001 From: Kyle Sexton <153232337+kyle-sexton@users.noreply.github.com> Date: Sun, 4 Oct 2026 03:27:31 -0400 Subject: [PATCH 6/6] fix(pr-require-checks): validate nested composite action metadata The jsonschema lane's bash glob did not reach the two actions moved under .github/actions/pr-require-checks/, so the ci-status actions skipped schema validation. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/pr-require-checks.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pr-require-checks.yml b/.github/workflows/pr-require-checks.yml index 6f42c748..8bf63175 100644 --- a/.github/workflows/pr-require-checks.yml +++ b/.github/workflows/pr-require-checks.yml @@ -349,7 +349,7 @@ jobs: uses: ./.github/actions/check-jsonschema with: builtin-schema: vendor.github-actions - files: .github/actions/*/action.yml + files: .github/actions/*/action.yml .github/actions/*/*/action.yml check-action-filename: if: ${{ github.event_name != 'issues' && !(github.event.pull_request.head.repo.full_name == github.repository && (contains(fromJSON('["labeled","unlabeled"]'), github.event.action) || (github.event.action == 'edited' && !github.event.changes.base))) }}