browser4-cli 4.14.0-rc.5 → 4.14.0-rc.7
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 +38 -21
- 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 +86 -1
- package/scripts/install-browser4-cli.ps1 +174 -46
- package/scripts/install-browser4-cli.sh +98 -28
- package/scripts/reconcile-release-assets.sh +293 -0
- 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/reconcile-release-assets.tests.sh +421 -0
- package/scripts/tests/wait-for-npm-version.tests.sh +219 -0
- package/scripts/tests/wait-for-oss-sync.tests.sh +382 -0
- package/scripts/wait-for-npm-version.sh +89 -0
- package/scripts/wait-for-oss-sync.sh +314 -0
|
@@ -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,382 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# wait-for-oss-sync.tests.sh
|
|
3
|
+
# Test suite for wait-for-oss-sync.sh
|
|
4
|
+
# Uses only bash builtins and standard Unix tools -- no external test
|
|
5
|
+
# frameworks and no network: `gh` is stubbed on PATH.
|
|
6
|
+
#
|
|
7
|
+
# Run:
|
|
8
|
+
# bash cli/scripts/tests/wait-for-oss-sync.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-oss-sync.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 gh ----------------------------------------------------------
|
|
40
|
+
#
|
|
41
|
+
# The stub answers from files under $FAKE_GH_DIR, so every test reads as a
|
|
42
|
+
# description of what GitHub was reporting:
|
|
43
|
+
#
|
|
44
|
+
# baseline.tsv `gh run list --limit 50` (the pre-dispatch listing)
|
|
45
|
+
# baseline-fail if present, that listing fails instead
|
|
46
|
+
# discover-<n>.tsv n-th `gh run list --limit 20` (falls back to
|
|
47
|
+
# discover-last.tsv ...discover-last.tsv, and to empty output)
|
|
48
|
+
# view-<n>.tsv n-th `gh run view` (falls back to
|
|
49
|
+
# view-last.tsv ...view-last.tsv, and to empty output)
|
|
50
|
+
# dispatch-exit exit status for `gh workflow run`
|
|
51
|
+
#
|
|
52
|
+
# Each file holds TSV exactly as the script's own -q filters produce it:
|
|
53
|
+
# run list : <databaseId>\t<createdAt>
|
|
54
|
+
# run view : <status>\t<conclusion>\t<in-progress step>
|
|
55
|
+
# The script's own -q filters turn "no value" into "--" rather than leaving the
|
|
56
|
+
# field empty (tab is IFS whitespace, so `read` would collapse the run of tabs
|
|
57
|
+
# and an empty conclusion would swallow the step name).
|
|
58
|
+
# An empty response stands for "gh could not answer", which is what the
|
|
59
|
+
# script's `2>/dev/null` turns a failed call into.
|
|
60
|
+
|
|
61
|
+
STUB_DIR="$(mktemp -d)"
|
|
62
|
+
cleanup() { rm -rf "$STUB_DIR"; }
|
|
63
|
+
trap cleanup EXIT
|
|
64
|
+
|
|
65
|
+
cat > "$STUB_DIR/gh" <<'STUB'
|
|
66
|
+
#!/usr/bin/env bash
|
|
67
|
+
set -euo pipefail
|
|
68
|
+
|
|
69
|
+
dir="${FAKE_GH_DIR:?FAKE_GH_DIR is required}"
|
|
70
|
+
echo "$*" >> "$dir/calls.log"
|
|
71
|
+
|
|
72
|
+
counter_next() {
|
|
73
|
+
local file="$dir/$1.count" n=0
|
|
74
|
+
if [ -f "$file" ]; then n="$(cat "$file")"; fi
|
|
75
|
+
n=$((n + 1))
|
|
76
|
+
printf '%s' "$n" > "$file"
|
|
77
|
+
printf '%s' "$n"
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
emit() {
|
|
81
|
+
# $1 = base name, $2 = sequence number: numbered response, else the fallback
|
|
82
|
+
local numbered="$dir/$1-$2.tsv" fallback="$dir/$1-last.tsv"
|
|
83
|
+
if [ -f "$numbered" ]; then
|
|
84
|
+
cat "$numbered"
|
|
85
|
+
elif [ -f "$fallback" ]; then
|
|
86
|
+
cat "$fallback"
|
|
87
|
+
fi
|
|
88
|
+
return 0
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
limit=''
|
|
92
|
+
prev=''
|
|
93
|
+
for arg in "$@"; do
|
|
94
|
+
if [ "$prev" = '--limit' ]; then limit="$arg"; fi
|
|
95
|
+
prev="$arg"
|
|
96
|
+
done
|
|
97
|
+
|
|
98
|
+
case "${1:-} ${2:-}" in
|
|
99
|
+
'run list')
|
|
100
|
+
if [ "$limit" = '50' ]; then
|
|
101
|
+
if [ -f "$dir/baseline-fail" ]; then exit 1; fi
|
|
102
|
+
if [ -f "$dir/baseline.tsv" ]; then cat "$dir/baseline.tsv"; fi
|
|
103
|
+
else
|
|
104
|
+
emit discover "$(counter_next list)"
|
|
105
|
+
fi
|
|
106
|
+
;;
|
|
107
|
+
'run view')
|
|
108
|
+
emit view "$(counter_next view)"
|
|
109
|
+
;;
|
|
110
|
+
'workflow run')
|
|
111
|
+
if [ -f "$dir/dispatch-exit" ]; then exit "$(cat "$dir/dispatch-exit")"; fi
|
|
112
|
+
;;
|
|
113
|
+
*)
|
|
114
|
+
echo "fake gh: unhandled invocation: $*" >&2
|
|
115
|
+
exit 2
|
|
116
|
+
;;
|
|
117
|
+
esac
|
|
118
|
+
STUB
|
|
119
|
+
chmod +x "$STUB_DIR/gh"
|
|
120
|
+
|
|
121
|
+
# ---- harness ----------------------------------------------------------
|
|
122
|
+
|
|
123
|
+
RUN_STATUS=0
|
|
124
|
+
RUN_OUTPUT=''
|
|
125
|
+
|
|
126
|
+
# reset_gh -- empty the stub's response directory (and the call log)
|
|
127
|
+
reset_gh() {
|
|
128
|
+
rm -rf "$STUB_DIR/gh-state"
|
|
129
|
+
mkdir -p "$STUB_DIR/gh-state"
|
|
130
|
+
export FAKE_GH_DIR="$STUB_DIR/gh-state"
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
# gh_respond <file> [<content>] -- write one stub response file
|
|
134
|
+
gh_respond() {
|
|
135
|
+
local file="$1"
|
|
136
|
+
shift
|
|
137
|
+
if [ $# -gt 0 ]; then
|
|
138
|
+
printf '%s\n' "$*" > "$FAKE_GH_DIR/$file"
|
|
139
|
+
else
|
|
140
|
+
: > "$FAKE_GH_DIR/$file"
|
|
141
|
+
fi
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
# run_wait [<extra args>...] -- run the script on a 4 s / 1 s budget, so a
|
|
145
|
+
# "never finishes" case costs ~4 s instead of the 2700 s default.
|
|
146
|
+
run_wait() {
|
|
147
|
+
RUN_STATUS=0
|
|
148
|
+
RUN_OUTPUT="$(
|
|
149
|
+
FAKE_GH_DIR="$FAKE_GH_DIR" \
|
|
150
|
+
PATH="$STUB_DIR:$PATH" \
|
|
151
|
+
bash "$SCRIPT_UNDER_TEST" \
|
|
152
|
+
--tag v9.9.9 --repo acme/browser4 \
|
|
153
|
+
--timeout 4 --interval 1 --discovery-timeout 2 "$@" 2>&1
|
|
154
|
+
)" || RUN_STATUS=$?
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
# ---- pre-flight -------------------------------------------------------
|
|
158
|
+
|
|
159
|
+
cyan "============================================"
|
|
160
|
+
cyan " wait-for-oss-sync.sh Test Suite"
|
|
161
|
+
cyan "============================================"
|
|
162
|
+
echo ""
|
|
163
|
+
|
|
164
|
+
dim "--- Pre-flight ---"
|
|
165
|
+
|
|
166
|
+
test "file exists" bash -c "[[ -f '$SCRIPT_UNDER_TEST' ]]"
|
|
167
|
+
test "starts with shebang" bash -c "head -c 20 '$SCRIPT_UNDER_TEST' | grep -q '^#!/bin/bash'"
|
|
168
|
+
test "bash syntax check passes" bash -c "bash -n '$SCRIPT_UNDER_TEST' 2>&1"
|
|
169
|
+
test "no non-ASCII bytes" bash -c "! grep -Pn '[^\\x00-\\x7F]' '$SCRIPT_UNDER_TEST' >/dev/null 2>&1"
|
|
170
|
+
test "--help exits 0 and prints usage" bash -c "bash '$SCRIPT_UNDER_TEST' --help 2>&1 | grep -q 'Usage:'"
|
|
171
|
+
|
|
172
|
+
echo ""
|
|
173
|
+
|
|
174
|
+
# ---- behaviour --------------------------------------------------------
|
|
175
|
+
|
|
176
|
+
# The happy path, and the shape of the success message the workflows rely on.
|
|
177
|
+
test_run_that_completes_successfully() {
|
|
178
|
+
reset_gh
|
|
179
|
+
gh_respond baseline.tsv "$(printf '111\t2026-09-20T15:20:51Z')"
|
|
180
|
+
gh_respond discover-last.tsv "$(printf '222\t2026-09-23T13:05:09Z')"
|
|
181
|
+
gh_respond view-1.tsv "$(printf 'in_progress\t--\tUpload to OSS')"
|
|
182
|
+
gh_respond view-last.tsv "$(printf 'completed\tsuccess\t--')"
|
|
183
|
+
|
|
184
|
+
run_wait
|
|
185
|
+
[ "$RUN_STATUS" -eq 0 ] || return 1
|
|
186
|
+
[[ "$RUN_OUTPUT" == *"Watching sync-to-oss.yml run 222"* ]] || return 1
|
|
187
|
+
[[ "$RUN_OUTPUT" == *"completed successfully"* ]] || return 1
|
|
188
|
+
[[ "$RUN_OUTPUT" == *"actions/runs/222"* ]] || return 1
|
|
189
|
+
grep -q 'workflow run sync-to-oss.yml' "$FAKE_GH_DIR/calls.log" || return 1
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
# The v4.13.17/v4.13.21 defect: the freshly dispatched run is not indexed yet,
|
|
193
|
+
# so `gh run list` answers with the PREVIOUS release's run -- which completed
|
|
194
|
+
# successfully. Adopting it (the old inline loop did: first non-empty answer
|
|
195
|
+
# won) reports the CDN as updated without waiting for anything. Ours must win
|
|
196
|
+
# even though the stale run is the more attractive answer.
|
|
197
|
+
test_never_adopts_the_previous_runs_success() {
|
|
198
|
+
reset_gh
|
|
199
|
+
gh_respond baseline.tsv "$(printf '111\t2026-09-20T15:20:51Z')"
|
|
200
|
+
# both runs visible: 111 (before we dispatched, already green) and 222 (ours)
|
|
201
|
+
gh_respond discover-last.tsv "$(printf '222\t2026-09-23T13:05:09Z\n111\t2026-09-20T15:20:51Z')"
|
|
202
|
+
gh_respond view-last.tsv "$(printf 'completed\tfailure\t--')"
|
|
203
|
+
|
|
204
|
+
run_wait
|
|
205
|
+
[ "$RUN_STATUS" -eq 1 ] || return 1
|
|
206
|
+
# ours is named as the verdict ...
|
|
207
|
+
[[ "$RUN_OUTPUT" == *"run 222 concluded 'failure'"* ]] || return 1
|
|
208
|
+
# ... and the stale run's success was never claimed
|
|
209
|
+
[[ "$RUN_OUTPUT" != *"completed successfully"* ]] || return 1
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
# The same guard while our run is still queued behind the stale one.
|
|
213
|
+
test_waits_for_our_run_when_only_stale_runs_are_listed() {
|
|
214
|
+
reset_gh
|
|
215
|
+
gh_respond baseline.tsv "$(printf '111\t2026-09-20T15:20:51Z')"
|
|
216
|
+
gh_respond discover-1.tsv "$(printf '111\t2026-09-20T15:20:51Z')"
|
|
217
|
+
gh_respond discover-2.tsv "$(printf '111\t2026-09-20T15:20:51Z')"
|
|
218
|
+
gh_respond discover-last.tsv "$(printf '222\t2026-09-23T13:05:09Z\n111\t2026-09-20T15:20:51Z')"
|
|
219
|
+
gh_respond view-last.tsv "$(printf 'completed\tsuccess\t--')"
|
|
220
|
+
|
|
221
|
+
# a generous discovery budget: the point is that it keeps looking, not
|
|
222
|
+
# that it finds the run on any particular poll
|
|
223
|
+
run_wait --discovery-timeout 20
|
|
224
|
+
[ "$RUN_STATUS" -eq 0 ] || return 1
|
|
225
|
+
[[ "$RUN_OUTPUT" == *"Watching sync-to-oss.yml run 222"* ]] || return 1
|
|
226
|
+
[[ "$RUN_OUTPUT" == *"Waiting for the sync-to-oss.yml run to appear"* ]] || return 1
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
# A dispatch that never becomes a run must be reported, not waited out.
|
|
230
|
+
test_fails_when_no_new_run_appears() {
|
|
231
|
+
reset_gh
|
|
232
|
+
gh_respond baseline.tsv "$(printf '111\t2026-09-20T15:20:51Z')"
|
|
233
|
+
gh_respond discover-last.tsv "$(printf '111\t2026-09-20T15:20:51Z')"
|
|
234
|
+
|
|
235
|
+
run_wait
|
|
236
|
+
[ "$RUN_STATUS" -eq 1 ] || return 1
|
|
237
|
+
[[ "$RUN_OUTPUT" == *"no new run appeared within 2s"* ]] || return 1
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
# The v4.13.21 failure itself: the sync stalls, the budget expires. The
|
|
241
|
+
# message has to name the run, the step it is stuck on and how to re-check it
|
|
242
|
+
# -- the old step timeout reported none of that.
|
|
243
|
+
test_reports_the_stuck_run_when_the_budget_expires() {
|
|
244
|
+
reset_gh
|
|
245
|
+
gh_respond baseline.tsv "$(printf '111\t2026-09-20T15:20:51Z')"
|
|
246
|
+
gh_respond discover-last.tsv "$(printf '222\t2026-09-23T13:05:09Z')"
|
|
247
|
+
gh_respond view-last.tsv "$(printf 'in_progress\t--\tUpload to OSS')"
|
|
248
|
+
|
|
249
|
+
run_wait
|
|
250
|
+
[ "$RUN_STATUS" -eq 1 ] || return 1
|
|
251
|
+
[[ "$RUN_OUTPUT" == *"was still in_progress (step: Upload to OSS) after 4s"* ]] || return 1
|
|
252
|
+
[[ "$RUN_OUTPUT" == *"actions/runs/222"* ]] || return 1
|
|
253
|
+
[[ "$RUN_OUTPUT" == *"gh run view 222 --repo acme/browser4"* ]] || return 1
|
|
254
|
+
[[ "$RUN_OUTPUT" == *"gh workflow run sync-to-oss.yml --repo acme/browser4 -f tag_name=v9.9.9"* ]] || return 1
|
|
255
|
+
# the progress line names the stuck step too, so the log shows where it sat
|
|
256
|
+
[[ "$RUN_OUTPUT" == *"status=in_progress, step=Upload to OSS"* ]] || return 1
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
test_reports_a_failed_run() {
|
|
260
|
+
reset_gh
|
|
261
|
+
gh_respond baseline.tsv ''
|
|
262
|
+
gh_respond discover-last.tsv "$(printf '222\t2026-09-23T13:05:09Z')"
|
|
263
|
+
gh_respond view-last.tsv "$(printf 'completed\tfailure\t--')"
|
|
264
|
+
|
|
265
|
+
run_wait
|
|
266
|
+
[ "$RUN_STATUS" -eq 1 ] || return 1
|
|
267
|
+
[[ "$RUN_OUTPUT" == *"concluded 'failure'"* ]] || return 1
|
|
268
|
+
[[ "$RUN_OUTPUT" == *"actions/runs/222"* ]] || return 1
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
test_reports_a_cancelled_run_as_a_failure() {
|
|
272
|
+
reset_gh
|
|
273
|
+
gh_respond baseline.tsv ''
|
|
274
|
+
gh_respond discover-last.tsv "$(printf '222\t2026-09-23T13:05:09Z')"
|
|
275
|
+
gh_respond view-last.tsv "$(printf 'completed\tcancelled\t--')"
|
|
276
|
+
|
|
277
|
+
run_wait
|
|
278
|
+
[ "$RUN_STATUS" -eq 1 ] || return 1
|
|
279
|
+
[[ "$RUN_OUTPUT" == *"concluded 'cancelled'"* ]] || return 1
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
# An unreadable poll (rate limit, network blip) must not abort the wait: the
|
|
283
|
+
# stub answers nothing for the first view call, then reports our run done.
|
|
284
|
+
test_survives_an_unreadable_poll() {
|
|
285
|
+
reset_gh
|
|
286
|
+
gh_respond baseline.tsv ''
|
|
287
|
+
gh_respond discover-last.tsv "$(printf '222\t2026-09-23T13:05:09Z')"
|
|
288
|
+
gh_respond view-1.tsv ''
|
|
289
|
+
gh_respond view-last.tsv "$(printf 'completed\tsuccess\t--')"
|
|
290
|
+
|
|
291
|
+
run_wait
|
|
292
|
+
[ "$RUN_STATUS" -eq 0 ] || return 1
|
|
293
|
+
[[ "$RUN_OUTPUT" == *"completed successfully"* ]] || return 1
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
test_fails_when_the_dispatch_itself_fails() {
|
|
297
|
+
reset_gh
|
|
298
|
+
gh_respond baseline.tsv ''
|
|
299
|
+
printf '1' > "$FAKE_GH_DIR/dispatch-exit"
|
|
300
|
+
|
|
301
|
+
run_wait
|
|
302
|
+
[ "$RUN_STATUS" -eq 1 ] || return 1
|
|
303
|
+
[[ "$RUN_OUTPUT" == *"Could not dispatch sync-to-oss.yml"* ]] || return 1
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
# With the baseline listing unavailable the run is identified by creation time
|
|
307
|
+
# alone -- a run created after the dispatch is still accepted ...
|
|
308
|
+
test_falls_back_to_creation_time_when_the_baseline_listing_fails() {
|
|
309
|
+
reset_gh
|
|
310
|
+
: > "$FAKE_GH_DIR/baseline-fail"
|
|
311
|
+
gh_respond discover-last.tsv "$(printf '222\t2099-01-01T00:00:00Z')"
|
|
312
|
+
gh_respond view-last.tsv "$(printf 'completed\tsuccess\t--')"
|
|
313
|
+
|
|
314
|
+
run_wait
|
|
315
|
+
[ "$RUN_STATUS" -eq 0 ] || return 1
|
|
316
|
+
[[ "$RUN_OUTPUT" == *"could not list existing sync-to-oss.yml runs"* ]] || return 1
|
|
317
|
+
[[ "$RUN_OUTPUT" == *"Watching sync-to-oss.yml run 222"* ]] || return 1
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
# ... and a run from before the dispatch is not, even with no id baseline.
|
|
321
|
+
test_creation_time_fallback_still_refuses_a_stale_run() {
|
|
322
|
+
reset_gh
|
|
323
|
+
: > "$FAKE_GH_DIR/baseline-fail"
|
|
324
|
+
gh_respond discover-last.tsv "$(printf '111\t2020-01-01T00:00:00Z')"
|
|
325
|
+
gh_respond view-last.tsv "$(printf 'completed\tsuccess\t--')"
|
|
326
|
+
|
|
327
|
+
run_wait
|
|
328
|
+
[ "$RUN_STATUS" -eq 1 ] || return 1
|
|
329
|
+
[[ "$RUN_OUTPUT" == *"no new run appeared"* ]] || return 1
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
test_requires_the_tag_argument() {
|
|
333
|
+
local output status
|
|
334
|
+
output="$(PATH="$STUB_DIR:$PATH" bash "$SCRIPT_UNDER_TEST" --repo acme/browser4 2>&1)" && status=0 || status=$?
|
|
335
|
+
[ "$status" -ne 0 ] || return 1
|
|
336
|
+
[[ "$output" == *"Usage:"* ]] || return 1
|
|
337
|
+
[[ "$output" == *"--tag is required"* ]] || return 1
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
test_rejects_an_unknown_argument() {
|
|
341
|
+
local output status
|
|
342
|
+
output="$(PATH="$STUB_DIR:$PATH" bash "$SCRIPT_UNDER_TEST" --tag v1 --nonsense 2>&1)" && status=0 || status=$?
|
|
343
|
+
[ "$status" -ne 0 ] || return 1
|
|
344
|
+
[[ "$output" == *"unknown argument '--nonsense'"* ]] || return 1
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
test_rejects_a_non_numeric_timeout() {
|
|
348
|
+
local output status
|
|
349
|
+
output="$(PATH="$STUB_DIR:$PATH" bash "$SCRIPT_UNDER_TEST" --tag v1 --timeout soon 2>&1)" && status=0 || status=$?
|
|
350
|
+
[ "$status" -ne 0 ] || return 1
|
|
351
|
+
[[ "$output" == *"--timeout must be a whole number"* ]] || return 1
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
dim "--- Behaviour ---"
|
|
355
|
+
|
|
356
|
+
test "run completes successfully" test_run_that_completes_successfully
|
|
357
|
+
test "never adopts the previous run's success" test_never_adopts_the_previous_runs_success
|
|
358
|
+
test "waits for our run when only stale runs are listed" test_waits_for_our_run_when_only_stale_runs_are_listed
|
|
359
|
+
test "fails when no new run appears" test_fails_when_no_new_run_appears
|
|
360
|
+
test "reports the stuck run when the budget expires" test_reports_the_stuck_run_when_the_budget_expires
|
|
361
|
+
test "reports a failed run" test_reports_a_failed_run
|
|
362
|
+
test "reports a cancelled run as a failure" test_reports_a_cancelled_run_as_a_failure
|
|
363
|
+
test "survives an unreadable poll" test_survives_an_unreadable_poll
|
|
364
|
+
test "fails when the dispatch itself fails" test_fails_when_the_dispatch_itself_fails
|
|
365
|
+
test "falls back to creation time when the baseline listing fails" test_falls_back_to_creation_time_when_the_baseline_listing_fails
|
|
366
|
+
test "creation-time fallback still refuses a stale run" test_creation_time_fallback_still_refuses_a_stale_run
|
|
367
|
+
test "requires the tag argument" test_requires_the_tag_argument
|
|
368
|
+
test "rejects an unknown argument" test_rejects_an_unknown_argument
|
|
369
|
+
test "rejects a non-numeric timeout" test_rejects_a_non_numeric_timeout
|
|
370
|
+
|
|
371
|
+
# ---- summary ----------------------------------------------------------
|
|
372
|
+
|
|
373
|
+
echo ""
|
|
374
|
+
cyan "============================================"
|
|
375
|
+
if [ "$FAIL" -eq 0 ]; then
|
|
376
|
+
green " All $PASS tests passed"
|
|
377
|
+
else
|
|
378
|
+
red " $FAIL of $((PASS + FAIL)) tests failed"
|
|
379
|
+
fi
|
|
380
|
+
cyan "============================================"
|
|
381
|
+
|
|
382
|
+
[ "$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
|