From 23be0824f2ea4fc8050c6b5a3faa41261c49be54 Mon Sep 17 00:00:00 2001 From: Nikolai Emil Damm Date: Sat, 24 Oct 2026 19:44:33 +0200 Subject: [PATCH 1/3] feat(ci)!: retire the unused lint workflow Retain the live Go pipeline, both signing assertions, scanner and credential controls after the final production consumer migrated. BREAKING CHANGE: the whole-repository lint workflow is no longer exported. Existing immutable pins remain available; consuming repositories own this single-product job. Refs #731. --- .github/agent-guides/invariants.md | 6 +- .github/agent-guides/workflows.md | 4 +- .github/scripts/ci-selection/inventory.json | 2 - .../tests/megalinter-clean-fixture/good.json | 4 - .../test-actionlint-arguments-lockstep.sh | 6 +- .../test-catalogue-pin-guards-controls.sh | 4 +- .../tests/test-catalogue-tool-pin-parity.sh | 6 +- .github/tests/test-ci-catalogue-scope.sh | 7 +- .../tests/test-lint-credential-boundary.sh | 22 ++- .github/tests/test-lint-recovery-gates.sh | 9 +- .../test-lint-signed-commit-shape-ablation.sh | 5 +- .github/tests/test-lint-signed-commit.sh | 10 +- .github/tests/test-lint-workflow-fixes.sh | 2 +- .github/tests/test-megalinter-go-toolchain.sh | 25 +-- ...ti-trigger-input-gate-polarity-ablation.sh | 15 +- .../test-multi-trigger-input-gate-polarity.sh | 6 +- .github/tests/test-workflow-readme-parity.sh | 4 +- .github/workflows/ci.yaml | 45 +---- .github/workflows/lint.yaml | 182 ------------------ .../validate-go-project-readonly.yaml | 6 +- .github/workflows/validate-go-project.yaml | 6 +- .scripts/check-workflow-readme.sh | 4 +- README.md | 63 ------ 23 files changed, 75 insertions(+), 368 deletions(-) delete mode 100644 .github/tests/megalinter-clean-fixture/good.json delete mode 100644 .github/workflows/lint.yaml diff --git a/.github/agent-guides/invariants.md b/.github/agent-guides/invariants.md index 6cc602cb..edb0ed26 100644 --- a/.github/agent-guides/invariants.md +++ b/.github/agent-guides/invariants.md @@ -25,9 +25,9 @@ unclassified sources and partial expression evidence fail closed as UNKNOWN. Kee `lint-ci-coverage-parity`, whose result gates the required check. This admission guard does not prove the cleanup selector behavior still tracked in #350 or replace individual user-path tests. -**MegaLinter Go toolchain:** host-side setup does not replace the Docker action’s compiler. The production Go workflow, its generated read-only projection, and the whole-repository lint workflow select the exact `setup-go` output through a fixed named `GOTOOLCHAIN`, without `+auto`. The whole-repository workflow keeps `local` when no Go declaration is supplied. `test-megalinter-go-toolchain.sh` rejects binding substitutions; native positive Go and whole-repository fixtures prove real scanner analysis. Preserve the enabled scanners and fixer credential boundary. +**MegaLinter Go toolchain:** host-side setup does not replace the Docker action’s compiler. The production Go workflow and its generated read-only projection select the exact `setup-go` output through a fixed named `GOTOOLCHAIN`, without `+auto`. `test-megalinter-go-toolchain.sh` rejects binding substitutions in both entrypoints; native positive Go fixtures prove real scanner analysis. Preserve the enabled scanners and fixer credential boundary. -**MegaLinter root configuration:** Both production entrypoints and the generated +**MegaLinter root configuration:** Production Go and its generated read-only projection select the repository-root configuration; GitHub's Docker action handler translates its host path into the container mount. The native `test-lint-blocks` job executes five independent cases on one runner using the exact @@ -41,7 +41,7 @@ pinned loader, not merely through an image or runner error. Keep binding mutatio per-file receipt controls and immutable image-pin parity required. The synthetic fixture narrows only its own JSON checker; production scanner scope is unchanged. -**Fixer-lane credential boundary (tested invariant):** `validate-go-project.yaml`'s three fixer lanes (`tidy`, `golangci-lint`, `lint` — every job that exports `fixes-created`) run tooling configured by the pull request under review, so they hold no credential that can write to the branch: `contents: read`, no App-token step, no App private key, no secret other than `GITHUB_TOKEN` (every expression spelling, case-insensitively, matched on complete scalars, and including the workflow-level `env:` every job inherits), and `persist-credentials: false` on every checkout. The commit is made on a fresh runner by `apply-signed-fixes.yaml`. `ci.yaml` asserts this with `.github/tests/test-fixer-credential-boundary.sh` (workflow- and job-scoped, so `lint.yaml` reuses it) and proves each assertion fires for its own reason with `test-fixer-credential-boundary-ablation.sh`; the lane list is derived from the workflow, so a new fixer lane is covered by construction. **A lane reshape must keep that guard green** — never move a write credential back into a fixer lane to make a step simpler. +**Fixer-lane credential boundary (tested invariant):** `validate-go-project.yaml`'s three fixer lanes (`tidy`, `golangci-lint`, `lint` — every job that exports `fixes-created`) run tooling configured by the pull request under review, so they hold no credential that can write to the branch: `contents: read`, no App-token step, no App private key, no secret other than `GITHUB_TOKEN` (every expression spelling, case-insensitively, matched on complete scalars, and including the workflow-level `env:` every job inherits), and `persist-credentials: false` on every checkout. The commit is made on a fresh runner by `apply-signed-fixes.yaml`. `ci.yaml` asserts this with `.github/tests/test-fixer-credential-boundary.sh` (workflow- and job-scoped, including the read-only Go projection) and proves each assertion fires for its own reason with `test-fixer-credential-boundary-ablation.sh`; the lane list is derived from the workflow, so a new fixer lane is covered by construction. **A lane reshape must keep that guard green** — never move a write credential back into a fixer lane to make a step simpler. **gh installer digest pinning (tested invariant):** `.scripts/ensure-gh-skill.sh` verifies a downloaded cli/cli archive against `.scripts/gh-release-digests.tsv`, a reviewed version-to-digest manifest, in addition to attestation and the release-served checksum. That manifest is the only gate that rejects a **same-or-newer** substituted archive: attestation cannot tell cli/cli releases apart, the checksums file ships from the same mutable release, and the post-install version assertion is a floor rather than an equality. Enforcement is per row — an unpinned version warns and falls through, so a consumer passing a custom `gh-version` is never blocked. `test-ensure-gh-skill-script` guards it (cases 8e–8j: substitution rejected, matching digest installs, unpinned warns, malformed row fails closed); keep those green and refresh rows with `bash .scripts/refresh-gh-digests.sh ` rather than by hand. diff --git a/.github/agent-guides/workflows.md b/.github/agent-guides/workflows.md index a42e2fbf..5823da19 100644 --- a/.github/agent-guides/workflows.md +++ b/.github/agent-guides/workflows.md @@ -253,11 +253,11 @@ A new job/step/behaviour must not go live for every consumer the moment it merge - You **cannot** wrap `if:` around an individual `with:` input — gate the whole **step** (or **job**), or select the value with a ternary in the `with:` value (`${{ inputs. && 'a' || 'b' }}`). - **Test both states.** Per the feature-flag-first "test both states" rule, cover the flag **off** (default) and **on** with `[Test]` jobs (see *Test jobs* above) — a flag whose off-path is untested can regress silently. -**Workflow-file fixes:** `manual-workflow-fixes` defaults to false in both lint workflows. Opted-in +**Workflow-file fixes:** `manual-workflow-fixes` defaults to false in Go and its read-only projection. Opted-in runs export the full patch but withhold automatic signing when any changed path is under the repository-root `.github/workflows/`. Keep mixed edits and renames in one downloadable artifact, with a warning explaining local application. Do not widen the signer's -token permissions to apply them. `test-lint-workflow-fixes.sh` exercises both exporters against real +token permissions to apply them. `test-lint-workflow-fixes.sh` exercises all three Go fixer lanes against real Git fixtures in both flag states, including binary edits, mode changes, additions, deletions, renames, and ordinary paths. Eligible opted-in runs recover patches after lint errors without making the failed job successful; cancelled runs do not force recovery. `test-lint-recovery-gates.sh` exercises diff --git a/.github/scripts/ci-selection/inventory.json b/.github/scripts/ci-selection/inventory.json index 9be61b2b..795cccc2 100644 --- a/.github/scripts/ci-selection/inventory.json +++ b/.github/scripts/ci-selection/inventory.json @@ -160,8 +160,6 @@ "test-validate-go-test-blocks": ["actions/upload-coverage/"], "test-validate-go-project-concurrency-key": ["actions/upload-coverage/"], "test-validate-go-gate-lockstep": ["actions/upload-coverage/", "actions/dependency-review/"], - "test-lint-workflow": [], - "test-lint-workflow-manual-fixes": [], "test-lint-blocks": [], "test-prepare-fixes": [], "test-lint-gate-lockstep": ["actions/upload-coverage/"], diff --git a/.github/tests/megalinter-clean-fixture/good.json b/.github/tests/megalinter-clean-fixture/good.json deleted file mode 100644 index 336f312b..00000000 --- a/.github/tests/megalinter-clean-fixture/good.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "purpose": "clean fixture for the lint.yaml pass-on-good-input self-test", - "valid": true -} diff --git a/.github/tests/test-actionlint-arguments-lockstep.sh b/.github/tests/test-actionlint-arguments-lockstep.sh index 19116cdd..3cd9cc63 100644 --- a/.github/tests/test-actionlint-arguments-lockstep.sh +++ b/.github/tests/test-actionlint-arguments-lockstep.sh @@ -7,7 +7,7 @@ # a pattern that also swallowed other unexpected concurrency keys would hide real typos. # # Asserted: -# 1. lint.yaml and validate-go-project.yaml pass identical ACTION_ACTIONLINT_ARGUMENTS; +# 1. production Go and its read-only projection pass identical ACTION_ACTIONLINT_ARGUMENTS; # 2. those arguments keep the code-quality ignore and carry the queue ignore; # 3. the queue pattern matches actionlint's real queue message and does NOT match the # same message for any other key; @@ -32,7 +32,7 @@ check() { return 1 fi if [[ "$lint_args" != "$go_args" ]]; then - echo "lint.yaml and validate-go-project.yaml pass different actionlint arguments ('$lint_args' vs '$go_args')" + echo "production Go and its read-only projection pass different actionlint arguments ('$lint_args' vs '$go_args')" status=1 fi @@ -67,7 +67,7 @@ pattern_is_narrow() { } status=0 -lint_args="$(megalinter_args .github/workflows/lint.yaml)" +lint_args="$(megalinter_args .github/workflows/validate-go-project-readonly.yaml)" go_args="$(megalinter_args .github/workflows/validate-go-project.yaml)" if ! out="$(check "$lint_args" "$go_args")"; then diff --git a/.github/tests/test-catalogue-pin-guards-controls.sh b/.github/tests/test-catalogue-pin-guards-controls.sh index 6a3802fe..05a61ba6 100644 --- a/.github/tests/test-catalogue-pin-guards-controls.sh +++ b/.github/tests/test-catalogue-pin-guards-controls.sh @@ -67,7 +67,7 @@ for family in golangci megalinter version; do yq -i "(.jobs.test-validate-go-lint-blocks.steps[] | select(.id == \"lint\")).uses = \"golangci/golangci-lint-action@$sha\"" "$fixture/.github/workflows/ci.yaml" ;; megalinter) - for gate in validate-go-project lint; do + for gate in validate-go-project validate-go-project-readonly; do yq -i "(.jobs.lint.steps[] | select(.id == \"ml\")).uses = \"oxsecurity/megalinter/flavors/go@$sha\"" "$fixture/.github/workflows/$gate.yaml" done yq -i "(.jobs.test-lint-blocks.steps[] | select((.id // \"\") | test(\"^ml-\"))).uses = \"oxsecurity/megalinter/flavors/go@$sha\"" "$fixture/.github/workflows/ci.yaml" @@ -103,7 +103,7 @@ for file in validate-go-project ci; do reject parity ".github/workflows/$file.yaml" "(.jobs.$job.steps[] | select(.id == \"$id\")).with.version = \"\"" 'versions differ or are missing' reject parity ".github/workflows/$file.yaml" ".jobs.$job.steps += [.jobs.$job.steps[] | select(.id == \"$id\")]" 'exactly one full-SHA' done -for file in validate-go-project lint ci; do +for file in validate-go-project validate-go-project-readonly ci; do job=lint ids=(ml) diagnostic='exactly one full-SHA' if [[ "$file" == ci ]]; then job=test-lint-blocks ids=(ml-root ml-nested ml-absent ml-malformed ml-finding) diff --git a/.github/tests/test-catalogue-tool-pin-parity.sh b/.github/tests/test-catalogue-tool-pin-parity.sh index 1ae99fbb..751559fb 100644 --- a/.github/tests/test-catalogue-tool-pin-parity.sh +++ b/.github/tests/test-catalogue-tool-pin-parity.sh @@ -4,7 +4,7 @@ set -euo pipefail scope="${1:-all}" ci="${2:-.github/workflows/ci.yaml}" go_gate="${3:-.github/workflows/validate-go-project.yaml}" -lint_gate="${4:-.github/workflows/lint.yaml}" +readonly_gate="${4:-.github/workflows/validate-go-project-readonly.yaml}" fail() { echo "::error::$*" >&2 exit 1 @@ -55,10 +55,10 @@ if [[ "$scope" == all || "$scope" == megalinter ]]; then jq -ce '[.[] | .uses // "" | select(startswith("oxsecurity/megalinter/flavors/go@"))] | select(length == 5 and all(.[]; test("^oxsecurity/megalinter/flavors/go@[0-9a-f]{40}$")))')" || fail 'CI must select five full-SHA MegaLinter invocations' - for gate in "$go_gate" "$lint_gate"; do + for gate in "$go_gate" "$readonly_gate"; do gate_pin="$(read_pin "$gate" lint ml oxsecurity/megalinter/flavors/go)" jq -e --arg pin "$gate_pin" 'all(.[]; . == $pin)' <<<"$fixture_pins" >/dev/null || fail "$gate and test-lint-blocks use different MegaLinter pins" done - echo 'PASS: both production gates and the failure fixture use the same complete MegaLinter pin' + echo 'PASS: production Go, its read-only projection and the failure fixture use the same complete MegaLinter pin' fi diff --git a/.github/tests/test-ci-catalogue-scope.sh b/.github/tests/test-ci-catalogue-scope.sh index 64d90691..b4f08ce8 100644 --- a/.github/tests/test-ci-catalogue-scope.sh +++ b/.github/tests/test-ci-catalogue-scope.sh @@ -207,9 +207,8 @@ git -C "${current}" remote add origin "${current}" [[ "$(cat "${output}")" == catalogue=false ]] || fail 'real workflow did not recognize deployment-only changes' # Compose real Git classification, the production selector and the actual -# required reducer. A false scope omits 48 gated smoke jobs plus the separately -# event-gated dependency-review smoke, not the other 32 selectable jobs. Only -# the retired policy-sync and three cleanup fixtures leave the previous floor. +# required reducer. A false scope omits the inventoried smoke jobs plus the +# separately event-gated dependency-review smoke, not the preserved jobs. yq -o=json . "${ci}" >"${fixture}/workflow.json" selection_output="${fixture}/selection-output" : >"${selection_output}" @@ -233,8 +232,6 @@ expected_selected='[ "test-govulncheck-main-coverage-blocks", "test-govulncheck-timeout", "test-lint-gate-lockstep", - "test-lint-workflow", - "test-lint-workflow-manual-fixes", "test-release-please-semantics", "test-run-dotnet-tests-mtp", "test-run-dotnet-tests-workflow", diff --git a/.github/tests/test-lint-credential-boundary.sh b/.github/tests/test-lint-credential-boundary.sh index 57900c37..fa3741e5 100644 --- a/.github/tests/test-lint-credential-boundary.sh +++ b/.github/tests/test-lint-credential-boundary.sh @@ -2,7 +2,7 @@ set -euo pipefail -lint_workflow="${1:-.github/workflows/lint.yaml}" +lint_workflow="${1:-.github/workflows/validate-go-project-readonly.yaml}" ci_workflow="${2:-.github/workflows/ci.yaml}" fail() { @@ -11,25 +11,29 @@ fail() { } caller_contents="$( - yq -r '.jobs.test-lint-workflow.permissions.contents // ""' "$ci_workflow" + yq -r '.jobs.test-validate-go-project.permissions.contents // ""' "$ci_workflow" )" [[ "$caller_contents" == "read" ]] || fail "the clean-fixture caller must grant contents: read, found '${caller_contents:-unset}'" caller_write_scopes="$( yq -r \ - '[.jobs.test-lint-workflow.permissions + '[.jobs.test-validate-go-project.permissions | to_entries[] - | select(.value == "write")] | length' \ + | select(.value == "write" and .key != "code-quality")] | length' \ "$ci_workflow" )" [[ "$caller_write_scopes" == "0" ]] || - fail "the clean-fixture caller must not grant any write-scoped GITHUB_TOKEN permission" + fail "the clean-fixture caller may grant only coverage's code-quality write scope" + +[[ "$(yq -r '.jobs.test-validate-go-project.uses' "$ci_workflow")" == './.github/workflows/validate-go-project-readonly.yaml' ]] || + fail 'the clean-fixture caller must exercise the actual read-only Go entrypoint' # The workflow-scoped half — contents: read, no App token, no App key, persist-credentials: false — # lives in test-fixer-credential-boundary.sh so the same assertions run against the org-required -# Go pipeline's three fixer lanes. What follows is the STRICTER set that only lint.yaml meets: -# no write scope of any kind and no GitHub token at all. +# Go pipeline's three fixer lanes. The read-only projection's lint job meets a STRICTER +# boundary: no write scope of any kind and no GitHub token at all. Its caller grants +# code-quality write for separate coverage jobs; the lint job explicitly narrows it. bash "$(dirname "${BASH_SOURCE[0]}")/test-fixer-credential-boundary.sh" "$lint_workflow" lint >/dev/null lint_write_scopes="$( @@ -51,7 +55,7 @@ lint_token_refs() { # '[((.env // {}), .jobs.lint) | .. | select(tag == "!!str") - | select(test("(?i)secrets[[:space:]]*(\\.|\\[)|github[[:space:]]*(\\.token|\\[[[:space:]]*[\"'"'"']token[\"'"'"'][[:space:]]*\\])|toJSON[[:space:]]*\\([[:space:]]*(secrets|github)"))] | length' \ + | select(test("(?i)\\bsecrets\\b|\\bgithub[[:space:]]*(\\.[[:space:]]*token|\\[)|tojson[[:space:]]*\\([[:space:]]*github\\b"))] | length' \ "$1" } lint_tokens="$(lint_token_refs "$lint_workflow")" @@ -64,7 +68,7 @@ lint_tokens="$(lint_token_refs "$lint_workflow")" work="$(mktemp -d)" trap 'rm -rf "$work"' EXIT # shellcheck disable=SC2016 # literal Actions expressions handed to yq -for mutation in '.env.TOKEN = "${{ github.token }}"' '.jobs.lint.env.TOKEN = "${{ github.token }}"' '.jobs.lint.env.TOKEN = "${{ GITHUB.TOKEN }}"' '.jobs.lint.env.ALL = "${{ toJson(Secrets) }}"'; do +for mutation in '.env.TOKEN = "${{ github.token }}"' '.jobs.lint.env.TOKEN = "${{ github.token }}"' '.jobs.lint.env.TOKEN = "${{ GITHUB.TOKEN }}"' '.jobs.lint.env.ALL = "${{ toJson(Secrets) }}"' '.jobs.lint.env.TOKEN = "${{ github[format('"'"'tok{0}'"'"', '"'"'en'"'"')] }}"'; do yq "$mutation" "$lint_workflow" >"$work/mutant.yaml" [[ "$(lint_token_refs "$work/mutant.yaml")" == "1" ]] || fail "the lint token scan must count a github.token planted with: $mutation" diff --git a/.github/tests/test-lint-recovery-gates.sh b/.github/tests/test-lint-recovery-gates.sh index 27b0622b..876a80e5 100644 --- a/.github/tests/test-lint-recovery-gates.sh +++ b/.github/tests/test-lint-recovery-gates.sh @@ -5,7 +5,7 @@ set -euo pipefail root="$(cd "$(dirname "$0")/../.." && pwd)" work="$(mktemp -d)" trap 'rm -rf "$work"' EXIT -for workflow in lint validate-go-project validate-go-project-readonly apply-signed-fixes ci; do +for workflow in validate-go-project validate-go-project-readonly apply-signed-fixes ci; do yq -o=json '.' "$root/.github/workflows/$workflow.yaml" >"$work/$workflow.json" done yq -o=json '.' "$root/.github/actions/prepare-fixes/action.yaml" >"$work/exporter.json" @@ -143,11 +143,10 @@ function verify(name, prepareGate, uploadGate, composite) { const optedIn = input === true || String(input).toLowerCase() === 'true'; const recoveryAllowed = status !== 'cancelled' && (status === 'success' || optedIn); const eligible = apply && event === 'pull_request' && !fork; - const expectedPrepare = recoveryAllowed && (name !== 'lint' || eligible); - const expectedUpload = recoveryAllowed && changed && (name === 'lint' || (eligible && !bots.includes(author) && !bots.includes(owner))); + const expectedPrepare = recoveryAllowed; + const expectedUpload = recoveryAllowed && changed && eligible && !bots.includes(author) && !bots.includes(owner); const values = { [`inputs.${flag}`]: input ?? '', - 'inputs.apply-fixes': apply, 'needs.changes.outputs.signed-fixes': String(apply), 'github.event_name': event, 'github.event.pull_request.head.repo.fork': fork, @@ -292,7 +291,7 @@ for (const lane of ['tidy', 'golangci-lint', 'lint']) { } console.log(`PASS: ${lane}, exact-commit wiring and ${fixtures.length} eligibility/read-only cases`); } -for (const name of ['lint', 'validate-go-project', 'validate-go-project-readonly']) { +for (const name of ['validate-go-project', 'validate-go-project-readonly']) { const workflow = JSON.parse(fs.readFileSync(`${directory}/${name}.json`, 'utf8')); const job = workflow.jobs.lint; const prepare = job.steps.find(step => step.id === 'fixes'); diff --git a/.github/tests/test-lint-signed-commit-shape-ablation.sh b/.github/tests/test-lint-signed-commit-shape-ablation.sh index d720aeaa..acac2335 100755 --- a/.github/tests/test-lint-signed-commit-shape-ablation.sh +++ b/.github/tests/test-lint-signed-commit-shape-ablation.sh @@ -42,8 +42,9 @@ fail() { sandbox_guard() { local fixture="$1" dir dir="$(mktemp -d "$work/sandbox.XXXXXX")" - mkdir -p "$dir/.github/workflows" "$dir/.github/tests" - cp .github/workflows/lint.yaml "$dir/.github/workflows/lint.yaml" + mkdir -p "$dir/.github/workflows" "$dir/.github/tests" "$dir/.github/actions/prepare-fixes" + cp .github/workflows/validate-go-project.yaml "$dir/.github/workflows/validate-go-project.yaml" + cp .github/actions/prepare-fixes/action.yaml "$dir/.github/actions/prepare-fixes/action.yaml" cp "$guard" "$dir/$guard" cp "$fixture" "$dir/$signer" (cd "$dir" && bash "$guard" 2>&1) || true diff --git a/.github/tests/test-lint-signed-commit.sh b/.github/tests/test-lint-signed-commit.sh index 345a2d4e..13dadf73 100644 --- a/.github/tests/test-lint-signed-commit.sh +++ b/.github/tests/test-lint-signed-commit.sh @@ -16,7 +16,7 @@ set -euo pipefail -caller_workflow="${1:-.github/workflows/lint.yaml}" +caller_workflow="${1:-.github/workflows/validate-go-project.yaml}" signer_workflow="${2:-.github/workflows/apply-signed-fixes.yaml}" consumer_relative_signer="./${signer_workflow#./}" workflow_source_signer="\$/${signer_workflow#./}" @@ -250,13 +250,13 @@ job_if="$(yq -r "${job}.if // \"\"" "$signer_workflow")" grep -qE "$bot_suppression" <<<"$job_if" || fail "apply-fixes must itself suppress dependency-bot pull requests; their branches are owned by their automation" -# 8. The pull-request event predicate moved here with the other gates, and lint.yaml no longer -# supplies it. Without this assertion a later edit could let a caller reach the signing job on a +# 8. The signing workflow owns its pull-request event predicate; callers cannot be relied on +# to supply it. Without this assertion a later edit could let a caller reach the signing job on a # non-pull_request event, where github.head_ref is empty -- so the checkout and expectedHeadOid # would resolve against nothing, and the branch the commit targets would not be the one under # review. grep -q "github.event_name == 'pull_request'" <<<"$job_if" || - fail "apply-fixes must itself require a pull_request event; lint.yaml no longer supplies that gate" + fail "apply-fixes must itself require a pull_request event rather than relying on a caller" # 9. Fork pull requests never reach the commit. They get no secrets, so the App token cannot be # minted; committing there could only fail, and the caller lints them read-only instead. @@ -276,7 +276,6 @@ conjuncts="$(grep -o '&&' <<<"$job_if" | wc -l | tr -d ' ')" [[ "$conjuncts" == "3" ]] || fail "apply-fixes' if: must carry exactly the 4 audited conjuncts (pull_request, non-fork, non-bot author, non-bot pr-owner), found $((conjuncts + 1)); a predicate added here can suppress every eligible pull request while assertions 7-9 stay green" - # Exact dependency aliases in BOTH contexts are the recorded policy (#278). IFS= read -r expected_policy <<'POLICY' github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork != true && !contains(fromJSON('["dependabot[bot]","dependabot","renovate[bot]","renovatebot","renovate"]'), github.event.pull_request.user.login) && !contains(fromJSON('["dependabot[bot]","dependabot","renovate[bot]","renovatebot","renovate"]'), inputs.pr-owner) @@ -335,7 +334,6 @@ step_shape="$( [[ "$step_shape" == "uses,run,uses,uses,uses,run,run" ]] || fail "apply-fixes' step inventory changed (found '${step_shape}', audited 'uses,run,uses,uses,uses,run,run'); a step added or retyped here can execute checked-out code, which is the premise CodeQL alert 312 was dismissed on -- re-audit the job and update this pin deliberately" - # 13. The job is pinned by CONTENT, in two halves. Assertion 12 sees a step ADDED; it cannot see a # change INSIDE an existing step, and the two steps that run after the checkout do so with the # untrusted head on disk and the write-scoped App token available. diff --git a/.github/tests/test-lint-workflow-fixes.sh b/.github/tests/test-lint-workflow-fixes.sh index 46ce5d6d..ce9cfe94 100644 --- a/.github/tests/test-lint-workflow-fixes.sh +++ b/.github/tests/test-lint-workflow-fixes.sh @@ -12,7 +12,7 @@ trap 'rm -rf "$work"' EXIT fail() { echo "FAIL: $*" >&2; exit 1; } -for workflow in lint:lint validate-go-project:tidy validate-go-project:golangci-lint validate-go-project:lint; do +for workflow in validate-go-project:tidy validate-go-project:golangci-lint validate-go-project:lint; do job="${workflow#*:}" file="$root/.github/workflows/${workflow%:*}.yaml" prepare="$(JOB="$job" yq -o=json '.jobs[strenv(JOB)].steps[] | select(.id == "fixes")' "$file")" diff --git a/.github/tests/test-megalinter-go-toolchain.sh b/.github/tests/test-megalinter-go-toolchain.sh index 7365b981..9a92a2ad 100644 --- a/.github/tests/test-megalinter-go-toolchain.sh +++ b/.github/tests/test-megalinter-go-toolchain.sh @@ -5,29 +5,24 @@ set -euo pipefail root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" scratch="$(mktemp -d)" trap 'rm -rf "$scratch"' EXIT -yq -o=json '.' "$root/.github/workflows/lint.yaml" >"$scratch/lint.json" yq -o=json '.' "$root/.github/workflows/validate-go-project.yaml" >"$scratch/go.json" yq -o=json '.' "$root/.github/workflows/validate-go-project-readonly.yaml" >"$scratch/readonly.json" -jq -s '{lint:.[0],go:.[1],readonly:.[2]}' "$scratch/lint.json" "$scratch/go.json" "$scratch/readonly.json" >"$scratch/actual.json" +jq -s '{go:.[0],readonly:.[1]}' "$scratch/go.json" "$scratch/readonly.json" >"$scratch/actual.json" cat >"$scratch/admit.jq" <<'JQ' def setup: [.jobs.lint.steps[] | select(.id == "setup-go")]; def ml: [.jobs.lint.steps[] | select(.id == "ml")]; -(.go | setup) as $go | (.lint | setup) as $lint -| ($go | length) == 1 and ($lint | length) == 1 +(.go | setup) as $go +| ($go | length) == 1 and ($go[0] | (has("if") | not) and .with["go-version-file"] == "${{ needs.changes.outputs.module-directory }}/go.mod") -and ($lint[0] | .if == "${{ inputs.go-version-file != '' }}" and .with["go-version-file"] == "${{ inputs.go-version-file }}") -and all([$go[0],$lint[0]][]; .uses | test("^actions/setup-go@[0-9a-f]{40}$")) +and ($go[0].uses | test("^actions/setup-go@[0-9a-f]{40}$")) and ($go[0].with | keys == ["cache","go-version-file"] and .cache == false) -and ($lint[0].with | keys == ["go-version-file"]) -and (.go | ml | length) == 1 and (.lint | ml | length) == 1 +and (.go | ml | length) == 1 and (.go | ml | .[0].env.GOTOOLCHAIN) == "go${{ steps.setup-go.outputs.go-version }}" -and (.lint | ml | .[0].env.GOTOOLCHAIN) == "${{ inputs.go-version-file != '' && format('go{0}', steps.setup-go.outputs.go-version) || 'local' }}" and (.readonly | setup) == $go and (.readonly | ml | length) == 1 and (.readonly | ml | .[0].env.GOTOOLCHAIN) == (.go | ml | .[0].env.GOTOOLCHAIN) and (.readonly.jobs.lint.steps | map(.id) | index("setup-go") < index("ml")) and (.go.jobs.lint.steps | map(.id) | index("setup-go") < index("ml")) -and (.lint.jobs.lint.steps | map(.id) | index("setup-go") < index("ml")) JQ # Accepts only the declared compiler bindings and ordering in the supplied workflow snapshot. admit() { jq -e -f "$scratch/admit.jq" "$1" >/dev/null; } @@ -46,11 +41,11 @@ for mutation in \ '(.go.jobs.lint.steps[] | select(.id == "setup-go") | .with["go-version"])="1.26.3"' \ '(.readonly.jobs.lint.steps[] | select(.id == "ml") | .env.GOTOOLCHAIN)="local"' \ '(.readonly.jobs.lint.steps[] | select(.id == "setup-go") | .with["go-version"])="1.26.3"' \ - '(.lint.jobs.lint.steps[] | select(.id == "ml") | .env.GOTOOLCHAIN)="local"' \ - '(.lint.jobs.lint.steps[] | select(.id == "ml") | .env.GOTOOLCHAIN)="auto"' \ - '(.lint.jobs.lint.steps[] | select(.id == "setup-go") | .if)="true"' \ - '(.lint.jobs.lint.steps[] | select(.id == "setup-go") | .with["go-version"])="1.26.3"' \ - '(.lint.jobs.lint.steps[] | select(.id == "setup-go") | .with["go-version-file"])="other/go.mod"'; do + 'del(.readonly.jobs.lint.steps[] | select(.id == "ml") | .env.GOTOOLCHAIN)' \ + '(.readonly.jobs.lint.steps[] | select(.id == "ml") | .env.GOTOOLCHAIN)="auto"' \ + '(.readonly.jobs.lint.steps[] | select(.id == "ml") | .env.GOTOOLCHAIN)="go1.26.8"' \ + '(.readonly.jobs.lint.steps[] | select(.id == "setup-go") | .id)="other-go"' \ + '(.readonly.jobs.lint.steps[] | select(.id == "setup-go") | .if)="true"'; do jq "$mutation" "$scratch/actual.json" >"$scratch/mutant.json" if cmp -s "$scratch/actual.json" "$scratch/mutant.json" || admit "$scratch/mutant.json"; then echo "TEST FAIL -- compiler binding mutation was ineffective or admitted: $mutation" >&2 diff --git a/.github/tests/test-multi-trigger-input-gate-polarity-ablation.sh b/.github/tests/test-multi-trigger-input-gate-polarity-ablation.sh index 93bde640..55ee1c8a 100755 --- a/.github/tests/test-multi-trigger-input-gate-polarity-ablation.sh +++ b/.github/tests/test-multi-trigger-input-gate-polarity-ablation.sh @@ -11,7 +11,7 @@ set -euo pipefail guard=".github/tests/test-multi-trigger-input-gate-polarity.sh" real=".github/workflows/validate-go-project.yaml" -call_only=".github/workflows/lint.yaml" +call_only=".github/workflows/validate-go-project-readonly.yaml" work="$(mktemp -d)" trap 'rm -rf "$work"' EXIT @@ -76,15 +76,14 @@ YAML } # Controls: the real org-required workflow passes, and the guard still refuses to assert anything -# about a workflow_call-only file (lint.yaml, where a bare read is correct because defaults apply). +# about the actual workflow_call-only projection, where call defaults apply. expect_pass "control — the real ${real} passes" "$real" expect_refused "control — a workflow_call-only workflow is refused as vacuous" "$call_only" "is workflow_call-only" -# The exact env shape lint.yaml uses, lifted from lint.yaml itself so the fixture cannot drift from -# the pattern the guard exists to stop. Asserted non-empty, or every env arm below is a no-op. -lint_shape="$(yq -r '[.jobs[].steps[]?.env.APPLY_FIXES | select(. != null)] | .[0] // ""' "$call_only")" -[[ "$lint_shape" == *'inputs.apply-fixes &&'* ]] || - fail "lint.yaml no longer carries the APPLY_FIXES bare-read shape (got '${lint_shape}'); re-derive this fixture" +# Keep an independent literal of the historically unsafe bare-read shape. Its +# coverage must not depend on retaining a retired production interface. +# shellcheck disable=SC2016 # a literal Actions expression, never shell interpolation +lint_shape='${{ inputs.apply-fixes && github.event_name == '"'"'pull_request'"'"' && github.event.pull_request.head.repo.fork != true && '"'"'all'"'"' || '"'"'none'"'"' }}' env_fixture="$(fixture env <\n' >> "$repo/README.md" expect 'historical generation marker cannot misrepresent ownership' 2 'historical generated marker' -for retired in delete-workflow-runs delete-workflow-runs-readonly deploy-github-pages sync-cluster-policies; do +for retired in delete-workflow-runs delete-workflow-runs-readonly deploy-github-pages sync-cluster-policies lint; do for extension in yaml yml; do fixture printf 'on: {workflow_call: {}}\njobs: {}\n' >"$repo/.github/workflows/$retired.$extension" @@ -232,7 +232,7 @@ expect 'repository missing-row ablation' 1 "validate-go-project.yaml: input 'tes # Exercise the actual catalogue navigation independently of input-table parity. navigation() { local document=$1 anchor count - for anchor in create-release dependency-review enable-auto-merge lint publish-app \ + for anchor in create-release dependency-review enable-auto-merge publish-app \ publish-manifests scan-todos scan-workflows template-sync update-agent-skills validate-go \ repository-admission deploy-guards world-regressions applied-fixes todos-readonly \ todos-fixture go-readonly inspect-recovery publish-recovery recovery-fixture; do diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 986c0bfe..cb385990 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -5642,37 +5642,6 @@ jobs: fi echo "validate-go-project.yaml concurrency group is invocation-aware ✅" - # ── lint.yaml self-tests ───────────────────────────────────────────────────── - # lint.yaml is a GATING reusable workflow (it fails a PR on a lint finding), so per the - # AGENTS.md convention it carries BOTH a passes-on-good-input and a blocks-on-bad-input - # self-test — a happy-path test alone cannot catch a gate that silently stopped biting. - # apply-fixes is false here: the self-test must never commit to the branch under test. - test-lint-workflow: - needs: [select-ci-tests] - if: "${{ github.event_name != 'merge_group' && !startsWith(github.event.head_commit.message, 'chore(main): release ') && contains(fromJSON(needs.select-ci-tests.outputs.selected), 'test-lint-workflow') }}" - name: "[Test] Lint - Passes On Clean Fixture" - # This is the real read-only consumer proof: GitHub rejects a called workflow at - # startup if any job tries to elevate beyond these caller permissions. - permissions: - contents: read - uses: ./.github/workflows/lint.yaml - with: - working-directory: .github/tests/go-valid-fixture - go-version-file: .github/tests/go-valid-fixture/go.mod - apply-fixes: false - - test-lint-workflow-manual-fixes: - needs: [select-ci-tests] - if: "${{ github.event_name != 'merge_group' && !startsWith(github.event.head_commit.message, 'chore(main): release ') && contains(fromJSON(needs.select-ci-tests.outputs.selected), 'test-lint-workflow-manual-fixes') }}" - name: "[Test] Lint - Manual Workflow Fixes Opted In" - permissions: - contents: read - uses: ./.github/workflows/lint.yaml - with: - working-directory: .github/tests/megalinter-clean-fixture - apply-fixes: false - manual-workflow-fixes: true - test-lint-blocks: needs: [select-ci-tests] if: "${{ github.event_name != 'merge_group' && !startsWith(github.event.head_commit.message, 'chore(main): release ') && contains(fromJSON(needs.select-ci-tests.outputs.selected), 'test-lint-blocks') }}" @@ -5950,7 +5919,7 @@ jobs: test-lint-gate-lockstep: needs: [select-ci-tests] if: "${{ github.event_name != 'merge_group' && !startsWith(github.event.head_commit.message, 'chore(main): release ') && contains(fromJSON(needs.select-ci-tests.outputs.selected), 'test-lint-gate-lockstep') }}" - name: "[Test] Lint - Gate Pins Self-Tested Image" + name: "[Test] Go Lint - Source and Credential Boundaries" runs-on: ubuntu-latest permissions: contents: read @@ -6017,7 +5986,7 @@ jobs: # validate-go-project.yaml is the ORG-REQUIRED workflow: it runs on every Go pull request # in the organisation and carries three fixer lanes (tidy, golangci-lint, MegaLinter), so - # it writes far more automation commits than lint.yaml does. The assertion is file-scoped, + # all three lanes must delegate to the same signer. The assertion is file-scoped, # which is what makes it meaningful here — one lane left on a CLI commit would keep # unsigned commits on the branch and hold #142's option open on its own. - name: 🔏 Assert the org-required Go pipeline commits fixes through the signing API @@ -6041,8 +6010,8 @@ jobs: shell: bash run: bash .github/tests/test-multi-trigger-input-gate-polarity.sh - # The guard above must see a bare read wherever an expression is evaluated — lint.yaml's - # APPLY_FIXES `env:` shape, `with:`, `run:`, cancel-in-progress — not only in `if:`, and + # The guard above must see a bare read wherever an expression is evaluated — + # APPLY_FIXES in `env:`, `with:`, `run:`, cancel-in-progress — not only in `if:`, and # must accept an explicit comparison written with the input on the right. This ablation # proves both on fixtures, each refusal checked for the guard's own message (#1028). - name: 🎚️ Ablate the multi-trigger input-gate guard's expression coverage @@ -6081,7 +6050,7 @@ jobs: # test-lint-signed-commit.sh, whose assertions are each proven by ablation. So this job is # the consumer-contract proof rather than a behavioural one: it is called the way a real # caller calls it, with a dependency-bot pr-owner it must refuse, which exercises the input - # contract and -- as with test-lint-workflow -- the permission contract, since GitHub + # contract and the read-only permission contract, since GitHub # rejects a called workflow at startup if any of its jobs elevates beyond these caller # permissions. The call commits nothing by construction as well as by gate: no patch # artifact exists and no App key is passed. @@ -6423,8 +6392,6 @@ jobs: - test-validate-go-test-blocks - test-validate-go-gate-lockstep - test-validate-go-project-concurrency-key - - test-lint-workflow - - test-lint-workflow-manual-fixes - test-lint-blocks - test-lint-gate-lockstep - test-prepare-fixes @@ -6561,8 +6528,6 @@ jobs: ${{ needs.test-validate-go-test-blocks.result }} ${{ needs.test-validate-go-gate-lockstep.result }} ${{ needs.test-validate-go-project-concurrency-key.result }} - ${{ needs.test-lint-workflow.result }} - ${{ needs.test-lint-workflow-manual-fixes.result }} ${{ needs.test-lint-blocks.result }} ${{ needs.test-lint-gate-lockstep.result }} ${{ needs.test-prepare-fixes.result }} diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml deleted file mode 100644 index d4f4dc7b..00000000 --- a/.github/workflows/lint.yaml +++ /dev/null @@ -1,182 +0,0 @@ -name: 🧹 Lint - -# Repo-wide linting with MegaLinter, for consumers that are NOT plain Go modules. -# -# validate-go-project.yaml already embeds a MegaLinter job, but it is one stage of a -# full Go pipeline (tidy, golangci-lint, deadcode, govulncheck, build, test, coverage) -# and scopes MegaLinter to the Go module directory. A repo whose Go code is one subtree -# of a larger tree — a Godot client plus a Go server, say — needs the opposite: lint the -# WHOLE repo, and leave the Go build/test to the consumer's own job. That is this -# workflow. -# -# Flavor: `go`. MegaLinter's flavors differ only in which linters are baked into the -# image, and the `go` flavor's 55 cover every surface this org actually lints — Go, JSON, -# Markdown, YAML, shell, GitHub Actions, Dockerfile, Kubernetes, spelling, secrets and -# copy-paste. `cupcake` adds Python/Terraform/JS at roughly double the image; `all` adds -# far more still. Staying on `go` also means consumers share one warm image layer with -# validate-go-project.yaml. Keep the SHA below in lockstep with that workflow's. -# -# NOTE: `workflow_call` only — deliberately NO `pull_request`/`merge_group` triggers. A -# repo already covered by the org-required validate-go-project.yaml would otherwise run -# MegaLinter twice on every PR. - -on: - workflow_call: - inputs: - working-directory: - required: false - type: string - default: "" - description: "Directory to lint. Empty = the whole repository (the normal case); the CI self-test sets it to a fixture." - go-version-file: - required: false - type: string - default: "" - description: "Path to a go.mod. When set, MegaLinter selects the exact declared Go toolchain inside its container, downloading that fixed version if needed. Leave empty when the consumer disables the Go linters or has no Go code." - apply-fixes: - required: false - type: boolean - default: true - description: "Auto-fix and commit the result back to the PR branch. Set false for a read-only gate. Opt in to manual-workflow-fixes to recover patches that touch workflow files." - manual-workflow-fixes: - required: false - type: boolean - default: false - description: "Opt in to retaining complete workflow-file fixes for manual application, including after a lint error. Existing fix and artifact eligibility rules still apply; lint errors still fail the job." - pr-owner: - required: false - type: string - default: "" - description: "Pull request author login. Auto-fix commits are suppressed for dependency-bot PRs, whose branches are owned by their automation." - secrets: - APP_PRIVATE_KEY: - description: "GitHub App private key. Required only for auto-fix commits; without it the job still lints, but a fixable finding fails the build instead of being committed." - required: false - -# A reusable workflow inherits the CALLER's github.workflow, so keying on it keeps -# separate invocations in separate groups while runs of the same invocation supersede -# (devantler-tech/actions#587). -concurrency: - group: "lint-${{ github.repository }}-${{ github.ref }}-${{ github.workflow }}-${{ inputs.manual-workflow-fixes }}" - cancel-in-progress: true - -permissions: {} - -jobs: - lint: - name: 🧹 Lint - mega-linter - runs-on: ubuntu-latest - outputs: - fixes-created: ${{ steps.fixes.outputs.changed == 'true' && steps.fixes.outputs.manual-required == 'false' }} - fixes-artifact: ${{ steps.fixes.outputs.artifact-name }} - permissions: - contents: read - steps: - - name: 🛡️ Harden runner - uses: step-security/harden-runner@ccd8616d44fd3846e67624a50d5aad6d37bf2d25 # v2.22.1 - with: - egress-policy: audit - - # Fork pull requests never get secrets, so the App token cannot be minted and fixes - # can never be committed back. Auto-fixing there would leave changes that the job then - # fails on — a red check an outside contributor cannot resolve except by installing - # this image locally. So forks lint READ-ONLY: nothing is fixed, nothing is left - # uncommitted, nothing fails for a reason they cannot act on. - # - # This is not a softer gate. With APPLY_FIXES=none a fixable formatting issue is - # reported as a warning and MegaLinter exits 0, but real lint ERRORS — invalid JSON, - # shellcheck failures — still fail the job exactly as they do on a branch. - - name: 📄 Checkout - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - persist-credentials: false - - - name: ⚙️ Setup Go - id: setup-go - if: ${{ inputs.go-version-file != '' }} - uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 - with: - go-version-file: ${{ inputs.go-version-file }} - - # Keep this `uses:` in lockstep with validate-go-project.yaml's MegaLinter step - # (enforced by test-lint-gate-lockstep) so both consumers run the same image and - # the failure-mode self-test always exercises the real gate. - - name: 🧹 Lint - id: ml - uses: oxsecurity/megalinter/flavors/go@9949bad031045f366be2467e00e8371a7328a2e2 # v10.1.0 - env: - DEFAULT_WORKSPACE: ${{ inputs.working-directory || '.' }} - GOTOOLCHAIN: ${{ inputs.go-version-file != '' && format('go{0}', steps.setup-go.outputs.go-version) || 'local' }} - MEGALINTER_CONFIG: ${{ github.workspace }}/.mega-linter.yml - VALIDATE_ALL_CODEBASE: true - APPLY_FIXES: ${{ inputs.apply-fixes && github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork != true && 'all' || 'none' }} - APPLY_FIXES_EVENT: all - APPLY_FIXES_MODE: commit - # actionlint (<=1.7.12, bundled in MegaLinter) doesn't yet know the `code-quality` - # permission scope that GitHub Code Quality coverage requires callers to grant, so - # it reports a false "unknown permission scope" error. Ignore it until actionlint - # catches up. It also rejects the documented `concurrency.queue` key as "unexpected - # key"; that one message is ignored by a space-free pattern, and any other unexpected - # concurrency key is still reported. Keep in lockstep with validate-go-project.yaml - # (test-actionlint-arguments-lockstep.sh). - ACTION_ACTIONLINT_ARGUMENTS: -ignore code-quality -ignore unexpected.key..queue..for..concurrency..section - - # PR-controlled MegaLinter configuration may execute commands. Export only its - # resulting patch here; write credentials are minted later on a fresh runner. - - name: 📦 Prepare applied fixes - if: ${{ !cancelled() && (success() || inputs.manual-workflow-fixes == true || inputs.manual-workflow-fixes == 'true') && inputs.apply-fixes && github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork != true }} - id: fixes - shell: bash - env: - FIXES_ARTIFACT: megalinter-fixes-${{ job.check_run_id }} - MANUAL_WORKFLOW_FIXES: ${{ inputs.manual-workflow-fixes == true || inputs.manual-workflow-fixes == 'true' }} - run: | - set -euo pipefail - echo "artifact-name=${FIXES_ARTIFACT}" >> "$GITHUB_OUTPUT" - git add --intent-to-add . - git diff --no-color --no-ext-diff --no-textconv --src-prefix=a/ --dst-prefix=b/ --binary --full-index HEAD > "${RUNNER_TEMP}/${FIXES_ARTIFACT}.patch" - if [ -s "${RUNNER_TEMP}/${FIXES_ARTIFACT}.patch" ]; then - echo "changed=true" >> "$GITHUB_OUTPUT" - else - echo "changed=false" >> "$GITHUB_OUTPUT" - fi - # The signer has contents permission only. Preserve the complete patch when - # workflow edits need manual application; splitting a rename or related changes - # could leave the branch with only part of the formatter's result. - manual_required=false - workflow_changes="$(git diff --no-ext-diff --no-textconv --name-only --no-renames HEAD -- .github/workflows/)" - if [ "${MANUAL_WORKFLOW_FIXES:-false}" = true ] && [ -n "$workflow_changes" ]; then - manual_required=true - echo "::warning::Linter fixes include workflow files and will not be committed automatically. Run the formatter locally and push, or download ${FIXES_ARTIFACT} if available and run git apply ${FIXES_ARTIFACT}.patch from the repository root, review, and push." - fi - echo "manual-required=${manual_required}" >> "$GITHUB_OUTPUT" - - - name: 📤 Upload applied fixes - if: ${{ !cancelled() && (success() || inputs.manual-workflow-fixes == true || inputs.manual-workflow-fixes == 'true') && steps.fixes.outputs.changed == 'true' }} - uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 - with: - name: ${{ steps.fixes.outputs.artifact-name }} - path: ${{ runner.temp }}/${{ steps.fixes.outputs.artifact-name }}.patch - if-no-files-found: error - - # The commit itself lives in apply-signed-fixes.yaml so every fixer lane in this repo - # commits the same signed way. Only the gate this side can see stays here: whether - # auto-fixing is enabled. Whether MegaLinter changed anything is passed IN, not used to - # skip the call, so the called workflow still checks the branch tip on a replacement run - # where the fixer finds its own work already done. The pull_request/non-fork/dependency-bot - # gates are enforced in the called workflow. - apply-fixes: - name: 🧹 Lint - apply fixes - needs: lint - if: | - inputs.apply-fixes - uses: ./.github/workflows/apply-signed-fixes.yaml - permissions: - contents: read - with: - artifact-name: ${{ needs.lint.outputs.fixes-artifact }} - commit-message: "chore: Apply megalinter fixes" - pr-owner: ${{ inputs.pr-owner }} - fixes-created: ${{ needs.lint.outputs.fixes-created == 'true' }} - secrets: - APP_PRIVATE_KEY: ${{ secrets.APP_PRIVATE_KEY }} diff --git a/.github/workflows/validate-go-project-readonly.yaml b/.github/workflows/validate-go-project-readonly.yaml index 055f1073..aeaa2bf7 100644 --- a/.github/workflows/validate-go-project-readonly.yaml +++ b/.github/workflows/validate-go-project-readonly.yaml @@ -808,8 +808,8 @@ jobs: path: ${{ steps.go-mod-cache-path.outputs.path }} key: go-mod-v1-${{ runner.os }}-${{ runner.arch }}-${{ needs.changes.outputs.module-directory }}-${{ hashFiles(format('{0}/go.sum', needs.changes.outputs.module-directory)) }} restore-keys: ${{ github.ref != format('refs/heads/{0}', github.event.repository.default_branch) && format('go-mod-v1-{0}-{1}-{2}-', runner.os, runner.arch, needs.changes.outputs.module-directory) || '' }} - # Keep this `uses:` in lockstep with lint.yaml's MegaLinter step so both consumers run - # the same image and the failure-mode self-test always exercises the real gate. + # Keep this pin aligned with the generated read-only projection and all five + # native scanner cases (test-catalogue-tool-pin-parity.sh). - name: "\U0001F9F9 Lint" id: ml uses: oxsecurity/megalinter/flavors/go@9949bad031045f366be2467e00e8371a7328a2e2 # v10.1.0 @@ -827,7 +827,7 @@ jobs: # It also rejects the documented `concurrency.queue` key as "unexpected key", which would # fail every consumer that serializes runs with `queue: max`. That one message is ignored # by a space-free pattern (safe for MegaLinter's argument split); any other unexpected - # concurrency key is still reported. Keep in lockstep with lint.yaml + # concurrency key is still reported. Keep in lockstep with the read-only projection # (test-actionlint-arguments-lockstep.sh). ACTION_ACTIONLINT_ARGUMENTS: -ignore code-quality -ignore unexpected.key..queue..for..concurrency..section # MegaLinter leaves its fixes in the working tree and does not commit them itself; the diff --git a/.github/workflows/validate-go-project.yaml b/.github/workflows/validate-go-project.yaml index b1258184..67c015d2 100644 --- a/.github/workflows/validate-go-project.yaml +++ b/.github/workflows/validate-go-project.yaml @@ -877,8 +877,8 @@ jobs: key: go-mod-v1-${{ runner.os }}-${{ runner.arch }}-${{ needs.changes.outputs.module-directory }}-${{ hashFiles(format('{0}/go.sum', needs.changes.outputs.module-directory)) }} restore-keys: ${{ github.ref != format('refs/heads/{0}', github.event.repository.default_branch) && format('go-mod-v1-{0}-{1}-{2}-', runner.os, runner.arch, needs.changes.outputs.module-directory) || '' }} - # Keep this `uses:` in lockstep with lint.yaml's MegaLinter step so both consumers run - # the same image and the failure-mode self-test always exercises the real gate. + # Keep this pin aligned with the generated read-only projection and all five + # native scanner cases (test-catalogue-tool-pin-parity.sh). - name: 🧹 Lint id: ml uses: oxsecurity/megalinter/flavors/go@9949bad031045f366be2467e00e8371a7328a2e2 # v10.1.0 @@ -897,7 +897,7 @@ jobs: # It also rejects the documented `concurrency.queue` key as "unexpected key", which would # fail every consumer that serializes runs with `queue: max`. That one message is ignored # by a space-free pattern (safe for MegaLinter's argument split); any other unexpected - # concurrency key is still reported. Keep in lockstep with lint.yaml + # concurrency key is still reported. Keep in lockstep with the read-only projection # (test-actionlint-arguments-lockstep.sh). ACTION_ACTIONLINT_ARGUMENTS: -ignore code-quality -ignore unexpected.key..queue..for..concurrency..section diff --git a/.scripts/check-workflow-readme.sh b/.scripts/check-workflow-readme.sh index fa8e40e8..e6bb7ddb 100644 --- a/.scripts/check-workflow-readme.sh +++ b/.scripts/check-workflow-readme.sh @@ -9,7 +9,7 @@ cd "${1:-.}" exit 2 } # The one-time migration is complete. Current docs and components are authored -# here, never regenerated from the frozen legacy repository. +# here, never regenerated from the deleted legacy repository. if [[ -e scripts/import-actions-catalogue.sh || -L scripts/import-actions-catalogue.sh ]]; then echo 'Remove the retired catalogue importer; current sources must not be overwritten.' >&2 exit 2 @@ -18,7 +18,7 @@ if grep -Fq '