Skip to content

Commit da0ebbe

Browse files
ci(spm): pin generated SPM dependency to a commit SHA and auto-bump it (DEVA11Y-477) (#45)
scripts/{bash,zsh,fish}/spm.sh generated a Package.swift that tracked branch: "main", so every scan resolved whatever HEAD of main was at that moment (F-005, CWE-829). Pin it to revision 806f98c (latest main commit touching Package.swift/Plugins) and regenerate the .sha256 self-update sidecars. To keep scans on the latest plugin without a mutable ref, add spm-pin-bump.yml: when plugin code lands on main (plus weekly / manual), it runs .github/scripts/update-spm-pin.sh and opens or refreshes a PR moving the pin, listing the plugin commits being pulled in. Bumps go through review, and since bump PRs only touch scripts/ they never move the pin themselves. A guard job fails PRs where any spm.sh stops pinning a full 40-char revision, so the PR #30-style revert to branch: "main" can't recur silently. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 806f98c commit da0ebbe

8 files changed

Lines changed: 190 additions & 6 deletions

File tree

‎.github/scripts/update-spm-pin.sh‎

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
#!/usr/bin/env bash
2+
# Pins the AccessibilityDevTools dependency in the synthetic Package.swift that
3+
# scripts/{bash,zsh,fish}/spm.sh generate to an immutable commit SHA, and
4+
# regenerates each script's .sha256 self-update sidecar (DEVA11Y-477 / F-005).
5+
#
6+
# Usage: .github/scripts/update-spm-pin.sh [<40-char sha>]
7+
# With no argument, pins to the latest first-parent commit on origin/main that
8+
# touched the plugin itself (Package.swift or Plugins/). Commits that only touch
9+
# scripts/ (including the pin bumps this produces) never move that SHA, so a
10+
# merged bump cannot trigger another bump.
11+
#
12+
# Prints the pinned SHA. Run from the repository root.
13+
set -euo pipefail
14+
15+
SCRIPTS=(scripts/bash/spm.sh scripts/zsh/spm.sh scripts/fish/spm.sh)
16+
DEP_URL='https://github.com/browserstack/AccessibilityDevTools.git'
17+
18+
sha="${1:-$(git log -1 --first-parent --format=%H origin/main -- Package.swift Plugins)}"
19+
if ! printf '%s' "$sha" | grep -Eq '^[0-9a-f]{40}$'; then
20+
echo "error: '${sha}' is not a full 40-character commit SHA" >&2
21+
exit 1
22+
fi
23+
24+
sha256_of() {
25+
if command -v sha256sum >/dev/null 2>&1; then sha256sum "$1" | awk '{print $1}'
26+
else shasum -a 256 "$1" | awk '{print $1}'; fi
27+
}
28+
29+
for script in "${SCRIPTS[@]}"; do
30+
# Replace whatever requirement the dependency line has today (branch: "main" or
31+
# a previous revision: "<sha>") with the new revision pin.
32+
sed -E "s#(\\.package\\(url: \"${DEP_URL}\", )(branch|revision): \"[^\"]*\"\\)#\\1revision: \"${sha}\")#" \
33+
"$script" > "${script}.tmp"
34+
if ! grep -Fq ".package(url: \"${DEP_URL}\", revision: \"${sha}\")" "${script}.tmp"; then
35+
rm -f "${script}.tmp"
36+
echo "error: dependency line not found in ${script}" >&2
37+
exit 1
38+
fi
39+
cat "${script}.tmp" > "$script" # keep the original file mode
40+
rm -f "${script}.tmp"
41+
printf '%s %s\n' "$(sha256_of "$script")" "$(basename "$script")" > "${script}.sha256"
42+
done
43+
44+
echo "$sha"

‎.github/workflows/spm-pin-bump.yml‎

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
name: SPM dependency pin
2+
3+
# scripts/{bash,zsh,fish}/spm.sh generate a throwaway Package.swift that depends on
4+
# this repository. It used to track `branch: "main"`, so every scan resolved
5+
# whatever HEAD of main was at that moment (DEVA11Y-477 / F-005, CWE-829). It is
6+
# now pinned to an immutable `revision:` SHA, and this workflow keeps that pin
7+
# current without going back to a mutable ref:
8+
#
9+
# bump — when plugin code (Package.swift / Plugins/) lands on main, runs
10+
# .github/scripts/update-spm-pin.sh and opens (or refreshes) a PR moving
11+
# the pin to that commit. The bump goes through review like any other
12+
# change, so a bad commit on main does not reach users automatically.
13+
# Bump PRs only touch scripts/, so merging one never moves the pin again.
14+
# guard — on PRs touching scripts/, fails if any spm.sh stops pinning to a full
15+
# 40-char revision (e.g. a revert back to `branch: "main"`).
16+
#
17+
# PRs opened with the default GITHUB_TOKEN do not trigger other workflows, so the
18+
# required checks will not start on them on their own. Set a PIN_BUMP_TOKEN secret
19+
# (GitHub App installation token or fine-grained PAT with contents + pull-requests
20+
# write) to have them run; without it, close/reopen the bump PR to start checks.
21+
22+
on:
23+
push:
24+
branches: [main]
25+
paths:
26+
- 'Package.swift'
27+
- 'Plugins/**'
28+
pull_request:
29+
paths:
30+
- 'scripts/**'
31+
- '.github/scripts/update-spm-pin.sh'
32+
- '.github/workflows/spm-pin-bump.yml'
33+
schedule:
34+
- cron: '0 6 * * 1' # weekly safety net in case a push run was missed
35+
workflow_dispatch:
36+
37+
permissions:
38+
contents: read
39+
40+
jobs:
41+
bump:
42+
name: Open PR bumping the SPM pin
43+
if: github.event_name != 'pull_request'
44+
runs-on: ubuntu-latest
45+
timeout-minutes: 5
46+
permissions:
47+
contents: write
48+
pull-requests: write
49+
concurrency:
50+
group: spm-pin-bump
51+
cancel-in-progress: false
52+
env:
53+
BUMP_BRANCH: ci/spm-pin-bump
54+
GH_TOKEN: ${{ secrets.PIN_BUMP_TOKEN || github.token }}
55+
steps:
56+
- name: Checkout main (full history to find the latest plugin commit)
57+
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
58+
with:
59+
ref: main
60+
fetch-depth: 0
61+
token: ${{ secrets.PIN_BUMP_TOKEN || github.token }}
62+
63+
- name: Update pin in spm.sh scripts and sidecars
64+
id: pin
65+
run: |
66+
set -euo pipefail
67+
old=$(grep -oE 'AccessibilityDevTools\.git", revision: "[0-9a-f]{40}"' scripts/bash/spm.sh \
68+
| grep -oE '[0-9a-f]{40}' || true)
69+
new=$(.github/scripts/update-spm-pin.sh)
70+
echo "old=${old}" >> "$GITHUB_OUTPUT"
71+
echo "new=${new}" >> "$GITHUB_OUTPUT"
72+
if git diff --quiet; then
73+
echo "Pin already at ${new}; nothing to do."
74+
echo "changed=false" >> "$GITHUB_OUTPUT"
75+
else
76+
echo "changed=true" >> "$GITHUB_OUTPUT"
77+
fi
78+
79+
- name: Push bump branch and open/update PR
80+
if: steps.pin.outputs.changed == 'true'
81+
env:
82+
OLD: ${{ steps.pin.outputs.old }}
83+
NEW: ${{ steps.pin.outputs.new }}
84+
run: |
85+
set -euo pipefail
86+
short="${NEW:0:12}"
87+
git config user.name 'github-actions[bot]'
88+
git config user.email '41898282+github-actions[bot]@users.noreply.github.com'
89+
git switch -C "$BUMP_BRANCH"
90+
git commit -qam "chore(spm): pin AccessibilityDevTools dependency to ${short} (DEVA11Y-477)"
91+
# The bump branch is bot-owned and always rebuilt from main.
92+
git push --force origin "HEAD:refs/heads/${BUMP_BRANCH}"
93+
94+
{
95+
echo "Moves the \`revision:\` pin in the Package.swift generated by \`scripts/{bash,zsh,fish}/spm.sh\` to \`${NEW}\`, the latest commit on \`main\` that changed the plugin, and regenerates the \`.sha256\` self-update sidecars."
96+
echo
97+
if [ -n "$OLD" ]; then
98+
echo "Plugin changes since the previous pin \`${OLD:0:12}\`:"
99+
echo
100+
echo '```'
101+
git log --first-parent --format='%h %s' "${OLD}..${NEW}" -- Package.swift Plugins
102+
echo '```'
103+
echo
104+
fi
105+
echo "Review those changes before merging: once merged, every \`spm.sh\` scan resolves the plugin at this commit."
106+
echo
107+
echo "Opened automatically by \`.github/workflows/spm-pin-bump.yml\` (DEVA11Y-477)."
108+
} > "$RUNNER_TEMP/pr-body.md"
109+
110+
pr=$(gh pr list --head "$BUMP_BRANCH" --base main --state open --json number --jq '.[0].number // empty')
111+
if [ -n "$pr" ]; then
112+
gh pr edit "$pr" --title "chore(spm): pin AccessibilityDevTools dependency to ${short}" --body-file "$RUNNER_TEMP/pr-body.md"
113+
echo "Updated PR #${pr}"
114+
else
115+
gh pr create --base main --head "$BUMP_BRANCH" \
116+
--title "chore(spm): pin AccessibilityDevTools dependency to ${short}" \
117+
--body-file "$RUNNER_TEMP/pr-body.md"
118+
fi
119+
120+
guard:
121+
name: spm.sh pins a full commit SHA
122+
if: github.event_name == 'pull_request'
123+
runs-on: ubuntu-latest
124+
timeout-minutes: 5
125+
steps:
126+
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
127+
128+
- name: Require revision pin in every spm.sh
129+
run: |
130+
set -uo pipefail
131+
status=0
132+
for script in scripts/bash/spm.sh scripts/zsh/spm.sh scripts/fish/spm.sh; do
133+
if grep -Eq '\.package\(url: "https://github\.com/browserstack/AccessibilityDevTools\.git", revision: "[0-9a-f]{40}"\)' "$script"; then
134+
echo "OK $script"
135+
else
136+
echo "FAILED $script must pin AccessibilityDevTools with revision: \"<40-char sha>\" (DEVA11Y-477); run .github/scripts/update-spm-pin.sh"
137+
status=1
138+
fi
139+
done
140+
exit "$status"

‎scripts/bash/spm.sh‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ import PackageDescription
112112
let package = Package(
113113
name: "Dummy",
114114
dependencies: [
115-
.package(url: "https://github.com/browserstack/AccessibilityDevTools.git", branch: "main")
115+
.package(url: "https://github.com/browserstack/AccessibilityDevTools.git", revision: "806f98cb0e8a93656e52aba6394b9366ec0c5b3b")
116116
],
117117
targets: []
118118
)

‎scripts/bash/spm.sh.sha256‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
b520c458bec538505c9ff9ca75a02ec901597fd66b3c2d951b928e58e58e60da spm.sh
1+
6ec53f99f4976f5769c094e015fa99a9eb92cac406a7379fb1c0ef8d166bdf88 spm.sh

‎scripts/fish/spm.sh‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@ import PackageDescription
125125
let package = Package(
126126
name: "Dummy",
127127
dependencies: [
128-
.package(url: "https://github.com/browserstack/AccessibilityDevTools.git", branch: "main")
128+
.package(url: "https://github.com/browserstack/AccessibilityDevTools.git", revision: "806f98cb0e8a93656e52aba6394b9366ec0c5b3b")
129129
],
130130
targets: []
131131
)

‎scripts/fish/spm.sh.sha256‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
982bbb10bb9bd55428208454a766384fae58c41dacabb1e5b8a74d2366f0d1e7 spm.sh
1+
07afdbe6582a562721f297eb5773cb5dd21b78e3ecb5de9f806ed07201aeafa3 spm.sh

‎scripts/zsh/spm.sh‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,7 @@ import PackageDescription
124124
let package = Package(
125125
name: "Dummy",
126126
dependencies: [
127-
.package(url: "https://github.com/browserstack/AccessibilityDevTools.git", branch: "main")
127+
.package(url: "https://github.com/browserstack/AccessibilityDevTools.git", revision: "806f98cb0e8a93656e52aba6394b9366ec0c5b3b")
128128
],
129129
targets: []
130130
)

‎scripts/zsh/spm.sh.sha256‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
1f28d5b3025636d9b81299032f34886f8804b1c6c95b830204325b325bd1d84f spm.sh
1+
e2e9c9104892cbcca63edd3664783b058272d396efa03028a0083ae3789de90e spm.sh

0 commit comments

Comments
 (0)