From c2c1753d77aba1072837946ad885abe39abd04d1 Mon Sep 17 00:00:00 2001 From: Crash0v3rrid3 Date: Fri, 16 Oct 2026 11:52:40 +0530 Subject: [PATCH] ci(spm): pin generated SPM dependency to a commit SHA and auto-bump it (DEVA11Y-477) 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 806f98cb (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 --- .github/scripts/update-spm-pin.sh | 44 +++++++++ .github/workflows/spm-pin-bump.yml | 140 +++++++++++++++++++++++++++++ scripts/bash/spm.sh | 2 +- scripts/bash/spm.sh.sha256 | 2 +- scripts/fish/spm.sh | 2 +- scripts/fish/spm.sh.sha256 | 2 +- scripts/zsh/spm.sh | 2 +- scripts/zsh/spm.sh.sha256 | 2 +- 8 files changed, 190 insertions(+), 6 deletions(-) create mode 100755 .github/scripts/update-spm-pin.sh create mode 100644 .github/workflows/spm-pin-bump.yml diff --git a/.github/scripts/update-spm-pin.sh b/.github/scripts/update-spm-pin.sh new file mode 100755 index 0000000..bab4af9 --- /dev/null +++ b/.github/scripts/update-spm-pin.sh @@ -0,0 +1,44 @@ +#!/usr/bin/env bash +# Pins the AccessibilityDevTools dependency in the synthetic Package.swift that +# scripts/{bash,zsh,fish}/spm.sh generate to an immutable commit SHA, and +# regenerates each script's .sha256 self-update sidecar (DEVA11Y-477 / F-005). +# +# Usage: .github/scripts/update-spm-pin.sh [<40-char sha>] +# With no argument, pins to the latest first-parent commit on origin/main that +# touched the plugin itself (Package.swift or Plugins/). Commits that only touch +# scripts/ (including the pin bumps this produces) never move that SHA, so a +# merged bump cannot trigger another bump. +# +# Prints the pinned SHA. Run from the repository root. +set -euo pipefail + +SCRIPTS=(scripts/bash/spm.sh scripts/zsh/spm.sh scripts/fish/spm.sh) +DEP_URL='https://github.com/browserstack/AccessibilityDevTools.git' + +sha="${1:-$(git log -1 --first-parent --format=%H origin/main -- Package.swift Plugins)}" +if ! printf '%s' "$sha" | grep -Eq '^[0-9a-f]{40}$'; then + echo "error: '${sha}' is not a full 40-character commit SHA" >&2 + exit 1 +fi + +sha256_of() { + if command -v sha256sum >/dev/null 2>&1; then sha256sum "$1" | awk '{print $1}' + else shasum -a 256 "$1" | awk '{print $1}'; fi +} + +for script in "${SCRIPTS[@]}"; do + # Replace whatever requirement the dependency line has today (branch: "main" or + # a previous revision: "") with the new revision pin. + sed -E "s#(\\.package\\(url: \"${DEP_URL}\", )(branch|revision): \"[^\"]*\"\\)#\\1revision: \"${sha}\")#" \ + "$script" > "${script}.tmp" + if ! grep -Fq ".package(url: \"${DEP_URL}\", revision: \"${sha}\")" "${script}.tmp"; then + rm -f "${script}.tmp" + echo "error: dependency line not found in ${script}" >&2 + exit 1 + fi + cat "${script}.tmp" > "$script" # keep the original file mode + rm -f "${script}.tmp" + printf '%s %s\n' "$(sha256_of "$script")" "$(basename "$script")" > "${script}.sha256" +done + +echo "$sha" diff --git a/.github/workflows/spm-pin-bump.yml b/.github/workflows/spm-pin-bump.yml new file mode 100644 index 0000000..44d84e9 --- /dev/null +++ b/.github/workflows/spm-pin-bump.yml @@ -0,0 +1,140 @@ +name: SPM dependency pin + +# scripts/{bash,zsh,fish}/spm.sh generate a throwaway Package.swift that depends on +# this repository. It used to track `branch: "main"`, so every scan resolved +# whatever HEAD of main was at that moment (DEVA11Y-477 / F-005, CWE-829). It is +# now pinned to an immutable `revision:` SHA, and this workflow keeps that pin +# current without going back to a mutable ref: +# +# bump — when plugin code (Package.swift / Plugins/) lands on main, runs +# .github/scripts/update-spm-pin.sh and opens (or refreshes) a PR moving +# the pin to that commit. The bump goes through review like any other +# change, so a bad commit on main does not reach users automatically. +# Bump PRs only touch scripts/, so merging one never moves the pin again. +# guard — on PRs touching scripts/, fails if any spm.sh stops pinning to a full +# 40-char revision (e.g. a revert back to `branch: "main"`). +# +# PRs opened with the default GITHUB_TOKEN do not trigger other workflows, so the +# required checks will not start on them on their own. Set a PIN_BUMP_TOKEN secret +# (GitHub App installation token or fine-grained PAT with contents + pull-requests +# write) to have them run; without it, close/reopen the bump PR to start checks. + +on: + push: + branches: [main] + paths: + - 'Package.swift' + - 'Plugins/**' + pull_request: + paths: + - 'scripts/**' + - '.github/scripts/update-spm-pin.sh' + - '.github/workflows/spm-pin-bump.yml' + schedule: + - cron: '0 6 * * 1' # weekly safety net in case a push run was missed + workflow_dispatch: + +permissions: + contents: read + +jobs: + bump: + name: Open PR bumping the SPM pin + if: github.event_name != 'pull_request' + runs-on: ubuntu-latest + timeout-minutes: 5 + permissions: + contents: write + pull-requests: write + concurrency: + group: spm-pin-bump + cancel-in-progress: false + env: + BUMP_BRANCH: ci/spm-pin-bump + GH_TOKEN: ${{ secrets.PIN_BUMP_TOKEN || github.token }} + steps: + - name: Checkout main (full history to find the latest plugin commit) + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + ref: main + fetch-depth: 0 + token: ${{ secrets.PIN_BUMP_TOKEN || github.token }} + + - name: Update pin in spm.sh scripts and sidecars + id: pin + run: | + set -euo pipefail + old=$(grep -oE 'AccessibilityDevTools\.git", revision: "[0-9a-f]{40}"' scripts/bash/spm.sh \ + | grep -oE '[0-9a-f]{40}' || true) + new=$(.github/scripts/update-spm-pin.sh) + echo "old=${old}" >> "$GITHUB_OUTPUT" + echo "new=${new}" >> "$GITHUB_OUTPUT" + if git diff --quiet; then + echo "Pin already at ${new}; nothing to do." + echo "changed=false" >> "$GITHUB_OUTPUT" + else + echo "changed=true" >> "$GITHUB_OUTPUT" + fi + + - name: Push bump branch and open/update PR + if: steps.pin.outputs.changed == 'true' + env: + OLD: ${{ steps.pin.outputs.old }} + NEW: ${{ steps.pin.outputs.new }} + run: | + set -euo pipefail + short="${NEW:0:12}" + git config user.name 'github-actions[bot]' + git config user.email '41898282+github-actions[bot]@users.noreply.github.com' + git switch -C "$BUMP_BRANCH" + git commit -qam "chore(spm): pin AccessibilityDevTools dependency to ${short} (DEVA11Y-477)" + # The bump branch is bot-owned and always rebuilt from main. + git push --force origin "HEAD:refs/heads/${BUMP_BRANCH}" + + { + 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." + echo + if [ -n "$OLD" ]; then + echo "Plugin changes since the previous pin \`${OLD:0:12}\`:" + echo + echo '```' + git log --first-parent --format='%h %s' "${OLD}..${NEW}" -- Package.swift Plugins + echo '```' + echo + fi + echo "Review those changes before merging: once merged, every \`spm.sh\` scan resolves the plugin at this commit." + echo + echo "Opened automatically by \`.github/workflows/spm-pin-bump.yml\` (DEVA11Y-477)." + } > "$RUNNER_TEMP/pr-body.md" + + pr=$(gh pr list --head "$BUMP_BRANCH" --base main --state open --json number --jq '.[0].number // empty') + if [ -n "$pr" ]; then + gh pr edit "$pr" --title "chore(spm): pin AccessibilityDevTools dependency to ${short}" --body-file "$RUNNER_TEMP/pr-body.md" + echo "Updated PR #${pr}" + else + gh pr create --base main --head "$BUMP_BRANCH" \ + --title "chore(spm): pin AccessibilityDevTools dependency to ${short}" \ + --body-file "$RUNNER_TEMP/pr-body.md" + fi + + guard: + name: spm.sh pins a full commit SHA + if: github.event_name == 'pull_request' + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + + - name: Require revision pin in every spm.sh + run: | + set -uo pipefail + status=0 + for script in scripts/bash/spm.sh scripts/zsh/spm.sh scripts/fish/spm.sh; do + if grep -Eq '\.package\(url: "https://github\.com/browserstack/AccessibilityDevTools\.git", revision: "[0-9a-f]{40}"\)' "$script"; then + echo "OK $script" + else + echo "FAILED $script must pin AccessibilityDevTools with revision: \"<40-char sha>\" (DEVA11Y-477); run .github/scripts/update-spm-pin.sh" + status=1 + fi + done + exit "$status" diff --git a/scripts/bash/spm.sh b/scripts/bash/spm.sh index 387daf2..576a593 100644 --- a/scripts/bash/spm.sh +++ b/scripts/bash/spm.sh @@ -112,7 +112,7 @@ import PackageDescription let package = Package( name: "Dummy", dependencies: [ - .package(url: "https://github.com/browserstack/AccessibilityDevTools.git", branch: "main") + .package(url: "https://github.com/browserstack/AccessibilityDevTools.git", revision: "806f98cb0e8a93656e52aba6394b9366ec0c5b3b") ], targets: [] ) diff --git a/scripts/bash/spm.sh.sha256 b/scripts/bash/spm.sh.sha256 index c4f9ac6..5f78e23 100644 --- a/scripts/bash/spm.sh.sha256 +++ b/scripts/bash/spm.sh.sha256 @@ -1 +1 @@ -b520c458bec538505c9ff9ca75a02ec901597fd66b3c2d951b928e58e58e60da spm.sh +6ec53f99f4976f5769c094e015fa99a9eb92cac406a7379fb1c0ef8d166bdf88 spm.sh diff --git a/scripts/fish/spm.sh b/scripts/fish/spm.sh index 2842249..035d477 100644 --- a/scripts/fish/spm.sh +++ b/scripts/fish/spm.sh @@ -125,7 +125,7 @@ import PackageDescription let package = Package( name: "Dummy", dependencies: [ - .package(url: "https://github.com/browserstack/AccessibilityDevTools.git", branch: "main") + .package(url: "https://github.com/browserstack/AccessibilityDevTools.git", revision: "806f98cb0e8a93656e52aba6394b9366ec0c5b3b") ], targets: [] ) diff --git a/scripts/fish/spm.sh.sha256 b/scripts/fish/spm.sh.sha256 index 7aac3c4..7ee79b8 100644 --- a/scripts/fish/spm.sh.sha256 +++ b/scripts/fish/spm.sh.sha256 @@ -1 +1 @@ -982bbb10bb9bd55428208454a766384fae58c41dacabb1e5b8a74d2366f0d1e7 spm.sh +07afdbe6582a562721f297eb5773cb5dd21b78e3ecb5de9f806ed07201aeafa3 spm.sh diff --git a/scripts/zsh/spm.sh b/scripts/zsh/spm.sh index 9619a0b..8377862 100644 --- a/scripts/zsh/spm.sh +++ b/scripts/zsh/spm.sh @@ -124,7 +124,7 @@ import PackageDescription let package = Package( name: "Dummy", dependencies: [ - .package(url: "https://github.com/browserstack/AccessibilityDevTools.git", branch: "main") + .package(url: "https://github.com/browserstack/AccessibilityDevTools.git", revision: "806f98cb0e8a93656e52aba6394b9366ec0c5b3b") ], targets: [] ) diff --git a/scripts/zsh/spm.sh.sha256 b/scripts/zsh/spm.sh.sha256 index 0cac838..9656f08 100644 --- a/scripts/zsh/spm.sh.sha256 +++ b/scripts/zsh/spm.sh.sha256 @@ -1 +1 @@ -1f28d5b3025636d9b81299032f34886f8804b1c6c95b830204325b325bd1d84f spm.sh +e2e9c9104892cbcca63edd3664783b058272d396efa03028a0083ae3789de90e spm.sh