browser4-cli 4.14.0-rc.4 → 4.14.0-rc.6
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.
- package/README.md +10 -10
- package/bin/browser4-cli-darwin-arm64 +0 -0
- package/bin/browser4-cli-darwin-x64 +0 -0
- package/bin/browser4-cli-linux-arm64 +0 -0
- package/bin/browser4-cli-linux-musl-arm64 +0 -0
- package/bin/browser4-cli-linux-musl-x64 +0 -0
- package/bin/browser4-cli-linux-x64 +0 -0
- package/bin/browser4-cli-win32-x64.exe +0 -0
- package/package.json +1 -1
- package/scripts/README.md +20 -1
- package/scripts/install-browser4-cli.ps1 +174 -46
- package/scripts/install-browser4-cli.sh +98 -28
- package/scripts/smoke-test-runtime-bundle.sh +18 -3
- package/scripts/tests/install-browser4-cli.tests.ps1 +150 -15
- package/scripts/tests/install-browser4-cli.tests.sh +157 -10
- package/scripts/tests/wait-for-npm-version.tests.sh +219 -0
- package/scripts/wait-for-npm-version.sh +89 -0
|
@@ -27,8 +27,8 @@ dim() { echo -e "\033[2m$*\033[0m"; }
|
|
|
27
27
|
|
|
28
28
|
test() {
|
|
29
29
|
local name="$1"
|
|
30
|
-
|
|
31
|
-
if "
|
|
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 -
|
|
133
|
+
bash '$INSTALL_SCRIPT' --dry-run 2>&1 | grep -q 'DRY-RUN'
|
|
114
134
|
"
|
|
115
135
|
|
|
116
|
-
test "--silent
|
|
117
|
-
bash '$INSTALL_SCRIPT' --silent --dry-run 2>&1
|
|
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 -
|
|
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 -
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|