diff --git a/.github/workflows/actions.lock b/.github/workflows/actions.lock index dda12a26..85e03d00 100644 --- a/.github/workflows/actions.lock +++ b/.github/workflows/actions.lock @@ -10,7 +10,7 @@ workflows: - 'actions/checkout@v7.0.1' - 'actions/configure-pages@v6.0.0' - 'actions/deploy-pages@v5.0.1' - - 'actions/download-artifact@v8.0.1' + - 'actions/download-artifact@v8.0.2' - 'actions/upload-pages-artifact@v5.0.0' - 'haskell-actions/setup@v2.12.1' '.github/workflows/codeql.yml': @@ -23,11 +23,11 @@ workflows: '.github/workflows/e2e.yml': - 'actions/checkout@v7.0.1' '.github/workflows/governance.yml': - - 'hyperpolymath/standards@da2c748aad55c1a1dcba00b60fe4a35017bc6540' + - 'hyperpolymath/standards@75bd5994470661a099df562d6d5ad2fe744dbd66' '.github/workflows/hypatia-dispatch-intake.yml': - 'actions/checkout@v7.0.1' '.github/workflows/hypatia-scan.yml': - - 'hyperpolymath/standards@da2c748aad55c1a1dcba00b60fe4a35017bc6540' + - 'hyperpolymath/standards@75bd5994470661a099df562d6d5ad2fe744dbd66' '.github/workflows/inbox-steward.yml': - 'actions/checkout@v7.0.1' '.github/workflows/instant-sync.yml': @@ -36,17 +36,17 @@ workflows: '.github/workflows/labels.yml': [] '.github/workflows/learning-loop.yml': - 'actions/checkout@v7.0.1' - - 'actions/upload-artifact@v7.0.1' + - 'actions/upload-artifact@v7.0.2' '.github/workflows/lock-sync-gate.yml': [] '.github/workflows/mirror.yml': - - 'hyperpolymath/standards@571cc734cd69fb846032ec77a662aa8ee4fc32cd' + - 'hyperpolymath/standards@75bd5994470661a099df562d6d5ad2fe744dbd66' '.github/workflows/pages.yml': - 'actions/checkout@v7.0.1' - 'actions/deploy-pages@v5.0.1' - 'actions/upload-pages-artifact@v5.0.0' '.github/workflows/panicbot-sweep.yml': - 'actions/checkout@v7.0.1' - - 'actions/upload-artifact@v7.0.1' + - 'actions/upload-artifact@v7.0.2' - 'dtolnay/rust-toolchain@v1' - 'swatinem/rust-cache@v2.9.2' '.github/workflows/push-email-notify.yml': @@ -56,16 +56,16 @@ workflows: '.github/workflows/rust.yml': - 'actions/checkout@v7.0.1' '.github/workflows/scorecard.yml': - - 'hyperpolymath/standards@da2c748aad55c1a1dcba00b60fe4a35017bc6540' + - 'hyperpolymath/standards@75bd5994470661a099df562d6d5ad2fe744dbd66' '.github/workflows/secret-scanner.yml': - - 'hyperpolymath/standards@571cc734cd69fb846032ec77a662aa8ee4fc32cd' + - 'hyperpolymath/standards@75bd5994470661a099df562d6d5ad2fe744dbd66' '.github/workflows/supervised-fleet-scan.yml': - 'actions/checkout@v7.0.1' '.github/workflows/vendored-bot-drift.yml': - 'actions/checkout@v7.0.1' dependencies: 'actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9': - ref: '55cc8345863c7cc4c66a329aec7e433d2d1c52a9' + ref: 'v6.1.0' commit: 'sha1-55cc8345863c7cc4c66a329aec7e433d2d1c52a9' owner_id: 44036562 repo_id: 215566462 @@ -75,7 +75,7 @@ dependencies: owner_id: 44036562 repo_id: 215566462 'actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1': - ref: '3d3c42e5aac5ba805825da76410c181273ba90b1' + ref: 'v7.0.1' commit: 'sha1-3d3c42e5aac5ba805825da76410c181273ba90b1' owner_id: 44036562 repo_id: 197814629 @@ -94,24 +94,24 @@ dependencies: commit: 'sha1-368f82528645a54fb793d4d04e342629a3f51346' owner_id: 44036562 repo_id: 438112499 - 'actions/download-artifact@v8.0.1': - ref: 'v8.0.1' - commit: 'sha1-3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c' + 'actions/download-artifact@v8.0.2': + ref: 'v8.0.2' + commit: 'sha1-9000827ccba6bdab643e8b6fd33ac0654aef8333' owner_id: 44036562 repo_id: 192626254 - 'actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a': - ref: '043fb46d1a93c77aae656e7c1c64a875d1fc6a0a' - commit: 'sha1-043fb46d1a93c77aae656e7c1c64a875d1fc6a0a' - owner_id: 44036562 - repo_id: 192625955 'actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f': ref: 'v7.0.0' commit: 'sha1-bbbca2ddaa5d8feaa63e36b76fdaad77386f024f' owner_id: 44036562 repo_id: 192625955 - 'actions/upload-artifact@v7.0.1': - ref: 'v7.0.1' - commit: 'sha1-043fb46d1a93c77aae656e7c1c64a875d1fc6a0a' + 'actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9': + ref: 'v7.0.2' + commit: 'sha1-cf430e030ddbb5b0abf93d22962f4752f3646cd9' + owner_id: 44036562 + repo_id: 192625955 + 'actions/upload-artifact@v7.0.2': + ref: 'v7.0.2' + commit: 'sha1-cf430e030ddbb5b0abf93d22962f4752f3646cd9' owner_id: 44036562 repo_id: 192625955 'actions/upload-pages-artifact@v5.0.0': @@ -121,9 +121,14 @@ dependencies: repo_id: 496012378 uses: - 'actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f' - 'dtolnay/rust-toolchain@6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772': - ref: '6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772' - commit: 'sha1-6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772' + 'denoland/setup-deno@22d081ff2d3a40755e97629de92e3bcbfa7cf2ed': + ref: 'v2.0.5' + commit: 'sha1-22d081ff2d3a40755e97629de92e3bcbfa7cf2ed' + owner_id: 42048915 + repo_id: 356423100 + 'dtolnay/rust-toolchain@e2a55d2ffb04f378e9626c28d38b36d230d1e12f': + ref: 'v1' + commit: 'sha1-e2a55d2ffb04f378e9626c28d38b36d230d1e12f' owner_id: 1940490 repo_id: 260749683 'dtolnay/rust-toolchain@v1': @@ -132,18 +137,18 @@ dependencies: owner_id: 1940490 repo_id: 260749683 'editorconfig-checker/action-editorconfig-checker@51f63319f592f97930c73d9c46184d20bd206393': - ref: '51f63319f592f97930c73d9c46184d20bd206393' + ref: 'v3.0.0' commit: 'sha1-51f63319f592f97930c73d9c46184d20bd206393' owner_id: 26415196 repo_id: 297874902 'erlef/setup-beam@54075bcc5e249e4758d363f27d099f55d843f124': - ref: '54075bcc5e249e4758d363f27d099f55d843f124' + ref: 'v1.24.1' commit: 'sha1-54075bcc5e249e4758d363f27d099f55d843f124' owner_id: 47606891 repo_id: 331103973 - 'github/codeql-action@cdf488f595d80d6e07e03d4674febd5ab45fa938': - ref: 'cdf488f595d80d6e07e03d4674febd5ab45fa938' - commit: 'sha1-cdf488f595d80d6e07e03d4674febd5ab45fa938' + 'github/codeql-action@b96794f015dfd88f77b49b1c93e0fa7110f94c63': + ref: 'v4.38.0' + commit: 'sha1-b96794f015dfd88f77b49b1c93e0fa7110f94c63' owner_id: 9919 repo_id: 259445878 'github/codeql-action@v4.38.2': @@ -171,33 +176,33 @@ dependencies: commit: 'sha1-c1c9fa07992a02c1fd3d67a0dc1b08cccb852aef' owner_id: 6759885 repo_id: 1352485172 - 'hyperpolymath/standards@571cc734cd69fb846032ec77a662aa8ee4fc32cd': - ref: '571cc734cd69fb846032ec77a662aa8ee4fc32cd' - commit: 'sha1-571cc734cd69fb846032ec77a662aa8ee4fc32cd' - owner_id: 6759885 - repo_id: 1116521501 - uses: - - 'actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1' - - 'dtolnay/rust-toolchain@6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772' - - 'webfactory/ssh-agent@e83874834305fe9a4a2997156cb26c5de65a8555' - 'hyperpolymath/standards@da2c748aad55c1a1dcba00b60fe4a35017bc6540': - ref: 'da2c748aad55c1a1dcba00b60fe4a35017bc6540' - commit: 'sha1-da2c748aad55c1a1dcba00b60fe4a35017bc6540' + 'hyperpolymath/standards@75bd5994470661a099df562d6d5ad2fe744dbd66': + ref: '75bd5994470661a099df562d6d5ad2fe744dbd66' + commit: 'sha1-75bd5994470661a099df562d6d5ad2fe744dbd66' owner_id: 6759885 repo_id: 1116521501 uses: - 'actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9' - 'actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1' - - 'actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a' + - 'actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9' + - 'denoland/setup-deno@22d081ff2d3a40755e97629de92e3bcbfa7cf2ed' + - 'dtolnay/rust-toolchain@e2a55d2ffb04f378e9626c28d38b36d230d1e12f' - 'editorconfig-checker/action-editorconfig-checker@51f63319f592f97930c73d9c46184d20bd206393' - 'erlef/setup-beam@54075bcc5e249e4758d363f27d099f55d843f124' - - 'github/codeql-action@cdf488f595d80d6e07e03d4674febd5ab45fa938' + - 'github/codeql-action@b96794f015dfd88f77b49b1c93e0fa7110f94c63' - 'ossf/scorecard-action@2d1146689b8cda280b9bc96326124645441f03bc' + - 'oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6' + - 'webfactory/ssh-agent@e83874834305fe9a4a2997156cb26c5de65a8555' 'ossf/scorecard-action@2d1146689b8cda280b9bc96326124645441f03bc': - ref: '2d1146689b8cda280b9bc96326124645441f03bc' + ref: 'v2.4.4' commit: 'sha1-2d1146689b8cda280b9bc96326124645441f03bc' owner_id: 67707773 repo_id: 421101922 + 'oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6': + ref: 'v2.2.0' + commit: 'sha1-0c5077e51419868618aeaa5fe8019c62421857d6' + owner_id: 108928776 + repo_id: 512644635 'peter-evans/repository-dispatch@v4.0.1': ref: 'v4.0.1' commit: 'sha1-28959ce8df70de7be546dd1250a005dd32156697' @@ -209,7 +214,7 @@ dependencies: owner_id: 580492 repo_id: 298565987 'webfactory/ssh-agent@e83874834305fe9a4a2997156cb26c5de65a8555': - ref: 'e83874834305fe9a4a2997156cb26c5de65a8555' + ref: 'v0.10.0' commit: 'sha1-e83874834305fe9a4a2997156cb26c5de65a8555' owner_id: 135788 repo_id: 208510314 diff --git a/.github/workflows/governance.yml b/.github/workflows/governance.yml index df2c3165..039dbdca 100644 --- a/.github/workflows/governance.yml +++ b/.github/workflows/governance.yml @@ -35,7 +35,7 @@ permissions: jobs: governance: - uses: hyperpolymath/standards/.github/workflows/governance-reusable.yml@da2c748aad55c1a1dcba00b60fe4a35017bc6540 + uses: hyperpolymath/standards/.github/workflows/governance-reusable.yml@75bd5994470661a099df562d6d5ad2fe744dbd66 permissions: actions: read contents: read diff --git a/.github/workflows/hypatia-scan.yml b/.github/workflows/hypatia-scan.yml index 83451cea..ac8947d5 100644 --- a/.github/workflows/hypatia-scan.yml +++ b/.github/workflows/hypatia-scan.yml @@ -31,8 +31,10 @@ permissions: jobs: hypatia: - uses: hyperpolymath/standards/.github/workflows/hypatia-scan-reusable.yml@da2c748aad55c1a1dcba00b60fe4a35017bc6540 - secrets: inherit + uses: hyperpolymath/standards/.github/workflows/hypatia-scan-reusable.yml@75bd5994470661a099df562d6d5ad2fe744dbd66 + # hypatia-scan-reusable declares one secret; pass that one, not everything. + secrets: + HYPATIA_SCAN_PAT: ${{ secrets.HYPATIA_SCAN_PAT }} # Total caller-side wall-clock cap for the reusable. Matches # Hypatia's `missing_timeout_minutes` rule expectation. The scan is # typically ~3 min; the cap leaves headroom for slow estate builds. diff --git a/.github/workflows/mirror.yml b/.github/workflows/mirror.yml index adac35c1..93ef2652 100644 --- a/.github/workflows/mirror.yml +++ b/.github/workflows/mirror.yml @@ -15,8 +15,17 @@ permissions: jobs: mirror: - uses: hyperpolymath/standards/.github/workflows/mirror-reusable.yml@571cc734cd69fb846032ec77a662aa8ee4fc32cd - secrets: inherit + uses: hyperpolymath/standards/.github/workflows/mirror-reusable.yml@75bd5994470661a099df562d6d5ad2fe744dbd66 + # mirror-reusable declares exactly these seven forge keys and forbids + # `secrets: inherit`; pass them by name. + secrets: + GITLAB_SSH_KEY: ${{ secrets.GITLAB_SSH_KEY }} + BITBUCKET_SSH_KEY: ${{ secrets.BITBUCKET_SSH_KEY }} + CODEBERG_SSH_KEY: ${{ secrets.CODEBERG_SSH_KEY }} + SOURCEHUT_SSH_KEY: ${{ secrets.SOURCEHUT_SSH_KEY }} + DISROOT_SSH_KEY: ${{ secrets.DISROOT_SSH_KEY }} + GITEA_SSH_KEY: ${{ secrets.GITEA_SSH_KEY }} + RADICLE_KEY: ${{ secrets.RADICLE_KEY }} permissions: actions: read contents: read diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index c034b328..b6cc0357 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -65,12 +65,11 @@ jobs: run: cargo test --locked --verbose - name: Clippy (deny warnings) run: cargo clippy --locked --all-targets -- -D warnings - - name: Rustfmt check (informational) - # Pre-existing formatting drift is not yet gated; surfaced here so it - # stays visible without blocking. Flip to a hard gate after a dedicated - # `cargo fmt` pass lands. + - name: Rustfmt check + # A hard gate since 2026-10-19: all four matrix crates were measured + # rustfmt-clean, so `continue-on-error` (hypatia RE005, a silent + # green) no longer buys anything. run: cargo fmt --check - continue-on-error: true dispatch-contracts: name: Dispatch path and outcome contracts @@ -83,6 +82,8 @@ jobs: with: persist-credentials: false - run: bash scripts/tests/dispatch-paths.sh + - name: Workflow-pin fix scripts keep their contracts + run: bash scripts/tests/fix-workflow-pins.sh gsbot: name: GSBot build, tests and dependency security diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index 8c4afbbd..34aded1e 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -18,4 +18,4 @@ jobs: contents: read security-events: write id-token: write - uses: hyperpolymath/standards/.github/workflows/scorecard-reusable.yml@da2c748aad55c1a1dcba00b60fe4a35017bc6540 + uses: hyperpolymath/standards/.github/workflows/scorecard-reusable.yml@75bd5994470661a099df562d6d5ad2fe744dbd66 diff --git a/.github/workflows/secret-scanner.yml b/.github/workflows/secret-scanner.yml index cb885b38..dbac2872 100644 --- a/.github/workflows/secret-scanner.yml +++ b/.github/workflows/secret-scanner.yml @@ -24,5 +24,5 @@ jobs: contents: read pull-requests: write actions: read - uses: hyperpolymath/standards/.github/workflows/secret-scanner-reusable.yml@571cc734cd69fb846032ec77a662aa8ee4fc32cd - secrets: inherit + uses: hyperpolymath/standards/.github/workflows/secret-scanner-reusable.yml@75bd5994470661a099df562d6d5ad2fe744dbd66 + # secret-scanner-reusable declares no caller secret, so none is passed. diff --git a/.gitleaksignore b/.gitleaksignore new file mode 100644 index 00000000..b2e082ec --- /dev/null +++ b/.gitleaksignore @@ -0,0 +1,10 @@ +# SPDX-License-Identifier: MPL-2.0 +# gitleaks fingerprints (commit:file:rule:line) of historical false positives. +# Each is a documented placeholder, not a credential; the sustainabot file no +# longer exists (deleted in ded43d0) and the seambot literal is `ghs_xxx…`. +d1d8e1db885c6bf15833d413cdae42a5710d7ff4:bots/seambot/tests/github_integration.rs:github-app-token:154 +37db42f48f7f85f2558a57e3f6643a62f39da2bc:bots/seambot/tests/github_integration.rs:github-app-token:154 +f1b3dc853aa9c13e47fde308681c4a1e0bb58eb1:bots/seambot/tests/github_integration.rs:github-app-token:154 +d1d8e1db885c6bf15833d413cdae42a5710d7ff4:bots/sustainabot/docs/GITHUB_APP_SETUP.md:private-key:89 +37db42f48f7f85f2558a57e3f6643a62f39da2bc:bots/sustainabot/docs/GITHUB_APP_SETUP.md:private-key:89 +f1b3dc853aa9c13e47fde308681c4a1e0bb58eb1:bots/sustainabot/docs/GITHUB_APP_SETUP.md:private-key:89 diff --git a/.machine_readable/descriptiles/AGENTIC.a2ml b/.machine_readable/descriptiles/AGENTIC.a2ml index 645cb6c2..67211252 100644 --- a/.machine_readable/descriptiles/AGENTIC.a2ml +++ b/.machine_readable/descriptiles/AGENTIC.a2ml @@ -35,6 +35,25 @@ allow-silent-skip = false require-rerun-after-fix = true release-claim-requires-hard-pass = true +[workflow-pin-remediation] +# The fleet's half of the pin-propagation architecture +# hypatia (detection) -> gitbot-fleet (this: actuation) -> .git-private-farm (propagation) +# Three fix scripts, one contract test (scripts/tests/fix-workflow-pins.sh, run +# by rust.yml), and two data files that carry the SAME facts hypatia detects +# on, so detection and repair cannot drift apart: +# scripts/fix-rust-toolchain-input.sh WF026 SHA-pinned dtolnay/rust-toolchain with no toolchain: input +# scripts/fix-stale-reusable-pin.sh WF027, PIN-003 a caller pinned at a reusable revision the estate has MEASURED dead +# (not WF016: its orphan SHAs have no recorded replacement in the table) +# scripts/fix-lock-self-sha-refs.sh PI006 actions.lock record whose ref is its own SHA (GitHub: "invalid lockfile pin") +# scripts/lib/stale-reusable-pins.tsv the dead revisions and the one replacement each moves to +# scripts/lib/action-ref-tags.tsv the tag that resolves to each commit the estate pins by SHA +may-rewrite-reusable-pins = true # only to a replacement the table names; never "newest" +may-rewrite-actions-lock = true # SHA substitution and ref:tag only; nested uses: lists are never recomputed here +may-choose-replacement-sha = false # a human picks it against standards/scripts/lock-selfcheck.sh and records it in the table +may-run-network-lookups = true # git ls-remote --tags for a tag the map lacks; --offline disables it +must-report-unresolved = true # a self-SHA record with no tag is reported and left, never guessed (exit 1) +must-be-idempotent = true # every script's second run is byte-identical; the contract test asserts it + [automation-hooks] # on-enter: Read 0-AI-MANIFEST.a2ml, then .machine_readable/descriptiles/STATE.a2ml # on-exit: Update .machine_readable/descriptiles/STATE.a2ml with session outcomes diff --git a/fleet-coordinator.sh b/fleet-coordinator.sh index fbe2fd4a..e186faf3 100755 --- a/fleet-coordinator.sh +++ b/fleet-coordinator.sh @@ -632,7 +632,7 @@ EOF fi } -create_rule_approval_pr() { +create_rule_approval_pr() ( local rule_file="$1" local rule_type="$2" local observations="$3" @@ -640,7 +640,9 @@ create_rule_approval_pr() { log_bot "hypatia" "Creating rule approval PR for $rule_type..." # Clone hypatia repo to temp directory - local hypatia_dir="/tmp/hypatia-rule-pr-$$" + local hypatia_dir + hypatia_dir="$(mktemp -d)" + trap 'rm -rf "$hypatia_dir"' EXIT git clone https://github.com/hyperpolymath/hypatia.git "$hypatia_dir" 2>/dev/null cd "$hypatia_dir" @@ -680,7 +682,6 @@ COMMIT_EOF git push origin "$branch_name" 2>/dev/null || { log_warn "Failed to push rule PR branch" cd - > /dev/null - rm -rf "$hypatia_dir" return 1 } @@ -734,8 +735,7 @@ PR_BODY fi cd - > /dev/null - rm -rf "$hypatia_dir" -} +) deploy_bots() { log_error "BLOCKED: deployment readiness is not verified; deployment is quarantined. See docs/AUTOMATION-QUARANTINE.adoc." diff --git a/robot-repo-automaton/templates/skeleton/Justfile b/robot-repo-automaton/templates/skeleton/Justfile index d39fe7d4..75272b90 100644 --- a/robot-repo-automaton/templates/skeleton/Justfile +++ b/robot-repo-automaton/templates/skeleton/Justfile @@ -150,7 +150,7 @@ e2e: @echo "Running E2E tests..." # TODO: Replace with your E2E test command. Examples: # bash tests/e2e.sh # Shell-based E2E - # npx playwright test # Browser E2E + # bunx playwright test # Browser E2E # mix test test/integration/e2e_test.exs # Elixir E2E # cargo test --test end_to_end # Rust E2E @echo "E2E tests passed!" diff --git a/run-fleet.sh b/run-fleet.sh index 468146f9..a5972854 100755 --- a/run-fleet.sh +++ b/run-fleet.sh @@ -379,7 +379,7 @@ cmd_fix() { innerhtml_assignment|*innerHTML*) registry_key="XSS" ;; rust_unwrap|unwrap_without_check|*unwrap*) registry_key="PanicPath" ;; wildcard_cors|*cors*) registry_key="CORSWildcard" ;; - eval_in_shell|*eval*) registry_key="DynamicCodeExecution" ;; + eval_in_shell|*eval*) registry_key="DynamicCodeExecution" ;; # hypatia: allow content_patterns/eval_in_shell -- detector or message text naming eval in target files; this line does not run eval hardcoded_tmp|*tmp_path*) registry_key="UncheckedAllocation" ;; *sql_inject*|*parameteriz*) registry_key="SQLInjection" ;; *shell_inject*|*shell_quot*) registry_key="ShellInjection" ;; diff --git a/scripts/fix-actions-policy.sh b/scripts/fix-actions-policy.sh index b788f06f..860eb18f 100755 --- a/scripts/fix-actions-policy.sh +++ b/scripts/fix-actions-policy.sh @@ -69,6 +69,8 @@ JSONL="" ONE_REPO="" FINDING_FILE="" # dispatch-runner contract: $2 is the finding JSON STD_REPO="hyperpolymath/standards" +allowlist_tmp="" +trap 'if [ -n "$allowlist_tmp" ]; then rm -f "$allowlist_tmp"; fi' EXIT die() { echo "fix-actions-policy: $*" >&2; exit "${2:-2}"; } @@ -222,9 +224,10 @@ for R in $REPOS; do # a healthy `selected` repo. SRC="$ALLOWLIST" if [ -z "$SRC" ]; then - gh api "repos/${STD_REPO}/actions/permissions/selected-actions" > "/tmp/allow.$$.json" 2>/dev/null \ + allowlist_tmp="$(mktemp)" + gh api "repos/${STD_REPO}/actions/permissions/selected-actions" > "$allowlist_tmp" 2>/dev/null \ || die "cannot read the standards allowlist; pass --allowlist FILE instead" - SRC="/tmp/allow.$$.json" + SRC="$allowlist_tmp" fi [ -s "$SRC" ] || die "allowlist '$SRC' is missing or empty" jq -e '(.patterns_allowed|length) > 0' "$SRC" >/dev/null 2>&1 \ @@ -232,7 +235,10 @@ for R in $REPOS; do gh api --method PUT "repos/${FULL}/actions/permissions/selected-actions" --input "$SRC" >/dev/null # `&& rm` alone would return 1 when the test is false and, under `set -e`, # abort the sweep on the --allowlist path. Keep the guard total. - if [ "$SRC" = "/tmp/allow.$$.json" ]; then rm -f "/tmp/allow.$$.json"; fi + if [ -n "$allowlist_tmp" ]; then + rm -f "$allowlist_tmp" + allowlist_tmp="" + fi fi # GET-verify. Trading sha_pinning away for permissiveness is never acceptable, diff --git a/scripts/fix-chapel-runs-on-pin.sh b/scripts/fix-chapel-runs-on-pin.sh index 4c7515fe..2655e9e7 100755 --- a/scripts/fix-chapel-runs-on-pin.sh +++ b/scripts/fix-chapel-runs-on-pin.sh @@ -31,8 +31,8 @@ for f in $(find .github/workflows -name '*.yml' -o -name '*.yaml' 2>/dev/null); fi # Also normalise the apt install pattern (declarative resolution beats dpkg+fix loop) - if grep -qF 'sudo dpkg -i /tmp/chapel.deb || sudo apt-get install -f -y' "$f"; then - sed -i 's|sudo dpkg -i /tmp/chapel.deb || sudo apt-get install -f -y|sudo apt-get install -y /tmp/chapel.deb|' "$f" + if grep -qF 'sudo dpkg -i /tmp/chapel.deb || sudo apt-get install -f -y' "$f"; then # hypatia: allow content_patterns/hardcoded_tmp -- literal path this fixer matches in target files; nothing is created at that path + sed -i 's|sudo dpkg -i /tmp/chapel.deb || sudo apt-get install -f -y|sudo apt-get install -y /tmp/chapel.deb|' "$f" # hypatia: allow content_patterns/hardcoded_tmp -- literal path this fixer matches in target files; nothing is created at that path echo " apt-get install replaces dpkg fallback: $f" CHANGED=$((CHANGED + 1)) fi diff --git a/scripts/fix-command-injection.sh b/scripts/fix-command-injection.sh index b6d6a6e5..03e7d909 100755 --- a/scripts/fix-command-injection.sh +++ b/scripts/fix-command-injection.sh @@ -75,13 +75,13 @@ for file in "${SHELL_FILES[@]}"; do # --- Fix 2: Annotate eval usage with warning --- # Insert a warning comment above eval lines that use variable expansion - if grep -qP '^\s*eval\s+["\$]' "$file" 2>/dev/null; then + if grep -qP '^\s*eval\s+["\$]' "$file" 2>/dev/null; then # hypatia: allow content_patterns/eval_in_shell -- detector or message text naming eval in target files; this line does not run eval # Check if warning comment already exists (idempotent) - if ! grep -q "SECURITY: eval with variable expansion is a command injection risk" "$file" 2>/dev/null; then - sed -i.bak '/^\s*eval\s\+["\$]/i\# SECURITY: eval with variable expansion is a command injection risk — refactor to avoid eval' "$file" + if ! grep -q "SECURITY: eval with variable expansion is a command injection risk" "$file" 2>/dev/null; then # hypatia: allow content_patterns/eval_in_shell -- detector or message text naming eval in target files; this line does not run eval + sed -i.bak '/^\s*eval\s\+["\$]/i\# SECURITY: eval with variable expansion is a command injection risk — refactor to avoid eval' "$file" # hypatia: allow content_patterns/eval_in_shell -- detector or message text naming eval in target files; this line does not run eval if [[ -f "$file.bak" ]] && ! diff -q "$file" "$file.bak" >/dev/null 2>&1; then ((CHANGES++)) || true - echo " [shell] Annotated eval usage in $rel_path" + echo " [shell] Annotated eval usage in $rel_path" # hypatia: allow content_patterns/eval_in_shell -- detector or message text naming eval in target files; this line does not run eval fi rm -f "$file.bak" fi diff --git a/scripts/fix-dynamic-code-exec.sh b/scripts/fix-dynamic-code-exec.sh index 100827d8..356ca362 100755 --- a/scripts/fix-dynamic-code-exec.sh +++ b/scripts/fix-dynamic-code-exec.sh @@ -65,7 +65,7 @@ for file in "${JS_FILES[@]}"; do # Preserve indentation match($0, /^[[:space:]]*/); indent = substr($0, RSTART, RLENGTH); - print indent "// SECURITY WARNING: dynamic code execution — consider eliminating eval"; + print indent "// SECURITY WARNING: dynamic code execution — consider eliminating eval"; # hypatia: allow content_patterns/eval_in_shell -- detector or message text naming eval in target files; this line does not run eval } # Replace simple eval(varName) with Function form # Matches eval(identifier) but not eval("string") or eval(complex.expr()) @@ -95,7 +95,7 @@ for file in "${JS_FILES[@]}"; do if (prev !~ /SECURITY WARNING: dynamic code execution/) { match($0, /^[[:space:]]*/); indent = substr($0, RSTART, RLENGTH); - print indent "// SECURITY WARNING: dynamic code execution — new Function() is equivalent to eval"; + print indent "// SECURITY WARNING: dynamic code execution — new Function() is equivalent to eval"; # hypatia: allow content_patterns/eval_in_shell -- detector or message text naming eval in target files; this line does not run eval } print prev = $0 @@ -170,16 +170,16 @@ for file in "${PY_FILES[@]}"; do continue fi - if grep -qP '^\s*[^#]*\b(exec|eval)\(' "$file" 2>/dev/null; then + if grep -qP '^\s*[^#]*\b(exec|eval)\(' "$file" 2>/dev/null; then # hypatia: allow content_patterns/eval_in_shell -- detector or message text naming eval in target files; this line does not run eval tmpfile=$(mktemp) awk ' /# SECURITY WARNING: dynamic code execution/ { print; next } /^\s*#/ { print; next } - /\b(exec|eval)\(/ { + /\b(exec|eval)\(/ { # hypatia: allow content_patterns/eval_in_shell -- detector or message text naming eval in target files; this line does not run eval if (prev !~ /SECURITY WARNING: dynamic code execution/) { match($0, /^[[:space:]]*/); indent = substr($0, RSTART, RLENGTH); - print indent "# SECURITY WARNING: dynamic code execution — avoid exec()/eval() where possible"; + print indent "# SECURITY WARNING: dynamic code execution — avoid exec()/eval() where possible"; # hypatia: allow content_patterns/eval_in_shell -- detector or message text naming eval in target files; this line does not run eval } print prev = $0 @@ -191,7 +191,7 @@ for file in "${PY_FILES[@]}"; do if ! diff -q "$file" "$tmpfile" >/dev/null 2>&1; then cp "$tmpfile" "$file" rel_path="${file#"$REPO_PATH"/}" - echo " Flagged exec/eval in $rel_path" + echo " Flagged exec/eval in $rel_path" # hypatia: allow content_patterns/eval_in_shell -- detector or message text naming eval in target files; this line does not run eval ((FIXED_COUNT++)) || true fi rm -f "$tmpfile" diff --git a/scripts/fix-eval-to-safe.sh b/scripts/fix-eval-to-safe.sh index 4948ea16..92a49069 100755 --- a/scripts/fix-eval-to-safe.sh +++ b/scripts/fix-eval-to-safe.sh @@ -35,9 +35,9 @@ while IFS= read -r -d '' file; do # --- Fix 1: eval statements --- # Annotate eval calls that are not already marked - if grep -v '^\s*#' "$file" 2>/dev/null | grep -qw 'eval'; then - if ! grep -q 'SECURITY.*eval' "$file" 2>/dev/null; then - sed -i '/^\s*#/!{/\beval\b/{/SECURITY/!s/\(.*\beval\b\)/# SECURITY: eval is unsafe with untrusted input — refactor to avoid dynamic code execution\n\1/}}' "$file" 2>/dev/null || true + if grep -v '^\s*#' "$file" 2>/dev/null | grep -qw 'eval'; then # hypatia: allow content_patterns/eval_in_shell -- detector or message text naming eval in target files; this line does not run eval + if ! grep -q 'SECURITY.*eval' "$file" 2>/dev/null; then # hypatia: allow content_patterns/eval_in_shell -- detector or message text naming eval in target files; this line does not run eval + sed -i '/^\s*#/!{/\beval\b/{/SECURITY/!s/\(.*\beval\b\)/# SECURITY: eval is unsafe with untrusted input — refactor to avoid dynamic code execution\n\1/}}' "$file" 2>/dev/null || true # hypatia: allow content_patterns/eval_in_shell -- detector or message text naming eval in target files; this line does not run eval changed=true fi fi @@ -54,7 +54,7 @@ while IFS= read -r -d '' file; do if [[ "$changed" == true ]]; then rel_path="${file#"$REPO_PATH"/}" - echo " FIXED $rel_path (shell eval/pipe-to-shell)" + echo " FIXED $rel_path (shell eval/pipe-to-shell)" # hypatia: allow content_patterns/eval_in_shell -- detector or message text naming eval in target files; this line does not run eval ((FIXED_COUNT++)) || true fi done < <(find "$REPO_PATH" -type f \( -name "*.sh" -o -name "*.bash" \) "${FIND_EXCLUDES[@]}" -print0 2>/dev/null) @@ -70,9 +70,9 @@ while IFS= read -r -d '' file; do changed=false # eval annotation - if grep -v '^\s*#' "$file" 2>/dev/null | grep -qw 'eval'; then - if ! grep -q 'SECURITY.*eval' "$file" 2>/dev/null; then - sed -i '/^\s*#/!{/\beval\b/{/SECURITY/!s/\(.*\beval\b\)/# SECURITY: eval is unsafe with untrusted input — refactor to avoid dynamic code execution\n\1/}}' "$file" 2>/dev/null || true + if grep -v '^\s*#' "$file" 2>/dev/null | grep -qw 'eval'; then # hypatia: allow content_patterns/eval_in_shell -- detector or message text naming eval in target files; this line does not run eval + if ! grep -q 'SECURITY.*eval' "$file" 2>/dev/null; then # hypatia: allow content_patterns/eval_in_shell -- detector or message text naming eval in target files; this line does not run eval + sed -i '/^\s*#/!{/\beval\b/{/SECURITY/!s/\(.*\beval\b\)/# SECURITY: eval is unsafe with untrusted input — refactor to avoid dynamic code execution\n\1/}}' "$file" 2>/dev/null || true # hypatia: allow content_patterns/eval_in_shell -- detector or message text naming eval in target files; this line does not run eval changed=true fi fi @@ -87,7 +87,7 @@ while IFS= read -r -d '' file; do if [[ "$changed" == true ]]; then rel_path="${file#"$REPO_PATH"/}" - echo " FIXED $rel_path (shell eval/pipe-to-shell)" + echo " FIXED $rel_path (shell eval/pipe-to-shell)" # hypatia: allow content_patterns/eval_in_shell -- detector or message text naming eval in target files; this line does not run eval ((FIXED_COUNT++)) || true fi done < <(find "$REPO_PATH" -type f -not -name "*.*" "${FIND_EXCLUDES[@]}" -print0 2>/dev/null) @@ -96,5 +96,5 @@ echo "" if [[ "$FIXED_COUNT" -gt 0 ]]; then echo "Annotated dynamic code execution patterns in $FIXED_COUNT file(s)" else - echo "No eval/pipe-to-shell patterns found" + echo "No eval/pipe-to-shell patterns found" # hypatia: allow content_patterns/eval_in_shell -- detector or message text naming eval in target files; this line does not run eval fi diff --git a/scripts/fix-heredoc-install.sh b/scripts/fix-heredoc-install.sh index 24a8d394..18de8c50 100755 --- a/scripts/fix-heredoc-install.sh +++ b/scripts/fix-heredoc-install.sh @@ -29,7 +29,7 @@ while IFS= read -r -d '' file; do # Pattern 1: curl | bash / curl | sh (pipe-to-shell) if grep -qP 'curl\s.*\|\s*(ba)?sh' "$file" 2>/dev/null; then - echo " FOUND curl|bash in $rel_path" + echo " FOUND curl|bash in $rel_path" # hypatia: allow content_patterns/download_then_run_shell -- diagnostic message text; nothing is downloaded or executed # Add warning comment (idempotent — skip if already annotated) if ! grep -q "WARNING: Pipe-to-shell is unsafe" "$file" 2>/dev/null; then sed -i 's/\(.*curl.*|.*sh\)/# WARNING: Pipe-to-shell is unsafe — download and verify first\n\1/' "$file" 2>/dev/null || true @@ -38,14 +38,14 @@ while IFS= read -r -d '' file; do fi # Pattern 2: eval "$(cat ..." or eval "$(heredoc..." - if grep -qP 'eval\s+["\x27]?\$\(' "$file" 2>/dev/null; then - echo " FOUND eval \$() in $rel_path" + if grep -qP 'eval\s+["\x27]?\$\(' "$file" 2>/dev/null; then # hypatia: allow content_patterns/eval_in_shell -- detector or message text naming eval in target files; this line does not run eval + echo " FOUND eval \$() in $rel_path" # hypatia: allow content_patterns/eval_in_shell -- detector or message text naming eval in target files; this line does not run eval ((changes++)) || true fi # Pattern 3: eval $VAR - if grep -qP 'eval\s+\$[A-Z_]' "$file" 2>/dev/null; then - echo " FOUND eval \$VAR in $rel_path" + if grep -qP 'eval\s+\$[A-Z_]' "$file" 2>/dev/null; then # hypatia: allow content_patterns/eval_in_shell -- detector or message text naming eval in target files; this line does not run eval + echo " FOUND eval \$VAR in $rel_path" # hypatia: allow content_patterns/eval_in_shell -- detector or message text naming eval in target files; this line does not run eval ((changes++)) || true fi @@ -56,7 +56,7 @@ done < <(find "$REPO_PATH" -type f \( -name "*.sh" -o -name "*.bash" \) -not -pa echo "" if [[ "$FIXED_COUNT" -gt 0 ]]; then - echo "Found eval/heredoc patterns in $FIXED_COUNT file(s)" + echo "Found eval/heredoc patterns in $FIXED_COUNT file(s)" # hypatia: allow content_patterns/eval_in_shell -- detector or message text naming eval in target files; this line does not run eval else - echo "No eval/heredoc patterns found" + echo "No eval/heredoc patterns found" # hypatia: allow content_patterns/eval_in_shell -- detector or message text naming eval in target files; this line does not run eval fi diff --git a/scripts/fix-lock-self-sha-refs.sh b/scripts/fix-lock-self-sha-refs.sh new file mode 100755 index 00000000..89dcad8b --- /dev/null +++ b/scripts/fix-lock-self-sha-refs.sh @@ -0,0 +1,514 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: MPL-2.0 +# fix-lock-self-sha-refs.sh — give every actions.lock dependency record whose +# `ref:` is its own commit SHA the tag that commit was resolved from. +# Hypatia rule PI006 (self-SHA lock record). +# +# MEASURED 2026-10-15/16 across the estate (cicd-squabbler run 38846539059, +# hypatia#929): GitHub now validates a reusable workflow against the CALLEE +# repository's lock at the called SHA, and rejects at Set up job +# Unable to resolve action `actions/checkout@3d3c42e5…`: invalid lockfile pin +# whenever the record reads +# 'actions/checkout@3d3c42e5…': +# ref: '3d3c42e5…' <- the SHA repeated as the ref +# `gh actions-lock` writes the tag (`ref: 'v7.0.1'`); hand-added records +# wrote the SHA, and every caller of a reusable whose repository carries such +# a record died. standards main carried them from 2026-08-19 to 2026-10-19. +# +# What it does: for each record `'owner/repo@':` followed by +# `ref: ''` (either quote style), where the workflows use owner/repo at +# that revision as an ACTION — a reusable-workflow call at that revision +# legitimately keeps its SHA as ref, standards' accepted lock does; a record no +# workflow uses directly falls back to hyperpolymath/standards and +# hyperpolymath/cicd-suite by name — look the commit up in +# scripts/lib/action-ref-tags.tsv, else (unless --offline) in +# `git ls-remote --tags https://github.com/owner/repo`, preferring the most +# specific tag; rewrite the ref line. A commit with no tag is reported and +# left alone — a wrong tag is worse than a known-bad one. +# +# Idempotent. Exit 0 whether or not anything changed; exit 1 if a self-SHA +# record remains unresolved (so a caller can tell "fixed" from "still dead"); +# exit 2, lock untouched, if the lock cannot be parsed. +# +# Usage: +# fix-lock-self-sha-refs.sh [--offline] +# TAG_MAP=/path/map.tsv fix-lock-self-sha-refs.sh + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +TAG_MAP="${TAG_MAP:-$SCRIPT_DIR/lib/action-ref-tags.tsv}" +REPO_PATH="${1:?Usage: fix-lock-self-sha-refs.sh [--offline]}" +OFFLINE=0; [ "${2:-}" = "--offline" ] && OFFLINE=1 +LOCK="$REPO_PATH/.github/workflows/actions.lock" + +[ -f "$LOCK" ] || { echo "OK (no actions.lock): $REPO_PATH"; exit 0; } +# One workflow-pin fixer per checkout at a time (see lib/pin-fixer-lock.sh). +. "$SCRIPT_DIR/lib/pin-fixer-lock.sh" +# anchor_defs/2: the anchors a line defines, outside quoted scalars and comments. +ANCHORS_AWK=$(cat "$SCRIPT_DIR/lib/yaml-anchors.awk") +pin_fixer_lock "$REPO_PATH" + +# Read tag names from stdin and print the most specific: more dots, then longer. +best_tag() { awk '{ n = gsub(/\./, ".", $0); print n "\t" length($0) "\t" $0 }' | sort -t $'\t' -k1,1nr -k2,2nr | head -1 | cut -f3; } + +# Print the tag for owner/repo $1 at SHA $2 from TAG_MAP, falling back to +# remote tags unless OFFLINE is set. Print nothing when no tag is found. +lookup_tag() { # + local repo="$1" sha="$2" tag + # GitHub resolves an owner/repository in either case and git a commit ID: + # the map is read case-folded, so `Actions/Checkout` meets `actions/checkout`. + tag=$(awk -F'\t' -v r="$repo" -v s="$sha" 'tolower($1) == tolower(r) && tolower($2) == tolower(s) { print $3; exit }' "$TAG_MAP" 2>/dev/null || true) + if [ -z "$tag" ] && [ "$OFFLINE" -eq 0 ]; then + tag=$(git ls-remote --tags "https://github.com/$repo" 2>/dev/null \ + | awk -v s="$sha" '$1 == s { sub(/^refs\/tags\//, "", $2); sub(/\^\{\}$/, "", $2); print $2 }' \ + | sort -u | best_tag || true) + fi + printf '%s' "$tag" +} + +# A record's class is read from how the workflows USE that repository at that +# revision: `uses: owner/repo/.github/workflows/.yml@` is a reusable +# call (the record keeps its SHA ref — standards' accepted lock does), while +# `uses: owner/repo[/path]@` is an action (the SHA ref is the defect). +# One repository can be both at different revisions (cicd-suite ships gates +# AND composite actions), so the class is per repository@revision. A record no +# workflow uses directly (a nested dependency, a tree without workflows) is +# not classified by guess: the two estate reusable sources keep their SHA ref +# by name, and any other such record is reported UNRESOLVED and left as is. +# +# Only a `uses:` that is a real key counts: a job's key under `jobs:` is a +# reusable call (R), a step's key under `steps:` is an action (A). The same +# text inside a `run: |` script, a fixture under `env:`, or any other key is +# data, so it can neither keep a defective action record nor repair one. +# The sites are read once, with the YAML context the other fixers use. +uses_sites() { # -> lines "Rowner/repo/.github/workflows/x.yml@ref" | "Aowner/repo[/path]@ref" + local wf + for wf in "$REPO_PATH"/.github/workflows/*.yml "$REPO_PATH"/.github/workflows/*.yaml; do + [ -f "$wf" ] || continue + awk "$ANCHORS_AWK"' + function indent_of(s, t) { t = s; sub(/[^ ].*$/, "", t); return length(t) } + function plain(s) { return (s !~ /^[[:space:]]*$/ && s !~ /^[[:space:]]*#/) } + function keyname(s) { sub(/^[[:space:]]*(-[[:space:]]+)?/, "", s); sub(/[[:space:]]*:.*$/, "", s); gsub(/["'"'"']/, "", s); return s } + function ref_of(s) { sub(/^[[:space:]]*(-[[:space:]]+)?["'"'"']?uses["'"'"']?[[:space:]]*:[[:space:]]*([&!][^[:space:]]*[[:space:]]+)*["'"'"']?/, "", s); sub(/["'"'"'[:space:]].*$/, "", s); return s } + # A `uses:` whose value is not on its line — a block scalar + # (`uses: >-`), or nothing but a comment — names its site on the + # lines below, which this reader skips as scalar text. Such a site + # cannot be classified; the caller refuses (exit 2) rather than + # read the record it names as `unknown`. + # A value given as an alias (`uses: *call`) is the anchor'"'"'s value + # (`CALL: &call owner/repo/.github/workflows/x.yml@`), which + # YAML requires to come first; an alias of no anchor read here + # cannot be classified either. + function site(kind, r) { + if (r ~ /^\*/) r = (substr(r, 2) in anchor) ? anchor[substr(r, 2)] : "" + if (r == "" || r ~ /^#/ || r ~ /^[|>][-+0-9]*$/) print "F\t" FILENAME ":" FNR + else print kind "\t" r + } + # Every anchor line s defines (lib/yaml-anchors.awk: outside quoted + # scalars and comments), kept when its value is a reference + # (`&name a/b@ref`); a later anchor of the same name replaces it, + # as it does for the aliases after it. + function note_anchors(s, d, k) { + anchor_defs(s, d) + for (k in d) { + if (d[k] ~ /^[A-Za-z0-9_.-]+\/[^@[:space:]]*@[^[:space:]]+$/) anchor[k] = d[k] + else delete anchor[k] + } + } + function nearest_below(ind, k, p) { p = -1; for (k in keyat) if (k + 0 < ind && k + 0 > p) p = k + 0; return p } + # The line with its trailing comment dropped and every quoted + # scalar that is neither a key nor the value of a `uses` key + # blanked to `""`, so a `uses:` written inside a `name:` string + # or after a `#` names no site. Quotes are walked, not matched by + # pattern: a `#` or a brace inside a string is text. + function masked(s, out, i, n, c, j, body) { + out = ""; n = length(s); i = 1 + while (i <= n) { + c = substr(s, i, 1) + if (c == "#" && (i == 1 || substr(s, i - 1, 1) ~ /[[:space:]]/)) break + if (c == "\"" || c == "'"'"'") { + j = i + 1 + if (c == "\"") { while (j <= n && substr(s, j, 1) != "\"") { if (substr(s, j, 1) == "\\") j++; j++ } } + else { while (j <= n && (substr(s, j, 1) != "'"'"'" || substr(s, j + 1, 1) == "'"'"'")) j += (substr(s, j, 1) == "'"'"'" ? 2 : 1) } + body = substr(s, i + 1, j - i - 1) + if (substr(s, j + 1) ~ /^[[:space:]]*:/ || out ~ /(^|[{,[[:space:]])["'"'"']?uses["'"'"']?[[:space:]]*:[[:space:]]*([&!][^[:space:],}]*[[:space:]]+)*$/) out = out c body c + else out = out c c + i = j + 1; continue + } + out = out c; i++ + } + return out + } + # Every `uses` entry of the flow mappings written on the key'"'"'s own + # line (`call: { uses: ... }`, `- { uses: ... }`, `jobs: { a: { uses: ... }, + # b: { uses: ... } }`), into out[1..n]; returns n. Given the MASKED + # line: the raw one carries its comment and its strings. + # out_at[i] is the column of the `{`, `,` or `[` that precedes + # entry i (0 at the start of the line), for flow_kind/3. + function flow_uses_all(s, out, n, r, t, base) { + n = 0; split("", out); split("", out_at); t = s; base = 0 + while (match(t, /(^|[{,[])[[:space:]]*["'"'"']?uses["'"'"']?[[:space:]]*:[[:space:]]*([&!][^"'"'"'[:space:],}]*[[:space:]]+)*["'"'"']?[^]"'"'"'[:space:],}]+/)) { + r = substr(t, RSTART, RLENGTH) + out_at[n + 1] = (r ~ /^[{,[]/) ? base + RSTART : 0 + sub(/^.*uses["'"'"']?[[:space:]]*:[[:space:]]*([&!][^"'"'"'[:space:],}]*[[:space:]]+)*["'"'"']?/, "", r) + out[++n] = r; base += RSTART + RLENGTH - 1; t = substr(t, RSTART + RLENGTH) + } + return n + } + # The kind of the flow `uses` entry whose mapping is open at + # column `at` of masked line s, from the keys that open the + # brackets around it: in a mapping that is an item of a `[` that + # `steps:` opens it is an action (A); directly in a mapping a job key opens — inside the + # `{` that `jobs:` opens on this line, or on a job line (`call: + # { uses: ... }`) whose block parent is `jobs:` — it is a reusable + # call (R); anywhere else (`with: { uses: ... }`) it is data (""). + # `item` marks a `- { ... }` step: only its own entries are its + # action. + function flow_kind(s, at, parent, item, i, c, d, k, seg, kn) { + d = 0; segs[0] = 1 + for (i = 1; i <= at; i++) { + c = substr(s, i, 1) + if (c == "{" || c == "[") { + seg = substr(s, segs[d], i - segs[d]); kn = "" + if (match(seg, /["'"'"']?[A-Za-z0-9_.-]+["'"'"']?[[:space:]]*:[[:space:]]*$/)) { kn = substr(seg, RSTART, RLENGTH); sub(/[[:space:]]*:[[:space:]]*$/, "", kn); gsub(/["'"'"']/, "", kn) } + d++; kind[d] = c; kname[d] = kn; segs[d] = i + 1 + } else if (c == ",") segs[d] = i + 1 + else if ((c == "}" || c == "]") && d > 0) d-- + } + if (item) return (d == 1 && kind[1] == "{") ? "A" : "" + # A step is a mapping that is itself an item of the `[` that + # `steps:` opens: its own `uses` is the action. A `uses` deeper + # inside it (`with: { uses: ... }`) is an input'"'"'s data. + if (d >= 2 && kind[d] == "{" && kname[d] == "" && kind[d - 1] == "[" && kname[d - 1] == "steps") return "A" + # So is an entry written straight into that `[` (`steps: [uses: + # ...]`): YAML reads it as a mapping of that one pair, a step. + if (d >= 1 && kind[d] == "[" && kname[d] == "steps") return "A" + if (d >= 1 && kind[d] == "{" && kname[d] != "") { + if (d >= 2 && kind[d - 1] == "{" && kname[d - 1] == "jobs") return "R" + if (d == 1 && parent == "jobs") return "R" + } + return "" + } + # The brackets a masked line opens and leaves open (negative when + # it closes more than it opens). Quoted text is already blanked. + function depth_of(s, t) { t = s; return gsub(/[[{]/, "", t) - gsub(/[]}]/, "", s) } + # Every `uses` entry of the flow collection s (masked, possibly + # joined from several lines), classified by flow_kind/4. `steps` + # gates an item: only a `- { ... }` under `steps:` is a step. + function flow_emit(s, parent, item, steps, m, j, fk) { + m = flow_uses_all(s, fe) + for (j = 1; j <= m; j++) { + fk = flow_kind(s, out_at[j], parent, item) + if (fk != "" && (!item || steps)) site(fk, fe[j]) + } + } + # A flow collection that opens on line s and does not close there + # is read to its closing bracket before it is classified: the + # lines in between are its entries, never block keys. + function flow_open(s, parent, item, steps) { + fdepth = depth_of(s) + if (fdepth > 0) { facc = s; fparent = parent; fitem = item; fsteps = steps } + else { fdepth = 0; flow_emit(s, parent, item, steps) } + } + BEGIN { + Q = "[\"'"'"']?" + KEY_RE = "^[[:space:]]*" Q "[A-Za-z0-9_.-]+" Q "[[:space:]]*:([[:space:]]|$)" + KEY_FLOW_RE = "^[[:space:]]*" Q "[A-Za-z0-9_.-]+" Q "[[:space:]]*:[[:space:]]*[[{]" + ITEM_RE = "^[[:space:]]*-([[:space:]]|$)" + SCALAR_RE = ":[[:space:]]*[|>][-+0-9]*[[:space:]]*(#.*)?$" + USES_RE = "^" Q "uses" Q "[[:space:]]*:[[:space:]]*" Q + item_steps = 0; item_indent = -1 + } + { + ind = indent_of($0) + if (in_scalar) { if (plain($0) && ind <= scalar_indent) in_scalar = 0; else next } + if (!plain($0)) next + # Read as YAML reads it: a double-quoted value spelling the + # reference with escapes (`uses: "\x66oo/bar@"`), or + # continued past an escaped line break onto the lines below, + # runs the reference it decodes to (lib/yaml-anchors.awk). + while (yaml_dq_continued($0) && (getline nxt) > 0) $0 = yaml_dq_join($0, nxt) + $0 = yaml_decode_dq($0) + note_anchors($0) + if (fdepth > 0) { + ml = masked($0); facc = facc " " ml; fdepth += depth_of(ml) + if (fdepth <= 0) { fdepth = 0; flow_emit(facc, fparent, fitem, fsteps) } + next + } + if ((ml = masked($0)) ~ /^[[:space:]]*[[{]/) { + # A flow collection that is a whole line: the root of a + # KYAML document, or the value of the key above it + # (`steps:` then `[ ... ]`), read as if written after it. + p = nearest_below(ind); g = (p >= 0) ? nearest_below(p) : -1 + flow_open(((p >= 0) ? keyat[p] ": " : "") ml, (g >= 0 ? keyat[g] : ""), 0, 0) + next + } + if ($0 ~ ITEM_RE) { + # A list item: its parent is the nearest key at its indentation or + # shallower (`steps:\n- uses:` puts the dash under the key). + p = -1; for (k in keyat) if (k + 0 <= ind && k + 0 > p) p = k + 0 + for (k in keyat) if (k + 0 > ind) delete keyat[k] + item_steps = (p >= 0 && keyat[p] == "steps"); item_indent = ind + t = $0; sub(/^[[:space:]]*-[[:space:]]*/, "", t) + # The step'"'"'s own keys sit at the column of its first one (`- name:` + # puts `uses:` under `name`); a bare dash learns it from the next + # key line. A deeper `uses:` belongs to `with:`, `env:` or some + # other input and is data. + item_key = (t == "") ? -1 : length($0) - length(t) + if (t ~ USES_RE) { if (item_steps) site("A", ref_of(t)) } + else if (t ~ /^\{/) flow_open(masked(t), "", 1, item_steps) + else if (t ~ KEY_RE) { + keyat[ind + 2] = keyname(t) + if (t ~ SCALAR_RE) { in_scalar = 1; scalar_indent = ind + 2 } + # An input written as a flow mapping over several lines + # (`- with: {`) is read to its end; it names no site. + else if ((mt = masked(t)) ~ KEY_FLOW_RE && depth_of(mt) > 0) flow_open(mt, "", 0, 0) + } + next + } + if ($0 ~ KEY_RE) { + key = keyname($0) + for (k in keyat) if (k + 0 >= ind) delete keyat[k] + if (ind <= item_indent) item_steps = 0 + if (item_steps && item_key < 0 && ind > item_indent) item_key = ind + if (key == "uses") { + p = nearest_below(ind); g = (p >= 0) ? nearest_below(p) : -1 + if (p >= 0 && g >= 0 && keyat[g] == "jobs") site("R", ref_of($0)) + else if (item_steps && ind == item_key) site("A", ref_of($0)) + } else if ((ml = masked($0)) ~ KEY_FLOW_RE) { + # A flow collection as the key'"'"'s value: a job written + # `call: { uses: ... }`, or the whole `jobs: { ... }` mapping + # on one line, is a reusable call; `steps: [ { uses: ... } ]` + # is a list of actions. A flow mapping on one of a step'"'"'s + # own keys (`with: { uses: ... }`) is that input'"'"'s data: the + # step'"'"'s action is its scalar `uses:` key, never a mapping. + # A `{ uses: ... }` inside a job'"'"'s `name:` string or after + # a `#` is neither: the masked line has no such text. + # Each entry is classified by the keys around it, so a + # step inside a one-line `jobs: { b: { steps: [ ... ] } }` + # is an action, not a reusable call. A collection that + # spans lines (`steps: [` ... `]`) is read whole. + p = nearest_below(ind) + flow_open(ml, (p >= 0 ? keyat[p] : ""), 0, 0) + } + keyat[ind] = key + if ($0 ~ SCALAR_RE) { in_scalar = 1; scalar_indent = ind } + } + } + # A flow collection that never closes cannot be classified: the + # caller refuses rather than guess the kind of what it hides. + END { if (fdepth > 0) print "X\t" FILENAME } + ' "$wf" + done +} +SITES=$(mktemp) +uses_sites > "$SITES" +if grep -q '^X' "$SITES"; then + echo "fix-lock-self-sha-refs: a flow collection in $(grep -m1 '^X' "$SITES" | cut -f2) never closes, so its uses: sites cannot be classified; lock left unchanged" >&2 + rm -f "$SITES"; exit 2 +fi +if grep -q '^F' "$SITES"; then + echo "fix-lock-self-sha-refs: the uses: value at $(grep -m1 '^F' "$SITES" | cut -f2) is a block scalar or continues on the next line, so the site it names cannot be classified; lock left unchanged" >&2 + rm -f "$SITES"; exit 2 +fi + +# For owner/repo $1 at SHA $2, print reusable, action, or unknown; reusable +# takes precedence when both kinds of use exist. Repository and revision are +# compared as fields, never as a pattern — a `.` in a repository name is a +# dot, so `foo/action.test` is not `foo/actionXtest` — and in lower case: +# GitHub reads a repository name, and git a commit ID, in either case. +usage_of() { # -> reusable | action | unknown + awk -F'\t' -v r="$1" -v s="$2" ' + BEGIN { r = tolower(r); s = tolower(s); n = length(r) } + { + i = index($2, "@"); if (!i) next + path = tolower(substr($2, 1, i - 1)); rev = tolower(substr($2, i + 1)) + if (rev != s) next + if ($1 == "R" && substr(path, 1, n + 19) == r "/.github/workflows/" && substr(path, n + 20) ~ /^[a-z0-9_.-]+\.ya?ml$/) reusable = 1 + if ($1 == "A" && (path == r || substr(path, 1, n + 1) == r "/")) action = 1 + } + END { print (reusable ? "reusable" : (action ? "action" : "unknown")) }' "$SITES" +} + +# How the record for owner/repo $1 at SHA $2 is treated: `keep` its SHA ref +# (a reusable workflow, whose lock ref is the SHA), `rewrite` it to a tag (an +# action), or `unknown`. Only a site in this repository's workflows proves an +# action: a record no workflow here uses is a transitive dependency (a +# reusable's own `uses:`), whose edge `owner/repo@sha` says nothing of its +# kind, and a nested reusable's SHA ref is legitimate. Such a record is left +# as is and reported, never rewritten on a guess; the estate's two reusable +# sources are known reusables. +sha_ref_disposition() { # -> keep | rewrite | unknown + case "$(usage_of "$1" "$2")" in + reusable) echo keep ;; + action) echo rewrite ;; + # GitHub reads `Hyperpolymath/Standards` as hyperpolymath/standards. + *) case "${1,,}" in hyperpolymath/standards|hyperpolymath/cicd-suite) echo keep ;; *) echo unknown ;; esac ;; + esac +} + +fixed=0; unresolved=0 +tmp=$(mktemp); cp "$LOCK" "$tmp" +# Parse first, loop second: a parser that dies inside a process substitution +# would look like "no records" and the script would report OK. POSIX awk only +# (Debian/Ubuntu's default awk is mawk: no match() capture array, no interval +# expressions), so the SHA is checked for content and length separately. +# A record key and its ref may be single-quoted, double-quoted or plain (gh +# actions-lock writes single quotes; a hand-added record — exactly where this +# defect comes from — may not), and the quote style is kept on the rewrite. +# Either may write the commit ID in upper-case hex: git resolves it the same, +# so it is the same self-SHA record, looked up in lower case and rewritten as +# written. A record is read to its end (the next record key, or the end of +# the 8-space block): YAML mapping order is not fixed, so `ref:` may follow +# `commit:` and the other fields — exactly as a hand-added record might write +# it. Each finding carries the line of the `ref:` itself, and the ref as that +# line spells it. +# A record may also be written as a flow mapping on its key line (`'a/b@': +# { ref: '', commit: 'sha1-' }`): its `ref` entry is read from the +# braces, and rewritten there (kind I). One whose braces do not close on the +# key line is not read (kind M): it is reported unless its SHA ref is one to +# keep, never passed over as no record at all. Block records are kind B. +records=$(mktemp) +if ! awk -v sq="'" "$ANCHORS_AWK"' + # Each line is read as YAML reads it: a double-quoted key or ref spelled + # with escapes (`ref: "\x30..."`) is the value it decodes to. The rewrite + # below edits the bytes as written; a ref it cannot reach so is reported. + { $0 = yaml_decode_dq($0) } + # Trailing whitespace after the colon, a carriage return included (a + # lock with CRLF line endings), is no part of the key. + # The record key of a flow-mapping record line: repo and SHA, or 0. + function flow_key(s, t, i) { + t = s; sub(/^ ["'"'"']?/, "", t) + if (!match(t, /^[^"'"'"'@[:space:]#]+@[0-9a-fA-F]+["'"'"']?[[:space:]]*:[[:space:]]*\{/)) return 0 + t = substr(t, 1, RLENGTH); sub(/["'"'"']?[[:space:]]*:[[:space:]]*\{$/, "", t) + i = index(t, "@"); f_repo = substr(t, 1, i - 1); f_sha = substr(t, i + 1) + return length(f_sha) == 40 + } + # The `ref` entry at the top level of the flow mapping whose `{` is at + # column o of s: returns the column its scalar starts at (past any node + # properties; its opening quote when quoted) and sets rv to the scalar as + # written, or returns 0. Quoted scalars and nested brackets are walked + # over, so a `ref:` written inside a string (`note: "a, ref: "`) or + # a nested mapping is not the record'"'"'s field. The line is already + # decoded: no escapes are left in its double-quoted scalars. + function top_ref(s, o, n, i, c, d, q, j, t) { + n = length(s); d = 0; q = "" + for (i = o; i <= n; i++) { + c = substr(s, i, 1) + if (q != "") { + if (c == q) { if (q == sq && substr(s, i + 1, 1) == sq) { i++; continue }; q = "" } + continue + } + if (c == "\"" || c == sq) { q = c; continue } + if (c == "#" && substr(s, i - 1, 1) ~ /[[:space:]]/) return 0 + if (c == "{" || c == "[") d++ + else if (c == "}" || c == "]") { if (--d == 0) return 0 } + if (d == 1 && (c == "{" || c == ",") && match(substr(s, i + 1), /^[[:space:]]*["'"'"']?ref["'"'"']?[[:space:]]*:[[:space:]]*([&!][^[:space:],}]*[[:space:]]+)*/)) { + j = i + 1 + RLENGTH; t = substr(s, j) + if (match(t, /^"[^"]*"/) || match(t, /^'"'"'([^'"'"']|'"'"''"'"')*'"'"'/) || match(t, /^[^,}[:space:]]+/)) { rv = substr(t, 1, RLENGTH); return j } + return 0 + } + } + return 0 + } + flow_key($0) { + inrec = 0 + body = $0; sub(/^[^{]*\{/, "", body) + if (body !~ /\}[[:space:]]*(#.*)?$/) { print NR "\tM\t" f_repo "\t" f_sha "\t"; next } + # The `ref` entry of the record'"'"'s own braces, and the column of its + # SHA, where the rewrite below puts the tag. + if ((at = top_ref($0, index($0, "{"))) > 0) { + r = rv; gsub(/["'"'"']/, "", r) + if (tolower(r) == tolower(f_sha)) print NR "\tI\t" f_repo "\t" f_sha "\t" r "\t" (rv ~ /^["'"'"']/ ? at + 1 : at) + } + next + } + /^ ["'"'"']?[^"'"'"'@[:space:]#]+@[0-9a-fA-F]+["'"'"']?:([[:space:]]+#.*|[[:space:]]*)$/ { + s = $0; sub(/^ ["'"'"']?/, "", s); sub(/["'"'"']?:([[:space:]]+#.*|[[:space:]]*)$/, "", s) + i = index(s, "@"); repo = substr(s, 1, i - 1); sha = substr(s, i + 1) + if (length(sha) == 40) { inrec = 1; key_repo = repo; key_sha = sha } else inrec = 0 + next + } + /^[^[:space:]]/ || /^ [^[:space:]]/ { inrec = 0 } + inrec && /^ ["'"'"']?ref["'"'"']?[[:space:]]*:[[:space:]]/ { + # Quoted either way, or a plain scalar, in either case of hex: the + # same ref to GitHub. The key may be quoted too (`"ref": ...`), and + # the value may carry YAML node properties, a tag or an anchor + # (`ref: !!str '"'"''"'"'`): the scalar after them is the ref. + r = $0; sub(/^ ["'"'"']?ref["'"'"']?[[:space:]]*:[[:space:]]*/, "", r); sub(/^([&!][^[:space:]]*[[:space:]]+)+/, "", r); sub(/[[:space:]]+#.*$/, "", r); gsub(/["'"'"']/, "", r); sub(/[[:space:]]+$/, "", r) + if (tolower(r) == tolower(key_sha)) print NR "\tB\t" key_repo "\t" key_sha "\t" r + } +' "$LOCK" > "$records"; then + echo "fix-lock-self-sha-refs: could not parse $LOCK (awk failed); lock left unchanged" >&2 + rm -f "$tmp" "$records" "$SITES"; exit 2 +fi +while IFS=$'\t' read -r lineno kind repo sha ref_raw col; do + sha_lc="${sha,,}" + case "$(sha_ref_disposition "$repo" "$sha_lc")" in + keep) continue ;; + unknown) + echo "UNRESOLVED $repo@${sha:0:7}: no workflow here uses it (a transitive dependency), so whether it is an action or a reusable workflow cannot be told; record left as is" >&2 + unresolved=$((unresolved + 1)); continue ;; + esac + if [ "$kind" = M ]; then + echo "UNRESOLVED $repo@${sha:0:7}: the record is a flow mapping spread over several lines (line $lineno), which this fixer does not read; check its ref by hand (record left as is)" >&2 + unresolved=$((unresolved + 1)); continue + fi + tag=$(lookup_tag "$repo" "$sha_lc") + if [ -z "$tag" ]; then + echo "UNRESOLVED $repo@${sha:0:7}: no tag found for this commit (record left as is; GitHub rejects it)" >&2 + unresolved=$((unresolved + 1)); continue + fi + # A tag name Git allows may still carry `&`, `|` or a quote; such a tag is + # not written into a sed replacement or a YAML scalar from here. + if ! [[ "$tag" =~ ^[A-Za-z0-9][A-Za-z0-9._/+-]*$ ]]; then + echo "UNRESOLVED $repo@${sha:0:7}: resolved tag '$tag' carries characters this fixer will not write into the lock (record left as is)" >&2 + unresolved=$((unresolved + 1)); continue + fi + # lineno is the ref: line itself; its quote style (or none) is kept, and + # the SHA is matched as that line spells it (its case may differ from the + # key's). + # A plain field whose tag YAML would read as a non-string (`true`, `1`, + # `null`) is written quoted, so the lock keeps a tag, not a boolean. + q="" + if [ "$kind" = I ]; then + # The SHA of the top-level `ref` entry, at the column the reader found + # it, as that line spells it; nothing else on the line. The reader saw + # the line decoded, so a line that spells anything before it with + # escapes no longer has the SHA there, and is reported. + nonstring=0 + if pin_fixer_yaml_nonstring "$tag"; then nonstring=1; fi + if ! awk -v ln="$lineno" -v col="$col" -v old="$ref_raw" -v new="$tag" -v nonstring="$nonstring" -v sq="'" ' + NR == ln { + if (substr($0, col, length(old)) != old) exit 3 + pre = substr($0, col - 1, 1) + if (nonstring && pre != "\"" && pre != sq) new = sq new sq + $0 = substr($0, 1, col - 1) new substr($0, col + length(old)) + } + { print }' "$tmp" > "$tmp.ref"; then + rm -f "$tmp.ref" + echo "UNRESOLVED $repo@${sha:0:7}: the ref inside the record's flow mapping (line $lineno) is not written in a form this fixer rewrites; set it to $tag by hand (record left as is)" >&2 + unresolved=$((unresolved + 1)); continue + fi + mv "$tmp.ref" "$tmp" + echo "FIXED $repo@${sha:0:7} ref: $sha -> $tag" + fixed=$((fixed + 1)); continue + fi + if pin_fixer_yaml_nonstring "$tag" && ! sed -n "${lineno}p" "$tmp" | grep -qE "^ [\"']?ref[\"']?[[:space:]]*:[[:space:]]*([&!][^[:space:]]*[[:space:]]+)*[\"']"; then q="'"; fi + before=$(sed -n "${lineno}p" "$tmp") + # Any node properties before the value (`!!str`, an anchor) are kept. + sed -i -E "${lineno}s~^( [\"']?ref[\"']?[[:space:]]*:[[:space:]]*([&!][^[:space:]]*[[:space:]]+)*)([\"']?)$ref_raw\3~\1\3$q$tag$q\3~" "$tmp" + if [ "$(sed -n "${lineno}p" "$tmp")" = "$before" ]; then + echo "UNRESOLVED $repo@${sha:0:7}: the record's ref (line $lineno) is not written in a form this fixer rewrites (spelled with escapes?); set it to $tag by hand (record left as is)" >&2 + unresolved=$((unresolved + 1)); continue + fi + echo "FIXED $repo@${sha:0:7} ref: $sha -> $tag" + fixed=$((fixed + 1)) +done < "$records" +rm -f "$records" "$SITES" + +if ! cmp -s "$LOCK" "$tmp"; then pin_fixer_replace "$tmp" "$LOCK"; else rm -f "$tmp"; fi +if [ "$fixed" -eq 0 ] && [ "$unresolved" -eq 0 ]; then echo "OK (no self-SHA action records): $LOCK"; fi +[ "$fixed" -gt 0 ] && echo "Rewrote $fixed record(s) in $LOCK" +[ "$unresolved" -eq 0 ] diff --git a/scripts/fix-rust-toolchain-input.sh b/scripts/fix-rust-toolchain-input.sh new file mode 100755 index 00000000..2f876002 --- /dev/null +++ b/scripts/fix-rust-toolchain-input.sh @@ -0,0 +1,600 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: MPL-2.0 +# fix-rust-toolchain-input.sh — give every SHA-pinned dtolnay/rust-toolchain +# step a `toolchain:` input. Hypatia rule WF026. +# +# dtolnay/rust-toolchain reads the toolchain from its `@`-ref (`@stable`, +# `@1.85.0`). A full-SHA ref carries no version, so a SHA-pinned step with no +# `with: toolchain:` fails its first step with +# 'toolchain' is a required input +# before any cargo command runs — and the red reads as a code failure. +# plasma-parser-writer's `rust-ci / Cargo check + clippy + fmt` was red this +# way from 2026-09-29 to 2026-10-14 (plasma-parser-writer#105); standards' +# rust-ci-reusable.yml carried the defect from d73c93e0 to 49ed4715 +# (fixed by standards#439, 9fe0d8ff). +# +# What it does, per workflow file, for every list item under a `steps:` key +# that holds a step whose `uses:` is dtolnay/rust-toolchain@<40-hex>: +# * only a list item whose parent key is `steps:` is a step: a list item +# inside a block scalar (`run: |`, a fixture kept in `env:`) or under any +# other key is text and is never touched; +# * the step is inspected whole — from its opening `- ` line (a bare `-` on +# its own line opens a step too), so a `with:` written above `uses:` +# counts (YAML mapping keys carry no order); +# * only a `uses:` that is a KEY OF THE STEP MAPPING is the step's action: +# the same text nested under `env:`, inside a `run: |` script that writes +# a fixture, or under any other key is data and is left alone; +# * only the step's `with:` mapping decides, and only its DIRECT children: +# `toolchain:` under `env:` or any other key is not an action input, and +# the same text inside another input's block scalar (`components: |`) is +# text, not a key, and does not count either; +# * a block `with:` (with or without a trailing comment) that names no +# `toolchain:` gets `toolchain: ` (default `stable`) as its +# first key, at the indentation its other keys use; +# * a flow `with: { ... }` — on one line or spread over several up to its +# closing brace — without `toolchain` gets it as its first entry; +# * a step with no `with:` at all gets one, directly under `uses:`; +# * keys may be quoted (`"uses":`, "with":) — the same key to GitHub; +# * the action is matched in either case (`Dtolnay/Rust-Toolchain@` +# is the same action to GitHub) and its spelling is kept; +# * a tag-pinned step (`@stable`, `@1.85.0`) and a step that already names a +# toolchain are left alone; +# * a step written as a flow mapping (`- { name: x, uses: ... }`), a +# one-line `steps: [ { uses: ... } ]`, a `uses:` whose value continues +# below its key (`uses: >-`, or the reference alone on the next line), +# a `uses: *alias` of an anchored pinned reference, or a `with:` whose +# value this +# parser does not read (`with: &anchor`, `with: *alias`) is NOT +# rewritten: the script names the line as UNSUPPORTED and exits 2 rather +# than report the file as clean. Fix it by hand. +# Idempotent: a second run changes nothing. +# +# Exit 0 whether or not anything changed. Exit 2 when some file holds a +# SHA-pinned step the parser cannot place as a block-style step mapping: +# that file is left untouched (other files are still fixed) and a dispatcher +# must not record the run as a clean success. +# +# Usage: +# fix-rust-toolchain-input.sh +# fix-rust-toolchain-input.sh (every .github/workflows/*.yml) +# TOOLCHAIN=1.85.0 fix-rust-toolchain-input.sh +# (a toolchain name only — letters, digits, `.`, `_`, `-`; anything else +# exits 2 having changed nothing) +# +# Pairs with Hypatia.Rules.WorkflowAudit.check_sha_pinned_toolchain_without_input/1 +# (WF026) and recipe id `WF026` in fix-script-registry.json. + +set -euo pipefail + +TOOLCHAIN="${TOOLCHAIN:-stable}" +# The override is written into the workflow as a YAML value, so only a +# rustup toolchain name is accepted (`stable`, `1.85.0`, `nightly-2026-01-01`, +# `stable-x86_64-unknown-linux-gnu`): never text that could end the value, +# add a key or reach awk's replacement syntax. A name YAML would read as +# anything but a string is written single-quoted: a number (`1.80` is the +# float 1.8, `0x1f` an integer) or a null or boolean spelling (`null`, `true`, +# `no`, ... as YAML 1.1 and the core schema read them), which would otherwise +# leave the input empty or wrong. +if [[ ! "$TOOLCHAIN" =~ ^[A-Za-z0-9][A-Za-z0-9._-]*$ ]]; then + printf 'fix-rust-toolchain-input: TOOLCHAIN=%q is not a toolchain name (letters, digits, ., _, -); nothing was changed\n' "$TOOLCHAIN" >&2 + exit 2 +fi +TARGET="${1:?Usage: fix-rust-toolchain-input.sh }" +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# One workflow-pin fixer per checkout at a time (see lib/pin-fixer-lock.sh): +# a single workflow file is locked by the checkout it belongs to — found from +# the file'"'"'s directory (`/.github/workflows/`, relative or +# absolute), so every fixer on that checkout contends for the same lock. +. "$SCRIPT_DIR/lib/pin-fixer-lock.sh" +TOOLCHAIN_YAML="$TOOLCHAIN" +if pin_fixer_yaml_nonstring "$TOOLCHAIN"; then TOOLCHAIN_YAML="'$TOOLCHAIN'"; fi +# anchor_defs/2: the anchors a line defines, outside quoted scalars and comments. +ANCHORS_AWK=$(cat "$SCRIPT_DIR/lib/yaml-anchors.awk") +if [ -d "$TARGET" ]; then + pin_fixer_lock "$TARGET" +else + target_dir=$(cd "$(dirname "$TARGET")" 2>/dev/null && pwd -P || dirname "$TARGET") + case "$target_dir" in + */.github/workflows) pin_fixer_lock "${target_dir%/.github/workflows}" ;; + *) pin_fixer_lock "$target_dir" ;; + esac +fi + +# Add missing TOOLCHAIN inputs to SHA-pinned Rust toolchain steps in file $1. +# Return 0 for a repair or no-op; return 2 and leave the file untouched if +# parsing fails or a pinned reference uses an unsupported layout. +apply_to_file() { + local f="$1" tmp rc + [ -f "$f" ] || { echo "SKIP (not a file): $f" >&2; return 0; } + # GitHub resolves `Dtolnay/Rust-Toolchain` to the same action: the name is + # matched in either case here and in the parser below. A double-quoted + # scalar may spell it with escapes (`"\x64tolnay/..."`, `"dtolnay\/..."`), + # which only the parser decodes, so a file holding any backslash is read + # too. + grep -qiE 'dtolnay/rust-toolchain@[0-9a-fA-F]{40}|[\]' "$f" || return 0 + + tmp=$(mktemp) + set +e + awk -v toolchain="$TOOLCHAIN_YAML" "$ANCHORS_AWK"' + # Return the number of leading spaces in source line s. + function indent_of(s, t) { t = s; sub(/[^ ].*$/, "", t); return length(t) } + # Column of a key, with a list dash counted as indentation: ` - with:` -> 4. + function key_indent_of(s, t) { t = s; sub(/[A-Za-z"'"'"'].*$/, "", t); gsub(/-/, " ", t); return t } + # Return true when s is neither blank nor a full-line comment. + function plain(s) { return (s !~ /^[[:space:]]*$/ && s !~ /^[[:space:]]*#/) } + # Return a string of n spaces for an inserted YAML line. + function spaces(n, t) { t = ""; while (n-- > 0) t = t " "; return t } + # Only text OUTSIDE quoted scalars and comments is code: a + # `components: "}"` or a `# }` is text, not the end of the mapping. + function code_only(s, t) { t = s; gsub(/"([^"\\]|\\.)*"/, "\"\"", t); gsub(/'"'"'[^'"'"']*'"'"'/, "'"'"''"'"'", t); sub(/(^|[[:space:]])#.*$/, "", t); return t } + # The inputs one line names at the `with:` flow mapping'"'"'s OWN level, + # walked from brace depth `depth` — 0 on the `with: {` line, the depth + # the previous line left on a continuation line. A flow mapping closes + # when the depth returns to zero, not at the first `}`: an expression + # `${{ matrix.x }}` opens and closes its own braces (its `$` stays as + # the value'"'"'s text), and a nested mapping and its contents are dropped + # with its braces. The brace that opens the mapping and the one that + # closes it are kept. A quoted VALUE becomes the one-letter text `"v"` + # (a `"clippy, toolchain: nightly"` is text, a quoted key is still a + # key) while an empty one (`""`, `'"'"''"'"'`) stays empty, and a comment ends + # the line. Text BEFORE a nested group closes is hidden; text AFTER it + # on the same line (`channel: beta }, toolchain: nightly`) is read at + # the mapping'"'"'s level like any other entry. The depth the line leaves + # is left in lv_depth: the walk that reads the entries is the one that + # counts the braces, so the two can never disagree. lv_open is set + # when a quoted scalar is still open at the line'"'"'s end — continued + # past an escaped line break (`"toolch\` then `ain": nightly`) or + # folded onto the next line: the next line then starts inside it, and + # no line-by-line view reads the keys around it. + function level_view(s, depth, out, i, n, c, j, body) { + out = ""; n = length(s); i = 1; lv_open = 0 + while (i <= n) { + c = substr(s, i, 1) + if (c == "#" && (i == 1 || substr(s, i - 1, 1) ~ /[[:space:]]/)) break + if (c == "\"" || c == "'"'"'") { + j = i + 1 + if (c == "\"") { while (j <= n && substr(s, j, 1) != "\"") { if (substr(s, j, 1) == "\\") j++; j++ } } + else { while (j <= n && (substr(s, j, 1) != "'"'"'" || substr(s, j + 1, 1) == "'"'"'")) j += (substr(s, j, 1) == "'"'"'" ? 2 : 1) } + body = substr(s, i + 1, j - i - 1) + if (j > n) lv_open = 1 + if (depth == 1) { + if (substr(s, j + 1) ~ /^[[:space:]]*:/) out = out c body c + else out = out c (body == "" ? "" : "v") c + } + i = j + 1; continue + } + if (c == "{") { depth++; if (depth == 1) out = out c } + else if (c == "}") { if (depth == 1) out = out c; depth-- } + else if (depth == 1) out = out c + i++ + } + lv_depth = depth + return out + } + # A 40-hex pin of the action on a non-comment line, trailing comment + # stripped. `@stable` and `@1.85.0` are tags: the ref carries the + # channel and the step is not ours. + # The 40 hex characters must END the ref: `@-patched` is a tag + # that merely begins with one, and carries its own channel. Either + # case, for the commit ID (git resolves upper-case hex too) and for the + # action name (GitHub resolves `Dtolnay/Rust-Toolchain` too). The line + # is read as YAML reads it: a double-quoted value may spell the + # reference with escapes (`"\x64tolnay/..."`), lib/yaml-anchors.awk. + function sha_ref(s, t, m, nxt) { + if (!plain(s)) return 0 + t = tolower(yaml_decode_dq(s)); sub(/[[:space:]]#.*$/, "", t) + if (!match(t, /dtolnay\/rust-toolchain@[0-9a-f]+/)) return 0 + m = substr(t, RSTART + 23, RLENGTH - 23) + nxt = substr(t, RSTART + RLENGTH, 1) + return (length(m) == 40 && (nxt == "" || nxt ~ /["'"'"'[:space:],}\]]/)) + } + # The line with its comment dropped and every quoted scalar that is + # neither a key nor the value of a `uses` key (past any anchor or tag) + # blanked to `""`. + function site_view(s, out, i, n, c, j, body) { + out = ""; n = length(s); i = 1 + while (i <= n) { + c = substr(s, i, 1) + if (c == "#" && (i == 1 || substr(s, i - 1, 1) ~ /[[:space:]]/)) break + if (c == "\"" || c == "'"'"'") { + j = i + 1 + if (c == "\"") { while (j <= n && substr(s, j, 1) != "\"") { if (substr(s, j, 1) == "\\") j++; j++ } } + else { while (j <= n && (substr(s, j, 1) != "'"'"'" || substr(s, j + 1, 1) == "'"'"'")) j += (substr(s, j, 1) == "'"'"'" ? 2 : 1) } + body = substr(s, i + 1, j - i - 1) + if (substr(s, j + 1) ~ /^[[:space:]]*:/ || out ~ /(^|[{,[[:space:]])["'"'"']?uses["'"'"']?[[:space:]]*:[[:space:]]*([&!][^[:space:],}]*[[:space:]]+)*$/) out = out c body c + else out = out c c + i = j + 1; continue + } + out = out c; i++ + } + return out + } + # A SHA pin that is the value of a `uses` key, block or flow: the + # only pin the parser can be asked to place. The same text inside + # another key'"'"'s string (`name: "fixture dtolnay/...@"`) is data. + function uses_pin(s) { return sha_ref(s) && tolower(site_view(yaml_decode_dq(s))) ~ USES_SITE_RE } + # A `toolchain` value that carries YAML node properties (`&channel + # null`, `!!str ""`), as `val` writes it from its first property: + # "set" when the scalar after them is one the action reads (`!!str + # null` is the string null), "bad" when it is null or empty. A "bad" + # value is reported, never filled in place: every alias of its anchor + # would change with it. + function after_key(t) { sub(/^[^:]*:[[:space:]]*/, "", t); return t } + function tc_props(val, tags) { + tags = "" + while (match(val, /^[&!][^[:space:],}]*/)) { + if (substr(val, 1, 1) == "!") tags = tags " " substr(val, 1, RLENGTH) + val = substr(val, RLENGTH + 1); sub(/^[[:space:]]+/, "", val) + } + sub(/(^|[[:space:]]+)#.*$/, "", val); sub(/[[:space:]]+$/, "", val) + if ((tags " ") ~ / !!null /) return "bad" + if ((tags " ") ~ / !!str /) return (val == "" || val == "\"\"" || val == "'"'"''"'"'") ? "bad" : "set" + return (val ~ /^(null|Null|NULL|~|""|'"'"''"'"')?$/) ? "bad" : "set" + } + # Does line s give a `uses` key, block or flow, an alias that may be + # the SHA-pinned toolchain reference — of an anchor that holds it, or + # of no anchor read so far? Every such alias on the line is weighed. + function bad_alias(s, v, m) { + v = site_view(yaml_decode_dq(s)) + while (match(tolower(v), USES_ALIAS_RE)) { + m = substr(v, RSTART, RLENGTH); sub(/^[^*]*\*/, "", m) + if ((m in tc_anchor) || !(m in any_anchor)) return 1 + v = substr(v, RSTART + RLENGTH) + } + return 0 + } + # Buffer line $0 of the current step as buf[n], with the verdict on + # its `uses:` alias, if any, as the anchors read so far give it — + # YAML resolves an alias to the latest anchor of that name BEFORE it, + # so the verdict cannot wait for the end of the step, when a later + # line may have redefined the name: "bad" when the anchor holds a + # SHA-pinned toolchain reference or no anchor of that name has been + # seen, else "ok". Anchor names are case-sensitive: the key is + # matched in lower case, the name is taken as written. + function buffer( v, m) { + buf[++n] = $0; lnum[n] = NR; alias_state[n] = "" + v = site_view(yaml_decode_dq($0)) + if (match(tolower(v), USES_ALIAS_RE)) { + m = substr(v, RSTART, RLENGTH); sub(/^[^*]*\*/, "", m) + alias_state[n] = ((m in tc_anchor) || !(m in any_anchor)) ? "bad" : "ok" + } + } + # YAML context outside list items: the key at each indentation, so a + # list item opens a step only when its parent key is `steps:`. + function record_key(s, ind, k, t) { + ind = indent_of(s) + t = s; sub(/^[[:space:]]*/, "", t); sub(/[[:space:]]*:.*$/, "", t); gsub(/["'"'"']/, "", t) + for (k in keyat) if (k + 0 > ind) delete keyat[k] + keyat[ind] = t + } + # Return true if the nearest recorded key at or above ind is steps. + function parent_is_steps(ind, k, best) { + best = -1 + for (k in keyat) if (k + 0 <= ind && k + 0 > best) best = k + 0 + return (best >= 0 && keyat[best] == "steps") + } + # Return true if the nearest recorded key strictly above ind is steps: + # a key line inside a flow-style step spread over several lines. + function under_steps(ind, k, best) { + best = -1 + for (k in keyat) if (k + 0 < ind && k + 0 > best) best = k + 0 + return (best >= 0 && keyat[best] == "steps") + } + # Emit the buffered step, patched if it is a SHA-pinned dtolnay step + # whose `with:` names no toolchain. + function flush( i, j, jl, dl, line, ind, uses_i, has_with, with_bad, with_i, with_flow, with_indent, in_with, in_flow, flow_depth, child_indent, has_tc, key_indent, skey, flow_item, tc_empty_i, tc_empty_flow, tc_empty_eol, tc_nested, tc_await, tc_flow_await, m, pre, post, delim, comment, v, w, lv_rep) { + if (n == 0) return + # A step given as an alias (`- *rust`) is the mapping its anchor + # holds, which may be a SHA-pinned toolchain step: noted, and + # reported at the end when the file holds such a pin anywhere. + if (buf[1] ~ STEP_ALIAS_RE) alias_step[++nalias] = lnum[1] + # The step'"'"'s key column: the opener'"'"'s key (`- uses:` -> the column of + # `uses`), or for a bare `-` — alone, or followed only by a comment + # (`- # install Rust`) — the indentation of the first plain line under it. + flow_item = (buf[1] ~ /^[[:space:]]*-[[:space:]]*\{/) + if (buf[1] ~ /^[[:space:]]*-[[:space:]]+[^[:space:]#]/) skey = length(key_indent_of(buf[1])) + else { + skey = -1 + for (j = 2; j <= n; j++) if (plain(buf[j])) { skey = indent_of(buf[j]); break } + if (skey < 0) skey = indent_of(buf[1]) + 2 + } + # Place every SHA-pinned reference in the step: the step'"'"'s own `uses:` + # key, data nested under some other key (left alone), or something the + # parser cannot place (a flow-style step) — that is reported, not guessed. + uses_i = 0 + for (i = 1; i <= n; i++) { + # A double-quoted value continued past an escaped line break + # (`uses: "dtolnay/rust-\` then `toolchain@"`) is the + # reference YAML joins from the lines below: a SHA-pinned + # toolchain read so is reported, never passed over as no pin. + if (yaml_dq_continued(buf[i])) { + jl = buf[i] + for (j = i + 1; j <= n && yaml_dq_continued(jl); j++) jl = yaml_dq_join(jl, buf[j]) + if (uses_pin(jl)) unrec[++nunrec] = lnum[i] + } + # The step'"'"'s own `uses:` with its value continued below the key + # (`uses: >-` then `dtolnay/rust-toolchain@`): the key line + # names no action and the value reads as nested text, so a SHA + # pin there is reported, never passed over as a step that is fine. + if (!flow_item && length(key_indent_of(buf[i])) == skey && tolower(site_view(yaml_decode_dq(buf[i]))) ~ USES_OPEN_RE) + for (j = i + 1; j <= n && !(plain(buf[j]) && indent_of(buf[j]) <= skey); j++) + if (sha_ref(buf[j])) unrec[++nunrec] = lnum[j] + # The step'"'"'s own `uses:` given as an alias (`uses: *rust`) of an + # anchor that holds a SHA-pinned toolchain reference: the step + # runs that action, but the reference is not where the parser + # places a step'"'"'s action. Reported, never passed over. + # An alias of no anchor read here could be that reference too. + # The verdict was taken when the line was read (alias_state): + # an alias names the anchor defined before it, not one a later + # line of the step defines again under the same name. + if ((flow_item || length(key_indent_of(buf[i])) == skey) && alias_state[i] == "bad") unrec[++nunrec] = lnum[i] + if (!sha_ref(buf[i])) continue + if (!flow_item && tolower(yaml_decode_dq(buf[i])) ~ USES_RE && length(key_indent_of(buf[i])) == skey) { if (!uses_i) uses_i = i; continue } + if (!flow_item && i > 1 && indent_of(buf[i]) > skey) continue + if (!uses_pin(buf[i])) continue + unrec[++nunrec] = lnum[i] + } + if (!uses_i) { for (i = 1; i <= n; i++) print buf[i]; n = 0; return } + has_with = 0; with_bad = 0; with_i = 0; with_flow = 0; with_indent = -1; in_with = 0; in_flow = 0; child_indent = -1; has_tc = 0 + tc_empty_i = 0; tc_empty_flow = 0; tc_empty_eol = 0; tc_nested = 0; tc_await = 0; tc_flow_await = 0; lv_rep = 0 + for (i = 1; i <= n; i++) { + line = buf[i]; ind = indent_of(line) + if (in_flow) { + # A flow `with: {` spread over several lines: every entry up to + # the brace that closes it is an input. An EMPTY `toolchain:` + # entry (`toolchain: ,` / `toolchain: null`) is the key with no + # value: it is given one in place, never duplicated. Only text + # at the mapping'"'"'s own depth names inputs — outside quoted + # values and nested mappings, and including the rest of a line + # on which a nested mapping closes. + # Keys are read as YAML reads them: `"tool\x63hain":` is the + # input `toolchain`. + v = level_view(yaml_decode_dq(line), flow_depth); flow_depth = lv_depth + # A quoted key or value left open at this line'"'"'s end hides + # what the next line names: the inputs are not read here. + # The step is reported once, at the line the scalar opens on. + if (lv_open && !lv_rep) { lv_rep = 1; with_bad = 1; unrec[++nunrec] = lnum[i] } + # A bare `toolchain:` that ended the previous line is empty + # only when the next token closes the mapping or moves on + # (`}`, `,`); anything else is its value, spread over lines, + # and not this fixer'"'"'s to place. + if (tc_flow_await) { w = code_only(line); sub(/^[[:space:]]*/, "", w); if (w != "") { tc_flow_await = 0; if (w !~ /^[,}]/) tc_nested = 1 } } + if (match(v, TC_FLOW_PROP_RE)) { if (tc_props(after_key(substr(v, RSTART, RLENGTH))) == "set") has_tc = 1; else { with_bad = 1; unrec[++nunrec] = lnum[i] } } + else if (v ~ TC_FLOW_EMPTY_RE) { tc_empty_i = i; tc_empty_flow = 1 } + else if (v ~ TC_FLOW_EOL_RE) { tc_empty_i = i; tc_empty_eol = 1; if (v ~ TC_FLOW_BARE_EOL_RE) tc_flow_await = 1 } + else if (v ~ TC_RE || v ~ TC_FLOW_RE) has_tc = 1 + if (flow_depth <= 0) in_flow = 0 + continue + } + if (in_with) { + if (plain(line) && ind <= with_indent) in_with = 0 + else if (plain(line)) { + dl = yaml_decode_dq(line) + # Only a DIRECT child of `with:` is an input: the contents of + # another input'"'"'s block scalar (`components: |`) sit deeper. + # The first must be a block mapping key: `with:` whose value + # is a flow mapping, an alias or anything else written on the + # next line (`{ components: rustfmt }`, `*inputs`) is a shape + # a block child cannot be added to, so the step is reported. + if (child_indent < 0) { + child_indent = ind + if (dl !~ WITH_CHILD_KEY_RE) { with_bad = 1; unrec[++nunrec] = lnum[i] } + } + # An empty `toolchain:` whose next plain line (blank and + # comment lines between them notwithstanding) sits deeper is + # a nested mapping, not an empty value: nothing this fixer + # can repair. + if (tc_await) { tc_await = 0; if (ind > child_indent) tc_nested = 1 } + if (ind == child_indent && match(dl, TC_PROP_RE)) { if (tc_props(after_key(dl)) == "set") has_tc = 1; else { with_bad = 1; unrec[++nunrec] = lnum[i] } } + else if (ind == child_indent && dl ~ TC_EMPTY_RE) { tc_empty_i = i; tc_await = 1 } + else if (ind == child_indent && dl ~ TC_RE) has_tc = 1 + } + } + dl = yaml_decode_dq(line) + if (!in_with && !has_with && length(key_indent_of(line)) == skey) { + if (dl ~ WITH_BLOCK_RE) { + has_with = 1; with_i = i; with_indent = skey; in_with = 1 + } else if (dl ~ WITH_FLOW_RE) { + has_with = 1; with_i = i; with_flow = 1 + v = level_view(dl, 0); flow_depth = lv_depth + if (lv_open) { lv_rep = 1; with_bad = 1; unrec[++nunrec] = lnum[i] } + if (match(v, TC_FLOW_PROP_RE)) { if (tc_props(after_key(substr(v, RSTART, RLENGTH))) == "set") has_tc = 1; else { with_bad = 1; unrec[++nunrec] = lnum[i] } } + else if (v ~ TC_FLOW_EMPTY_RE) { tc_empty_i = i; tc_empty_flow = 1 } + else if (v ~ TC_FLOW_EOL_RE) { tc_empty_i = i; tc_empty_eol = 1; if (v ~ TC_FLOW_BARE_EOL_RE) tc_flow_await = 1 } + else if (v ~ TC_FLOW_RE) has_tc = 1 + if (flow_depth > 0) in_flow = 1 + } else if (dl ~ WITH_ANY_RE) { + # `with: &anchor`, `with: *alias`, or any value shape this + # parser does not read: a second `with:` would be worse than + # none, so the step is reported, not rewritten. + has_with = 1; with_bad = 1; unrec[++nunrec] = lnum[i] + } + } + } + if (tc_flow_await) tc_nested = 1 + if (tc_nested) { unrec[++nunrec] = lnum[tc_empty_i]; with_bad = 1 } + if (tc_empty_i && tc_empty_flow && !match(buf[tc_empty_i], TC_FLOW_EMPTY_RE)) { unrec[++nunrec] = lnum[tc_empty_i]; with_bad = 1 } + if (tc_empty_i && tc_empty_eol && !match(buf[tc_empty_i], TC_FLOW_EOL_RAW_RE)) { unrec[++nunrec] = lnum[tc_empty_i]; with_bad = 1 } + if (has_tc || with_bad) { for (i = 1; i <= n; i++) print buf[i]; n = 0; return } + for (i = 1; i <= n; i++) { + line = buf[i] + if (tc_empty_i && i == tc_empty_i) { + # The key is there with no value: fill it where it stands. + if (tc_empty_flow) { + match(line, TC_FLOW_EMPTY_RE) + pre = substr(line, 1, RSTART - 1); m = substr(line, RSTART, RLENGTH); post = substr(line, RSTART + RLENGTH) + delim = substr(m, length(m)) + sub("[[:space:]]*" NULLV "[[:space:]]*[,}]$", "", m) + line = pre m " " toolchain (delim == "}" ? " }" : ",") post + } else { + # A block entry, or a flow entry whose delimiter is on + # the next line: the value ends the line either way. + comment = "" + if (match(line, /[[:space:]]+#.*$/)) comment = substr(line, RSTART) + sub(":[[:space:]]*" NULLV "[[:space:]]*(#.*)?$", "", line) + line = line ": " toolchain comment + } + print line; fixed++ + } else if (has_with && with_flow && i == with_i && !tc_empty_i) { + if (line ~ /\{[[:space:]]*\}/) sub(/\{[[:space:]]*\}/, "{ toolchain: " toolchain " }", line) + else if (line ~ /\{[[:space:]]*$/) sub(/\{[[:space:]]*$/, "{ toolchain: " toolchain ",", line) + else sub(/\{[[:space:]]*/, "{ toolchain: " toolchain ", ", line) + print line; fixed++ + } else if (has_with && !with_flow && i == with_i && !tc_empty_i) { + print line + print spaces(child_indent >= 0 ? child_indent : with_indent + 2) "toolchain: " toolchain + fixed++ + } else if (!has_with && i == uses_i) { + print line + # `with:` sits at the column of the step'"'"'s keys: the `uses:` + # column, with a list dash counted as indentation. + key_indent = key_indent_of(line) + print key_indent "with:" + print key_indent " toolchain: " toolchain + fixed++ + } else print line + } + n = 0 + } + BEGIN { + n = 0; in_step = 0; in_scalar = 0; fixed = 0; nunrec = 0; anc_scalar = -1 + Q = "[\"'"'"']?" + LIST_RE = "^[[:space:]]*-([[:space:]]|$)" + KEY_RE = "^[[:space:]]*" Q "[A-Za-z0-9_.-]+" Q "[[:space:]]*:([[:space:]]|$)" + SCALAR_RE = ":[[:space:]]*[|>][-+0-9]*[[:space:]]*(#.*)?$" + # Matched against the line in lower case. + USES_RE = "^[[:space:]]*(-[[:space:]]+)?" Q "uses" Q "[[:space:]]*:[[:space:]]*" Q "dtolnay/rust-toolchain@[0-9a-f]+" + # The value may carry YAML node properties, an anchor or a tag + # (`uses: &rust ...`, `uses: !!str ...`): still the step'"'"'s pin, but + # not one USES_RE places, so such a step is reported. A flow entry + # starts after `{`, `,` or `[`: `steps: [uses: ...]` is a list of + # one step, a mapping written without its braces. + USES_SITE_RE = "(^|[-{,[[:space:]])" Q "uses" Q "[[:space:]]*:[[:space:]]*([&!][^[:space:],}]*[[:space:]]+)*" Q "dtolnay/rust-toolchain@[0-9a-f]+" + # A `uses:` whose value is an alias, `*name` (matched in lower case), + # the name read whole: up to whitespace or a flow indicator. + USES_ALIAS_RE = "(^|[-{,[[:space:]])" Q "uses" Q "[[:space:]]*:[[:space:]]*\\*[^],[{}[:space:]]+" + # A whole step given as an alias: a block item (`- *rust`), or an + # entry of a flow `steps:` sequence (`[*rust]`, `, *rust`). + STEP_ALIAS_RE = "^[[:space:]]*-[[:space:]]+\\*[^],[{}[:space:]]+[[:space:]]*(#.*)?$" + FLOW_ALIAS_RE = "(^|[[,])[[:space:]]*\\*[^],[{}[:space:]]+" + # A `uses:` key whose value is not on its line: a block scalar + # (`uses: >-`) or nothing at all, the reference on the lines below. + USES_OPEN_RE = "^[[:space:]]*(-[[:space:]]+)?" Q "uses" Q "[[:space:]]*:[[:space:]]*([&!][^[:space:],}]*[[:space:]]*)*([|>][-+0-9]*)?[[:space:]]*$" + WITH_BLOCK_RE = "^[[:space:]]*(-[[:space:]]+)?" Q "with" Q "[[:space:]]*:[[:space:]]*(#.*)?$" + WITH_FLOW_RE = "^[[:space:]]*(-[[:space:]]+)?" Q "with" Q "[[:space:]]*:[[:space:]]*\\{" + WITH_ANY_RE = "^[[:space:]]*(-[[:space:]]+)?" Q "with" Q "[[:space:]]*:" + WITH_CHILD_KEY_RE = "^[[:space:]]*" Q "[A-Za-z0-9_.-]+" Q "[[:space:]]*:([[:space:]]|$)" + TC_RE = "^[[:space:]]*" Q "toolchain" Q "[[:space:]]*:[[:space:]]*[^[:space:]]" + TC_FLOW_RE = "[{,][[:space:]]*" Q "toolchain" Q "[[:space:]]*:[[:space:]]*[^[:space:],}]" + # The key with a value that starts with node properties (an + # anchor or a tag): read by tc_props/1 rather than the patterns + # below, which see `&channel null` as a value. + TC_PROP_RE = "^[[:space:]]*" Q "toolchain" Q "[[:space:]]*:[[:space:]]*[&!]" + TC_FLOW_PROP_RE = "(^|[{,])[[:space:]]*" Q "toolchain" Q "[[:space:]]*:[[:space:]]*[&!][^,}]*" + # The key present with no value — YAML null in the spellings the core + # schema admits unquoted (`toolchain:`, `null`, `Null`, `NULL`, `~`) + # or the empty string (`""`, `'"'"''"'"'`): not an input the action can + # read, and not a key to write twice. `"null"` quoted is the string + # null, a value. + NULLV = "(null|Null|NULL|~|\"\"|'"'"''"'"')?" + TC_EMPTY_RE = "^[[:space:]]*" Q "toolchain" Q "[[:space:]]*:[[:space:]]*" NULLV "[[:space:]]*(#.*)?$" + TC_FLOW_EMPTY_RE = "(^|[{,])[[:space:]]*" Q "toolchain" Q "[[:space:]]*:[[:space:]]*" NULLV "[[:space:]]*[,}]" + # The same entry with its delimiter on the next line of a multi-line + # flow mapping (`toolchain: null` then `}`); a BARE key there waits + # for the next token, which must be that delimiter. + TC_FLOW_EOL_RE = "(^|[{,])[[:space:]]*" Q "toolchain" Q "[[:space:]]*:[[:space:]]*" NULLV "[[:space:]]*$" + TC_FLOW_EOL_RAW_RE = "(^|[{,])[[:space:]]*" Q "toolchain" Q "[[:space:]]*:[[:space:]]*" NULLV "[[:space:]]*(#.*)?$" + TC_FLOW_BARE_EOL_RE = "(^|[{,])[[:space:]]*" Q "toolchain" Q "[[:space:]]*:[[:space:]]*$" + } + { + ind = indent_of($0) + # Any SHA-pinned toolchain reference anywhere in the file: an + # aliased step may be one. + if (sha_ref($0)) any_pin = 1 + # Every anchor whose value is a SHA-pinned toolchain reference + # (`ACTION: &rust dtolnay/rust-toolchain@`), wherever it is + # defined: an alias of it is that reference. Anchors are read by + # lib/yaml-anchors.awk (outside quoted scalars and comments) and + # never from a block scalar'"'"'s content (`run: |` text); a later + # anchor of the same name replaces an earlier one. An anchor whose + # quoted value runs on past its line (`&rust "dtolnay/rust-\`) is + # not read from the one line, so its alias may be that reference + # too: it counts as one. + if (anc_scalar >= 0 && (!plain($0) || ind > anc_scalar)) { } + else { + anc_scalar = -1 + if (plain($0)) { + anchor_defs($0, d) + for (a in d) { any_anchor[a] = 1; if (sha_ref(d[a]) || index(d[a], "\002")) tc_anchor[a] = 1; else delete tc_anchor[a] } + if ($0 ~ SCALAR_RE) anc_scalar = length(key_indent_of($0)) + } + } + # A block scalar outside any step (`FIXTURE: |` under `env:`) is text + # until a plain line at its key'"'"'s indentation or shallower. + if (!in_step && in_scalar) { + if (plain($0) && ind <= scalar_indent) in_scalar = 0 + else { print; next } + } + if (in_step) { + # The step ends at the next list item at its own indentation (or + # shallower), or at a shallower key. Blank and comment lines and + # deeper lines belong to it. + if ($0 ~ LIST_RE && ind <= step_indent) { flush(); in_step = 0 } + else if (plain($0) && ind < step_indent) { flush(); in_step = 0 } + } + if (in_step) { buffer(); next } + if ($0 ~ LIST_RE) { + if (parent_is_steps(ind)) { in_step = 1; step_indent = ind; n = 0; buffer(); next } + # A list item whose parent is not `steps:` (a matrix, a fixture) is data. + print; next + } + if (plain($0) && $0 ~ KEY_RE) { + record_key($0) + if ($0 ~ SCALAR_RE) { in_scalar = 1; scalar_indent = ind } + # A key line that itself carries the pin (`steps: [ { uses: ... } ]` + # on one line, a key of a flow-style step spread over lines) is a + # step this parser cannot place. The pin inside some other value + # (a workflow-level `env:` string naming the action) is data. + # So is an alias such a step gives its `uses` (`steps: [uses: + # *rust]`) of an anchor that may hold the pin. + if ((uses_pin($0) || bad_alias($0)) && (parent_is_steps(ind) || under_steps(ind))) unrec[++nunrec] = NR + # A step written into `steps: [*rust]` as an alias, as `- *rust`. + if (keyat[ind] == "steps" && site_view(yaml_decode_dq($0)) ~ FLOW_ALIAS_RE) alias_step[++nalias] = NR + } else if (parent_is_steps(ind)) { + if (uses_pin($0) || bad_alias($0)) unrec[++nunrec] = NR + # A line of a `steps: [` sequence spread over lines. + if (site_view(yaml_decode_dq($0)) ~ FLOW_ALIAS_RE) alias_step[++nalias] = NR + } + print + } + END { + flush() + if (any_pin) for (j = 1; j <= nalias; j++) unrec[++nunrec] = alias_step[j] + if (nunrec) { + printf "UNSUPPORTED %s: %d SHA-pinned dtolnay/rust-toolchain reference(s) the parser cannot place as a block-style step key (a flow-style `- { ... }` step, a `uses:` value continued below its key, an alias of an anchored reference, or a step given as an alias?), line(s):", FILENAME, nunrec > "/dev/stderr" + for (j = 1; j <= nunrec; j++) printf " %d", unrec[j] > "/dev/stderr" + printf ". Fix by hand; the file was not changed.\n" > "/dev/stderr" + exit 2 + } + if (fixed) printf "FIXED %s steps=%d toolchain=%s\n", FILENAME, fixed, toolchain > "/dev/stderr" + } + ' "$f" > "$tmp"; rc=$? + set -e + if [ "$rc" -ne 0 ]; then rm -f "$tmp"; return 2; fi + + if ! cmp -s "$f" "$tmp"; then pin_fixer_replace "$tmp" "$f"; else rm -f "$tmp"; echo "OK (every SHA-pinned dtolnay step names a toolchain): $f"; fi +} + +unsupported=0 +if [ -d "$TARGET" ]; then + for f in "$TARGET"/.github/workflows/*.yml "$TARGET"/.github/workflows/*.yaml; do + [ -f "$f" ] || continue + apply_to_file "$f" || unsupported=$((unsupported + 1)) + done +else + apply_to_file "$TARGET" || unsupported=$((unsupported + 1)) +fi +if [ "$unsupported" -ne 0 ]; then + echo "fix-rust-toolchain-input: $unsupported file(s) left untouched (UNSUPPORTED above); not a clean success." >&2 + exit 2 +fi diff --git a/scripts/fix-script-registry.json b/scripts/fix-script-registry.json index c2f6bb5c..d9caf09d 100644 --- a/scripts/fix-script-registry.json +++ b/scripts/fix-script-registry.json @@ -49,7 +49,10 @@ "LicensePMPLDrift": "fix-pmpl-drift.sh", "ActionsPolicyTooRestrictive": "fix-actions-policy.sh", "SessionDetritus": "fix-session-detritus.sh", - "RetiredDescriptilePolicy": "fix-retired-descriptile-policy.sh" + "RetiredDescriptilePolicy": "fix-retired-descriptile-policy.sh", + "ToolchainInputMissing": "fix-rust-toolchain-input.sh", + "StaleReusablePin": "fix-stale-reusable-pin.sh", + "LockSelfShaRef": "fix-lock-self-sha-refs.sh" }, "by_recipe": { "recipe-actions-allow-all": "fix-actions-policy.sh", @@ -111,7 +114,14 @@ "reusable_workflow_sha_bump_needs_propagation": "propagate-sha-bump.sh", "recipe-fix-pmpl-drift": "fix-pmpl-drift.sh", "recipe-archive-session-detritus": "fix-session-detritus.sh", - "recipe-retired-descriptile-policy": "fix-retired-descriptile-policy.sh" + "recipe-retired-descriptile-policy": "fix-retired-descriptile-policy.sh", + "WF026": "fix-rust-toolchain-input.sh", + "workflow_audit/sha_pinned_toolchain_without_input": "fix-rust-toolchain-input.sh", + "WF027": "fix-stale-reusable-pin.sh", + "workflow_audit/known_broken_reusable_revision": "fix-stale-reusable-pin.sh", + "PIN-003": "fix-stale-reusable-pin.sh", + "PI006": "fix-lock-self-sha-refs.sh", + "pin_integrity/self_sha_lock_record": "fix-lock-self-sha-refs.sh" } } } diff --git a/scripts/fix-stale-reusable-pin.sh b/scripts/fix-stale-reusable-pin.sh new file mode 100755 index 00000000..d2c09247 --- /dev/null +++ b/scripts/fix-stale-reusable-pin.sh @@ -0,0 +1,895 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: MPL-2.0 +# fix-stale-reusable-pin.sh — move a caller off a reusable-workflow pin that is +# KNOWN DEAD. Hypatia rules WF027 and PI001 via PIN-003 (standards pins in a +# measured dead window), for the pins scripts/lib/stale-reusable-pins.tsv +# names. WF016's orphan pins are not in that table, so they are not this +# script's until a human records each one's replacement there; the auto-fix half of +# `reusable_workflow_sha_bump_needs_propagation` for pins the estate has already +# diagnosed. +# +# A caller line +# uses: hyperpolymath/standards/.github/workflows/rust-ci-reusable.yml@ +# is rewritten to the replacement SHA the table names for that pin, and ONLY +# for pins the table names: this script never guesses that "newer is better". +# standards' scripts/lock-selfcheck.sh exists because a newer standards commit +# can be LESS usable than an older one, so each replacement in the table was +# chosen by hand against that check. +# +# If the repository carries .github/workflows/actions.lock, the lock follows +# each rewritten CALLER, not the SHA: in `workflows:` only the rewritten +# workflow's own entry moves to the replacement. In `dependencies:` the stale +# record is renamed when no workflow references it any more, and copied under +# the replacement key (stale record kept) while another workflow still pins +# the stale SHA — a lock key is shared by every reusable of a repository, and +# a caller the table does not cover must keep its record. Records that +# collapse onto one key are merged. A record's nested `uses:` list is NOT +# recomputed: run `gh actions-lock` (or the repository's check-lock-sync.sh) +# afterwards; the script says so when it touches a lock. +# +# Idempotent. Rewrites nothing it does not recognise. Exit 0 whether or not +# anything changed; exit 2 when the table is unreadable or a file carries a +# known-dead pin the parser cannot place as a `uses` key (that file is left +# untouched and named), or when actions.lock is left listing a dependency +# twice that the record merger cannot fold (named); exit 3 when the pins were +# rewritten but actions.lock was touched and still needs `gh actions-lock` — +# a dispatcher must not record either as a clean success. +# +# Usage: +# fix-stale-reusable-pin.sh +# PINS_TABLE=/path/to/table.tsv fix-stale-reusable-pin.sh +# +# Table: scripts/lib/stale-reusable-pins.tsv — reusable_pathstale_sha +# replacement_shareason. reusable_path may end in `/*` to cover every +# reusable of that repository. + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PINS_TABLE="${PINS_TABLE:-$SCRIPT_DIR/lib/stale-reusable-pins.tsv}" +REPO_PATH="${1:?Usage: fix-stale-reusable-pin.sh }" + +[ -r "$PINS_TABLE" ] || { echo "fix-stale-reusable-pin: table not readable: $PINS_TABLE" >&2; exit 2; } + +# Every row is checked before any caller is touched: a malformed row found +# mid-run would leave earlier callers rewritten and the lock never reached. +# The owners the table names are the owners a caller may be pinned to; the +# candidate scan is built from them, so a private table for another owner +# (`octo/example/.github/workflows/*`) finds its callers. +TABLE_OWNERS_RE="" +validate_table() { + local n=0 v_path v_stale v_new bad=0 owners="" + # `|| [ -n ... ]`: a final row the file ends on without a newline is a + # row too — `read` returns 1 on it with the fields filled in. + while IFS=$'\t' read -r v_path v_stale v_new _ || [ -n "$v_path" ]; do + n=$((n + 1)) + case "$v_path" in ''|'#'*) continue ;; esac + [[ "$v_stale" =~ ^[0-9a-f]{40}$ ]] || { echo "fix-stale-reusable-pin: table row $n: bad stale SHA '$v_stale'" >&2; bad=1; } + [[ "$v_new" =~ ^[0-9a-f]{40}$ ]] || { echo "fix-stale-reusable-pin: table row $n: bad replacement SHA '$v_new' for ${v_stale:0:7}" >&2; bad=1; } + # A row that moves a SHA to itself repairs nothing, yet every caller + # it matches would be reported FIXED on every run. + [ "$v_new" != "$v_stale" ] || { echo "fix-stale-reusable-pin: table row $n: replacement equals the stale SHA ${v_stale:0:7}; a row must move the pin" >&2; bad=1; } + # The only wildcard is a terminal `/*` (every reusable of the repository); + # a `*` anywhere else would be matched literally, never as a glob. + [[ "$v_path" =~ ^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+/\.github/workflows/([A-Za-z0-9_.-]+|\*)$ ]] || { echo "fix-stale-reusable-pin: table row $n: bad reusable path '$v_path' (the only wildcard is a terminal /*)" >&2; bad=1; } + owners="$owners${v_path%%/*}"$'\n' + done < "$PINS_TABLE" + [ "$bad" -eq 0 ] || { echo "fix-stale-reusable-pin: table $PINS_TABLE is invalid; nothing was changed" >&2; exit 2; } + TABLE_OWNERS_RE=$(printf '%s' "$owners" | sed -E 's/[.]/\\./g' | sort -u | paste -sd'|' -) + [ -n "$TABLE_OWNERS_RE" ] || TABLE_OWNERS_RE="hyperpolymath" +} +validate_table +[ -d "$REPO_PATH/.github/workflows" ] || { echo "OK (no workflows): $REPO_PATH"; exit 0; } +# One workflow-pin fixer per checkout at a time (see lib/pin-fixer-lock.sh). +. "$SCRIPT_DIR/lib/pin-fixer-lock.sh" +# anchor_defs/2: the anchors a line defines, outside quoted scalars and comments. +ANCHORS_AWK=$(cat "$SCRIPT_DIR/lib/yaml-anchors.awk") + +# The value of anchor as an alias on line of sees it: the +# last `&name` defined above that line (lib/yaml-anchors.awk reads anchors +# outside quoted scalars and comments, and a block scalar's content is +# skipped as text), or "" when there is none. A line ending in an escaped +# line break inside a double-quoted scalar (`CALL: &call "a/b/.github/\`) is +# read joined with the lines that continue it, as YAML reads the value; a +# value still open at its line's end (folded into the next line) is not read, +# "" as well. +anchor_value() { # + WANT="$2" awk -v upto="$3" "$ANCHORS_AWK"' + function indent_of(s, t) { t = s; sub(/[^ ].*$/, "", t); return length(t) } + BEGIN { blk = -1; got = ""; want = ENVIRON["WANT"] } + NR >= upto { exit } + { + plain = ($0 !~ /^[[:space:]]*$/ && $0 !~ /^[[:space:]]*#/) + if (blk >= 0 && (!plain || indent_of($0) > blk)) next + blk = -1 + if (!plain) next + s = $0 + while (yaml_dq_continued(s) && (getline t) > 0) s = yaml_dq_join(s, t) + if (anchor_defs(s, d) && (want in d)) got = d[want] + if (s ~ /:[[:space:]]*[|>][-+0-9]*[[:space:]]*(#.*)?$/) { t = s; sub(/[^[:space:]-].*$/, "", t); blk = length(t) } + } + END { if (index(got, "\002")) got = ""; print got }' "$1" +} +pin_fixer_lock "$REPO_PATH" + +LOCK="$REPO_PATH/.github/workflows/actions.lock" +rewrites=0; lock_touched=0 + +# Two callers pinned at two different dead SHAs move to the SAME replacement, +# so after substitution the lock can hold the same dependency key twice. A +# duplicate key is a lockfile GitHub refuses. Keep the first record, fold the +# others' nested `uses:` into it (deduplicated, sorted as gh actions-lock sorts), +# and drop them. Keys and entries compare as GitHub resolves them — the +# repository and a commit ID in either case — so `Fixture/Repo@` and +# `fixture/repo@` are one record, written back as first spelled. Rewrite lockfile $1 in place, preserving gh-actions-lock's +# 4-space layout and leaving the file alone when the output is identical. +# +# The nested list may be written flow-style on one line (`uses: []`, +# `uses: ['a@x', 'b@y']`); its entries merge like a block list's, and a +# record keeps ONE `uses:` key — the block form when lists were merged, the +# line as written when there was only the one. A record listed twice whose +# `uses:` is a shape this merger cannot read (a flow list over several lines, +# an alias, a scalar) is not folded at all: merging it could drop its entries +# or leave two `uses:` keys. The merger then changes nothing, names the record +# (UNSUPPORTED) and sets merge_refused. +# +# A record written as a flow mapping on its key line (`'a/b@': { ref: +# ..., commit: ... }`), or any other line at record indentation that is no +# record key this merger reads, is kept as written, in its place, a record of +# its own: never folded, never moved into the record before it. It refuses +# instead, changing nothing, when the dependency such a record names is +# listed again (a fold could not reach it, and the lock would keep the key +# twice), and when a duplicate being folded away holds a line it has no rule +# for (folded, that line would be lost). A top-level key after +# `dependencies:` (`workflows:` written last) ends the section, and what +# follows it is written after the records, as it stood. +merge_duplicate_records() { + local lock="$1" tmp + tmp=$(mktemp) + if ! awk -v sq="'" ' + # Print dependency cur using its saved header and sorted, unique uses. + function emit_record( i, n, out) { + if (!cur) return + # Rebuild the record: header lines, then a sorted, unique uses: list. + print spelled[cur] + for (i = 1; i <= nhead[cur]; i++) print head[cur, i] + n = 0 + for (u in uses_of) if (index(u, cur SUBSEP) == 1) n++ + # One flow list and nothing merged into it: kept as written. + if (nlists[cur] == 1 && (cur in flow_line) && nextra[cur] == 0) { print flow_line[cur]; return } + if (n > 0 || nextra[cur] > 0) { + print (cur in uses_head ? uses_head[cur] : " uses:" flow_cmt[cur]) + fflush() + cmd = "sort" + for (u in uses_of) if (index(u, cur SUBSEP) == 1) print " - " uses_of[u] | cmd + close(cmd) + for (i = 1; i <= nextra[cur]; i++) print uses_extra[cur, i] + } else if (cur in flow_line) print flow_line[cur] + } + # A one-line flow list: its items, each a quoted or plain scalar (sq is + # the single quote), go into uses_of as block entries would. 0, with + # nothing stored, when an item is any other shape — a nested + # collection, a key, an embedded quote. + function flow_items(s, body, n, parts, i, it, k) { + body = s; sub(/^ ["'"'"']?uses["'"'"']?[[:space:]]*:[[:space:]]*\[/, "", body); sub(/\][[:space:]]*(#.*)?$/, "", body) + if (body ~ /^[[:space:]]*$/) return 1 + n = split(body, parts, ",") + for (i = 1; i <= n; i++) { + it = parts[i]; sub(/^[[:space:]]+/, "", it); sub(/[[:space:]]+$/, "", it) + if (it == "" && i == n && n > 1) { parts[i] = ""; continue } + if (!(it ~ ("^" sq "[^\"" sq "]+" sq "$") || it ~ ("^\"[^\"" sq "]+\"$") || it ~ ("^[^][{}:,\"" sq "#[:space:]]+$"))) return 0 + parts[i] = it + } + for (i = 1; i <= n; i++) if (parts[i] != "") { k = cur SUBSEP dep_id(unq(parts[i])); if (!(k in uses_of)) uses_of[k] = parts[i] } + return 1 + } + # A record key is the dependency between its quotes — or a plain + # scalar, as a hand-added record may write it (`actions/checkout@:`, + # exactly the kind of record this fixer meets); the key is compared + # unquoted so `'"'"'a@sha'"'"':`, `"a@sha":` and `a@sha:` are one record, and + # the first spelling seen is the one written back. A record this + # merger did not recognise was not kept: every line of it fell to the + # catch-all below with no current record, and was dropped. + function unq(s) { gsub(/["'"'"']/, "", s); return s } + # The dependency a record key or entry names, as GitHub resolves it: + # the repository in either case (`Fixture/Repo` is `fixture/repo`), + # a commit ID in either case; a tag or branch is a name, kept as is. + function dep_id(s, at, j, ref) { + at = 0; while ((j = index(substr(s, at + 1), "@")) > 0) at += j + if (!at) return tolower(s) + ref = substr(s, at + 1) + if (length(ref) == 40 && ref !~ /[^0-9A-Fa-f]/) ref = tolower(ref) + return tolower(substr(s, 1, at)) ref + } + function record_key(s) { sub(/[[:space:]]+#.*$/, "", s); sub(/[[:space:]]+$/, "", s); return (s ~ /^ ["'"'"'][^"'"'"']+["'"'"']:$/ || s ~ /^ [^[:space:]"'"'"'#][^[:space:]]*:$/) } + # A nested `uses:` entry, quoted or plain, with or without a trailing + # comment; it is deduplicated on its value, the comment kept with + # the first spelling seen. + # Trailing whitespace (a carriage return too) is no part of a key or + # an entry: `owner/repo@sha: ` is the record `owner/repo@sha`. + function uses_entry(s) { return (s ~ /^ - ["'"'"'][^"'"'"']+["'"'"']([[:space:]]+#.*|[[:space:]]*)$/ || s ~ /^ - [^[:space:]"'"'"'#][^[:space:]]*([[:space:]]+#.*|[[:space:]]*)$/) } + function entry_value(s) { sub(/^ - /, "", s); sub(/[[:space:]]+#.*$/, "", s); sub(/[[:space:]]+$/, "", s); return unq(s) } + # The dependency an unread record line names, or "": its key quoted + # either way, or plain, before the colon (sq is the single quote). + function line_dep(s, t) { + t = s; sub(/^ /, "", t) + if (match(t, "^(\"[^\"]*\"|" sq "[^" sq "]*" sq ")[[:space:]]*:")) { t = substr(t, 1, RLENGTH); sub(/[[:space:]]*:$/, "", t); return dep_id(unq(t)) } + if (match(t, "^[^[:space:]\"" sq "#][^[:space:]]*:([[:space:]]|$)")) { t = substr(t, 1, RLENGTH); sub(/:[[:space:]]*$/, "", t); return dep_id(t) } + return "" + } + BEGIN { indep = 0; cur = ""; in_uses = 0; after = 0 } + after { tail[++ntail] = $0; next } + /^dependencies:[[:space:]]*$/ { indep = 1; print; next } + indep && /^[^[:space:]#]/ { after = 1; tail[++ntail] = $0; next } + !indep { print; next } + # A record key or one of its own fields ends a nested `uses:` list. + /^ [^[:space:]]/ || /^ [^[:space:]]/ { in_uses = 0 } + record_key($0) { + key = $0; sub(/[[:space:]]+#.*$/, "", key); sub(/[[:space:]]+$/, "", key); key = unq(key) + sub(/^ +/, "", key); sub(/:$/, "", key); key = dep_id(key) + if (!(key in seen)) { seen[key] = 1; order[++norder] = key; spelled[key] = $0; nhead[key] = 0; first = 1 } else { first = 0 } + nrec[key]++; cur = key; next + } + /^ [^[:space:]#]/ { + key = "\034" NR; order[++norder] = key; spelled[key] = $0; nhead[key] = 0; nrec[key] = 1 + d = line_dep($0); if (d != "") { opaque_dep[key] = d; nopaque[d]++ } + cur = key; first = 1; next + } + # What stands between `dependencies:` and its first record (a + # comment) stays there. + cur == "" { print; next } + # The nested list header, quoted or not, with or without a trailing + # comment (kept as first spelled); only lines inside a list it opens + # are its entries. + /^ ["'"'"']?uses["'"'"']?[[:space:]]*:[[:space:]]*(#.*)?$/ { in_uses = 1; nlists[cur]++; if (!(cur in uses_head)) uses_head[cur] = $0; next } + /^ ["'"'"']?uses["'"'"']?[[:space:]]*:[[:space:]]*\[.*\][[:space:]]*(#.*)?$/ && flow_items($0) { + nlists[cur]++ + if (!(cur in flow_line)) { + flow_line[cur] = $0; c = $0; sub(/^.*\]/, "", c) + flow_cmt[cur] = (c ~ /#/ ? c : "") + } + next + } + # Any other `uses:` value is one the merger cannot read. The key may + # be quoted (`"uses":`, `'"'"'uses'"'"':`), as `ref` and `commit` may. + /^ ["'"'"']?uses["'"'"']?[[:space:]]*:/ { odd[cur] = 1 } + in_uses && uses_entry($0) { u = $0; sub(/^ - /, "", u); k = cur SUBSEP dep_id(entry_value($0)); if (!(k in uses_of)) uses_of[k] = u; next } + # Any other line inside the list (a comment, a shape this merger has + # no rule for) stays under the `uses:` header it came from: moved + # above it, it would be a child with no parent key. + in_uses { uses_extra[cur, ++nextra[cur]] = $0; next } + /^ [a-z_]+: / { if (first) { head[cur, ++nhead[cur]] = $0 } ; next } + { + # A line the merger does not understand is not the merger'"'"'s to drop: + # it is kept where it stands, under the record it follows. Under a + # duplicate being folded away it has nowhere to stand: unless it is + # blank or a comment, the merger refuses. + if (first) { head[cur, ++nhead[cur]] = $0 } + else if ($0 !~ /^[[:space:]]*(#.*)?$/) { lost[cur] = lost[cur] (lost[cur] == "" ? "" : "; ") NR ": " $0 } + next + } + END { + for (k = 1; k <= norder; k++) if (odd[order[k]] && nrec[order[k]] > 1) { + name = spelled[order[k]]; sub(/[[:space:]]+#.*$/, "", name); name = unq(name); sub(/^ +/, "", name); sub(/:$/, "", name) + printf "UNSUPPORTED .github/workflows/actions.lock: the record %s is listed %d times and its uses: is a shape the merger cannot fold (a flow list over several lines, an alias, a scalar); the duplicates were left in place. Merge them by hand.\n", name, nrec[order[k]] > "/dev/stderr" + refused = 1 + } + for (k = 1; k <= norder; k++) { + cur = order[k] + if (cur in lost) { + name = spelled[cur]; sub(/[[:space:]]+#.*$/, "", name); name = unq(name); sub(/^ +/, "", name); sub(/:$/, "", name) + printf "UNSUPPORTED .github/workflows/actions.lock: the record %s is listed %d times and a duplicate holds a line the merger has no rule for (line %s); the duplicates were left in place. Merge them by hand.\n", name, nrec[cur], lost[cur] > "/dev/stderr" + refused = 1 + } + if ((cur in opaque_dep) && (nopaque[opaque_dep[cur]] > 1 || (opaque_dep[cur] in seen))) { + printf "UNSUPPORTED .github/workflows/actions.lock: %s is a record the merger cannot read (written as a flow mapping?) and its dependency is listed again; the duplicates were left in place. Merge them by hand.\n", spelled[cur] > "/dev/stderr" + refused = 1 + } + } + if (refused) exit 4 + for (k = 1; k <= norder; k++) { + cur = order[k] + emit_record() + } + for (k = 1; k <= ntail; k++) print tail[k] + } + ' "$lock" > "$tmp"; then + rm -f "$tmp"; merge_refused=1; return 0 + fi + if ! cmp -s "$lock" "$tmp"; then pin_fixer_replace "$tmp" "$lock"; else rm -f "$tmp"; fi +} + +# Find the first PINS_TABLE row for reusable path $1 at stale SHA $2, including +# wildcard paths. Set replacement/reason and return 0 on a match; otherwise +# clear both variables and return 1. +lookup_replacement() { # + local t_path t_stale t_new t_reason t_path_lc + replacement=""; reason="" + while IFS=$'\t' read -r t_path t_stale t_new t_reason || [ -n "$t_path" ]; do + [[ -z "$t_path" || "$t_path" == \#* ]] && continue + t_path_lc="${t_path,,}" + [[ "$t_stale" == "$2" ]] || continue + # GitHub resolves an owner or repository name in either case: the + # table and the caller are compared case-folded. + if [[ "${t_path,,}" == "${1,,}" ]] || { [[ "$t_path" == */\* ]] && [[ "${1,,}" == "${t_path_lc%\*}"* ]]; }; then + replacement="$t_new"; reason="$t_reason"; return 0 + fi + done < "$PINS_TABLE" + return 1 +} + +# Does hold, anywhere in its text, a reusable pin the table names as +# known-dead? +dead_ref_in_file() { # + local ref sha + while IFS= read -r ref; do + [ -n "$ref" ] || continue + sha="${ref##*@}" + lookup_replacement "${ref%@*}" "${sha,,}" && return 0 + done < <(decode_dq < "$1" | grep -oiE "$REUSABLE_REF_RE" || true) + return 1 +} + +# Lines on stdin as YAML reads their double-quoted scalars: escapes decoded +# (`"\x68yperpolymath/..."` is the reference it spells), every other byte as +# written (lib/yaml-anchors.awk, yaml_decode_dq). +decode_dq() { awk "$ANCHORS_AWK"'{ print yaml_decode_dq($0) }'; } + +# Line of as YAML reads it: double-quoted escapes decoded, and a +# line ending in an escaped line break inside a double-quoted scalar +# (`uses: "hyperpolymath/standards/.github/workflows/rust-ci-\`) joined with +# the lines that continue it (lib/yaml-anchors.awk). +dq_logical_line() { # + awk -v ln="$2" "$ANCHORS_AWK"' + NR == ln { j = $0 } + NR > ln { j = yaml_dq_join(j, $0) } + NR >= ln && !yaml_dq_continued(j) { print yaml_decode_dq(j); done = 1; exit } + END { if (!done && j != "") print yaml_decode_dq(j) }' "$1" +} + +# Print YAML context markers for workflow file $1, one per line that matters: +# K line n is a `uses:` key of a job under `jobs:` — the one place a +# reusable is called; +# D line n is a `uses:` key under some other key (`env:`) — data; +# S line n is inside a block scalar (`run: |`, a fixture in `env:`) — text; +# U line n continues the value of a job's `uses:` key that holds none on +# its own line (`uses: >-`, or `uses:` then the reference below): the +# call is there, but not where the rewrite reads it. +# A line with none of these that still carries a known-dead pin is one the +# parser cannot place (a flow-style job `rust: { uses: ... }`). Keys are +# placed by indentation, which says nothing inside a flow collection opened +# on an earlier line: there a `uses:` key line is K when its indentation +# reads a job key (`rust: {` then ` uses: ...`, as KYAML indents it), and +# otherwise is left unplaced — never D. `jobs: {`, `rust: {`, `uses: ...` may +# all stand in column 0, and that call must be reported, not passed as data. +yaml_context() { # + awk -v sq="'" ' + # Return the number of leading spaces in source line s. + function indent_of(s, t) { t = s; sub(/[^ ].*$/, "", t); return length(t) } + # Return true when s is neither blank nor a full-line comment. + function plain(s) { return (s !~ /^[[:space:]]*$/ && s !~ /^[[:space:]]*#/) } + # Can a scalar start after `seen` (the line so far) at flow depth d? + # Where a value starts (`key: `, a list dash, past any anchor or tag), + # or inside a flow collection after `[`, `{`, `,` or `:`. + function scalar_start(seen, d) { + if (seen ~ VALUE_START_RE) return 1 + return d > 0 && seen ~ /[][{,:][[:space:]]*([&!][^[:space:]]*[[:space:]]+)*$/ + } + # The flow depth line s leaves, from depth d at its start: `{` and `[` + # open a collection where a value starts or inside one already open, + # `}` and `]` close one; quoted scalars and a comment are skipped. + # (Hypatia reads depth so: PinSites.line_depths/1.) + function depth_after(s, d, i, n, c, j, seen) { + n = length(s); i = 1; seen = "" + while (i <= n) { + c = substr(s, i, 1) + if (c == "#" && (i == 1 || substr(s, i - 1, 1) ~ /[[:space:]]/)) break + if ((c == "\"" || c == sq) && scalar_start(seen, d)) { + j = i + 1 + if (c == "\"") { while (j <= n && substr(s, j, 1) != "\"") { if (substr(s, j, 1) == "\\") j++; j++ } } + else { while (j <= n && (substr(s, j, 1) != sq || substr(s, j + 1, 1) == sq)) j += (substr(s, j, 1) == sq ? 2 : 1) } + seen = seen "x"; i = j + 1; continue + } + if ((c == "{" || c == "[") && (d > 0 || seen ~ VALUE_START_RE)) d++ + else if ((c == "}" || c == "]") && d > 0) d-- + seen = seen c; i++ + } + return d + } + BEGIN { + VALUE_START_RE = "(^[[:space:]]*(-[[:space:]]+)*|:[[:space:]]+)([&!][^[:space:]]*[[:space:]]+)*$" + Q = "[\"'"'"']?" + KEY_RE = "^[[:space:]]*" Q "[A-Za-z0-9_.-]+" Q "[[:space:]]*:([[:space:]]|$)" + ITEM_KEY_RE = "^[[:space:]]*-[[:space:]]+" Q "[A-Za-z0-9_.-]+" Q "[[:space:]]*:([[:space:]]|$)" + SCALAR_RE = ":[[:space:]]*[|>][-+0-9]*[[:space:]]*(#.*)?$" + # A `uses:` key with no value on its line (a block scalar, or nothing). + OPEN_RE = "^[[:space:]]*" Q "uses" Q "[[:space:]]*:[[:space:]]*(&[^[:space:]]+[[:space:]]*)?([|>][-+0-9]*)?[[:space:]]*(#.*)?$" + } + { + ind = indent_of($0) + if (in_scalar) { if (plain($0) && ind <= scalar_indent) in_scalar = 0; else { print (in_uses ? "U" : "S") NR; next } } + in_uses = 0 + if (!plain($0)) next + d0 = depth; depth = depth_after($0, depth) + if ($0 ~ KEY_RE) { + key = $0; sub(/^[[:space:]]*/, "", key); sub(/[[:space:]]*:.*$/, "", key); gsub(/["'"'"']/, "", key) + for (k in keyat) if (k + 0 >= ind) delete keyat[k] + if (key == "uses") { + p = -1; for (k in keyat) if (k + 0 < ind && k + 0 > p) p = k + 0 + g = -1; for (k in keyat) if (k + 0 < p && k + 0 > g) g = k + 0 + if (p >= 0 && g >= 0 && keyat[g] == "jobs") { + print "K" NR + # The value continues below the key: its lines are the call. + if ($0 ~ OPEN_RE) { in_scalar = 1; scalar_indent = ind; in_uses = 1; keyat[ind] = key; next } + } else if (d0 == 0) print "D" NR + } + keyat[ind] = key + if ($0 ~ SCALAR_RE) { in_scalar = 1; scalar_indent = ind } + } else if ($0 ~ ITEM_KEY_RE && $0 ~ SCALAR_RE) { + # `- run: |`: the scalar hangs off the item'"'"'s key column. + in_scalar = 1; scalar_indent = ind + 2 + } + } + ' "$1" +} + +# Every `uses:` of an estate reusable at a full SHA, across the workflows. +# Only a `uses:` that is a key of a job under `jobs:` is a call: the same text +# under `env:`, inside a `run: |` script or a fixture, or after a `#` is data +# and must not move anything. A covered pin the parser cannot place at all (a +# flow-style job `rust: { uses: ... }`) makes the whole file UNSUPPORTED: left +# untouched, named on stderr, exit 2 — never "OK". The lock is edited after +# the loop, once every workflow rewrite is known. +USES_KEY_RE="^[[:space:]]*(-[[:space:]]+)?[\"']?uses[\"']?[[:space:]]*:[[:space:]]*[\"']?" +# A SHA ref ends at a quote, whitespace or the end of the value: a tag that +# merely STARTS with a dead SHA (`-patched`) is a different ref the table +# never named, and must not be rewritten into `-patched`. A `#` +# starts a comment only after whitespace, so `#patched` is one plain +# scalar — that other ref — and a comment is reached through the whitespace. +REF_END="([\"'[:space:]]|$)" +# The fallback that looks for a pin the parser could not place also stops at +# flow punctuation: `{ uses: x@}` is a flow-style caller, and missing it +# would let the run report OK. +FALLBACK_REF_END="([\"'[:space:],}]|]|$)" +# Git resolves a commit ID in either case, and GitHub an owner or repository +# name: an upper-case spelling is the same pin, matched case-insensitively, +# looked up in lower case and rewritten as written. +REUSABLE_REF_RE="(${TABLE_OWNERS_RE})/[A-Za-z0-9_.-]+/\.github/workflows/[A-Za-z0-9_.-]+\.ya?ml@[0-9a-fA-F]{40}" +# A line with its comment dropped and every quoted scalar that is neither a +# key, the value of a `uses` key (past any anchor or tag) nor the whole start +# of the line (a value continued from the key above it) emptied, quote-aware. The fallback scan +# for pins the parser cannot place reads this, so a dead pin named in a +# trailing comment or inside a `name:` string is data, not an UNSUPPORTED +# caller. +mask_line() { + printf '%s\n' "$1" | awk -v sq="'" '{ + s = $0; out = ""; n = length(s); i = 1 + tail = "(^|[-{,[:space:]])[\"" sq "]?uses[\"" sq "]?[[:space:]]*:[[:space:]]*([&!][^[:space:],}]*[[:space:]]+)*$" + while (i <= n) { + c = substr(s, i, 1) + if (c == "#" && (i == 1 || substr(s, i - 1, 1) ~ /[[:space:]]/)) break + if (c == "\"" || c == sq) { + j = i + 1 + if (c == "\"") { while (j <= n && substr(s, j, 1) != "\"") { if (substr(s, j, 1) == "\\") j++; j++ } } + else { while (j <= n && (substr(s, j, 1) != sq || substr(s, j + 1, 1) == sq)) j += (substr(s, j, 1) == sq ? 2 : 1) } + if (substr(s, j + 1) ~ /^[[:space:]]*:/ || tolower(out) ~ tail || out ~ /^[[:space:]]*$/) out = out substr(s, i, j - i + 1) + else out = out c c + i = j + 1; continue + } + out = out c; i++ + } + print out + }' +} +# The value may carry YAML node properties — an anchor or a tag, in either +# order (`uses: &call owner/repo/...@`, `uses: !!str owner/...@`). +# The rewrite does not edit such values (every alias of an anchor would +# change with it, and the rewrite reads the reference right after the +# colon), so such a value is reported, never skipped as clean. +ANCHOR_RE="([&!][^[:space:],}]*[[:space:]]+)+" +FALLBACK_USES_RE="(^|[-{,[:space:]])[\"']?uses[\"']?[[:space:]]*:[[:space:]]*(${ANCHOR_RE})?[\"']?${REUSABLE_REF_RE}${FALLBACK_REF_END}" +ANCHORED_USES_RE="(^|[-{,[:space:]])[\"']?uses[\"']?[[:space:]]*:[[:space:]]*${ANCHOR_RE}[\"']?${REUSABLE_REF_RE}${FALLBACK_REF_END}" +# The dead reference a job-level `uses:` line carries, as written, or "". +k_ref() { grep -oiE "${USES_KEY_RE}${REUSABLE_REF_RE}${REF_END}" <<<"$1" | sed -E "s/${USES_KEY_RE}//; s/[\"'[:space:]]\$//" | head -1 || true; } + +# Every replacement the run would write, before one is written. Overlapping +# table rows are applied first match first, so two reusables of one +# repository at one stale SHA can be sent to two different replacements. +# One lock edge and one dependency record per repository@SHA cannot carry +# both, and which caller gets which cannot be said in the lock: the run +# refuses, and nothing is changed. +declare -A move_to=() +conflict=0 +while IFS= read -r -d '' wf; do + for lineno in $(yaml_context "$wf" | grep -o '^K[0-9]*' | tr -d K | sort -n || true); do + ref=$(k_ref "$(sed -n "${lineno}p" "$wf")") + [ -n "$ref" ] || continue + stale="${ref##*@}"; stale="${stale,,}" + lookup_replacement "${ref%@*}" "$stale" || continue + key="${ref%%/.github/workflows/*}"; key="${key,,}@$stale" + if [ -n "${move_to[$key]:-}" ] && [ "${move_to[$key]}" != "$replacement" ]; then + echo "CONFLICT ${wf#"$REPO_PATH"/}:$lineno: $key would move to ${move_to[$key]:0:7} here and to ${replacement:0:7} elsewhere; one lock edge and one record cannot carry both. Fix the table or the callers by hand; nothing was changed." >&2 + conflict=1 + fi + move_to[$key]="$replacement" + done +done < <(find "$REPO_PATH/.github/workflows" -maxdepth 1 -type f \( -name '*.yml' -o -name '*.yaml' \) -print0) +[ "$conflict" -eq 0 ] || exit 2 + +edits=$(mktemp) # workflow-keystalereplacementkeep-stale-edgerepository +lock_stale=0 # a rewritten caller whose lock entry never had the edge: the lock was already out of sync +unsupported=0 +merge_refused=0 # duplicate lock records the merger could not fold (UNSUPPORTED) +while IFS= read -r -d '' wf; do + ctx=$(yaml_context "$wf") + klines=$(grep -o '^K[0-9]*' <<<"$ctx" | tr -d K | sort -n || true) + placed=$(grep -oE '^[KDS][0-9]*' <<<"$ctx" | tr -d KDS || true) + skip=0 + while IFS= read -r numbered; do + lineno="${numbered%%:*}" + # A placed `uses:` key is the rewrite's, unless its value is anchored: + # the rewrite reads the reference right after the colon, finds none + # there, and would otherwise report the file clean. + if grep -qx "$lineno" <<<"$placed"; then + mask_line "${numbered#*:}" | grep -qiE "$ANCHORED_USES_RE" || continue + fi + # Only a `uses:` key outside comments and other scalars can be a + # caller the parser failed to place (a flow-style job); a dead pin + # named in a comment or a `name:` string is text. + # Every such site on the line is checked, each read from the masked + # line the guard read: a dead ref inside a `name:` string beside a + # live `uses:` is text, and a live first site does not hide a dead + # second one. + while IFS= read -r ref; do + [ -n "$ref" ] || continue + sha="${ref##*@}" + if lookup_replacement "${ref%@*}" "${sha,,}"; then + echo "UNSUPPORTED ${wf#"$REPO_PATH"/}:$lineno: $ref is a known-dead pin but not a \`uses\` key the parser can place (a flow-style job mapping, or a value carrying an anchor or tag). Fix by hand; the file was not changed." >&2 + skip=1 + fi + done < <(mask_line "${numbered#*:}" | grep -oiE "$FALLBACK_USES_RE" | grep -oiE "$REUSABLE_REF_RE" || true) + done < <(grep -niE "${REUSABLE_REF_RE}${FALLBACK_REF_END}" "$wf" | grep -vE "^[0-9]+:[[:space:]]*#" || true) + # A job's `uses:` whose value continues below the key (`uses: >-`): the + # rewrite reads the reference on the key line, finds none, and would + # report the file clean. A known-dead pin on such a line is reported. + for lineno in $(grep -o '^U[0-9]*' <<<"$ctx" | tr -d U || true); do + while IFS= read -r ref; do + [ -n "$ref" ] || continue + sha="${ref##*@}" + if lookup_replacement "${ref%@*}" "${sha,,}"; then + echo "UNSUPPORTED ${wf#"$REPO_PATH"/}:$lineno: $ref is a known-dead pin in a \`uses:\` value continued below its key (a block scalar such as \`uses: >-\`). Fix by hand; the file was not changed." >&2 + skip=1 + fi + done < <(mask_line "$(sed -n "${lineno}p" "$wf" | decode_dq)" | grep -oiE "${REUSABLE_REF_RE}${FALLBACK_REF_END}" | grep -oiE "$REUSABLE_REF_RE" || true) + done + # A double-quoted value may spell the reference with escapes (`uses: + # "\x68yperpolymath/..."`, or a line break escaped and continued on the + # next line): GitHub runs the decoded call, but the rewrite + # and the scan above read the bytes as written, find no pin, and would + # report the file clean. Every line outside data and text that holds an + # escape is read decoded; a known-dead pin found so at a `uses` key is + # reported, never rewritten through its escapes. + textual=$(grep -oE '^[DSU][0-9]*' <<<"$ctx" | tr -d DSU || true) + while IFS= read -r numbered; do + lineno="${numbered%%:*}" + grep -qx "$lineno" <<<"$textual" && continue + decoded=$(dq_logical_line "$wf" "$lineno") + [ "$decoded" != "${numbered#*:}" ] || continue + while IFS= read -r ref; do + [ -n "$ref" ] || continue + sha="${ref##*@}" + if lookup_replacement "${ref%@*}" "${sha,,}"; then + echo "UNSUPPORTED ${wf#"$REPO_PATH"/}:$lineno: $ref is a known-dead pin written with escapes in a double-quoted \`uses\` value; the rewrite edits a reference as written only. Fix by hand; the file was not changed." >&2 + skip=1 + fi + done < <(mask_line "$decoded" | grep -oiE "$FALLBACK_USES_RE" | grep -oiE "$REUSABLE_REF_RE" || true) + done < <(grep -n "[\\]" "$wf" | grep -vE "^[0-9]+:[[:space:]]*#" || true) + # A job's `uses:` given as an alias (`uses: *call`) runs the anchor's + # value, defined elsewhere in the file (`CALL: &call owner/repo/...@`): + # the rewrite reads no reference on the key line. A known-dead pin as + # that value is reported; the anchor is not rewritten from here. An alias + # whose anchor this reader cannot find (the anchor reader is line-local: + # `&call` after another item of a flow sequence begun on an earlier line + # is not seen) may be that pin: in a file that holds a known-dead pin + # anywhere, it is reported too, never passed as OK. + # A flow-style job (`rust: { uses: *call }`, `jobs: { rust: { uses: + # *call } }`) is no key the parser places, but runs its alias all the + # same: every `uses: *name` outside comments and quoted text, on a line + # that is neither data nor text, is read the same way. + alias_lines=$klines + while IFS= read -r numbered; do + lineno="${numbered%%:*}" + grep -qx "$lineno" <<<"$placed" || alias_lines="$alias_lines $lineno" + done < <(grep -n '[*]' "$wf" | grep -vE "^[0-9]+:[[:space:]]*#" || true) + for lineno in $alias_lines; do + while IFS= read -r alias; do + [ -n "$alias" ] || continue + value=$(anchor_value "$wf" "$alias" "$lineno") + if [ -z "$value" ] && dead_ref_in_file "$wf"; then + echo "UNSUPPORTED ${wf#"$REPO_PATH"/}:$lineno: \`uses: *$alias\` names an anchor this parser cannot read, and the file holds a known-dead pin it may hold. Fix by hand; the file was not changed." >&2 + skip=1; continue + fi + while IFS= read -r ref; do + [ -n "$ref" ] || continue + sha="${ref##*@}" + if lookup_replacement "${ref%@*}" "${sha,,}"; then + echo "UNSUPPORTED ${wf#"$REPO_PATH"/}:$lineno: \`uses: *$alias\` runs $ref, a known-dead pin held by the anchor \`&$alias\`. Fix by hand; the file was not changed." >&2 + skip=1 + fi + done < <(printf '%s\n' "$value" | grep -oiE "^${REUSABLE_REF_RE}\$" || true) + done < <(mask_line "$(sed -n "${lineno}p" "$wf")" | grep -oE "(^|[-{,[:space:]])[\"']?uses[\"']?[[:space:]]*:[[:space:]]*\*[^],[{}[:space:]]+" | sed 's/^[^*]*\*//' || true) + done + if [ "$skip" -eq 1 ]; then unsupported=$((unsupported + 1)); continue; fi + pending=() + for lineno in $klines; do + line=$(sed -n "${lineno}p" "$wf") + # A job-level call pinned by tag (`@v1`) carries no SHA: nothing for a + # SHA table to move, so it is a no-op here — not a grep exit 1 that + # `set -e` would turn into a silent exit 1 of the whole run. + ref=$(k_ref "$line") + [ -n "$ref" ] || continue + path="${ref%@*}"; stale_raw="${ref##*@}"; stale="${stale_raw,,}" + lookup_replacement "$path" "$stale" || continue + [[ "$replacement" =~ ^[0-9a-f]{40}$ ]] || { echo "fix-stale-reusable-pin: bad replacement SHA in table for $stale" >&2; exit 2; } + # ERE: a `$` inside a BRE group is literal to sed, so the boundary + # alternation needs -E (and a delimiter the pattern does not use). + # The path as the caller spells it, matched literally (a `.` in it + # is a dot). + path_re=$(printf '%s' "$path" | sed -E 's/[][\.^$*+?(){}|~\\]/\\&/g') + sed -i -E "${lineno}s~^([[:space:]]*(-[[:space:]]+)?[\"']?uses[\"']?[[:space:]]*:[[:space:]]*[\"']?$path_re)@$stale_raw([\"'[:space:]]|\$)~\1@$replacement\3~" "$wf" + echo "FIXED ${wf#"$REPO_PATH"/}:$lineno $path ${stale:0:7} -> ${replacement:0:7} ($reason)" + rewrites=$((rewrites + 1)) + pending+=("$stale"$'\t'"$replacement"$'\t'"${path%%/.github/workflows/*}") + done + # A lock edge is per repository@SHA, shared by every reusable of that + # repository the workflow calls. When a job of this workflow still calls + # another reusable of the same repository at the stale SHA (a pin the + # table does not cover), the stale edge must stay next to the new one. + for entry in ${pending[@]+"${pending[@]}"}; do + IFS=$'\t' read -r stale replacement repo_prefix <<<"$entry" + keep=0 + # The repository matched literally and the SHA to its end, as the + # caller matcher does: `-patched` is another ref, not this one. + repo_re=$(printf '%s' "$repo_prefix" | sed -E 's/[][\.^$*+?(){}|~\\]/\\&/g') + for other in $klines; do + sed -n "${other}p" "$wf" | grep -qiE "${USES_KEY_RE}${repo_re}/\.github/workflows/[A-Za-z0-9_.-]+\.ya?ml@${stale}${REF_END}" && keep=1 + done + printf '%s\t%s\t%s\t%s\t%s\n' ".github/workflows/${wf##*/}" "$stale" "$replacement" "$keep" "$repo_prefix" >> "$edits" + done +done < <(find "$REPO_PATH/.github/workflows" -maxdepth 1 -type f \( -name '*.yml' -o -name '*.yaml' \) -print0) + +# Lock: move each rewritten workflow's own `workflows:` edges first. An edge +# is per repository@SHA: an unrelated repository that happens to sit at the +# same SHA keeps its edge and its record. A rewritten caller whose lock entry +# has no such edge at all was already out of sync with its lock; that is +# said, and the run ends 3 (the lock needs `gh actions-lock`), never 0. +if [ -f "$LOCK" ] && [ -s "$edits" ]; then + sort -u "$edits" -o "$edits" + # A lock with CRLF line endings is edited as LF — every pass below reads + # `$`-anchored lines, and a carriage return would hide each record and + # edge from them — and written back with CRLF once the edits are done. + lock_crlf=0 + if grep -q $'\r$' "$LOCK"; then + lock_crlf=1; tmp=$(mktemp) + sed 's/\r$//' "$LOCK" > "$tmp" && pin_fixer_replace "$tmp" "$LOCK" + fi + while IFS=$'\t' read -r wfkey stale new keep repo; do + # The workflow key in either quote style, or none (`gh actions-lock` + # writes single quotes; a hand edit may not): compared unquoted. + # Repository and SHA are compared case-folded (GitHub and git read + # either case); every line keeps the spelling it has. + if ! awk -v wfkey="$wfkey" -v edge="$repo@$stale" ' + function unq(s) { gsub(/["'"'"']/, "", s); return s } + function keyv(s) { sub(/[[:space:]]+#.*$/, "", s); sub(/[[:space:]]+$/, "", s); return unq(s) } + function bare(s) { sub(/^ +- /, "", s); sub(/[[:space:]]+(#.*)?$/, "", s); return s } + BEGIN { edge = tolower(edge) } + /^workflows:[[:space:]]*$/ { sec = "w" } + /^dependencies:[[:space:]]*$/ { sec = "d" } + sec == "w" && /^ [^[:space:]#]/ { inwf = (keyv($0) == " " wfkey ":") } + # The edge is the list item'"'"'s own value: the same text quoted in + # a trailing comment of another edge is not it. + sec == "w" && inwf && /^ - / { if (tolower(unq(bare($0))) == edge) found = 1 } + END { exit !found }' "$LOCK"; then + echo "WARN: actions.lock has no edge '$repo@${stale:0:7}' under '$wfkey': the lock was already out of sync with this caller — run \`gh actions-lock\` or scripts/check-lock-sync.sh before pushing." >&2 + lock_stale=1 + continue + fi + tmp=$(mktemp) + # The lock is read twice: first for the edges this workflow entry + # already lists, then to move the stale one — an entry that already + # holds the replacement edge (a job already at the new revision) is + # never given it a second time. + awk -v wfkey="$wfkey" -v stale="$stale" -v new="$new" -v keep="$keep" -v repo="$repo" ' + function unq(s) { gsub(/["'"'"']/, "", s); return s } + function keyv(s) { sub(/[[:space:]]+#.*$/, "", s); sub(/[[:space:]]+$/, "", s); return unq(s) } + # An edge compared by its value: the list dash, a trailing + # comment, quotes and case are not part of it. + function edge_val(s) { sub(/^ +- /, "", s); sub(/[[:space:]]+#.*$/, "", s); sub(/[[:space:]]+$/, "", s); return tolower(unq(s)) } + FNR == NR { + if ($0 ~ /^workflows:[[:space:]]*$/) s1 = "w" + if ($0 ~ /^dependencies:[[:space:]]*$/) s1 = "d" + if (s1 == "w" && $0 ~ /^ [^[:space:]#]/) w1 = (keyv($0) == " " wfkey ":") + if (s1 == "w" && w1 && $0 ~ /^ - /) have[edge_val($0)] = 1 + next + } + /^workflows:[[:space:]]*$/ { sec = "w" } + /^dependencies:[[:space:]]*$/ { sec = "d" } + sec == "w" && /^ [^[:space:]#]/ { inwf = (keyv($0) == " " wfkey ":") } + sec == "w" && inwf && /^ - / { + # Either quote style, or none (a hand-written edge); the edge + # keeps the spelling it has. + l = tolower($0); e = tolower(repo "@" stale); i = 0 + # Only an item whose own value is the stale edge moves; the + # same text in its comment is not the edge. Its first quoted + # occurrence is then the value, before any comment. + if (edge_val($0) == e) { + q = "'"'"'"; i = index(l, q e q) + if (!i) { q = "\""; i = index(l, q e q) } + # Everything after the closing quote (a comment) stays. + if (i) moved = substr($0, 1, i + length(repo)) "@" new q substr($0, i + length(e) + 2) + else { + t = l; sub(/^ - /, "", t); sub(/[[:space:]]+(#.*)?$/, "", t) + if (t == e) { i = 1; rest = $0; sub(/^ - [^[:space:]]+/, "", rest); spell = substr($0, 11, length(repo)); moved = " - " spell "@" new rest } + } + } + if (i) { + # keep: an uncovered caller in this workflow still pins the stale + # SHA, so the edge is added beside the old one, not replaced. + if (keep == "1") print + if (edge_val(moved) in have) next + have[edge_val(moved)] = 1; $0 = moved + } + } + { print } + ' "$LOCK" "$LOCK" > "$tmp" + if ! cmp -s "$LOCK" "$tmp"; then pin_fixer_replace "$tmp" "$LOCK"; lock_touched=1; else rm -f "$tmp"; fi + done < "$edits" + # Then the records: rename a stale record nothing references any more; + # copy it under the replacement key while an edge still points at the + # stale SHA — a workflow's own edge, or another record's nested `uses:` + # (a reusable that itself calls the stale revision keeps needing it). + while IFS=$'\t' read -r stale new repo; do + grep -qiF "$repo@$stale" "$LOCK" || continue + still=$(awk -v edge="$repo@$stale" ' + function unq(s) { gsub(/["'"'"']/, "", s); return s } + function bare(s) { sub(/^ +- /, "", s); sub(/[[:space:]]+(#.*)?$/, "", s); return s } + BEGIN { edge = tolower(edge) } + /^ +- / { if (tolower(unq(bare($0))) == edge) n++ } END { print n + 0 }' "$LOCK") + tmp=$(mktemp) + # Only THIS repository'"'"'s record moves: its key line, and its ref: and + # commit: lines. Another repository at the same SHA is a different + # record and keeps its SHA. + awk -v stale="$stale" -v new="$new" -v repo="$repo" -v still="$still" ' + /^dependencies:[[:space:]]*$/ { sec = "d" } + # A top-level key (`workflows:` written after `dependencies:`) + # ends the section and the record in it: a kept record'"'"'s copy is + # written there, inside the section, never after what follows. + sec == "d" && /^[^[:space:]#]/ && !/^dependencies:[[:space:]]*$/ { + for (c = 1; c <= nc; c++) print copy[c] + nc = 0; sec = ""; inrec = 0 + } + # The record is the one whose unquoted key IS repo@stale: a key that + # merely contains it (`evilhyperpolymath/standards@`) is another + # record, and so is a plain unquoted key (a hand-written record). + # The SHA is replaced in either case wherever it stands in the + # record'"'"'s own key, ref and commit (own_sub); the rest of the line + # keeps its spelling. + function isub(s, old, new, l, o, i, out) { + out = ""; l = tolower(s); o = tolower(old) + while ((i = index(l, o)) > 0) { out = out substr(s, 1, i - 1) new; s = substr(s, i + length(old)); l = substr(l, i + length(old)) } + return out s + } + # The length of the quoted scalar t starts with (`'"'"''"'"'` is a quote + # inside single quotes, `\` escapes inside double quotes); all of t + # when it does not close. + function qlen(t, q, i, n) { + q = substr(t, 1, 1); n = length(t); i = 2 + while (i <= n) { + if (q == "\"" && substr(t, i, 1) == "\\") { i += 2; continue } + if (substr(t, i, 1) == q) { if (q == "'"'"'" && substr(t, i + 1, 1) == "'"'"'") { i += 2; continue }; return i } + i++ + } + return n + } + # The length of the flow value t starts with: up to the `,`, `}` or + # `]` that ends it, or a comment, quoted text and nested brackets + # passed over. + function vlen(t, i, n, c, d) { + n = length(t); i = 1; d = 0 + while (i <= n) { + c = substr(t, i, 1) + if (c == "\"" || c == "'"'"'") { i += qlen(substr(t, i)); continue } + if (d == 0 && (c == "," || c == "}" || c == "]")) return i - 1 + if (c == "#" && i > 1 && substr(t, i - 1, 1) ~ /[[:space:]]/) return i - 1 + if (c == "{" || c == "[") d++ + else if (c == "}" || c == "]") d-- + i++ + } + return n + } + # The SHA replaced in the parts of record line s that are the + # record'"'"'s own: its key, read up to its own colon, and the values + # of its `ref` and `commit` fields — on their own lines, or as + # entries of a flow mapping written on the key line (`'"'"'repo@sha'"'"': + # { ref: ..., commit: ... }`). A nested `uses:` list in that mapping + # names other revisions, which this fixer does not recompute + # (`uses: ['"'"'other/action@'"'"']` at the same SHA is another + # repository'"'"'s edge), and a comment is text: both are kept as + # written. + function own_sub(s, k, q, j, out, rest, i, n, c, d) { + if (s !~ /^ [^[:space:]#]/) { + # A field line: its key and value, not a comment after them. + k = s; s = "" + while (k != "") { + c = substr(k, 1, 1) + if (c == "\"" || c == "'"'"'") { j = qlen(k); s = s substr(k, 1, j); k = substr(k, j + 1); continue } + if (c == "#" && s ~ /[[:space:]]$/) break + s = s c; k = substr(k, 2) + } + return isub(s, stale, new) k + } + k = substr(s, 5) + if (k ~ /^["'"'"']/) { q = substr(k, 1, 1); j = index(substr(k, 2), q) + 1 } + else { match(k, /:([[:space:]]|$)/); j = RSTART - 1 } + out = isub(substr(s, 1, 4 + j), stale, new); rest = substr(s, 5 + j) + n = length(rest); i = 1; d = 0 + while (i <= n) { + c = substr(rest, i, 1) + if (c == "\"" || c == "'"'"'") { j = qlen(substr(rest, i)); out = out substr(rest, i, j); i += j; continue } + if (c == "#" && (i == 1 || substr(rest, i - 1, 1) ~ /[[:space:]]/)) { out = out substr(rest, i); break } + if (c == "{" || c == "[") d++ + else if (c == "}" || c == "]") d-- + out = out c; i++ + if (d == 1 && (c == "{" || c == ",") && match(substr(rest, i), /^[[:space:]]*["'"'"']?(ref|commit)["'"'"']?[[:space:]]*:[[:space:]]*/)) { + out = out substr(rest, i, RLENGTH); i += RLENGTH + j = vlen(substr(rest, i)); out = out isub(substr(rest, i, j), stale, new); i += j + } + } + return out + } + sec == "d" && /^ [^[:space:]#]/ { + k = $0; sub(/^ /, "", k); sub(/[[:space:]]+#.*$/, "", k) + if (k ~ /^["'"'"']/) { q = substr(k, 1, 1); k = substr(k, 2); k = substr(k, 1, index(k, q) - 1) } + else sub(/:([[:space:]].*)?$/, "", k) + inrec = (tolower(k) == tolower(repo "@" stale)) + } + # The field keys may be quoted (`"ref": ...`, `'"'"'commit'"'"': ...`), as + # a hand-written record may write them: the same fields. + sec == "d" && inrec && (/^ [^[:space:]#]/ || /^ ["'"'"']?(ref|commit)["'"'"']?[[:space:]]*:/) { + if (still == 0) $0 = own_sub($0) + else copy[++nc] = own_sub($0) + } + sec == "d" && inrec && still != 0 && !(/^ [^[:space:]#]/ || /^ ["'"'"']?(ref|commit)["'"'"']?[[:space:]]*:/) { copy[++nc] = $0 } + { print } + END { for (i = 1; i <= nc; i++) print copy[i] }' "$LOCK" > "$tmp" + [ "$still" -eq 0 ] || echo "NOTE: actions.lock keeps the record for ${stale:0:7}: a workflow the table does not cover still pins it; the replacement got its own record." >&2 + if ! cmp -s "$LOCK" "$tmp"; then pin_fixer_replace "$tmp" "$LOCK"; lock_touched=1; else rm -f "$tmp"; fi + done < <(cut -f2,3,5 "$edits" | sort -u) + # A kept stale edge means the new edge was ADDED, so the stale record is + # kept too (the branch above copies it); nothing else to do here. + [ "$lock_touched" -eq 1 ] && merge_duplicate_records "$LOCK" + if [ "$lock_crlf" -eq 1 ]; then + tmp=$(mktemp) + sed 's/$/\r/' "$LOCK" > "$tmp" && pin_fixer_replace "$tmp" "$LOCK" + fi +fi +rm -f "$edits" + +if [ "$rewrites" -eq 0 ] && [ "$unsupported" -eq 0 ]; then echo "OK (no known-dead reusable pins): $REPO_PATH"; elif [ "$rewrites" -gt 0 ]; then echo "Rewrote $rewrites pin(s) in $REPO_PATH"; fi +if [ "$lock_touched" -eq 1 ]; then + echo "WARN: actions.lock named a stale SHA and was rewritten to the replacement; its nested uses: list was NOT recomputed — run \`gh actions-lock\` or scripts/check-lock-sync.sh before pushing. Exit 3: not a clean success." >&2 +fi +if [ "$unsupported" -ne 0 ]; then + echo "fix-stale-reusable-pin: $unsupported file(s) left untouched (UNSUPPORTED above); not a clean success." >&2 + exit 2 +fi +if [ "$merge_refused" -ne 0 ]; then + echo "fix-stale-reusable-pin: actions.lock still lists a dependency twice (UNSUPPORTED above); not a clean success." >&2 + exit 2 +fi +if [ "$lock_stale" -eq 1 ]; then + echo "fix-stale-reusable-pin: a rewritten caller had no matching lock edge (WARN above); the lock is out of sync. Exit 3: not a clean success." >&2 + exit 3 +fi +[ "$lock_touched" -eq 1 ] && exit 3 +exit 0 diff --git a/scripts/fix-tmp-paths.sh b/scripts/fix-tmp-paths.sh index 4cf849af..b8234c49 100755 --- a/scripts/fix-tmp-paths.sh +++ b/scripts/fix-tmp-paths.sh @@ -29,7 +29,7 @@ while IFS= read -r -d '' file; do fi # Only match static /tmp/name patterns (not /tmp/$VAR or /tmp/${VAR}) - if grep -qP '/tmp/[a-zA-Z][\w.-]+' "$file" 2>/dev/null; then + if grep -qP '/tmp/[a-zA-Z][\w.-]+' "$file" 2>/dev/null; then # hypatia: allow content_patterns/hardcoded_tmp -- literal path this fixer matches in target files; nothing is created at that path rel_path="${file#$REPO_PATH/}" # Skip files that already use mktemp properly @@ -38,7 +38,7 @@ while IFS= read -r -d '' file; do continue fi - count=$(grep -cP '/tmp/[a-zA-Z][\w.-]+' "$file" 2>/dev/null || echo 0) + count=$(grep -cP '/tmp/[a-zA-Z][\w.-]+' "$file" 2>/dev/null || echo 0) # hypatia: allow content_patterns/hardcoded_tmp -- literal path this fixer matches in target files; nothing is created at that path # Check if file already has our TMPDIR block (idempotent) if grep -q 'HYPATIA_TMPDIR' "$file" 2>/dev/null; then @@ -48,13 +48,14 @@ while IFS= read -r -d '' file; do # Strategy: Add a mktemp block after set -e (or shebang) and replace /tmp/X with $HYPATIA_TMPDIR/X # Step 1: Extract all static /tmp/filename patterns - tmp_names=$(grep -oP '/tmp/\K[a-zA-Z][\w.-]+' "$file" 2>/dev/null | sort -u) + tmp_names=$(grep -oP '/tmp/\K[a-zA-Z][\w.-]+' "$file" 2>/dev/null | sort -u) # hypatia: allow content_patterns/hardcoded_tmp -- literal path this fixer matches in target files; nothing is created at that path if [[ -z "$tmp_names" ]]; then continue fi # Step 2: Insert mktemp block after the first 'set -' line or after shebang + # hypatia: allow content_patterns/hardcoded_tmp -- literal path this fixer matches in target files; nothing is created at that path MKTEMP_BLOCK='# Auto-generated temp dir (replaces hardcoded /tmp/) HYPATIA_TMPDIR="$(mktemp -d)" trap '\''rm -rf "$HYPATIA_TMPDIR"'\'' EXIT' @@ -73,7 +74,7 @@ ${MKTEMP_BLOCK}" "$file" 2>/dev/null || true # Step 3: Replace /tmp/name with $HYPATIA_TMPDIR/name for each static name while IFS= read -r name; do - sed -i "s|/tmp/${name}|\"\$HYPATIA_TMPDIR/${name}\"|g" "$file" 2>/dev/null || true + sed -i "s|/tmp/${name}|\"\$HYPATIA_TMPDIR/${name}\"|g" "$file" 2>/dev/null || true # hypatia: allow content_patterns/hardcoded_tmp -- literal path this fixer matches in target files; nothing is created at that path done <<< "$tmp_names" echo " FIXED $rel_path — replaced $count hardcoded /tmp/ path(s) with mktemp" @@ -85,5 +86,5 @@ echo "" if [[ "$FIXED_COUNT" -gt 0 ]]; then echo "Fixed $FIXED_COUNT file(s) — hardcoded /tmp/ replaced with mktemp" else - echo "No fixable /tmp/ patterns found" + echo "No fixable /tmp/ patterns found" # hypatia: allow content_patterns/hardcoded_tmp -- literal path this fixer matches in target files; nothing is created at that path fi diff --git a/scripts/fix-workflow-timeout-minutes.sh b/scripts/fix-workflow-timeout-minutes.sh index c41197ce..6075f63f 100755 --- a/scripts/fix-workflow-timeout-minutes.sh +++ b/scripts/fix-workflow-timeout-minutes.sh @@ -21,9 +21,12 @@ set -euo pipefail DEFAULT_TIMEOUT="${DEFAULT_TIMEOUT:-10}" TARGET="${1:?Usage: fix-workflow-timeout-minutes.sh }" -apply_to_file() { +apply_to_file() ( local f="$1" [ -f "$f" ] || { echo "SKIP (not a file): $f" >&2; return 0; } + local missing_timeouts tmp="" + missing_timeouts=$(mktemp) + trap 'rm -f "$missing_timeouts"; if [ -n "$tmp" ]; then rm -f "$tmp"; fi' EXIT # Get list of (line_number, job_name) for jobs that lack timeout-minutes # within their block. We scan the file and find each top-level `^ :$` @@ -38,10 +41,9 @@ apply_to_file() { } in_jobs && /^ timeout-minutes:/ { hadto = 1 } END { if (curjob && !hadto) print curjob_line "\t" curjob } - ' "$f" > /tmp/missing_timeouts.$$ + ' "$f" > "$missing_timeouts" - if [ ! -s /tmp/missing_timeouts.$$ ]; then - rm -f /tmp/missing_timeouts.$$ + if [ ! -s "$missing_timeouts" ]; then echo "OK (no missing timeouts): $f" return 0 fi @@ -51,7 +53,7 @@ apply_to_file() { tmp=$(mktemp) cp "$f" "$tmp" - tac /tmp/missing_timeouts.$$ | while IFS=$'\t' read -r job_start_line job_name; do + tac "$missing_timeouts" | while IFS=$'\t' read -r job_start_line job_name; do # Find the runs-on line within the next 20 lines from the job header runs_on_line=$(awk -v start="$job_start_line" -v end="$((job_start_line+20))" ' NR > start && NR < end && /^ runs-on:/ { print NR; exit } @@ -70,8 +72,7 @@ apply_to_file() { else rm -f "$tmp" fi - rm -f /tmp/missing_timeouts.$$ -} +) if [ -d "$TARGET" ]; then for f in "$TARGET"/.github/workflows/*.yml "$TARGET"/.github/workflows/*.yaml; do diff --git a/scripts/lib/action-ref-tags.tsv b/scripts/lib/action-ref-tags.tsv new file mode 100644 index 00000000..3f984040 --- /dev/null +++ b/scripts/lib/action-ref-tags.tsv @@ -0,0 +1,37 @@ +# SPDX-License-Identifier: MPL-2.0 +# action-ref-tags.tsv — the tag that resolves to a commit, for actions the estate pins by SHA. +# Columns (tab-separated): owner/repocommit_shatag +# Consumed by scripts/fix-lock-self-sha-refs.sh as its offline map; a commit not listed here is +# resolved with `git ls-remote --tags` unless --offline is given. Source of truth for every row: +# hyperpolymath/standards actions.lock at 75bd5994 (the lock GitHub accepts), plus `git ls-remote` +# for the two cicd-squabbler release pins. Add a row when you add a pin; never invent a tag. +actions/cache 55cc8345863c7cc4c66a329aec7e433d2d1c52a9 v6.1.0 +actions/checkout 3d3c42e5aac5ba805825da76410c181273ba90b1 v7.0.1 +actions/configure-pages 45bfe0192ca1faeb007ade9deae92b16b8254a0d v6.0.0 +actions/create-github-app-token bcd2ba49218906704ab6c1aa796996da409d3eb1 v3.2.0 +actions/deploy-pages 368f82528645a54fb793d4d04e342629a3f51346 v5.0.1 +actions/download-artifact 9000827ccba6bdab643e8b6fd33ac0654aef8333 v8.0.2 +actions/github-script 3a2844b7e9c422d3c10d287c895573f7108da1b3 v9.0.0 +actions/upload-artifact bbbca2ddaa5d8feaa63e36b76fdaad77386f024f v7.0.0 +actions/upload-artifact cf430e030ddbb5b0abf93d22962f4752f3646cd9 v7.0.2 +actions/upload-artifact 043fb46d1a93c77aae656e7c1c64a875d1fc6a0a v7.0.1 +actions/upload-pages-artifact fc324d3547104276b827a68afc52ff2a11cc49c9 v5.0.0 +actions/attest-build-provenance 4d101475d8b20a2381f78447822ac1eab6504dd8 v4.2.2 +denoland/setup-deno 22d081ff2d3a40755e97629de92e3bcbfa7cf2ed v2.0.5 +dtolnay/rust-toolchain e2a55d2ffb04f378e9626c28d38b36d230d1e12f v1 +dtolnay/rust-toolchain 683b588c487a69d01f6c6ddf40f036ff682cc2c8 v1 +editorconfig-checker/action-editorconfig-checker 51f63319f592f97930c73d9c46184d20bd206393 v3.0.0 +erlef/setup-beam 54075bcc5e249e4758d363f27d099f55d843f124 v1.24.1 +github/codeql-action b96794f015dfd88f77b49b1c93e0fa7110f94c63 v4.38.0 +goto-bus-stop/setup-zig abea47f85e598557f500fa1fd2ab7464fcb39406 v2.2.1 +haskell-actions/setup 0f8e8c99d88aeb3fbfd523f1ef2c6f762d10d64d v2.12.1 +haskell-actions/setup 6037f33647c3f17758a2356c80fc4a53d7e0685d v2.12.0 +ocaml/setup-ocaml 93303b622b2522e4411e295f9e77411a24912ac7 v3.9.0 +ossf/scorecard-action 2d1146689b8cda280b9bc96326124645441f03bc v2.4.4 +oven-sh/setup-bun 0c5077e51419868618aeaa5fe8019c62421857d6 v2.2.0 +peter-evans/repository-dispatch 28959ce8df70de7be546dd1250a005dd32156697 v4.0.1 +softprops/action-gh-release efb35369e0ad2afab669f228072c1b0d510eae64 v3.0.3 +step-security/harden-runner ccd8616d44fd3846e67624a50d5aad6d37bf2d25 v2.22.1 +swatinem/rust-cache 6323deb102c322ba6fcbdcafc7e3dddab59af2b6 v2.9.2 +tailscale/github-action d1b6cd204f8dceda5b3eaad7f1f767be390056cd v4.2.0 +webfactory/ssh-agent e83874834305fe9a4a2997156cb26c5de65a8555 v0.10.0 diff --git a/scripts/lib/pin-fixer-lock.sh b/scripts/lib/pin-fixer-lock.sh new file mode 100644 index 00000000..60f6aba0 --- /dev/null +++ b/scripts/lib/pin-fixer-lock.sh @@ -0,0 +1,110 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: MPL-2.0 +# pin-fixer-lock.sh — one workflow-pin fixer per repository at a time. +# +# The three fixers registered for WF026 (fix-rust-toolchain-input.sh), WF027 / +# PIN-003 (fix-stale-reusable-pin.sh) and PI006 (fix-lock-self-sha-refs.sh) +# all rewrite .github/workflows/*.yml and actions.lock of the same checkout. +# Under `dispatch-runner.sh --parallel N` two manifest entries for one +# repository can start together, so each fixer takes this lock on the +# repository before it reads anything: their edits never interleave (one's +# `sed -i` landing between another's read and write). +# +# Sourced, then `pin_fixer_lock `. The lock — a sentinel directory, +# `pin-fixer-.lock.d`, that every fixer holds, and with flock installed a +# `pin-fixer-.lock` file it takes first — lives outside the checkout, +# keyed by the checkout's real path, so every fixer run on the same checkout +# — by any path that reaches it, with or without flock — contends for the +# same sentinel. It is held until the fixer exits. A holder that does not go away within +# PIN_FIXER_LOCK_WAIT seconds (default 600) is reported and the fixer exits 2 +# having changed nothing: a dispatcher must not record that run as a clean +# repair. +# +# The directory is PIN_FIXER_LOCK_DIR when set, else a per-user +# `pin-fixer-` directory (created mode 0700) under XDG_RUNTIME_DIR, else +# TMPDIR, else /tmp. Opening `>file` follows a symlink, so the file must sit +# where no other user can plant one: the directory is refused — exit 2, +# nothing changed — unless it is a real directory (not a symlink) owned by +# this user and writable by neither group nor others. A predictable name in a +# shared, world-writable /tmp is exactly what this rules out. +pin_fixer_lock() { + local repo="$1" dir key wait + if [ -n "${PIN_FIXER_LOCK_DIR:-}" ]; then + dir="$PIN_FIXER_LOCK_DIR" + else + dir="${XDG_RUNTIME_DIR:-${TMPDIR:-/tmp}}/pin-fixer-$(id -u)" + mkdir -m 700 "$dir" 2>/dev/null || true + fi + if [ -L "$dir" ] || [ ! -d "$dir" ] || [ ! -O "$dir" ] \ + || [ -n "$(find "$dir" -maxdepth 0 \( -perm -0020 -o -perm -0002 \) -print 2>/dev/null)" ]; then + echo "fix-workflow-pins: lock directory $dir is not a private directory of this user (a symlink, missing, owned by another user, or group/world-writable); nothing was changed" >&2 + exit 2 + fi + wait="${PIN_FIXER_LOCK_WAIT:-600}" + key=$(cd "$repo" 2>/dev/null && pwd -P || printf '%s' "$repo") + key=$(printf '%s' "$key" | sha256sum | cut -c1-16) + # Every fixer holds the directory sentinel; one that has flock takes the + # flock first, as well. A run with flock and one without (a host and a + # minimal container sharing the checkout) therefore contend for the same + # object, the directory, and still never hold the checkout together. + if command -v flock >/dev/null 2>&1; then + exec 9>"$dir/pin-fixer-$key.lock" + flock -w "$wait" 9 || { + echo "fix-workflow-pins: another workflow-pin fixer holds $repo (waited ${wait}s); nothing was changed" >&2 + exit 2 + } + fi + pin_fixer_lock_mkdir "$repo" "$dir/pin-fixer-$key.lock.d" "$wait" +} + +# The lock every fixer holds is a directory: `mkdir` either creates it or +# fails because another fixer has, atomically, so two runs never hold it +# together, whether or not either has flock. The holder's EXIT trap removes +# it (an interrupt or termination exits through that trap too). A run killed +# outright leaves it behind; the next fixer then waits, reports the directory +# and its recorded holder, and exits 2 with nothing changed — it is never +# taken over silently (a recorded pid may belong to another pid namespace, so +# a holder cannot be judged gone), and never skipped. +pin_fixer_lock_mkdir() { # + local repo="$1" ldir="$2" wait="$3" waited=0 + until mkdir "$ldir" 2>/dev/null; do + if [ "$waited" -ge "$wait" ]; then + echo "fix-workflow-pins: another workflow-pin fixer holds $repo (waited ${wait}s; the lock is the directory $ldir, held by pid $(cat "$ldir/pid" 2>/dev/null || echo unknown) — remove it by hand only if that run is gone); nothing was changed" >&2 + exit 2 + fi + sleep 1 + waited=$((waited + 1)) + done + PIN_FIXER_LOCK_HELD="$ldir" + echo "$$" > "$ldir/pid" + trap 'rm -f "$PIN_FIXER_LOCK_HELD/pid"; rmdir "$PIN_FIXER_LOCK_HELD" 2>/dev/null' EXIT + trap 'exit 129' HUP + trap 'exit 130' INT + trap 'exit 143' TERM +} + +# Replace with the rewritten the way `sed -i` does: by a rename +# (never a half-written file), keeping 's mode, and its owner and group +# where this user may set them. A `mktemp` file is created 0600; renamed as it +# is over a 0644 workflow or lock, it would leave the repaired file unreadable +# to everyone else who shares the checkout. +pin_fixer_replace() { # + chmod --reference="$2" "$1" 2>/dev/null || chmod "$(stat -c %a "$2")" "$1" + chown --reference="$2" "$1" 2>/dev/null || true + mv -f "$1" "$2" +} + +# Would YAML read , written as a plain (unquoted) scalar, as anything +# but a string? As YAML 1.1 and the core schema read them: a number (`1`, +# `1.80` the float 1.8, `1e3`, `-1`, `.5`, `0x1f`, `0o17`, `0b1`, `1:30` base +# 60, `.inf`, `.nan`), a timestamp (`2026-10-20`, the date — a tag may be +# spelled so — with or without a time), or a null or boolean spelling +# (`null`, `~`, nothing, `true`, `no`, `on`, `y`, ... in any case). A fixer +# writing such a value into a workflow or lock quotes it. +pin_fixer_yaml_nonstring() { # + [[ -z "$1" || "$1" == "~" ]] \ + || [[ "$1" =~ ^[-+]?([0-9][0-9_]*(\.[0-9_]*)?([eE][-+]?[0-9]+)?|\.[0-9][0-9_]*([eE][-+]?[0-9]+)?|[0-9][0-9_]*(:[0-5]?[0-9])+(\.[0-9_]*)?|0[xX][0-9a-fA-F_]+|0[oO][0-7_]+|0[bB][01_]+|\.(inf|Inf|INF))$ ]] \ + || [[ "$1" =~ ^\.(nan|NaN|NAN)$ ]] \ + || [[ "$1" =~ ^[0-9]{4}-[0-9]{1,2}-[0-9]{1,2}(([Tt]|[[:space:]]+)[0-9]{1,2}:[0-9]{2}:[0-9]{2}(\.[0-9]*)?([[:space:]]*(Z|[-+][0-9]{1,2}(:[0-9]{2})?))?)?$ ]] \ + || [[ "${1,,}" =~ ^(null|true|false|yes|no|on|off|y|n)$ ]] +} diff --git a/scripts/lib/stale-reusable-pins.tsv b/scripts/lib/stale-reusable-pins.tsv new file mode 100644 index 00000000..f8be4f6c --- /dev/null +++ b/scripts/lib/stale-reusable-pins.tsv @@ -0,0 +1,640 @@ +# SPDX-License-Identifier: MPL-2.0 +# stale-reusable-pins.tsv — pins of estate reusables that are KNOWN DEAD, and the pin to move to. +# Columns (tab-separated): reusable_pathstale_shareplacement_shareason +# reusable_path is `hyperpolymath//.github/workflows/` or `hyperpolymath//.github/workflows/*` (every reusable of that repo). +# Consumed by scripts/fix-stale-reusable-pin.sh; mirrored by hypatia (pin denylist PIN-003) so detection and repair carry the same facts. +# +# Window A — standards main 2026-06-01..2026-06-24 (d73c93e0..49ed4715): rust-ci-reusable.yml pins dtolnay/rust-toolchain by SHA and passes +# no toolchain: input, so every caller fails in its first step with "'toolchain' is a required input" (standards#439 fixed it; plasma-parser-writer#105). +# Window B — standards commits reachable from main, 2026-08-19..2026-10-19 (5b5dab9f..fae7f010, 545 commits): actions.lock carries action records whose ref is the bare commit SHA; +# since ~2026-10-15 GitHub validates the CALLEE lock at the called SHA and rejects these ("invalid lockfile pin"), so every reusable of standards +# at these commits dies at Set up job for every caller (hypatia#929, cicd-squabbler run 38846539059). 75bd5994 is the first clean commit. +# Generated 2026-10-19 from a first-parent walk of hyperpolymath/standards; regenerate when standards fixes or re-breaks a window. +hyperpolymath/standards/.github/workflows/rust-ci-reusable.yml bf32531befa4bb7f692a5f7ecac1eab71af20506 75bd5994470661a099df562d6d5ad2fe744dbd66 Window A: toolchain input missing +hyperpolymath/standards/.github/workflows/rust-ci-reusable.yml 583a9346ebf28e0fd8114f22aef140eaa1be4ac0 75bd5994470661a099df562d6d5ad2fe744dbd66 Window A: toolchain input missing +hyperpolymath/standards/.github/workflows/rust-ci-reusable.yml d7c22711e830e1f383846472f6e9b99debdb201e 75bd5994470661a099df562d6d5ad2fe744dbd66 Window A: toolchain input missing +hyperpolymath/standards/.github/workflows/rust-ci-reusable.yml 747b2f0f690505b24c90991a030a01e564ff59c6 75bd5994470661a099df562d6d5ad2fe744dbd66 Window A: toolchain input missing +hyperpolymath/standards/.github/workflows/rust-ci-reusable.yml 509142da4ec04d74dccfc9e52813aeabc2c6fa02 75bd5994470661a099df562d6d5ad2fe744dbd66 Window A: toolchain input missing +hyperpolymath/standards/.github/workflows/rust-ci-reusable.yml e2ef79eecdd623ff631fcbf3fe0268ba4f4ce166 75bd5994470661a099df562d6d5ad2fe744dbd66 Window A: toolchain input missing +hyperpolymath/standards/.github/workflows/rust-ci-reusable.yml 0597948e81c0be7570f805d7ec49f64fa0eeefd6 75bd5994470661a099df562d6d5ad2fe744dbd66 Window A: toolchain input missing +hyperpolymath/standards/.github/workflows/rust-ci-reusable.yml da1ee5ebaec4de70e7530a9fcb4b2c693434bcc1 75bd5994470661a099df562d6d5ad2fe744dbd66 Window A: toolchain input missing +hyperpolymath/standards/.github/workflows/rust-ci-reusable.yml 0f49246bc5055f7159c92d649c4d3fb97b75cf66 75bd5994470661a099df562d6d5ad2fe744dbd66 Window A: toolchain input missing +hyperpolymath/standards/.github/workflows/rust-ci-reusable.yml f15ced3f44146de9931bcb70157e1825d07b83ed 75bd5994470661a099df562d6d5ad2fe744dbd66 Window A: toolchain input missing +hyperpolymath/standards/.github/workflows/rust-ci-reusable.yml 5e29ef7923aff017419bdce9354e9df4c3823cb5 75bd5994470661a099df562d6d5ad2fe744dbd66 Window A: toolchain input missing +hyperpolymath/standards/.github/workflows/rust-ci-reusable.yml ad8c45387878f2b00217330b0e1260edc6f878b5 75bd5994470661a099df562d6d5ad2fe744dbd66 Window A: toolchain input missing +hyperpolymath/standards/.github/workflows/rust-ci-reusable.yml 3ce22c244bd4b31ed99397a585cd458735a79819 75bd5994470661a099df562d6d5ad2fe744dbd66 Window A: toolchain input missing +hyperpolymath/standards/.github/workflows/rust-ci-reusable.yml 8c785d41adbbee534b158747a5c6796402e450f0 75bd5994470661a099df562d6d5ad2fe744dbd66 Window A: toolchain input missing +hyperpolymath/standards/.github/workflows/rust-ci-reusable.yml c1070339c80c4f1af71bb35cacc58c19f611f3a2 75bd5994470661a099df562d6d5ad2fe744dbd66 Window A: toolchain input missing +hyperpolymath/standards/.github/workflows/rust-ci-reusable.yml 49ed471515bdf587aeb9931af2d1314cd6aa5de2 75bd5994470661a099df562d6d5ad2fe744dbd66 Window A: toolchain input missing +hyperpolymath/standards/.github/workflows/rust-ci-reusable.yml 7282b1ac828d89eaecd3403b59591975746ec3ff 75bd5994470661a099df562d6d5ad2fe744dbd66 Window A: toolchain input missing +hyperpolymath/standards/.github/workflows/rust-ci-reusable.yml d135b05bfc647d0c0fbfedc7e80f37ea50f49236 75bd5994470661a099df562d6d5ad2fe744dbd66 Window A: toolchain input missing +hyperpolymath/standards/.github/workflows/rust-ci-reusable.yml 4762ba66441de05bccb36e074ba3d2202219126a 75bd5994470661a099df562d6d5ad2fe744dbd66 Window A: toolchain input missing +hyperpolymath/standards/.github/workflows/rust-ci-reusable.yml 59429dc666d347d030114cd6ef6f4ff7b7f6a6b4 75bd5994470661a099df562d6d5ad2fe744dbd66 Window A: toolchain input missing +hyperpolymath/standards/.github/workflows/rust-ci-reusable.yml e8fe153b568af5e3534a261eee0258fa84fa6c5e 75bd5994470661a099df562d6d5ad2fe744dbd66 Window A: toolchain input missing +hyperpolymath/standards/.github/workflows/rust-ci-reusable.yml 280cb7e3f2db8b0e5f26c42877ea5fa80243b06f 75bd5994470661a099df562d6d5ad2fe744dbd66 Window A: toolchain input missing +hyperpolymath/standards/.github/workflows/rust-ci-reusable.yml c6c095b38f7e729d3d355629b80e771dfe1a351b 75bd5994470661a099df562d6d5ad2fe744dbd66 Window A: toolchain input missing +hyperpolymath/standards/.github/workflows/rust-ci-reusable.yml 09e7023d24682621bea4e11965a1ef5e87d86c3b 75bd5994470661a099df562d6d5ad2fe744dbd66 Window A: toolchain input missing +hyperpolymath/standards/.github/workflows/rust-ci-reusable.yml 42455b19727fa0ccf73a9b914f0e9781d4c34577 75bd5994470661a099df562d6d5ad2fe744dbd66 Window A: toolchain input missing +hyperpolymath/standards/.github/workflows/rust-ci-reusable.yml 4ddc926b4b86451c2711a16bf927ed16ad2db45e 75bd5994470661a099df562d6d5ad2fe744dbd66 Window A: toolchain input missing +hyperpolymath/standards/.github/workflows/rust-ci-reusable.yml 0809b8e3630d38229b6c3629956e0dff4b937c33 75bd5994470661a099df562d6d5ad2fe744dbd66 Window A: toolchain input missing +hyperpolymath/standards/.github/workflows/rust-ci-reusable.yml d72fe5a14e841ac6d78514b53624b6173038ee20 75bd5994470661a099df562d6d5ad2fe744dbd66 Window A: toolchain input missing +hyperpolymath/standards/.github/workflows/rust-ci-reusable.yml 5a93d9d57cc04de4002d6d0ecd336fc7a8698910 75bd5994470661a099df562d6d5ad2fe744dbd66 Window A: toolchain input missing +hyperpolymath/standards/.github/workflows/rust-ci-reusable.yml 3e57141d38672438a7a9110bd10ea0f76e6099a7 75bd5994470661a099df562d6d5ad2fe744dbd66 Window A: toolchain input missing +hyperpolymath/standards/.github/workflows/rust-ci-reusable.yml 78b29005efe954822c86c553b40523b9fdae78d4 75bd5994470661a099df562d6d5ad2fe744dbd66 Window A: toolchain input missing +hyperpolymath/standards/.github/workflows/rust-ci-reusable.yml f290d4a696b8af78f9141daeced486b7b8a85d11 75bd5994470661a099df562d6d5ad2fe744dbd66 Window A: toolchain input missing +hyperpolymath/standards/.github/workflows/rust-ci-reusable.yml a8c69ecfcd87d5418dd4559019ac860fff20e834 75bd5994470661a099df562d6d5ad2fe744dbd66 Window A: toolchain input missing +hyperpolymath/standards/.github/workflows/rust-ci-reusable.yml a04c1d9f32e3c6cb5e91b7ba220080475449a4d2 75bd5994470661a099df562d6d5ad2fe744dbd66 Window A: toolchain input missing +hyperpolymath/standards/.github/workflows/rust-ci-reusable.yml c43aafaa2d707f3f195cad9742d3ccc8d068e3db 75bd5994470661a099df562d6d5ad2fe744dbd66 Window A: toolchain input missing +hyperpolymath/standards/.github/workflows/rust-ci-reusable.yml a955527810b82dbf7a9bc541502c5235f119499d 75bd5994470661a099df562d6d5ad2fe744dbd66 Window A: toolchain input missing +hyperpolymath/standards/.github/workflows/rust-ci-reusable.yml 2ff85f8f84212ae2cbedb1ea232c6f5a470acef3 75bd5994470661a099df562d6d5ad2fe744dbd66 Window A: toolchain input missing +hyperpolymath/standards/.github/workflows/rust-ci-reusable.yml 8e6ba7d4a0890d25425855a77404d4690d0ab4b5 75bd5994470661a099df562d6d5ad2fe744dbd66 Window A: toolchain input missing +hyperpolymath/standards/.github/workflows/rust-ci-reusable.yml 426368b72949d8235c8cf47f8bdc5e0e5cd5b54f 75bd5994470661a099df562d6d5ad2fe744dbd66 Window A: toolchain input missing +hyperpolymath/standards/.github/workflows/rust-ci-reusable.yml 025bba3f35f5040bf76d4954f02c744d61f68e8f 75bd5994470661a099df562d6d5ad2fe744dbd66 Window A: toolchain input missing +hyperpolymath/standards/.github/workflows/rust-ci-reusable.yml 8466cee8adbb814a2507a9a33e36ba415c5d7eba 75bd5994470661a099df562d6d5ad2fe744dbd66 Window A: toolchain input missing +hyperpolymath/standards/.github/workflows/rust-ci-reusable.yml f5f0506a6ec88e574753eee701a268e0d4b3a7f2 75bd5994470661a099df562d6d5ad2fe744dbd66 Window A: toolchain input missing +hyperpolymath/standards/.github/workflows/rust-ci-reusable.yml ae5b6731e0c1ce192edc14737a226d6b201341aa 75bd5994470661a099df562d6d5ad2fe744dbd66 Window A: toolchain input missing +hyperpolymath/standards/.github/workflows/rust-ci-reusable.yml 742424ec9cffeb414174276d9321325859e3aa64 75bd5994470661a099df562d6d5ad2fe744dbd66 Window A: toolchain input missing +hyperpolymath/standards/.github/workflows/rust-ci-reusable.yml 19995ace5f1179e9e2d4783fe1d36df0f343492d 75bd5994470661a099df562d6d5ad2fe744dbd66 Window A: toolchain input missing +hyperpolymath/standards/.github/workflows/rust-ci-reusable.yml 524523c1a6057942aadaef5999912fbf13a355c5 75bd5994470661a099df562d6d5ad2fe744dbd66 Window A: toolchain input missing +hyperpolymath/standards/.github/workflows/rust-ci-reusable.yml e4c7d968fa9aad6fcecab23bd60d1025679d1ad9 75bd5994470661a099df562d6d5ad2fe744dbd66 Window A: toolchain input missing +hyperpolymath/standards/.github/workflows/rust-ci-reusable.yml e96096c6614469e29e02edcc923fdacc0e611b34 75bd5994470661a099df562d6d5ad2fe744dbd66 Window A: toolchain input missing +hyperpolymath/standards/.github/workflows/rust-ci-reusable.yml ca29b1950ac493e4a29db3b61b6991fbdc8a57b2 75bd5994470661a099df562d6d5ad2fe744dbd66 Window A: toolchain input missing +hyperpolymath/standards/.github/workflows/rust-ci-reusable.yml e59affd85741ac786867a667bc1526ade31b1dd4 75bd5994470661a099df562d6d5ad2fe744dbd66 Window A: toolchain input missing +hyperpolymath/standards/.github/workflows/rust-ci-reusable.yml 7694d58cd4c82d6d706ef9653bcf545e28126794 75bd5994470661a099df562d6d5ad2fe744dbd66 Window A: toolchain input missing +hyperpolymath/standards/.github/workflows/rust-ci-reusable.yml 33a82ee5e3635bd137b2bc8ef2b1d71367195546 75bd5994470661a099df562d6d5ad2fe744dbd66 Window A: toolchain input missing +hyperpolymath/standards/.github/workflows/rust-ci-reusable.yml 52db79bee40380820e01c454efe6e85ae6115b2c 75bd5994470661a099df562d6d5ad2fe744dbd66 Window A: toolchain input missing +hyperpolymath/standards/.github/workflows/rust-ci-reusable.yml 6022e498a06552d71e51662283b9453855185b94 75bd5994470661a099df562d6d5ad2fe744dbd66 Window A: toolchain input missing +hyperpolymath/standards/.github/workflows/rust-ci-reusable.yml c97b2bdd12aeaff4624efcaec874ea7665dadae6 75bd5994470661a099df562d6d5ad2fe744dbd66 Window A: toolchain input missing +hyperpolymath/standards/.github/workflows/rust-ci-reusable.yml 9726a3814c895bf531f22c2b82cc15a3b1efa1f3 75bd5994470661a099df562d6d5ad2fe744dbd66 Window A: toolchain input missing +hyperpolymath/standards/.github/workflows/rust-ci-reusable.yml bec61613c821fdb1035f18b88e6e5d19d23328f7 75bd5994470661a099df562d6d5ad2fe744dbd66 Window A: toolchain input missing +hyperpolymath/standards/.github/workflows/rust-ci-reusable.yml 4236493691a06a6393b5ec4480f38e5d804f5b69 75bd5994470661a099df562d6d5ad2fe744dbd66 Window A: toolchain input missing +hyperpolymath/standards/.github/workflows/rust-ci-reusable.yml 6cd3772824e59c8c9affeab66061e25383544242 75bd5994470661a099df562d6d5ad2fe744dbd66 Window A: toolchain input missing +hyperpolymath/standards/.github/workflows/rust-ci-reusable.yml e763d82b93f46e7201d95e6d817ac61d739a8cc1 75bd5994470661a099df562d6d5ad2fe744dbd66 Window A: toolchain input missing +hyperpolymath/standards/.github/workflows/rust-ci-reusable.yml aa665acc04fff66632ede8368dbc9c2b5a630d3c 75bd5994470661a099df562d6d5ad2fe744dbd66 Window A: toolchain input missing +hyperpolymath/standards/.github/workflows/rust-ci-reusable.yml 3a5c23cb5888eca79df090fde6d52a8dc4820285 75bd5994470661a099df562d6d5ad2fe744dbd66 Window A: toolchain input missing +hyperpolymath/standards/.github/workflows/rust-ci-reusable.yml 0b96f61a3f043bd5409b913e526b29f3fd38a217 75bd5994470661a099df562d6d5ad2fe744dbd66 Window A: toolchain input missing +hyperpolymath/standards/.github/workflows/rust-ci-reusable.yml ae6619ecddd0eff0f957c73280efc36bb9302d75 75bd5994470661a099df562d6d5ad2fe744dbd66 Window A: toolchain input missing +hyperpolymath/standards/.github/workflows/rust-ci-reusable.yml 3b3549e293f87be99d7dbb939d380c8128842c8e 75bd5994470661a099df562d6d5ad2fe744dbd66 Window A: toolchain input missing +hyperpolymath/standards/.github/workflows/rust-ci-reusable.yml 2b88f34c97a9478b7a96b07425bb37ee5822bb8e 75bd5994470661a099df562d6d5ad2fe744dbd66 Window A: toolchain input missing +hyperpolymath/standards/.github/workflows/rust-ci-reusable.yml 9bdaff9479d53328ed907bdd5ba885398800fb00 75bd5994470661a099df562d6d5ad2fe744dbd66 Window A: toolchain input missing +hyperpolymath/standards/.github/workflows/rust-ci-reusable.yml 142dd7a0b9cd6101de8c4279c55b7051c537f76b 75bd5994470661a099df562d6d5ad2fe744dbd66 Window A: toolchain input missing +hyperpolymath/standards/.github/workflows/rust-ci-reusable.yml ac6b3252e7170d9459779c03bf31ae04c3629a61 75bd5994470661a099df562d6d5ad2fe744dbd66 Window A: toolchain input missing +hyperpolymath/standards/.github/workflows/rust-ci-reusable.yml f2b9d5eb5fb68a60b30468b8289a0f8cefbdd558 75bd5994470661a099df562d6d5ad2fe744dbd66 Window A: toolchain input missing +hyperpolymath/standards/.github/workflows/rust-ci-reusable.yml e401e47106ce0b0e32e3d0e2b7d910410e5b165e 75bd5994470661a099df562d6d5ad2fe744dbd66 Window A: toolchain input missing +hyperpolymath/standards/.github/workflows/rust-ci-reusable.yml e3ad4ca266e4dc93b7cebce5e3b3b48c6d523e45 75bd5994470661a099df562d6d5ad2fe744dbd66 Window A: toolchain input missing +hyperpolymath/standards/.github/workflows/rust-ci-reusable.yml 74748546395bf3dcedd5b660f58feb36be9d53d6 75bd5994470661a099df562d6d5ad2fe744dbd66 Window A: toolchain input missing +hyperpolymath/standards/.github/workflows/rust-ci-reusable.yml fc7abf589b93f9a33b8af74dbfe98ba22707156f 75bd5994470661a099df562d6d5ad2fe744dbd66 Window A: toolchain input missing +hyperpolymath/standards/.github/workflows/rust-ci-reusable.yml 7814dc67a489afc39f0289af5d3b6c3e23645156 75bd5994470661a099df562d6d5ad2fe744dbd66 Window A: toolchain input missing +hyperpolymath/standards/.github/workflows/rust-ci-reusable.yml c98915a225c98f2b90980cf045a00eb603e6af0f 75bd5994470661a099df562d6d5ad2fe744dbd66 Window A: toolchain input missing +hyperpolymath/standards/.github/workflows/rust-ci-reusable.yml 18dd92c19d5b9e50740826a4be8301afbab91bae 75bd5994470661a099df562d6d5ad2fe744dbd66 Window A: toolchain input missing +hyperpolymath/standards/.github/workflows/rust-ci-reusable.yml 82a8ac8e2fd446b710139a759bf22f175dc5e299 75bd5994470661a099df562d6d5ad2fe744dbd66 Window A: toolchain input missing +hyperpolymath/standards/.github/workflows/rust-ci-reusable.yml 55da6b331922f0ff7389c783d363ae12d8c61068 75bd5994470661a099df562d6d5ad2fe744dbd66 Window A: toolchain input missing +hyperpolymath/standards/.github/workflows/rust-ci-reusable.yml 1376eb6251fa9beeaea75241739b1d00dd6b4028 75bd5994470661a099df562d6d5ad2fe744dbd66 Window A: toolchain input missing +hyperpolymath/standards/.github/workflows/rust-ci-reusable.yml 28946811fb7fe2d05e9ac11c9ebee7295e966d40 75bd5994470661a099df562d6d5ad2fe744dbd66 Window A: toolchain input missing +hyperpolymath/standards/.github/workflows/rust-ci-reusable.yml d7c3b8f75d70ab640e6f37a4c8f9e6e8f2b6df56 75bd5994470661a099df562d6d5ad2fe744dbd66 Window A: toolchain input missing +hyperpolymath/standards/.github/workflows/rust-ci-reusable.yml d73c93e0e7d38f673b68e616d51a62f5860081fd 75bd5994470661a099df562d6d5ad2fe744dbd66 Window A: toolchain input missing +hyperpolymath/standards/.github/workflows/* fae7f01010f44606a65f84fe53e613201a0950ac 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* e16a23170fa378bf3b1444c3ee94100413ae3ff7 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* b2cb7b685e17bd24e0122d19950a45de94ddcbc6 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 8ff9ce65f7e54f8d5c5aa94584e2ead0f2ba460f 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* ce768a6abea6fb0bca9b4c3eeb135c3a75a7f06e 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 3de430074148c49c8db8878653b3c66707c344e4 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* f7573a00ba48a2dc19c6f912d3096f95b38f7be5 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 813dcbaba53f11c06e09a0092c8466b4cbb778ee 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* f335165a8882abadc99a092f55ff950b392af2d8 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* a88f511b9bc06b430a4b4071efe75d91921cd9f7 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 0d9a814ac8d7b1d5c1f39d8510639e8fdd99f3e4 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* f78a36c9058cbab0b6610a675d3d5d7b024fd837 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 68242e650c0bd83bbf280f3b578fa949c0ab4c16 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 94572e612a7bf11f8e6343d19adadf17ef2a3de3 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 80014df42eb81f09396c948299febb258cec4719 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 95de3dfc3fcf09a1cf3325ab42723e3d81682b45 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 6a9251037760690d56022ad6fab2cfe9d887dd76 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 35cf3e2b05ab1efaecf72f8e0875ee7b6da46b88 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* ed04132e56c086795d3cc478cbdc145333ab5780 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 25824f323e76d481a3d37f22f28440ef77dc7905 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 934b879f6068295e42dac20d80e010d555ebec82 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 25d23cf3934e6c1fffd275ac78398c70aa9d58f3 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 8a39fa0bdb4d7787f7529111be417f984c85381e 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 192389bf1b40b70c594464937db7fdbf39a9a964 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 2ae085c076a4b14990bf67da0dd3821ce1c12944 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* c11c255e1b5546854e874d1486d61d6d5a4da29a 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 75064de100849f1864f8fe1c912e64c48d70a3ee 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* ae36881b5ff4b469fb904b66542ac226a15107f3 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 3daaad25e39d671dde8ad58e6427282309b02cce 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* b26d3fb548fa2c4a7920cc69128821811d4e3222 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 50cfbd55d44130399bf1215c04fd154a49277ee8 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 507b89f1b35453bf3de62622373cc6573245b664 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* bf21155871bfd8d4789f33543d0c30880b8403cd 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* e3d5107b6319dc1121e3edf12f497bed2f9fcd61 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 7d2041a2315572d8257341449257e6cfddc9aebc 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 48e0ed9138ade95a5357f76079fc56d8f6f91bb5 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 736f792321e24af85b0dfc44b7666538bf4a2ff1 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 312ff116d9acbfff3523da5e9f71ac3ca9c327d3 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 0b1898e2e6a0fc88348c0f2517970593d373ca10 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 4c101fbbc0b20e816c6beca9fc0c909994163443 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 9164d76efef88f5173fdb717b231618a2d3c701a 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 3108c1b01186741eea80c00da3f4beda912286f9 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 7712ae1568af5c872408bfa1ba27578dd2afecd0 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 8496854d8241c3a0e20abaecf67e9c70638aa3b1 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* dd58dd9659f425a3f2372b123dde524c27d045a4 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 9d629d112daa396636bbb32757213552a8fb6878 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 76773445d8b0c3a2558f33cc426bacc638206a6f 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 6d19ce0ebae047027dcfd594c7810bf35c642faa 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* d8848f8509ccd94e931f6768487c88c8423f6033 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 2e12b312b529c51f095ce3b22cd35968f5aba898 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* ca12cd467cb13d02072837f9a2a69ede429b0b09 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 900c42c70b968e11a2bca04bf0ce050be5c286dc 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* a695e37936bacff66add7ec354eb629f659477e5 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* bbcc722bb0a5969ca0b1ebb1178af2ebcd5e9c34 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 691ea2af89d4313c906672d880690f81219dbdc5 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* d8a90cf8093ab787e98b940cd7d7675b2c981ec2 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* e35431d5bce94895b675d5a135e3ff5af1ca4a63 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* a709d3a642bee6e8d22829a69d2d64e1e8656d57 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* c9dfc2dcb333c1672aa779fcc86671be037ee735 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 8639b851db282e89083351aa782d906eb537def2 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 2f9d0be57ecbb09fb5d93df411fa6c9758ab305c 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 0683ddf00237ae92c5756cf858b3da1cc706894e 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 85aa1934975a8df576c7a4912bacbb8160ab2d2d 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 69ea45cfefab795faa2122df468ce7d120ebdff2 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* f77fc28ad400f281d26431b5e6a204dfc7a2ea22 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 63d2bbf64020b5f87bc546973d21881c19e79c65 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 8453510aa0da5027dabe54743e06ed7e7648fbe2 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* d7b85cac57eb16edf51508d6f30806e86d63c9d3 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* d5a7cfa4a9e9b77e1ec45b828ea32e3bdba4634f 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 9f68971b9fa254a80559432d9fcb7652b72e941d 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* b9e78cd426a83d87408d1f5db9e798b9fb8a3bbe 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 8b5dfd0646c1232174e680ae7772250610bfa2b8 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 7d51f30f992b51fe2b33119761901c594e266e82 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 89d778d9dc5343ad3dc178656c60e9caf235588b 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 043496c2d1efb9ba48f69ec533b5444ab61bbb5d 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* ebfcd98ba6c162df2eeca854254e725095fc85d2 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 239759976b272a426a3b7d34d0d48c0009bddcfb 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 79df53059d638e699b317951f717ee9710d2618d 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 640d86efeddb1e47da230515f5af9f4c936bba80 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* ad637e5134bc9f02ddf65431827d515b26bd58ee 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* bae58d9398f6deff6187a18c59620c984b6e1c47 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 223a9da337e327246d17ad49800cf567744a3d5e 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 617f3bcb83d33f5a0e3793c41d26a3c7937dfd16 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 0ae8b773b5a0f16ca1c5389442c907ceacf75e00 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* eafcfbb5933dde8b42fdbd61483794bb75fb4c97 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 5a0891e9e58b785181a92e141505ca96ca987b42 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 8464935a8028dbf0b06eb3d2b9bc7c7a7a35b4c6 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* b935e63937b1f1b5cbaeea7e375947d7b924038f 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 44a0de481c1b10843b358e6c5c278efbfd4eaad9 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* cfc3a92e55f6e6feeed33412d9fad94812d330ed 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 78a0a61f481bbe394214002f51f41a92325c0fff 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* ce92a8cdb60b60ea4b8cd5040a14c5ec2eec9f0e 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 5751b97ed597e560573242f56f1287db159edd00 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* a5ff08d7a12399fe21fa9ed72841a9c5749d086b 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 8556163f6bb9f06e2ed9beb67a91a335aa1f8913 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* ef7e6c4cc801d1ebf5504b940ff70f72061334d0 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 0cb69026a45f756d6610d8b67e298c4c3d6acc95 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 67e148fe710381c732ee1a6a8b2898688ad13110 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 80b3a1abccfe39b16d8af8ead874e09e9529737b 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 5b56aec07917394f48d51cc95c459b12dc9c179e 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 32d37263e96600ee81ec27f343bef812c4670968 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 3bd25bd0fa9bb8f5a50d9d8288ad6c615a83fbda 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* bee32592ef4952e7e0dce8035e66f827c79839b5 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 44ca61e2deca34d8fa68a31922b1efbe7f13f241 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 8ffcaf6251be5b05fe8ae4b3b88efbc5acc203bb 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 5693b8b28b402355ab4516db4db3246b0b1019b4 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 6075712d2138d0bdcb1192d473ba50fba9501e0f 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* aa1c4dd591fa2d5ecf0d65d53bb127eab6b95486 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* c33b22b1037d6eabceb7673975e50c78030059d7 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* a41f3692d3e17193f14ede5cd7f79172f339085b 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 65d9af0140d3761b4b450a5f475d5490b31ec5e2 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 4d60617725a5eb899cc5ebba85e72d598e66560c 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 4e13946f6ff22657c7ce1592e42100222da4e554 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 1f099de983ed1ec26b8fbfdf176b69950291ead3 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 177be894c56f68c3f3a3eb428b4993a8996da892 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* e59aad7153dd7bc87c056039fcd71ba0567f37af 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 93f0c3fdc0c2dbf24d41cb888668adc4e34c6352 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 2f8c3eebad6418c04ed8dcc35d0b1ac4bf3954d4 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* b3075e041f52cb6f7e3f9bc610e22000ce388548 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 5c300a80f82d94d6faf02de0da7a1ab25700461f 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* a9a3281d620bdd195369c9a6f0bc1d661910965a 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 0ad178797da63e826742c20600a89e50c47c20ec 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* e2e0f6d61c2d2a36ef516849dc1ca9223563479f 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* c55031473e743591b649da28cc16a4f095712082 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 804b780093b3c48842913ee472e9191afe5f8329 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* f14575da71758f21a437fc875ec99d68399d7931 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* f5f48a4605aff2622330356f45cf720871aa6c54 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* aa8fd9e81585ced5ce0fa580092b21e1f84d0fe1 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 202a215d01297eb825c0d45495fcbc46a82a5e30 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 907bbfe535d21e0709eb4a67fbea1e304beec22b 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 11ba299dca221b6d4e970eeb58dd17ccbb825a9a 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 56b767ff3f70ea5427900c37659b0ea94eb49628 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* c780e55efec3ef2f765cb0223cd154102a85a1a3 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* e4b64b00cf7698a624ea8a0a8bfba551d4674dc0 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 215df8e96590dda0539d168eba000a579ab52517 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 71bb615e234133d22f04106c812aad9fcb269aa5 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 721e4a9e1beb2392ee096d38ead44c0ece962eb3 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 10e0b6ffb979f0a27fe58206512055133e67460a 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 39ee4ec5e026dbdae084989c856a655e1e342629 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 9719570729342a13fac0ead93a68d4516c5dab79 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* df20618b3ce9cefc19d603d5300952550074a3fe 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 13b872c19a9459e21a7f91aaebaf2a29e99684c9 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 0187ccd95604614009b37bb23a55f2e634cdc404 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 89813bf8e4cb806eb01232b396410c28f30ca742 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 1b6e19eaef9e81ec19d6e826837e7e1a09f968cd 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 2c06ddf3d4f0d682c70ae75643db307df4fd36dd 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 7de6a8a7965950448a3abd0fe16653fe5af21e82 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 7243deb443c38ff4a47a840ae01546c8106de245 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 9f0a8bef3c4839c14751d6223a69b03e4f33fcea 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 3c5bf1e1cefc9f3ecc2cb3b07ea5981933df9fd9 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 658ded3ffea6353492de3f082e61aa489c728fa3 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 3fbfa71e126a95a884f74e9eeff8d189902d378a 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* bd9c319f9f10d9dc3c3d94fd490d92dd4270ba40 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 8cfad82a72b94d1b362c20c5be95fbf677cc31f1 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 3238461b0930ade9172a2a449497af12aad56e1a 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 74d2f66f575246cf6e313ae7775f44df6e097ff2 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 4b2a34740425b4bc124804edd8e37c51d303c135 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 2ee32bc65b7e0d2ca8aa05a6b952817fd87f93f1 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 5fb9ad197f5bf57cf86c5f8df0c061ccb0bcf6a2 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 5f82b635c5da5c3df44d5a0caa8983d9b95e0db9 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* d0dc04d9c315b2a658bcabb3b970994365034c6d 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 51eeca7ec47cf217f8bf296f6d1b2190584d0398 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 13baaa8df7dc7ba1c8e41351b08caacfd1f2244c 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 3a845452177a9910e21ce553a3c53d9f5e5763c0 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* bd9313a6fca4a08b4f0f0763f0079f6a24262726 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* d8b9ad9a680729df98bfa947c7c1538adab02b00 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* f428562bf63be8d5004bc871a6260d91a6a20d7c 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 6e0d69c42ee3b83080a8ec82bf76cb8fe558c24a 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 6e98f4bf810afb9b03d363b4c2c0c0adc1ac4953 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* fcf6e8d66463ddcb23a0abc1ba6fa9732ea595bd 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 60fa96c63967305b9784cbd8f685824281f67bea 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 7b2060bc871878d55adadb012f300fe3f00cac16 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 2479cf769ed5f0481ccf64860a2ab954514c2b59 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 53b7a0716e36654f041e922e1c41843d7a04ff4f 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* ebe60f2fc0f71a215cf0fd7fa848769f446f7fbf 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* c56f4ecc791676a2f8ce8f4e47aaf0b1c5b3763a 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 2c91e9e30567447d038f6bbe37308e9185087ea7 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* db10ad3fa281dfbd5f682a76b808193b8e179752 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 64ad98645b13a5746996af0ac964c19512fe6472 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* f56da4fbb342e7452dfbe9fd42430615833048d0 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 520dcf63a8c6d0f194de3ed4aa72f78fa573721f 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 30815fa6e9132605d8f0e8cfdce855638d2eb553 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 65de939f110068b1761504dcbed341383ab9819b 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 779d8a9e62cf9e5661dc3abeecf62e49ca64c15a 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 0f2d9b5447d3d6e56b41fe69d54e834849d7c932 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 682fc60a5381744863443a719dfbe6b9bac93ab7 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 392e84d63946e6337901baca316c0e106d3bb7ea 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 0f13f51fafe9d2b670252aa2a18993223bffdece 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* a613156836c24bfb1b94ea014273c0b711db6e69 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 991b13e8006d44c66a609ece7a3433f67112f06a 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* faed4dd1957a6b60ecc131ddd6aa41f6fdc3f0bc 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 48dc0add6170c4b6441554449ed21644230d7165 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* a55935e103729fc834780386b77176d6b7b7b285 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 73e84b8fd767310142604b67b67e41f7fea9cc6f 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* e3b9929cdc57149874e2211e6c8934145d245855 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 58aa824437eb20147acdf72dcabb4e271874a243 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* ed5e3f651305dd1ce0d0b5d2d08b97a963634632 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* d1bd7f42fcacd3fe29328b32be361964a915788e 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* f95130bbcbc8917bfec8f8875fa810ad93b08871 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* de6810309a6165547443f9970ee311af0bc7688c 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* edac0d6a533281e3bb2eb729dcd09d1b40023bbe 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 79991c75d1419fcf76e72eddb9b0aea14016e438 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 7b931ef7f9dbb8d2386fc9170895708c0acae95c 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 68acee77cebda300e1f36fb6279f19d53d5b5cda 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 2eb9bb19d43b6285466ca36b92d1324a09e7a207 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 417094b2224ea7cc130997f8d9c36847b2efb302 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 436a89b30cc988fd5505c9cd8f5dd81110a8606b 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 8aea869bd93421128db2b4c9d4fd0b936e2a3a99 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 12d76e891ffe5eda6265c79b438a07f5dce126d3 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 051f98ec6ab2479cd46e405432811d2bb129e4e6 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 3a7c90aeacbd418f2d29d2522027e5621060dbf7 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 38d211f19a4bcb9b8b684cdb3fe1f0459f05b73d 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 9c256b67486b4b30c757730e1b65b2c2d2af935b 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* be6c9580237ec7a4f86c3d98228e4db569d6dca4 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* e977cc67456de8349dcc08143b92ce5608083f67 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 7b05a32346a40789271f401afbbb7fb3310b281f 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* a2ff696ab8f5e0b38823ad3b5cdff72772093c6e 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 5c5b894a67b967484f7c34a6be0d076fda20e246 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 4f7f02ca528212c578fd56379a202d219d01abe0 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 8fd6c915c96073c818bfc42089a95936e80f265c 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* b1a3b7b7c9c8cef5e8c8d44771426e7764b9521e 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* a521ed701f0e58cc5e7effd239b10b360908365a 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* b77c53c85e5734756a793497845735485c9dc933 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 56723d0cf7cd8ff07c5f08bf9bc4575b4e2ed895 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 07f85ad7a273e9f7b761265e4a1edf4ef1d45252 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* fb15752f38f89920dc533e20572e1f6980fbe831 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* eb0561c9c4e2d7d8a334b9d58075aeac8393eec8 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 9379fb71b66a2e1412cab7c5b5a5ff9f04632552 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* d06afc106ce644f2deb5530a36aa7878bb0fe9f7 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* a8305e82e93d03322a704cb09d3caf09d14bad1d 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* c5b5450dab4e2fbef695e16856ec44228688c9ae 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 2b0709d61427694b5c21140021b61a0d402e9c21 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 6615f70c912e4af82f2149ae08a0dfa8e0ed1a2c 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* d1ca0e8c4774c776703106c7bd1efbd22ef44bcf 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 08f77c1486bf2028cea594d13cc1c1a7a3a8ca75 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* f86fc748ae0c1744484609f65abdfd9af59d258d 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 45b4c981e5f3fbb804b711787e4d5ac98f5942a9 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 6e8770f0d1a953d23ed475b47c299cd44a9fe7ee 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* bf41f3ac094a68854f2af8ab92d7752435a6e2af 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 2b77b4130f0de2e3e3846f8c51ece58720b0bad9 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 84a438ed9baa75c499ac51e67f1ee831cbacde57 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 59a3f1806f2535298003582703b4d43d52474999 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 8d7652776678320027c8f13a3af826e28423ae2b 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 3e388f6c6905dcb158fa1969b8a23ce3638bea6e 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 2aa452611d542482c8f854b565b5311aad3542d3 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 115a28148283de8ea6314aa468212541b50906aa 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* bcf9dfa654964e72b32b3ac74a34aaef2b5ec3c0 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* a087ce57288aab8945cdf4aca4ba554eb46fec94 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 69074330666ab52f4386e0ef3fcff622d0d63c93 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* e13e2ea3dbbc9c7815e39c3749b4480514f555a6 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 82e4206fdc6e045f1c6f0f372995a5c943f15237 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 588bc92c33dfad516c730819a763175cc1b1fccf 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 000b9f6baeb60071512616d345c355a5c1ff5de3 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 6e97d0dde7947a2e2d42fc8069c4d2c0d4c8817c 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 097ac91926e57cfdcf05f76144946ebe664123e7 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 7acfded6f6171adcf2466ce83d20c4cb27e74a62 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 4885cffc1b986dfbf29184c7ec7603f2cbb8447d 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* c9257ae75101d9731483ac39d5771f382e6da39c 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 4e6ffe5568d7864f540d99c7d4ea9b9250ed30f2 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 1fcf0271646951e9b89c7303a9c3519fcf4c08ea 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 9f68279ef0e04b184bd133ddb395b3e8b13c41ba 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 699c47fa4b5c9ec4056b59b351761ccb6402072a 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 8438e359ffa7be7ba0b375404b54ce36687241ff 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 92ae1834e8ff26f9d9b15b4afb9ea36556503c2d 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* a8a2bded92d86f6ef4f16e92e303896198dc92b5 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 950bbbca54a0bd723341882fcc354a16568ec4ac 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* c5f469cf2f823889aaa53fbf90a1c41f4f4bb891 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 6054f0fd118073a08752807758629bab61cf258a 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 408d3a678a8b72d6f20e52ca5df34834fb8bc7c6 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 4e0e123f5012a4154bbe4c20194c7c021be69c77 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 4c29ea1bcf0cffc114de0f49c9162fce894f4a4f 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 71d0b9f08ab6bbdd4fe0234b0486c52dd45498e7 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 0974c3717fb71650a25898f4067eaf861d52f93f 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* d79abd10f12bd15f7be6a4b737f3c5d891a245c4 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 816905345646edb682078451ec7e1fc6c549b68b 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 598d6a5005c06d15e74ae621fa93e08dfe5884ff 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* b3b3190d0ea0a8b8e00a9378892f23d3af362b61 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* b6d5842c64f903b63cd61b79212e9c4b2a21a9a5 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 5574d3c433b99861ca3452a60be6cff1060e6a0b 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* f5ba975e5d1f9d862bb1d43621da76c14b6403e8 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 482c1feb328e8e83edf55f6053fc592b09a8b9cd 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 418d5768ee3a81f5f6c711e967f5bb2aa70ed2f6 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 7298334213411e218b7bc15aa99dcaa80e0910ca 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 61362e7b629e018ccf41293eca0b6fe382162e09 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 81a19667ebd56b604c7a158702b270b517fdf0a4 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 92e25e207bddd35facb23ee9d15ded30b753aca0 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 3cdaa69bca7bcc1fae6013095a979a95708dfba4 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* cfbb662982caf238e9006810045d70e1285e33b6 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 7521a1572358a038a77bdc3d4c4c19c8e6f0549c 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* d7cf0ba471fbc278aad355b829b442c77aad7368 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* cf7d262d74679cd874d143a245a7bf3337383dc5 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 176d1f794a0779a5c22f786828602ac4d8063a65 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 37395b3ebf25b2680159526750f9e370b8ed134f 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 8ec31edb38353f95bb9cac14a73d7ee861d22390 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 0afcfd4c3c7edd3c039057cb0ea1d8c58bee6c62 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* b584759103b9fa3d1b2688394bfee2ccb7246094 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* cdec5738cfc2c09b51c44d0958222c63b64f5606 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 56e217b077abb33045bfb2dc88dbb0f3de3f8eac 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* c2191a3fb10641d904107c54e08e34778b4abc01 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* efaec62cd396d3f71a4a522b3a7ae4948e335a12 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 4ef3f49b137ddda009973360e7b5118b535ed919 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 33ec4a7d1f46de9c0ff62c966b332689a983c008 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 41a6d7baa54df23e06b1b7513c9218d12b11cdbe 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* bbecaeeaaf38425d9085d759ded5ad845e74406f 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 613408509f896c68616a2df6c9c6d3fa6662f519 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* e9ec616d2f10323199073af1b25a586f1619ecb9 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 1af4a5ecd0c05f722ca04dfd287d892e11677b5c 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* eb01951c9598f15b2bf35b1c653aedbe87c49f73 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* f02def8ae7d7fcff0deb967f2f11d88e9d099b4d 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* b7fd0d56b2e89e4624ccd81f1dd50a2cc8c2e091 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 0db83dd13da549be953a316ba32826d974eed8c2 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* f69b35c8cff251e82fa33b82acb48c60ea05bf3f 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* ea75b9f21956341d325fdce4e38b6db41603f09c 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 245a86c80a2b2e1515a80a217aa7afdc139a8d9c 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* cf0fc461ac82eb8e9e2ca121a3cb1865533283aa 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* ddf1d2a883276c1575606fb9794df53fa0e0b400 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 31d58a849958079807963ee428d211200538d66e 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 59b4daee8f3db173f6a170b455ed840ac80679da 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 26ded1686b33f7ed2ed5d73f95eceb740b8d4387 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* f8dae005ddb31b54275ec4333adb317902a4f9f5 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 6d3cb50f1d58a0c816ee9c187c0408e6462f1f1c 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 6bbb4283c62e81f7f2788d468bbaa88685a3306b 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 4257362a3e91d29a34ccf1d27f5e5d645c468cd7 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 131f3254e45c071d618b500e06dff22f444dcf48 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 1723e19174c7ef3aa13e3b368eae0f979b26d790 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 1fbeb72867dc079fc4857de481fa1220e8362853 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 1f9128710b2dd8fb396ddf70c2b0cc6b36014271 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* ac7ba305e8216a9aa115bf63f9358915f1306b4f 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* a9cd1ddef78f4bf5476bf0c46be548bcd794ca3f 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 7c50f4f5e5f2799f05c2137453c34cdfd3ee150c 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 32b26bb692e612f1d0264b1ed3ac8a38c9dc4ac7 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* a423e48a52edc03febf258af9a86d383be728842 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 2ef97728fcc962749f5b2f4c20e772d84f342c82 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* b1987eba6fa6f5c40f987504a2fe2aaa1a34bde8 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 84ae82df0065de23110e705e4d09bb06f38bc291 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* a9ff271af5500fcd4b1609b964f80aad8105d4b9 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 4c9225a17aad6cc3e32d747b6960f4bc9697242b 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 3d516202c3259dbd9b4783c26072a0029e405e53 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 5fe2cca931aa907e46855b5f6a1c5863b6b2e762 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* abd97578e14c62c3e7127b70c46153cbd9421ed6 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* ee04eb7c0f1896611fec9f054c82be45b8246a21 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 3b4b717030770423ddb1f9da18e454203d318cc0 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 0b7c72d9a5b2dda6ceeefbbe23220d19a5076414 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* af1e59d8cb5d8eaaebe73a11132cdadad902886f 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 71439735b8e1e17d8bfa5c0ee91aa539c949c6bc 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 317101e03b8fe642589498f4bdb84541ab466062 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* a310095a5e5a0b085b5685300a9d1b757b839cf2 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 28849f5da2a875129ba4cdfd46c6ddb1f750a9a7 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 3305a76b4be86519655b5d27ea24627b3239c9c6 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 2ddb42eb26d6b19c66fcb75a626931bf5f4eb46f 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 32c69eda9af620dc6c3cf293546bd78226fcaf2e 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* c27611fffa3c59dddacc0152ce767207b3773a27 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 145618fb84bf448299e91704c3b82a851bbe87e0 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 6eaf5d4cadaf5fcbca4b9e63cbe8c9f2de95917c 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 0925f99493413ffa512b2f9278d12f0f81d43797 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 40e5c1a4dc420d35698d8d90c1daa6dc2eaff7ab 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 5331019bef5906209c7661f24adefca29e2fe9b4 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 210f14e753c80064ec1bcae72f1d654dd9b0e687 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 0e6e9ad98496ace25ae4b4c3bf1754f718061548 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 3a829c5bde408289cddac5ce23e1dc687cfbb154 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* c78f9148873c2e0f8e6dd9248fe3d0c4d93015f5 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 853424a7406f3c85d8090d697f6964cd5e9ed674 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* b2b01e6c5ca5e9d11c58a4bd7517c106affd53a2 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 593a08c8b28f8dca79230774c2b5158549976303 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* ad7ed8aa8e3acfe3d0e2efa8a0592c1119a54133 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 26aaa4f2533324ffee8f70e2aecffaf44a7a1d4e 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* a06a278fb2599177b063bfc56e1abf0591a2fde9 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 7100c159f552c6262c82954e3626ffeabeb4f421 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* c57c5efb6380da3ebc408113f836aa079d3638ee 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 7f0357977e3bfcf7484ca7032dbb0146d48f3a14 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 0cb2c4eca79737ef1cdc0d00899a519b5b630551 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* db3ebdaab2b66808a2063c4e732003606fdf8adc 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* c0058d9452a45c5192c2b47cf80d71e75c191d78 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 8ebeb45c666f938fec64fb7d0b5b7259c6cbe608 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 1730741cc09f9724a53bb23476ab0aa55828a249 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* cc88e9cc38c44719aebcf16f4333fab0e75ab60c 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* da549f59cd2113ac8d1ca133e80ecedfeb4db6bb 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* a24c4d8320cf5aecfc3797a91f3b9c76a5a7233e 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 83d8d49757658d7a742526ba5c72250244fd57d9 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 2cea69eb79fe358f63223e4a83058981461bbc77 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 73118049bb38a2f854c8c1280188a96839a1663e 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 1729091101160b4f1a78b60f86e2a8f9eb7cc9e3 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 42599e7dfd21a23dc24f18dc650066cd69a6a9cd 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 7eb8c94a909187eb08219891742e8eb60bc265fc 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* d073cb1f08ba1cc193331e6b0778b25be3397fc9 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 4a64f9a844e0af8cab93cda2f1b03bde083008f9 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 74d10cab153ab6d08f9b8c1cfa7f0a3ba78ae15e 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 27d8e086071759e4a642195e06fd952da6de2928 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* ec59d8731a967cb33044ba90d725565cf85cc07f 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 635bccec53fd04a22367ed387295f2ef38a50b79 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 5ff8eb6998ac8fa894f8db5a4f32e2e4fbed72f9 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 4394a3bc03e92c2a7c99f6584b86de85e41a96f1 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* fed2736a78077642a73256045c6e67985a2a8522 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 1d95d26d7c15aaadded91503dd461a034aae16d4 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 9d5437bc483e714468f9fa343e34ea098c40fb6d 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 3900059f7cbd83537475837d9e4a2243853cbb40 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* e22f91d9229bee5cac23b11a212315ad07088927 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* e9ef0092c9e97a415e677e0568f57a76100516bf 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* e12248c0e1996e5c2f1c618fbb47da4f6cb3ff43 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 49bf1a22fd3a906c6dde59819a404095b64834dc 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* aeb2fe1c274be79898e4a0ce1345cd2d260c48ad 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* aa5cce1e6c279364fba5a63d94d48835214dec3d 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 313c0031bbeded98d1bdbf2ed8ff785e73615d38 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* da2c748aad55c1a1dcba00b60fe4a35017bc6540 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 591cea1b074b56367f25ec0084a0c4ab66b42a7e 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* e9fa12b965897f485f8ec143e048a424b52f689a 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 874ffe5839095bd9b181eb465ec20fb473cda7c5 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* febccbdc19e0fc80761797dc52811dc056c0a39e 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 62e38a72a41d072f70ad78baa402d5ca4bb13022 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 460c375ff6889a2ad7eeffbdc246eedf767b6136 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* af24e419f48e543ced58dbca99ab39dc48ab6b11 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 5ab3a2849d68a074ebc70d0ac3aa4525804cea0f 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 486dda64980163cf4ab642b00514fcca169dd148 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* c94044ef540811ec9546606c8f641ce8c7fb7495 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* cc5f7693398ffd9844316ef7c16233f106bee8d7 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 8e50188e183222ffeb44f9729dde056a33dded38 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 953756fcbc88d5c17943faee243bd3efe9a5815c 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 0ec476f32193c33d024ec99d26e2cea0d74168db 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* d746d4625b2cb306f2c7427c5c97efc6d25fcb34 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* f69ab255a58896e066a754aeea1e4381c4e858b0 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 73856a817dcaa91a87cca97f32f47fb0edb8b9a1 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 7f89241892e2385aa1442c2afed93a5542018b45 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 1f3eef621985b2dd0e7e2d3ac265c0f0ba6b0a78 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 841eaea8885f568c5f195f22ff3c8738769f2fe6 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 704d27e3eac9b0ae42e600559f763cacd9a55959 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 9766128dbc8b3b47f154d1112e063183df0f5021 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 619713b3aa1bbaaca174387537a9b089f728f9b9 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* f90201b532678aeadd5933ad31893a5c720ef0a7 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* db5c90a92b6a3218f340af2847934c6549645082 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 5ec48f1be265535c91d4044048bb363fb17893bd 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* cf0af730bf0023484db82d4cc337d2f68541cb6e 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 60837d59cfad2b8adaba1aaf7b459171e4588792 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 14ed3374e4e70ffdbdfef740a6272e92e4a7300c 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* d8aba486f57f013a337f4a48774f161b7cc594ee 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* cd2ab490341f080607792ec86b6c4e61cfb760d3 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* ba8dbd364dd18a16396bf10f722a156241fffcf6 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 3c616d0c3b519cff42ee675fa113bd86c05a77a0 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 4f85a1b7fd2a6dda80872e28ccd1c426ece01b36 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 8f2ee50841e216cd8c192eeb68953118190f105c 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* be39d4eac8653672120843ad9bc715bad8fca05c 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* fdd54cf0d1643a1060147159e1d9ced46402a1a1 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 83a79230dc2a0e30ed0dfc03121a0ae2f9ee6de6 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 5483e909b32272ee294e3518036479498af37902 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 257869d3061d5a8ed1529bf34225d90a2416d51a 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 9e9513b11fff182b08cf8cd6c9c2272460636586 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* d200ddcafb7f7336b0ae0353ea1504153876d6f2 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* fad242d35291de1898242d6737ba02b74a59a2f2 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* d32478ac7c6cb7fdad4dc7a8fdc54f7f171a22fe 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* a8f7249c3c5aef1ed6dfa16e637f13dfde815464 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* fcb566cfb8a86cea2d3666bf65a4f177a49b1313 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* a26cc7479b14cdf70433919bfc228625f1c5c6f2 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 092dedada188f56c5915f74a5fd40aac093742c3 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* a3308ea8d0cc513727c8ad149b1132317a886c98 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 76117d7031e856687c5bd9f5f99c79b94b1316d7 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 9637782beb62abfc9a53a59ab762d39f6ae6fb29 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 51662dfb31fe27ffd77d375397c539f31ed5a648 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* a4ab5378e4cc20b30ef94848cca5700af46449fe 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 3e85179a5601bf322bff7697cfc40d5212249707 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 516bd724d2f4820c822d94af2b3b523dd0a87ae3 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 5db75ff7f6a7c6bb5ae534dd5c618c4e787e7ff3 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 48c78d90a3625cae8716bb20091bda775378b075 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 67e2663dd470950473d6183be345f6d8b1269165 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* aed079ec655046f6a876654d7c36f388682b777f 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 62f33637281ffb45ca8fa91b9411e1cd73e06dab 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 2da539ab426f347c58640a45f5428a80b4ca930f 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 25ca9884a186e07efd9c6ad0c26ab53552db2f11 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 4dd1f01c26e4b24e691075f1b78bcbf90f7f19b1 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 6036065906a8bff074116960cd7dd39db9887b56 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* ba9506eb36a78cd081ea10ffcc543e0f1abd840b 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* e083dfe79605163283d635989662937bd34dbe79 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 055c65edc75d5c6e95b96d2094aadda49b44c965 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 5ba80ae9a40991b8f01c69debd791ffdad5ceb2d 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 969a93e25e377c6990508e8d0bc86aa526812f59 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* bebca33f522165afbdd23f95118715ff6637a731 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* afd733ebdb9260f143953f817a3ce31855ba8812 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* d64ae050c7936eb0a5d00144900f892fc826707c 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 174c264c0a59e84e3c4d2a283e94d6981606a8be 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 4703b76221f1cc7194e11e8af2b9d5959c672a55 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* c59f24f76e02a8d4ecd2aea4309d185d7ed13128 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 5dda3276064d871b8372501271cd62264efb8d89 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 9b171118ea9a3207fe3992d56104582c5fb75be5 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* c51fb9761ebdbf9f6fb46300e047635b1c8afcd3 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 33c8d5f15521969a09ea245c076e541d768b5070 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 0ef5917b3a6742dc69b6758b3e40a7beb8b97d04 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* f8cc276bb0add12debc9eddce79e2a842bac5d3e 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* b5e96eef9e61e53c947e3ba407d4025d71375af9 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 571cc734cd69fb846032ec77a662aa8ee4fc32cd 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 6b38eb50104901e2fec80f9455a972bc3eced813 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* b1048c6b106073c8e51dd61d3104200dfbe7f807 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 4d104d325b96c348d685956c6678139640c15c4b 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* f83f03f59b00d9ed0efc0f372c9a153bcd5dc56d 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 28f7a2cba34c51ebccbc4e99acd4cb7cbe07c71a 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 541214b84dea7f2a94e5a5ca9506c4a0aadcda28 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 26112958664f295cd0ffa28936fc940aa001acda 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 4a329372d9257c59929624e8d05d67124d3b84a3 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* f192f08d8f7b27bbb61ad905764255c83d684cdf 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 0a70a726ce116e2d51c65a2b7282ca2134325759 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 0ecfa6323d4bcc400f1a32c6693ef11691c26448 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 77eaea773f952715656a1734e24f274d41e751a7 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 18d883be86974f1836675db59f277861de57c750 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 4e74daad2f74bd836eec04ff2f4202bee973107c 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 24a12d6f9278a4ab2928475e8ce06a0f59ae73d4 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 59c181fb6e48f7c58e7fe12a1dda10d2183d0e2b 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* cc02f9afca03970a97c2ddea6d36df3b864e7e18 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* e4326a39e36a0b72b15adc95c91671e6a6dcce66 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 1441c354eec2ab2d25319e1d96b33ef347123f47 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 3007f0538458a697749520100901aa80885aeae2 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* de3a4883c3197a29abafab5e87a0a8bead421fbe 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 84355587cb2a1f86e6882de83514a32db2646e7a 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* e9b9c9c34e06822ca1b643d0fdccea3efb4d2899 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* cf064b91ac6d89d5cc6014365d3c0d4753cd6378 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 1d74c6cc47df5cbf75b0b71151c80262459af8d8 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 6cb237c5f1c11c10377ab406385617ee783707ad 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 53eb5671945452618ad3288e6f939717b8b446ef 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 685341c6544a6ed2ce488028184430456ca07005 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 9f916c1709074435f18c6bdc6ee968078cb9cd43 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 9de5f9213b29c2469410048b94f783697734d56e 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* a63b2761ba0b97575c4f964ffc0833afb4d39986 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 4ac10c837490a0d2b68e589643b9c0e36f539297 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* e23bb022327c32995eb415991e7fefb5b26a6ec4 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* d5fe075a50ab3ce4f41614d66ed77f152fda134f 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* d67def6716c4ec2a650e7a8d18264fa22aff2750 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 11886177fe1b02839bd4f6d5f2ac12e03d651a7d 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* beb62aaaad0c1701fbce6f3f21777837a424d737 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 6fd77606f4ead7b81d4aa948a88b4064ebd50b97 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 3de9c413664d81786e789a379e9d8ce9b31402d5 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* bbfae30d95cf72ec632a5914265c3f4ab50a9ec8 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 210dc96d7d514abb943a8b314603397c5577ffff 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 14c162416ebe76d1eb4d542af75f876fc2237c1d 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 07d7b63f09b6685278032e189e9f6d52dccb0939 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 8c702cea303c5efd4348d1b6cad58a5a9b545121 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 63f31133a69fb15322546633a334864d6636834d 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 23187bb333d50fdeddba88a9eb769f4cc6df1127 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* f3139b7e8104dbe3da75ab238b86695c75525a3f 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* f451a2bb3874ca0a286a5f09cd15f7fe6470ddda 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 70cdad0e95bb2366a9b2ae9789c0e377fef6e3ef 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* c65070f9cd162346bb724d7bf4f2fa20cde8f1cf 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* a22a7abf2f6d6dda8e2ad2e41aeaeff9d7b2d02b 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 19bccaf083d56fce700beb7550414231e562b4ea 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 6f45838f3cca3ec8f25b0ee62d1d4259b0e85c2a 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 321873914f47d759dce3fc87bb18f77f8455bf78 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 6e612b12d7fee334726651eb156c57151c64ab7f 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* dfe8b4600c2f80fddba6835b21a6091939882a5e 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 8d0ae097adba4f90a2fad325abc5585ad8b9966e 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) +hyperpolymath/standards/.github/workflows/* 5b5dab9fb89c0ab7187ddc334d8995bd24392e41 75bd5994470661a099df562d6d5ad2fe744dbd66 Window B: self-SHA lock records (invalid lockfile pin) diff --git a/scripts/lib/yaml-anchors.awk b/scripts/lib/yaml-anchors.awk new file mode 100644 index 00000000..8a409892 --- /dev/null +++ b/scripts/lib/yaml-anchors.awk @@ -0,0 +1,141 @@ +# SPDX-License-Identifier: MPL-2.0 +# yaml-anchors.awk — the YAML anchors one line of a workflow defines. +# +# Shared by the workflow-pin fixers, which prepend this file to their own awk +# program (`awk "$(cat lib/yaml-anchors.awk)"''`); POSIX awk only, so +# mawk reads it as gawk does. +# +# anchor_defs(s, defs) clears defs and sets defs[name] = value for every +# `&name value` on line s, and returns how many there are. A name is every +# character up to whitespace or a flow indicator (`,[]{}`), as YAML reads +# one: `&rust/tool` names `rust/tool`, never `rust`. An anchor is +# `&name` where a YAML node starts, outside any quoted scalar and before any +# comment: at the line's start or after its list dashes, after `key: `, or +# inside a flow collection after `[`, `{` or `,`, past any tag (`!!map &x`). +# `&call` written inside `NOTE: "fixture &call a/b@"`, inside a plain +# scalar (`NOTE: fixture &call a/b@`) or after a `#` is text, not an +# anchor. Its value is the scalar after it: a quoted one +# read to its closing quote (`''` is a quote inside single quotes; inside +# double quotes each `\` escape is decoded, yaml_unescape/1), a plain one up +# to whitespace, `,`, `}`, `]` or a comment; "" when the anchor names a +# mapping or sequence that follows; "\002", which no reference holds, when a +# quoted value runs past the line's end — continued past an escaped line +# break or folded into the next line, it is not the value YAML reads, and a +# caller that wants it joins the line first (yaml_dq_join). The caller skips +# the content lines of block scalars (`run: |`), which are text throughout. +# +# The character a double-quoted escape stands for, `t` being the text after +# its backslash; esc_len is set to how much of `t` it spans. YAML resolves +# `"\x64tolnay/rust-toolchain@"` to the action itself, so an anchored +# reference is read decoded, never as the raw bytes that hide it. An ASCII +# character comes back as itself (`\x6f`, `\u006f`, `\"`, `\\`, `\/`, an +# escaped space or tab); anything else — a code point past ASCII, `\n`, an +# unknown or malformed escape — as "\001", which no action reference holds. +function yaml_unescape(t, e, n, k, d, code) { + e = substr(t, 1, 1); esc_len = 1 + n = (e == "x") ? 2 : (e == "u") ? 4 : (e == "U") ? 8 : 0 + if (n) { + code = 0 + for (k = 2; k <= n + 1; k++) { + d = index("0123456789abcdef", tolower(substr(t, k, 1))) - 1 + if (d < 0) return "\001" + code = code * 16 + d + } + esc_len = n + 1 + return (code >= 32 && code < 127) ? sprintf("%c", code) : "\001" + } + if (e == "\"" || e == "\\" || e == "/" || e == " ") return e + if (e == "t" || e == "\t") return "\t" + return "\001" +} +# Line s as YAML reads its double-quoted scalars: each `\` escape decoded +# (yaml_unescape), every other byte kept as written, so a reference spelled +# with escapes (`uses: "\x64tolnay/rust-toolchain@"`) is matched as +# the reference it is. An escaped quote decodes to "\001", never to `"`: the +# quotes that delimit a scalar stay the only ones, and a reader of the line +# sees the same scalars. Single-quoted scalars have no escapes; a comment is +# copied as written. +function yaml_decode_dq(s, out, i, len, c, q) { + out = ""; i = 1; len = length(s); q = "" + while (i <= len) { + c = substr(s, i, 1) + if (q == "\"" && c == "\\") { + c = yaml_unescape(substr(s, i + 1, 9)); out = out (c == "\"" ? "\001" : c); i += 1 + esc_len; continue + } + if (q != "") { if (c == q) q = "" } + else if (c == "#" && (i == 1 || substr(s, i - 1, 1) ~ /[[:space:]]/)) { out = out substr(s, i); break } + else if (c == "\"" || c == "'") q = c + out = out c; i++ + } + return out +} +# Does line s end in an escaped line break inside a double-quoted scalar +# (`uses: "dtolnay/rust-\`)? YAML then joins the next line to it, the break +# and that line's leading whitespace removed (yaml_dq_join), so the scalar +# is read whole only across both lines. A comment ends the line. +function yaml_dq_continued(s, i, len, c, q) { + i = 1; len = length(s); q = "" + while (i <= len) { + c = substr(s, i, 1) + if (q == "\"" && c == "\\") { if (i == len) return 1; i += 2; continue } + if (q != "") { if (c == q) { if (q == "'" && substr(s, i + 1, 1) == "'") { i += 2; continue }; q = "" } } + else if (c == "#" && (i == 1 || substr(s, i - 1, 1) ~ /[[:space:]]/)) return 0 + else if (c == "\"" || c == "'") q = c + i++ + } + return 0 +} +# Line s, ending in an escaped line break (yaml_dq_continued), joined with +# the line t that continues its scalar. +function yaml_dq_join(s, t) { sub(/^[[:space:]]+/, "", t); return substr(s, 1, length(s) - 1) t } +function anchor_defs(s, defs, n, i, len, c, q, name, v, seen, depth) { + split("", defs); n = 0; i = 1; q = ""; len = length(s); seen = ""; depth = 0 + while (i <= len) { + c = substr(s, i, 1) + if (q != "") { + if (q == "\"" && c == "\\") { i += 2; continue } + if (c == q) { + if (q == "'" && substr(s, i + 1, 1) == "'") { i += 2; continue } + q = ""; seen = seen "x" + } + i++; continue + } + if (c == "#" && (i == 1 || substr(s, i - 1, 1) ~ /[[:space:]]/)) break + if (c == "\"" || c == "'") { q = c; seen = seen "x"; i++; continue } + if (c == "{" || c == "[") depth++ + else if ((c == "}" || c == "]") && depth > 0) depth-- + if (c == "&" && anchor_at(seen, depth) && match(substr(s, i + 1), /^[^],[{}[:space:]]+/)) { + name = substr(s, i + 1, RLENGTH); i += RLENGTH + 1 + while (i <= len && substr(s, i, 1) ~ /[[:space:]]/) i++ + c = substr(s, i, 1); v = "" + if (c == "\"" || c == "'") { + q = c; i++ + while (i <= len) { + c = substr(s, i, 1) + if (q == "\"" && c == "\\") { v = v yaml_unescape(substr(s, i + 1, 9)); i += 1 + esc_len; continue } + if (c == q) { + if (q == "'" && substr(s, i + 1, 1) == "'") { v = v "'"; i += 2; continue } + break + } + v = v c; i++ + } + if (i > len) v = "\002" + q = ""; i++ + } else { + while (i <= len && substr(s, i, 1) !~ /[],}[:space:]]/) { v = v substr(s, i, 1); i++ } + } + defs[name] = v; n++; seen = seen "x" + continue + } + seen = seen c; i++ + } + return n +} + +# Can a node start after `seen` (the line so far, quoted scalars as `x`), at +# flow depth `depth`? At the line's start or after its list dashes, after +# `key: `, or inside a flow collection after `[`, `{` or `,`; past any tag. +function anchor_at(seen, depth) { + if (seen ~ /(^[[:space:]]*(-[[:space:]]+)*|:[[:space:]]+)(![^[:space:]]*[[:space:]]+)*$/) return 1 + return depth > 0 && seen ~ /[{,[][[:space:]]*(![^[:space:]]*[[:space:]]+)*$/ +} diff --git a/scripts/maintenance-hard-pass.sh b/scripts/maintenance-hard-pass.sh index 60fc577e..88275c67 100755 --- a/scripts/maintenance-hard-pass.sh +++ b/scripts/maintenance-hard-pass.sh @@ -59,7 +59,18 @@ repo_name="$(basename "$repo")" ts="$(date -u +%Y%m%dT%H%M%SZ)" if [[ -z "$output" ]]; then - output="/tmp/gitbot-fleet-maintenance/${repo_name}-${ts}.json" + # Reports persist between runs; keep them in the user's state directory, + # private to this user. Only the report is: its directory and the empty + # report are created under umask 077, and the caller's umask is back in + # force before the runner starts, so whatever else the runner creates is + # as the caller would have it. The runner writes into the existing file, + # which keeps its mode. + output="${XDG_STATE_HOME:-$HOME/.local/state}/gitbot-fleet/maintenance/${repo_name}-${ts}.json" + caller_umask=$(umask) + umask 077 + mkdir -p "$(dirname "$output")" + : > "$output" + umask "$caller_umask" fi mkdir -p "$(dirname "$output")" diff --git a/scripts/submit-finding.sh b/scripts/submit-finding.sh index 658f51f9..fd816729 100755 --- a/scripts/submit-finding.sh +++ b/scripts/submit-finding.sh @@ -40,7 +40,7 @@ REPO_SLUG=$(echo "$REPO_NAME" | tr '/' '-' | tr -cd 'a-zA-Z0-9._-') TARGET_FILE="shared-context/findings/${REPO_SLUG}/${TIMESTAMP}.json" # Clone or update fleet repo -FLEET_DIR="/tmp/gitbot-fleet-$$" +FLEET_DIR="$(mktemp -d)" trap 'rm -rf "$FLEET_DIR"' EXIT if [ -n "$PUSH_TOKEN" ]; then diff --git a/scripts/tests/fix-workflow-pins.sh b/scripts/tests/fix-workflow-pins.sh new file mode 100755 index 00000000..d1aebd73 --- /dev/null +++ b/scripts/tests/fix-workflow-pins.sh @@ -0,0 +1,3813 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: MPL-2.0 +# Contracts of the three workflow-pin fix scripts, each against a planted +# defect, a planted non-defect that must be left alone, and a second run that +# must change nothing. Offline: no network, no gh. Run by rust.yml +# (dispatch-contracts) and `just test`. +# shellcheck disable=SC2034 # out/rc variables are read inside check()'s eval strings +set -euo pipefail +fleet="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +fx="$(mktemp -d)" +trap 'rm -rf "$fx"' EXIT +# Keep the fixers' own temporary files inside the fixture, including failures. +export TMPDIR="$fx" +pass=0; fail=0 + +# Increment the pass count and print assertion label $1 to stdout. +ok() { pass=$((pass + 1)); echo " ok $1"; } + +# Increment the failure count and print assertion label $1 to stderr. +bad() { fail=$((fail + 1)); echo " FAIL $1" >&2; } + +# Evaluate trusted fixture assertion $2 and record its result under label $1. +check(){ if eval "$2"; then ok "$1"; else bad "$1"; fi; } +# Capture both channels and the exit status, including expected failures. +run_fix() { rc=0; out=$("$@" 2>&1) || rc=$?; } + +# ── 1. fix-rust-toolchain-input.sh ─────────────────────────────────────────── +mkdir -p "$fx/tc/.github/workflows" +cat > "$fx/tc/.github/workflows/rust.yml" <<'YML' +name: Rust +jobs: + check: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772 # stable + with: + components: clippy, rustfmt + - uses: dtolnay/rust-toolchain@e2a55d2ffb04f378e9626c28d38b36d230d1e12f + - uses: dtolnay/rust-toolchain@stable + with: + components: rustfmt + - uses: dtolnay/rust-toolchain@683b588c487a69d01f6c6ddf40f036ff682cc2c8 + with: + toolchain: 1.85.0 + - run: cargo check --locked +YML +cp "$fx/tc/.github/workflows/rust.yml" "$fx/tc-before.yml" +bash "$fleet/scripts/fix-rust-toolchain-input.sh" "$fx/tc" >/dev/null 2>&1 +check "toolchain: inserted as first key of an existing with: block" \ + "grep -A2 '6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772' '$fx/tc/.github/workflows/rust.yml' | grep -q '^ toolchain: stable$'" +check "toolchain: inserted with a new with: block under a bare uses:" \ + "grep -A2 'e2a55d2ffb04f378e9626c28d38b36d230d1e12f' '$fx/tc/.github/workflows/rust.yml' | grep -q '^ with:$' && grep -A2 'e2a55d2ffb04f378e9626c28d38b36d230d1e12f' '$fx/tc/.github/workflows/rust.yml' | grep -q '^ toolchain: stable$'" +check "a tag-pinned step is untouched" \ + "grep -A2 'rust-toolchain@stable' '$fx/tc/.github/workflows/rust.yml' | grep -vq 'toolchain: stable$' " +check "a step that already names a toolchain keeps it, once" \ + "[ \$(grep -c 'toolchain: 1.85.0' '$fx/tc/.github/workflows/rust.yml') -eq 1 ] && ! grep -B1 'toolchain: 1.85.0' '$fx/tc/.github/workflows/rust.yml' | grep -q 'toolchain: stable'" +check "the checkout step and the run step are untouched" \ + "grep -q 'actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1' '$fx/tc/.github/workflows/rust.yml' && grep -q 'cargo check --locked' '$fx/tc/.github/workflows/rust.yml'" +check "exactly two steps were fixed" \ + "[ \$(grep -c 'toolchain: stable' '$fx/tc/.github/workflows/rust.yml') -eq 2 ]" +cp "$fx/tc/.github/workflows/rust.yml" "$fx/tc-after1.yml" +bash "$fleet/scripts/fix-rust-toolchain-input.sh" "$fx/tc" >/dev/null 2>&1 +check "second run is a no-op" "cmp -s '$fx/tc-after1.yml' '$fx/tc/.github/workflows/rust.yml'" +check "the fixed file still parses as a step list (actionlint-free structural probe)" \ + "! grep -nE '^\s*with:\s*$' '$fx/tc/.github/workflows/rust.yml' | awk -F: '{print \$1}' | while read -r n; do sed -n \"\$((n+1))p\" '$fx/tc/.github/workflows/rust.yml' | grep -qvE '^\s+[a-z-]+:' && echo bad; done | grep -q bad" +# Planted control: a tree with no SHA-pinned dtolnay step is left byte-identical. +mkdir -p "$fx/tc0/.github/workflows"; printf 'name: x\njobs:\n a:\n steps:\n - uses: dtolnay/rust-toolchain@stable\n' > "$fx/tc0/.github/workflows/x.yml" +cp "$fx/tc0/.github/workflows/x.yml" "$fx/tc0-before.yml" +bash "$fleet/scripts/fix-rust-toolchain-input.sh" "$fx/tc0" >/dev/null 2>&1 +check "control: a tree without the defect is untouched" "cmp -s '$fx/tc0-before.yml' '$fx/tc0/.github/workflows/x.yml'" + + +# Review cases (gitbot-fleet#615): a `with:` with a trailing comment, a flow +# `with:`, `with:` written above `uses:`, `env.toolchain` (not an input, so it +# must not count), and a quoted key. Each step is inspected whole. +mkdir -p "$fx/tc2/.github/workflows" +cat > "$fx/tc2/.github/workflows/rust.yml" <<'YML' +jobs: + a: + steps: + - uses: dtolnay/rust-toolchain@6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772 + with: # pinned inputs + components: clippy + - uses: dtolnay/rust-toolchain@e2a55d2ffb04f378e9626c28d38b36d230d1e12f + with: { components: rustfmt } + - with: + components: llvm-tools + uses: dtolnay/rust-toolchain@683b588c487a69d01f6c6ddf40f036ff682cc2c8 + - with: { toolchain: nightly } + uses: dtolnay/rust-toolchain@1234567890abcdef1234567890abcdef12345678 + - uses: dtolnay/rust-toolchain@abcdefabcdefabcdefabcdefabcdefabcdefabcd + env: + toolchain: beta + - env: + toolchain: beta + uses: dtolnay/rust-toolchain@0000000000000000000000000000000000000001 + with: + components: clippy + - "uses": "dtolnay/rust-toolchain@0000000000000000000000000000000000000002" + - run: cargo test +YML +f2="$fx/tc2/.github/workflows/rust.yml" +bash "$fleet/scripts/fix-rust-toolchain-input.sh" "$fx/tc2" >/dev/null 2>&1 +# Print each match of pattern $1 in fixture $f2 with $2 following lines, +# merging overlapping groups as grep -A does; return grep's exit status. +after() { grep -A"$2" -- "$1" "$f2"; } # the N lines after the first match +check "a with: with a trailing comment is reused: toolchain: goes under it, no second with:" \ + "after 6c977a6c 2 | sed -n '2p' | grep -q '^ with: # pinned inputs$' && after 6c977a6c 2 | sed -n '3p' | grep -q '^ toolchain: stable$'" +check "a flow with: { } gets toolchain as its first entry" \ + "grep -q '^ with: { toolchain: stable, components: rustfmt }$' '$f2'" +check "a with: written above uses: is found and extended; nothing is inserted under uses:" \ + "grep -B3 683b588c '$f2' | grep -q '^ toolchain: stable$' && after 683b588c 1 | sed -n '2p' | grep -qv '^ with:'" +check "a flow with: above uses: that names a toolchain is left alone" \ + "grep -q '^ - with: { toolchain: nightly }$' '$f2' && after 12345678 1 | sed -n '2p' | grep -qv '^ with:'" +check "env.toolchain is not an input: the step still gets with: toolchain:, env: is kept" \ + "after abcdefab 2 | sed -n '2p' | grep -q '^ with:$' && after abcdefab 2 | sed -n '3p' | grep -q '^ toolchain: stable$' && grep -A4 abcdefab '$f2' | grep -q '^ toolchain: beta$'" +check "env.toolchain above uses: does not count either; the existing with: block is extended" \ + "grep -A3 '0000000000000000000000000000000000000001' '$f2' | grep -q '^ toolchain: stable$' && grep -B3 '0000000000000000000000000000000000000001' '$f2' | grep -q '^ toolchain: beta$'" +check "a quoted \"uses\" key is recognised" \ + "after 0000000000000000000000000000000000000002 2 | sed -n '2p' | grep -q '^ with:$' && after 0000000000000000000000000000000000000002 2 | sed -n '3p' | grep -q '^ toolchain: stable$'" +check "exactly six steps were fixed, one toolchain: each" \ + "[ \$(grep -c 'toolchain: stable' '$f2') -eq 6 ] && [ \$(grep -c 'toolchain: nightly' '$f2') -eq 1 ] && grep -q 'cargo test' '$f2'" +cp "$f2" "$fx/tc2-after1.yml" +bash "$fleet/scripts/fix-rust-toolchain-input.sh" "$fx/tc2" >/dev/null 2>&1 +check "second run over the review cases is a no-op" "cmp -s '$fx/tc2-after1.yml' '$f2'" + +# Review cases (Codex, gitbot-fleet#615): a quoted "with" key; the same text +# inside a `run: |` script and under `env:` (data, not the step's action); a +# tag ref that starts with hex digits (`@1.85.0`); bare `-` openers; and a +# flow-style step the parser must refuse out loud rather than report clean. +mkdir -p "$fx/tc3/.github/workflows" +cat > "$fx/tc3/.github/workflows/rust.yml" <<'YML' +jobs: + a: + steps: + - "uses": "dtolnay/rust-toolchain@0000000000000000000000000000000000000003" + "with": { components: clippy } + - name: write a fixture + run: | + cat > fixture.yml <<'EOF' + uses: dtolnay/rust-toolchain@0000000000000000000000000000000000000004 + EOF + - uses: dtolnay/rust-toolchain@0000000000000000000000000000000000000005 + env: + uses: dtolnay/rust-toolchain@0000000000000000000000000000000000000006 + - uses: dtolnay/rust-toolchain@1.85.0 + - + with: + toolchain: nightly + uses: dtolnay/rust-toolchain@0000000000000000000000000000000000000007 + - + uses: dtolnay/rust-toolchain@0000000000000000000000000000000000000008 + - run: cargo test +YML +f3="$fx/tc3/.github/workflows/rust.yml" +cp "$f3" "$fx/tc3-before.yml" +set +e +bash "$fleet/scripts/fix-rust-toolchain-input.sh" "$fx/tc3" >/dev/null 2>&1; rc_tc3=$? +set -e +check "a quoted \"with\" key is reused: toolchain goes into it, nothing inserted under uses:" \ + "grep -A1 '0000000000000000000000000000000000000003' '$f3' | sed -n '2p' | grep -q '^ \"with\": { toolchain: stable, components: clippy }$'" +check "a SHA ref inside a run: | script is data: left alone, nothing inserted after it" \ + "grep -A1 '0000000000000000000000000000000000000004' '$f3' | sed -n '2p' | grep -q '^ EOF$' && grep -q '^ uses: dtolnay/rust-toolchain@0000000000000000000000000000000000000004$' '$f3'" +check "a SHA ref under env: is data: the step gets with: under its own uses:, env: is kept as is" \ + "grep -A2 '0000000000000000000000000000000000000005' '$f3' | sed -n '2p' | grep -q '^ with:$' && grep -A2 '0000000000000000000000000000000000000005' '$f3' | sed -n '3p' | grep -q '^ toolchain: stable$' && grep -A1 '0000000000000000000000000000000000000006' '$f3' | sed -n '2p' | grep -q '^ - uses: dtolnay/rust-toolchain@1.85.0$'" +check "a tag ref that starts with hex digits (@1.85.0) is a tag: untouched" \ + "grep -A1 'rust-toolchain@1.85.0' '$f3' | sed -n '2p' | grep -q '^ -$'" +check "a bare - opener: a with: toolchain: above uses: counts, nothing inserted" \ + "grep -A1 '0000000000000000000000000000000000000007' '$f3' | sed -n '2p' | grep -q '^ -$'" +check "a bare - opener with a bare uses: gets with: at the keys' column" \ + "grep -A2 '0000000000000000000000000000000000000008' '$f3' | sed -n '2p' | grep -q '^ with:$' && grep -A2 '0000000000000000000000000000000000000008' '$f3' | sed -n '3p' | grep -q '^ toolchain: stable$'" +check "exactly three steps were fixed and the run exits 0" \ + "[ \$rc_tc3 -eq 0 ] && [ \$(grep -c 'toolchain: stable' '$f3') -eq 3 ] && grep -q 'toolchain: nightly' '$f3' && grep -q 'cargo test' '$f3'" +cp "$f3" "$fx/tc3-after1.yml" +bash "$fleet/scripts/fix-rust-toolchain-input.sh" "$fx/tc3" >/dev/null 2>&1 +check "second run over these cases is a no-op" "cmp -s '$fx/tc3-after1.yml' '$f3'" +if command -v mawk >/dev/null; then + mkdir -p "$fx/mawk-bin" "$fx/tc3m/.github/workflows"; ln -sf "$(command -v mawk)" "$fx/mawk-bin/awk" + cp "$fx/tc3-before.yml" "$fx/tc3m/.github/workflows/rust.yml" + PATH="$fx/mawk-bin:$PATH" bash "$fleet/scripts/fix-rust-toolchain-input.sh" "$fx/tc3m" >/dev/null 2>&1 || true + check "under mawk the same cases produce the same file" "cmp -s '$fx/tc3-after1.yml' '$fx/tc3m/.github/workflows/rust.yml'" +fi +# A `toolchain: stable` line inside ANOTHER input's block scalar is text, not +# an input: the step still gets its own toolchain, as the first child of with:. +mkdir -p "$fx/tc5/.github/workflows" +printf 'jobs:\n a:\n steps:\n - uses: dtolnay/rust-toolchain@000000000000000000000000000000000000000b\n with:\n components: |\n toolchain: stable\n' > "$fx/tc5/.github/workflows/x.yml" +f5="$fx/tc5/.github/workflows/x.yml" +bash "$fleet/scripts/fix-rust-toolchain-input.sh" "$fx/tc5" >/dev/null 2>&1 +check "toolchain: inside another input's block scalar does not count: a real input is added first" \ + "grep -A2 '000000000000000000000000000000000000000b' '$f5' | sed -n '3p' | grep -q '^ toolchain: stable$' && grep -q '^ toolchain: stable$' '$f5' && [ \$(grep -c 'toolchain: stable' '$f5') -eq 2 ]" +# A flow `with: {` spread over several lines is read to its closing brace: +# a toolchain inside it counts; without one it gets the input as first entry. +# A list item whose parent is not `steps:` — a fixture kept in a top-level +# `env:` block scalar, even one that spells out `steps:` inside — is text. +mkdir -p "$fx/tc6/.github/workflows" +cat > "$fx/tc6/.github/workflows/x.yml" <<'YML' +env: + FIXTURE: | + jobs: + a: + steps: + - uses: dtolnay/rust-toolchain@000000000000000000000000000000000000000c +jobs: + a: + steps: + - uses: dtolnay/rust-toolchain@000000000000000000000000000000000000000d + with: { + toolchain: nightly, + } + - uses: dtolnay/rust-toolchain@000000000000000000000000000000000000000e + with: { + components: clippy, + } + - uses: dtolnay/rust-toolchain@000000000000000000000000000000000000000f +YML +f6="$fx/tc6/.github/workflows/x.yml" +set +e +bash "$fleet/scripts/fix-rust-toolchain-input.sh" "$fx/tc6" >/dev/null 2>&1; rc_tc6=$? +set -e +check "a fixture in a top-level env: block scalar is text: its dtolnay line is untouched, nothing inserted" \ + "grep -A1 '000000000000000000000000000000000000000c' '$f6' | sed -n '2p' | grep -q '^jobs:$' && grep -q '^ - uses: dtolnay/rust-toolchain@000000000000000000000000000000000000000c$' '$f6'" +check "a multi-line flow with: naming a toolchain counts: untouched" \ + "grep -A1 '000000000000000000000000000000000000000d' '$f6' | sed -n '2p' | grep -q '^ with: {$' && [ \$(grep -c 'toolchain: nightly' '$f6') -eq 1 ]" +check "a multi-line flow with: without a toolchain gets it as its first entry" \ + "grep -A1 '000000000000000000000000000000000000000e' '$f6' | sed -n '2p' | grep -q '^ with: { toolchain: stable,$' && grep -A2 '000000000000000000000000000000000000000e' '$f6' | sed -n '3p' | grep -q '^ components: clippy,$'" +check "the real steps were fixed, the fixture was not, exit 0" \ + "[ \$rc_tc6 -eq 0 ] && [ \$(grep -c 'toolchain: stable' '$f6') -eq 2 ] && grep -A2 '000000000000000000000000000000000000000f' '$f6' | sed -n '2p' | grep -q '^ with:$'" +cp "$f6" "$fx/tc6-after1.yml" +bash "$fleet/scripts/fix-rust-toolchain-input.sh" "$fx/tc6" >/dev/null 2>&1 +check "second run over these cases is a no-op" "cmp -s '$fx/tc6-after1.yml' '$f6'" +# An expression inside a multi-line flow with: (`${{ matrix.component }}`) +# opens and closes its own braces: the mapping ends when the depth returns to +# zero, so a toolchain written after the expression still counts. +mkdir -p "$fx/tc7/.github/workflows" +cat > "$fx/tc7/.github/workflows/x.yml" <<'YML' +jobs: + a: + steps: + - uses: dtolnay/rust-toolchain@0000000000000000000000000000000000000010 + with: { + components: ${{ matrix.component }}, + toolchain: nightly, + } + - uses: dtolnay/rust-toolchain@0000000000000000000000000000000000000011 + with: { + components: ${{ matrix.component }}, + } +YML +f7="$fx/tc7/.github/workflows/x.yml" +set +e +bash "$fleet/scripts/fix-rust-toolchain-input.sh" "$fx/tc7" >/dev/null 2>&1; rc_tc7=$? +set -e +check "a toolchain after an expression's braces in a multi-line flow with: counts: untouched" \ + "grep -A1 '0000000000000000000000000000000000000010' '$f7' | sed -n '2p' | grep -q '^ with: {$' && [ \$(grep -c 'toolchain: nightly' '$f7') -eq 1 ]" +check "the same shape without a toolchain gets it as first entry, the expression kept" \ + "[ \$rc_tc7 -eq 0 ] && grep -A1 '0000000000000000000000000000000000000011' '$f7' | sed -n '2p' | grep -q '^ with: { toolchain: stable,$' && [ \$(grep -c 'components: \${{ matrix.component }},' '$f7') -eq 2 ] && [ \$(grep -c 'toolchain: stable' '$f7') -eq 1 ]" +# A `with:` whose value the parser does not read (an anchor, an alias) and a +# one-line `steps: [ { uses: ... } ]` are refused out loud, file untouched. +mkdir -p "$fx/tc8/.github/workflows" "$fx/tc9/.github/workflows" +cat > "$fx/tc8/.github/workflows/x.yml" <<'YML' +jobs: + a: + steps: + - uses: dtolnay/rust-toolchain@0000000000000000000000000000000000000012 + with: &rust-inputs + components: clippy + - uses: dtolnay/rust-toolchain@0000000000000000000000000000000000000013 + with: *rust-inputs +YML +printf 'jobs:\n a:\n steps: [ { uses: dtolnay/rust-toolchain@0000000000000000000000000000000000000014 } ]\n' > "$fx/tc9/.github/workflows/x.yml" +cp "$fx/tc8/.github/workflows/x.yml" "$fx/tc8-before.yml"; cp "$fx/tc9/.github/workflows/x.yml" "$fx/tc9-before.yml" +set +e +out8=$(bash "$fleet/scripts/fix-rust-toolchain-input.sh" "$fx/tc8" 2>&1); rc_tc8=$? +out9=$(bash "$fleet/scripts/fix-rust-toolchain-input.sh" "$fx/tc9" 2>&1); rc_tc9=$? +set -e +check "with: &anchor and with: *alias are UNSUPPORTED (no second with:), file untouched, exit 2" \ + "[ \$rc_tc8 -eq 2 ] && grep -q 'UNSUPPORTED.*line(s): 5 8' <<<\"\$out8\" && cmp -s '$fx/tc8-before.yml' '$fx/tc8/.github/workflows/x.yml'" +check "a one-line steps: [ { uses: ... } ] is UNSUPPORTED, file untouched, exit 2, never OK" \ + "[ \$rc_tc9 -eq 2 ] && grep -q 'UNSUPPORTED.*line(s): 3' <<<\"\$out9\" && ! grep -q '^OK' <<<\"\$out9\" && cmp -s '$fx/tc9-before.yml' '$fx/tc9/.github/workflows/x.yml'" +# A flow-style step: refused out loud, file untouched, exit 2 — never "OK". +mkdir -p "$fx/tc4/.github/workflows" +printf 'jobs:\n a:\n steps:\n - { name: toolchain, uses: dtolnay/rust-toolchain@0000000000000000000000000000000000000009 }\n - uses: dtolnay/rust-toolchain@000000000000000000000000000000000000000a\n' > "$fx/tc4/.github/workflows/x.yml" +cp "$fx/tc4/.github/workflows/x.yml" "$fx/tc4-before.yml" +set +e +out4=$(bash "$fleet/scripts/fix-rust-toolchain-input.sh" "$fx/tc4" 2>&1); rc_tc4=$? +set -e +check "a flow-style step is UNSUPPORTED: named with its line, file untouched, exit 2, never OK" \ + "[ \$rc_tc4 -eq 2 ] && grep -q 'UNSUPPORTED.*line(s): 4' <<<\"\$out4\" && ! grep -q '^OK' <<<\"\$out4\" && cmp -s '$fx/tc4-before.yml' '$fx/tc4/.github/workflows/x.yml'" + +# Custom toolchains must work with empty mappings, quoted keys, reordered +# inputs, .yaml files and paths containing spaces. Existing expressions stay. +tc_custom="$fx/tc custom/.github/workflows" +mkdir -p "$tc_custom" +cat > "$tc_custom/rust.yaml" <<'YML' +jobs: + check: + steps: + - 'uses': 'dtolnay/rust-toolchain@aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' + 'with': { } + - uses: dtolnay/rust-toolchain@bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb + with: # intentionally empty + - uses: dtolnay/rust-toolchain@cccccccccccccccccccccccccccccccccccccccc + with: + toolchain: ${{ matrix.rust }} + - with: {} + uses: dtolnay/rust-toolchain@dddddddddddddddddddddddddddddddddddddddd +YML +cat > "$fx/tc-custom-expected.yaml" <<'YML' +jobs: + check: + steps: + - 'uses': 'dtolnay/rust-toolchain@aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' + 'with': { toolchain: 1.85.0 } + - uses: dtolnay/rust-toolchain@bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb + with: # intentionally empty + toolchain: 1.85.0 + - uses: dtolnay/rust-toolchain@cccccccccccccccccccccccccccccccccccccccc + with: + toolchain: ${{ matrix.rust }} + - with: { toolchain: 1.85.0 } + uses: dtolnay/rust-toolchain@dddddddddddddddddddddddddddddddddddddddd +YML +run_fix env TOOLCHAIN=1.85.0 bash "$fleet/scripts/fix-rust-toolchain-input.sh" "$fx/tc custom" +check "custom toolchain fills empty inputs and preserves expressions, quotes and comments exactly" \ + "[ \$rc -eq 0 ] && cmp -s '$fx/tc-custom-expected.yaml' '$tc_custom/rust.yaml'" +run_fix env TOOLCHAIN=beta bash "$fleet/scripts/fix-rust-toolchain-input.sh" "$tc_custom/rust.yaml" +check "direct-file mode preserves existing inputs even when the requested toolchain changes" \ + "[ \$rc -eq 0 ] && cmp -s '$fx/tc-custom-expected.yaml' '$tc_custom/rust.yaml'" + +# An unsupported file must not prevent a supported sibling from being fixed, +# and a sibling's success must not hide the overall error. Nested files and +# files without a YAML extension are outside repository-mode discovery. +tc_mixed="$fx/tc-mixed/.github/workflows" +mkdir -p "$tc_mixed/nested" +cp "$fx/tc4-before.yml" "$tc_mixed/a-unsupported.yml" +printf 'jobs:\n check:\n steps:\n - uses: dtolnay/rust-toolchain@aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\n' > "$tc_mixed/z-supported.yaml" +cp "$tc_mixed/z-supported.yaml" "$tc_mixed/nested/ignored.yml" +cp "$tc_mixed/z-supported.yaml" "$tc_mixed/ignored.txt" +cp "$tc_mixed/z-supported.yaml" "$fx/tc-mixed-before.yaml" +run_fix bash "$fleet/scripts/fix-rust-toolchain-input.sh" "$fx/tc-mixed" +check "repository mode fixes a supported .yaml sibling while returning UNSUPPORTED for another file" \ + "[ \$rc -eq 2 ] && grep -q 'UNSUPPORTED.*a-unsupported.yml' <<<\"\$out\" && grep -q '^ toolchain: stable$' '$tc_mixed/z-supported.yaml' && cmp -s '$fx/tc4-before.yml' '$tc_mixed/a-unsupported.yml'" +check "repository mode leaves nested workflows and non-YAML files byte-identical" \ + "cmp -s '$fx/tc-mixed-before.yaml' '$tc_mixed/nested/ignored.yml' && cmp -s '$fx/tc-mixed-before.yaml' '$tc_mixed/ignored.txt'" + +# tc10: the key present with no value (`toolchain:`, `null`, `~`) is filled +# where it stands — block or flow, inline or on a continuation line — never +# written a second time; a trailing comment survives. +mkdir -p "$fx/tc10/.github/workflows" +cat > "$fx/tc10/.github/workflows/x.yml" <<'YML' +jobs: + check: + steps: + - uses: dtolnay/rust-toolchain@6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772 + with: + toolchain: + components: clippy + - uses: dtolnay/rust-toolchain@6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772 + with: + components: clippy + toolchain: null # chosen at runtime + - uses: dtolnay/rust-toolchain@6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772 + with: { toolchain: , components: clippy } + - uses: dtolnay/rust-toolchain@6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772 + with: { components: clippy, toolchain: ~ } + - uses: dtolnay/rust-toolchain@6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772 + with: { + toolchain: null, + components: clippy, + } +YML +cat > "$fx/tc10-expected.yml" <<'YML' +jobs: + check: + steps: + - uses: dtolnay/rust-toolchain@6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772 + with: + toolchain: stable + components: clippy + - uses: dtolnay/rust-toolchain@6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772 + with: + components: clippy + toolchain: stable # chosen at runtime + - uses: dtolnay/rust-toolchain@6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772 + with: { toolchain: stable, components: clippy } + - uses: dtolnay/rust-toolchain@6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772 + with: { components: clippy, toolchain: stable } + - uses: dtolnay/rust-toolchain@6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772 + with: { + toolchain: stable, + components: clippy, + } +YML +run_fix bash "$fleet/scripts/fix-rust-toolchain-input.sh" "$fx/tc10" +check "an empty toolchain: input is filled in place in every shape, never duplicated" \ + "[ \$rc -eq 0 ] && cmp -s '$fx/tc10-expected.yml' '$fx/tc10/.github/workflows/x.yml' && [ \$(grep -c 'toolchain:' '$fx/tc10/.github/workflows/x.yml') -eq 5 ]" +run_fix bash "$fleet/scripts/fix-rust-toolchain-input.sh" "$fx/tc10" +check "filled inputs are inputs: the second run is byte-identical and OK" \ + "[ \$rc -eq 0 ] && cmp -s '$fx/tc10-expected.yml' '$fx/tc10/.github/workflows/x.yml' && grep -q '^OK' <<<\"\$out\"" + +# tc11: an empty `toolchain:` with a mapping nested under it is not an empty +# value; the fixer refuses rather than write a value over a mapping. +mkdir -p "$fx/tc11/.github/workflows" +printf 'jobs:\n check:\n steps:\n - uses: dtolnay/rust-toolchain@6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772\n with:\n toolchain:\n channel: stable\n' > "$fx/tc11/.github/workflows/x.yml" +cp "$fx/tc11/.github/workflows/x.yml" "$fx/tc11-before.yml" +run_fix bash "$fleet/scripts/fix-rust-toolchain-input.sh" "$fx/tc11" +check "a toolchain: key with a nested mapping is UNSUPPORTED: file untouched, exit 2" \ + "[ \$rc -eq 2 ] && grep -q 'UNSUPPORTED.*line(s): 6' <<<\"\$out\" && cmp -s '$fx/tc11-before.yml' '$fx/tc11/.github/workflows/x.yml'" + +# tc12: braces inside quoted scalars and comments are text. A `"}"` value or +# a `# }` comment inside a multi-line flow `with:` must not end the mapping +# early (and hide a later toolchain, or misplace the insertion). +mkdir -p "$fx/tc12/.github/workflows" +cat > "$fx/tc12/.github/workflows/x.yml" <<'YML' +jobs: + check: + steps: + - uses: dtolnay/rust-toolchain@6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772 + with: { + components: "}", # a } in a comment + toolchain: nightly, + } + - uses: dtolnay/rust-toolchain@6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772 + with: { + components: '{', + } +YML +cat > "$fx/tc12-expected.yml" <<'YML' +jobs: + check: + steps: + - uses: dtolnay/rust-toolchain@6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772 + with: { + components: "}", # a } in a comment + toolchain: nightly, + } + - uses: dtolnay/rust-toolchain@6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772 + with: { toolchain: stable, + components: '{', + } +YML +run_fix bash "$fleet/scripts/fix-rust-toolchain-input.sh" "$fx/tc12" +check "quoted braces and comment braces do not close a flow with: — the named toolchain is seen, the missing one added once" \ + "[ \$rc -eq 0 ] && cmp -s '$fx/tc12-expected.yml' '$fx/tc12/.github/workflows/x.yml'" + +# tc13: a ref that merely begins with 40 hex characters (`@-patched`) is +# a tag, carries its own channel, and is not ours. +mkdir -p "$fx/tc13/.github/workflows" +printf 'jobs:\n check:\n steps:\n - uses: dtolnay/rust-toolchain@6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772-patched\n - uses: dtolnay/rust-toolchain@6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772 # pinned\n' > "$fx/tc13/.github/workflows/x.yml" +run_fix bash "$fleet/scripts/fix-rust-toolchain-input.sh" "$fx/tc13" +check "a tag beginning with a SHA is a tag: untouched; the real SHA pin beside it is fixed" \ + "[ \$rc -eq 0 ] && sed -n '4p' '$fx/tc13/.github/workflows/x.yml' | grep -q 'patched\$' && sed -n '5p' '$fx/tc13/.github/workflows/x.yml' | grep -q '# pinned\$' && [ \$(grep -c 'toolchain: stable' '$fx/tc13/.github/workflows/x.yml') -eq 1 ] && sed -n '6,7p' '$fx/tc13/.github/workflows/x.yml' | grep -q '^ toolchain: stable$'" + +# tc14: inputs are read at the `with:` mapping's own level. `toolchain:` +# text inside a quoted value or a nested mapping is not an input; a quoted +# toolchain value still is. +mkdir -p "$fx/tc14/.github/workflows" +cat > "$fx/tc14/.github/workflows/x.yml" <<'YML' +jobs: + check: + steps: + - uses: dtolnay/rust-toolchain@6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772 + with: { components: "clippy, toolchain: nightly" } + - uses: dtolnay/rust-toolchain@6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772 + with: { env: { toolchain: nightly } } + - uses: dtolnay/rust-toolchain@6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772 + with: { + env: { + toolchain: nightly, + }, + } + - uses: dtolnay/rust-toolchain@6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772 + with: { components: "clippy", toolchain: "1.85.0" } +YML +run_fix bash "$fleet/scripts/fix-rust-toolchain-input.sh" "$fx/tc14" +check "toolchain: inside a quoted value or a nested mapping is not an input: the three steps are fixed, the quoted real one is left" \ + "[ \$rc -eq 0 ] && [ \$(grep -c 'toolchain: stable' '$fx/tc14/.github/workflows/x.yml') -eq 3 ] && sed -n '5p' '$fx/tc14/.github/workflows/x.yml' | grep -q '^ with: { toolchain: stable, components: \"clippy, toolchain: nightly\" }$' && sed -n '7p' '$fx/tc14/.github/workflows/x.yml' | grep -q '^ with: { toolchain: stable, env: { toolchain: nightly } }$' && sed -n '9p' '$fx/tc14/.github/workflows/x.yml' | grep -q '^ with: { toolchain: stable,$' && sed -n '15p' '$fx/tc14/.github/workflows/x.yml' | grep -q 'toolchain: \"1.85.0\" }$'" + +# Ref length is part of the contract: abbreviated and overlong hex refs are +# not full SHA pins, and another action at the same SHA is not dtolnay. +tc_boundary="$fx/tc-boundary/.github/workflows" +mkdir -p "$tc_boundary" +cat > "$tc_boundary/ci.yml" <<'YML' +jobs: + check: + steps: + - uses: dtolnay/rust-toolchain@aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + - uses: dtolnay/rust-toolchain@aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + - uses: other/rust-toolchain@aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + - uses: dtolnay/rust-toolchain@aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +YML +cat > "$fx/tc-boundary-expected.yml" <<'YML' +jobs: + check: + steps: + - uses: dtolnay/rust-toolchain@aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + - uses: dtolnay/rust-toolchain@aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + - uses: other/rust-toolchain@aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + - uses: dtolnay/rust-toolchain@aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + with: + toolchain: stable +YML +run_fix bash "$fleet/scripts/fix-rust-toolchain-input.sh" "$fx/tc-boundary" +check "only the exact 40-hex dtolnay pin is repaired, including the last step at EOF" \ + "[ \$rc -eq 0 ] && cmp -s '$fx/tc-boundary-expected.yml' '$tc_boundary/ci.yml'" +run_fix bash "$fleet/scripts/fix-rust-toolchain-input.sh" "$tc_boundary/ci.yml" +check "SHA boundary controls remain unchanged on a second direct-file run" \ + "[ \$rc -eq 0 ] && cmp -s '$fx/tc-boundary-expected.yml' '$tc_boundary/ci.yml' && ! grep -q '^FIXED' <<<\"\$out\"" + +# Refusing an unsupported step is atomic for its file, even when a preceding +# supported step could be repaired. No partial output may replace the file. +tc_atomic="$fx/tc-atomic/.github/workflows" +mkdir -p "$tc_atomic" +cat > "$tc_atomic/ci.yml" <<'YML' +jobs: + check: + steps: + - uses: dtolnay/rust-toolchain@aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + - { uses: dtolnay/rust-toolchain@bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb } +YML +cp "$tc_atomic/ci.yml" "$fx/tc-atomic-before.yml" +run_fix bash "$fleet/scripts/fix-rust-toolchain-input.sh" "$fx/tc-atomic" +check "an unsupported step prevents earlier supported repairs in the same file" \ + "[ \$rc -eq 2 ] && grep -q 'UNSUPPORTED.*line(s): 5' <<<\"\$out\" && ! grep -q '^FIXED' <<<\"\$out\" && cmp -s '$fx/tc-atomic-before.yml' '$tc_atomic/ci.yml'" + +# Indentless step sequences and a following job use different key columns. +# A folded run scalar is data; a quoted null input in the next job is filled. +tc_indent="$fx/tc-indent/.github/workflows" +mkdir -p "$tc_indent" +cat > "$tc_indent/ci.yaml" <<'YML' +jobs: + first: + steps: + - with: + components: clippy + uses: dtolnay/rust-toolchain@aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + - run: >- + echo dtolnay/rust-toolchain@bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb + second: + steps: + - uses: dtolnay/rust-toolchain@cccccccccccccccccccccccccccccccccccccccc + with: + 'toolchain': null # keep this comment +YML +cat > "$fx/tc-indent-expected.yaml" <<'YML' +jobs: + first: + steps: + - with: + toolchain: stable + components: clippy + uses: dtolnay/rust-toolchain@aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + - run: >- + echo dtolnay/rust-toolchain@bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb + second: + steps: + - uses: dtolnay/rust-toolchain@cccccccccccccccccccccccccccccccccccccccc + with: + 'toolchain': stable # keep this comment +YML +run_fix bash "$fleet/scripts/fix-rust-toolchain-input.sh" "$fx/tc-indent" +check "indentless steps and a following job get inputs at their own key columns, preserving scalar data" \ + "[ \$rc -eq 0 ] && cmp -s '$fx/tc-indent-expected.yaml' '$tc_indent/ci.yaml'" + +# ── 2. fix-stale-reusable-pin.sh ───────────────────────────────────────────── +STALE_A=d135b05bfc647d0c0fbfedc7e80f37ea50f49236 # Window A (table row for rust-ci-reusable only) +STALE_B=8f2ee50841e216cd8c192eeb68953118190f105c # Window B (table row for every standards reusable) +GOOD=75bd5994470661a099df562d6d5ad2fe744dbd66 +mkdir -p "$fx/sp/.github/workflows" +printf 'jobs:\n rust-ci:\n uses: hyperpolymath/standards/.github/workflows/rust-ci-reusable.yml@%s\n' "$STALE_A" > "$fx/sp/.github/workflows/rust-ci.yml" +printf 'jobs:\n governance:\n uses: hyperpolymath/standards/.github/workflows/governance-reusable.yml@%s\n' "$STALE_B" > "$fx/sp/.github/workflows/governance.yml" +printf 'jobs:\n mirror:\n uses: hyperpolymath/standards/.github/workflows/mirror-reusable.yml@%s\n' "$STALE_A" > "$fx/sp/.github/workflows/mirror.yml" +printf 'jobs:\n audit:\n uses: hyperpolymath/cicd-suite/.github/workflows/main-estate-audit.yml@%s\n' "$STALE_B" > "$fx/sp/.github/workflows/audit.yml" +cat > "$fx/sp/.github/workflows/actions.lock" <&1); rc_sp=$? +set -e +check "Window A pin of rust-ci-reusable is moved to the replacement" "grep -q \"rust-ci-reusable.yml@$GOOD\" '$fx/sp/.github/workflows/rust-ci.yml'" +check "Window B pin of any standards reusable is moved (wildcard row)" "grep -q \"governance-reusable.yml@$GOOD\" '$fx/sp/.github/workflows/governance.yml'" +check "a Window-A SHA on a reusable the window does not cover is left alone" "grep -q \"mirror-reusable.yml@$STALE_A\" '$fx/sp/.github/workflows/mirror.yml'" +check "a cicd-suite pin that happens to share the SHA is not a standards pin and is untouched" "grep -q \"main-estate-audit.yml@$STALE_B\" '$fx/sp/.github/workflows/audit.yml'" +check "the lock's workflows entry and record follow the pin" "[ \$(grep -c \"$GOOD\" '$fx/sp/.github/workflows/actions.lock') -eq 4 ] && ! grep -q \"$STALE_B\" '$fx/sp/.github/workflows/actions.lock'" +check "touching the lock is said out loud and exits 3, never a clean success" "[ \$rc_sp -eq 3 ] && grep -q 'nested uses: list was NOT recomputed' <<<\"\$out\"" +tar -C "$fx/sp" -cf "$fx/sp1.tar" .github +bash "$fleet/scripts/fix-stale-reusable-pin.sh" "$fx/sp" >/dev/null 2>&1; rc_sp2=$? +tar -C "$fx/sp" -cf "$fx/sp2.tar" .github +check "second run is a no-op and exits 0 (nothing left to say about the lock)" "[ \$rc_sp2 -eq 0 ] && cmp -s '$fx/sp1.tar' '$fx/sp2.tar'" +# Planted control: an empty table rewrites nothing. +printf '# empty\n' > "$fx/empty.tsv" +mkdir -p "$fx/sp0/.github/workflows"; printf 'jobs:\n rust-ci:\n uses: hyperpolymath/standards/.github/workflows/rust-ci-reusable.yml@%s\n' "$STALE_A" > "$fx/sp0/.github/workflows/rust-ci.yml" +PINS_TABLE="$fx/empty.tsv" bash "$fleet/scripts/fix-stale-reusable-pin.sh" "$fx/sp0" >/dev/null 2>&1; rc_sp0=$? +check "control: with no table rows nothing is rewritten and the run exits 0" "[ \$rc_sp0 -eq 0 ] && grep -q \"@$STALE_A\" '$fx/sp0/.github/workflows/rust-ci.yml'" +check "the shipped table names both windows' edges and never lists the replacement as stale" \ + "grep -q \"rust-ci-reusable.yml.$STALE_A\" '$fleet/scripts/lib/stale-reusable-pins.tsv' && grep -q \"workflows/\\*.$STALE_B\" '$fleet/scripts/lib/stale-reusable-pins.tsv' && ! awk -F'\\t' -v g=\"$GOOD\" '\$2 == g' '$fleet/scripts/lib/stale-reusable-pins.tsv' | grep -q ." +# Two callers at two different dead SHAs that move to the SAME replacement must +# leave ONE record for it in the lock, with the union of both nested lists. +mkdir -p "$fx/sp2/.github/workflows" +printf 'jobs:\n governance:\n uses: hyperpolymath/standards/.github/workflows/governance-reusable.yml@%s\n' "$STALE_B" > "$fx/sp2/.github/workflows/governance.yml" +STALE_C=da2c748aad55c1a1dcba00b60fe4a35017bc6540 +printf 'jobs:\n scorecard:\n uses: hyperpolymath/standards/.github/workflows/scorecard-reusable.yml@%s\n' "$STALE_C" > "$fx/sp2/.github/workflows/scorecard.yml" +cat > "$fx/sp2/.github/workflows/actions.lock" </dev/null 2>&1 || true +check "two dead SHAs moving to one replacement leave ONE record for it" "[ \$(grep -c \"^ 'hyperpolymath/standards@$GOOD':\" '$fx/sp2/.github/workflows/actions.lock') -eq 1 ]" +check "the merged record carries the union of both nested lists, once each" \ + "[ \$(grep -c \" - 'actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1'\" '$fx/sp2/.github/workflows/actions.lock') -eq 1 ] && grep -q 'erlef/setup-beam' '$fx/sp2/.github/workflows/actions.lock' && grep -q 'ossf/scorecard-action' '$fx/sp2/.github/workflows/actions.lock'" +check "both workflows: entries point at the replacement" "[ \$(grep -c \" - 'hyperpolymath/standards@$GOOD'\" '$fx/sp2/.github/workflows/actions.lock') -eq 2 ]" + +check "the merged record carries its header once (one ref: line)" \ + "[ \$(grep -c \" ref: '$GOOD'\" '$fx/sp2/.github/workflows/actions.lock') -eq 1 ] && [ \$(grep -c \" commit: 'sha1-$GOOD'\" '$fx/sp2/.github/workflows/actions.lock') -eq 1 ]" +# Partial migration (review): two callers share a Window-A SHA but the table +# row covers only rust-ci-reusable. The lock must follow the rewritten caller +# only: mirror's edge and the stale record stay, the replacement gets its own. +mkdir -p "$fx/sp3/.github/workflows" +printf 'jobs:\n rust-ci:\n uses: hyperpolymath/standards/.github/workflows/rust-ci-reusable.yml@%s\n' "$STALE_A" > "$fx/sp3/.github/workflows/rust-ci.yml" +printf 'jobs:\n mirror:\n uses: hyperpolymath/standards/.github/workflows/mirror-reusable.yml@%s\n' "$STALE_A" > "$fx/sp3/.github/workflows/mirror.yml" +cat > "$fx/sp3/.github/workflows/actions.lock" <&1); rc_sp3=$? +set -e +l3="$fx/sp3/.github/workflows/actions.lock" +check "partial migration: the rewritten caller's lock edge moves" \ + "grep -A1 \"'.github/workflows/rust-ci.yml':\" '$l3' | grep -q \"hyperpolymath/standards@$GOOD\"" +check "partial migration: the untouched caller's lock edge stays at the stale SHA" \ + "grep -A1 \"'.github/workflows/mirror.yml':\" '$l3' | grep -q \"hyperpolymath/standards@$STALE_A\" && grep -q \"mirror-reusable.yml@$STALE_A\" '$fx/sp3/.github/workflows/mirror.yml'" +check "partial migration: both dependency records exist, each with its own ref and commit" \ + "[ \$(grep -c \"^ 'hyperpolymath/standards@\" '$l3') -eq 2 ] && grep -q \"ref: '$GOOD'\" '$l3' && grep -q \"ref: '$STALE_A'\" '$l3' && grep -q \"commit: 'sha1-$GOOD'\" '$l3'" +check "partial migration is said out loud and exits 3" "[ \$rc_sp3 -eq 3 ] && grep -q 'keeps the record for' <<<\"\$out3\"" +tar -C "$fx/sp3" -cf "$fx/sp3a.tar" .github +bash "$fleet/scripts/fix-stale-reusable-pin.sh" "$fx/sp3" >/dev/null 2>&1; rc_sp3b=$? +tar -C "$fx/sp3" -cf "$fx/sp3b.tar" .github +check "partial migration: second run is a no-op and exits 0" "[ \$rc_sp3b -eq 0 ] && cmp -s '$fx/sp3a.tar' '$fx/sp3b.tar'" +# A quoted key and value are still a pin. +mkdir -p "$fx/sp4/.github/workflows" +printf 'jobs:\n governance:\n "uses": "hyperpolymath/standards/.github/workflows/governance-reusable.yml@%s"\n' "$STALE_B" > "$fx/sp4/.github/workflows/governance.yml" +bash "$fleet/scripts/fix-stale-reusable-pin.sh" "$fx/sp4" >/dev/null 2>&1; rc_sp4=$? +check "a quoted \"uses\" key with a quoted value is rewritten" "[ \$rc_sp4 -eq 0 ] && grep -q \"\\\"hyperpolymath/standards/.github/workflows/governance-reusable.yml@$GOOD\\\"\" '$fx/sp4/.github/workflows/governance.yml'" +# Review cases (Codex, gitbot-fleet#615): a commented-out caller is not a +# caller; a workflow calling two reusables of one repository at the same dead +# SHA, only one covered by the table, keeps the stale lock edge beside the new. +mkdir -p "$fx/sp5/.github/workflows" +printf 'jobs:\n mirror:\n uses: hyperpolymath/standards/.github/workflows/mirror-reusable.yml@%s\n # rust-ci:\n # uses: hyperpolymath/standards/.github/workflows/rust-ci-reusable.yml@%s\n' "$STALE_A" "$STALE_A" > "$fx/sp5/.github/workflows/ci.yml" +cat > "$fx/sp5/.github/workflows/actions.lock" </dev/null 2>&1; rc_sp5=$? +set -e +tar -C "$fx/sp5" -cf "$fx/sp5b.tar" .github +check "a commented-out caller of a covered reusable moves nothing: workflow and lock byte-identical, exit 0" \ + "[ \$rc_sp5 -eq 0 ] && cmp -s '$fx/sp5a.tar' '$fx/sp5b.tar'" +mkdir -p "$fx/sp6/.github/workflows" +printf 'jobs:\n rust-ci:\n uses: hyperpolymath/standards/.github/workflows/rust-ci-reusable.yml@%s\n mirror:\n uses: hyperpolymath/standards/.github/workflows/mirror-reusable.yml@%s\n' "$STALE_A" "$STALE_A" > "$fx/sp6/.github/workflows/ci.yml" +sed 's#sp5#sp6#' "$fx/sp5/.github/workflows/actions.lock" > "$fx/sp6/.github/workflows/actions.lock" +set +e +bash "$fleet/scripts/fix-stale-reusable-pin.sh" "$fx/sp6" >/dev/null 2>&1; rc_sp6=$? +set -e +l6="$fx/sp6/.github/workflows/actions.lock" +check "two reusables of one repository at one dead SHA, one covered: the covered pin moves, the other stays" \ + "grep -q \"rust-ci-reusable.yml@$GOOD\" '$fx/sp6/.github/workflows/ci.yml' && grep -q \"mirror-reusable.yml@$STALE_A\" '$fx/sp6/.github/workflows/ci.yml'" +check "the workflow's lock entry then carries BOTH edges, the stale one kept beside the new" \ + "grep -A2 \"'.github/workflows/ci.yml':\" '$l6' | grep -q \"hyperpolymath/standards@$STALE_A\" && grep -A2 \"'.github/workflows/ci.yml':\" '$l6' | grep -q \"hyperpolymath/standards@$GOOD\"" +check "both dependency records exist, each with its ref, and the run exits 3" \ + "[ \$rc_sp6 -eq 3 ] && grep -q \"ref: '$GOOD'\" '$l6' && grep -q \"ref: '$STALE_A'\" '$l6'" +tar -C "$fx/sp6" -cf "$fx/sp6a.tar" .github +bash "$fleet/scripts/fix-stale-reusable-pin.sh" "$fx/sp6" >/dev/null 2>&1; rc_sp6b=$? +tar -C "$fx/sp6" -cf "$fx/sp6b.tar" .github +check "kept-edge case: second run is a no-op and exits 0" "[ \$rc_sp6b -eq 0 ] && cmp -s '$fx/sp6a.tar' '$fx/sp6b.tar'" +# A flow-style job mapping carrying a known-dead pin: the parser cannot place +# it as a `uses` key, so the file is refused out loud — never "OK". +mkdir -p "$fx/sp7/.github/workflows" +printf 'jobs:\n mirror:\n uses: hyperpolymath/standards/.github/workflows/mirror-reusable.yml@%s\n rust: { uses: hyperpolymath/standards/.github/workflows/rust-ci-reusable.yml@%s }\n' "$STALE_A" "$STALE_A" > "$fx/sp7/.github/workflows/ci.yml" +cp "$fx/sp7/.github/workflows/ci.yml" "$fx/sp7-before.yml" +set +e +out7=$(bash "$fleet/scripts/fix-stale-reusable-pin.sh" "$fx/sp7" 2>&1); rc_sp7=$? +set -e +check "a flow-style job with a known-dead pin is UNSUPPORTED: named with its line, file untouched, exit 2, never OK" \ + "[ \$rc_sp7 -eq 2 ] && grep -q 'UNSUPPORTED .github/workflows/ci.yml:4:' <<<\"\$out7\" && ! grep -q '^OK' <<<\"\$out7\" && cmp -s '$fx/sp7-before.yml' '$fx/sp7/.github/workflows/ci.yml'" +# A nested `uses:` edge of ANOTHER record still pointing at the stale SHA (a +# reusable that itself calls the stale revision) keeps the stale record alive. +mkdir -p "$fx/sp8/.github/workflows" +printf 'jobs:\n governance:\n uses: hyperpolymath/standards/.github/workflows/governance-reusable.yml@%s\n' "$STALE_B" > "$fx/sp8/.github/workflows/governance.yml" +cat > "$fx/sp8/.github/workflows/actions.lock" </dev/null 2>&1; rc_sp8=$? +set -e +l8="$fx/sp8/.github/workflows/actions.lock" +check "transitive edge: the caller's own edge moves, the other record's nested edge stays at the stale SHA" \ + "grep -A1 \"'.github/workflows/governance.yml':\" '$l8' | grep -q \"standards@$GOOD\" && grep -A6 \"'hyperpolymath/cicd-suite@1111\" '$l8' | grep -q \" - 'hyperpolymath/standards@$STALE_B'\"" +check "transitive edge: the stale record is kept beside the replacement's, exit 3" \ + "[ \$rc_sp8 -eq 3 ] && grep -q \"^ 'hyperpolymath/standards@$STALE_B':\" '$l8' && grep -q \"^ 'hyperpolymath/standards@$GOOD':\" '$l8'" +# Only a `uses:` key of a job is a call: the same text under `env:` or inside +# a `run: |` fixture is data, left byte-identical, and never moves the lock. +mkdir -p "$fx/sp9/.github/workflows" +cat > "$fx/sp9/.github/workflows/ci.yml" < caller.yml <<'EOF' + uses: hyperpolymath/standards/.github/workflows/governance-reusable.yml@$STALE_B + EOF + governance: + uses: hyperpolymath/standards/.github/workflows/governance-reusable.yml@$STALE_B +YML +cat > "$fx/sp9/.github/workflows/actions.lock" </dev/null 2>&1; rc_sp9=$? +set -e +check "only the job-level call moves; the env: copy and the run: | fixture line are untouched" \ + "sed -n '11p' '$fx/sp9/.github/workflows/ci.yml' | grep -q \"governance-reusable.yml@$GOOD\" && sed -n '4p;8p' '$fx/sp9/.github/workflows/ci.yml' | grep -c \"@$STALE_B\" | grep -qx 2" +check "the lock follows the call: edge and record at the replacement, stale gone, exit 3" \ + "[ \$rc_sp9 -eq 3 ] && grep -q \"hyperpolymath/standards@$GOOD\" '$fx/sp9/.github/workflows/actions.lock' && ! grep -q \"$STALE_B\" '$fx/sp9/.github/workflows/actions.lock'" + +# A private table selects the replacement, independent of the shipped estate +# history. A wildcard is scoped to its repository and unknown SHAs stay put. +sp_custom="$fx/sp custom/.github/workflows" +mkdir -p "$sp_custom" +printf '# fixture table\n\nhyperpolymath/example/.github/workflows/*\t%s\t%s\tfixture replacement\n' \ + "$STALE_A" "$GOOD" > "$fx/custom-pins.tsv" +cat > "$sp_custom/ci.yaml" < "$fx/sp-custom-expected.yaml" < "$fx/sp-tag/.github/workflows/ci.yaml" +cp "$fx/sp-tag/.github/workflows/ci.yaml" "$fx/sp-tag-before.yaml" +run_fix env PINS_TABLE="$fx/custom-pins.tsv" bash "$fleet/scripts/fix-stale-reusable-pin.sh" "$fx/sp-tag" +check "a tag-pinned reusable is a successful no-op (observed exit $rc)" \ + "[ \$rc -eq 0 ] && cmp -s '$fx/sp-tag-before.yaml' '$fx/sp-tag/.github/workflows/ci.yaml'" + +# Invalid configuration must fail before either the caller or its lock moves. +sp_invalid="$fx/sp-invalid/.github/workflows" +mkdir -p "$sp_invalid" +cp "$fx/sp0/.github/workflows/rust-ci.yml" "$sp_invalid/rust-ci.yml" +cp "$fx/sp5/.github/workflows/actions.lock" "$sp_invalid/actions.lock" +cp "$sp_invalid/actions.lock" "$fx/sp-invalid-before.lock" +run_fix env PINS_TABLE="$fx/missing-table.tsv" bash "$fleet/scripts/fix-stale-reusable-pin.sh" "$fx/sp-invalid" +check "a missing table exits 2 with a diagnostic and preserves the workflow and lock" \ + "[ \$rc -eq 2 ] && grep -q 'table not readable' <<<\"\$out\" && cmp -s '$fx/sp0/.github/workflows/rust-ci.yml' '$sp_invalid/rust-ci.yml' && cmp -s '$fx/sp-invalid-before.lock' '$sp_invalid/actions.lock'" +for replacement in 123456789012345678901234567890123456789 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA stable; do + printf 'hyperpolymath/standards/.github/workflows/rust-ci-reusable.yml\t%s\t%s\tinvalid replacement\n' \ + "$STALE_A" "$replacement" > "$fx/invalid-pins.tsv" + run_fix env PINS_TABLE="$fx/invalid-pins.tsv" bash "$fleet/scripts/fix-stale-reusable-pin.sh" "$fx/sp-invalid" + check "invalid replacement $replacement exits 2 without changing caller or lock" \ + "[ \$rc -eq 2 ] && grep -q 'bad replacement SHA' <<<\"\$out\" && cmp -s '$fx/sp0/.github/workflows/rust-ci.yml' '$sp_invalid/rust-ci.yml' && cmp -s '$fx/sp-invalid-before.lock' '$sp_invalid/actions.lock'" +done +run_fix env PINS_TABLE="$fx/empty.tsv" bash "$fleet/scripts/fix-stale-reusable-pin.sh" "$fx/no-workflows" +check "a repository without workflows is a successful no-op" \ + "[ \$rc -eq 0 ] && grep -q 'OK (no workflows)' <<<\"\$out\" && [ ! -e '$fx/no-workflows' ]" + +# Exit 2 takes precedence over the exit 3 warning from a successfully repaired +# sibling's lock. The unsupported workflow and its stale edge must survive. +sp_mixed="$fx/sp-mixed/.github/workflows" +mkdir -p "$sp_mixed" +cp "$fx/sp7-before.yml" "$sp_mixed/ci.yml" +cp "$fx/sp0/.github/workflows/rust-ci.yml" "$sp_mixed/rust-ci.yaml" +cat > "$sp_mixed/actions.lock" < "$fx/sp10/.github/workflows/rust-ci.yml" +cat > "$fx/sp10/.github/workflows/actions.lock" <-patched`) is another +# ref the table never named: left alone, while the real dead pin beside it moves. +mkdir -p "$fx/sp11/.github/workflows" +printf 'jobs:\n patched:\n uses: hyperpolymath/standards/.github/workflows/rust-ci-reusable.yml@%s-patched\n dead:\n uses: hyperpolymath/standards/.github/workflows/rust-ci-reusable.yml@%s\n' "$STALE_A" "$STALE_A" > "$fx/sp11/.github/workflows/ci.yml" +run_fix bash "$fleet/scripts/fix-stale-reusable-pin.sh" "$fx/sp11" +check "a tag that begins with a dead SHA is not that SHA: untouched, exit 0, the dead pin beside it moved" \ + "[ \$rc -eq 0 ] && sed -n '3p' '$fx/sp11/.github/workflows/ci.yml' | grep -q \"@$STALE_A-patched\$\" && sed -n '5p' '$fx/sp11/.github/workflows/ci.yml' | grep -q \"@$GOOD\$\" && ! grep -q 'UNSUPPORTED' <<<\"\$out\"" + +# sp12: the whole table is validated before any caller is touched: a good +# row followed by a malformed one leaves every workflow and the lock as they +# were, exit 2. +mkdir -p "$fx/sp12/.github/workflows" +printf 'jobs:\n rust:\n uses: hyperpolymath/standards/.github/workflows/rust-ci-reusable.yml@%s\n' "$STALE_A" > "$fx/sp12/.github/workflows/rust-ci.yml" +cp "$fx/sp12/.github/workflows/rust-ci.yml" "$fx/sp12-before.yml" +cp "$fx/sp5/.github/workflows/actions.lock" "$fx/sp12/.github/workflows/actions.lock" +cp "$fx/sp12/.github/workflows/actions.lock" "$fx/sp12-before.lock" +printf 'hyperpolymath/standards/.github/workflows/rust-ci-reusable.yml\t%s\t%s\tgood row\nhyperpolymath/standards/.github/workflows/*\t%s\tnot-a-sha\tbad row after a good one\n' \ + "$STALE_A" "$GOOD" "$STALE_B" > "$fx/sp12-pins.tsv" +run_fix env PINS_TABLE="$fx/sp12-pins.tsv" bash "$fleet/scripts/fix-stale-reusable-pin.sh" "$fx/sp12" +check "a malformed row anywhere in the table stops the run before any caller or lock is touched: exit 2" \ + "[ \$rc -eq 2 ] && grep -q 'table row 2: bad replacement SHA' <<<\"\$out\" && cmp -s '$fx/sp12-before.yml' '$fx/sp12/.github/workflows/rust-ci.yml' && cmp -s '$fx/sp12-before.lock' '$fx/sp12/.github/workflows/actions.lock'" + +# sp13: a lock edge is per repository@SHA. An unrelated repository that sits +# at the same SHA keeps its edge and its record when the covered one moves. +mkdir -p "$fx/sp13/.github/workflows" +printf 'jobs:\n rust:\n uses: hyperpolymath/standards/.github/workflows/rust-ci-reusable.yml@%s\n other:\n uses: octo-org/example/.github/workflows/ci.yml@%s\n' "$STALE_A" "$STALE_A" > "$fx/sp13/.github/workflows/ci.yml" +cat > "$fx/sp13/.github/workflows/actions.lock" < "$fx/sp14/.github/workflows/ci.yml" +cat > "$fx/sp14/.github/workflows/actions.lock" < "$fx/sp15-pins.tsv" +printf 'jobs:\n ci:\n uses: octo/example/.github/workflows/ci.yml@%s\n' "$STALE_A" > "$fx/sp15/.github/workflows/ci.yml" +run_fix env PINS_TABLE="$fx/sp15-pins.tsv" bash "$fleet/scripts/fix-stale-reusable-pin.sh" "$fx/sp15" +check "a table row for another owner finds and moves that owner's caller" \ + "[ \$rc -eq 0 ] && grep -q \"octo/example/.github/workflows/ci.yml@$GOOD\$\" '$fx/sp15/.github/workflows/ci.yml' && grep -q '^FIXED' <<<\"\$out\"" + +# sp16: a double-quoted lock edge moves like a single-quoted one and keeps +# its quotes; a record whose key merely CONTAINS repo@sha +# (`evilhyperpolymath/standards@`) is another record and stays. +mkdir -p "$fx/sp16/.github/workflows" +printf 'jobs:\n rust:\n uses: hyperpolymath/standards/.github/workflows/rust-ci-reusable.yml@%s\n' "$STALE_A" > "$fx/sp16/.github/workflows/ci.yml" +cat > "$fx/sp16/.github/workflows/actions.lock" < "$sp_atomic/ci.yml" < "$sp_atomic/actions.lock" < "$fx/ordered-pins.tsv" + chosen="$GOOD" + else + printf 'fixture/reusable/.github/workflows/*\t%s\t%s\twildcard first\nfixture/reusable/.github/workflows/ci.yml\t%s\t%s\texact second\n' \ + "$STALE_A" "$alternate" "$STALE_A" "$GOOD" > "$fx/ordered-pins.tsv" + chosen="$alternate" + fi + printf 'jobs:\n call:\n uses: fixture/reusable/.github/workflows/ci.yml@%s\n' "$STALE_A" > "$sp_order/ci.yml" + printf 'jobs:\n call:\n uses: fixture/reusable/.github/workflows/ci.yml@%s\n' "$chosen" > "$fx/sp-order-expected.yml" + run_fix env PINS_TABLE="$fx/ordered-pins.tsv" bash "$fleet/scripts/fix-stale-reusable-pin.sh" "$fx/sp-order" + check "overlapping table rows choose the first match when $first_match comes first" \ + "[ \$rc -eq 0 ] && cmp -s '$fx/sp-order-expected.yml' '$sp_order/ci.yml'" + run_fix env PINS_TABLE="$fx/ordered-pins.tsv" bash "$fleet/scripts/fix-stale-reusable-pin.sh" "$fx/sp-order" + check "the $first_match-first replacement is a stable no-op on the next run" \ + "[ \$rc -eq 0 ] && cmp -s '$fx/sp-order-expected.yml' '$sp_order/ci.yml' && ! grep -q '^FIXED' <<<\"\$out\"" +done + +# A replacement may already have a dependency record. Merging must preserve +# its spelling and metadata and produce a sorted union without duplicate uses. +sp_existing="$fx/sp-existing/.github/workflows" +mkdir -p "$sp_existing" +printf 'jobs:\n call:\n uses: hyperpolymath/standards/.github/workflows/rust-ci-reusable.yml@%s\n' "$STALE_A" > "$sp_existing/ci.yml" +cat > "$sp_existing/actions.lock" < "$fx/sp-existing-expected.lock" < "$fx/sp-existing-expected.yml" +check "the caller follows the existing replacement and the run reports the incomplete closure" \ + "cmp -s '$fx/sp-existing-expected.yml' '$sp_existing/ci.yml' && grep -q 'nested uses: list was NOT recomputed' <<<\"\$out\"" +run_fix bash "$fleet/scripts/fix-stale-reusable-pin.sh" "$fx/sp-existing" +check "migration into an existing record is byte-identical on the second run and exits 0" \ + "[ \$rc -eq 0 ] && cmp -s '$fx/sp-existing-expected.lock' '$sp_existing/actions.lock' && cmp -s '$fx/sp-existing-expected.yml' '$sp_existing/ci.yml'" + +# Validate stale refs and reusable paths as well as replacement refs. Invalid +# rows after a matching good row must still stop all mutations up front. +while IFS=$'\t' read -r invalid_path invalid_sha diagnostic; do + printf 'hyperpolymath/standards/.github/workflows/rust-ci-reusable.yml\t%s\t%s\tgood row\n%s\t%s\t%s\tbad row\n' \ + "$STALE_A" "$GOOD" "$invalid_path" "$invalid_sha" "$GOOD" > "$fx/invalid-row-pins.tsv" + run_fix env PINS_TABLE="$fx/invalid-row-pins.tsv" bash "$fleet/scripts/fix-stale-reusable-pin.sh" "$fx/sp12" + check "$diagnostic in a later table row refuses the run before caller or lock changes" \ + "[ \$rc -eq 2 ] && grep -q 'table row 2: $diagnostic' <<<\"\$out\" && cmp -s '$fx/sp12-before.yml' '$fx/sp12/.github/workflows/rust-ci.yml' && cmp -s '$fx/sp12-before.lock' '$fx/sp12/.github/workflows/actions.lock'" +done <<'TSV' +fixture/reusable/.github/workflows/ci.yml 123456789012345678901234567890123456789 bad stale SHA +fixture/reusable/.github/workflows/../ci.yml aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa bad reusable path +TSV + +# ── 3. fix-lock-self-sha-refs.sh ───────────────────────────────────────────── +# Only a workflow site proves a record is an action (a record no workflow +# uses is a transitive dependency, which the fixer will not classify by +# guess), so each action fixture carries the step that uses it. +action_steps() { # ... : a ci.yml with one step per action + { printf 'jobs:\n build:\n steps:\n' + for a in "${@:2}"; do printf ' - uses: %s\n' "$a"; done + } > "$1/ci.yml" +} +mkdir -p "$fx/ls/.github/workflows" +action_steps "$fx/ls/.github/workflows" "actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1" +cat > "$fx/ls/.github/workflows/actions.lock" <<'LOCK' +version: 'v0.0.3' +workflows: + '.github/workflows/ci.yml': + - 'actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1' +dependencies: + 'actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1': + ref: '3d3c42e5aac5ba805825da76410c181273ba90b1' + commit: 'sha1-3d3c42e5aac5ba805825da76410c181273ba90b1' + owner_id: 44036562 + repo_id: 197814629 + 'actions/checkout@v7.0.1': + ref: 'v7.0.1' + commit: 'sha1-3d3c42e5aac5ba805825da76410c181273ba90b1' + owner_id: 44036562 + repo_id: 197814629 + 'hyperpolymath/standards@427eb35c40552d912d8c757518e8326f0d20f597': + ref: '427eb35c40552d912d8c757518e8326f0d20f597' + commit: 'sha1-427eb35c40552d912d8c757518e8326f0d20f597' + owner_id: 6759885 + repo_id: 1116521501 + 'nobody/unknown-action@0000000000000000000000000000000000000001': + ref: '0000000000000000000000000000000000000001' + commit: 'sha1-0000000000000000000000000000000000000001' + owner_id: 1 + repo_id: 1 +LOCK +set +e +bash "$fleet/scripts/fix-lock-self-sha-refs.sh" "$fx/ls" --offline > "$fx/ls.out" 2>&1; rc=$? +set -e +check "the self-SHA checkout record gets its tag from the offline map" "grep -A1 \"'actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1':\" '$fx/ls/.github/workflows/actions.lock' | grep -q \"ref: 'v7.0.1'\"" +check "a tag-ref record is untouched" "[ \$(grep -c \"ref: 'v7.0.1'\" '$fx/ls/.github/workflows/actions.lock') -eq 2 ]" +check "a reusable-source record keeps its SHA ref (standards' accepted lock does the same)" "grep -A1 \"'hyperpolymath/standards@427eb35c\" '$fx/ls/.github/workflows/actions.lock' | grep -q \"ref: '427eb35c40552d912d8c757518e8326f0d20f597'\"" +check "an unresolvable record is left alone and reported, and the exit code says so" "[ \$rc -eq 1 ] && grep -q 'UNRESOLVED nobody/unknown-action' '$fx/ls.out' && grep -A1 \"'nobody/unknown-action@\" '$fx/ls/.github/workflows/actions.lock' | grep -q \"ref: '0000000000000000000000000000000000000001'\"" +check "commit: lines and ids are untouched" "[ \$(grep -c \"commit: 'sha1-3d3c42e5aac5ba805825da76410c181273ba90b1'\" '$fx/ls/.github/workflows/actions.lock') -eq 2 ] && grep -q 'repo_id: 197814629' '$fx/ls/.github/workflows/actions.lock'" +sed -i '/nobody\/unknown-action/,/repo_id: 1$/d' "$fx/ls/.github/workflows/actions.lock" +cp "$fx/ls/.github/workflows/actions.lock" "$fx/ls1.lock" +bash "$fleet/scripts/fix-lock-self-sha-refs.sh" "$fx/ls" --offline >/dev/null 2>&1 +check "second run is a no-op and exits 0" "cmp -s '$fx/ls1.lock' '$fx/ls/.github/workflows/actions.lock'" +check "a repository without a lock is OK, not an error" "mkdir -p '$fx/nolock' && bash '$fleet/scripts/fix-lock-self-sha-refs.sh' '$fx/nolock' --offline | grep -q 'OK (no actions.lock)'" +# A reusable source is whatever the workflows call as a reusable, not a +# hard-coded pair of names: its record keeps its SHA ref and is not "unresolved". +mkdir -p "$fx/ls4/.github/workflows" +printf 'jobs:\n corpus:\n uses: hyperpolymath/proven-tests-and-benches/.github/workflows/corpus-check.yml@2222222222222222222222222222222222222222\n' > "$fx/ls4/.github/workflows/ci.yml" +cat > "$fx/ls4/.github/workflows/actions.lock" <<'LOCK' +version: 'v0.0.3' +workflows: + '.github/workflows/ci.yml': + - 'hyperpolymath/proven-tests-and-benches@2222222222222222222222222222222222222222' +dependencies: + 'hyperpolymath/proven-tests-and-benches@2222222222222222222222222222222222222222': + ref: '2222222222222222222222222222222222222222' + commit: 'sha1-2222222222222222222222222222222222222222' + owner_id: 6759885 + repo_id: 2 +LOCK +cp "$fx/ls4/.github/workflows/actions.lock" "$fx/ls4-before.lock" +set +e +bash "$fleet/scripts/fix-lock-self-sha-refs.sh" "$fx/ls4" --offline > "$fx/ls4.out" 2>&1; rc4=$? +set -e +check "a reusable source read from the workflows keeps its SHA ref: untouched, not UNRESOLVED, exit 0" \ + "[ \$rc4 -eq 0 ] && ! grep -q UNRESOLVED '$fx/ls4.out' && cmp -s '$fx/ls4-before.lock' '$fx/ls4/.github/workflows/actions.lock'" +# One repository, two revisions, two classes: a reusable call keeps its SHA +# ref; a composite action from the same repository at another revision is +# repaired. The class is per repository@revision, read from the workflows. +SHA_A=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa; SHA_B=bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb +mkdir -p "$fx/ls5/.github/workflows" +printf 'jobs:\n gate:\n uses: hyperpolymath/cicd-suite/.github/workflows/rust-ci.yml@%s\n check:\n steps:\n - uses: hyperpolymath/cicd-suite/actions/manifest-check@%s\n' "$SHA_A" "$SHA_B" > "$fx/ls5/.github/workflows/ci.yml" +cat > "$fx/ls5/.github/workflows/actions.lock" < "$fx/ls5-tags.tsv" +set +e +TAG_MAP="$fx/ls5-tags.tsv" bash "$fleet/scripts/fix-lock-self-sha-refs.sh" "$fx/ls5" --offline > "$fx/ls5.out" 2>&1; rc5=$? +set -e +check "same repository, two revisions: the reusable call keeps its SHA ref, the action record gets its tag" \ + "[ \$rc5 -eq 0 ] && grep -A1 \"'hyperpolymath/cicd-suite@$SHA_A':\" '$fx/ls5/.github/workflows/actions.lock' | grep -q \"ref: '$SHA_A'\" && grep -A1 \"'hyperpolymath/cicd-suite@$SHA_B':\" '$fx/ls5/.github/workflows/actions.lock' | grep -q \"ref: 'v1.2.3'\" && ! grep -q UNRESOLVED '$fx/ls5.out'" +# A double-quoted record is the same record: repaired, quote style kept. +mkdir -p "$fx/ls6/.github/workflows" +action_steps "$fx/ls6/.github/workflows" "actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1" +printf 'version: "v0.0.3"\nworkflows:\n ".github/workflows/ci.yml":\n - "actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1"\ndependencies:\n "actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1":\n ref: "3d3c42e5aac5ba805825da76410c181273ba90b1"\n commit: "sha1-3d3c42e5aac5ba805825da76410c181273ba90b1"\n owner_id: 44036562\n repo_id: 197814629\n' > "$fx/ls6/.github/workflows/actions.lock" +set +e +bash "$fleet/scripts/fix-lock-self-sha-refs.sh" "$fx/ls6" --offline > "$fx/ls6.out" 2>&1; rc6=$? +set -e +check "a double-quoted self-SHA record is parsed and repaired, keeping its quotes" \ + "[ \$rc6 -eq 0 ] && grep -q 'ref: \"v7.0.1\"' '$fx/ls6/.github/workflows/actions.lock' && grep -q 'FIXED actions/checkout' '$fx/ls6.out'" +# A tag Git allows but this fixer will not write (`&`, `|`, a quote) is refused. +SHA_C=cccccccccccccccccccccccccccccccccccccccc +mkdir -p "$fx/ls7/.github/workflows" +action_steps "$fx/ls7/.github/workflows" "nobody/weird@$SHA_C" +printf "version: 'v0.0.3'\nworkflows: {}\ndependencies:\n 'nobody/weird@%s':\n ref: '%s'\n commit: 'sha1-%s'\n owner_id: 1\n repo_id: 1\n" "$SHA_C" "$SHA_C" "$SHA_C" > "$fx/ls7/.github/workflows/actions.lock" +printf 'nobody/weird\t%s\trelease&stable\n' "$SHA_C" > "$fx/ls7-tags.tsv" +cp "$fx/ls7/.github/workflows/actions.lock" "$fx/ls7-before.lock" +set +e +TAG_MAP="$fx/ls7-tags.tsv" bash "$fleet/scripts/fix-lock-self-sha-refs.sh" "$fx/ls7" --offline > "$fx/ls7.out" 2>&1; rc7=$? +set -e +check "a tag with characters the fixer will not write is UNRESOLVED, the record untouched, exit 1" \ + "[ \$rc7 -eq 1 ] && grep -q 'carries characters' '$fx/ls7.out' && cmp -s '$fx/ls7-before.lock' '$fx/ls7/.github/workflows/actions.lock'" + +# Exercise tag resolution through a local Git stub: even the online branch +# stays offline, and the call log proves when a lookup did or did not occur. +write_action_lock() { # + cat > "$1" < "$fx/git-bin/git" <<'SH' +#!/usr/bin/env bash +set -euo pipefail +printf '%s\n' "$*" >> "$PIN_TEST_GIT_LOG" +if [ "$#" -ne 3 ] || [ "$1" != ls-remote ] || [ "$2" != --tags ] || [ "$3" != https://github.com/fixture/action ]; then + exit 99 +fi +cat "$PIN_TEST_GIT_RESPONSE" +exit "${PIN_TEST_GIT_STATUS:-0}" +SH +chmod +x "$fx/git-bin/git" +: > "$fx/git-calls" +cat > "$fx/git-response" < "$fx/lookup-tags.tsv" +write_action_lock "$lookup_lock" fixture/action "$SHA_A" "$SHA_A" +write_action_lock "$fx/lookup-expected.lock" fixture/action "$SHA_A" release/v3.2.1+build-4 +run_lookup() { + run_fix env PATH="$fx/git-bin:$PATH" PIN_TEST_GIT_LOG="$fx/git-calls" \ + PIN_TEST_GIT_RESPONSE="$fx/git-response" TAG_MAP="$fx/lookup-tags.tsv" "$@" \ + bash "$fleet/scripts/fix-lock-self-sha-refs.sh" "$lookup_repo" "${lookup_mode[@]}" +} +lookup_mode=() +run_lookup +check "tag map matches repository and revision exactly, accepts safe punctuation and avoids Git" \ + "[ \$rc -eq 0 ] && [ ! -s '$fx/git-calls' ] && cmp -s '$fx/lookup-expected.lock' '$lookup_lock'" + +# Empty map: --offline refuses the unresolved record without consulting Git. +: > "$fx/lookup-tags.tsv" +write_action_lock "$lookup_lock" fixture/action "$SHA_A" "$SHA_A" +cp "$lookup_lock" "$fx/lookup-before.lock" +lookup_mode=(--offline) +run_lookup +check "offline mode never calls Git, leaves an unknown record byte-identical and exits 1" \ + "[ \$rc -eq 1 ] && grep -q 'UNRESOLVED fixture/action' <<<\"\$out\" && [ ! -s '$fx/git-calls' ] && cmp -s '$fx/lookup-before.lock' '$lookup_lock'" + +# The highest specificity wins before length; an annotated tag resolves via +# its peeled commit, and unrelated commits must not influence the choice. +lookup_mode=() +write_action_lock "$fx/lookup-expected.lock" fixture/action "$SHA_A" v2.4.7 +run_lookup +check "Git fallback chooses the most specific matching tag including peeled annotated tags" \ + "[ \$rc -eq 0 ] && cmp -s '$fx/lookup-expected.lock' '$lookup_lock'" +printf '%s\n' 'ls-remote --tags https://github.com/fixture/action' > "$fx/expected-git-calls" +check "Git fallback requests only the action repository's tags, once" \ + "cmp -s '$fx/expected-git-calls' '$fx/git-calls'" +: > "$fx/git-calls" +run_lookup +check "resolved lock is idempotent and does not perform another Git lookup" \ + "[ \$rc -eq 0 ] && [ ! -s '$fx/git-calls' ] && cmp -s '$fx/lookup-expected.lock' '$lookup_lock'" + +# A transport failure and a successful response containing no matching commit +# both leave the lock unchanged and explicitly unresolved. +printf '%s\trefs/tags/v99.99.99\n' "$SHA_C" > "$fx/git-response" +for git_status in 0 128; do + write_action_lock "$lookup_lock" fixture/action "$SHA_A" "$SHA_A" + : > "$fx/git-calls" + run_lookup PIN_TEST_GIT_STATUS="$git_status" + check "Git response with no usable tag (status $git_status) exits 1 and preserves the lock" \ + "[ \$rc -eq 1 ] && grep -q 'UNRESOLVED fixture/action' <<<\"\$out\" && cmp -s '$fx/expected-git-calls' '$fx/git-calls' && cmp -s '$fx/lookup-before.lock' '$lookup_lock'" +done + +# Sed/YAML metacharacters must never enter a lock, even from a local map. +lookup_mode=(--offline) +: > "$fx/git-calls" +for unsafe_tag in 'release|stable' "release'candidate" 'release"candidate' 'release&stable'; do + write_action_lock "$lookup_lock" fixture/action "$SHA_A" "$SHA_A" + printf 'fixture/action\t%s\t%s\n' "$SHA_A" "$unsafe_tag" > "$fx/lookup-tags.tsv" + run_lookup + check "unsafe mapped tag $unsafe_tag is refused without changing the lock" \ + "[ \$rc -eq 1 ] && grep -q 'carries characters' <<<\"\$out\" && [ ! -s '$fx/git-calls' ] && cmp -s '$fx/lookup-before.lock' '$lookup_lock'" +done + +# Similar-looking records are non-defects: a 41-digit key and a full SHA key +# whose ref names another SHA are not self-SHA action records. +write_action_lock "$lookup_lock" fixture/action "${SHA_A}a" "${SHA_A}a" +cp "$lookup_lock" "$fx/non-self-before.lock" +run_lookup +check "a 41-hex record key is untouched and never reported unresolved" \ + "[ \$rc -eq 0 ] && ! grep -q UNRESOLVED <<<\"\$out\" && [ ! -s '$fx/git-calls' ] && cmp -s '$fx/non-self-before.lock' '$lookup_lock'" +write_action_lock "$lookup_lock" fixture/action "$SHA_A" "$SHA_B" +cp "$lookup_lock" "$fx/non-self-before.lock" +run_lookup +check "a ref different from its record's SHA is untouched" \ + "[ \$rc -eq 0 ] && ! grep -q UNRESOLVED <<<\"\$out\" && [ ! -s '$fx/git-calls' ] && cmp -s '$fx/non-self-before.lock' '$lookup_lock'" + +# Equal specificity is resolved by tag length. A missing optional map still +# allows the Git fallback, whose response remains controlled by the stub. +lookup_mode=() +write_action_lock "$lookup_lock" fixture/action "$SHA_A" "$SHA_A" +printf '%s\trefs/tags/v2.4.7\n%s\trefs/tags/v2.4.7-rc1\n' "$SHA_A" "$SHA_A" > "$fx/git-response" +write_action_lock "$fx/lookup-length-expected.lock" fixture/action "$SHA_A" v2.4.7-rc1 +: > "$fx/lookup-tags.tsv" +: > "$fx/git-calls" +run_lookup +check "Git fallback breaks equal dot-count ties by tag length" \ + "[ \$rc -eq 0 ] && cmp -s '$fx/lookup-length-expected.lock' '$lookup_lock' && cmp -s '$fx/expected-git-calls' '$fx/git-calls'" +write_action_lock "$lookup_lock" fixture/action "$SHA_A" "$SHA_A" +rm "$fx/lookup-tags.tsv" +: > "$fx/git-calls" +run_lookup +check "a missing tag map falls back to Git and repairs the record" \ + "[ \$rc -eq 0 ] && cmp -s '$fx/lookup-length-expected.lock' '$lookup_lock' && cmp -s '$fx/expected-git-calls' '$fx/git-calls'" +write_action_lock "$lookup_lock" fixture/action "$SHA_A" "$SHA_A" +printf '%s\trefs/tags/v2.4.7&unsafe\n' "$SHA_A" > "$fx/git-response" +: > "$fx/git-calls" +run_lookup +check "unsafe tags from Git are refused just like unsafe mapped tags" \ + "[ \$rc -eq 1 ] && grep -q 'carries characters' <<<\"\$out\" && cmp -s '$fx/lookup-before.lock' '$lookup_lock' && cmp -s '$fx/expected-git-calls' '$fx/git-calls'" + +# ls8: a record's class comes from real `uses:` keys. The same reusable path +# inside a `run: |` script or a fixture under `env:` is text: it cannot make an +# action record "a reusable call" and so hide its self-SHA ref from repair. +mkdir -p "$fx/ls8/.github/workflows" +cat > "$fx/ls8/.github/workflows/ci.yml" < "$fx/ls8/.github/workflows/actions.lock" < "$fx/ls8-tags.tsv" +TAG_MAP="$fx/ls8-tags.tsv" bash "$fleet/scripts/fix-lock-self-sha-refs.sh" "$fx/ls8" --offline > "$fx/ls8.out" 2>&1; rc8=$? +check "a reusable path inside run: | or env: is text: the action record it names is still repaired" \ + "[ \$rc8 -eq 0 ] && grep -q \"ref: 'v1.0.0'\" '$fx/ls8/.github/workflows/actions.lock' && ! grep -q UNRESOLVED '$fx/ls8.out'" + +# ls9: a record is read to its end: `ref:` after `commit:` and the ids is +# still the record's ref, and still repaired. +mkdir -p "$fx/ls9/.github/workflows" +printf 'jobs:\n check:\n steps:\n - uses: foo/bar@%s\n' "$SHA_A" > "$fx/ls9/.github/workflows/ci.yml" +cat > "$fx/ls9/.github/workflows/actions.lock" < "$fx/ls9-tags.tsv" +TAG_MAP="$fx/ls9-tags.tsv" bash "$fleet/scripts/fix-lock-self-sha-refs.sh" "$fx/ls9" --offline > "$fx/ls9.out" 2>&1; rc9=$? +check "a ref: that is not the first line of its record is still the record's ref: repaired, exit 0" \ + "[ \$rc9 -eq 0 ] && grep -q \"^ ref: 'v1.0.0'\$\" '$fx/ls9/.github/workflows/actions.lock' && ! grep -q \"ref: '$SHA_A'\" '$fx/ls9/.github/workflows/actions.lock' && grep -q '^FIXED' '$fx/ls9.out'" + +# ls10: a plain-scalar `ref: ` (no quotes) is the same ref: found and +# repaired, the absence of quotes kept. +mkdir -p "$fx/ls10/.github/workflows" +printf 'jobs:\n check:\n steps:\n - uses: foo/bar@%s\n' "$SHA_A" > "$fx/ls10/.github/workflows/ci.yml" +cat > "$fx/ls10/.github/workflows/actions.lock" < "$fx/ls10-tags.tsv" +TAG_MAP="$fx/ls10-tags.tsv" bash "$fleet/scripts/fix-lock-self-sha-refs.sh" "$fx/ls10" --offline > "$fx/ls10.out" 2>&1; rc10=$? +check "an unquoted self-SHA ref is found and repaired, unquoted" \ + "[ \$rc10 -eq 0 ] && grep -q '^ ref: v1.0.0$' '$fx/ls10/.github/workflows/actions.lock' && grep -q '^FIXED' '$fx/ls10.out'" + +# ls11: a flow-style job (`call: { uses: ... }`) is a reusable call and keeps +# its SHA ref; a flow-style step (`- { uses: ... }`) is an action and is repaired. +mkdir -p "$fx/ls11/.github/workflows" +cat > "$fx/ls11/.github/workflows/ci.yml" < "$fx/ls11/.github/workflows/actions.lock" < "$fx/ls11-tags.tsv" +TAG_MAP="$fx/ls11-tags.tsv" bash "$fleet/scripts/fix-lock-self-sha-refs.sh" "$fx/ls11" --offline > "$fx/ls11.out" 2>&1; rc11=$? +check "a flow-style job is a reusable call (SHA ref kept); a flow-style step is an action (repaired)" \ + "[ \$rc11 -eq 0 ] && grep -A1 \"^ 'octo/example@$SHA_A':\$\" '$fx/ls11/.github/workflows/actions.lock' | grep -q \"ref: '$SHA_A'\" && grep -A1 \"^ 'octo/act@$SHA_B':\$\" '$fx/ls11/.github/workflows/actions.lock' | grep -q \"ref: 'v1.2.3'\" && ! grep -q UNRESOLVED '$fx/ls11.out'" + +# At the same revision a reusable call takes precedence over an action use, +# even across workflow files and when a tag is available. Removing the call +# exposes the action use, including its subdirectory, for repair. +ls_shared="$fx/ls-shared/.github/workflows" +mkdir -p "$ls_shared" +printf 'jobs:\n call:\n uses: fixture/dual/.github/workflows/ci.yml@%s\n' "$SHA_A" > "$ls_shared/reusable.yml" +printf 'jobs:\n build:\n steps:\n - name: Nested action\n uses: fixture/dual/actions/check@%s\n' "$SHA_A" > "$ls_shared/action.yaml" +write_action_lock "$ls_shared/actions.lock" fixture/dual "$SHA_A" "$SHA_A" +cp "$ls_shared/actions.lock" "$fx/ls-shared-before.lock" +printf 'fixture/dual\t%s\tv1.2.3\n' "$SHA_A" > "$fx/ls-shared-tags.tsv" +run_fix env TAG_MAP="$fx/ls-shared-tags.tsv" bash "$fleet/scripts/fix-lock-self-sha-refs.sh" "$fx/ls-shared" --offline +check "a reusable and action at the same revision keep their shared SHA ref despite an available tag" \ + "[ \$rc -eq 0 ] && cmp -s '$fx/ls-shared-before.lock' '$ls_shared/actions.lock' && ! grep -qE '^(FIXED|UNRESOLVED)' <<<\"\$out\"" +rm "$ls_shared/reusable.yml" +write_action_lock "$fx/ls-shared-expected.lock" fixture/dual "$SHA_A" v1.2.3 +run_fix env TAG_MAP="$fx/ls-shared-tags.tsv" bash "$fleet/scripts/fix-lock-self-sha-refs.sh" "$fx/ls-shared" --offline +check "without the reusable call the subdirectory action in a .yaml workflow gets its tag" \ + "[ \$rc -eq 0 ] && cmp -s '$fx/ls-shared-expected.lock' '$ls_shared/actions.lock' && grep -q '^FIXED fixture/dual' <<<\"\$out\"" + +# A missing tag and an unsafe tag bracket a resolvable record. Neither may +# stop that repair, alter neighboring records, or hide the unresolved status. +ls_partial="$fx/ls-partial/.github/workflows" +mkdir -p "$ls_partial" +action_steps "$ls_partial" "fixture/missing@$SHA_A" "fixture/resolved@$SHA_B" "fixture/unsafe@$SHA_C" +cat > "$ls_partial/actions.lock" < "$fx/ls-partial-expected.lock" < "$fx/ls-partial-tags.tsv" +run_fix env TAG_MAP="$fx/ls-partial-tags.tsv" bash "$fleet/scripts/fix-lock-self-sha-refs.sh" "$fx/ls-partial" --offline +check "mixed records repair the resolvable ref in place and preserve both unresolved records" \ + "[ \$rc -eq 1 ] && cmp -s '$fx/ls-partial-expected.lock' '$ls_partial/actions.lock' && [ \$(grep -c '^FIXED' <<<\"\$out\") -eq 1 ] && [ \$(grep -c '^UNRESOLVED' <<<\"\$out\") -eq 2 ] && grep -q '^Rewrote 1 record(s)' <<<\"\$out\"" +run_fix env TAG_MAP="$fx/ls-partial-tags.tsv" bash "$fleet/scripts/fix-lock-self-sha-refs.sh" "$fx/ls-partial" --offline +check "a partially resolved lock is byte-identical on rerun and still exits 1" \ + "[ \$rc -eq 1 ] && cmp -s '$fx/ls-partial-expected.lock' '$ls_partial/actions.lock' && ! grep -qE '^(FIXED|OK)' <<<\"\$out\" && [ \$(grep -c '^UNRESOLVED' <<<\"\$out\") -eq 2 ]" +printf 'fixture/missing\t%s\tv1.0.0\nfixture/unsafe\t%s\tv3.0.0\n' "$SHA_A" "$SHA_C" > "$fx/ls-partial-tags.tsv" +run_fix env TAG_MAP="$fx/ls-partial-tags.tsv" bash "$fleet/scripts/fix-lock-self-sha-refs.sh" "$fx/ls-partial" --offline +check "supplying safe tags resolves the remaining records without revisiting the repaired one" \ + "[ \$rc -eq 0 ] && [ \$(grep -c '^FIXED' <<<\"\$out\") -eq 2 ] && ! grep -q '^UNRESOLVED' <<<\"\$out\" && grep -q \"ref: 'v1.0.0' # no tag\" '$ls_partial/actions.lock' && grep -q \"ref: 'v3.0.0'\" '$ls_partial/actions.lock' && grep -q 'ref: \"v2.3.4\" # keep quote style and comment' '$ls_partial/actions.lock'" +cp "$ls_partial/actions.lock" "$fx/ls-partial-resolved.lock" +run_fix env TAG_MAP="$fx/ls-partial-tags.tsv" bash "$fleet/scripts/fix-lock-self-sha-refs.sh" "$fx/ls-partial" --offline +check "a fully resolved mixed lock is an idempotent success" \ + "[ \$rc -eq 0 ] && cmp -s '$fx/ls-partial-resolved.lock' '$ls_partial/actions.lock' && grep -q '^OK (no self-SHA action records)' <<<\"\$out\"" + +# Portability (review): Debian/Ubuntu's default awk is mawk. Run the same +# fixture through mawk when it is installed, and refuse a lock the parser +# cannot read instead of reporting OK. +if command -v mawk >/dev/null; then + mkdir -p "$fx/mawk-bin" "$fx/ls2/.github/workflows"; ln -sf "$(command -v mawk)" "$fx/mawk-bin/awk" + cp "$fx/ls1.lock" "$fx/ls2/.github/workflows/actions.lock" + cp "$fx/ls/.github/workflows/ci.yml" "$fx/ls2/.github/workflows/ci.yml" + sed -i "s/ref: 'v7.0.1'/ref: '3d3c42e5aac5ba805825da76410c181273ba90b1'/" "$fx/ls2/.github/workflows/actions.lock" + sed -i '0,/ref: .3d3c42e5aac5ba805825da76410c181273ba90b1./!{0,/ref: .3d3c42e5aac5ba805825da76410c181273ba90b1./s//ref: '"'"'v7.0.1'"'"'/}' "$fx/ls2/.github/workflows/actions.lock" + printf " 'x/thirty-nine@123456789012345678901234567890123456789':\n ref: '123456789012345678901234567890123456789'\n commit: 'sha1-123456789012345678901234567890123456789'\n" >> "$fx/ls2/.github/workflows/actions.lock" + set +e + PATH="$fx/mawk-bin:$PATH" bash "$fleet/scripts/fix-lock-self-sha-refs.sh" "$fx/ls2" --offline > "$fx/ls2.out" 2>&1; rc2=$? + set -e + check "under mawk the self-SHA record is repaired and the run exits 0" \ + "[ \$rc2 -eq 0 ] && grep -A1 \"'actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1':\" '$fx/ls2/.github/workflows/actions.lock' | grep -q \"ref: 'v7.0.1'\"" + check "a 39-hex key is not a record and is neither rewritten nor reported" \ + "grep -q \"ref: '123456789012345678901234567890123456789'\" '$fx/ls2/.github/workflows/actions.lock' && ! grep -q UNRESOLVED '$fx/ls2.out'" +else + echo " skip mawk portability (mawk not installed)" +fi +mkdir -p "$fx/noawk-bin"; printf '#!/bin/sh\nexit 1\n' > "$fx/noawk-bin/awk"; chmod +x "$fx/noawk-bin/awk" +cp "$fx/ls1.lock" "$fx/ls3.lock"; mkdir -p "$fx/ls3/.github/workflows"; cp "$fx/ls1.lock" "$fx/ls3/.github/workflows/actions.lock" +set +e +PATH="$fx/noawk-bin:$PATH" bash "$fleet/scripts/fix-lock-self-sha-refs.sh" "$fx/ls3" --offline > "$fx/ls3.out" 2>&1; rc3=$? +set -e +check "a parser failure exits 2, says so, and leaves the lock untouched (never OK)" \ + "[ \$rc3 -eq 2 ] && grep -q 'could not parse' '$fx/ls3.out' && ! grep -q '^OK' '$fx/ls3.out' && cmp -s '$fx/ls3.lock' '$fx/ls3/.github/workflows/actions.lock'" + +# ── 4. Registry routes for the three new fixers ───────────────────────────── +# jq is also used by dispatch-paths.sh in the same CI job. Assert only the +# routes introduced here so unrelated registry maintenance stays independent. +while IFS=$'\t' read -r section key expected; do + check "registry $section/$key selects $expected" \ + "jq -e --arg section '$section' --arg key '$key' --arg expected '$expected' '.registry[\$section][\$key] == \$expected' '$fleet/scripts/fix-script-registry.json' >/dev/null" +done <<'TSV' +by_category ToolchainInputMissing fix-rust-toolchain-input.sh +by_category StaleReusablePin fix-stale-reusable-pin.sh +by_category LockSelfShaRef fix-lock-self-sha-refs.sh +by_recipe WF026 fix-rust-toolchain-input.sh +by_recipe workflow_audit/sha_pinned_toolchain_without_input fix-rust-toolchain-input.sh +by_recipe WF027 fix-stale-reusable-pin.sh +by_recipe workflow_audit/known_broken_reusable_revision fix-stale-reusable-pin.sh +by_recipe PIN-003 fix-stale-reusable-pin.sh +by_recipe PI006 fix-lock-self-sha-refs.sh +by_recipe pin_integrity/self_sha_lock_record fix-lock-self-sha-refs.sh +TSV + + +# ── Review cases (gitbot-fleet#615, rounds 10–11) ─────────────────────────── +# tc15: the empty string (`""`, `''`), the other null spellings YAML admits +# unquoted (`Null`, `NULL`) and an upper-case SHA are all the key with no +# value: filled in place, never duplicated. `"null"` quoted is the string +# null — a value the step names, left alone. +mkdir -p "$fx/tc15/.github/workflows" +cat > "$fx/tc15/.github/workflows/x.yml" <<'YML' +jobs: + check: + steps: + - uses: dtolnay/rust-toolchain@6C977A6CA4077A0CEB28FFBE03F59D46E9AC8772 + with: + toolchain: "" + components: clippy + - uses: dtolnay/rust-toolchain@6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772 + with: { toolchain: '', components: clippy } + - uses: dtolnay/rust-toolchain@6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772 + with: + toolchain: NULL # chosen later + - uses: dtolnay/rust-toolchain@6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772 + with: { toolchain: Null } + - uses: dtolnay/rust-toolchain@6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772 + with: { toolchain: "null" } +YML +cat > "$fx/tc15-expected.yml" <<'YML' +jobs: + check: + steps: + - uses: dtolnay/rust-toolchain@6C977A6CA4077A0CEB28FFBE03F59D46E9AC8772 + with: + toolchain: stable + components: clippy + - uses: dtolnay/rust-toolchain@6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772 + with: { toolchain: stable, components: clippy } + - uses: dtolnay/rust-toolchain@6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772 + with: + toolchain: stable # chosen later + - uses: dtolnay/rust-toolchain@6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772 + with: { toolchain: stable } + - uses: dtolnay/rust-toolchain@6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772 + with: { toolchain: "null" } +YML +run_fix bash "$fleet/scripts/fix-rust-toolchain-input.sh" "$fx/tc15" +check "quoted-empty, Null/NULL and an upper-case SHA are filled in place; a quoted \"null\" string is a value" \ + "[ \$rc -eq 0 ] && cmp -s '$fx/tc15-expected.yml' '$fx/tc15/.github/workflows/x.yml'" +cp "$fx/tc15/.github/workflows/x.yml" "$fx/tc15-after1.yml" +run_fix bash "$fleet/scripts/fix-rust-toolchain-input.sh" "$fx/tc15" +check "tc15 second run is a no-op" "[ \$rc -eq 0 ] && cmp -s '$fx/tc15-after1.yml' '$fx/tc15/.github/workflows/x.yml'" + +# tc16: an empty `toolchain:` whose next PLAIN line sits deeper is a nested +# mapping even when a comment or blank line sits between them: unsupported, +# the file untouched, exit 2 — never a second `toolchain:` key. +mkdir -p "$fx/tc16/.github/workflows" +cat > "$fx/tc16/.github/workflows/x.yml" <<'YML' +jobs: + check: + steps: + - uses: dtolnay/rust-toolchain@6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772 + with: + toolchain: + # chosen below + + channel: stable +YML +cp "$fx/tc16/.github/workflows/x.yml" "$fx/tc16-before.yml" +run_fix bash "$fleet/scripts/fix-rust-toolchain-input.sh" "$fx/tc16" +check "a nested toolchain mapping behind a comment line is unsupported: exit 2, file untouched" \ + "[ \$rc -eq 2 ] && grep -q UNSUPPORTED <<<\"\$out\" && cmp -s '$fx/tc16-before.yml' '$fx/tc16/.github/workflows/x.yml'" + +# tc17: a multi-line flow `with:` whose empty entry has its delimiter on the +# next line — `toolchain: null` then `}`, or a bare `toolchain:` followed by +# the closing brace — is filled where it stands; a bare key whose next token +# is a value (spread over lines) is not this fixer's to place. +mkdir -p "$fx/tc17/.github/workflows" "$fx/tc17b/.github/workflows" +cat > "$fx/tc17/.github/workflows/x.yml" <<'YML' +jobs: + check: + steps: + - uses: dtolnay/rust-toolchain@6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772 + with: { + toolchain: null + } + - uses: dtolnay/rust-toolchain@6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772 + with: { + components: clippy, + toolchain: + } + - uses: dtolnay/rust-toolchain@6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772 + with: { toolchain: ~ + } +YML +cat > "$fx/tc17-expected.yml" <<'YML' +jobs: + check: + steps: + - uses: dtolnay/rust-toolchain@6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772 + with: { + toolchain: stable + } + - uses: dtolnay/rust-toolchain@6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772 + with: { + components: clippy, + toolchain: stable + } + - uses: dtolnay/rust-toolchain@6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772 + with: { toolchain: stable + } +YML +run_fix bash "$fleet/scripts/fix-rust-toolchain-input.sh" "$fx/tc17" +check "an empty flow entry whose delimiter is on the next line is filled in place, never duplicated" \ + "[ \$rc -eq 0 ] && cmp -s '$fx/tc17-expected.yml' '$fx/tc17/.github/workflows/x.yml'" +cat > "$fx/tc17b/.github/workflows/x.yml" <<'YML' +jobs: + check: + steps: + - uses: dtolnay/rust-toolchain@6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772 + with: { + toolchain: + nightly } +YML +cp "$fx/tc17b/.github/workflows/x.yml" "$fx/tc17b-before.yml" +run_fix bash "$fleet/scripts/fix-rust-toolchain-input.sh" "$fx/tc17b" +check "a bare flow toolchain: whose value starts on the next line is unsupported: exit 2, file untouched" \ + "[ \$rc -eq 2 ] && grep -q UNSUPPORTED <<<\"\$out\" && cmp -s '$fx/tc17b-before.yml' '$fx/tc17b/.github/workflows/x.yml'" + +# tc18: only a pin the parser cannot place AS A STEP is unsupported. A +# workflow-level `env:` string naming the action is data: the step below it +# is still repaired and the run is clean. A key of a flow-style step spread +# over several lines IS a step the parser cannot place. +mkdir -p "$fx/tc18/.github/workflows" "$fx/tc18b/.github/workflows" +cat > "$fx/tc18/.github/workflows/x.yml" <<'YML' +env: + TOOLCHAIN_ACTION: dtolnay/rust-toolchain@6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772 +jobs: + check: + steps: + - uses: dtolnay/rust-toolchain@6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772 +YML +run_fix bash "$fleet/scripts/fix-rust-toolchain-input.sh" "$fx/tc18" +check "a workflow-level env string naming the pin is data: the step is repaired and the run is clean" \ + "[ \$rc -eq 0 ] && ! grep -q UNSUPPORTED <<<\"\$out\" && grep -q '^ TOOLCHAIN_ACTION: dtolnay/rust-toolchain@6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772$' '$fx/tc18/.github/workflows/x.yml' && [ \$(grep -c 'toolchain: stable' '$fx/tc18/.github/workflows/x.yml') -eq 1 ]" +cat > "$fx/tc18b/.github/workflows/x.yml" <<'YML' +jobs: + check: + steps: [ + { name: toolchain, + uses: dtolnay/rust-toolchain@6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772 } + ] +YML +cp "$fx/tc18b/.github/workflows/x.yml" "$fx/tc18b-before.yml" +run_fix bash "$fleet/scripts/fix-rust-toolchain-input.sh" "$fx/tc18b" +check "a key of a flow-style step spread over lines is still unsupported: exit 2, file untouched" \ + "[ \$rc -eq 2 ] && grep -q UNSUPPORTED <<<\"\$out\" && cmp -s '$fx/tc18b-before.yml' '$fx/tc18b/.github/workflows/x.yml'" + +# ls12: only a DIRECT key of a step is its `uses:`; the same key nested under +# `with:` or `env:` is an input and names no action. An estate record such +# an input names is then unreferenced and keeps its SHA ref (the reusable +# fallback) — as an action site it would have been given the tag. A +# bare-dash item learns its key column from its first key line. +mkdir -p "$fx/ls12/.github/workflows" +cat > "$fx/ls12/.github/workflows/ci.yml" < "$fx/ls12/.github/workflows/actions.lock" < "$fx/ls12-tags.tsv" +TAG_MAP="$fx/ls12-tags.tsv" run_fix bash "$fleet/scripts/fix-lock-self-sha-refs.sh" "$fx/ls12" --offline +check "a step's direct uses: (under - name: and under a bare dash) is an action site; a uses: nested under with: is not" \ + "grep -q \"^ ref: 'v1.0.0'\" '$fx/ls12/.github/workflows/actions.lock' && grep -q \"^ ref: 'v3.0.0'\" '$fx/ls12/.github/workflows/actions.lock' && grep -q \"^ ref: '$SHA_B'\" '$fx/ls12/.github/workflows/actions.lock'" + +# ls13: the whole `jobs:` mapping written on one line is still a job map: +# its `uses:` entries are reusable calls, whose records keep their SHA ref. +# Two steps on one `steps: [ ... ]` line are each an action site. +mkdir -p "$fx/ls13/.github/workflows" +cat > "$fx/ls13/.github/workflows/ci.yml" < "$fx/ls13/.github/workflows/two.yml" < "$fx/ls13/.github/workflows/actions.lock" < "$fx/ls13-tags.tsv" +TAG_MAP="$fx/ls13-tags.tsv" run_fix bash "$fleet/scripts/fix-lock-self-sha-refs.sh" "$fx/ls13" --offline +check "an inline jobs: { ... } mapping is a reusable call: its record keeps the SHA ref; two inline steps are both actions" \ + "[ \$rc -eq 0 ] && grep -q \"^ ref: '$SHA_A'\" '$fx/ls13/.github/workflows/actions.lock' && grep -q \"^ ref: 'v2.0.0'\" '$fx/ls13/.github/workflows/actions.lock' && grep -q \"^ ref: 'v3.0.0'\" '$fx/ls13/.github/workflows/actions.lock'" + +# sp17: an upper-case SHA is the same dead pin: looked up in lower case, the +# caller rewritten as written, the (lower-case) lock edge moved. +mkdir -p "$fx/sp17/.github/workflows" +printf 'jobs:\n rust:\n uses: hyperpolymath/standards/.github/workflows/rust-ci-reusable.yml@%s\n' "${STALE_A^^}" > "$fx/sp17/.github/workflows/ci.yml" +cat > "$fx/sp17/.github/workflows/actions.lock" < "$fx/sp18/.github/workflows/ci.yml" +cp "$fx/sp18/.github/workflows/ci.yml" "$fx/sp18-before.yml" +printf 'hyperpolymath/standards/.github/workflows/rust-*.yml\t%s\t%s\tglob in the file name\n' "$STALE_A" "$GOOD" > "$fx/sp18-table.tsv" +PINS_TABLE="$fx/sp18-table.tsv" run_fix bash "$fleet/scripts/fix-stale-reusable-pin.sh" "$fx/sp18" +check "a wildcard anywhere but a terminal /* is a bad row: exit 2, nothing changed" \ + "[ \$rc -eq 2 ] && grep -q 'terminal /\\*' <<<\"\$out\" && cmp -s '$fx/sp18-before.yml' '$fx/sp18/.github/workflows/ci.yml'" + +# sp19: a hand-edited lock whose workflow key is double-quoted is the same +# key: the edge is found and moved, not reported as missing. +mkdir -p "$fx/sp19/.github/workflows" +printf 'jobs:\n rust:\n uses: hyperpolymath/standards/.github/workflows/rust-ci-reusable.yml@%s\n' "$STALE_A" > "$fx/sp19/.github/workflows/ci.yml" +cat > "$fx/sp19/.github/workflows/actions.lock" < "$fx/sp20/.github/workflows/ci.yml" +cat > "$fx/sp20/.github/workflows/actions.lock" < "$fx/pl1/.github/workflows/ci.yml" +cp "$fx/pl1/.github/workflows/ci.yml" "$fx/pl1-before.yml" +pl1_key=$(cd "$fx/pl1" && pwd -P | tr -d '\n' | sha256sum | cut -c1-16) +flock -x "$fx/pin-fixer-$pl1_key.lock" sleep 3 & +pl1_holder=$! +sleep 0.3 +PIN_FIXER_LOCK_DIR="$fx" PIN_FIXER_LOCK_WAIT=1 run_fix bash "$fleet/scripts/fix-stale-reusable-pin.sh" "$fx/pl1" +check "a fixer that cannot take the checkout's lock in time reports it, exits 2 and changes nothing" \ + "[ \$rc -eq 2 ] && grep -q 'another workflow-pin fixer holds' <<<\"\$out\" && cmp -s '$fx/pl1-before.yml' '$fx/pl1/.github/workflows/ci.yml'" +wait "$pl1_holder" || true +PIN_FIXER_LOCK_DIR="$fx" run_fix bash "$fleet/scripts/fix-stale-reusable-pin.sh" "$fx/pl1" +check "once the holder is gone the same run takes the lock and repairs" \ + "[ \$rc -eq 0 ] && grep -q \"rust-ci-reusable.yml@$GOOD\" '$fx/pl1/.github/workflows/ci.yml'" + + +# ── Review cases (gitbot-fleet#615, round 12) ─────────────────────────────── +# tc19: an upper-case SHA that begins with a letter is still the step's own +# `uses:` key (USES_RE accepts the same hex as sha_ref), so it is repaired, +# not reported as a pin the parser cannot place. +mkdir -p "$fx/tc19/.github/workflows" +cat > "$fx/tc19/.github/workflows/x.yml" <<'YML' +jobs: + check: + steps: + - uses: dtolnay/rust-toolchain@AC977A6CA4077A0CEB28FFBE03F59D46E9AC8772 + with: + components: clippy +YML +run_fix bash "$fleet/scripts/fix-rust-toolchain-input.sh" "$fx/tc19" +check "an upper-case SHA beginning with a letter is the step's uses: key: repaired, not unsupported" \ + "[ \$rc -eq 0 ] && ! grep -q UNSUPPORTED <<<\"\$out\" && grep -A2 'AC977A6CA4077A0CEB28FFBE03F59D46E9AC8772' '$fx/tc19/.github/workflows/x.yml' | grep -q '^ toolchain: stable$'" + +# pl2: a single workflow file named relative to the checkout root locks the +# checkout, not the file: the lock file is the one every fixer run on that +# checkout computes. +mkdir -p "$fx/pl2/.github/workflows" +printf 'jobs:\n check:\n steps:\n - uses: dtolnay/rust-toolchain@6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772\n' > "$fx/pl2/.github/workflows/x.yml" +pl2_key=$(cd "$fx/pl2" && pwd -P | tr -d '\n' | sha256sum | cut -c1-16) +(cd "$fx/pl2" && PIN_FIXER_LOCK_DIR="$fx/pl2-locks" bash -c 'mkdir -p "$PIN_FIXER_LOCK_DIR"; bash "$1" .github/workflows/x.yml' _ "$fleet/scripts/fix-rust-toolchain-input.sh") > "$fx/pl2.out" 2>&1; rc_pl2=$? +check "a relative workflow-file target is locked by its checkout (same lock file as a checkout-path run)" \ + "[ \$rc_pl2 -eq 0 ] && [ -f '$fx/pl2-locks/pin-fixer-$pl2_key.lock' ] && grep -q 'toolchain: stable' '$fx/pl2/.github/workflows/x.yml'" + +# ls14: a flow mapping on one of a step's own keys (`with: { uses: ... }`) is +# that input's data, not the step's action: the estate record it names stays +# unreferenced and keeps its SHA ref. +mkdir -p "$fx/ls14/.github/workflows" +cat > "$fx/ls14/.github/workflows/ci.yml" < "$fx/ls14/.github/workflows/actions.lock" < "$fx/ls14-tags.tsv" +TAG_MAP="$fx/ls14-tags.tsv" run_fix bash "$fleet/scripts/fix-lock-self-sha-refs.sh" "$fx/ls14" --offline +check "a with: { uses: ... } flow input on a step key is data: the step's action is repaired, the estate record keeps its SHA ref" \ + "grep -q \"^ ref: 'v1.0.0'\" '$fx/ls14/.github/workflows/actions.lock' && grep -q \"^ ref: '$SHA_B'\" '$fx/ls14/.github/workflows/actions.lock'" + +# Review cases (Codex round on 5fa7192, gitbot-fleet#615). + +# tc20: a continuation line of a multi-line flow `with:` is read at the +# mapping's own level even when it STARTS inside a nested mapping: the text +# after the brace that closes the nested group (`channel: beta }, toolchain: +# nightly`) is an entry of `with:` — a toolchain that must count, or an +# empty one that is filled where it stands — while `toolchain:` inside the +# nested group is not. +mkdir -p "$fx/tc20/.github/workflows" +cat > "$fx/tc20/.github/workflows/x.yml" <<'YML' +jobs: + check: + steps: + - uses: dtolnay/rust-toolchain@6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772 + with: { + env: { + channel: beta }, toolchain: nightly + } + - uses: dtolnay/rust-toolchain@6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772 + with: { + env: { + channel: beta }, toolchain: null } + - uses: dtolnay/rust-toolchain@6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772 + with: { + env: { + toolchain: nightly }, components: clippy + } + - uses: dtolnay/rust-toolchain@6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772 + with: { + env: { channel: "}" }, # toolchain: nightly + toolchain: ${{ matrix.rust }} + } +YML +cat > "$fx/tc20-expected.yml" <<'YML' +jobs: + check: + steps: + - uses: dtolnay/rust-toolchain@6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772 + with: { + env: { + channel: beta }, toolchain: nightly + } + - uses: dtolnay/rust-toolchain@6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772 + with: { + env: { + channel: beta }, toolchain: stable } + - uses: dtolnay/rust-toolchain@6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772 + with: { toolchain: stable, + env: { + toolchain: nightly }, components: clippy + } + - uses: dtolnay/rust-toolchain@6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772 + with: { + env: { channel: "}" }, # toolchain: nightly + toolchain: ${{ matrix.rust }} + } +YML +run_fix bash "$fleet/scripts/fix-rust-toolchain-input.sh" "$fx/tc20" +check "tc20 an entry after a nested mapping closes mid-line counts (kept, or filled in place); one inside it does not" \ + "[ \$rc -eq 0 ] && cmp -s '$fx/tc20-expected.yml' '$fx/tc20/.github/workflows/x.yml'" +cp "$fx/tc20/.github/workflows/x.yml" "$fx/tc20-after1.yml" +run_fix bash "$fleet/scripts/fix-rust-toolchain-input.sh" "$fx/tc20" +check "tc20 second run is a no-op" "[ \$rc -eq 0 ] && cmp -s '$fx/tc20-after1.yml' '$fx/tc20/.github/workflows/x.yml'" + +# sp21: a table whose last row has no trailing newline is read whole — the +# row is matched and, when it is malformed, rejected — never skipped. +mkdir -p "$fx/sp21/.github/workflows" +printf 'jobs:\n rust:\n uses: octo/example/.github/workflows/ci.yml@%s\n' "$STALE_A" > "$fx/sp21/.github/workflows/ci.yml" +printf '# a private table\nocto/example/.github/workflows/*\t%s\t%s\tdead window' "$STALE_A" "$GOOD" > "$fx/sp21-table.tsv" +PINS_TABLE="$fx/sp21-table.tsv" run_fix bash "$fleet/scripts/fix-stale-reusable-pin.sh" "$fx/sp21" +check "sp21 a final table row without a newline is a row: its owner is scanned and its dead pin moved" \ + "[ \$rc -eq 0 ] && grep -q \"ci.yml@$GOOD\$\" '$fx/sp21/.github/workflows/ci.yml' && grep -q '^FIXED' <<<\"\$out\"" +printf 'jobs:\n rust:\n uses: octo/example/.github/workflows/ci.yml@%s\n' "$STALE_A" > "$fx/sp21/.github/workflows/ci.yml" +printf 'octo/example/.github/workflows/*\t%s\tnot-a-sha\tbad row' "$STALE_A" > "$fx/sp21-bad.tsv" +PINS_TABLE="$fx/sp21-bad.tsv" run_fix bash "$fleet/scripts/fix-stale-reusable-pin.sh" "$fx/sp21" +check "sp21 a malformed final row without a newline is still validated: exit 2, nothing changed" \ + "[ \$rc -eq 2 ] && grep -q 'bad replacement SHA' <<<\"\$out\" && grep -q \"ci.yml@$STALE_A\$\" '$fx/sp21/.github/workflows/ci.yml'" + +# sp22: a hand-written lock may leave its keys and edges unquoted. A plain +# record (`actions/checkout@:`) written before the quoted ones used to +# fall through the merger with no current record and vanish from +# `dependencies:` on any repair; it is a record like the others, and a plain +# edge or record of the dead pin itself moves like a quoted one. +mkdir -p "$fx/sp22/.github/workflows" +printf 'jobs:\n rust:\n uses: hyperpolymath/standards/.github/workflows/rust-ci-reusable.yml@%s\n' "$STALE_A" > "$fx/sp22/.github/workflows/ci.yml" +cat > "$fx/sp22/.github/workflows/actions.lock" < "$fx/ls15/.github/workflows/ci.yml" < "$fx/ls15/.github/workflows/two.yml" < "$fx/ls15/.github/workflows/actions.lock" < "$fx/ls15-tags.tsv" +TAG_MAP="$fx/ls15-tags.tsv" run_fix bash "$fleet/scripts/fix-lock-self-sha-refs.sh" "$fx/ls15" --offline +check "ls15 a uses: inside a quoted string or a comment names no site: the two action records are repaired, the real reusable call keeps its SHA" \ + "[ \$rc -eq 0 ] && grep -q \"^ ref: 'v1.0.0'\" '$fx/ls15/.github/workflows/actions.lock' && grep -q \"^ ref: 'v3.0.0'\" '$fx/ls15/.github/workflows/actions.lock' && grep -q \"^ ref: '$SHA_B'\" '$fx/ls15/.github/workflows/actions.lock' && [ \$(grep -c '^FIXED' <<<\"\$out\") -eq 2 ]" + +# ls16: a hand-added record may spell its commit ID in upper-case hex, in +# its key, its ref, or the workflow's pin — git resolves it the same. The +# record is found, looked up in lower case, and its ref rewritten as the +# ref line spells it; a plain (unquoted) key is a record too. +mkdir -p "$fx/ls16/.github/workflows" +printf 'jobs:\n check:\n steps:\n - uses: foo/bar@%s\n - uses: foo/baz@%s\n' "${SHA_A^^}" "$SHA_B" > "$fx/ls16/.github/workflows/ci.yml" +cat > "$fx/ls16/.github/workflows/actions.lock" < "$fx/ls16-tags.tsv" +TAG_MAP="$fx/ls16-tags.tsv" run_fix bash "$fleet/scripts/fix-lock-self-sha-refs.sh" "$fx/ls16" --offline +check "ls16 an upper-case self-SHA record (quoted) and a plain-keyed one whose key and ref differ in case are both found and repaired" \ + "[ \$rc -eq 0 ] && grep -q \"^ ref: 'v1.0.0'\$\" '$fx/ls16/.github/workflows/actions.lock' && grep -q '^ ref: v2.0.0\$' '$fx/ls16/.github/workflows/actions.lock' && ! grep -q 'no self-SHA' <<<\"\$out\" && [ \$(grep -c '^FIXED' <<<\"\$out\") -eq 2 ]" + +# ls17: a repository name is compared as a name, not as a pattern: the `.` +# of `foo/action.test` is a dot, so a reusable call of `foo/actionXtest` at +# the same SHA is not a reusable use of the action, whose self-SHA record is +# repaired. +mkdir -p "$fx/ls17/.github/workflows" +cat > "$fx/ls17/.github/workflows/ci.yml" < "$fx/ls17/.github/workflows/actions.lock" < "$fx/ls17-tags.tsv" +TAG_MAP="$fx/ls17-tags.tsv" run_fix bash "$fleet/scripts/fix-lock-self-sha-refs.sh" "$fx/ls17" --offline +check "ls17 a dot in a repository name is a dot: the action's record is repaired, the reusable at the same SHA keeps its SHA ref" \ + "[ \$rc -eq 0 ] && grep -A1 \"^ 'foo/action.test@$SHA_A':\$\" '$fx/ls17/.github/workflows/actions.lock' | grep -q \"ref: 'v1.0.0'\" && grep -A1 \"^ 'foo/actionXtest@$SHA_A':\$\" '$fx/ls17/.github/workflows/actions.lock' | grep -q \"ref: '$SHA_A'\"" + +# Review cases (Codex round on 709c153, gitbot-fleet#615). + +# pl3: the lock file is opened only inside a directory private to this user. +# By default that is a `pin-fixer-` directory created 0700 under the +# runtime/temp base, so a symlink planted at the old predictable name in a +# shared, world-writable directory is never followed. A lock directory that +# is group/world-writable or a symlink is refused: exit 2, nothing changed. +mkdir -p "$fx/pl3/.github/workflows" "$fx/pl3-shared" "$fx/pl3-private" +chmod 1777 "$fx/pl3-shared"; chmod 700 "$fx/pl3-private" +printf 'jobs:\n rust:\n uses: hyperpolymath/standards/.github/workflows/rust-ci-reusable.yml@%s\n' "$STALE_A" > "$fx/pl3/.github/workflows/ci.yml" +cp "$fx/pl3/.github/workflows/ci.yml" "$fx/pl3-before.yml" +pl3_key=$(cd "$fx/pl3" && pwd -P | tr -d '\n' | sha256sum | cut -c1-16) +printf 'keep\n' > "$fx/pl3-victim" +ln -s "$fx/pl3-victim" "$fx/pl3-shared/pin-fixer-$pl3_key.lock" +PIN_FIXER_LOCK_DIR="$fx/pl3-shared" run_fix bash "$fleet/scripts/fix-stale-reusable-pin.sh" "$fx/pl3" +check "pl3 a world-writable lock directory is refused: exit 2, nothing changed, the planted link not followed" \ + "[ \$rc -eq 2 ] && grep -q 'not a private directory' <<<\"\$out\" && cmp -s '$fx/pl3-before.yml' '$fx/pl3/.github/workflows/ci.yml' && [ \"\$(cat '$fx/pl3-victim')\" = keep ]" +ln -s "$fx/pl3-private" "$fx/pl3-link" +PIN_FIXER_LOCK_DIR="$fx/pl3-link" run_fix bash "$fleet/scripts/fix-stale-reusable-pin.sh" "$fx/pl3" +check "pl3 a symlinked lock directory is refused: exit 2, nothing changed" \ + "[ \$rc -eq 2 ] && grep -q 'not a private directory' <<<\"\$out\" && cmp -s '$fx/pl3-before.yml' '$fx/pl3/.github/workflows/ci.yml'" +rc=0; out=$(env -u PIN_FIXER_LOCK_DIR -u XDG_RUNTIME_DIR TMPDIR="$fx/pl3-shared" bash "$fleet/scripts/fix-stale-reusable-pin.sh" "$fx/pl3" 2>&1) || rc=$? +pl3_dir="$fx/pl3-shared/pin-fixer-$(id -u)" +check "pl3 by default the lock sits in a 0700 per-user directory; the shared name is never opened" \ + "[ \$rc -eq 0 ] && [ -f '$pl3_dir/pin-fixer-$pl3_key.lock' ] && [ -z \"\$(find '$pl3_dir' -maxdepth 0 \\( -perm -0020 -o -perm -0002 -o -perm -0040 -o -perm -0004 \\) -print)\" ] && [ \"\$(cat '$fx/pl3-victim')\" = keep ] && grep -q \"rust-ci-reusable.yml@$GOOD\" '$fx/pl3/.github/workflows/ci.yml'" + +# sp23: a dead pin named only in a trailing comment or a quoted scalar is +# data: no caller to repair and no UNSUPPORTED exit. The real caller beside +# it is moved; a flow-style job carrying the dead pin as a `uses:` key is +# still a caller the parser cannot place. +mkdir -p "$fx/sp23/.github/workflows" "$fx/sp23b/.github/workflows" +cat > "$fx/sp23/.github/workflows/ci.yml" < "$fx/sp23b/.github/workflows/ci.yml" +cp "$fx/sp23b/.github/workflows/ci.yml" "$fx/sp23b-before.yml" +run_fix bash "$fleet/scripts/fix-stale-reusable-pin.sh" "$fx/sp23b" +check "sp23 a flow-style job whose uses: key carries the dead pin is still UNSUPPORTED: exit 2, untouched" \ + "[ \$rc -eq 2 ] && grep -q UNSUPPORTED <<<\"\$out\" && cmp -s '$fx/sp23b-before.yml' '$fx/sp23b/.github/workflows/ci.yml'" + +# sp24: GitHub reads an owner or repository name in either case. A caller +# spelled `Hyperpolymath/Standards/...` meets the table's row, is rewritten +# with its own spelling kept, and finds its (lower-case) lock edge and record. +mkdir -p "$fx/sp24/.github/workflows" +printf 'jobs:\n rust:\n uses: Hyperpolymath/Standards/.github/workflows/rust-ci-reusable.yml@%s\n' "$STALE_A" > "$fx/sp24/.github/workflows/ci.yml" +cat > "$fx/sp24/.github/workflows/actions.lock" < "$fx/sp25/.github/workflows/ci.yml" +cat > "$fx/sp25/.github/workflows/actions.lock" < "$fx/sp25-expected.lock" < } ] } }` +# used to classify every uses: on the line as R, so the estate action record +# kept its self-SHA ref; the job-level reusable call beside it still keeps +# its SHA. +mkdir -p "$fx/ls18/.github/workflows" +cat > "$fx/ls18/.github/workflows/ci.yml" < "$fx/ls18/.github/workflows/actions.lock" < "$fx/ls18-tags.tsv" +TAG_MAP="$fx/ls18-tags.tsv" run_fix bash "$fleet/scripts/fix-lock-self-sha-refs.sh" "$fx/ls18" --offline +check "ls18 a step in a one-line jobs map is an action (tag written); the job-level reusable call keeps its SHA" \ + "[ \$rc -eq 0 ] && grep -A1 \"^ 'hyperpolymath/standards@$SHA_A':\$\" '$fx/ls18/.github/workflows/actions.lock' | grep -q \"ref: 'v1.0.0'\" && grep -A1 \"^ 'hyperpolymath/cicd-suite@$SHA_B':\$\" '$fx/ls18/.github/workflows/actions.lock' | grep -q \"ref: '$SHA_B'\"" + +# Review cases (Codex round on c7392ee, gitbot-fleet#615). + +# sp26: an entry that already lists the replacement edge WITH a trailing +# comment is the same edge: moving the stale one must not add it again. +mkdir -p "$fx/sp26/.github/workflows" +printf 'jobs:\n rust:\n uses: hyperpolymath/standards/.github/workflows/rust-ci-reusable.yml@%s\n done:\n uses: hyperpolymath/standards/.github/workflows/mirror-reusable.yml@%s\n' "$STALE_A" "$GOOD" > "$fx/sp26/.github/workflows/ci.yml" +cat > "$fx/sp26/.github/workflows/actions.lock" < "$fx/ls19/.github/workflows/ci.yml" +cat > "$fx/ls19/.github/workflows/actions.lock" < "$fx/ls19-tags.tsv" +TAG_MAP="$fx/ls19-tags.tsv" run_fix bash "$fleet/scripts/fix-lock-self-sha-refs.sh" "$fx/ls19" --offline +check "ls19 a mixed-case action meets its lower-case tag-map row offline: repaired, exit 0" \ + "[ \$rc -eq 0 ] && grep -q \"^ ref: 'v7.0.1'\$\" '$fx/ls19/.github/workflows/actions.lock' && ! grep -q UNRESOLVED <<<\"\$out\"" + +# ls20: a flow `steps:` sequence spread over lines is read to its closing +# bracket: an estate repository used there is an action, so its self-SHA +# record gets the tag instead of falling back to "estate reusable source" +# and being reported OK. The block reusable call after it keeps its SHA. +lock_records() { # ... : a lock with one record per argument + { printf "version: 'v0.0.3'\nworkflows:\n '.github/workflows/ci.yml':\n" + for r in "${@:2}"; do printf " - '%s'\n" "$r"; done + printf "dependencies:\n" + for r in "${@:2}"; do printf " '%s':\n ref: '%s'\n commit: 'sha1-%s'\n" "$r" "${r##*@}" "${r##*@}"; done + } > "$1/.github/workflows/actions.lock" +} +printf 'hyperpolymath/standards\t%s\tv2.0.0\nfoo/bar\t%s\tv1.0.0\nhyperpolymath/cicd-suite\t%s\tv9.9.9\n' "$SHA_A" "$SHA_B" "$SHA_C" > "$fx/ls20-tags.tsv" +mkdir -p "$fx/ls20/.github/workflows" +cat > "$fx/ls20/.github/workflows/ci.yml" < "$fx/ls21/.github/workflows/ci.yml" +lock_records "$fx/ls21" "hyperpolymath/standards@$SHA_A" +TAG_MAP="$fx/ls20-tags.tsv" run_fix bash "$fleet/scripts/fix-lock-self-sha-refs.sh" "$fx/ls21" --offline +check "ls21 an estate action in a one-line steps: [ ] under a block job is an action: tag written" \ + "[ \$rc -eq 0 ] && grep -q \"^ ref: 'v2.0.0'\$\" '$fx/ls21/.github/workflows/actions.lock'" + +# ls22: a KYAML workflow is one flow mapping from its first line: its steps +# are actions and its job-level calls reusable, read through the brackets. +mkdir -p "$fx/ls22/.github/workflows" +cat > "$fx/ls22/.github/workflows/ci.yml" < "$fx/ls23/.github/workflows/ci.yml" +lock_records "$fx/ls23" "hyperpolymath/standards@$SHA_A" +cp "$fx/ls23/.github/workflows/actions.lock" "$fx/ls23-before.lock" +TAG_MAP="$fx/ls20-tags.tsv" run_fix bash "$fleet/scripts/fix-lock-self-sha-refs.sh" "$fx/ls23" --offline +check "ls23 an unclosed flow collection is refused: exit 2, said, lock untouched" \ + "[ \$rc -eq 2 ] && grep -q 'never closes' <<<\"\$out\" && cmp -s '$fx/ls23-before.lock' '$fx/ls23/.github/workflows/actions.lock'" + +# ls24: the estate-source fallback for a record no workflow uses reads the +# repository case-folded: `Hyperpolymath/Standards` is the reusable source +# and keeps its SHA ref, even with a tag-map row that would retag it. +mkdir -p "$fx/ls24/.github/workflows" +printf 'jobs:\n check:\n steps:\n - uses: foo/bar@v1\n' > "$fx/ls24/.github/workflows/ci.yml" +lock_records "$fx/ls24" "Hyperpolymath/Standards@$SHA_A" +TAG_MAP="$fx/ls20-tags.tsv" run_fix bash "$fleet/scripts/fix-lock-self-sha-refs.sh" "$fx/ls24" --offline +check "ls24 an unreferenced Hyperpolymath/Standards record is the estate source in any case: keeps its SHA ref" \ + "[ \$rc -eq 0 ] && grep -q \"^ ref: '$SHA_A'\$\" '$fx/ls24/.github/workflows/actions.lock' && ! grep -q FIXED <<<\"\$out\"" + +# ls25/ls26: a hand-added record may quote its `ref` key, and may carry a +# trailing comment on its own key line; both are the same record. +mkdir -p "$fx/ls25/.github/workflows" +printf 'jobs:\n check:\n steps:\n - uses: foo/bar@%s\n - uses: foo/baz@%s\n' "$SHA_B" "$SHA_C" > "$fx/ls25/.github/workflows/ci.yml" +cat > "$fx/ls25/.github/workflows/actions.lock" < "$fx/ls25-tags.tsv" +TAG_MAP="$fx/ls25-tags.tsv" run_fix bash "$fleet/scripts/fix-lock-self-sha-refs.sh" "$fx/ls25" --offline +check "ls25 a quoted \"ref\": key is the record's ref: rewritten in place, its quotes kept" \ + "[ \$rc -eq 0 ] && grep -q '^ \"ref\": \"v1.0.0\"\$' '$fx/ls25/.github/workflows/actions.lock'" +check "ls26 a record key with a trailing comment is a record: its ref rewritten, the comment kept" \ + "grep -q \"^ 'foo/baz@$SHA_C': # added by hand\$\" '$fx/ls25/.github/workflows/actions.lock' && grep -q \"^ ref: 'v3.0.0'\$\" '$fx/ls25/.github/workflows/actions.lock'" + +# tc21: the pin text inside an ordinary step field is data, not a step the +# parser cannot place: no UNSUPPORTED, and a real bare step beside it is fixed. +mkdir -p "$fx/tc21/.github/workflows" "$fx/tc21b/.github/workflows" +cat > "$fx/tc21/.github/workflows/x.yml" <<'YML' +jobs: + check: + steps: + - name: "fixture dtolnay/rust-toolchain@6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772" + uses: actions/checkout@v4 +YML +cp "$fx/tc21/.github/workflows/x.yml" "$fx/tc21-before.yml" +run_fix bash "$fleet/scripts/fix-rust-toolchain-input.sh" "$fx/tc21" +check "tc21 the pin named in a step's name: string is data: exit 0, no UNSUPPORTED, file untouched" \ + "[ \$rc -eq 0 ] && ! grep -q UNSUPPORTED <<<\"\$out\" && cmp -s '$fx/tc21-before.yml' '$fx/tc21/.github/workflows/x.yml'" +cat > "$fx/tc21b/.github/workflows/x.yml" <<'YML' +jobs: + check: + steps: + - name: "pins dtolnay/rust-toolchain@6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772" + uses: dtolnay/rust-toolchain@6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772 +YML +run_fix bash "$fleet/scripts/fix-rust-toolchain-input.sh" "$fx/tc21b" +check "tc21b the same name: beside the real bare step: the step is repaired, the name kept" \ + "[ \$rc -eq 0 ] && ! grep -q UNSUPPORTED <<<\"\$out\" && grep -q 'toolchain: stable' '$fx/tc21b/.github/workflows/x.yml' && grep -q '^ - name: \"pins dtolnay/rust-toolchain@6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772\"\$' '$fx/tc21b/.github/workflows/x.yml'" + +# sp27: the fallback for pins the parser cannot place reads each `uses:` +# site of the masked line. A dead ref inside `name:` beside a live `uses:` +# is text (no UNSUPPORTED); a live first site does not hide a dead second +# one; and a flow brace right after the SHA ends the ref. +RC_REUSABLE=hyperpolymath/standards/.github/workflows/rust-ci-reusable.yml +mkdir -p "$fx/sp27a/.github/workflows" "$fx/sp27b/.github/workflows" "$fx/sp27c/.github/workflows" +printf 'jobs:\n call: { name: "%s@%s", uses: %s@%s }\n' "$RC_REUSABLE" "$STALE_A" "$RC_REUSABLE" "$GOOD" > "$fx/sp27a/.github/workflows/ci.yml" +cp "$fx/sp27a/.github/workflows/ci.yml" "$fx/sp27a-before.yml" +run_fix bash "$fleet/scripts/fix-stale-reusable-pin.sh" "$fx/sp27a" +check "sp27a a dead ref inside name: beside a live flow uses: is text: exit 0, no UNSUPPORTED, untouched" \ + "[ \$rc -eq 0 ] && ! grep -q UNSUPPORTED <<<\"\$out\" && cmp -s '$fx/sp27a-before.yml' '$fx/sp27a/.github/workflows/ci.yml'" +printf 'jobs: { a: { uses: %s@%s }, b: { uses: %s@%s } }\n' "$RC_REUSABLE" "$GOOD" "$RC_REUSABLE" "$STALE_A" > "$fx/sp27b/.github/workflows/ci.yml" +run_fix bash "$fleet/scripts/fix-stale-reusable-pin.sh" "$fx/sp27b" +check "sp27b a dead second flow site behind a live first one is UNSUPPORTED: exit 2" \ + "[ \$rc -eq 2 ] && grep -q \"UNSUPPORTED.*@$STALE_A\" <<<\"\$out\"" +printf 'jobs:\n call: { uses: %s@%s}\n' "$RC_REUSABLE" "$STALE_A" > "$fx/sp27c/.github/workflows/ci.yml" +run_fix bash "$fleet/scripts/fix-stale-reusable-pin.sh" "$fx/sp27c" +check "sp27c a dead flow site closed by a brace with no space is UNSUPPORTED, never OK: exit 2" \ + "[ \$rc -eq 2 ] && grep -q UNSUPPORTED <<<\"\$out\"" + +# sp28: `#` with no whitespace before it is part of a plain scalar, so +# `#patched` is another ref the table never named: left alone. +mkdir -p "$fx/sp28/.github/workflows" +printf 'jobs:\n rust:\n uses: %s@%s#patched\n' "$RC_REUSABLE" "$STALE_A" > "$fx/sp28/.github/workflows/ci.yml" +cp "$fx/sp28/.github/workflows/ci.yml" "$fx/sp28-before.yml" +run_fix bash "$fleet/scripts/fix-stale-reusable-pin.sh" "$fx/sp28" +check "sp28 #patched is a different ref: not rewritten, exit 0" \ + "[ \$rc -eq 0 ] && cmp -s '$fx/sp28-before.yml' '$fx/sp28/.github/workflows/ci.yml'" + +# sp29: a lock record or workflow key with a trailing comment is a key: +# the merger keeps the unrelated commented record (it used to vanish), and +# the commented workflow key still carries the edge that moves. +mkdir -p "$fx/sp29/.github/workflows" +printf 'jobs:\n rust:\n uses: %s@%s\n' "$RC_REUSABLE" "$STALE_A" > "$fx/sp29/.github/workflows/ci.yml" +cat > "$fx/sp29/.github/workflows/actions.lock" < "$fx/sp30/.github/workflows/ci.yml" +cp "$fx/sp30/.github/workflows/ci.yml" "$fx/sp30-before.yml" +printf 'fixture/reusable/.github/workflows/ci.yml\t%s\t%s\texact\nfixture/reusable/.github/workflows/*\t%s\t%s\twildcard\n' "$STALE_A" "$GOOD" "$STALE_A" "$SHA_B" > "$fx/sp30-pins.tsv" +run_fix env PINS_TABLE="$fx/sp30-pins.tsv" bash "$fleet/scripts/fix-stale-reusable-pin.sh" "$fx/sp30" +check "sp30 one repository@stale sent to two replacements: CONFLICT, exit 2, nothing rewritten" \ + "[ \$rc -eq 2 ] && grep -q 'CONFLICT' <<<\"\$out\" && ! grep -q '^FIXED' <<<\"\$out\" && cmp -s '$fx/sp30-before.yml' '$fx/sp30/.github/workflows/ci.yml'" + +# sp31: the stale edge quoted in another edge's trailing comment is not the +# edge: the entry that lacks it is reported out of sync, and the other edge +# is kept as written (it used to be moved, then dropped as a duplicate). +mkdir -p "$fx/sp31/.github/workflows" +printf 'jobs:\n rust:\n uses: %s@%s\n' "$RC_REUSABLE" "$STALE_A" > "$fx/sp31/.github/workflows/ci.yml" +cat > "$fx/sp31/.github/workflows/actions.lock" < "$fx/sp32/.github/workflows/ci.yml" +cat > "$fx/sp32/.github/workflows/actions.lock" < "$fx/sp33/.github/workflows/ci.yml" +cp "$fx/sp33/.github/workflows/ci.yml" "$fx/sp33-before.yml" +run_fix bash "$fleet/scripts/fix-stale-reusable-pin.sh" "$fx/sp33" +check "sp33 an anchored dead reusable value is UNSUPPORTED: exit 2, file untouched, never OK" \ + "[ \$rc -eq 2 ] && grep -q 'UNSUPPORTED.*a value carrying an anchor or tag' <<<\"\$out\" && cmp -s '$fx/sp33-before.yml' '$fx/sp33/.github/workflows/ci.yml'" +printf 'jobs:\n check:\n steps:\n - uses: &rt dtolnay/rust-toolchain@6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772\n' > "$fx/tc22/.github/workflows/x.yml" +cp "$fx/tc22/.github/workflows/x.yml" "$fx/tc22-before.yml" +run_fix bash "$fleet/scripts/fix-rust-toolchain-input.sh" "$fx/tc22" +check "tc22 an anchored SHA-pinned toolchain step is UNSUPPORTED: exit 2, file untouched, never OK" \ + "[ \$rc -eq 2 ] && grep -q UNSUPPORTED <<<\"\$out\" && cmp -s '$fx/tc22-before.yml' '$fx/tc22/.github/workflows/x.yml'" + +# ls27: the self-SHA fixer reads an anchored `uses:` value as the site it +# is: the step is an action (tag written), the job call a reusable (kept). +mkdir -p "$fx/ls27/.github/workflows" +cat > "$fx/ls27/.github/workflows/ci.yml" <#patched` is another ref, not a reusable call at +# that would keep the action's self-SHA record from its tag. +mkdir -p "$fx/ls28/.github/workflows" +cat > "$fx/ls28/.github/workflows/ci.yml" < "$fx/ls28-tags.tsv" +TAG_MAP="$fx/ls28-tags.tsv" run_fix bash "$fleet/scripts/fix-lock-self-sha-refs.sh" "$fx/ls28" --offline +check "ls28 an unspaced # keeps the ref whole: #patched is no reusable call at , the action gets its tag" \ + "[ \$rc -eq 0 ] && grep -q \"^ ref: 'v1.2.3'\$\" '$fx/ls28/.github/workflows/actions.lock'" + +# sp34: a row that moves a SHA to itself repairs nothing: the table is +# refused rather than every caller reported FIXED on every run. +mkdir -p "$fx/sp34/.github/workflows" +printf 'jobs:\n rust:\n uses: %s@%s\n' "$RC_REUSABLE" "$STALE_A" > "$fx/sp34/.github/workflows/ci.yml" +printf 'hyperpolymath/standards/.github/workflows/rust-ci-reusable.yml\t%s\t%s\tself\n' "$STALE_A" "$STALE_A" > "$fx/sp34-pins.tsv" +run_fix env PINS_TABLE="$fx/sp34-pins.tsv" bash "$fleet/scripts/fix-stale-reusable-pin.sh" "$fx/sp34" +check "sp34 a row whose replacement equals its stale SHA is refused: exit 2, nothing FIXED" \ + "[ \$rc -eq 2 ] && grep -q 'replacement equals the stale SHA' <<<\"\$out\" && ! grep -q '^FIXED' <<<\"\$out\"" + +# mode: a repaired workflow or lock keeps the mode it had (a mktemp file is +# 0600; renamed as it is, the file would be unreadable to the checkout's +# other users). 0640 is no default either path could produce by chance. +mkdir -p "$fx/mode-tc/.github/workflows" "$fx/mode-sp/.github/workflows" "$fx/mode-ls/.github/workflows" +printf 'jobs:\n check:\n steps:\n - uses: dtolnay/rust-toolchain@6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772\n' > "$fx/mode-tc/.github/workflows/x.yml" +chmod 0640 "$fx/mode-tc/.github/workflows/x.yml" +run_fix bash "$fleet/scripts/fix-rust-toolchain-input.sh" "$fx/mode-tc" +check "mode: the toolchain fixer's repaired workflow keeps its 0640" \ + "[ \$rc -eq 0 ] && grep -q 'toolchain: stable' '$fx/mode-tc/.github/workflows/x.yml' && [ \$(stat -c %a '$fx/mode-tc/.github/workflows/x.yml') = 640 ]" +printf 'jobs:\n rust:\n uses: %s@%s\n' "$RC_REUSABLE" "$STALE_A" > "$fx/mode-sp/.github/workflows/ci.yml" +lock_records "$fx/mode-sp" "hyperpolymath/standards@$STALE_A" +chmod 0640 "$fx/mode-sp/.github/workflows/ci.yml" "$fx/mode-sp/.github/workflows/actions.lock" +run_fix bash "$fleet/scripts/fix-stale-reusable-pin.sh" "$fx/mode-sp" +check "mode: the stale-pin fixer's repaired workflow and lock keep their 0640" \ + "grep -q '@$GOOD' '$fx/mode-sp/.github/workflows/ci.yml' && [ \$(stat -c %a '$fx/mode-sp/.github/workflows/ci.yml') = 640 ] && [ \$(stat -c %a '$fx/mode-sp/.github/workflows/actions.lock') = 640 ]" +printf 'jobs:\n check:\n steps:\n - uses: foo/bar@%s\n' "$SHA_B" > "$fx/mode-ls/.github/workflows/ci.yml" +lock_records "$fx/mode-ls" "foo/bar@$SHA_B" +chmod 0640 "$fx/mode-ls/.github/workflows/actions.lock" +TAG_MAP="$fx/ls20-tags.tsv" run_fix bash "$fleet/scripts/fix-lock-self-sha-refs.sh" "$fx/mode-ls" --offline +check "mode: the self-SHA fixer's repaired lock keeps its 0640" \ + "[ \$rc -eq 0 ] && grep -q \"^ ref: 'v1.0.0'\$\" '$fx/mode-ls/.github/workflows/actions.lock' && [ \$(stat -c %a '$fx/mode-ls/.github/workflows/actions.lock') = 640 ]" + +# tc23: GitHub resolves `Dtolnay/Rust-Toolchain` to the same action, so the +# step lacks its input all the same: it is repaired, its spelling kept. +mkdir -p "$fx/tc23/.github/workflows" +printf 'jobs:\n check:\n steps:\n - uses: Dtolnay/Rust-Toolchain@6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772\n' > "$fx/tc23/.github/workflows/x.yml" +run_fix bash "$fleet/scripts/fix-rust-toolchain-input.sh" "$fx/tc23" +check "tc23 a SHA-pinned toolchain step spelled in another case is repaired, the spelling kept" \ + "[ \$rc -eq 0 ] && grep -q '^ - uses: Dtolnay/Rust-Toolchain@6c977a6' '$fx/tc23/.github/workflows/x.yml' && grep -q '^ toolchain: stable\$' '$fx/tc23/.github/workflows/x.yml'" + +# tc24 / tc25: a step's `uses:` whose value continues below the key (a +# folded scalar, or the reference alone on the next line) names its action +# where the parser does not read it: UNSUPPORTED, never "every step is fine". +mkdir -p "$fx/tc24/.github/workflows" "$fx/tc25/.github/workflows" +printf 'jobs:\n check:\n steps:\n - uses: >-\n dtolnay/rust-toolchain@6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772\n - run: cargo check\n' > "$fx/tc24/.github/workflows/x.yml" +cp "$fx/tc24/.github/workflows/x.yml" "$fx/tc24-before.yml" +run_fix bash "$fleet/scripts/fix-rust-toolchain-input.sh" "$fx/tc24" +check "tc24 a folded uses: >- toolchain pin is UNSUPPORTED: exit 2, file untouched" \ + "[ \$rc -eq 2 ] && grep -q 'UNSUPPORTED.*line(s): 5\\.' <<<\"\$out\" && cmp -s '$fx/tc24-before.yml' '$fx/tc24/.github/workflows/x.yml'" +printf 'jobs:\n check:\n steps:\n - name: tc\n uses:\n dtolnay/rust-toolchain@6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772\n' > "$fx/tc25/.github/workflows/x.yml" +cp "$fx/tc25/.github/workflows/x.yml" "$fx/tc25-before.yml" +run_fix bash "$fleet/scripts/fix-rust-toolchain-input.sh" "$fx/tc25" +check "tc25 a uses: whose reference sits on the next line is UNSUPPORTED: exit 2, file untouched" \ + "[ \$rc -eq 2 ] && grep -q UNSUPPORTED <<<\"\$out\" && cmp -s '$fx/tc25-before.yml' '$fx/tc25/.github/workflows/x.yml'" + +# tc26 (control): the same text inside a `run: |` script is the script's, +# and a real step after it is still repaired. +mkdir -p "$fx/tc26/.github/workflows" +printf 'jobs:\n check:\n steps:\n - run: |\n cat > fx.yml <-\n dtolnay/rust-toolchain@6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772\n EOF\n - uses: dtolnay/rust-toolchain@6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772\n' > "$fx/tc26/.github/workflows/x.yml" +run_fix bash "$fleet/scripts/fix-rust-toolchain-input.sh" "$fx/tc26" +check "tc26 a folded uses: inside a run: | script is text: the real step is repaired, the script unchanged" \ + "[ \$rc -eq 0 ] && [ \$(grep -c 'toolchain: stable' '$fx/tc26/.github/workflows/x.yml') -eq 1 ] && grep -q '^ uses: >-\$' '$fx/tc26/.github/workflows/x.yml'" + +# sp35: a job's `uses:` written as a folded scalar holds the call on the +# line below: a dead pin there is UNSUPPORTED, never "OK" and left dead. +mkdir -p "$fx/sp35/.github/workflows" +printf 'jobs:\n rust:\n uses: >-\n %s@%s\n' "$RC_REUSABLE" "$STALE_A" > "$fx/sp35/.github/workflows/ci.yml" +cp "$fx/sp35/.github/workflows/ci.yml" "$fx/sp35-before.yml" +run_fix bash "$fleet/scripts/fix-stale-reusable-pin.sh" "$fx/sp35" +check "sp35 a folded uses: >- dead reusable pin is UNSUPPORTED: exit 2, file untouched, never OK" \ + "[ \$rc -eq 2 ] && grep -q 'UNSUPPORTED.*ci.yml:4:.*continued below its key' <<<\"\$out\" && ! grep -q '^OK' <<<\"\$out\" && cmp -s '$fx/sp35-before.yml' '$fx/sp35/.github/workflows/ci.yml'" + +# sp36: a caller at `-patched` is another ref, not the stale SHA: it +# does not keep the stale lock edge next to the replacement. +mkdir -p "$fx/sp36/.github/workflows" +printf 'jobs:\n rust:\n uses: %s@%s\n other:\n uses: hyperpolymath/standards/.github/workflows/other.yml@%s-patched\n' "$RC_REUSABLE" "$STALE_A" "$STALE_A" > "$fx/sp36/.github/workflows/ci.yml" +lock_records "$fx/sp36" "hyperpolymath/standards@$STALE_A" +run_fix bash "$fleet/scripts/fix-stale-reusable-pin.sh" "$fx/sp36" +check "sp36 a -patched caller keeps no stale edge: the edge moves to the replacement" \ + "grep -q \"^ - 'hyperpolymath/standards@$GOOD'\$\" '$fx/sp36/.github/workflows/actions.lock' && ! grep -q \"standards@$STALE_A'\" '$fx/sp36/.github/workflows/actions.lock'" + +# ls29 / ls30: a `uses:` site whose value continues below the key cannot be +# classified, so the record it names would read `unknown` and a reusable's +# SHA ref be retagged as if it were an action's: the lock is left alone. +mkdir -p "$fx/ls29/.github/workflows" "$fx/ls30/.github/workflows" +cat > "$fx/ls29/.github/workflows/ci.yml" <- + foo/reus/.github/workflows/x.yml@$SHA_A +YML +lock_records "$fx/ls29" "foo/reus@$SHA_A" +printf 'foo/reus\t%s\tv1.2.3\n' "$SHA_A" > "$fx/ls29-tags.tsv" +cp "$fx/ls29/.github/workflows/actions.lock" "$fx/ls29-before.lock" +TAG_MAP="$fx/ls29-tags.tsv" run_fix bash "$fleet/scripts/fix-lock-self-sha-refs.sh" "$fx/ls29" --offline +check "ls29 a folded job uses: is refused: exit 2, the reusable's SHA ref not retagged" \ + "[ \$rc -eq 2 ] && grep -q 'ci.yml:3 is a block scalar or continues' <<<\"\$out\" && cmp -s '$fx/ls29-before.lock' '$fx/ls29/.github/workflows/actions.lock'" +cat > "$fx/ls30/.github/workflows/ci.yml" < "$fx/ls31/.github/workflows/ci.yml" <- + foo/reus/.github/workflows/x.yml@$SHA_A + - uses: foo/bar@$SHA_B +YML +lock_records "$fx/ls31" "foo/bar@$SHA_B" +TAG_MAP="$fx/ls20-tags.tsv" run_fix bash "$fleet/scripts/fix-lock-self-sha-refs.sh" "$fx/ls31" --offline +check "ls31 a folded uses: inside run: | is text: the action's record still gets its tag" \ + "[ \$rc -eq 0 ] && grep -q \"^ ref: 'v1.0.0'\$\" '$fx/ls31/.github/workflows/actions.lock'" + +# tc27: an alias of an anchor that holds the SHA-pinned toolchain reference +# (`uses: *rust`) runs that action all the same: UNSUPPORTED, never "fine". +mkdir -p "$fx/tc27/.github/workflows" +printf 'env: { ACTION: &rust dtolnay/rust-toolchain@6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772 }\njobs:\n check:\n steps:\n - uses: *rust\n' > "$fx/tc27/.github/workflows/x.yml" +cp "$fx/tc27/.github/workflows/x.yml" "$fx/tc27-before.yml" +run_fix bash "$fleet/scripts/fix-rust-toolchain-input.sh" "$fx/tc27" +check "tc27 an aliased SHA-pinned toolchain step is UNSUPPORTED: exit 2, file untouched" \ + "[ \$rc -eq 2 ] && grep -q 'UNSUPPORTED.*line(s): 5\\.' <<<\"\$out\" && cmp -s '$fx/tc27-before.yml' '$fx/tc27/.github/workflows/x.yml'" + +# tc28 / tc29: TOOLCHAIN is written as a YAML value, so it must be a +# toolchain name; a number YAML would read as a float is written quoted. +mkdir -p "$fx/tc28/.github/workflows" "$fx/tc29/.github/workflows" +printf 'jobs:\n check:\n steps:\n - uses: dtolnay/rust-toolchain@6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772\n' > "$fx/tc28/.github/workflows/x.yml" +cp "$fx/tc28/.github/workflows/x.yml" "$fx/tc28-before.yml" +cp "$fx/tc28/.github/workflows/x.yml" "$fx/tc29/.github/workflows/x.yml" +run_fix env TOOLCHAIN=$'stable\n timeout-minutes: 1' bash "$fleet/scripts/fix-rust-toolchain-input.sh" "$fx/tc28" +check "tc28 a TOOLCHAIN that is not a toolchain name is refused: exit 2, file untouched" \ + "[ \$rc -eq 2 ] && grep -q 'is not a toolchain name' <<<\"\$out\" && cmp -s '$fx/tc28-before.yml' '$fx/tc28/.github/workflows/x.yml'" +run_fix env TOOLCHAIN=1.80 bash "$fleet/scripts/fix-rust-toolchain-input.sh" "$fx/tc29" +check "tc29 TOOLCHAIN=1.80 is written quoted, never read as the float 1.8" \ + "[ \$rc -eq 0 ] && grep -q \"^ toolchain: '1.80'\$\" '$fx/tc29/.github/workflows/x.yml'" + +# sp37: a job calling `uses: *call`, the anchor holding a known-dead pin: +# UNSUPPORTED, never "OK" with the dead caller left in place. +mkdir -p "$fx/sp37/.github/workflows" +printf 'env: { CALL: &call %s@%s }\njobs:\n rust:\n uses: *call\n' "$RC_REUSABLE" "$STALE_A" > "$fx/sp37/.github/workflows/ci.yml" +cp "$fx/sp37/.github/workflows/ci.yml" "$fx/sp37-before.yml" +run_fix bash "$fleet/scripts/fix-stale-reusable-pin.sh" "$fx/sp37" +check "sp37 an aliased dead reusable call is UNSUPPORTED: exit 2, file untouched, never OK" \ + "[ \$rc -eq 2 ] && grep -q 'UNSUPPORTED.*ci.yml:4:.*uses: \\*call' <<<\"\$out\" && ! grep -q '^OK' <<<\"\$out\" && cmp -s '$fx/sp37-before.yml' '$fx/sp37/.github/workflows/ci.yml'" + +# ls32 / ls33: an aliased job call is classified by its anchor's value, so a +# reusable called through `uses: *call` keeps its SHA ref; an alias with no +# anchor read before it cannot be classified and the lock is left alone. +mkdir -p "$fx/ls32/.github/workflows" "$fx/ls33/.github/workflows" +cat > "$fx/ls32/.github/workflows/ci.yml" < "$fx/ls33/.github/workflows/ci.yml" +lock_records "$fx/ls33" "foo/reus@$SHA_A" +cp "$fx/ls33/.github/workflows/actions.lock" "$fx/ls33-before.lock" +TAG_MAP="$fx/ls29-tags.tsv" run_fix bash "$fleet/scripts/fix-lock-self-sha-refs.sh" "$fx/ls33" --offline +check "ls33 an alias of no anchor is refused: exit 2, lock untouched" \ + "[ \$rc -eq 2 ] && cmp -s '$fx/ls33-before.lock' '$fx/ls33/.github/workflows/actions.lock'" + +# tc30: TOOLCHAIN=null is a name YAML reads as null: written quoted, so the +# input is the string, and a second run finds it present (no FIXED again). +mkdir -p "$fx/tc30/.github/workflows" +printf 'jobs:\n check:\n steps:\n - uses: dtolnay/rust-toolchain@6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772\n' > "$fx/tc30/.github/workflows/x.yml" +run_fix env TOOLCHAIN=null bash "$fleet/scripts/fix-rust-toolchain-input.sh" "$fx/tc30" +first_rc=$rc +run_fix env TOOLCHAIN=null bash "$fleet/scripts/fix-rust-toolchain-input.sh" "$fx/tc30" +check "tc30 TOOLCHAIN=null is written quoted, and a rerun repairs nothing" \ + "[ $first_rc -eq 0 ] && [ \$rc -eq 0 ] && grep -q \"^ toolchain: 'null'\$\" '$fx/tc30/.github/workflows/x.yml' && ! grep -q '^FIXED' <<<\"\$out\"" + +# tc31 / sp38 / ls34: anchor-like text inside a quoted scalar is text, not +# the anchor an alias resolves to (lib/yaml-anchors.awk). +mkdir -p "$fx/tc31/.github/workflows" "$fx/sp38/.github/workflows" "$fx/ls34/.github/workflows" +printf 'env:\n ACTION: &rust dtolnay/rust-toolchain@stable\n NOTE: "fixture &rust dtolnay/rust-toolchain@6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772"\njobs:\n check:\n steps:\n - uses: *rust\n' > "$fx/tc31/.github/workflows/x.yml" +cp "$fx/tc31/.github/workflows/x.yml" "$fx/tc31-before.yml" +run_fix bash "$fleet/scripts/fix-rust-toolchain-input.sh" "$fx/tc31" +check "tc31 a quoted &rust is no anchor: the alias of a tag-pinned anchor is fine, not UNSUPPORTED" \ + "[ \$rc -eq 0 ] && ! grep -q UNSUPPORTED <<<\"\$out\" && cmp -s '$fx/tc31-before.yml' '$fx/tc31/.github/workflows/x.yml'" +printf 'env:\n CALL: &call %s@%s\n NOTE: "fixture &call %s@%s"\njobs:\n rust:\n uses: *call\n' "$RC_REUSABLE" "$GOOD" "$RC_REUSABLE" "$STALE_A" > "$fx/sp38/.github/workflows/ci.yml" +run_fix bash "$fleet/scripts/fix-stale-reusable-pin.sh" "$fx/sp38" +check "sp38 a quoted &call naming a dead pin is no anchor: the live alias is not UNSUPPORTED" \ + "[ \$rc -eq 0 ] && ! grep -q UNSUPPORTED <<<\"\$out\"" +cat > "$fx/ls34/.github/workflows/ci.yml" < "$fx/ls34-tags.tsv" +TAG_MAP="$fx/ls34-tags.tsv" run_fix bash "$fleet/scripts/fix-lock-self-sha-refs.sh" "$fx/ls34" --offline +check "ls34 a quoted &call does not replace the real anchor: the aliased reusable keeps its SHA ref" \ + "[ \$rc -eq 0 ] && grep -q \"^ ref: '$SHA_A'\$\" '$fx/ls34/.github/workflows/actions.lock'" + +# tc32 / tc33: anchor names are case-sensitive (`&Rust` is not `&rust`), and +# an alias of no anchor read could be the pinned toolchain: UNSUPPORTED. +mkdir -p "$fx/tc32/.github/workflows" "$fx/tc33/.github/workflows" +printf 'env:\n A: &Rust dtolnay/rust-toolchain@6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772\n B: &rust dtolnay/rust-toolchain@stable\njobs:\n check:\n steps:\n - uses: *Rust\n' > "$fx/tc32/.github/workflows/x.yml" +run_fix bash "$fleet/scripts/fix-rust-toolchain-input.sh" "$fx/tc32" +check "tc32 *Rust is the SHA-pinned &Rust, not the tag-pinned &rust: UNSUPPORTED" \ + "[ \$rc -eq 2 ] && grep -q 'UNSUPPORTED.*line(s): 7\\.' <<<\"\$out\"" +# The anchor sits mid-line in a flow sequence continued from the line +# above, where the line-local reader does not take `&` for an anchor; the +# file does carry the pinned reference, so the unread alias is reported. +printf 'x-list: [a,\n b, &rust dtolnay/rust-toolchain@6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772]\njobs:\n check:\n steps:\n - uses: *rust\n' > "$fx/tc33/.github/workflows/x.yml" +run_fix bash "$fleet/scripts/fix-rust-toolchain-input.sh" "$fx/tc33" +check "tc33 an alias of no anchor read, in a file carrying a SHA-pinned toolchain ref, is UNSUPPORTED" \ + "[ \$rc -eq 2 ] && grep -q UNSUPPORTED <<<\"\$out\"" + +# sp39: a nested `uses: # rationale` header is the list header: two records +# merging onto the replacement leave one header, its comment kept. +mkdir -p "$fx/sp39/.github/workflows" +printf 'jobs:\n governance:\n uses: hyperpolymath/standards/.github/workflows/governance-reusable.yml@%s\n' "$STALE_B" > "$fx/sp39/.github/workflows/governance.yml" +printf 'jobs:\n scorecard:\n uses: hyperpolymath/standards/.github/workflows/scorecard-reusable.yml@%s\n' "$STALE_C" > "$fx/sp39/.github/workflows/scorecard.yml" +cat > "$fx/sp39/.github/workflows/actions.lock" </dev/null 2>&1 || true +check "sp39 a commented nested uses: header is the one header of the merged record" \ + "[ \$(grep -c '^ uses:' '$fx/sp39/.github/workflows/actions.lock') -eq 1 ] && grep -q '^ uses: # transitive actions of the reusable\$' '$fx/sp39/.github/workflows/actions.lock' && grep -q 'ossf/scorecard-action' '$fx/sp39/.github/workflows/actions.lock'" + +# ls35 / ls36: `&call` inside a plain scalar is text; a tag YAML would read +# as a non-string is written quoted into a plain ref field. +mkdir -p "$fx/ls35/.github/workflows" "$fx/ls36/.github/workflows" +cat > "$fx/ls35/.github/workflows/ci.yml" < "$fx/ls36/.github/workflows/ci.yml" +printf "version: 'v0.0.3'\nworkflows:\n '.github/workflows/ci.yml':\n - 'foo/bar@%s'\ndependencies:\n 'foo/bar@%s':\n ref: %s\n commit: 'sha1-%s'\n" "$SHA_B" "$SHA_B" "$SHA_B" "$SHA_B" > "$fx/ls36/.github/workflows/actions.lock" +printf 'foo/bar\t%s\ttrue\n' "$SHA_B" > "$fx/ls36-tags.tsv" +TAG_MAP="$fx/ls36-tags.tsv" run_fix bash "$fleet/scripts/fix-lock-self-sha-refs.sh" "$fx/ls36" --offline +check "ls36 a tag YAML reads as a boolean is written quoted into a plain ref field" \ + "[ \$rc -eq 0 ] && grep -q \"^ ref: 'true'\$\" '$fx/ls36/.github/workflows/actions.lock'" + +# sp40 / sp41: a nested `uses:` list written flow-style merges like a block +# one — the merged record keeps ONE `uses:` key and every entry: `uses: []` +# on the first record does not stand beside the block list folded into it, +# and a flow list on the record folded away is not dropped with it. +sp_flow_fixture() { # + mkdir -p "$1/.github/workflows" + printf 'jobs:\n governance:\n uses: hyperpolymath/standards/.github/workflows/governance-reusable.yml@%s\n' "$STALE_B" > "$1/.github/workflows/governance.yml" + printf 'jobs:\n scorecard:\n uses: hyperpolymath/standards/.github/workflows/scorecard-reusable.yml@%s\n' "$STALE_C" > "$1/.github/workflows/scorecard.yml" + printf "version: 'v0.0.3'\nworkflows:\n '.github/workflows/governance.yml':\n - 'hyperpolymath/standards@%s'\n '.github/workflows/scorecard.yml':\n - 'hyperpolymath/standards@%s'\ndependencies:\n 'hyperpolymath/standards@%s':\n ref: '%s'\n commit: 'sha1-%s'\n%b 'hyperpolymath/standards@%s':\n ref: '%s'\n commit: 'sha1-%s'\n%b" \ + "$STALE_B" "$STALE_C" "$STALE_B" "$STALE_B" "$STALE_B" "$2" "$STALE_C" "$STALE_C" "$STALE_C" "$3" > "$1/.github/workflows/actions.lock" +} +sp_flow_fixture "$fx/sp40" " uses: []\n" " uses:\n - 'ossf/scorecard-action@2d1146689b8cda280b9bc96326124645441f03bc'\n" +run_fix bash "$fleet/scripts/fix-stale-reusable-pin.sh" "$fx/sp40" +check "sp40 uses: [] on the first record and a block list on the merged one leave one uses: key holding the entry" \ + "[ \$rc -eq 3 ] && [ \$(grep -c '^ uses:' '$fx/sp40/.github/workflows/actions.lock') -eq 1 ] && grep -q \"^ - 'ossf/scorecard-action@2d1146689b8cda280b9bc96326124645441f03bc'\\\$\" '$fx/sp40/.github/workflows/actions.lock'" +sp_flow_fixture "$fx/sp41" " uses:\n - 'actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1'\n" " uses: ['ossf/scorecard-action@2d1146689b8cda280b9bc96326124645441f03bc', \"actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1\"]\n" +run_fix bash "$fleet/scripts/fix-stale-reusable-pin.sh" "$fx/sp41" +check "sp41 a one-line flow list on the record folded away merges into the block list: one key, each entry once" \ + "[ \$rc -eq 3 ] && [ \$(grep -c '^ uses:' '$fx/sp41/.github/workflows/actions.lock') -eq 1 ] && grep -q \"^ - 'ossf/scorecard-action@2d1146689b8cda280b9bc96326124645441f03bc'\\\$\" '$fx/sp41/.github/workflows/actions.lock' && [ \$(grep -c 'actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1' '$fx/sp41/.github/workflows/actions.lock') -eq 1 ]" +sp_flow_fixture "$fx/sp41b" " uses: [] # none yet\n" " uses: []\n" +run_fix bash "$fleet/scripts/fix-stale-reusable-pin.sh" "$fx/sp41b" +check "sp41b two empty flow lists merge to the first one as written" \ + "[ \$rc -eq 3 ] && [ \$(grep -c '^ uses:' '$fx/sp41b/.github/workflows/actions.lock') -eq 1 ] && grep -q '^ uses: \\[\\] # none yet\$' '$fx/sp41b/.github/workflows/actions.lock'" + +# sp42: a record listed twice whose `uses:` is a flow list over several lines +# is a shape the merger cannot fold. It is not folded with its entries lost: +# the duplicates stay, the record is named UNSUPPORTED, exit 2. +sp_flow_fixture "$fx/sp42" " uses:\n - 'actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1'\n" " uses: [\n 'ossf/scorecard-action@2d1146689b8cda280b9bc96326124645441f03bc' ]\n" +run_fix bash "$fleet/scripts/fix-stale-reusable-pin.sh" "$fx/sp42" +check "sp42 a duplicated record with a multi-line flow uses: is refused, named and left with every entry" \ + "[ \$rc -eq 2 ] && grep -q \"UNSUPPORTED .*actions.lock: the record hyperpolymath/standards@$GOOD is listed 2 times\" <<<\"\$out\" && grep -q 'ossf/scorecard-action@2d1146689b8cda280b9bc96326124645441f03bc' '$fx/sp42/.github/workflows/actions.lock' && grep -q 'actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1' '$fx/sp42/.github/workflows/actions.lock'" + +# sp43 (control): a record with a flow list and nothing merged into it is +# written back exactly as it was, not re-spelled as a block list. +mkdir -p "$fx/sp43/.github/workflows" +printf 'jobs:\n governance:\n uses: hyperpolymath/standards/.github/workflows/governance-reusable.yml@%s\n' "$STALE_B" > "$fx/sp43/.github/workflows/governance.yml" +printf "version: 'v0.0.3'\nworkflows:\n '.github/workflows/governance.yml':\n - 'hyperpolymath/standards@%s'\ndependencies:\n 'hyperpolymath/standards@%s':\n ref: '%s'\n commit: 'sha1-%s'\n uses: ['b/b@3d3c42e5aac5ba805825da76410c181273ba90b1', 'a/a@3d3c42e5aac5ba805825da76410c181273ba90b1']\n" \ + "$STALE_B" "$STALE_B" "$STALE_B" "$STALE_B" > "$fx/sp43/.github/workflows/actions.lock" +run_fix bash "$fleet/scripts/fix-stale-reusable-pin.sh" "$fx/sp43" +check "sp43 a flow list nothing was merged into is kept as written" \ + "[ \$rc -eq 3 ] && grep -qF \" uses: ['b/b@3d3c42e5aac5ba805825da76410c181273ba90b1', 'a/a@3d3c42e5aac5ba805825da76410c181273ba90b1']\" '$fx/sp43/.github/workflows/actions.lock'" + +# pl4: without flock the lock is a directory created with mkdir — still one +# fixer per checkout. A held lock is waited for, reported with its holder, +# and the run exits 2 having changed nothing; a free one is taken, and the +# run removes it on exit. +nofl="$fx/no-flock-bin"; mkdir -p "$nofl" +IFS=: read -ra pl4_dirs <<<"$PATH" +for d in "${pl4_dirs[@]}"; do [ -d "$d" ] && { ln -s "$d"/* "$nofl"/ 2>/dev/null || true; }; done +rm -f "$nofl/flock" +mkdir -p "$fx/pl4/.github/workflows" +printf 'jobs:\n rust:\n uses: hyperpolymath/standards/.github/workflows/rust-ci-reusable.yml@%s\n' "$STALE_A" > "$fx/pl4/.github/workflows/ci.yml" +cp "$fx/pl4/.github/workflows/ci.yml" "$fx/pl4-before.yml" +pl4_key=$(cd "$fx/pl4" && pwd -P | tr -d '\n' | sha256sum | cut -c1-16) +mkdir "$fx/pin-fixer-$pl4_key.lock.d"; echo 4242 > "$fx/pin-fixer-$pl4_key.lock.d/pid" +PATH="$nofl" PIN_FIXER_LOCK_DIR="$fx" PIN_FIXER_LOCK_WAIT=1 run_fix bash "$fleet/scripts/fix-stale-reusable-pin.sh" "$fx/pl4" +check "pl4 without flock a held lock directory makes the fixer report its holder, exit 2 and change nothing" \ + "[ \$rc -eq 2 ] && grep -q 'another workflow-pin fixer holds.*held by pid 4242' <<<\"\$out\" && cmp -s '$fx/pl4-before.yml' '$fx/pl4/.github/workflows/ci.yml'" +rm -rf "$fx/pin-fixer-$pl4_key.lock.d" +PATH="$nofl" PIN_FIXER_LOCK_DIR="$fx" PIN_FIXER_LOCK_WAIT=1 run_fix bash "$fleet/scripts/fix-stale-reusable-pin.sh" "$fx/pl4" +check "pl4 without flock a free lock is taken, the run repairs, and the lock directory is gone afterwards" \ + "[ \$rc -eq 0 ] && grep -q \"rust-ci-reusable.yml@$GOOD\" '$fx/pl4/.github/workflows/ci.yml' && [ ! -e '$fx/pin-fixer-$pl4_key.lock.d' ]" + +# ── Review cases (gitbot-fleet#615, round on ccfc924) ─────────────────────── +# sp44: a stale record spelled `Hyperpolymath/Standards@` renamed onto +# the replacement that `hyperpolymath/standards@` already holds is the +# same dependency — GitHub reads the repository in either case — so the two +# records merge into one, the first spelling kept. +mkdir -p "$fx/sp44/.github/workflows" +printf 'jobs:\n governance:\n uses: hyperpolymath/standards/.github/workflows/governance-reusable.yml@%s\n' "$STALE_B" > "$fx/sp44/.github/workflows/governance.yml" +cat > "$fx/sp44/.github/workflows/actions.lock" < "$fx/tc34/.github/workflows/x.yml" <<'YML' +jobs: + check: + steps: + - uses: dtolnay/rust-toolchain@6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772 + with: + { components: rustfmt } + - uses: dtolnay/rust-toolchain@6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772 + with: + # shared inputs + *inputs +YML +cp "$fx/tc34/.github/workflows/x.yml" "$fx/tc34-before.yml" +run_fix bash "$fleet/scripts/fix-rust-toolchain-input.sh" "$fx/tc34" +check "tc34 with: holding a flow mapping or an alias on the next line is reported, exit 2, file untouched" \ + "[ \$rc -eq 2 ] && grep -q 'UNSUPPORTED.*line(s): 6 10' <<<\"\$out\" && cmp -s '$fx/tc34-before.yml' '$fx/tc34/.github/workflows/x.yml'" +printf 'jobs:\n check:\n steps:\n - uses: dtolnay/rust-toolchain@6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772\n with:\n components: rustfmt\n' > "$fx/tc34b/.github/workflows/x.yml" +run_fix bash "$fleet/scripts/fix-rust-toolchain-input.sh" "$fx/tc34b" +check "tc34 (control) a block child on the next line is a block mapping: toolchain added beside it" \ + "[ \$rc -eq 0 ] && grep -q '^ toolchain: stable\$' '$fx/tc34b/.github/workflows/x.yml' && grep -q '^ components: rustfmt\$' '$fx/tc34b/.github/workflows/x.yml'" + +# sp45: an anchor defined after another item of a flow sequence begun on an +# earlier line is one the line-local anchor reader does not see; a job's +# `uses: *call` naming it, in a file that holds a known-dead pin, is +# reported (exit 2, untouched), never passed as OK. The same unreadable +# alias in a file with no known-dead pin is no finding. +mkdir -p "$fx/sp45/.github/workflows" "$fx/sp45b/.github/workflows" +cat > "$fx/sp45/.github/workflows/ci.yml" < "$fx/sp45b/.github/workflows/ci.yml" +run_fix bash "$fleet/scripts/fix-stale-reusable-pin.sh" "$fx/sp45b" +check "sp45 (control) the same alias in a file with no known-dead pin is no finding" \ + "[ \$rc -eq 0 ] && grep -q '^OK (no known-dead reusable pins)' <<<\"\$out\"" + +# pin_fixer_yaml_nonstring: the YAML 1.1 / core-schema plain scalars that are +# not strings — timestamps, signed and dotted numbers, base 60, .inf/.nan, +# ~ and nothing — and names that only look like them. +yn_out=$(bash -c '. "$1"; for v in 2026-10-20 "2026-10-20T10:00:00Z" -1 +2 .5 1:30 .inf -.Inf .NaN "~" "" 0x1F 1e3 null Yes; do pin_fixer_yaml_nonstring "$v" || echo "STRING:$v"; done; for v in v2026-10-20 nightly-2026-10-20 2026-10 1.2.3 stable release v1 1.85.0 .nanx; do pin_fixer_yaml_nonstring "$v" && echo "NONSTRING:$v"; done; true' _ "$fleet/scripts/lib/pin-fixer-lock.sh") +check "pin_fixer_yaml_nonstring reads timestamps, signed/dotted numbers, base 60, .inf/.nan and ~ as non-strings, and only those" \ + "[ -z \"\$yn_out\" ]" + +# ls37: a tag spelled as a date is written quoted into a plain ref field, so +# the lock holds the tag string, not a timestamp. +mkdir -p "$fx/ls37/.github/workflows" +action_steps "$fx/ls37/.github/workflows" "foo/bar@$SHA_B" +printf "version: 'v0.0.3'\nworkflows:\n '.github/workflows/ci.yml':\n - 'foo/bar@%s'\ndependencies:\n 'foo/bar@%s':\n ref: %s\n commit: 'sha1-%s'\n" "$SHA_B" "$SHA_B" "$SHA_B" "$SHA_B" > "$fx/ls37/.github/workflows/actions.lock" +printf 'foo/bar\t%s\t2026-10-20\n' "$SHA_B" > "$fx/ls37-tags.tsv" +TAG_MAP="$fx/ls37-tags.tsv" run_fix bash "$fleet/scripts/fix-lock-self-sha-refs.sh" "$fx/ls37" --offline +check "ls37 a tag spelled as a date is written quoted into a plain ref field" \ + "[ \$rc -eq 0 ] && grep -q \"^ ref: '2026-10-20'\$\" '$fx/ls37/.github/workflows/actions.lock'" + +# ls38: a self-SHA record no workflow here uses (a transitive dependency: a +# reusable's own call, which may be a reusable whose SHA ref is legitimate) +# is not rewritten to a tag on a guess, even with a tag to hand: reported +# UNRESOLVED, the record unchanged, exit 1. +mkdir -p "$fx/ls38/.github/workflows" +action_steps "$fx/ls38/.github/workflows" "actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1" +lock_records "$fx/ls38" "third/party@$SHA_A" +printf 'third/party\t%s\tv1.0.0\n' "$SHA_A" > "$fx/ls38-tags.tsv" +cp "$fx/ls38/.github/workflows/actions.lock" "$fx/ls38-before.lock" +TAG_MAP="$fx/ls38-tags.tsv" run_fix bash "$fleet/scripts/fix-lock-self-sha-refs.sh" "$fx/ls38" --offline +check "ls38 a record no workflow uses is UNRESOLVED and left as is, never rewritten on a guess" \ + "[ \$rc -eq 1 ] && grep -q 'UNRESOLVED third/party@aaaaaaa: no workflow here uses it' <<<\"\$out\" && cmp -s '$fx/ls38-before.lock' '$fx/ls38/.github/workflows/actions.lock'" + +# tc35 / sp46: a `uses:` value carrying a YAML tag (`!!str`), alone or with +# an anchor, is still the step's or job's pin, but not one the rewrite +# places: reported UNSUPPORTED, exit 2, the file untouched — never passed as +# OK. (hypatia WF026/WF027 route such findings to review.) +mkdir -p "$fx/tc35/.github/workflows" "$fx/sp46/.github/workflows" +printf 'jobs:\n check:\n steps:\n - uses: !!str dtolnay/rust-toolchain@6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772\n - uses: !!str &rust "dtolnay/rust-toolchain@6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772"\n' > "$fx/tc35/.github/workflows/x.yml" +cp "$fx/tc35/.github/workflows/x.yml" "$fx/tc35-before.yml" +run_fix bash "$fleet/scripts/fix-rust-toolchain-input.sh" "$fx/tc35" +check "tc35 a tagged uses: value is reported UNSUPPORTED, exit 2, file untouched" \ + "[ \$rc -eq 2 ] && grep -q 'UNSUPPORTED.*line(s): 4 5' <<<\"\$out\" && cmp -s '$fx/tc35-before.yml' '$fx/tc35/.github/workflows/x.yml'" +printf 'jobs:\n rust:\n uses: !!str hyperpolymath/standards/.github/workflows/rust-ci-reusable.yml@%s\n' "$STALE_A" > "$fx/sp46/.github/workflows/ci.yml" +cp "$fx/sp46/.github/workflows/ci.yml" "$fx/sp46-before.yml" +run_fix bash "$fleet/scripts/fix-stale-reusable-pin.sh" "$fx/sp46" +check "sp46 a tagged job uses: holding a known-dead pin is UNSUPPORTED, exit 2, file untouched" \ + "[ \$rc -eq 2 ] && grep -q 'UNSUPPORTED .github/workflows/ci.yml:3: .*a value carrying an anchor or tag' <<<\"\$out\" && cmp -s '$fx/sp46-before.yml' '$fx/sp46/.github/workflows/ci.yml'" + +mkdir -p "$fx/sp46b/.github/workflows" +printf 'jobs:\n rust:\n uses: !!str "hyperpolymath/standards/.github/workflows/rust-ci-reusable.yml@%s"\n' "$STALE_A" > "$fx/sp46b/.github/workflows/ci.yml" +cp "$fx/sp46b/.github/workflows/ci.yml" "$fx/sp46b-before.yml" +run_fix bash "$fleet/scripts/fix-stale-reusable-pin.sh" "$fx/sp46b" +check "sp46 a quoted value after a tag is the job's value too: UNSUPPORTED, exit 2, untouched" \ + "[ \$rc -eq 2 ] && grep -q 'UNSUPPORTED .github/workflows/ci.yml:3: ' <<<\"\$out\" && cmp -s '$fx/sp46b-before.yml' '$fx/sp46b/.github/workflows/ci.yml'" +# ls39: a step whose `uses:` value carries a tag still proves its record an +# action: the lock's self-SHA ref gets its tag. +mkdir -p "$fx/ls39/.github/workflows" +printf 'jobs:\n build:\n steps:\n - uses: !!str "foo/bar@%s"\n' "$SHA_B" > "$fx/ls39/.github/workflows/ci.yml" +lock_records "$fx/ls39" "foo/bar@$SHA_B" +printf 'foo/bar\t%s\tv1.0.0\n' "$SHA_B" > "$fx/ls39-tags.tsv" +TAG_MAP="$fx/ls39-tags.tsv" run_fix bash "$fleet/scripts/fix-lock-self-sha-refs.sh" "$fx/ls39" --offline +check "ls39 a tagged step value still classifies its record as an action: the ref gets its tag" \ + "[ \$rc -eq 0 ] && grep -q \"^ ref: 'v1.0.0'\$\" '$fx/ls39/.github/workflows/actions.lock'" + +# ── Review cases (gitbot-fleet#615, round on dcfca15) ─────────────────────── +# tc36: an alias names the anchor defined before it. A step whose `uses: +# *rust` follows `&rust dtolnay/rust-toolchain@` runs that reference, +# even when a later line of the same step defines `&rust` again: reported, +# exit 2, file untouched — not judged by the later definition and passed. +mkdir -p "$fx/tc36/.github/workflows" +cat > "$fx/tc36/.github/workflows/x.yml" <<'YML' +env: + RUST: &rust dtolnay/rust-toolchain@6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772 +jobs: + check: + steps: + - uses: *rust + env: + OTHER: &rust actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 +YML +cp "$fx/tc36/.github/workflows/x.yml" "$fx/tc36-before.yml" +run_fix bash "$fleet/scripts/fix-rust-toolchain-input.sh" "$fx/tc36" +check "tc36 an alias is judged by the anchor before it, not a later redefinition in the step" \ + "[ \$rc -eq 2 ] && grep -q 'UNSUPPORTED.*line(s): 6' <<<\"\$out\" && cmp -s '$fx/tc36-before.yml' '$fx/tc36/.github/workflows/x.yml'" + +# sp47: a record whose nested list key is quoted (`"uses":`, `'uses':`) is +# the same list: merging a renamed record into it leaves one `uses` key. +for qk in '"uses"' "'uses'"; do + d="$fx/sp47-$([ "$qk" = '"uses"' ] && echo dq || echo sq)" + sp_flow_fixture "$d" " $qk:\n - 'actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1'\n" " uses:\n - 'ossf/scorecard-action@2d1146689b8cda280b9bc96326124645441f03bc'\n" + run_fix bash "$fleet/scripts/fix-stale-reusable-pin.sh" "$d" + check "sp47 a quoted $qk key is the nested list: one uses key after the merge, both entries kept" \ + "[ \$rc -eq 3 ] && [ \$(grep -cE \"^ [\\\"']?uses[\\\"']?:\" '$d/.github/workflows/actions.lock') -eq 1 ] && grep -q 'ossf/scorecard-action@' '$d/.github/workflows/actions.lock' && grep -q 'actions/checkout@' '$d/.github/workflows/actions.lock'" +done + +# ── Review cases (gitbot-fleet#615, round on d83bce8) ─────────────────────── +# ls40: a record key followed by trailing spaces, in a lock with CRLF line +# endings, is a record: the self-SHA action ref gets its tag, CRLF kept. +mkdir -p "$fx/ls40/.github/workflows" +action_steps "$fx/ls40/.github/workflows" "foo/bar@$SHA_B" +printf "version: 'v0.0.3'\r\nworkflows:\r\n '.github/workflows/ci.yml':\r\n - 'foo/bar@%s'\r\ndependencies:\r\n 'foo/bar@%s': \r\n ref: '%s'\r\n commit: 'sha1-%s'\r\n" "$SHA_B" "$SHA_B" "$SHA_B" "$SHA_B" > "$fx/ls40/.github/workflows/actions.lock" +printf 'foo/bar\t%s\tv1.0.0\n' "$SHA_B" > "$fx/ls40-tags.tsv" +TAG_MAP="$fx/ls40-tags.tsv" run_fix bash "$fleet/scripts/fix-lock-self-sha-refs.sh" "$fx/ls40" --offline +check "ls40 a CRLF lock's record key with trailing spaces is a record: its ref gets its tag, CRLF kept" \ + "[ \$rc -eq 0 ] && grep -q \"^ ref: 'v1.0.0'\"\$'\\r'\"\$\" '$fx/ls40/.github/workflows/actions.lock' && [ \$(grep -c \$'\\r\$' '$fx/ls40/.github/workflows/actions.lock') -eq 8 ]" + +# ls41: in a flow `steps:` sequence only the step mapping's own `uses` is an +# action; a `uses` inside a step input (`with: { uses: ... }`) is data and +# proves nothing — that record stays UNRESOLVED, untouched, exit 1. +mkdir -p "$fx/ls41/.github/workflows" +printf 'jobs:\n build:\n steps: [{ uses: fixture/wrapper@v1, with: { uses: foo/bar/.github/workflows/x.yml@%s } }]\n' "$SHA_A" > "$fx/ls41/.github/workflows/ci.yml" +lock_records "$fx/ls41" "foo/bar@$SHA_A" +printf 'foo/bar\t%s\tv1.0.0\n' "$SHA_A" > "$fx/ls41-tags.tsv" +cp "$fx/ls41/.github/workflows/actions.lock" "$fx/ls41-before.lock" +TAG_MAP="$fx/ls41-tags.tsv" run_fix bash "$fleet/scripts/fix-lock-self-sha-refs.sh" "$fx/ls41" --offline +check "ls41 a uses inside a flow step's input proves no action: the record stays UNRESOLVED and untouched" \ + "[ \$rc -eq 1 ] && grep -q 'UNRESOLVED foo/bar@aaaaaaa' <<<\"\$out\" && cmp -s '$fx/ls41-before.lock' '$fx/ls41/.github/workflows/actions.lock'" + +# tc37: a step opened by `- # comment`, its `uses:` on the next line, is the +# bare `-` layout: repaired, not reported UNSUPPORTED. +mkdir -p "$fx/tc37/.github/workflows" +printf 'jobs:\n check:\n steps:\n - # install Rust\n uses: dtolnay/rust-toolchain@6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772\n - run: cargo test\n' > "$fx/tc37/.github/workflows/x.yml" +run_fix bash "$fleet/scripts/fix-rust-toolchain-input.sh" "$fx/tc37" +check "tc37 a step opened by '- # comment' is repaired like a bare '-' step" \ + "[ \$rc -eq 0 ] && grep -q '^ with:\$' '$fx/tc37/.github/workflows/x.yml' && grep -q '^ toolchain: stable\$' '$fx/tc37/.github/workflows/x.yml'" + +# sp48: a lock with CRLF line endings is edited and merged as one with LF +# endings would be — no record dropped — and written back with CRLF. +for v in lf cr; do + sp_flow_fixture "$fx/sp48-$v" " uses:\n - 'actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1'\n" " uses:\n - 'ossf/scorecard-action@2d1146689b8cda280b9bc96326124645441f03bc'\n" +done +sed -i 's/$/\r/' "$fx/sp48-cr/.github/workflows/actions.lock" +run_fix bash "$fleet/scripts/fix-stale-reusable-pin.sh" "$fx/sp48-lf" +run_fix bash "$fleet/scripts/fix-stale-reusable-pin.sh" "$fx/sp48-cr" +check "sp48 a CRLF lock is repaired and merged exactly as its LF twin, and keeps CRLF" \ + "[ \$rc -eq 3 ] && tr -d '\\r' < '$fx/sp48-cr/.github/workflows/actions.lock' | cmp -s - '$fx/sp48-lf/.github/workflows/actions.lock' && [ \$(grep -c \$'\\r\$' '$fx/sp48-cr/.github/workflows/actions.lock') -eq \$(wc -l < '$fx/sp48-cr/.github/workflows/actions.lock') ] && grep -q 'ossf/scorecard-action@' '$fx/sp48-cr/.github/workflows/actions.lock'" + +# ── Review cases (gitbot-fleet#615, round on 01ee2aa) ─────────────────────── +# pl5: a fixer with flock and one without contend for the same sentinel. A +# run that has flock still waits on the directory a flock-less run holds, +# reports it and exits 2 with nothing changed; once it is free the run +# repairs and removes the sentinel it took. +mkdir -p "$fx/pl5/.github/workflows" +printf 'jobs:\n rust:\n uses: hyperpolymath/standards/.github/workflows/rust-ci-reusable.yml@%s\n' "$STALE_A" > "$fx/pl5/.github/workflows/ci.yml" +cp "$fx/pl5/.github/workflows/ci.yml" "$fx/pl5-before.yml" +pl5_key=$(cd "$fx/pl5" && pwd -P | tr -d '\n' | sha256sum | cut -c1-16) +mkdir "$fx/pin-fixer-$pl5_key.lock.d"; echo 4343 > "$fx/pin-fixer-$pl5_key.lock.d/pid" +PIN_FIXER_LOCK_DIR="$fx" PIN_FIXER_LOCK_WAIT=1 run_fix bash "$fleet/scripts/fix-stale-reusable-pin.sh" "$fx/pl5" +check "pl5 a run with flock waits on the sentinel a flock-less run holds: exit 2, nothing changed" \ + "[ \$rc -eq 2 ] && grep -q 'held by pid 4343' <<<\"\$out\" && cmp -s '$fx/pl5-before.yml' '$fx/pl5/.github/workflows/ci.yml'" +rm -rf "$fx/pin-fixer-$pl5_key.lock.d" +PIN_FIXER_LOCK_DIR="$fx" PIN_FIXER_LOCK_WAIT=1 run_fix bash "$fleet/scripts/fix-stale-reusable-pin.sh" "$fx/pl5" +check "pl5 with the sentinel free the run with flock repairs and removes the sentinel it took" \ + "[ \$rc -eq 0 ] && grep -q \"rust-ci-reusable.yml@$GOOD\" '$fx/pl5/.github/workflows/ci.yml' && [ ! -e '$fx/pin-fixer-$pl5_key.lock.d' ]" + +# sp49: a dependency record key followed by trailing spaces is a record: the +# merger keeps it, with its fields, when it rewrites the lock. +sp_flow_fixture "$fx/sp49" " uses:\n - 'actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1'\n" " uses:\n - 'ossf/scorecard-action@2d1146689b8cda280b9bc96326124645441f03bc'\n 'other/action@3d3c42e5aac5ba805825da76410c181273ba90b1': \n ref: 'v7.0.1'\n commit: 'sha1-3d3c42e5aac5ba805825da76410c181273ba90b1'\n" +run_fix bash "$fleet/scripts/fix-stale-reusable-pin.sh" "$fx/sp49" +check "sp49 a record key with trailing spaces survives the merge with its fields" \ + "[ \$rc -eq 3 ] && grep -q \"^ 'other/action@3d3c42e5aac5ba805825da76410c181273ba90b1':\" '$fx/sp49/.github/workflows/actions.lock' && grep -q \"^ ref: 'v7.0.1'\$\" '$fx/sp49/.github/workflows/actions.lock'" + +# tc38 / sp50: an anchor whose double-quoted value hides the reference behind +# YAML escapes (`"\x64tolnay/..."`) holds that reference, decoded as YAML +# reads it: the alias of it is reported (exit 2), never passed as OK. +mkdir -p "$fx/tc38/.github/workflows" "$fx/sp50/.github/workflows" +cat > "$fx/tc38/.github/workflows/x.yml" <<'YML' +env: + RUST: &rust "\x64tolnay/rust-toolchain@6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772" +jobs: + check: + steps: + - uses: *rust +YML +cp "$fx/tc38/.github/workflows/x.yml" "$fx/tc38-before.yml" +run_fix bash "$fleet/scripts/fix-rust-toolchain-input.sh" "$fx/tc38" +check "tc38 an alias of an escaped double-quoted toolchain anchor is reported, exit 2, file untouched" \ + "[ \$rc -eq 2 ] && grep -q 'UNSUPPORTED.*line(s): 6' <<<\"\$out\" && cmp -s '$fx/tc38-before.yml' '$fx/tc38/.github/workflows/x.yml'" +printf 'env:\n CALL: &call "\\x68yperpolymath/standards/.github/workflows/rust-ci-reusable.yml@%s"\njobs:\n rust:\n uses: *call\n' "$STALE_A" > "$fx/sp50/.github/workflows/ci.yml" +cp "$fx/sp50/.github/workflows/ci.yml" "$fx/sp50-before.yml" +run_fix bash "$fleet/scripts/fix-stale-reusable-pin.sh" "$fx/sp50" +check "sp50 an alias of an escaped double-quoted reusable anchor holding a dead pin is UNSUPPORTED, exit 2" \ + "[ \$rc -eq 2 ] && grep -q 'UNSUPPORTED .github/workflows/ci.yml:5: .*known-dead pin held by the anchor' <<<\"\$out\" && cmp -s '$fx/sp50-before.yml' '$fx/sp50/.github/workflows/ci.yml'" +ya_out=$(printf '%s\n' 'A: &a "\x6fcto/r\u0065po@\"x\"\\\/"' 'B: &b "x\ny\U0001F600z"' | awk "$(cat "$fleet/scripts/lib/yaml-anchors.awk")"'{ anchor_defs($0, d); for (k in d) printf "%s=%s|", k, d[k] }') +check "yaml-anchors decodes ASCII escapes and maps anything else to a byte no reference holds" \ + "[ \"\$ya_out\" = \"\$(printf 'a=octo/repo@\"x\"\\\\/|b=x\\001y\\001z|')\" ]" + +# ── Review cases (gitbot-fleet#615, round on 7547098) ─────────────────────── +# sp51: a dependency record written as a flow mapping on its key line, after +# the duplicate the merger folds away, is a record of its own: kept as +# written, in its place, never dropped with the duplicate's lines. +sp51_inline=" 'other/action@3d3c42e5aac5ba805825da76410c181273ba90b1': { ref: 'v7.0.1', commit: 'sha1-3d3c42e5aac5ba805825da76410c181273ba90b1' }" +sp_flow_fixture "$fx/sp51" " uses:\n - 'actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1'\n" " uses:\n - 'ossf/scorecard-action@2d1146689b8cda280b9bc96326124645441f03bc'\n$sp51_inline\n" +run_fix bash "$fleet/scripts/fix-stale-reusable-pin.sh" "$fx/sp51" +check "sp51 a flow-mapping record after a folded duplicate survives the merge as written" \ + "[ \$rc -eq 3 ] && grep -qxF \"\$sp51_inline\" '$fx/sp51/.github/workflows/actions.lock' && [ \$(grep -c \"^ 'hyperpolymath/standards@$GOOD':\" '$fx/sp51/.github/workflows/actions.lock') -eq 1 ]" +# sp51b: a flow-mapping record naming the very dependency being folded +# cannot be folded with it: refused, named, nothing of it lost. +sp51b_inline=" 'hyperpolymath/standards@$GOOD': { ref: '$GOOD', commit: 'sha1-$GOOD' }" +sp_flow_fixture "$fx/sp51b" " uses: []\n" " uses: []\n$sp51b_inline\n" +run_fix bash "$fleet/scripts/fix-stale-reusable-pin.sh" "$fx/sp51b" +check "sp51b a flow-mapping record of the dependency being folded is refused, named, and kept" \ + "[ \$rc -eq 2 ] && grep -q 'UNSUPPORTED .*actions.lock: .*hyperpolymath/standards@$GOOD.*cannot read' <<<\"\$out\" && grep -qxF \"\$sp51b_inline\" '$fx/sp51b/.github/workflows/actions.lock'" +# sp51c: a duplicate holding a line the merger has no rule for is not folded +# with that line lost: refused, named, the line kept. +sp_flow_fixture "$fx/sp51c" " uses: []\n" " uses: []\n provenance:\n - 'x'\n" +run_fix bash "$fleet/scripts/fix-stale-reusable-pin.sh" "$fx/sp51c" +check "sp51c a duplicate holding a line the merger has no rule for is refused, named, the line kept" \ + "[ \$rc -eq 2 ] && grep -q 'UNSUPPORTED .*the record hyperpolymath/standards@$GOOD is listed 2 times and a duplicate holds a line' <<<\"\$out\" && grep -q '^ provenance:\$' '$fx/sp51c/.github/workflows/actions.lock'" +# sp51d: `workflows:` written after `dependencies:` ends that section: the +# merged records come first, the workflow edges after them, as they stood. +sp_flow_fixture "$fx/sp51d" " uses: []\n" " uses: []\n" +awk '/^workflows:/ { w = 1 } /^dependencies:/ { w = 0 } w { wf = wf $0 "\n"; next } { print } END { printf "%s", wf }' "$fx/sp51d/.github/workflows/actions.lock" > "$fx/sp51d.lock" && mv "$fx/sp51d.lock" "$fx/sp51d/.github/workflows/actions.lock" +run_fix bash "$fleet/scripts/fix-stale-reusable-pin.sh" "$fx/sp51d" +check "sp51d workflows: after dependencies: stays a section after the merged records, its edges moved" \ + "[ \$rc -eq 3 ] && [ \$(grep -c \"^ 'hyperpolymath/standards@$GOOD':\" '$fx/sp51d/.github/workflows/actions.lock') -eq 1 ] && tail -5 '$fx/sp51d/.github/workflows/actions.lock' | head -1 | grep -qx 'workflows:' && [ \$(grep -c \"^ - 'hyperpolymath/standards@$GOOD'\$\" '$fx/sp51d/.github/workflows/actions.lock') -eq 2 ]" + +# sp52: a `workflows:` key followed by trailing spaces is that workflow's +# entry: its edge moves with the caller, no out-of-sync warning. +mkdir -p "$fx/sp52/.github/workflows" +printf 'jobs:\n rust:\n uses: hyperpolymath/standards/.github/workflows/rust-ci-reusable.yml@%s\n' "$STALE_A" > "$fx/sp52/.github/workflows/ci.yml" +printf "version: 'v0.0.3'\nworkflows:\n '.github/workflows/ci.yml': \n - 'hyperpolymath/standards@%s'\ndependencies:\n 'hyperpolymath/standards@%s':\n ref: '%s'\n commit: 'sha1-%s'\n" "$STALE_A" "$STALE_A" "$STALE_A" "$STALE_A" > "$fx/sp52/.github/workflows/actions.lock" +run_fix bash "$fleet/scripts/fix-stale-reusable-pin.sh" "$fx/sp52" +check "sp52 a workflows: key with trailing spaces has its edge moved, no out-of-sync warning" \ + "[ \$rc -eq 3 ] && ! grep -q 'has no edge' <<<\"\$out\" && grep -q \"^ - 'hyperpolymath/standards@$GOOD'\$\" '$fx/sp52/.github/workflows/actions.lock' && ! grep -q \"$STALE_A\" '$fx/sp52/.github/workflows/actions.lock'" + +# sp53: a job's `uses:` that names a dead reusable through double-quoted +# escapes, or as a quoted value continued below its key, is that call: the +# rewrite cannot reach either, so each is reported (exit 2), never OK. +mkdir -p "$fx/sp53/.github/workflows" "$fx/sp53b/.github/workflows" "$fx/sp53c/.github/workflows" +printf 'jobs:\n rust:\n uses: "\\x68yperpolymath/standards/.github/workflows/rust-ci-reusable.yml@%s"\n' "$STALE_A" > "$fx/sp53/.github/workflows/ci.yml" +printf 'jobs:\n rust:\n uses:\n "hyperpolymath/standards/.github/workflows/rust-ci-reusable.yml@%s"\n' "$STALE_A" > "$fx/sp53b/.github/workflows/ci.yml" +cp "$fx/sp53/.github/workflows/ci.yml" "$fx/sp53-before.yml"; cp "$fx/sp53b/.github/workflows/ci.yml" "$fx/sp53b-before.yml" +run_fix bash "$fleet/scripts/fix-stale-reusable-pin.sh" "$fx/sp53" +check "sp53 a dead reusable spelled with escapes in a double-quoted uses: is UNSUPPORTED, exit 2, untouched" \ + "[ \$rc -eq 2 ] && grep -q 'UNSUPPORTED .github/workflows/ci.yml:3: .*written with escapes' <<<\"\$out\" && cmp -s '$fx/sp53-before.yml' '$fx/sp53/.github/workflows/ci.yml'" +run_fix bash "$fleet/scripts/fix-stale-reusable-pin.sh" "$fx/sp53b" +check "sp53 a dead reusable as a quoted value continued below its key is UNSUPPORTED, exit 2, untouched" \ + "[ \$rc -eq 2 ] && grep -q 'UNSUPPORTED .github/workflows/ci.yml:4: .*continued below its key' <<<\"\$out\" && cmp -s '$fx/sp53b-before.yml' '$fx/sp53b/.github/workflows/ci.yml'" +# Control: an escaped quote inside a `name:` string beside a live call is +# text; the escape hides no call. +printf 'jobs:\n rust:\n name: "say \\" uses: hyperpolymath/standards/.github/workflows/rust-ci-reusable.yml@%s"\n uses: hyperpolymath/standards/.github/workflows/rust-ci-reusable.yml@%s\n' "$STALE_A" "$GOOD" > "$fx/sp53c/.github/workflows/ci.yml" +run_fix bash "$fleet/scripts/fix-stale-reusable-pin.sh" "$fx/sp53c" +check "sp53 control: a dead pin inside a name: string holding an escaped quote is text: OK, exit 0" \ + "[ \$rc -eq 0 ] && grep -q '^OK' <<<\"\$out\"" + +# sp54: a job written as flow mappings all in column 0 (`jobs: {`, `rust: {`, +# `uses: ...`): indentation says nothing there, so the `uses:` cannot be +# passed over as data: reported, exit 2, untouched. Indented as KYAML +# indents it, the same call is rewritten. +mkdir -p "$fx/sp54/.github/workflows" "$fx/sp54b/.github/workflows" +printf 'jobs: {\nrust: {\nuses: hyperpolymath/standards/.github/workflows/rust-ci-reusable.yml@%s\n}\n}\n' "$STALE_A" > "$fx/sp54/.github/workflows/ci.yml" +printf 'jobs: {\n rust: {\n uses: hyperpolymath/standards/.github/workflows/rust-ci-reusable.yml@%s\n }\n}\n' "$STALE_A" > "$fx/sp54b/.github/workflows/ci.yml" +cp "$fx/sp54/.github/workflows/ci.yml" "$fx/sp54-before.yml" +run_fix bash "$fleet/scripts/fix-stale-reusable-pin.sh" "$fx/sp54" +check "sp54 a column-0 flow job's dead uses: is UNSUPPORTED, exit 2, untouched, never OK" \ + "[ \$rc -eq 2 ] && grep -q 'UNSUPPORTED .github/workflows/ci.yml:3:' <<<\"\$out\" && cmp -s '$fx/sp54-before.yml' '$fx/sp54/.github/workflows/ci.yml'" +run_fix bash "$fleet/scripts/fix-stale-reusable-pin.sh" "$fx/sp54b" +check "sp54 control: the same flow job indented as KYAML indents it is rewritten, exit 0" \ + "[ \$rc -eq 0 ] && grep -q \"rust-ci-reusable.yml@$GOOD\" '$fx/sp54b/.github/workflows/ci.yml'" + +# tc39: a step's own `uses:` spelling the SHA-pinned toolchain with +# double-quoted escapes is that step: it gets its input. An escaped quote in +# another step's `name:` string hides no step. +mkdir -p "$fx/tc39/.github/workflows" +cat > "$fx/tc39/.github/workflows/x.yml" <<'YML' +jobs: + check: + steps: + - uses: "\x64tolnay/rust-toolchain@6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772" + - name: "say \" uses: dtolnay/rust-toolchain@6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772" + run: cargo test +YML +run_fix bash "$fleet/scripts/fix-rust-toolchain-input.sh" "$fx/tc39" +check "tc39 an escaped double-quoted toolchain reference is the step's pin: it gets toolchain:, exit 0" \ + "[ \$rc -eq 0 ] && sed -n '5,6p' '$fx/tc39/.github/workflows/x.yml' | tr '\\n' '|' | grep -qx ' with:| toolchain: stable|' && [ \$(grep -c 'toolchain: stable' '$fx/tc39/.github/workflows/x.yml') -eq 1 ]" +cp "$fx/tc39/.github/workflows/x.yml" "$fx/tc39-after1.yml" +run_fix bash "$fleet/scripts/fix-rust-toolchain-input.sh" "$fx/tc39" +check "tc39 second run is a no-op" "[ \$rc -eq 0 ] && cmp -s '$fx/tc39-after1.yml' '$fx/tc39/.github/workflows/x.yml'" + +# ls42: a self-SHA record written as a flow mapping on its key line has its +# ref rewritten inside the braces. One spread over several lines is not read: +# UNRESOLVED, untouched, exit 1. A reusable's flow record keeps its SHA ref. +mkdir -p "$fx/ls42/.github/workflows" "$fx/ls42b/.github/workflows" "$fx/ls42c/.github/workflows" +action_steps "$fx/ls42/.github/workflows" "foo/bar@$SHA_B" +printf "version: 'v0.0.3'\nworkflows:\n '.github/workflows/ci.yml':\n - 'foo/bar@%s'\ndependencies:\n 'foo/bar@%s': { ref: '%s', commit: 'sha1-%s' }\n" "$SHA_B" "$SHA_B" "$SHA_B" "$SHA_B" > "$fx/ls42/.github/workflows/actions.lock" +printf 'foo/bar\t%s\tv1.0.0\n' "$SHA_B" > "$fx/ls42-tags.tsv" +TAG_MAP="$fx/ls42-tags.tsv" run_fix bash "$fleet/scripts/fix-lock-self-sha-refs.sh" "$fx/ls42" --offline +check "ls42 a flow-mapping self-SHA record gets its tag inside the braces, exit 0" \ + "[ \$rc -eq 0 ] && grep -qxF \" 'foo/bar@$SHA_B': { ref: 'v1.0.0', commit: 'sha1-$SHA_B' }\" '$fx/ls42/.github/workflows/actions.lock'" +action_steps "$fx/ls42b/.github/workflows" "foo/bar@$SHA_B" +printf "version: 'v0.0.3'\nworkflows:\n '.github/workflows/ci.yml':\n - 'foo/bar@%s'\ndependencies:\n 'foo/bar@%s': {\n ref: '%s', commit: 'sha1-%s' }\n" "$SHA_B" "$SHA_B" "$SHA_B" "$SHA_B" > "$fx/ls42b/.github/workflows/actions.lock" +cp "$fx/ls42b/.github/workflows/actions.lock" "$fx/ls42b-before.lock" +TAG_MAP="$fx/ls42-tags.tsv" run_fix bash "$fleet/scripts/fix-lock-self-sha-refs.sh" "$fx/ls42b" --offline +check "ls42b a flow-mapping record over several lines is UNRESOLVED, untouched, exit 1, never OK" \ + "[ \$rc -eq 1 ] && grep -q 'UNRESOLVED foo/bar@bbbbbbb: the record is a flow mapping spread over several lines' <<<\"\$out\" && cmp -s '$fx/ls42b-before.lock' '$fx/ls42b/.github/workflows/actions.lock'" +printf 'jobs:\n call:\n uses: foo/bar/.github/workflows/x.yml@%s\n' "$SHA_B" > "$fx/ls42c/.github/workflows/ci.yml" +printf "version: 'v0.0.3'\nworkflows:\n '.github/workflows/ci.yml':\n - 'foo/bar@%s'\ndependencies:\n 'foo/bar@%s': { ref: '%s', commit: 'sha1-%s' }\n" "$SHA_B" "$SHA_B" "$SHA_B" "$SHA_B" > "$fx/ls42c/.github/workflows/actions.lock" +cp "$fx/ls42c/.github/workflows/actions.lock" "$fx/ls42c-before.lock" +TAG_MAP="$fx/ls42-tags.tsv" run_fix bash "$fleet/scripts/fix-lock-self-sha-refs.sh" "$fx/ls42c" --offline +check "ls42 control: a reusable's flow-mapping record keeps its SHA ref, exit 0" \ + "[ \$rc -eq 0 ] && cmp -s '$fx/ls42c-before.lock' '$fx/ls42c/.github/workflows/actions.lock'" + +# ── Review cases (gitbot-fleet#615, round on 23e1337) ─────────────────────── +# sp55: a doubled quote inside a single-quoted sibling is a quote, not the +# scalar's end: its braces are text, the flow job stays open, and the +# column-0 dead call below it is reported (exit 2), never passed as data. +mkdir -p "$fx/sp55/.github/workflows" +printf "jobs: {\nrust: {\nname: 'it''s }}',\nuses: hyperpolymath/standards/.github/workflows/rust-ci-reusable.yml@%s\n}\n}\n" "$STALE_A" > "$fx/sp55/.github/workflows/ci.yml" +cp "$fx/sp55/.github/workflows/ci.yml" "$fx/sp55-before.yml" +run_fix bash "$fleet/scripts/fix-stale-reusable-pin.sh" "$fx/sp55" +check "sp55 braces inside a single-quoted scalar with a doubled quote keep the flow job open: UNSUPPORTED, exit 2" \ + "[ \$rc -eq 2 ] && grep -q 'UNSUPPORTED .github/workflows/ci.yml:4:' <<<\"\$out\" && cmp -s '$fx/sp55-before.yml' '$fx/sp55/.github/workflows/ci.yml'" + +# sp56: a stale dependency record written as a flow mapping on its key line +# moves with its caller: key, ref and commit, on that line. +mkdir -p "$fx/sp56/.github/workflows" +printf 'jobs:\n rust:\n uses: hyperpolymath/standards/.github/workflows/rust-ci-reusable.yml@%s\n' "$STALE_A" > "$fx/sp56/.github/workflows/ci.yml" +printf "version: 'v0.0.3'\nworkflows:\n '.github/workflows/ci.yml':\n - 'hyperpolymath/standards@%s'\ndependencies:\n 'hyperpolymath/standards@%s': { ref: '%s', commit: 'sha1-%s' }\n" "$STALE_A" "$STALE_A" "$STALE_A" "$STALE_A" > "$fx/sp56/.github/workflows/actions.lock" +run_fix bash "$fleet/scripts/fix-stale-reusable-pin.sh" "$fx/sp56" +check "sp56 a flow-mapping stale record is renamed with its caller: key, ref and commit, no stale SHA left" \ + "[ \$rc -eq 3 ] && grep -qxF \" 'hyperpolymath/standards@$GOOD': { ref: '$GOOD', commit: 'sha1-$GOOD' }\" '$fx/sp56/.github/workflows/actions.lock' && ! grep -q \"$STALE_A\" '$fx/sp56/.github/workflows/actions.lock'" + +# tc40: `\/` is a YAML escape too: `"dtolnay\/rust-toolchain@"` is the +# SHA-pinned step, read past the prefilter, and gets its input. +mkdir -p "$fx/tc40/.github/workflows" +printf 'jobs:\n check:\n steps:\n - uses: "dtolnay\\/rust-toolchain@6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772"\n' > "$fx/tc40/.github/workflows/x.yml" +run_fix bash "$fleet/scripts/fix-rust-toolchain-input.sh" "$fx/tc40" +check "tc40 a \\/-escaped toolchain reference reaches the parser and the step gets toolchain:, exit 0" \ + "[ \$rc -eq 0 ] && grep -q '^ toolchain: stable\$' '$fx/tc40/.github/workflows/x.yml'" + +# ls43: a step's `uses:` spelling the action with escapes is that action: the +# self-SHA record it proves gets its tag. +mkdir -p "$fx/ls43/.github/workflows" +printf 'jobs:\n build:\n steps:\n - uses: "\\x66oo/bar@%s"\n' "$SHA_B" > "$fx/ls43/.github/workflows/ci.yml" +lock_records "$fx/ls43" "foo/bar@$SHA_B" +printf 'foo/bar\t%s\tv1.0.0\n' "$SHA_B" > "$fx/ls43-tags.tsv" +TAG_MAP="$fx/ls43-tags.tsv" run_fix bash "$fleet/scripts/fix-lock-self-sha-refs.sh" "$fx/ls43" --offline +check "ls43 an escaped step reference proves the action: its self-SHA record gets its tag, exit 0" \ + "[ \$rc -eq 0 ] && grep -q \"^ ref: 'v1.0.0'\$\" '$fx/ls43/.github/workflows/actions.lock'" + +# The rules AGENTIC.a2ml assigns to each workflow-pin fixer are the ones the +# registry routes to it: a rule advertised but not registered is never +# dispatched, and one registered for a fixer that cannot repair it is a +# silent clean run. +agentic_ok=1 +while IFS= read -r row; do + script=$(awk '{print $2}' <<<"$row"); script=${script#scripts/} + rules=$(sed -E 's/^#[[:space:]]+scripts\/[^[:space:]]+[[:space:]]+//; s/[[:space:]]{2,}.*$//' <<<"$row") + IFS=', ' read -r -a rule_list <<<"$rules" + for rule in "${rule_list[@]}"; do + [ -n "$rule" ] || continue + jq -e --arg k "$rule" --arg v "$script" '.registry.by_recipe[$k] == $v' "$fleet/scripts/fix-script-registry.json" >/dev/null || { agentic_ok=0; echo " (AGENTIC assigns $rule to $script; the registry does not)" >&2; } + done +done < <(grep -E '^#[[:space:]]+scripts/fix-[a-z-]+\.sh[[:space:]]' "$fleet/.machine_readable/descriptiles/AGENTIC.a2ml") +check "every rule AGENTIC assigns to a workflow-pin fixer is registered to it" "[ \$agentic_ok -eq 1 ]" + +# ── Review cases (gitbot-fleet#615, round on 780bd7d) ─────────────────────── +# ls44: a self-SHA record whose ref is spelled with escapes is that record: +# YAML reads the SHA, the rewrite cannot reach the bytes, so it is reported +# (exit 1, untouched), never passed as no record. +mkdir -p "$fx/ls44/.github/workflows" +action_steps "$fx/ls44/.github/workflows" "foo/bar@$SHA_B" +printf "version: 'v0.0.3'\nworkflows:\n '.github/workflows/ci.yml':\n - 'foo/bar@%s'\ndependencies:\n 'foo/bar@%s':\n ref: \"\\\\x62%s\"\n commit: 'sha1-%s'\n" "$SHA_B" "$SHA_B" "${SHA_B:1}" "$SHA_B" > "$fx/ls44/.github/workflows/actions.lock" +printf 'foo/bar\t%s\tv1.0.0\n' "$SHA_B" > "$fx/ls44-tags.tsv" +cp "$fx/ls44/.github/workflows/actions.lock" "$fx/ls44-before.lock" +TAG_MAP="$fx/ls44-tags.tsv" run_fix bash "$fleet/scripts/fix-lock-self-sha-refs.sh" "$fx/ls44" --offline +check "ls44 an escaped self-SHA ref is the record: UNRESOLVED, untouched, exit 1, never OK" \ + "[ \$rc -eq 1 ] && grep -q 'UNRESOLVED foo/bar@bbbbbbb: the record.s ref (line 7)' <<<\"\$out\" && cmp -s '$fx/ls44-before.lock' '$fx/ls44/.github/workflows/actions.lock'" + +# sp57: with `dependencies:` written before `workflows:`, a stale record a +# caller the table does not cover still needs is copied INSIDE the section; +# `workflows:` and its edges stay as they were, not swallowed by the copy. +mkdir -p "$fx/sp57/.github/workflows" +printf 'jobs:\n rust:\n uses: hyperpolymath/standards/.github/workflows/rust-ci-reusable.yml@%s\n' "$STALE_A" > "$fx/sp57/.github/workflows/ci.yml" +printf 'jobs:\n mirror:\n uses: hyperpolymath/standards/.github/workflows/mirror-reusable.yml@%s\n' "$STALE_A" > "$fx/sp57/.github/workflows/other.yml" +printf "version: 'v0.0.3'\ndependencies:\n 'hyperpolymath/standards@%s':\n ref: '%s'\n commit: 'sha1-%s'\nworkflows:\n '.github/workflows/ci.yml':\n - 'hyperpolymath/standards@%s'\n '.github/workflows/other.yml':\n - 'hyperpolymath/standards@%s'\n" "$STALE_A" "$STALE_A" "$STALE_A" "$STALE_A" "$STALE_A" > "$fx/sp57/.github/workflows/actions.lock" +run_fix bash "$fleet/scripts/fix-stale-reusable-pin.sh" "$fx/sp57" +sp57_lock="$fx/sp57/.github/workflows/actions.lock" +check "sp57 a kept stale record is copied inside dependencies:, before workflows:, which keeps both its entries" \ + "[ \$rc -eq 3 ] && [ \$(grep -c '^workflows:\$' '$sp57_lock') -eq 1 ] && [ \$(grep -n \"^ 'hyperpolymath/standards@$GOOD':\$\" '$sp57_lock' | cut -d: -f1) -lt \$(grep -n '^workflows:\$' '$sp57_lock' | cut -d: -f1) ] && grep -A1 \"^ '.github/workflows/other.yml':\" '$sp57_lock' | grep -q \"@$STALE_A'\" && grep -A1 \"^ '.github/workflows/ci.yml':\" '$sp57_lock' | grep -q \"@$GOOD'\"" + +# sp58: a quoted workflow edge keeps its trailing comment when it moves. +mkdir -p "$fx/sp58/.github/workflows" +printf 'jobs:\n rust:\n uses: hyperpolymath/standards/.github/workflows/rust-ci-reusable.yml@%s\n' "$STALE_A" > "$fx/sp58/.github/workflows/ci.yml" +printf "version: 'v0.0.3'\nworkflows:\n '.github/workflows/ci.yml':\n - 'hyperpolymath/standards@%s' # rationale\ndependencies:\n 'hyperpolymath/standards@%s':\n ref: '%s'\n commit: 'sha1-%s'\n" "$STALE_A" "$STALE_A" "$STALE_A" "$STALE_A" > "$fx/sp58/.github/workflows/actions.lock" +run_fix bash "$fleet/scripts/fix-stale-reusable-pin.sh" "$fx/sp58" +check "sp58 a quoted edge moved to the replacement keeps its trailing comment" \ + "[ \$rc -eq 3 ] && grep -qxF \" - 'hyperpolymath/standards@$GOOD' # rationale\" '$fx/sp58/.github/workflows/actions.lock'" + +# sp59 / tc41: a double-quoted `uses:` value continued past an escaped line +# break is the reference YAML joins: a dead call, or a SHA-pinned toolchain, +# spelled so is reported (exit 2, untouched), never passed as clean. +mkdir -p "$fx/sp59/.github/workflows" "$fx/tc41/.github/workflows" +printf 'jobs:\n rust:\n uses: "hyperpolymath/standards/.github/workflows/rust-ci-\\\n reusable.yml@%s"\n' "$STALE_A" > "$fx/sp59/.github/workflows/ci.yml" +cp "$fx/sp59/.github/workflows/ci.yml" "$fx/sp59-before.yml" +run_fix bash "$fleet/scripts/fix-stale-reusable-pin.sh" "$fx/sp59" +check "sp59 a dead call continued past an escaped line break is UNSUPPORTED, exit 2, untouched" \ + "[ \$rc -eq 2 ] && grep -q 'UNSUPPORTED .github/workflows/ci.yml:3: .*written with escapes' <<<\"\$out\" && cmp -s '$fx/sp59-before.yml' '$fx/sp59/.github/workflows/ci.yml'" +printf 'jobs:\n check:\n steps:\n - uses: "dtolnay/rust-\\\n toolchain@6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772"\n - name: "a \\\n b"\n run: cargo test\n' > "$fx/tc41/.github/workflows/x.yml" +cp "$fx/tc41/.github/workflows/x.yml" "$fx/tc41-before.yml" +run_fix bash "$fleet/scripts/fix-rust-toolchain-input.sh" "$fx/tc41" +check "tc41 a toolchain pin continued past an escaped line break is UNSUPPORTED (line 4 only), exit 2, untouched" \ + "[ \$rc -eq 2 ] && grep -q 'UNSUPPORTED.*line(s): 4\\.' <<<\"\$out\" && cmp -s '$fx/tc41-before.yml' '$fx/tc41/.github/workflows/x.yml'" + +# tc42: an input key spelled with escapes (`"tool\x63hain":`) is the input +# `toolchain`: the step is configured, in flow or block form, and no second +# toolchain key is written. +mkdir -p "$fx/tc42/.github/workflows" +cat > "$fx/tc42/.github/workflows/x.yml" <<'YML' +jobs: + check: + steps: + - uses: dtolnay/rust-toolchain@6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772 + with: { "tool\x63hain": nightly } + - uses: dtolnay/rust-toolchain@6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772 + with: + "tool\x63hain": nightly +YML +cp "$fx/tc42/.github/workflows/x.yml" "$fx/tc42-before.yml" +run_fix bash "$fleet/scripts/fix-rust-toolchain-input.sh" "$fx/tc42" +check "tc42 an escaped toolchain key is the input: flow and block steps left as they are, exit 0" \ + "[ \$rc -eq 0 ] && cmp -s '$fx/tc42-before.yml' '$fx/tc42/.github/workflows/x.yml'" + +# ── Review cases (gitbot-fleet#615, round on a5cd438) ─────────────────────── +# sp60 / tc43: an anchor whose double-quoted value is continued past an +# escaped line break holds the value YAML joins. A dead call read so through +# `uses: *call` is reported; the toolchain fixer, which reads anchors a line +# at a time, counts an alias of such an anchor as possibly the pin. Both exit +# 2 with the file untouched, never pass the alias as a known other value. +mkdir -p "$fx/sp60/.github/workflows" "$fx/tc43/.github/workflows" +printf 'env:\n CALL: &call "hyperpolymath/standards/.github/workflows/rust-ci-\\\n reusable.yml@%s"\njobs:\n rust:\n uses: *call\n' "$STALE_A" > "$fx/sp60/.github/workflows/ci.yml" +cp "$fx/sp60/.github/workflows/ci.yml" "$fx/sp60-before.yml" +run_fix bash "$fleet/scripts/fix-stale-reusable-pin.sh" "$fx/sp60" +check "sp60 an aliased dead call held by a continued anchor value is UNSUPPORTED, exit 2, untouched" \ + "[ \$rc -eq 2 ] && grep -q 'UNSUPPORTED .github/workflows/ci.yml:6: .uses: \\*call. runs hyperpolymath/standards/.github/workflows/rust-ci-reusable.yml@$STALE_A' <<<\"\$out\" && cmp -s '$fx/sp60-before.yml' '$fx/sp60/.github/workflows/ci.yml'" +printf 'env:\n ACTION: &rust "dtolnay/rust-\\\n toolchain@6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772"\njobs:\n check:\n steps:\n - uses: *rust\n' > "$fx/tc43/.github/workflows/x.yml" +cp "$fx/tc43/.github/workflows/x.yml" "$fx/tc43-before.yml" +run_fix bash "$fleet/scripts/fix-rust-toolchain-input.sh" "$fx/tc43" +check "tc43 an alias of a continued anchor value is UNSUPPORTED (line 7), exit 2, untouched" \ + "[ \$rc -eq 2 ] && grep -q 'UNSUPPORTED.*line(s): 7\\.' <<<\"\$out\" && cmp -s '$fx/tc43-before.yml' '$fx/tc43/.github/workflows/x.yml'" + +# ls45: a self-SHA ref carrying YAML node properties (`ref: !!str ''`) +# is that SHA: the record gets its tag, block or flow, the tag kept. +mkdir -p "$fx/ls45/.github/workflows" "$fx/ls45f/.github/workflows" +action_steps "$fx/ls45/.github/workflows" "foo/bar@$SHA_B" +action_steps "$fx/ls45f/.github/workflows" "foo/bar@$SHA_B" +printf "version: 'v0.0.3'\nworkflows:\n '.github/workflows/ci.yml':\n - 'foo/bar@%s'\ndependencies:\n 'foo/bar@%s':\n ref: !!str '%s'\n commit: 'sha1-%s'\n" "$SHA_B" "$SHA_B" "$SHA_B" "$SHA_B" > "$fx/ls45/.github/workflows/actions.lock" +printf "version: 'v0.0.3'\nworkflows:\n '.github/workflows/ci.yml':\n - 'foo/bar@%s'\ndependencies:\n 'foo/bar@%s': { ref: !!str '%s', commit: 'sha1-%s' }\n" "$SHA_B" "$SHA_B" "$SHA_B" "$SHA_B" > "$fx/ls45f/.github/workflows/actions.lock" +printf 'foo/bar\t%s\tv1.0.0\n' "$SHA_B" > "$fx/ls45-tags.tsv" +TAG_MAP="$fx/ls45-tags.tsv" run_fix bash "$fleet/scripts/fix-lock-self-sha-refs.sh" "$fx/ls45" --offline +check "ls45 a tagged block self-SHA ref is the record: it gets its tag, the tag property kept, exit 0" \ + "[ \$rc -eq 0 ] && grep -qxF \" ref: !!str 'v1.0.0'\" '$fx/ls45/.github/workflows/actions.lock'" +TAG_MAP="$fx/ls45-tags.tsv" run_fix bash "$fleet/scripts/fix-lock-self-sha-refs.sh" "$fx/ls45f" --offline +check "ls45f a tagged flow self-SHA ref is the record: it gets its tag in its braces, exit 0" \ + "[ \$rc -eq 0 ] && grep -qxF \" 'foo/bar@$SHA_B': { ref: !!str 'v1.0.0', commit: 'sha1-$SHA_B' }\" '$fx/ls45f/.github/workflows/actions.lock'" + +# tc44: a quoted key or value of a flow `with: {` left open at its line's +# end — continued past an escaped line break, or folded onto the next line — +# hides the keys the next line names: the step is reported (exit 2, +# untouched), never given a second `toolchain`. +mkdir -p "$fx/tc44/.github/workflows" "$fx/tc44b/.github/workflows" +cat > "$fx/tc44/.github/workflows/x.yml" <<'YML' +jobs: + check: + steps: + - uses: dtolnay/rust-toolchain@6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772 + with: { "toolch\ + ain": nightly } +YML +cat > "$fx/tc44b/.github/workflows/x.yml" <<'YML' +jobs: + check: + steps: + - uses: dtolnay/rust-toolchain@6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772 + with: { + components: "rustfmt, + clippy", toolchain: nightly } +YML +cp "$fx/tc44/.github/workflows/x.yml" "$fx/tc44-before.yml" +cp "$fx/tc44b/.github/workflows/x.yml" "$fx/tc44b-before.yml" +run_fix bash "$fleet/scripts/fix-rust-toolchain-input.sh" "$fx/tc44" +check "tc44 a flow input key continued past an escaped line break is UNSUPPORTED (line 5), exit 2, untouched" \ + "[ \$rc -eq 2 ] && grep -q 'UNSUPPORTED.*line(s): 5\\.' <<<\"\$out\" && cmp -s '$fx/tc44-before.yml' '$fx/tc44/.github/workflows/x.yml'" +run_fix bash "$fleet/scripts/fix-rust-toolchain-input.sh" "$fx/tc44b" +check "tc44b a flow value folded onto the next line is UNSUPPORTED (line 6), exit 2, untouched" \ + "[ \$rc -eq 2 ] && grep -q 'UNSUPPORTED.*line(s): 6\\.' <<<\"\$out\" && cmp -s '$fx/tc44b-before.yml' '$fx/tc44b/.github/workflows/x.yml'" + +# tc45 / ls46: `steps: [uses: ...]` is a list of one step, a mapping written +# without braces. The toolchain fixer reports such a SHA-pinned step (exit 2, +# untouched), on the key's line or the next; the self-SHA fixer reads it as +# the action site it is, and the record it proves gets its tag. +mkdir -p "$fx/tc45/.github/workflows" "$fx/ls46/.github/workflows" +cat > "$fx/tc45/.github/workflows/x.yml" <<'YML' +jobs: + a: + steps: [uses: dtolnay/rust-toolchain@6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772] + b: + steps: + [uses: dtolnay/rust-toolchain@6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772] +YML +cp "$fx/tc45/.github/workflows/x.yml" "$fx/tc45-before.yml" +run_fix bash "$fleet/scripts/fix-rust-toolchain-input.sh" "$fx/tc45" +check "tc45 a step written straight into steps: [ ] is UNSUPPORTED (lines 3 and 6), exit 2, untouched" \ + "[ \$rc -eq 2 ] && grep -q 'UNSUPPORTED.*line(s): 3 6\\.' <<<\"\$out\" && cmp -s '$fx/tc45-before.yml' '$fx/tc45/.github/workflows/x.yml'" +printf 'jobs:\n build:\n steps: [uses: foo/bar@%s]\n' "$SHA_B" > "$fx/ls46/.github/workflows/ci.yml" +lock_records "$fx/ls46" "foo/bar@$SHA_B" +printf 'foo/bar\t%s\tv1.0.0\n' "$SHA_B" > "$fx/ls46-tags.tsv" +TAG_MAP="$fx/ls46-tags.tsv" run_fix bash "$fleet/scripts/fix-lock-self-sha-refs.sh" "$fx/ls46" --offline +check "ls46 steps: [uses: ...] proves the action: its self-SHA record gets its tag, exit 0" \ + "[ \$rc -eq 0 ] && grep -q \"^ ref: 'v1.0.0'\$\" '$fx/ls46/.github/workflows/actions.lock'" + +# tc46 / sp62: an alias given as the `uses` of a flow-style step or job — +# `steps: [uses: *rust]`, `steps: [{ uses: *rust }]`, a step of a `[` opened +# on the line above, `rust: { uses: *call }`, `jobs: { rust: { uses: *call } +# }` — runs the anchor's value like a block key's alias: an anchored +# SHA-pinned toolchain, or a known-dead call, is reported (exit 2, +# untouched), never passed as clean. +mkdir -p "$fx/tc46/.github/workflows" "$fx/sp62/.github/workflows" "$fx/sp62b/.github/workflows" +cat > "$fx/tc46/.github/workflows/x.yml" <<'YML' +env: { ACTION: &rust dtolnay/rust-toolchain@6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772 } +jobs: + a: + steps: [uses: *rust] + b: + steps: [{ uses: *rust }] + c: + steps: [ + { uses: *rust } + ] +YML +cp "$fx/tc46/.github/workflows/x.yml" "$fx/tc46-before.yml" +run_fix bash "$fleet/scripts/fix-rust-toolchain-input.sh" "$fx/tc46" +check "tc46 aliased flow steps of an anchored toolchain pin are UNSUPPORTED (lines 4 6 9), exit 2, untouched" \ + "[ \$rc -eq 2 ] && grep -q 'UNSUPPORTED.*line(s): 4 6 9\\.' <<<\"\$out\" && cmp -s '$fx/tc46-before.yml' '$fx/tc46/.github/workflows/x.yml'" +printf 'env: { CALL: &call %s@%s }\njobs:\n rust: { uses: *call }\n' "$RC_REUSABLE" "$STALE_A" > "$fx/sp62/.github/workflows/ci.yml" +printf 'env: { CALL: &call %s@%s }\njobs: { rust: { uses: *call } }\n' "$RC_REUSABLE" "$STALE_A" > "$fx/sp62b/.github/workflows/ci.yml" +cp "$fx/sp62/.github/workflows/ci.yml" "$fx/sp62-before.yml" +cp "$fx/sp62b/.github/workflows/ci.yml" "$fx/sp62b-before.yml" +run_fix bash "$fleet/scripts/fix-stale-reusable-pin.sh" "$fx/sp62" +check "sp62 a flow job calling an alias of a dead pin is UNSUPPORTED, exit 2, untouched, never OK" \ + "[ \$rc -eq 2 ] && grep -q 'UNSUPPORTED .github/workflows/ci.yml:3: .uses: \\*call. runs' <<<\"\$out\" && cmp -s '$fx/sp62-before.yml' '$fx/sp62/.github/workflows/ci.yml'" +run_fix bash "$fleet/scripts/fix-stale-reusable-pin.sh" "$fx/sp62b" +check "sp62b a job of a one-line jobs: { } calling an alias of a dead pin is UNSUPPORTED, exit 2, untouched" \ + "[ \$rc -eq 2 ] && grep -q 'UNSUPPORTED .github/workflows/ci.yml:2: .uses: \\*call. runs' <<<\"\$out\" && cmp -s '$fx/sp62b-before.yml' '$fx/sp62b/.github/workflows/ci.yml'" + +# sp61: a stale record written as a flow mapping on its key line moves its +# own key, ref and commit; a nested `uses:` edge at the same SHA (another +# repository's revision, not recomputed here) and a comment stay as written. +mkdir -p "$fx/sp61/.github/workflows" +printf 'jobs:\n rust:\n uses: hyperpolymath/standards/.github/workflows/rust-ci-reusable.yml@%s\n' "$STALE_A" > "$fx/sp61/.github/workflows/ci.yml" +printf "version: 'v0.0.3'\nworkflows:\n '.github/workflows/ci.yml':\n - 'hyperpolymath/standards@%s'\ndependencies:\n 'hyperpolymath/standards@%s': { ref: '%s', commit: 'sha1-%s', uses: ['other/action@%s'] } # pinned at %s\n" "$STALE_A" "$STALE_A" "$STALE_A" "$STALE_A" "$STALE_A" "$STALE_A" > "$fx/sp61/.github/workflows/actions.lock" +run_fix bash "$fleet/scripts/fix-stale-reusable-pin.sh" "$fx/sp61" +check "sp61 a flow-mapping record moves its key, ref and commit; its nested edge and comment stay as written" \ + "[ \$rc -eq 3 ] && grep -qxF \" 'hyperpolymath/standards@$GOOD': { ref: '$GOOD', commit: 'sha1-$GOOD', uses: ['other/action@$STALE_A'] } # pinned at $STALE_A\" '$fx/sp61/.github/workflows/actions.lock'" + +# ── Review cases (gitbot-fleet#615, round on 8978704) ─────────────────────── +# tc47 / sp63: an anchor name is every character up to whitespace or a flow +# indicator: `&rust/tool` names `rust/tool`, and `*rust/tool` is its alias, +# never `*rust`. The aliased SHA-pinned toolchain, or dead call, is reported +# (exit 2, untouched), never passed as an alias of some other value. +mkdir -p "$fx/tc47/.github/workflows" "$fx/sp63/.github/workflows" +printf 'env:\n ACTION: &rust/tool dtolnay/rust-toolchain@6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772\njobs:\n check:\n steps:\n - uses: *rust/tool\n' > "$fx/tc47/.github/workflows/x.yml" +cp "$fx/tc47/.github/workflows/x.yml" "$fx/tc47-before.yml" +run_fix bash "$fleet/scripts/fix-rust-toolchain-input.sh" "$fx/tc47" +check "tc47 an alias read to its whole name is the anchored toolchain pin: UNSUPPORTED (line 6), exit 2, untouched" \ + "[ \$rc -eq 2 ] && grep -q 'UNSUPPORTED.*line(s): 6\\.' <<<\"\$out\" && cmp -s '$fx/tc47-before.yml' '$fx/tc47/.github/workflows/x.yml'" +printf 'env:\n CALL: &call/x %s@%s\njobs:\n rust:\n uses: *call/x\n' "$RC_REUSABLE" "$STALE_A" > "$fx/sp63/.github/workflows/ci.yml" +cp "$fx/sp63/.github/workflows/ci.yml" "$fx/sp63-before.yml" +run_fix bash "$fleet/scripts/fix-stale-reusable-pin.sh" "$fx/sp63" +check "sp63 an alias read to its whole name runs the anchored dead call: UNSUPPORTED, exit 2, untouched" \ + "[ \$rc -eq 2 ] && grep -q 'UNSUPPORTED .github/workflows/ci.yml:5: .uses: \\*call/x. runs' <<<\"\$out\" && cmp -s '$fx/sp63-before.yml' '$fx/sp63/.github/workflows/ci.yml'" + +# tc48: a toolchain value carrying node properties is read past them. Null +# or empty after them (`&channel null`, `!!null null`, `!!str ""`) is no +# input, and is reported rather than filled in place (every alias of the +# anchor would change with it): exit 2, untouched. A value after them is +# the input (`!!str null` is the string null): left as it is, exit 0. +mkdir -p "$fx/tc48/.github/workflows" "$fx/tc48b/.github/workflows" +cat > "$fx/tc48/.github/workflows/x.yml" <<'YML' +jobs: + check: + steps: + - uses: dtolnay/rust-toolchain@6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772 + with: + toolchain: &channel null + - uses: dtolnay/rust-toolchain@6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772 + with: { toolchain: !!null null } + - uses: dtolnay/rust-toolchain@6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772 + with: + toolchain: !!str "" +YML +cat > "$fx/tc48b/.github/workflows/x.yml" <<'YML' +jobs: + check: + steps: + - uses: dtolnay/rust-toolchain@6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772 + with: + toolchain: !!str null + - uses: dtolnay/rust-toolchain@6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772 + with: { toolchain: &channel nightly } +YML +cp "$fx/tc48/.github/workflows/x.yml" "$fx/tc48-before.yml" +cp "$fx/tc48b/.github/workflows/x.yml" "$fx/tc48b-before.yml" +run_fix bash "$fleet/scripts/fix-rust-toolchain-input.sh" "$fx/tc48" +check "tc48 a null or empty toolchain behind node properties is UNSUPPORTED (lines 6 8 11), exit 2, untouched" \ + "[ \$rc -eq 2 ] && grep -q 'UNSUPPORTED.*line(s): 6 8 11\\.' <<<\"\$out\" && cmp -s '$fx/tc48-before.yml' '$fx/tc48/.github/workflows/x.yml'" +run_fix bash "$fleet/scripts/fix-rust-toolchain-input.sh" "$fx/tc48b" +check "tc48b a toolchain value behind node properties is the input: left as it is, exit 0" \ + "[ \$rc -eq 0 ] && cmp -s '$fx/tc48b-before.yml' '$fx/tc48b/.github/workflows/x.yml'" + +# tc49: a step given as an alias (`- *rust`, `steps: [*rust]`) is the +# mapping its anchor holds, which may be the SHA-pinned toolchain step: in a +# file that holds such a pin it is reported (exit 2, untouched). Control: +# with no pinned toolchain anywhere, an aliased step is nothing to repair. +mkdir -p "$fx/tc49/.github/workflows" "$fx/tc49b/.github/workflows" +cat > "$fx/tc49/.github/workflows/x.yml" <<'YML' +jobs: + check: + strategy: + matrix: + include: + - step: &rust { uses: dtolnay/rust-toolchain@6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772 } + steps: + - *rust + b: + steps: [*rust] +YML +printf 'x: &chk { uses: actions/checkout@v4 }\njobs:\n b:\n steps: [*chk]\n c:\n steps:\n - *chk\n' > "$fx/tc49b/.github/workflows/x.yml" +cp "$fx/tc49/.github/workflows/x.yml" "$fx/tc49-before.yml" +cp "$fx/tc49b/.github/workflows/x.yml" "$fx/tc49b-before.yml" +run_fix bash "$fleet/scripts/fix-rust-toolchain-input.sh" "$fx/tc49" +check "tc49 aliased steps in a file with a pinned toolchain are UNSUPPORTED (lines 8 10), exit 2, untouched" \ + "[ \$rc -eq 2 ] && grep -q 'UNSUPPORTED.*line(s): 8 10\\.' <<<\"\$out\" && cmp -s '$fx/tc49-before.yml' '$fx/tc49/.github/workflows/x.yml'" +run_fix bash "$fleet/scripts/fix-rust-toolchain-input.sh" "$fx/tc49b" +check "tc49b aliased steps with no pinned toolchain in the file are left as they are, exit 0" \ + "[ \$rc -eq 0 ] && cmp -s '$fx/tc49b-before.yml' '$fx/tc49b/.github/workflows/x.yml'" + +# ls47: the `ref` of a flow-mapping record is its top-level entry: a +# `ref:` written inside a quoted string beside it is text. The record's own +# self-SHA ref gets its tag and the string keeps its text; a record whose +# own ref is a tag is left as it is. +mkdir -p "$fx/ls47/.github/workflows" "$fx/ls47b/.github/workflows" +action_steps "$fx/ls47/.github/workflows" "foo/bar@$SHA_B" +action_steps "$fx/ls47b/.github/workflows" "foo/bar@$SHA_B" +printf "version: 'v0.0.3'\nworkflows:\n '.github/workflows/ci.yml':\n - 'foo/bar@%s'\ndependencies:\n 'foo/bar@%s': { note: \"hello, ref: %s, fixture\", ref: '%s', commit: 'sha1-%s' }\n" "$SHA_B" "$SHA_B" "$SHA_B" "$SHA_B" "$SHA_B" > "$fx/ls47/.github/workflows/actions.lock" +printf "version: 'v0.0.3'\nworkflows:\n '.github/workflows/ci.yml':\n - 'foo/bar@%s'\ndependencies:\n 'foo/bar@%s': { note: \"hello, ref: %s, fixture\", ref: 'v1', commit: 'sha1-%s' }\n" "$SHA_B" "$SHA_B" "$SHA_B" "$SHA_B" > "$fx/ls47b/.github/workflows/actions.lock" +cp "$fx/ls47b/.github/workflows/actions.lock" "$fx/ls47b-before.lock" +printf 'foo/bar\t%s\tv1.0.0\n' "$SHA_B" > "$fx/ls47-tags.tsv" +TAG_MAP="$fx/ls47-tags.tsv" run_fix bash "$fleet/scripts/fix-lock-self-sha-refs.sh" "$fx/ls47" --offline +check "ls47 the record's top-level ref gets its tag; a ref: inside its quoted note stays as written, exit 0" \ + "[ \$rc -eq 0 ] && grep -qxF \" 'foo/bar@$SHA_B': { note: \\\"hello, ref: $SHA_B, fixture\\\", ref: 'v1.0.0', commit: 'sha1-$SHA_B' }\" '$fx/ls47/.github/workflows/actions.lock'" +TAG_MAP="$fx/ls47-tags.tsv" run_fix bash "$fleet/scripts/fix-lock-self-sha-refs.sh" "$fx/ls47b" --offline +check "ls47b a record whose own ref is a tag is no self-SHA record: its quoted note is not rewritten, exit 0" \ + "[ \$rc -eq 0 ] && cmp -s '$fx/ls47b-before.lock' '$fx/ls47b/.github/workflows/actions.lock'" + +dq_out=$(printf '%s\n' 'uses: "\x64tolnay/x@1" # "\x41"' "name: 'a\\x41' \"b\\\"c\"" | awk "$(cat "$fleet/scripts/lib/yaml-anchors.awk")"'{ printf "%s|", yaml_decode_dq($0) }') +check "yaml_decode_dq decodes double-quoted escapes only, an escaped quote to a byte no reference holds" \ + "[ \"\$dq_out\" = \"\$(printf 'uses: \"dtolnay/x@1\" # \"\\\\x41\"|name: '\"'\"'a\\\\x41'\"'\"' \"b\\001c\"|')\" ]" + +bash "$fleet/scripts/tests/fix-workflow-resources.sh" + +echo "fix-workflow-pins: $pass passed, $fail failed" +[ "$fail" -eq 0 ] diff --git a/scripts/tests/fix-workflow-resources.sh b/scripts/tests/fix-workflow-resources.sh new file mode 100755 index 00000000..bde85847 --- /dev/null +++ b/scripts/tests/fix-workflow-resources.sh @@ -0,0 +1,323 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: MPL-2.0 +# Resource boundaries for the workflow fixers changed in PR #615. Run alone +# or through fix-workflow-pins.sh. All mutations and command stubs stay in +# disposable fixtures; no GitHub requests or background processes are needed. +set -euo pipefail +# Child shells must use fixture commands rather than a user's startup file. +unset BASH_ENV +fleet="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +fx="$(mktemp -d)" +trap 'rm -rf "$fx"' EXIT +mkdir -m 700 "$fx/tmp" "$fx/locks" +export TMPDIR="$fx/tmp" PIN_FIXER_LOCK_DIR="$fx/locks" PIN_FIXER_LOCK_WAIT=0 +export TOOLCHAIN=stable +pass=0; fail=0 + +check() { + local label="$1"; shift + if "$@"; then + pass=$((pass + 1)); echo " ok $label" + else + fail=$((fail + 1)); echo " FAIL $label" >&2 + fi +} +run_fix() { rc=0; out=$("$@" 2>&1) || rc=$?; } +empty_dir() { [ -z "$(find "$1" -mindepth 1 -print -quit)" ]; } +same_inputs() { + cmp -s "$1/.github/workflows/ci.yml" "$2/.github/workflows/ci.yml" && + cmp -s "$1/.github/workflows/actions.lock" "$2/.github/workflows/actions.lock" +} + +stale=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +good=bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb +rust_sha=cccccccccccccccccccccccccccccccccccccccc +action_sha=dddddddddddddddddddddddddddddddddddddddd +printf 'fixture/reusables/.github/workflows/*\t%s\t%s\tfixture repair\n' "$stale" "$good" > "$fx/pins.tsv" +printf 'fixture/action\t%s\tv1.2.3\n' "$action_sha" > "$fx/tags.tsv" +export PINS_TABLE="$fx/pins.tsv" TAG_MAP="$fx/tags.tsv" + +make_repo() { + mkdir -p "$1/.github/workflows" + cat > "$1/.github/workflows/ci.yml" < "$1/.github/workflows/actions.lock" < "$fx/regular-file" +cp "$fx/regular-file" "$fx/regular-file.before" +for invalid_dir in "$fx/missing" "$fx/regular-file" "$fx/group-writable"; do + run_fix env PIN_FIXER_LOCK_DIR="$invalid_dir" bash -c \ + '. "$1"; pin_fixer_lock "$2"; echo acquired' _ \ + "$fleet/scripts/lib/pin-fixer-lock.sh" "$repo" + check "lock helper rejects $(basename "$invalid_dir")" test "$rc" -eq 2 + check "lock helper diagnoses $(basename "$invalid_dir")" grep -q 'not a private directory' <<<"$out" +done +check "a file passed as the lock directory is preserved" cmp -s "$fx/regular-file.before" "$fx/regular-file" +check "a missing configured lock directory is not silently created" test ! -e "$fx/missing" + +# XDG_RUNTIME_DIR takes precedence over TMPDIR. The default child directory +# is private even when the base is shared; an explicit directory wins over +# both environment defaults. +mkdir -m 1777 "$fx/runtime" "$fx/fallback" +run_fix env -u PIN_FIXER_LOCK_DIR XDG_RUNTIME_DIR="$fx/runtime" TMPDIR="$fx/fallback" \ + bash -c '. "$1"; pin_fixer_lock "$2"' _ "$fleet/scripts/lib/pin-fixer-lock.sh" "$repo" +check "the helper can use XDG_RUNTIME_DIR" test "$rc" -eq 0 +check "the runtime lock directory has mode 0700" test "$(stat -c %a "$fx/runtime/pin-fixer-$(id -u)")" = 700 +check "TMPDIR stays untouched when XDG_RUNTIME_DIR is available" empty_dir "$fx/fallback" +make_repo "$fx/explicit-lock-repo" +runtime_locks_before=$(find "$fx/runtime" -type f | wc -l) +explicit_locks_before=$(find "$PIN_FIXER_LOCK_DIR" -type f | wc -l) +run_fix env XDG_RUNTIME_DIR="$fx/runtime" TMPDIR="$fx/fallback" \ + bash -c '. "$1"; pin_fixer_lock "$2"' _ "$fleet/scripts/lib/pin-fixer-lock.sh" "$fx/explicit-lock-repo" +check "an explicit private lock directory overrides both defaults" test "$rc" -eq 0 +check "an explicit lock directory receives the new lock" \ + test "$(find "$PIN_FIXER_LOCK_DIR" -type f | wc -l)" -eq "$((explicit_locks_before + 1))" +check "an explicit lock directory leaves runtime locks unchanged" \ + test "$(find "$fx/runtime" -type f | wc -l)" -eq "$runtime_locks_before" +check "an explicit lock directory does not use the TMPDIR fallback" empty_dir "$fx/fallback" + +# The timeout fixer changed to mktemp plus an EXIT trap in a per-file +# subshell. Cover mutation, no-op and failure after both temporaries exist. +timeouts="$fx/timeouts" +mkdir -p "$timeouts/.github/workflows" "$timeouts/tmp" "$timeouts/bin" +cat > "$timeouts/before.yml" <<'YML' +jobs: + first: + runs-on: ubuntu-latest + steps: [] + existing: + runs-on: ubuntu-latest + timeout-minutes: 42 + steps: [] + last: + runs-on: ubuntu-latest + steps: [] +YML +cat > "$timeouts/expected.yml" <<'YML' +jobs: + first: + runs-on: ubuntu-latest + timeout-minutes: 17 + steps: [] + existing: + runs-on: ubuntu-latest + timeout-minutes: 42 + steps: [] + last: + runs-on: ubuntu-latest + timeout-minutes: 17 + steps: [] +YML +cp "$timeouts/before.yml" "$timeouts/.github/workflows/a.yml" +cp "$timeouts/before.yml" "$timeouts/.github/workflows/b.yaml" +run_fix env TMPDIR="$timeouts/tmp" DEFAULT_TIMEOUT=17 bash "$fleet/scripts/fix-workflow-timeout-minutes.sh" "$timeouts" +check "timeout fixer repairs both workflow extensions" test "$rc" -eq 0 +for workflow in a.yml b.yaml; do + check "timeout fixer preserves existing limits and repairs multiple jobs in $workflow" \ + cmp -s "$timeouts/expected.yml" "$timeouts/.github/workflows/$workflow" +done +check "timeout fixer removes temporary files after repairs" empty_dir "$timeouts/tmp" +run_fix env TMPDIR="$timeouts/tmp" DEFAULT_TIMEOUT=99 bash "$fleet/scripts/fix-workflow-timeout-minutes.sh" "$timeouts" +check "timeout no-op succeeds with a different default" test "$rc" -eq 0 +check "timeout no-op preserves the repaired workflow" cmp -s "$timeouts/expected.yml" "$timeouts/.github/workflows/a.yml" +check "timeout no-op removes its scan temporary" empty_dir "$timeouts/tmp" +cat > "$timeouts/bin/sed" <<'SH' +#!/usr/bin/env bash +echo 'fixture: insertion failed' >&2 +exit 47 +SH +chmod +x "$timeouts/bin/sed" +cp "$timeouts/before.yml" "$timeouts/failure.yml" +run_fix env TMPDIR="$timeouts/tmp" PATH="$timeouts/bin:$PATH" \ + bash "$fleet/scripts/fix-workflow-timeout-minutes.sh" "$timeouts/failure.yml" +check "timeout insertion errors remain failures" test "$rc" -eq 47 +check "timeout insertion failure preserves the source workflow" cmp -s "$timeouts/before.yml" "$timeouts/failure.yml" +check "timeout insertion failure removes both temporary files" empty_dir "$timeouts/tmp" + +# Exercise the changed allowlist temporary's lifecycle with an inert gh. +# Unknown commands fail, so a new API call cannot escape to the real CLI. +policy="$fx/policy" +mkdir -p "$policy/bin" +cat > "$policy/bin/gh" <<'SH' +#!/usr/bin/env bash +set -euo pipefail +printf '%s\n' "$*" >> "$POLICY_CASE/calls" +case "$*" in + 'api repos/hyperpolymath/standards/contents/.machine_readable/bot_exclusion_registry.a2ml --jq .content') + printf 'excluded = []\n' | base64 ;; + 'api users/fixture-owner --jq .type') printf 'User\n' ;; + 'api repos/fixture-owner/demo/actions/permissions') + printf '%s\n' '{"allowed_actions":"selected","sha_pinning_required":true}' ;; + 'api repos/fixture-owner/demo/actions/permissions/selected-actions --jq .patterns_allowed|length') + printf '0\n' ;; + 'api repos/hyperpolymath/standards/actions/permissions/selected-actions') + case "$POLICY_SCENARIO" in + fetch-failure) exit 48 ;; + empty) printf '%s\n' '{"patterns_allowed":[]}' ;; + invalid) printf 'not json\n' ;; + *) printf '%s\n' '{"patterns_allowed":["fixture/*"]}' ;; + esac ;; + 'api --method PUT repos/fixture-owner/demo/actions/permissions/selected-actions --input '*) + [ "$POLICY_SCENARIO" != put-failure ] || exit 47 + cp "${@: -1}" "$POLICY_CASE/applied.json" ;; + *) echo "unexpected fixture gh call: $*" >&2; exit 99 ;; +esac +SH +chmod +x "$policy/bin/gh" +for scenario in success fetch-failure empty invalid put-failure; do + policy_case="$policy/$scenario" + mkdir -p "$policy_case/tmp" + run_fix env TMPDIR="$policy_case/tmp" PATH="$policy/bin:$PATH" \ + POLICY_CASE="$policy_case" POLICY_SCENARIO="$scenario" HYPATIA_AUTOMATION=on \ + bash "$fleet/scripts/fix-actions-policy.sh" fixture-owner --repo demo --apply + case "$scenario" in success) expected_rc=0 ;; put-failure) expected_rc=47 ;; *) expected_rc=2 ;; esac + check "allowlist $scenario reports the expected status" test "$rc" -eq "$expected_rc" + check "allowlist $scenario reaches the download path" \ + grep -qx 'api repos/hyperpolymath/standards/actions/permissions/selected-actions' "$policy_case/calls" + check "allowlist $scenario leaves no temporary files" empty_dir "$policy_case/tmp" + if [ "$scenario" = success ]; then + check "the downloaded allowlist reaches the mocked PUT intact" \ + jq -e '.patterns_allowed == ["fixture/*"]' "$policy_case/applied.json" + else + check "allowlist $scenario never applies a payload" test ! -e "$policy_case/applied.json" + fi +done + +# A user-supplied allowlist is not owned by the fixer, including on failure. +printf '%s\n' '{"patterns_allowed":["fixture/*"]}' > "$policy/curated.json" +cp "$policy/curated.json" "$policy/curated.before" +for scenario in success put-failure; do + policy_case="$policy/curated-$scenario" + mkdir -p "$policy_case/tmp" + run_fix env TMPDIR="$policy_case/tmp" PATH="$policy/bin:$PATH" \ + POLICY_CASE="$policy_case" POLICY_SCENARIO="$scenario" HYPATIA_AUTOMATION=on \ + bash "$fleet/scripts/fix-actions-policy.sh" fixture-owner --repo demo --apply --allowlist "$policy/curated.json" + expected_rc=0; [ "$scenario" != put-failure ] || expected_rc=47 + check "curated allowlist $scenario reports the expected status" test "$rc" -eq "$expected_rc" + check "curated allowlist $scenario preserves the caller's file" cmp -s "$policy/curated.before" "$policy/curated.json" + check "curated allowlist $scenario needs no temporary file" empty_dir "$policy_case/tmp" + check "curated allowlist $scenario never downloads a replacement" \ + test "$(grep -c '^api repos/hyperpolymath/standards/actions/permissions/selected-actions$' "$policy_case/calls" || true)" -eq 0 +done + +# The maintenance report now persists under XDG_STATE_HOME with a private +# umask. A fixture runner exercises the real wrapper without running scans. +maintenance="$fx/maintenance" +mkdir -p "$maintenance/repo/scripts/maintenance" "$maintenance/case" +cat > "$maintenance/repo/scripts/maintenance/run-maintenance.sh" <<'SH' +#!/usr/bin/env bash +set -euo pipefail +printf '%s\n' "$@" > "$MAINTENANCE_CASE/args" +umask > "$MAINTENANCE_CASE/umask" +while [ "$#" -gt 0 ]; do + case "$1" in + --repo) shift 2 ;; + --output) output="$2"; shift 2 ;; + --strict|--fail-on-warn) shift ;; + *) exit 99 ;; + esac +done +printf '%s\n' "$output" > "$MAINTENANCE_CASE/output-path" +printf '%s\n' '{"summary":{"fail":0,"warn":0}}' > "$output" +SH +chmod +x "$maintenance/repo/scripts/maintenance/run-maintenance.sh" +run_fix env XDG_STATE_HOME="$maintenance/state with spaces" REPOS_BASE="$maintenance/no-repos" \ + PANIC_ATTACK_BIN= MAINTENANCE_CASE="$maintenance/case" \ + bash -c 'umask 022; exec bash "$@"' _ "$fleet/scripts/maintenance-hard-pass.sh" --repo "$maintenance/repo" +check "maintenance writes its default report successfully" test "$rc" -eq 0 +report=$(cat "$maintenance/case/output-path") +check "maintenance default report lives under XDG_STATE_HOME" \ + test "$(dirname "$report")" = "$maintenance/state with spaces/gitbot-fleet/maintenance" +check "maintenance default report is private despite the caller's umask" test "$(stat -c %a "$report")" = 600 +check "the maintenance runner runs under the caller's umask, not the report's" test "$(cat "$maintenance/case/umask")" = 0022 +check "the default report directory is private" test "$(stat -c %a "$(dirname "$report")")" = 700 +check "maintenance passes the strict gate to its runner" grep -qx -- --strict "$maintenance/case/args" +check "maintenance passes the warning gate to its runner" grep -qx -- --fail-on-warn "$maintenance/case/args" +run_fix env XDG_STATE_HOME="$maintenance/unused-state" REPOS_BASE="$maintenance/no-repos" \ + PANIC_ATTACK_BIN= MAINTENANCE_CASE="$maintenance/case" \ + bash "$fleet/scripts/maintenance-hard-pass.sh" --repo "$maintenance/repo" --output "$maintenance/explicit/report.json" +check "maintenance accepts an explicit report path" test "$rc" -eq 0 +check "maintenance preserves the explicit output path" \ + test "$(cat "$maintenance/case/output-path")" = "$maintenance/explicit/report.json" +check "an explicit maintenance report does not create the default state directory" test ! -e "$maintenance/unused-state" + +echo "fix-workflow-resources: $pass passed, $fail failed" +[ "$fail" -eq 0 ]