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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 28 additions & 6 deletions scripts/complete-job-refs.sh
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ WF="${1:-.github/workflows}"
L="$WF/actions.lock"
[ -f "$L" ] || { echo "complete-job-refs: no $L" >&2; exit 1; }
TMP="$(mktemp -d)"; trap 'rm -rf "$TMP"' EXIT
WANT="$TMP/want"; NEW="$TMP/new.lock"
WANT="$TMP/want"; NEW="$TMP/new.lock"; DEPS="$TMP/deps"
# Local composite actions (standards#1122). A local action (`uses: ./dir`) has
# no dependency record of its own, so the actions it uses belong in the CALLING
# workflow's entry. gh actions-lock writes them there; prune-stale used to drop
Expand Down Expand Up @@ -97,18 +97,40 @@ for f in "$WF"/*.yml "$WF"/*.yaml; do
set -e
done | LC_ALL=C sort -u >> "$WANT"

# The dependency keys already recorded. When one workflow entry holds two
# case spellings of the same owner/repo@ref, the one with a record is kept.
awk -v q="'" '/^dependencies:/ { d = 1; next }
d && index($0, " " q) == 1 && substr($0, length($0) - 1) == q ":" { print substr($0, 6, length($0) - 7) }' "$L" > "$DEPS"

# 2. rewrite the workflows: section, merging wanted job refs into each path entry
LC_ALL=C awk -v want="$WANT" '
BEGIN { while ((getline l < want) > 0) { split(l, w, "\t"); WANT[w[1]] = WANT[w[1]] SUBSEP w[2] } }
LC_ALL=C awk -v want="$WANT" -v deps="$DEPS" '
BEGIN { while ((getline l < want) > 0) { split(l, w, "\t"); WANT[w[1]] = WANT[w[1]] SUBSEP w[2] }
while ((getline l < deps) > 0) DEP[l] = 1 }
# ck: the dedup key of a ref, split at the FIRST @ (owner/repo cannot contain
# one; a tag such as `release@2026` can). GitHub owner/repository names are
# case-insensitive; refs are not (the same rule as check-lock-sync.sh). An
# existing `swatinem/rust-cache@v2` entry therefore already covers
# `uses: Swatinem/rust-cache@v2`; adding the second spelling left a ref that
# close-lock then tried to resolve as a new repository.
function ck(s, i) { i = index(s, "@"); return i ? tolower(substr(s, 1, i - 1)) substr(s, i) : s }
# keep: add ref a to out unless a case spelling of it is already there; of two
# spellings, the one with a dependency record wins (a lock written before the
# case fix can hold both, sorted capitalised-first).
function keep(a, k) {
if (a == "") return
k = ck(a)
if (!(k in seen)) { seen[k] = ++c; out[c] = a }
else if ((a in DEP) && !(out[seen[k]] in DEP)) out[seen[k]] = a
}
# flush: write the current workflow entry, merged with its wanted refs, sorted and deduplicated.
function flush( i, n, arr, j, k, seen, out, c) {
function flush( i, n, arr, j, k) {
if (cur == "") return
SEEN[cur] = 1
n = split(items, arr, SUBSEP)
c = 0; delete seen; delete out
for (i = 1; i <= n; i++) if (arr[i] != "" && !(arr[i] in seen)) { seen[arr[i]] = 1; out[++c] = arr[i] }
for (i = 1; i <= n; i++) keep(arr[i])
if (cur in WANT) { n = split(WANT[cur], arr, SUBSEP)
for (i = 1; i <= n; i++) if (arr[i] != "" && !(arr[i] in seen)) { seen[arr[i]] = 1; out[++c] = arr[i] } }
for (i = 1; i <= n; i++) keep(arr[i]) }
if (c == 0) { printf " '\''%s'\'': []\n", cur; cur = ""; items = ""; return }
for (i = 1; i < c; i++) for (j = i + 1; j <= c; j++) if (out[j] < out[i]) { k = out[i]; out[i] = out[j]; out[j] = k }
printf " '\''%s'\'':\n", cur
Expand Down
56 changes: 56 additions & 0 deletions scripts/tests/complete-job-refs-test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,62 @@ else
bad "the local-composite block differs between complete-job-refs, prune-stale and check-lock-sync"
fi

echo "=== owner/repo case: an existing entry covers a differently cased uses: ==="
mkdir -p "$WORK/t5/.github/workflows"
printf 'jobs:\n a:\n runs-on: x\n steps:\n - uses: Swatinem/rust-cache@v2\n - uses: o/r@V3\n' \
> "$WORK/t5/.github/workflows/c.yml"
cat > "$WORK/t5/.github/workflows/actions.lock" <<EOF
workflows:
'.github/workflows/c.yml':
- 'o/r@v3'
- 'swatinem/rust-cache@v2'
dependencies:
'swatinem/rust-cache@v2':
ref: 'v2'
EOF
run t5 >/dev/null; st=$?
L5="$WORK/t5/.github/workflows/actions.lock"
[[ "$st" = 0 ]] && grep -qxF " - 'swatinem/rust-cache@v2'" "$L5" && ! grep -qF "'Swatinem/rust-cache@v2'" "$L5" \
&& ok "owner case differs only → existing entry kept, no second spelling" \
|| bad "owner case differs only → existing entry kept, no second spelling (st=$st)"
grep -qxF " - 'o/r@V3'" "$L5" && grep -qxF " - 'o/r@v3'" "$L5" \
&& ok "ref case differs → both refs kept (refs are case-sensitive)" \
|| bad "ref case differs → both refs kept (refs are case-sensitive)"

echo "=== a lock that already holds both spellings keeps the one with a record ==="
mkdir -p "$WORK/t6/.github/workflows"
cp "$WORK/t5/.github/workflows/c.yml" "$WORK/t6/.github/workflows/c.yml"
cat > "$WORK/t6/.github/workflows/actions.lock" <<EOF
workflows:
'.github/workflows/c.yml':
- 'Swatinem/rust-cache@v2'
- 'o/r@v3'
- 'swatinem/rust-cache@v2'
dependencies:
'swatinem/rust-cache@v2':
ref: 'v2'
EOF
run t6 >/dev/null; st=$?
L6="$WORK/t6/.github/workflows/actions.lock"
[[ "$st" = 0 ]] && grep -qxF " - 'swatinem/rust-cache@v2'" "$L6" && ! grep -qF "'Swatinem/rust-cache@v2'" "$L6" \
&& ok "both spellings present → the dependency-backed one survives" \
|| bad "both spellings present → the dependency-backed one survives (st=$st)"

echo "=== a tag containing @ keeps its case (split at the first @) ==="
mkdir -p "$WORK/t7/.github/workflows"
printf 'jobs:\n a:\n runs-on: x\n steps:\n - uses: o/r@Release@2026\n' > "$WORK/t7/.github/workflows/c.yml"
cat > "$WORK/t7/.github/workflows/actions.lock" <<EOF
workflows:
'.github/workflows/c.yml':
- 'o/r@release@2026'
dependencies:
EOF
run t7 >/dev/null; st=$?
L7="$WORK/t7/.github/workflows/actions.lock"
[[ "$st" = 0 ]] && grep -qxF " - 'o/r@Release@2026'" "$L7" && grep -qxF " - 'o/r@release@2026'" "$L7" \
&& ok "o/r@Release@2026 and o/r@release@2026 are different refs → both kept" \
|| bad "o/r@Release@2026 and o/r@release@2026 are different refs → both kept (st=$st)"

echo "=== structural ==="
mkrepo t3; printf 'dependencies:\n' > "$WORK/t3/.github/workflows/actions.lock"
run t3 >/dev/null; st=$?
Expand Down
Loading