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
|
@@ -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,421 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# reconcile-release-assets.tests.sh
|
|
3
|
+
# Test suite for reconcile-release-assets.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/reconcile-release-assets.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)/reconcile-release-assets.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
|
+
# ---- fixture: three release assets ------------------------------------
|
|
40
|
+
|
|
41
|
+
STUB_DIR="$(mktemp -d)"
|
|
42
|
+
cleanup() { rm -rf "$STUB_DIR"; }
|
|
43
|
+
trap cleanup EXIT
|
|
44
|
+
|
|
45
|
+
ASSET_DIR="$STUB_DIR/assets"
|
|
46
|
+
mkdir -p "$ASSET_DIR"
|
|
47
|
+
|
|
48
|
+
# make_asset <name> <bytes> -- a real file of an exact size, so the script's
|
|
49
|
+
# size comparison runs against real byte counts.
|
|
50
|
+
make_asset() {
|
|
51
|
+
local path="$ASSET_DIR/$1" bytes="$2" i=0
|
|
52
|
+
rm -f "$path"
|
|
53
|
+
: > "$path"
|
|
54
|
+
while [ "$i" -lt "$bytes" ]; do
|
|
55
|
+
printf 'x' >> "$path"
|
|
56
|
+
i=$((i + 1))
|
|
57
|
+
done
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
JAR="Browser4.jar"
|
|
61
|
+
CLI_BIN="browser4-cli-linux-x64"
|
|
62
|
+
BUNDLE="browser4-bundle-runtime-linux-x64.tar.gz"
|
|
63
|
+
JAR_BYTES=101
|
|
64
|
+
CLI_BIN_BYTES=202
|
|
65
|
+
BUNDLE_BYTES=303
|
|
66
|
+
make_asset "$JAR" "$JAR_BYTES"
|
|
67
|
+
make_asset "$CLI_BIN" "$CLI_BIN_BYTES"
|
|
68
|
+
make_asset "$BUNDLE" "$BUNDLE_BYTES"
|
|
69
|
+
|
|
70
|
+
ASSET_PATHS=("$ASSET_DIR/$JAR" "$ASSET_DIR/$CLI_BIN" "$ASSET_DIR/$BUNDLE")
|
|
71
|
+
|
|
72
|
+
# ---- stub gh ----------------------------------------------------------
|
|
73
|
+
|
|
74
|
+
cat > "$STUB_DIR/gh" <<'STUB'
|
|
75
|
+
#!/usr/bin/env bash
|
|
76
|
+
# Fake `gh release view` / `gh release upload`, driven by the test environment:
|
|
77
|
+
#
|
|
78
|
+
# FAKE_GH_STATE directory holding one file per asset the release "has",
|
|
79
|
+
# whose content is the size GitHub reports for it
|
|
80
|
+
# FAKE_GH_COUNTS directory holding the per-command invocation counters
|
|
81
|
+
# FAKE_GH_LOG append-only command log the assertions read
|
|
82
|
+
# FAKE_GH_MODE ok | view-fails-once | upload-fails-once |
|
|
83
|
+
# upload-never-succeeds | release-missing
|
|
84
|
+
#
|
|
85
|
+
# upload-fails-once and upload-never-succeeds answer with the message the
|
|
86
|
+
# v4.14.0-rc.6 release run died on.
|
|
87
|
+
set -euo pipefail
|
|
88
|
+
|
|
89
|
+
state="${FAKE_GH_STATE:?FAKE_GH_STATE is required}"
|
|
90
|
+
counts="${FAKE_GH_COUNTS:?FAKE_GH_COUNTS is required}"
|
|
91
|
+
log="${FAKE_GH_LOG:?FAKE_GH_LOG is required}"
|
|
92
|
+
mode="${FAKE_GH_MODE:?FAKE_GH_MODE is required}"
|
|
93
|
+
|
|
94
|
+
bump() {
|
|
95
|
+
local file="$counts/$1" count=0
|
|
96
|
+
if [ -f "$file" ]; then count="$(cat "$file")"; fi
|
|
97
|
+
count=$((count + 1))
|
|
98
|
+
printf '%s' "$count" > "$file"
|
|
99
|
+
printf '%s' "$count"
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
command_name="${1:-} ${2:-}"
|
|
103
|
+
|
|
104
|
+
case "$command_name" in
|
|
105
|
+
"release view")
|
|
106
|
+
printf 'view\n' >> "$log"
|
|
107
|
+
n="$(bump view)"
|
|
108
|
+
|
|
109
|
+
if [ "$mode" = "release-missing" ]; then
|
|
110
|
+
echo "release not found" >&2
|
|
111
|
+
exit 1
|
|
112
|
+
fi
|
|
113
|
+
if [ "$mode" = "view-fails-once" ] && [ "$n" -le 1 ]; then
|
|
114
|
+
echo "HTTP 502: Bad Gateway (https://api.github.com/repos/o/r/releases/tags/v1.2.3)" >&2
|
|
115
|
+
exit 1
|
|
116
|
+
fi
|
|
117
|
+
|
|
118
|
+
for asset in "$state"/*; do
|
|
119
|
+
[ -e "$asset" ] || continue
|
|
120
|
+
printf '%s %s\n' "$(basename "$asset")" "$(cat "$asset")"
|
|
121
|
+
done
|
|
122
|
+
;;
|
|
123
|
+
|
|
124
|
+
"release upload")
|
|
125
|
+
# Skip the subcommand and the tag; keep the arguments that are real
|
|
126
|
+
# files -- --repo and its value are not.
|
|
127
|
+
shift 2
|
|
128
|
+
shift
|
|
129
|
+
files=()
|
|
130
|
+
for arg in "$@"; do
|
|
131
|
+
[ -f "$arg" ] || continue
|
|
132
|
+
files+=("$arg")
|
|
133
|
+
done
|
|
134
|
+
|
|
135
|
+
names=()
|
|
136
|
+
for f in "${files[@]}"; do names+=("${f##*/}"); done
|
|
137
|
+
printf 'upload %s\n' "${names[*]}" >> "$log"
|
|
138
|
+
|
|
139
|
+
n="$(bump upload)"
|
|
140
|
+
fail_round=0
|
|
141
|
+
case "$mode" in
|
|
142
|
+
upload-fails-once)
|
|
143
|
+
if [ "$n" -le 1 ]; then fail_round=1; fi
|
|
144
|
+
;;
|
|
145
|
+
upload-never-succeeds)
|
|
146
|
+
fail_round=1
|
|
147
|
+
;;
|
|
148
|
+
esac
|
|
149
|
+
if [ "$fail_round" -eq 1 ]; then
|
|
150
|
+
echo "Error creating asset temp dir" >&2
|
|
151
|
+
exit 1
|
|
152
|
+
fi
|
|
153
|
+
|
|
154
|
+
for f in "${files[@]}"; do
|
|
155
|
+
printf '%s' "$(wc -c < "$f" | tr -d '[:space:]')" > "$state/${f##*/}"
|
|
156
|
+
echo "Uploaded ${f##*/}"
|
|
157
|
+
done
|
|
158
|
+
;;
|
|
159
|
+
|
|
160
|
+
*)
|
|
161
|
+
echo "fake gh: unhandled command '${command_name}'" >&2
|
|
162
|
+
exit 2
|
|
163
|
+
;;
|
|
164
|
+
esac
|
|
165
|
+
STUB
|
|
166
|
+
chmod +x "$STUB_DIR/gh"
|
|
167
|
+
|
|
168
|
+
# ---- runner -----------------------------------------------------------
|
|
169
|
+
|
|
170
|
+
RUN_STATUS=0
|
|
171
|
+
RUN_OUTPUT=''
|
|
172
|
+
RUN_STATE=''
|
|
173
|
+
RUN_COUNTS=''
|
|
174
|
+
RUN_LOG=''
|
|
175
|
+
|
|
176
|
+
# new_run -- fresh release state, command log and counters for one scenario
|
|
177
|
+
new_run() {
|
|
178
|
+
RUN_STATE="$STUB_DIR/state.$$.$RANDOM"
|
|
179
|
+
RUN_COUNTS="$STUB_DIR/counts.$$.$RANDOM"
|
|
180
|
+
RUN_LOG="$STUB_DIR/log.$$.$RANDOM"
|
|
181
|
+
rm -rf "$RUN_STATE" "$RUN_COUNTS" "$RUN_LOG"
|
|
182
|
+
mkdir -p "$RUN_STATE" "$RUN_COUNTS"
|
|
183
|
+
: > "$RUN_LOG"
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
# seed_asset <name> [<bytes>] -- an asset the release already carries, at the
|
|
187
|
+
# size GitHub reports for it (defaults to the local size, i.e. a good upload)
|
|
188
|
+
seed_asset() {
|
|
189
|
+
local name="$1" bytes="${2:-}"
|
|
190
|
+
if [ -z "$bytes" ]; then
|
|
191
|
+
bytes="$(wc -c < "$ASSET_DIR/$name" | tr -d '[:space:]')"
|
|
192
|
+
fi
|
|
193
|
+
printf '%s' "$bytes" > "$RUN_STATE/$name"
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
# run_script <mode> <timeout> <interval> <settle> [file...]
|
|
197
|
+
run_script() {
|
|
198
|
+
local mode="$1" timeout="$2" interval="$3" settle="$4"
|
|
199
|
+
shift 4
|
|
200
|
+
|
|
201
|
+
RUN_STATUS=0
|
|
202
|
+
RUN_OUTPUT="$(
|
|
203
|
+
FAKE_GH_MODE="$mode" \
|
|
204
|
+
FAKE_GH_STATE="$RUN_STATE" \
|
|
205
|
+
FAKE_GH_COUNTS="$RUN_COUNTS" \
|
|
206
|
+
FAKE_GH_LOG="$RUN_LOG" \
|
|
207
|
+
PATH="$STUB_DIR:$PATH" \
|
|
208
|
+
bash "$SCRIPT_UNDER_TEST" \
|
|
209
|
+
--tag v1.2.3 \
|
|
210
|
+
--repo owner/repo \
|
|
211
|
+
--timeout "$timeout" \
|
|
212
|
+
--interval "$interval" \
|
|
213
|
+
--settle "$settle" \
|
|
214
|
+
"$@" 2>&1
|
|
215
|
+
)" || RUN_STATUS=$?
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
count_of() {
|
|
219
|
+
local file="$RUN_COUNTS/$1"
|
|
220
|
+
if [ -f "$file" ]; then cat "$file"; else printf '0'; fi
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
upload_calls() { count_of upload; }
|
|
224
|
+
|
|
225
|
+
# uploaded_names -- every asset name the script asked gh to upload
|
|
226
|
+
uploaded_names() {
|
|
227
|
+
sed -n 's/^upload //p' "$RUN_LOG" | tr ' ' '\n' | sed '/^$/d' | LC_ALL=C sort
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
output_has() {
|
|
231
|
+
case "$RUN_OUTPUT" in
|
|
232
|
+
*"$1"*) return 0 ;;
|
|
233
|
+
*) return 1 ;;
|
|
234
|
+
esac
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
# ---- pre-flight -------------------------------------------------------
|
|
238
|
+
|
|
239
|
+
cyan "============================================"
|
|
240
|
+
cyan " reconcile-release-assets.sh Test Suite"
|
|
241
|
+
cyan "============================================"
|
|
242
|
+
echo ""
|
|
243
|
+
|
|
244
|
+
dim "--- Pre-flight ---"
|
|
245
|
+
|
|
246
|
+
test "file exists" bash -c "[ -f '$SCRIPT_UNDER_TEST' ]"
|
|
247
|
+
test "starts with shebang" bash -c "head -c 20 '$SCRIPT_UNDER_TEST' | grep -q '^#!/bin/bash'"
|
|
248
|
+
test "bash syntax check passes" bash -c "bash -n '$SCRIPT_UNDER_TEST' 2>&1"
|
|
249
|
+
# Byte-oriented on purpose: `grep -P` is unavailable in some environments and
|
|
250
|
+
# decodes the file through the host locale.
|
|
251
|
+
test "no non-ASCII bytes" bash -c "! LC_ALL=C grep -q '[^[:print:][:space:]]' '$SCRIPT_UNDER_TEST'"
|
|
252
|
+
|
|
253
|
+
echo ""
|
|
254
|
+
|
|
255
|
+
# ---- behaviour --------------------------------------------------------
|
|
256
|
+
|
|
257
|
+
# The common case: the publishing action already uploaded everything. The
|
|
258
|
+
# script must cost one listing call, no upload, and say so.
|
|
259
|
+
test_verifies_a_complete_release_without_uploading() {
|
|
260
|
+
new_run
|
|
261
|
+
seed_asset "$JAR"
|
|
262
|
+
seed_asset "$CLI_BIN"
|
|
263
|
+
seed_asset "$BUNDLE"
|
|
264
|
+
run_script ok 20 1 0 "${ASSET_PATHS[@]}"
|
|
265
|
+
[ "$RUN_STATUS" -eq 0 ] || return 1
|
|
266
|
+
output_has "Verified 3 asset(s) on release v1.2.3" || return 1
|
|
267
|
+
[ "$(upload_calls)" -eq 0 ] || return 1
|
|
268
|
+
[ "$(count_of view)" -eq 1 ] || return 1
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
# The v4.14.0-rc.6 failure: some assets never made it. Only those are sent
|
|
272
|
+
# again -- re-uploading the whole set is what makes the job slow.
|
|
273
|
+
test_uploads_only_the_missing_assets() {
|
|
274
|
+
new_run
|
|
275
|
+
seed_asset "$JAR"
|
|
276
|
+
run_script ok 20 1 0 "${ASSET_PATHS[@]}"
|
|
277
|
+
[ "$RUN_STATUS" -eq 0 ] || return 1
|
|
278
|
+
[ "$(uploaded_names)" = "$(printf '%s\n%s\n' "$BUNDLE" "$CLI_BIN" | LC_ALL=C sort)" ] || return 1
|
|
279
|
+
[ "$(count_of view)" -ge 2 ] || return 1
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
# A failed upload can leave a partial asset behind: size, not presence, decides.
|
|
283
|
+
test_repairs_a_partial_asset_by_size() {
|
|
284
|
+
new_run
|
|
285
|
+
seed_asset "$JAR"
|
|
286
|
+
seed_asset "$CLI_BIN"
|
|
287
|
+
seed_asset "$BUNDLE" 7
|
|
288
|
+
run_script ok 20 1 0 "${ASSET_PATHS[@]}"
|
|
289
|
+
[ "$RUN_STATUS" -eq 0 ] || return 1
|
|
290
|
+
output_has "size mismatch: the release has 7 bytes, the file has 303" || return 1
|
|
291
|
+
[ "$(uploaded_names)" = "$BUNDLE" ] || return 1
|
|
292
|
+
[ "$(cat "$RUN_STATE/$BUNDLE")" = "$BUNDLE_BYTES" ] || return 1
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
# The CI failure itself: GitHub answers "Error creating asset temp dir". The
|
|
296
|
+
# round must be retried instead of failing the release.
|
|
297
|
+
test_survives_the_transient_upload_error() {
|
|
298
|
+
new_run
|
|
299
|
+
run_script upload-fails-once 20 1 0 "${ASSET_PATHS[@]}"
|
|
300
|
+
[ "$RUN_STATUS" -eq 0 ] || return 1
|
|
301
|
+
output_has "Error creating asset temp dir" || return 1
|
|
302
|
+
output_has "WARN: gh release upload failed" || return 1
|
|
303
|
+
output_has "Verified 3 asset(s) on release v1.2.3" || return 1
|
|
304
|
+
[ "$(upload_calls)" -ge 2 ] || return 1
|
|
305
|
+
[ "$(cat "$RUN_STATE/$BUNDLE")" = "$BUNDLE_BYTES" ] || return 1
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
# The listing API is just as capable of a 5xx as the upload endpoint.
|
|
309
|
+
test_survives_a_transient_listing_error() {
|
|
310
|
+
new_run
|
|
311
|
+
seed_asset "$JAR"
|
|
312
|
+
seed_asset "$CLI_BIN"
|
|
313
|
+
seed_asset "$BUNDLE"
|
|
314
|
+
run_script view-fails-once 20 1 0 "${ASSET_PATHS[@]}"
|
|
315
|
+
[ "$RUN_STATUS" -eq 0 ] || return 1
|
|
316
|
+
output_has "WARN: could not list the assets of release v1.2.3" || return 1
|
|
317
|
+
output_has "Verified 3 asset(s) on release v1.2.3" || return 1
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
# A missing release is the creation path, not the upload path: say so instead
|
|
321
|
+
# of recreating it here with metadata the workflow did not pass.
|
|
322
|
+
test_fails_when_the_release_does_not_exist() {
|
|
323
|
+
new_run
|
|
324
|
+
run_script release-missing 20 1 0 "${ASSET_PATHS[@]}"
|
|
325
|
+
[ "$RUN_STATUS" -eq 1 ] || return 1
|
|
326
|
+
output_has "release v1.2.3 does not exist" || return 1
|
|
327
|
+
output_has "release not found" || return 1
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
# Uploads that never take effect must fail the job, with the offenders named.
|
|
331
|
+
test_fails_when_uploads_never_take_effect() {
|
|
332
|
+
new_run
|
|
333
|
+
run_script upload-never-succeeds 2 1 0 "${ASSET_PATHS[@]}"
|
|
334
|
+
[ "$RUN_STATUS" -eq 1 ] || return 1
|
|
335
|
+
output_has "still does not match the build output after 2s" || return 1
|
|
336
|
+
output_has " - $BUNDLE: missing from the release" || return 1
|
|
337
|
+
output_has "Re-run the failed jobs of this workflow" || return 1
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
# ... and it must give up on time rather than sleep past its budget.
|
|
341
|
+
test_respects_the_time_budget() {
|
|
342
|
+
local started elapsed
|
|
343
|
+
new_run
|
|
344
|
+
started=$SECONDS
|
|
345
|
+
run_script upload-never-succeeds 2 1 0 "${ASSET_PATHS[@]}"
|
|
346
|
+
elapsed=$((SECONDS - started))
|
|
347
|
+
[ "$RUN_STATUS" -eq 1 ] || return 1
|
|
348
|
+
[ "$elapsed" -le 10 ] || return 1
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
# The release workflow hands over the manifest it already builds: one asset
|
|
352
|
+
# path per line, with blank lines and CRLF tolerated.
|
|
353
|
+
test_reads_a_newline_separated_manifest() {
|
|
354
|
+
local manifest="$STUB_DIR/manifest.txt"
|
|
355
|
+
{
|
|
356
|
+
printf '%s\r\n' "${ASSET_PATHS[0]}"
|
|
357
|
+
printf '\n'
|
|
358
|
+
printf '%s\n%s\n' "${ASSET_PATHS[1]}" "${ASSET_PATHS[2]}"
|
|
359
|
+
} > "$manifest"
|
|
360
|
+
|
|
361
|
+
new_run
|
|
362
|
+
seed_asset "$JAR"
|
|
363
|
+
seed_asset "$CLI_BIN"
|
|
364
|
+
seed_asset "$BUNDLE"
|
|
365
|
+
run_script ok 20 1 0 --files-from "$manifest"
|
|
366
|
+
[ "$RUN_STATUS" -eq 0 ] || return 1
|
|
367
|
+
output_has "Verified 3 asset(s) on release v1.2.3" || return 1
|
|
368
|
+
[ "$(upload_calls)" -eq 0 ] || return 1
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
test_rejects_a_missing_asset_file() {
|
|
372
|
+
new_run
|
|
373
|
+
run_script ok 20 1 0 "$ASSET_DIR/not-built.tar.gz"
|
|
374
|
+
[ "$RUN_STATUS" -eq 1 ] || return 1
|
|
375
|
+
output_has "does not exist" || return 1
|
|
376
|
+
[ "$(count_of view)" -eq 0 ] || return 1
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
test_requires_a_tag() {
|
|
380
|
+
local output status
|
|
381
|
+
output="$(PATH="$STUB_DIR:$PATH" bash "$SCRIPT_UNDER_TEST" "${ASSET_PATHS[@]}" 2>&1)" && status=0 || status=$?
|
|
382
|
+
[ "$status" -ne 0 ] || return 1
|
|
383
|
+
[[ "$output" == *"--tag is required"* ]] || return 1
|
|
384
|
+
[[ "$output" == *"Usage:"* ]] || return 1
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
test_requires_asset_files() {
|
|
388
|
+
local output status
|
|
389
|
+
output="$(PATH="$STUB_DIR:$PATH" bash "$SCRIPT_UNDER_TEST" --tag v1.2.3 2>&1)" && status=0 || status=$?
|
|
390
|
+
[ "$status" -ne 0 ] || return 1
|
|
391
|
+
[[ "$output" == *"no asset files given"* ]] || return 1
|
|
392
|
+
[[ "$output" == *"Usage:"* ]] || return 1
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
dim "--- Behaviour ---"
|
|
396
|
+
|
|
397
|
+
test "complete release: verified, nothing uploaded" test_verifies_a_complete_release_without_uploading
|
|
398
|
+
test "uploads only the missing assets" test_uploads_only_the_missing_assets
|
|
399
|
+
test "repairs a partial asset by size" test_repairs_a_partial_asset_by_size
|
|
400
|
+
test "survives the transient upload error that failed v4.14.0-rc.6" test_survives_the_transient_upload_error
|
|
401
|
+
test "survives a transient listing error" test_survives_a_transient_listing_error
|
|
402
|
+
test "fails when the release does not exist" test_fails_when_the_release_does_not_exist
|
|
403
|
+
test "fails when uploads never take effect" test_fails_when_uploads_never_take_effect
|
|
404
|
+
test "respects the time budget" test_respects_the_time_budget
|
|
405
|
+
test "reads a newline-separated manifest" test_reads_a_newline_separated_manifest
|
|
406
|
+
test "rejects a missing asset file" test_rejects_a_missing_asset_file
|
|
407
|
+
test "requires a tag" test_requires_a_tag
|
|
408
|
+
test "requires asset files" test_requires_asset_files
|
|
409
|
+
|
|
410
|
+
# ---- summary ----------------------------------------------------------
|
|
411
|
+
|
|
412
|
+
echo ""
|
|
413
|
+
cyan "============================================"
|
|
414
|
+
if [ "$FAIL" -eq 0 ]; then
|
|
415
|
+
green " All $PASS tests passed"
|
|
416
|
+
else
|
|
417
|
+
red " $FAIL of $((PASS + FAIL)) tests failed"
|
|
418
|
+
fi
|
|
419
|
+
cyan "============================================"
|
|
420
|
+
|
|
421
|
+
[ "$FAIL" -eq 0 ]
|