-
Notifications
You must be signed in to change notification settings - Fork 1
fix(scripts): pin self-update + SPM dependency to immutable revision (DEVA11Y-475,478,477) #30
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
fc2a1ef
12fc128
870b4f7
4293136
796017a
b6e699f
5f3c3d2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,74 @@ | ||
| name: Verify self-update checksums | ||
|
|
||
| # Self-update fetches each launcher script from `main` and verifies it against a | ||
| # committed `<script>.sha256` sidecar. If a script is edited without regenerating | ||
| # its sidecar, self-update silently breaks for every user (checksum mismatch → | ||
| # update refused). This workflow fails the PR/push when a sidecar is missing or | ||
| # out of sync, keeping the two in lockstep. (DEVA11Y-475 review follow-up.) | ||
|
|
||
| on: | ||
| pull_request: | ||
| paths: | ||
| - 'scripts/**' | ||
| - '.github/workflows/verify-selfupdate-checksums.yml' | ||
| push: | ||
| branches: [main] | ||
| paths: | ||
| - 'scripts/**' | ||
| - '.github/workflows/verify-selfupdate-checksums.yml' | ||
|
|
||
| permissions: | ||
| contents: read | ||
|
|
||
| jobs: | ||
| verify-sidecars: | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 | ||
|
|
||
| - name: Verify scripts and .sha256 sidecars are in sync | ||
| run: | | ||
| set -uo pipefail | ||
| shopt -s globstar nullglob | ||
| status=0 | ||
|
|
||
| # 1. Every self-updating script must have a sidecar. | ||
| script_files=(scripts/**/*.sh) | ||
| if [ ${#script_files[@]} -eq 0 ]; then | ||
| echo "::error::No .sh scripts found under scripts/ — checkout or glob is wrong." | ||
| exit 1 | ||
| fi | ||
| for script in "${script_files[@]}"; do | ||
| if [ ! -f "${script}.sha256" ]; then | ||
| echo "::error file=${script}::Missing checksum sidecar ${script}.sha256. Generate it from the script's directory: shasum -a 256 <name>.sh | awk '{print \$1\" <name>.sh\"}' > <name>.sh.sha256" | ||
| status=1 | ||
| fi | ||
| done | ||
|
|
||
| # 2. Every sidecar must match its script. | ||
| sidecars=(scripts/**/*.sha256) | ||
| if [ ${#sidecars[@]} -eq 0 ]; then | ||
| echo "::error::No .sha256 sidecars found under scripts/." | ||
| exit 1 | ||
| fi | ||
| for sidecar in "${sidecars[@]}"; do | ||
| dir=$(dirname "$sidecar") | ||
| script="${sidecar%.sha256}" | ||
| if [ ! -f "$script" ]; then | ||
| echo "::error file=${sidecar}::Sidecar references missing script ${script}." | ||
| status=1 | ||
| continue | ||
| fi | ||
| # Sidecars store "<sha256> <basename>", so verify from the script's dir. | ||
| if ( cd "$dir" && sha256sum -c "$(basename "$sidecar")" > /dev/null ); then | ||
| echo "::notice file=${script}::Checksum OK" | ||
| else | ||
| echo "::error file=${script}::Checksum mismatch — regenerate ${sidecar} after editing ${script} (run from ${dir}): shasum -a 256 <name>.sh | awk '{print \$1\" <name>.sh\"}' > <name>.sh.sha256" | ||
| status=1 | ||
| fi | ||
| done | ||
|
|
||
| if [ "$status" -ne 0 ]; then | ||
| echo "::error::Self-update checksum verification failed. Regenerate the affected .sha256 sidecar(s) and commit them." | ||
| fi | ||
| exit "$status" | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,3 +1,3 @@ | ||
| #!/usr/bin/env bash -il | ||
|
|
||
| GIT_ROOT=$(git rev-parse --show-toplevel 2>/dev/null) | ||
|
|
@@ -78,12 +78,103 @@ | |
| $BINARY_PATH a11y $EXTRA_ARGS | ||
| } | ||
|
|
||
| # Self-update tracks the latest launcher on `main` so users always run the | ||
| # newest version. DEVA11Y-475/477/478: we deliberately follow main HEAD rather | ||
| # than a pinned revision (per maintainer intent: always take the latest). | ||
| # Hardening retained from the pinning work: download to a temp dir, verify a | ||
| # SHA-256 sidecar (a download-integrity check, NOT an authenticity signature -- | ||
| # script and checksum share one origin), sanity-check the shebang, then | ||
| # atomically replace the on-disk script. Keep scripts/bash/cli.sh.sha256 on main in | ||
| # sync with this file (regenerate on every change) or updates will abort. | ||
| SELF_UPDATE_BRANCH="main" | ||
| readonly SELF_UPDATE_BRANCH | ||
| SELF_UPDATE_RELPATH="scripts/bash/cli.sh" | ||
| readonly SELF_UPDATE_RELPATH | ||
|
|
||
| # sha256 with a portable fallback: GNU `sha256sum` (Linux) or `shasum -a 256` | ||
| # (macOS / Perl Digest::SHA). | ||
| _self_update_sha256() { | ||
| if command -v sha256sum >/dev/null 2>&1; then | ||
| sha256sum "$1" | awk '{print $1}' | ||
| else | ||
| shasum -a 256 "$1" | awk '{print $1}' | ||
| fi | ||
| } | ||
|
|
||
| script_self_update() { | ||
| local remote_url="https://raw.githubusercontent.com/browserstack/AccessibilityDevTools/refs/heads/main/scripts/bash/cli.sh" | ||
| local base_url="https://raw.githubusercontent.com/browserstack/AccessibilityDevTools/refs/heads/${SELF_UPDATE_BRANCH}/${SELF_UPDATE_RELPATH}" | ||
| local tmp_dir tmp_script tmp_sum expected_sum actual_sum local_sum target_path stage_file | ||
|
|
||
| # Resolve the on-disk target absolutely so the replace never depends on CWD. | ||
| if [[ -n "$GIT_ROOT" && "$SCRIPT_PATH" != /* ]]; then | ||
| target_path="${GIT_ROOT}/${SCRIPT_PATH}" | ||
| else | ||
| target_path="$SCRIPT_PATH" | ||
| fi | ||
|
|
||
| tmp_dir=$(mktemp -d "${TMPDIR:-/tmp}/bs-a11y-selfupdate.XXXXXX") || { | ||
| echo "Self-update: failed to create temp dir." >&2 | ||
| return 1 | ||
| } | ||
| # Clean the work dir and any half-written staged file so an interrupt between | ||
| # staging and the final mv can't leak a dotfile in the target directory. The | ||
| # RETURN trap also clears the signal traps so they don't linger past this | ||
| # function (which would otherwise swallow Ctrl-C during the main command). | ||
| # tmp_dir is expanded now; stage_file is expanded when the trap fires (escaped $). | ||
| # shellcheck disable=SC2064 | ||
| trap "rm -rf -- '${tmp_dir}'; rm -f -- \"\${stage_file:-}\"; trap - INT TERM" RETURN | ||
| # shellcheck disable=SC2064 | ||
| trap "rm -rf -- '${tmp_dir}'; rm -f -- \"\${stage_file:-}\"; exit 130" INT TERM | ||
| tmp_script="${tmp_dir}/cli.sh" | ||
| tmp_sum="${tmp_dir}/cli.sh.sha256" | ||
|
|
||
| # Fetch the checksum first; if our on-disk copy already matches, we're current. | ||
| if ! curl -fsSL --connect-timeout 10 --max-time 30 "${base_url}.sha256" -o "$tmp_sum"; then | ||
| echo "Self-update: could not fetch checksum from ${SELF_UPDATE_BRANCH}; skipping update." >&2 | ||
| return 0 | ||
| fi | ||
| # Published sidecar is "<sha256> <filename>"; take the first field. | ||
| expected_sum=$(awk '{print $1; exit}' "$tmp_sum") | ||
| if [[ -f "$target_path" ]]; then | ||
| local_sum=$(_self_update_sha256 "$target_path") | ||
| if [[ -n "$expected_sum" && "$local_sum" == "$expected_sum" ]]; then | ||
| return 0 | ||
| fi | ||
| fi | ||
|
|
||
| if ! curl -fsSL --connect-timeout 10 --max-time 30 "$base_url" -o "$tmp_script"; then | ||
| echo "Self-update: could not download latest script; skipping update." >&2 | ||
| return 0 | ||
| fi | ||
|
|
||
| actual_sum=$(_self_update_sha256 "$tmp_script") | ||
| if [[ -z "$expected_sum" || -z "$actual_sum" || "$expected_sum" != "$actual_sum" ]]; then | ||
| echo "Self-update: checksum mismatch; refusing to apply." >&2 | ||
| echo " expected: ${expected_sum:-<empty>}" >&2 | ||
| echo " actual: ${actual_sum:-<empty>}" >&2 | ||
| # Integrity violation — distinct exit code (2) so the caller can tell this | ||
| # apart from a benign network skip (0) or an operational error (1). | ||
| return 2 | ||
| fi | ||
|
|
||
| updated_script=$(curl -R -z "$SCRIPT_PATH" "$remote_url") | ||
| if [[ $updated_script =~ ^#! ]]; then | ||
| echo "$updated_script" > "$SCRIPT_PATH" | ||
| # Sanity check AFTER integrity: ensure the verified payload is a script. | ||
| if ! head -c2 "$tmp_script" | grep -q '^#!'; then | ||
| echo "Self-update: downloaded file is not a script; aborting." >&2 | ||
| return 2 | ||
| fi | ||
|
|
||
| # Stage inside the target's directory so the rename is atomic (mv across | ||
| # filesystems would degrade to a non-atomic copy). | ||
| stage_file=$(mktemp "$(dirname "$target_path")/.bs-a11y-update.XXXXXX") || { | ||
|
Crash0v3rrid3 marked this conversation as resolved.
|
||
| echo "Self-update: failed to stage update next to ${target_path}." >&2 | ||
| return 1 | ||
| } | ||
| if cp "$tmp_script" "$stage_file" && chmod 0755 "$stage_file" && mv -f "$stage_file" "$target_path"; then | ||
|
Crash0v3rrid3 marked this conversation as resolved.
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Low] Clear After Suggestion: set Reviewer: stack:code-reviewer |
||
| echo "Self-update: updated ${target_path} to latest ${SELF_UPDATE_BRANCH}." | ||
| else | ||
| rm -f -- "$stage_file" | ||
| echo "Self-update: failed to replace ${target_path}." >&2 | ||
| return 1 | ||
| fi | ||
| } | ||
|
|
||
|
|
@@ -92,7 +183,18 @@ | |
| bsdtar -xvf "$BINARY_ZIP_PATH" -O > "$BINARY_PATH" && chmod 0775 "$BINARY_PATH" | ||
| } | ||
|
|
||
| script_self_update | ||
| # Best-effort auto-update: always fetch the latest launcher from main before | ||
| # running. Network/offline failures are silent (rc 0) and operational errors | ||
| # (rc 1) are non-fatal -- the existing script keeps working. An integrity | ||
| # failure (rc 2: checksum mismatch or non-script payload) leaves the verified | ||
| # on-disk script untouched and is surfaced loudly below, but still does not | ||
| # block the tool (per the always-run-latest, never-block design). | ||
| _self_update_rc=0 | ||
| script_self_update || _self_update_rc=$? | ||
| if [[ "$_self_update_rc" -eq 2 ]]; then | ||
| echo "Self-update: integrity verification FAILED; kept the existing verified script (possible corruption or tampering)." >&2 | ||
| fi | ||
|
|
||
| if [[ $SUBCOMMAND == "register-pre-commit-hook" ]]; then | ||
| register_git_hook | ||
| exit 0 | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| 2340d95e6ce35ea8656acb02e0b524eeccca02c12a940d44e8cd0c0032b0efdd cli.sh |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| 2d627fbe06991da445f7fbcd8bc8c4f5c4dd126c04372f74dd0f4ab39222c57e spm.sh |
Uh oh!
There was an error while loading. Please reload this page.