browser4-cli 4.13.17 → 4.13.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -27,8 +27,8 @@ dim() { echo -e "\033[2m$*\033[0m"; }
27
27
 
28
28
  test() {
29
29
  local name="$1"
30
- local fn="$2"
31
- if "$fn"; then
30
+ shift
31
+ if "$@"; then
32
32
  PASS=$((PASS + 1))
33
33
  green " PASS $name"
34
34
  else
@@ -109,26 +109,65 @@ test "--source invalid rejected" bash -c "
109
109
  bash '$INSTALL_SCRIPT' --source invalid --dry-run 2>&1 | grep -qi 'github.*oss\|must be'
110
110
  "
111
111
 
112
+ test "--version rejects a following option as its value" bash -c "
113
+ rc=0
114
+ out=\$(bash '$INSTALL_SCRIPT' --version --dry-run 2>&1) || rc=\$?
115
+ [[ \$rc -ne 0 ]] && echo \"\$out\" | grep -q 'requires a value'
116
+ "
117
+
118
+ test "--install-dir rejects a following option as its value" bash -c "
119
+ rc=0
120
+ out=\$(bash '$INSTALL_SCRIPT' --install-dir --silent 2>&1) || rc=\$?
121
+ [[ \$rc -ne 0 ]] && echo \"\$out\" | grep -q 'requires a value'
122
+ "
123
+
124
+ test "--version accepts a bare semver and prefixes it with v" bash -c "
125
+ bash '$INSTALL_SCRIPT' --locate --version 4.13.18 2>&1 | grep -q 'releases/download/v4\.13\.18/'
126
+ "
127
+
128
+ test "--version with the v prefix is left untouched" bash -c "
129
+ bash '$INSTALL_SCRIPT' --locate --version v4.13.18 2>&1 | grep -q 'releases/download/v4\.13\.18/'
130
+ "
131
+
112
132
  test "--dry-run flag accepted" bash -c "
113
- bash '$INSTALL_SCRIPT' --dry-run 2>&1 | grep -qi 'DRY-RUN\|dry-run\|Install'
133
+ bash '$INSTALL_SCRIPT' --dry-run 2>&1 | grep -q 'DRY-RUN'
114
134
  "
115
135
 
116
- test "--silent flag accepted" bash -c "
117
- bash '$INSTALL_SCRIPT' --silent --dry-run 2>&1 | grep -v '^$' | head -1 | grep -qv '→\|step'
136
+ test "--silent suppresses progress output" bash -c "
137
+ out=\$(bash '$INSTALL_SCRIPT' --silent --dry-run 2>&1)
138
+ echo \"\$out\" | grep -q 'DRY-RUN' && ! echo \"\$out\" | grep -q 'Install:'
118
139
  "
119
140
 
120
141
  test "--skip-local flag accepted" bash -c "
121
- bash '$INSTALL_SCRIPT' --skip-local --dry-run 2>&1 | grep -qi 'skip\|DRY-RUN\|Already\|Install'
142
+ bash '$INSTALL_SCRIPT' --skip-local --dry-run 2>&1 | grep -q 'Skipping local binary check'
122
143
  "
123
144
 
124
145
  test "--no-path flag accepted" bash -c "
125
- bash '$INSTALL_SCRIPT' --no-path --dry-run 2>&1 | grep -vq 'Added to PATH'
146
+ ! bash '$INSTALL_SCRIPT' --no-path --dry-run 2>&1 | grep -q 'Added to PATH'
126
147
  "
127
148
 
128
149
  test "unknown arg rejected" bash -c "
129
150
  bash '$INSTALL_SCRIPT' --bogus-flag 2>&1 | grep -q 'Unknown argument'
130
151
  "
131
152
 
153
+ # A PATH that has everything the diagnostics need except the download tooling.
154
+ # `--locate` must still answer; a real install must refuse with a clear message
155
+ # instead of failing later with 'curl: command not found'.
156
+ minimal_path_locate_works() {
157
+ local dir; dir="$(mktemp -d)"
158
+ local bash_bin; bash_bin=$(command -v bash)
159
+ local tool tool_path
160
+ for tool in uname stat cat grep awk sed dirname basename tr od; do
161
+ tool_path=$(command -v "$tool") && ln -sf "$tool_path" "$dir/$tool"
162
+ done
163
+ local locate_rc=0 install_rc=0
164
+ PATH="$dir" "$bash_bin" "$INSTALL_SCRIPT" --locate >/dev/null 2>&1 || locate_rc=$?
165
+ PATH="$dir" "$bash_bin" "$INSTALL_SCRIPT" --dry-run --no-path >/dev/null 2>&1 || install_rc=$?
166
+ rm -rf "$dir"
167
+ [[ $locate_rc -eq 0 ]] && [[ $install_rc -ne 0 ]]
168
+ }
169
+ test "--locate needs no download tooling, installing does" minimal_path_locate_works
170
+
132
171
  echo ""
133
172
 
134
173
  # ── Locate mode ────────────────────────────────────────
@@ -191,6 +230,103 @@ test "OSS URL NOT using broken download/latest pattern" bash -c "
191
230
 
192
231
  echo ""
193
232
 
233
+ # ── Download integrity (stubbed curl -- no network) ────
234
+
235
+ echo "--- Download integrity ---" | cyan
236
+
237
+ STUB_DIR="$(mktemp -d)"
238
+ mkdir -p "$STUB_DIR"
239
+
240
+ cat > "$STUB_DIR/curl" << 'STUBEOF'
241
+ #!/usr/bin/env bash
242
+ # curl stub for the installer tests. Honours -o <file>, takes the body from
243
+ # STUB_BODY (html = an error page, elf = a native binary), reports STUB_STATUS
244
+ # and records its own command line in STUB_ARGS_FILE so a test can assert on
245
+ # the headers the installer would have sent.
246
+ out=""
247
+ printf '%s\n' "$*" >> "${STUB_ARGS_FILE:-/dev/null}"
248
+ while [[ $# -gt 0 ]]; do
249
+ case "$1" in
250
+ -o) out="$2"; shift 2 ;;
251
+ *) shift ;;
252
+ esac
253
+ done
254
+ if [[ "${STUB_STATUS:-200}" == "200" ]]; then
255
+ if [[ "${STUB_BODY:-elf}" == "html" ]]; then
256
+ { printf '<html>'; head -c 200000 /dev/zero | tr '\0' 'x'; printf '</html>'; } > "$out"
257
+ else
258
+ { printf '\177ELF\002\001\001\000'; head -c 200000 /dev/zero; } > "$out"
259
+ fi
260
+ fi
261
+ echo "${STUB_STATUS:-200}"
262
+ exit "${STUB_EXIT:-0}"
263
+ STUBEOF
264
+ chmod +x "$STUB_DIR/curl"
265
+
266
+ stub_install() {
267
+ # $1 = body kind (html|elf), $2 = source, $3 = args file, $4 = extra env
268
+ local body="$1" source="$2" args_file="$3" dir rc=0
269
+ dir="$(mktemp -d)"
270
+ STUB_BODY="$body" STUB_ARGS_FILE="$args_file" \
271
+ PATH="$STUB_DIR:$PATH" bash "$INSTALL_SCRIPT" \
272
+ --source "$source" --no-path --skip-backend --skip-local \
273
+ --install-dir "$dir/inst" >"$dir/out" 2>&1 || rc=$?
274
+ printf '%s\n' "$rc" > "$dir/rc"
275
+ echo "$dir"
276
+ }
277
+
278
+ download_rejects_html() {
279
+ local args; args="$(mktemp)"
280
+ local dir; dir=$(stub_install html oss "$args")
281
+ local rc; rc=$(cat "$dir/rc")
282
+ local rejected=0
283
+ grep -q 'not a native executable' "$dir/out" && rejected=1
284
+ rm -rf "$dir" "$args"
285
+ [[ "$rc" -ne 0 ]] && [[ $rejected -eq 1 ]]
286
+ }
287
+
288
+ download_accepts_elf() {
289
+ local args; args="$(mktemp)"
290
+ local dir; dir=$(stub_install elf oss "$args")
291
+ local rc; rc=$(cat "$dir/rc")
292
+ local installed; installed=$(find "$dir/inst" -maxdepth 1 -type f -name 'browser4-cli-*' 2>/dev/null | head -1)
293
+ local ok=1
294
+ [[ "$rc" -eq 0 ]] && [[ -n "$installed" ]] && [[ -x "$installed" ]] && ok=0
295
+ rm -rf "$dir" "$args"
296
+ [[ $ok -eq 0 ]]
297
+ }
298
+
299
+ token_not_sent_to_oss() {
300
+ local args; args="$(mktemp)"
301
+ export GITHUB_TOKEN=stub-secret-token
302
+ local dir; dir=$(stub_install elf oss "$args")
303
+ unset GITHUB_TOKEN
304
+ local leaked=0
305
+ grep -q 'stub-secret-token' "$args" && leaked=1
306
+ rm -rf "$dir" "$args"
307
+ [[ $leaked -eq 0 ]]
308
+ }
309
+
310
+ token_sent_to_github() {
311
+ local args; args="$(mktemp)"
312
+ export GITHUB_TOKEN=stub-secret-token
313
+ local dir; dir=$(stub_install elf github "$args")
314
+ unset GITHUB_TOKEN
315
+ local sent=0
316
+ grep -q 'stub-secret-token' "$args" && sent=1
317
+ rm -rf "$dir" "$args"
318
+ [[ $sent -eq 1 ]]
319
+ }
320
+
321
+ test "download rejects an HTML error page (bad magic bytes)" download_rejects_html
322
+ test "download accepts a native binary body" download_accepts_elf
323
+ test "GITHUB_TOKEN is never sent to the OSS mirror" token_not_sent_to_oss
324
+ test "GITHUB_TOKEN is sent to GitHub" token_sent_to_github
325
+
326
+ rm -rf "$STUB_DIR"
327
+
328
+ echo ""
329
+
194
330
  # ── Functions (extracted via copy without main call) ──
195
331
 
196
332
  echo "--- Functions ---" | cyan
@@ -232,7 +368,9 @@ test "get_default_install_dir returns non-empty" bash -c "
232
368
 
233
369
  test "detect_china_locale returns 0 or 1 (no crash)" bash -c "
234
370
  source '$FUNC_TEST_SCRIPT' >/dev/null 2>&1
235
- detect_china_locale && true || true
371
+ rc=0
372
+ detect_china_locale || rc=\$?
373
+ [[ \$rc -eq 0 || \$rc -eq 1 ]]
236
374
  "
237
375
 
238
376
  test "detect_os returns valid OS" bash -c "
@@ -323,7 +461,10 @@ test "header is ASCII (no box-drawing chars)" bash -c "
323
461
  "
324
462
 
325
463
  test "header does not contain Unicode box-drawing" bash -c "
326
- ! bash '$INSTALL_SCRIPT' --locate 2>&1 | grep -P '[\x{2500}-\x{257F}]' 2>/dev/null || true
464
+ # Portable (no grep -P): drop the ANSI colour escapes first, then any byte
465
+ # outside printable ASCII would be a non-ASCII character; the installer
466
+ # prints ASCII only.
467
+ ! bash '$INSTALL_SCRIPT' --locate 2>&1 | tr -d '\033' | LC_ALL=C grep -q '[^ -~]'
327
468
  "
328
469
 
329
470
  echo ""
@@ -333,7 +474,13 @@ echo ""
333
474
  echo "--- Edge cases ---" | cyan
334
475
 
335
476
  test "double dash in --version handled (no operator parsing)" bash -c "
336
- bash '$INSTALL_SCRIPT' --locate 2>&1 | grep -q 'version'
477
+ # '--version --dry-run' must not be read as the tag '--dry-run'; the value
478
+ # has to be a release tag, and a bare semver gets the 'v' prefix.
479
+ rc=0
480
+ out=\$(bash '$INSTALL_SCRIPT' --version --dry-run 2>&1) || rc=\$?
481
+ [[ \$rc -ne 0 ]] || exit 1
482
+ echo \"\$out\" | grep -q 'looks like another option' || exit 1
483
+ bash '$INSTALL_SCRIPT' --locate --version 4.13.18 2>&1 | grep -q 'releases/download/v4\.13\.18/'
337
484
  "
338
485
 
339
486
  test "empty --version with dry-run doesn't download" bash -c "
@@ -0,0 +1,219 @@
1
+ #!/usr/bin/env bash
2
+ # wait-for-npm-version.tests.sh
3
+ # Test suite for wait-for-npm-version.sh
4
+ # Uses only bash builtins and standard Unix tools -- no external test
5
+ # frameworks and no network: `npm` is stubbed on PATH.
6
+ #
7
+ # Run:
8
+ # bash cli/scripts/tests/wait-for-npm-version.tests.sh
9
+
10
+ set -euo pipefail
11
+
12
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
13
+ SCRIPT_UNDER_TEST="$(cd "$SCRIPT_DIR/.." && pwd)/wait-for-npm-version.sh"
14
+
15
+ PASS=0
16
+ FAIL=0
17
+
18
+ green() { echo -e "\033[0;32m$*\033[0m"; }
19
+ red() { echo -e "\033[0;31m$*\033[0m"; }
20
+ cyan() { echo -e "\033[0;36m$*\033[0m"; }
21
+ dim() { echo -e "\033[2m$*\033[0m"; }
22
+
23
+ # test <name> <command...>
24
+ # The whole command is forwarded: `test "x" bash -c "..."` must not drop the
25
+ # `-c "..."` part, or a bare `bash` would exit 0 and the assertion would pass
26
+ # vacuously.
27
+ test() {
28
+ local name="$1"
29
+ shift
30
+ if "$@"; then
31
+ PASS=$((PASS + 1))
32
+ green " PASS $name"
33
+ else
34
+ FAIL=$((FAIL + 1))
35
+ red " FAIL $name"
36
+ fi
37
+ }
38
+
39
+ # ---- stub npm ---------------------------------------------------------
40
+
41
+ STUB_DIR="$(mktemp -d)"
42
+ cleanup() { rm -rf "$STUB_DIR"; }
43
+ trap cleanup EXIT
44
+
45
+ cat > "$STUB_DIR/npm" <<'STUB'
46
+ #!/usr/bin/env bash
47
+ # Fake `npm view <package>@<version> version`, driven by the test environment:
48
+ # FAKE_NPM_MODE available | available-after:<n> | unavailable |
49
+ # network-error-once | wrong-version
50
+ # FAKE_NPM_VERSION version reported on success
51
+ # FAKE_NPM_COUNT_FILE file counting the invocations
52
+ set -euo pipefail
53
+
54
+ count_file="${FAKE_NPM_COUNT_FILE:?FAKE_NPM_COUNT_FILE is required}"
55
+ count=0
56
+ if [ -f "$count_file" ]; then count="$(cat "$count_file")"; fi
57
+ count=$((count + 1))
58
+ printf '%s' "$count" > "$count_file"
59
+
60
+ not_found() {
61
+ echo "npm error code E404" >&2
62
+ echo "npm error 404 No match found for version ${FAKE_NPM_VERSION:?FAKE_NPM_VERSION is required}" >&2
63
+ exit 1
64
+ }
65
+
66
+ mode="${FAKE_NPM_MODE:?FAKE_NPM_MODE is required}"
67
+ case "$mode" in
68
+ available)
69
+ printf '%s\n' "$FAKE_NPM_VERSION"
70
+ ;;
71
+ available-after:*)
72
+ threshold="${mode#available-after:}"
73
+ if [ "$count" -ge "$threshold" ]; then
74
+ printf '%s\n' "$FAKE_NPM_VERSION"
75
+ else
76
+ not_found
77
+ fi
78
+ ;;
79
+ unavailable)
80
+ not_found
81
+ ;;
82
+ wrong-version)
83
+ printf '%s\n' "0.0.1"
84
+ ;;
85
+ network-error-once)
86
+ if [ "$count" -ge 2 ]; then
87
+ printf '%s\n' "$FAKE_NPM_VERSION"
88
+ else
89
+ echo "npm error network request to https://registry.npmjs.org failed" >&2
90
+ exit 1
91
+ fi
92
+ ;;
93
+ *)
94
+ echo "fake npm: unknown FAKE_NPM_MODE '$mode'" >&2
95
+ exit 2
96
+ ;;
97
+ esac
98
+ STUB
99
+ chmod +x "$STUB_DIR/npm"
100
+
101
+ # run_wait <mode> <timeout-seconds> <poll-interval-seconds> <package> <version>
102
+ # Sets RUN_STATUS, RUN_OUTPUT (stdout+stderr) and RUN_COUNT (npm invocations).
103
+ RUN_STATUS=0
104
+ RUN_OUTPUT=''
105
+ RUN_COUNT=0
106
+
107
+ run_wait() {
108
+ local mode="$1" timeout="$2" interval="$3" package="$4" version="$5"
109
+ local count_file="$STUB_DIR/count.$$.$RANDOM"
110
+ rm -f "$count_file"
111
+
112
+ RUN_STATUS=0
113
+ RUN_OUTPUT="$(
114
+ FAKE_NPM_MODE="$mode" \
115
+ FAKE_NPM_VERSION="$version" \
116
+ FAKE_NPM_COUNT_FILE="$count_file" \
117
+ PATH="$STUB_DIR:$PATH" \
118
+ bash "$SCRIPT_UNDER_TEST" "$package" "$version" "$timeout" "$interval" 2>&1
119
+ )" || RUN_STATUS=$?
120
+
121
+ RUN_COUNT=0
122
+ if [ -f "$count_file" ]; then RUN_COUNT="$(cat "$count_file")"; fi
123
+ }
124
+
125
+ # ---- pre-flight -------------------------------------------------------
126
+
127
+ cyan "============================================"
128
+ cyan " wait-for-npm-version.sh Test Suite"
129
+ cyan "============================================"
130
+ echo ""
131
+
132
+ dim "--- Pre-flight ---"
133
+
134
+ test "file exists" bash -c "[[ -f '$SCRIPT_UNDER_TEST' ]]"
135
+ test "starts with shebang" bash -c "head -c 20 '$SCRIPT_UNDER_TEST' | grep -q '^#!/bin/bash'"
136
+ test "bash syntax check passes" bash -c "bash -n '$SCRIPT_UNDER_TEST' 2>&1"
137
+ test "no non-ASCII bytes" bash -c "! grep -Pn '[^\\x00-\\x7F]' '$SCRIPT_UNDER_TEST' >/dev/null 2>&1"
138
+
139
+ echo ""
140
+
141
+ # ---- behaviour --------------------------------------------------------
142
+
143
+ test_version_visible_immediately() {
144
+ run_wait available 30 1 browser4-cli 1.2.3
145
+ [ "$RUN_STATUS" -eq 0 ] || return 1
146
+ [[ "$RUN_OUTPUT" == *"Verified browser4-cli@1.2.3 on npm"* ]] || return 1
147
+ [ "$RUN_COUNT" -eq 1 ] || return 1
148
+ }
149
+
150
+ test_waits_until_the_version_appears() {
151
+ run_wait "available-after:3" 30 1 browser4-cli 1.2.3
152
+ [ "$RUN_STATUS" -eq 0 ] || return 1
153
+ [[ "$RUN_OUTPUT" == *"Verified browser4-cli@1.2.3 on npm"* ]] || return 1
154
+ [ "$RUN_COUNT" -ge 3 ] || return 1
155
+ }
156
+
157
+ # Discrimination test for the CI failure this script fixes: the registry
158
+ # reports the version only on the 7th call -- beyond the 5 attempts the old
159
+ # inline loop allowed. A 3 s budget reproduces the old give-up, a 30 s budget
160
+ # rides it out.
161
+ test_keeps_polling_past_the_old_five_attempt_window() {
162
+ run_wait "available-after:7" 3 1 browser4-cli 1.2.3
163
+ [ "$RUN_STATUS" -eq 1 ] || return 1
164
+
165
+ run_wait "available-after:7" 30 1 browser4-cli 1.2.3
166
+ [ "$RUN_STATUS" -eq 0 ] || return 1
167
+ [ "$RUN_COUNT" -ge 7 ] || return 1
168
+ [[ "$RUN_OUTPUT" == *"Verified browser4-cli@1.2.3 on npm"* ]] || return 1
169
+ }
170
+
171
+ test_fails_when_the_version_never_appears() {
172
+ run_wait unavailable 3 1 browser4-cli 1.2.3
173
+ [ "$RUN_STATUS" -eq 1 ] || return 1
174
+ [[ "$RUN_OUTPUT" == *"unable to verify browser4-cli@1.2.3 on npm within 3s"* ]] || return 1
175
+ [[ "$RUN_OUTPUT" == *"E404"* ]] || return 1
176
+ [[ "$RUN_OUTPUT" == *"npm view browser4-cli@1.2.3 version"* ]] || return 1
177
+ }
178
+
179
+ test_tolerates_a_transient_registry_error() {
180
+ run_wait network-error-once 30 1 browser4-cli 1.2.3
181
+ [ "$RUN_STATUS" -eq 0 ] || return 1
182
+ [[ "$RUN_OUTPUT" == *"WARN: npm view failed"* ]] || return 1
183
+ }
184
+
185
+ test_reports_a_mismatched_version_and_keeps_waiting() {
186
+ run_wait wrong-version 3 1 browser4-cli 1.2.3
187
+ [ "$RUN_STATUS" -eq 1 ] || return 1
188
+ [[ "$RUN_OUTPUT" == *"npm reports '0.0.1'"* ]] || return 1
189
+ }
190
+
191
+ test_requires_both_arguments() {
192
+ local output status
193
+ output="$(PATH="$STUB_DIR:$PATH" bash "$SCRIPT_UNDER_TEST" 2>&1)" && status=0 || status=$?
194
+ [ "$status" -ne 0 ] || return 1
195
+ [[ "$output" == *"Usage:"* ]] || return 1
196
+ }
197
+
198
+ dim "--- Behaviour ---"
199
+
200
+ test "version already visible: verifies on the first poll" test_version_visible_immediately
201
+ test "waits until the registry reports the version" test_waits_until_the_version_appears
202
+ test "keeps polling past the old 5-attempt window" test_keeps_polling_past_the_old_five_attempt_window
203
+ test "fails when the version never appears" test_fails_when_the_version_never_appears
204
+ test "tolerates a transient registry error" test_tolerates_a_transient_registry_error
205
+ test "reports a mismatched version and keeps waiting" test_reports_a_mismatched_version_and_keeps_waiting
206
+ test "requires both arguments" test_requires_both_arguments
207
+
208
+ # ---- summary ----------------------------------------------------------
209
+
210
+ echo ""
211
+ cyan "============================================"
212
+ if [ "$FAIL" -eq 0 ]; then
213
+ green " All $PASS tests passed"
214
+ else
215
+ red " $FAIL of $((PASS + FAIL)) tests failed"
216
+ fi
217
+ cyan "============================================"
218
+
219
+ [ "$FAIL" -eq 0 ]
@@ -0,0 +1,89 @@
1
+ #!/bin/bash
2
+ # ---------------------------------------------------------------------------
3
+ # wait-for-npm-version.sh -- wait until a published version is visible on npm
4
+ # ---------------------------------------------------------------------------
5
+ # Usage:
6
+ # wait-for-npm-version.sh <package> <version> [timeout-seconds] [poll-interval-seconds]
7
+ #
8
+ # Defaults: timeout 600 s, poll interval 15 s.
9
+ #
10
+ # `npm publish` exiting 0 only means the registry ACCEPTED the upload. npm now
11
+ # processes publishes asynchronously and answers with
12
+ # "Your package is being processed and may take a few minutes to become available."
13
+ # after which the new version stays invisible to `npm view` for an unpredictable
14
+ # while (observed on v4.13.18: still missing 41 s after a successful publish,
15
+ # i.e. past the whole 5 x 10 s window this script replaces). Verification must
16
+ # therefore wait minutes, not seconds -- otherwise a successful publish is
17
+ # reported as a failed release job, and everything gated on that job (the
18
+ # GitHub release) is skipped.
19
+ #
20
+ # Every answer that is not the expected version is retried:
21
+ # - 404 / "No match found for version" -> not published yet (expected, quiet)
22
+ # - any other error -> transient registry/network problem
23
+ # (logged, still retried)
24
+ #
25
+ # Exit codes:
26
+ # 0 the exact version is visible on the registry
27
+ # 1 still not visible when the timeout expires, or bad usage
28
+ #
29
+ # Designed for CI (GitHub Actions) but also runs on developer machines.
30
+ # ---------------------------------------------------------------------------
31
+ set -euo pipefail
32
+
33
+ PACKAGE="${1:?Usage: $0 <package> <version> [timeout-seconds] [poll-interval-seconds]}"
34
+ VERSION="${2:?Usage: $0 <package> <version> [timeout-seconds] [poll-interval-seconds]}"
35
+ TIMEOUT_SECONDS="${3:-600}"
36
+ POLL_INTERVAL_SECONDS="${4:-15}"
37
+
38
+ STDERR_FILE="$(mktemp)"
39
+ trap 'rm -f "$STDERR_FILE"' EXIT
40
+
41
+ started=$SECONDS
42
+ attempt=0
43
+ published=''
44
+ npm_error=''
45
+
46
+ while :; do
47
+ attempt=$((attempt + 1))
48
+
49
+ set +e
50
+ published="$(npm view "$PACKAGE@$VERSION" version 2>"$STDERR_FILE")"
51
+ status=$?
52
+ set -e
53
+
54
+ published="$(printf '%s' "$published" | tr -d '[:space:]')"
55
+ npm_error="$(head -n 1 "$STDERR_FILE" 2>/dev/null || true)"
56
+ elapsed=$((SECONDS - started))
57
+
58
+ if [ "$status" -eq 0 ] && [ "$published" = "$VERSION" ]; then
59
+ echo "Verified $PACKAGE@$VERSION on npm after ${elapsed}s (attempt $attempt)"
60
+ exit 0
61
+ fi
62
+
63
+ remaining=$((TIMEOUT_SECONDS - elapsed))
64
+ if [ "$remaining" -le 0 ]; then
65
+ break
66
+ fi
67
+
68
+ if [ "$status" -eq 0 ]; then
69
+ echo "WARN: npm reports '${published:-<empty>}' for $PACKAGE@$VERSION (attempt $attempt, ${elapsed}s elapsed, ${remaining}s left)"
70
+ elif printf '%s' "$npm_error" | grep -qE 'E404|404'; then
71
+ echo "Waiting for $PACKAGE@$VERSION to appear on npm (attempt $attempt, ${elapsed}s elapsed, ${remaining}s left) -- the registry may still be processing the publish"
72
+ else
73
+ echo "WARN: npm view failed (attempt $attempt, ${elapsed}s elapsed, ${remaining}s left): ${npm_error:-<no output>}"
74
+ fi
75
+
76
+ # never sleep past the deadline: the total wait stays within the budget
77
+ if [ "$POLL_INTERVAL_SECONDS" -gt "$remaining" ]; then
78
+ sleep "$remaining"
79
+ else
80
+ sleep "$POLL_INTERVAL_SECONDS"
81
+ fi
82
+ done
83
+
84
+ echo "ERROR: unable to verify $PACKAGE@$VERSION on npm within ${TIMEOUT_SECONDS}s after publish" >&2
85
+ echo "ERROR: last registry answer: ${npm_error:-<no output>}" >&2
86
+ echo "ERROR: the version may still be processing on the registry side; re-check with:" >&2
87
+ echo "ERROR: npm view $PACKAGE@$VERSION version" >&2
88
+ echo "ERROR: https://www.npmjs.com/package/$PACKAGE/v/$VERSION" >&2
89
+ exit 1