-
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 4 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,69 @@ | ||
| 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@v4 | ||
|
|
||
| - 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. | ||
| for script in scripts/**/*.sh; do | ||
|
Crash0v3rrid3 marked this conversation as resolved.
Outdated
|
||
| 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")" ); then | ||
|
Crash0v3rrid3 marked this conversation as resolved.
Outdated
|
||
| echo "OK: $sidecar" | ||
| 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" | ||
|
github-advanced-security[bot] marked this conversation as resolved.
Fixed
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -78,12 +78,94 @@ a11y_scan() { | |
| $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 | ||
|
|
||
| updated_script=$(curl -R -z "$SCRIPT_PATH" "$remote_url") | ||
| if [[ $updated_script =~ ^#! ]]; then | ||
| echo "$updated_script" > "$SCRIPT_PATH" | ||
| # 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 | ||
| } | ||
| # shellcheck disable=SC2064 | ||
| trap "rm -rf -- '${tmp_dir}'" RETURN | ||
| 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 | ||
| return 1 | ||
| fi | ||
|
|
||
| # 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 1 | ||
| 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 +174,11 @@ download_binary() { | |
| 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. Failures (offline, integrity) are non-fatal -- the current script | ||
| # keeps working and any update applies on the next invocation. | ||
| script_self_update || true | ||
|
Crash0v3rrid3 marked this conversation as resolved.
Outdated
|
||
|
|
||
| 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 @@ | ||
| 9af5ce77ada28741e91d2323e4664c47e7e7531e10b34168cfe6bc50a74f5d62 cli.sh |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| 9be47b26350acd877948997dce43c6582da9cb0206c5c2e56db88c415c63579c spm.sh |
Uh oh!
There was an error while loading. Please reload this page.