Skip to content
Open
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
96 changes: 91 additions & 5 deletions scripts/bash/cli.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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") || {
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
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
}

Expand All @@ -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

if [[ $SUBCOMMAND == "register-pre-commit-hook" ]]; then
register_git_hook
exit 0
Expand Down
1 change: 1 addition & 0 deletions scripts/bash/cli.sh.sha256
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
9af5ce77ada28741e91d2323e4664c47e7e7531e10b34168cfe6bc50a74f5d62 cli.sh
101 changes: 95 additions & 6 deletions scripts/bash/spm.sh
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,10 @@ import PackageDescription
let package = Package(
name: "Dummy",
dependencies: [
.package(url: "https://github.com/browserstack/AccessibilityDevTools.git", branch: "main")
// DEVA11Y-477 / F-005: pin to an immutable revision instead of a mutable
// branch HEAD. No release tags exist yet; this is the current origin/main
// SHA. Bump to a release tag (.exact("x.y.z")) once tags are published.
.package(url: "https://github.com/browserstack/AccessibilityDevTools.git", revision: "db817c37cf74cba47e2fef535f53a35bfc88ec6a")
],
targets: []
)
Expand All @@ -83,16 +86,102 @@ EOF
scan $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/spm.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/spm.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/spm.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
}
# shellcheck disable=SC2064
trap "rm -rf -- '${tmp_dir}'" RETURN
tmp_script="${tmp_dir}/spm.sh"
tmp_sum="${tmp_dir}/spm.sh.sha256"

updated_script=$(curl -R -z "$SCRIPT_PATH" "$remote_url")
if [[ $updated_script =~ ^#! ]]; then
echo "$updated_script" > "$SCRIPT_PATH"
# 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") || {
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
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
}

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

if [[ $SUBCOMMAND == "register-pre-commit-hook" ]]; then
register_git_hook
exit 0
Expand Down
1 change: 1 addition & 0 deletions scripts/bash/spm.sh.sha256
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
9be47b26350acd877948997dce43c6582da9cb0206c5c2e56db88c415c63579c spm.sh
96 changes: 91 additions & 5 deletions scripts/fish/cli.sh
Original file line number Diff line number Diff line change
Expand Up @@ -90,12 +90,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/fish/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/fish/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/fish/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

updated_script=$(curl -R -z "$SCRIPT_PATH" "$remote_url")
if [[ $updated_script =~ ^#! ]]; then
echo "$updated_script" > "$SCRIPT_PATH"
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") || {
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
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
}

Expand All @@ -104,7 +186,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

if [[ $SUBCOMMAND == "register-pre-commit-hook" ]]; then
register_git_hook
exit 0
Expand Down
1 change: 1 addition & 0 deletions scripts/fish/cli.sh.sha256
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
637518c077cf013e1d420d49334a5bd792b674b9b9fd04469f2198875afd7ea1 cli.sh
Loading
Loading