@@ -78,19 +78,39 @@ a11y_scan() {
7878 $BINARY_PATH a11y $EXTRA_ARGS
7979}
8080
81- # Pinned, immutable git revision the self-update is allowed to fetch from.
82- # DEVA11Y-475: never fetch executable code from a mutable branch HEAD.
83- # Bump this (and the published .sha256 sidecars) on every release.
84- SELF_UPDATE_REF=" db817c37cf74cba47e2fef535f53a35bfc88ec6a"
81+ # Self-update tracks the latest launcher on `main` so users always run the
82+ # newest version. DEVA11Y-475/477/478: we deliberately follow main HEAD rather
83+ # than a pinned revision (per maintainer intent: always take the latest).
84+ # Hardening retained from the pinning work: download to a temp dir, verify a
85+ # SHA-256 sidecar (a download-integrity check, NOT an authenticity signature --
86+ # script and checksum share one origin), sanity-check the shebang, then
87+ # atomically replace the on-disk script. Keep scripts/bash/cli.sh.sha256 on main in
88+ # sync with this file (regenerate on every change) or updates will abort.
89+ SELF_UPDATE_BRANCH=" main"
90+ readonly SELF_UPDATE_BRANCH
8591SELF_UPDATE_RELPATH=" scripts/bash/cli.sh"
92+ readonly SELF_UPDATE_RELPATH
93+
94+ # sha256 with a portable fallback: GNU `sha256sum` (Linux) or `shasum -a 256`
95+ # (macOS / Perl Digest::SHA).
96+ _self_update_sha256 () {
97+ if command -v sha256sum > /dev/null 2>&1 ; then
98+ sha256sum " $1 " | awk ' {print $1}'
99+ else
100+ shasum -a 256 " $1 " | awk ' {print $1}'
101+ fi
102+ }
86103
87- # DEVA11Y-475 / F-003: self-update is OPT-IN (run with `--self-update`),
88- # fetches from a pinned revision (not a mutable branch), verifies a SHA-256
89- # checksum before use, and atomically replaces the script instead of
90- # overwriting the currently-running file in place.
91104script_self_update () {
92- local base_url=" https://raw.githubusercontent.com/browserstack/AccessibilityDevTools/${SELF_UPDATE_REF} /${SELF_UPDATE_RELPATH} "
93- local tmp_dir tmp_script tmp_sum expected_sum actual_sum
105+ local base_url=" https://raw.githubusercontent.com/browserstack/AccessibilityDevTools/refs/heads/${SELF_UPDATE_BRANCH} /${SELF_UPDATE_RELPATH} "
106+ local tmp_dir tmp_script tmp_sum expected_sum actual_sum local_sum target_path stage_file
107+
108+ # Resolve the on-disk target absolutely so the replace never depends on CWD.
109+ if [[ -n " $GIT_ROOT " && " $SCRIPT_PATH " != /* ]]; then
110+ target_path=" ${GIT_ROOT} /${SCRIPT_PATH} "
111+ else
112+ target_path=" $SCRIPT_PATH "
113+ fi
94114
95115 tmp_dir=$( mktemp -d " ${TMPDIR:-/ tmp} /bs-a11y-selfupdate.XXXXXX" ) || {
96116 echo " Self-update: failed to create temp dir." >&2
@@ -101,36 +121,50 @@ script_self_update() {
101121 tmp_script=" ${tmp_dir} /cli.sh"
102122 tmp_sum=" ${tmp_dir} /cli.sh.sha256"
103123
104- if ! curl -fsSL " $base_url " -o " $tmp_script " ; then
105- echo " Self-update: failed to download script from pinned revision." >&2
106- return 1
124+ # Fetch the checksum first; if our on-disk copy already matches, we're current.
125+ if ! curl -fsSL --connect-timeout 10 --max-time 30 " ${base_url} .sha256" -o " $tmp_sum " ; then
126+ echo " Self-update: could not fetch checksum from ${SELF_UPDATE_BRANCH} ; skipping update." >&2
127+ return 0
107128 fi
108- if ! curl -fsSL " ${base_url} .sha256" -o " $tmp_sum " ; then
109- echo " Self-update: failed to download checksum; aborting (integrity unverifiable)." >&2
110- return 1
129+ # Published sidecar is "<sha256> <filename>"; take the first field.
130+ expected_sum=$( awk ' {print $1; exit}' " $tmp_sum " )
131+ if [[ -f " $target_path " ]]; then
132+ local_sum=$( _self_update_sha256 " $target_path " )
133+ if [[ -n " $expected_sum " && " $local_sum " == " $expected_sum " ]]; then
134+ return 0
135+ fi
111136 fi
112137
113- if ! head -c2 " $tmp_script " | grep -q ' ^#! ' ; then
114- echo " Self-update: downloaded file is not a script; aborting ." >&2
115- return 1
138+ if ! curl -fsSL --connect-timeout 10 --max-time 30 " $base_url " -o " $tmp_script " ; then
139+ echo " Self-update: could not download latest script; skipping update ." >&2
140+ return 0
116141 fi
117142
118- # Published sidecar is "<sha256> <filename>"; take the first field.
119- expected_sum=$( awk ' {print $1; exit}' " $tmp_sum " )
120- actual_sum=$( shasum -a 256 " $tmp_script " | awk ' {print $1}' )
121- if [[ -z " $expected_sum " || " $expected_sum " != " $actual_sum " ]]; then
143+ actual_sum=$( _self_update_sha256 " $tmp_script " )
144+ if [[ -z " $expected_sum " || -z " $actual_sum " || " $expected_sum " != " $actual_sum " ]]; then
122145 echo " Self-update: checksum mismatch; refusing to apply." >&2
123146 echo " expected: ${expected_sum:- <empty>} " >&2
124- echo " actual: ${actual_sum} " >&2
147+ echo " actual: ${actual_sum:- <empty>} " >&2
148+ return 1
149+ fi
150+
151+ # Sanity check AFTER integrity: ensure the verified payload is a script.
152+ if ! head -c2 " $tmp_script " | grep -q ' ^#!' ; then
153+ echo " Self-update: downloaded file is not a script; aborting." >&2
125154 return 1
126155 fi
127156
128- chmod 0755 " $tmp_script "
129- # Atomic replace: never overwrite the running script in place.
130- if mv -f " $tmp_script " " $SCRIPT_PATH " ; then
131- echo " Self-update: updated ${SCRIPT_PATH} to pinned revision ${SELF_UPDATE_REF} ."
157+ # Stage inside the target's directory so the rename is atomic (mv across
158+ # filesystems would degrade to a non-atomic copy).
159+ stage_file=$( mktemp " $( dirname " $target_path " ) /.bs-a11y-update.XXXXXX" ) || {
160+ echo " Self-update: failed to stage update next to ${target_path} ." >&2
161+ return 1
162+ }
163+ if cp " $tmp_script " " $stage_file " && chmod 0755 " $stage_file " && mv -f " $stage_file " " $target_path " ; then
164+ echo " Self-update: updated ${target_path} to latest ${SELF_UPDATE_BRANCH} ."
132165 else
133- echo " Self-update: failed to replace ${SCRIPT_PATH} ." >&2
166+ rm -f -- " $stage_file "
167+ echo " Self-update: failed to replace ${target_path} ." >&2
134168 return 1
135169 fi
136170}
@@ -140,10 +174,10 @@ download_binary() {
140174 bsdtar -xvf " $BINARY_ZIP_PATH " -O > " $BINARY_PATH " && chmod 0775 " $BINARY_PATH "
141175}
142176
143- if [[ $SUBCOMMAND == " --self- update" ]] ; then
144- script_self_update
145- exit $?
146- fi
177+ # Best-effort auto- update: always fetch the latest launcher from main before
178+ # running. Failures (offline, integrity) are non-fatal -- the current script
179+ # keeps working and any update applies on the next invocation.
180+ script_self_update || true
147181
148182if [[ $SUBCOMMAND == " register-pre-commit-hook" ]]; then
149183 register_git_hook
0 commit comments