diff --git a/.githooks/validate-actions-lock.sh b/.githooks/validate-actions-lock.sh index 734dec811..d93ec8ef7 100755 --- a/.githooks/validate-actions-lock.sh +++ b/.githooks/validate-actions-lock.sh @@ -108,6 +108,40 @@ lock_has() { return 1 } +# Leaf action entries in `dependencies:` whose `ref:` equals their own 40-hex +# SHA pin fail GitHub runner lockfile verification during `Set up job` +# ("Unable to resolve action ...: invalid lockfile pin", standards#1271). +mapfile -t SELF_SHA_KEYS < <( + awk ' + /^dependencies:/ { in_dep = 1; next } + /^[a-z_]+:/ { in_dep = 0; cur = ""; next } + !in_dep { next } + /^ \x27[^\x27]+\x27:$/ { + cur = $0; sub(/^ \x27/, "", cur); sub(/\x27:$/, "", cur) + next + } + cur != "" && /^ ref: \x27[^\x27]+\x27$/ { + r = $0; sub(/^ ref: \x27/, "", r); sub(/\x27$/, "", r) + n = index(cur, "@") + pin = (n > 0) ? substr(cur, n + 1) : "" + if (length(r) == 40 && r ~ /^[0-9a-fA-F]+$/ && tolower(r) == tolower(pin)) { + print tolower(cur) + } + cur = "" + } + ' "$LOCKFILE" | sort -u +) + +# Check a normalized owner/repo@sha key against the parsed self-SHA entries. +# Return 0 if its dependency ref equals its own SHA pin, or 1 if absent. +lock_has_self_sha() { + local needle="$1" k + for k in ${SELF_SHA_KEYS[@]+"${SELF_SHA_KEYS[@]}"}; do + [ "$k" = "$needle" ] && return 0 + done + return 1 +} + ERRORS=0 CHECKED=0 declare -a SEEN_ABSENT=() @@ -189,6 +223,10 @@ for ref in "${RAW[@]}"; do echo -e "${RED}[validate-actions-lock] ERROR: not in actions.lock: ${ref}${NC}" >&2 echo " normalised to: $norm" >&2 ERRORS=$((ERRORS + 1)) + elif lock_has_self_sha "$norm"; then + echo -e "${RED}[validate-actions-lock] ERROR: self-SHA ref in actions.lock dependencies: ${norm}${NC}" >&2 + echo " leaf action entries must carry a tag/branch ref, not their own commit SHA (standards#1271)" >&2 + ERRORS=$((ERRORS + 1)) fi done diff --git a/.github/workflows/actions.lock b/.github/workflows/actions.lock index 24a50d8ee..de16c0924 100644 --- a/.github/workflows/actions.lock +++ b/.github/workflows/actions.lock @@ -35,7 +35,7 @@ workflows: '.github/workflows/ci-pipeline.yml': - 'actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1' - 'haskell-actions/setup@0f8e8c99d88aeb3fbfd523f1ef2c6f762d10d64d' - - 'hyperpolymath/standards@571cc734cd69fb846032ec77a662aa8ee4fc32cd' + - 'hyperpolymath/standards@427eb35c40552d912d8c757518e8326f0d20f597' - 'oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6' '.github/workflows/codeql-reusable.yml': - 'actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1' @@ -109,7 +109,7 @@ workflows: - 'dtolnay/rust-toolchain@e2a55d2ffb04f378e9626c28d38b36d230d1e12f' - 'webfactory/ssh-agent@e83874834305fe9a4a2997156cb26c5de65a8555' '.github/workflows/mirror.yml': - - 'hyperpolymath/standards@b935e63937b1f1b5cbaeea7e375947d7b924038f' + - 'hyperpolymath/standards@427eb35c40552d912d8c757518e8326f0d20f597' '.github/workflows/no-js-scan.yml': - 'actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1' '.github/workflows/pages-archive.yml': @@ -229,11 +229,6 @@ dependencies: commit: 'sha1-22d081ff2d3a40755e97629de92e3bcbfa7cf2ed' owner_id: 42048915 repo_id: 356423100 - 'dtolnay/rust-toolchain@7e38f4b43b4db5c8dd498af069a4f6196df1d067': - ref: 'v1' - commit: 'sha1-7e38f4b43b4db5c8dd498af069a4f6196df1d067' - owner_id: 1940490 - repo_id: 260749683 'dtolnay/rust-toolchain@e2a55d2ffb04f378e9626c28d38b36d230d1e12f': ref: 'v1' commit: 'sha1-e2a55d2ffb04f378e9626c28d38b36d230d1e12f' @@ -250,12 +245,12 @@ dependencies: owner_id: 47606891 repo_id: 331103973 'github/codeql-action@b96794f015dfd88f77b49b1c93e0fa7110f94c63': - ref: 'b96794f015dfd88f77b49b1c93e0fa7110f94c63' + ref: 'v4.38.0' commit: 'sha1-b96794f015dfd88f77b49b1c93e0fa7110f94c63' owner_id: 9919 repo_id: 259445878 'goto-bus-stop/setup-zig@abea47f85e598557f500fa1fd2ab7464fcb39406': - ref: 'abea47f85e598557f500fa1fd2ab7464fcb39406' + ref: 'v2.2.1' commit: 'sha1-abea47f85e598557f500fa1fd2ab7464fcb39406' owner_id: 1006268 repo_id: 212984112 @@ -264,19 +259,16 @@ dependencies: commit: 'sha1-0f8e8c99d88aeb3fbfd523f1ef2c6f762d10d64d' owner_id: 75048950 repo_id: 623796603 - 'hyperpolymath/standards@571cc734cd69fb846032ec77a662aa8ee4fc32cd': - ref: '571cc734cd69fb846032ec77a662aa8ee4fc32cd' - commit: 'sha1-571cc734cd69fb846032ec77a662aa8ee4fc32cd' - owner_id: 6759885 - repo_id: 1116521501 - 'hyperpolymath/standards@b935e63937b1f1b5cbaeea7e375947d7b924038f': - ref: 'b935e63937b1f1b5cbaeea7e375947d7b924038f' - commit: 'sha1-b935e63937b1f1b5cbaeea7e375947d7b924038f' + 'hyperpolymath/standards@427eb35c40552d912d8c757518e8326f0d20f597': + ref: '427eb35c40552d912d8c757518e8326f0d20f597' + commit: 'sha1-427eb35c40552d912d8c757518e8326f0d20f597' owner_id: 6759885 repo_id: 1116521501 uses: - 'actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1' - - 'dtolnay/rust-toolchain@7e38f4b43b4db5c8dd498af069a4f6196df1d067' + - 'dtolnay/rust-toolchain@e2a55d2ffb04f378e9626c28d38b36d230d1e12f' + - 'goto-bus-stop/setup-zig@abea47f85e598557f500fa1fd2ab7464fcb39406' + - 'swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6' - 'webfactory/ssh-agent@e83874834305fe9a4a2997156cb26c5de65a8555' 'ocaml/setup-ocaml@93303b622b2522e4411e295f9e77411a24912ac7': ref: 'v3.9.0' @@ -304,7 +296,7 @@ dependencies: owner_id: 88700172 repo_id: 422287306 'swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6': - ref: '6323deb102c322ba6fcbdcafc7e3dddab59af2b6' + ref: 'v2.9.2' commit: 'sha1-6323deb102c322ba6fcbdcafc7e3dddab59af2b6' owner_id: 580492 repo_id: 298565987 @@ -314,7 +306,7 @@ dependencies: owner_id: 48932923 repo_id: 360548653 'webfactory/ssh-agent@e83874834305fe9a4a2997156cb26c5de65a8555': - ref: 'e83874834305fe9a4a2997156cb26c5de65a8555' + ref: 'v0.10.0' commit: 'sha1-e83874834305fe9a4a2997156cb26c5de65a8555' owner_id: 135788 repo_id: 208510314 diff --git a/.github/workflows/ci-pipeline.yml b/.github/workflows/ci-pipeline.yml index 9d2ec1478..6b69f765e 100644 --- a/.github/workflows/ci-pipeline.yml +++ b/.github/workflows/ci-pipeline.yml @@ -319,7 +319,7 @@ jobs: name: Secret scanning # Deliberately NOT gated on `detect`: a repo with no recognised ecosystem # can still be leaking credentials. - uses: hyperpolymath/standards/.github/workflows/secret-scanner-reusable.yml@571cc734cd69fb846032ec77a662aa8ee4fc32cd + uses: hyperpolymath/standards/.github/workflows/secret-scanner-reusable.yml@427eb35c40552d912d8c757518e8326f0d20f597 with: runs-on: ${{ inputs.runs-on }} @@ -330,7 +330,7 @@ jobs: name: Rust needs: detect if: ${{ needs.detect.outputs.has_rust == 'true' }} - uses: hyperpolymath/standards/.github/workflows/rust-ci-reusable.yml@571cc734cd69fb846032ec77a662aa8ee4fc32cd + uses: hyperpolymath/standards/.github/workflows/rust-ci-reusable.yml@427eb35c40552d912d8c757518e8326f0d20f597 with: runs-on: ${{ inputs.runs-on }} enable_coverage: ${{ inputs.enable_coverage }} diff --git a/.github/workflows/mirror.yml b/.github/workflows/mirror.yml index 03ca9e537..9298823f9 100644 --- a/.github/workflows/mirror.yml +++ b/.github/workflows/mirror.yml @@ -14,7 +14,7 @@ permissions: jobs: mirror: - uses: hyperpolymath/standards/.github/workflows/mirror-reusable.yml@b935e63937b1f1b5cbaeea7e375947d7b924038f + uses: hyperpolymath/standards/.github/workflows/mirror-reusable.yml@427eb35c40552d912d8c757518e8326f0d20f597 # Exactly the seven optional secrets the callee declares at the # pinned SHA (verified declared ⊇ referenced). Was `secrets: # inherit`, which forwarded every repo/org secret (standards#1108 diff --git a/scripts/close-lock.sh b/scripts/close-lock.sh index cc2f173d6..a3b2babe0 100755 --- a/scripts/close-lock.sh +++ b/scripts/close-lock.sh @@ -178,8 +178,24 @@ for pass in 1 2 3 4 5 6; do if ! printf '%s' "$sha" | grep -qxE '[0-9a-f]{40}'; then echo " SKIP $k (cannot resolve commit)"; skipped=$((skipped + 1)); continue fi + rec_ref="$ref" + if printf '%s' "$ref" | grep -qxE '[0-9a-f]{40}' && ! grep -qxF "$k$TAB" "$SC/owed"; then + # Consume all pages before choosing a tag. An early pipeline exit can + # SIGPIPE gh under pipefail and discard an otherwise valid result. + tag_ref="" + if "$GH_BIN" api "repos/$owner/$name/tags?per_page=100" --paginate \ + --jq '.[] | select(.commit.sha == "'"$sha"'") | .name' > "$SC/tags" 2>/dev/null; then + tag_ref=$(awk '/^[A-Za-z0-9._+\/-]+$/ && !(length($0) == 40 && $0 ~ /^[0-9a-f]+$/) { if (!first) first = $0; if (!preferred && $0 ~ /^v?[0-9]+\.[0-9]+\.[0-9]+/) preferred = $0 } END { if (preferred) print preferred; else if (first) print first }' "$SC/tags") + fi + if [ -z "$tag_ref" ]; then + echo " SKIP $k (cannot resolve a symbolic tag for leaf SHA)" + skipped=$((skipped + 1)) + continue + fi + rec_ref="$tag_ref" + fi { printf " '%s':\n ref: '%s'\n commit: 'sha1-%s'\n owner_id: %s\n repo_id: %s\n" \ - "$k" "$ref" "$sha" "${ids%% *}" "${ids##* }" + "$k" "$rec_ref" "$sha" "${ids%% *}" "${ids##* }" uses_block "$k"; } >> "$SC/new" echo " + $k" done < "$SC/miss" diff --git a/scripts/lock-selfcheck.sh b/scripts/lock-selfcheck.sh index aa8d3562b..b6f9b1a8d 100755 --- a/scripts/lock-selfcheck.sh +++ b/scripts/lock-selfcheck.sh @@ -110,6 +110,7 @@ for SHA in "$@"; do # Parse the lock into "\t" pairs. awk ' /^workflows:/ { in_wf = 1; next } + /^[a-z_]+:/ { in_wf = 0; next } !in_wf { next } # A workflow key line: \x27.github/workflows/foo.yml\x27: /^[[:space:]]+\x27[^\x27]+\x27:/ { @@ -128,6 +129,27 @@ for SHA in "$@"; do } ' "$TMP/lock" | sort -u > "$TMP/locked" + # Parse dependencies: entries whose `ref:` equals their own 40-hex SHA pin + # (self-SHA ref; rejected by the GitHub runner during Set up job, standards#1271). + awk ' + /^dependencies:/ { in_dep = 1; next } + /^[a-z_]+:/ { in_dep = 0; cur = ""; next } + !in_dep { next } + /^ \x27[^\x27]+\x27:$/ { + cur = $0; sub(/^ \x27/, "", cur); sub(/\x27:$/, "", cur) + next + } + cur != "" && /^ ref: \x27[^\x27]+\x27$/ { + r = $0; sub(/^ ref: \x27/, "", r); sub(/\x27$/, "", r) + n = index(cur, "@") + pin = (n > 0) ? substr(cur, n + 1) : "" + if (length(r) == 40 && r ~ /^[0-9a-fA-F]+$/ && tolower(r) == tolower(pin)) { + print tolower(cur) + } + cur = "" + } + ' "$TMP/lock" | sort -u > "$TMP/self_sha_deps" + locked_pairs=$(wc -l < "$TMP/locked") locked_wfs=$(cut -f1 "$TMP/locked" | sort -u | wc -l) @@ -144,6 +166,7 @@ for SHA in "$@"; do tr 'A-Z' 'a-z' < "$TMP/locked" > "$TMP/locked.lc" : > "$TMP/missing" + : > "$TMP/self_sha_used" : > "$TMP/unkeyed_wf" : > "$TMP/scanned" : > "$TMP/unparsed" @@ -189,6 +212,8 @@ for SHA in "$@"; do printf '%s\t%s\n' "$wf" "$key" >> "$TMP/unkeyed_wf" elif ! /usr/bin/grep -qF "$(printf '%s\t%s' "$wf_lc" "$key_lc")" "$TMP/locked.lc"; then printf '%s\t%s\n' "$wf" "$key" >> "$TMP/missing" + elif /usr/bin/grep -qxF "$key_lc" "$TMP/self_sha_deps"; then + printf '%s\t%s\n' "$wf" "$key" >> "$TMP/self_sha_used" fi done done < "$TMP/wfs" @@ -196,6 +221,7 @@ for SHA in "$@"; do n_actions=$(/usr/bin/grep -c 'ACTION$' "$TMP/scanned" 2>/dev/null || echo 0) n_reusable=$(/usr/bin/grep -c 'REUSABLE-CALL$' "$TMP/scanned" 2>/dev/null || echo 0) n_missing=$(sort -u "$TMP/missing" 2>/dev/null | wc -l) + n_self_sha=$(sort -u "$TMP/self_sha_used" 2>/dev/null | wc -l) n_unkeyed=$(sort -u "$TMP/unkeyed_wf" 2>/dev/null | wc -l) n_unkeyed_wf=$(cut -f1 "$TMP/unkeyed_wf" 2>/dev/null | sort -u | wc -l) @@ -211,18 +237,28 @@ for SHA in "$@"; do echo " VERDICT: UNEXAMINED — yq could not parse $(sort -u "$TMP/unparsed" | wc -l) workflow(s); their refs were not checked:" sort -u "$TMP/unparsed" | sed 's/^/ /' overall_rc=1 - elif [ "$n_missing" -eq 0 ]; then + elif [ "$n_missing" -eq 0 ] && [ "$n_self_sha" -eq 0 ]; then echo " VERDICT: SELF-CONSISTENT — every action ref used is keyed in this SHA's own lock." echo " (Reachability at the remote is NOT proven by this check.)" else - echo " VERDICT: POISON — ${n_missing} action ref(s) used but NOT keyed in this SHA's own lock." - echo " Any caller pinned here dies at startup with 0 jobs and no stated reason." - sort -u "$TMP/missing" | while IFS=$'\t' read -r wf key; do - echo " ${wf}" - echo " uses ${key}" - locked_for=$(/usr/bin/grep -F "$(printf '%s\t' "$wf")" "$TMP/locked" | cut -f2 | sort -u | tr '\n' ' ') - echo " lock keys for this workflow: ${locked_for:-}" - done + if [ "$n_missing" -gt 0 ]; then + echo " VERDICT: POISON — ${n_missing} action ref(s) used but NOT keyed in this SHA's own lock." + echo " Any caller pinned here dies at startup with 0 jobs and no stated reason." + sort -u "$TMP/missing" | while IFS=$'\t' read -r wf key; do + echo " ${wf}" + echo " uses ${key}" + locked_for=$(/usr/bin/grep -F "$(printf '%s\t' "$wf")" "$TMP/locked" | cut -f2 | sort -u | tr '\n' ' ') + echo " lock keys for this workflow: ${locked_for:-}" + done + fi + if [ "$n_self_sha" -gt 0 ]; then + echo " VERDICT: POISON — ${n_self_sha} action ref(s) carry a self-SHA ref: in dependencies: (standards#1271)." + echo " GitHub runner verification fails at Set up job ('invalid lockfile pin')." + sort -u "$TMP/self_sha_used" | while IFS=$'\t' read -r wf key; do + echo " ${wf}" + echo " self-SHA ref ${key}" + done + fi overall_rc=1 fi done diff --git a/scripts/relock-sha-keys.sh b/scripts/relock-sha-keys.sh index 28ca4ecd9..35fafb0da 100755 --- a/scripts/relock-sha-keys.sh +++ b/scripts/relock-sha-keys.sh @@ -39,11 +39,12 @@ MERGED="" trap 'rm -f "$INLINE" "$TAGGED" "$MAP" "$TMPLOCK" "$MERGED"' EXIT find "$WF_DIR" -maxdepth 1 -type f \( -name '*.yml' -o -name '*.yaml' \) -print0 \ | xargs -0 -r awk ' - match($0, /uses:[ \t]*[A-Za-z0-9_.-]+\/[A-Za-z0-9_.\/-]+@[0-9a-f]{40}/) { + match($0, /uses:[ \t]*[A-Za-z0-9_.-]+\/[A-Za-z0-9_.\/-]+@[0-9a-f]+/) { s = substr($0, RSTART, RLENGTH) sub(/^uses:[ \t]*/, "", s) n = split(s, parts, "@") path = parts[1]; sha = parts[2] + if (length(sha) != 40) next split(path, seg, "/") print tolower(seg[1] "/" seg[2]) "\t" sha }' | sort -u > "$INLINE" @@ -63,7 +64,7 @@ find "$WF_DIR" -maxdepth 1 -type f \( -name '*.yml' -o -name '*.yaml' \) -print0 sub(/^uses:[ \t]*/, "", s) n = index(s, "@") path = substr(s, 1, n - 1); ref = substr(s, n + 1) - if (ref ~ /^[0-9a-f]{40}$/) next + if (length(ref) == 40 && ref ~ /^[0-9a-f]+$/) next split(path, seg, "/") print tolower(seg[1] "/" seg[2]) "\t" ref }' | sort -u > "$TAGGED" @@ -82,16 +83,16 @@ awk -v inline="$INLINE" -v tagged="$TAGGED" ' n = index(key, "@") action = substr(key, 1, n - 1); ref = substr(key, n + 1) # already SHA-keyed: nothing to do - if (ref ~ /^[0-9a-f]{40}$/) { cur = ""; next } + if (length(ref) == 40 && ref ~ /^[0-9a-f]+$/) { cur = ""; next } # still written as this tag somewhere: keep the tag key (see 1b) if ((tolower(action) SUBSEP ref) in intag) { cur = ""; next } cur = key; curaction = action; next } # its body carries commit: '\''sha1-'\'' - cur != "" && match($0, /commit:[ \t]*'\''sha1-[0-9a-f]{40}'\''/) { + cur != "" && match($0, /commit:[ \t]*'\''sha1-[0-9a-f]+'\''/) { c = substr($0, RSTART, RLENGTH) sub(/.*sha1-/, "", c); sub(/'\''.*/, "", c) - if ((tolower(curaction) SUBSEP c) in seen) print cur "\t" curaction "@" c + if (length(c) == 40 && ((tolower(curaction) SUBSEP c) in seen)) print cur "\t" curaction "@" c cur = "" } ' "$LOCK" > "$MAP" @@ -127,18 +128,39 @@ rm -f "$MAP.count" # already holds a SHA-keyed record for a commit and the tool added a # tag-keyed record for the same commit, step 3 leaves TWO records under one # key, and the lockfile reader rejects the file ("already defined"). Both -# describe the same commit, so the first is kept and the later one dropped -# with its body. A list (a workflow entry, a record's uses:) that now names -# the same ref twice keeps it once. +# describe the same commit, so prefer the record whose `ref:` is symbolic +# (tag/branch) over a self-SHA `ref:` (which fails GitHub runner lockfile +# verification; standards#1271) and drop the duplicate with its body. A list +# (a workflow entry, a record's uses:) that now names the same ref twice +# keeps it once. MERGED="$(mktemp)" awk ' + NR == FNR { + if ($0 ~ /^[a-z_]+:/) { p1_sec = $0; p1_k = ""; next } + if (p1_sec ~ /^dependencies:/ && $0 ~ /^ \047[^\047]+\047:/) { + p1_k = $0; sub(/:.*$/, "", p1_k) + occ[p1_k]++ + p1_i = occ[p1_k] + if (!(p1_k in chosen)) chosen[p1_k] = p1_i + next + } + if (p1_sec ~ /^dependencies:/ && p1_k != "" && $0 ~ /^ ref: \047[^\047]+\047$/) { + r = $0; sub(/^ ref: \047/, "", r); sub(/\047$/, "", r) + is_sym = !(length(r) == 40 && r ~ /^[0-9a-f]+$/) + if (is_sym && !chosen_sym[p1_k]) { + chosen[p1_k] = p1_i + chosen_sym[p1_k] = 1 + } + } + next + } /^[a-z_]+:/ { sec = $0; skip = 0; delete items; print; next } /^ \047[^\047]+\047:/ { delete items if (sec ~ /^dependencies:/) { k = $0; sub(/:.*$/, "", k) - if (k in seen) { skip = 1; merged++; next } - seen[k] = 1 + seen[k]++ + if (seen[k] != chosen[k]) { skip = 1; merged++; next } } skip = 0; print; next } @@ -151,7 +173,7 @@ awk ' } { delete items; print } END { print merged + 0 > "/dev/stderr" } -' "$TMPLOCK" > "$MERGED" 2>"$MAP.merged" +' "$TMPLOCK" "$TMPLOCK" > "$MERGED" 2>"$MAP.merged" merged="$(cat "$MAP.merged" 2>/dev/null || echo 0)" rm -f "$MAP.merged" "$TMPLOCK" mv "$MERGED" "$LOCK" diff --git a/scripts/relock-standards-pin.sh b/scripts/relock-standards-pin.sh index e8f1d6156..912e5a5e4 100755 --- a/scripts/relock-standards-pin.sh +++ b/scripts/relock-standards-pin.sh @@ -130,7 +130,30 @@ awk -v k=" ${q}hyperpolymath/standards@$TARGET${q}:" '$0==k{s=1;next} s&&/^ git -C "$SROOT" show "${TARGET}:.github/workflows/actions.lock" > "$tmp/slock" 2>/dev/null || : > "$tmp/slock" while read -r u; do [ -n "$u" ] || continue - grep -qF " '$u':" "$tmp/lock" && continue + existing=$(rec_body "$u" "$tmp/lock") + if [ -n "$existing" ]; then + pin=${u##*@} + if [[ "$pin" =~ ^[0-9a-f]{40}$ ]] && printf '%s\n' "$existing" | grep -qxF " ref: '$pin'"; then + # A consumer can retain a poisoned copy after Standards repairs its own + # record. Refresh only ref:, and only with identical commit and IDs. + repaired=$(rec_body "$u" "$tmp/slock") + symbolic=$(printf '%s\n' "$repaired" | sed -n "s/^ ref: '\([^']*\)'$/\1/p") + [ -n "$symbolic" ] && [[ ! "$symbolic" =~ ^[0-9a-fA-F]{40}$ ]] \ + || refuse "no symbolic repair for $u in target Standards lock" + local_identity=$(printf '%s\n' "$existing" | grep -E '^ (commit|owner_id|repo_id):' || true) + source_identity=$(printf '%s\n' "$repaired" | grep -E '^ (commit|owner_id|repo_id):' || true) + [ "$local_identity" = "$source_identity" ] && [ "$(printf '%s\n' "$source_identity" | wc -l)" -eq 3 ] \ + || refuse "identity mismatch while repairing $u" + awk -v k=" '$u':" -v replacement=" ref: '$symbolic'" ' + $0 == k { selected = 1; print; next } + /^ [^ ]/ { selected = 0 } + selected && /^ ref:/ { print replacement; next } + { print } + ' "$tmp/lock" > "$tmp/repaired.lock" + mv "$tmp/repaired.lock" "$tmp/lock" + fi + continue + fi r=$(rec_body "$u" "$tmp/slock") [ -n "$r" ] || refuse "no record for $u in this lock or in standards' own lock at ${TARGET:0:12}" printf '%s\n' "$r" >> "$tmp/lock" diff --git a/scripts/tests/close-lock-test.sh b/scripts/tests/close-lock-test.sh index 2fd715a9a..24c8217d2 100755 --- a/scripts/tests/close-lock-test.sh +++ b/scripts/tests/close-lock-test.sh @@ -39,6 +39,14 @@ case "\$2" in if [ -f "\$f" ]; then cat "\$f"; else echo '{"message":"Not Found","status":"404"}'; exit 1; fi ;; repos/o/missing*) echo '{"message":"Not Found","status":"404"}'; exit 1 ;; repos/o/garbled*) echo '{"messa'; exit 0 ;; + repos/o/untagged/tags*) exit 0 ;; + repos/o/tagerror/tags*) echo 'v1.2.3'; exit 1 ;; + repos/o/badtag/tags*) echo '{"message":"bad response"}'; exit 0 ;; + repos/o/laterpage/tags*) + case " \$* " in *' --paginate '*) echo 'v2.3.4' ;; *) exit 0 ;; esac ;; + repos/o/a/tags*|repos/o/b/tags*|repos/o/c/tags*) echo 'v1.0.0' ;; + repos/o/buildmeta/tags*) echo 'v1.2.3+build.4' ;; + repos/o/leaf/tags*) printf 'v4\nv4.38.0\n' ;; repos/o/*/commits/v1) echo $C ;; repos/o/*/commits/*) echo '{"message":"No commit found"}'; exit 1 ;; repos/o/*) echo '7 9' ;; @@ -71,6 +79,33 @@ run "$d" >/dev/null grep -qxF " ref: 'v1'" "$d/actions.lock" && grep -qxF " commit: 'sha1-$C'" "$d/actions.lock" \ && ok "tag ref recorded with its resolved commit" || bad "tag ref recorded with its resolved commit" +echo "=== resolves a leaf SHA ref to its upstream semver tag (standards#1271) ===" +d=$(mklock t2b "o/leaf@$A" /dev/null +grep -qxF " 'o/leaf@$A':" "$d/actions.lock" && grep -qxF " ref: 'v4.38.0'" "$d/actions.lock" \ + && grep -qxF " commit: 'sha1-$A'" "$d/actions.lock" \ + && ok "leaf SHA ref records upstream semver tag in ref: ('v4.38.0')" \ + || bad "leaf SHA ref records upstream semver tag in ref:" + +echo "=== accepts a version tag with build metadata ===" +d=$(mklock buildmeta "o/buildmeta@$A" "$WF/actions.lock" <&1); st=$? && ok "a record's uses: list holds the merged ref once" || bad "a record's uses: list holds the merged ref once" [ "$(grep -c "^ commit: 'sha1-$A'" "$L")" = 1 ] \ && ok "the merged record's body appears once" || bad "the merged record's body appears once" +grep -qxF " ref: 'v7'" "$L" && ! grep -qxF " ref: '$A'" "$L" \ + && ok "merged record keeps the symbolic tag ref ('v7'), not the self-SHA ref" \ + || bad "merged record keeps the symbolic tag ref ('v7'), not the self-SHA ref" grep -qxF " 'actions/cache@$C':" "$L" && ! grep -qF "actions/cache@v4" "$L" \ && ok "plain re-key (no SHA record yet) still happens" || bad "plain re-key (no SHA record yet) still happens" grep -qxF " 'o/tagged@v2':" "$L" && grep -qxF " - 'o/tagged@v2'" "$L" \ diff --git a/scripts/tests/relock-standards-pin-test.sh b/scripts/tests/relock-standards-pin-test.sh index 5b6412e3e..db3e1cb1b 100755 --- a/scripts/tests/relock-standards-pin-test.sh +++ b/scripts/tests/relock-standards-pin-test.sh @@ -131,6 +131,22 @@ if command -v gawk >/dev/null 2>&1; then fi echo " PASS known answer (re-key, record rebuilt, needed record copied, OLD-only records pruned transitively, unrelated orphan kept)" +# Copied self-SHA leaf refs must not survive a repin to repaired Standards. +D=$(consumer stale-leaf) +sed -i "s/ref: 'v1'/ref: '1111111111111111111111111111111111111111'/" "$D/actions.lock" +bash "$RELOCK" --to "$NEW" --standards "$S" "$D" >/dev/null +grep -q "ref: '1111111111111111111111111111111111111111'" "$D/actions.lock" && fail "copied self-SHA leaf survived repin" +grep -q "ref: 'v1'" "$D/actions.lock" || fail "corrected symbolic ref missing" +echo " PASS copied self-SHA leaf refreshed from defining lock" + +D=$(consumer stale-identity) +sed -i "s/ref: 'v1'/ref: '1111111111111111111111111111111111111111'/; s/repo_id: 11$/repo_id: 999/" "$D/actions.lock" +cp "$D/actions.lock" "$T/identity-before" +if bash "$RELOCK" --to "$NEW" --standards "$S" "$D" >"$T/identity-log" 2>&1; then fail "identity mismatch accepted"; fi +cmp -s "$D/actions.lock" "$T/identity-before" || fail "refusal changed consumer lock" +grep -q 'identity mismatch' "$T/identity-log" || fail "wrong refusal reason" +echo " PASS identity mismatch refused without changing lock" + # 2. Idempotent. D=$(consumer idem); bash "$RELOCK" --to "$NEW" --standards "$S" "$D" >/dev/null cp "$D/actions.lock" "$T/once"; bash "$RELOCK" --to "$NEW" --standards "$S" "$D" >/dev/null diff --git a/scripts/tests/validate-actions-lock-test.sh b/scripts/tests/validate-actions-lock-test.sh index 2f20b6bc2..c62c1971b 100755 --- a/scripts/tests/validate-actions-lock-test.sh +++ b/scripts/tests/validate-actions-lock-test.sh @@ -116,6 +116,26 @@ reset_repo printf '{\n jobs: {\n' > "$WORK/repo/.github/workflows/ci.yml" expect "an unparseable workflow fails closed" 1 "ci.yml" +reset_repo +cat >> "$WORK/repo/.github/workflows/actions.lock" < "$WORK/repo/.github/workflows/ci.yml" < 0) ? substr(cur, 1, n - 1) : cur + pin = (n > 0) ? substr(cur, n + 1) : "" + if (tolower(nwo) != "hyperpolymath/standards" && length(r) == 40 && r ~ /^[0-9a-fA-F]+$/ && tolower(r) == tolower(pin)) { + print cur + } + cur = "" + } +' "$LOCKFILE")" +if [ -n "$self_sha_leaves" ]; then + fail "actions.lock contains leaf action entries with ref equal to their own SHA pin (standards#1271):" + printf '%s\n' "$self_sha_leaves" | sed 's/^/ /' +fi + # --- Verdict --------------------------------------------------------------- if [ "$fails" -gt 0 ]; then echo "::error::no-inherit: $fails check(s) failed"