Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .github/agent-guides/invariants.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 <version>` rather than by hand.

Expand Down
4 changes: 2 additions & 2 deletions .github/agent-guides/workflows.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.<enable-x> && '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
Expand Down
2 changes: 0 additions & 2 deletions .github/scripts/ci-selection/inventory.json
Original file line number Diff line number Diff line change
Expand Up @@ -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/"],
Expand Down
4 changes: 0 additions & 4 deletions .github/tests/megalinter-clean-fixture/good.json

This file was deleted.

6 changes: 3 additions & 3 deletions .github/tests/test-actionlint-arguments-lockstep.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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

Expand Down Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions .github/tests/test-catalogue-pin-guards-controls.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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)
Expand Down
6 changes: 3 additions & 3 deletions .github/tests/test-catalogue-tool-pin-parity.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
7 changes: 2 additions & 5 deletions .github/tests/test-ci-catalogue-scope.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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}"
Expand All @@ -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",
Expand Down
4 changes: 3 additions & 1 deletion .github/tests/test-ci-selection.sh
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,9 @@ touch "$work/output"
EVENT_NAME=push RUN_CATALOGUE=true CATALOGUE_SCOPE=true GITHUB_OUTPUT="$work/output" \
go -C "$root/.github/scripts/ci-selection" run . "$root" "$root/.github/scripts/ci-selection/inventory.json" "$work/workflow.json"
selected="$(sed -n 's/^selected=//p' "$work/output")"
[[ "$(jq 'length' <<< "$selected")" == 80 ]]
# The two retired Lint export-only callers are absent; all surviving executions remain selected.
jq -e 'length == 78 and index("test-lint-workflow") == null and
index("test-lint-workflow-manual-fixes") == null' <<< "$selected" >/dev/null
jq -e 'index("test-validate-go-postgresql-fixture") != null' <<<"$selected" >/dev/null
jq -e 'index("test-go-cache-native-observer") != null' <<<"$selected" >/dev/null
# Hosted CI reports these callers as skipped because their callee jobs are
Expand Down
22 changes: 13 additions & 9 deletions .github/tests/test-lint-credential-boundary.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand All @@ -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="$(
Expand All @@ -51,7 +55,7 @@ lint_token_refs() { # <workflow>
'[((.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")"
Expand All @@ -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"
Expand Down
9 changes: 4 additions & 5 deletions .github/tests/test-lint-recovery-gates.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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');
Expand Down
5 changes: 3 additions & 2 deletions .github/tests/test-lint-signed-commit-shape-ablation.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading
Loading