browser4-cli 4.13.21 → 4.13.22
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 +7 -5
- 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 +34 -0
- package/scripts/tests/wait-for-oss-sync.tests.sh +382 -0
- package/scripts/wait-for-oss-sync.sh +314 -0
package/README.md
CHANGED
|
@@ -227,7 +227,7 @@ These flags can appear before any command.
|
|
|
227
227
|
| `-h`, `--help [command\|category]` | Show top-level help, category help, or detailed command help |
|
|
228
228
|
| `--help-json` | Emit the machine-readable command reference |
|
|
229
229
|
| `-v`, `--version` | Print the CLI version |
|
|
230
|
-
| `-s`, `--session <name>` | Use a named session instead of the default session |
|
|
230
|
+
| `-s`, `--session <name>` | Use a named session instead of the default session. A **global** flag: place it **before** the command (`browser4-cli -s job-42 snapshot`) — after the command it is rejected as a positional argument |
|
|
231
231
|
| `--server <url>` | Override the Browser4 server URL |
|
|
232
232
|
| `--timeout <seconds>` | Override the HTTP timeout for the current command |
|
|
233
233
|
| `--proxy <url>` | Proxy used for runtime downloads/install operations |
|
|
@@ -281,7 +281,7 @@ export DEEPSEEK_API_KEY=sk-xxxxxxxxxxxxxxxxxxxxxxxx
|
|
|
281
281
|
| `close-all` | Close all sessions without stopping the backend. |
|
|
282
282
|
| `kill-all` | Force-stop the backend and Browser4-managed browser processes. |
|
|
283
283
|
| `stop` | Gracefully stop the Browser4 server. |
|
|
284
|
-
| `status` | Show server version, port, health, and the web status panel URL (`http://<server>:
|
|
284
|
+
| `status` | Show server version, port, health, and the web status panel URL (`http://<server>:18182/status`). When a session is active it also prints a current-session block: Name / Session ID / Status / Connection / Next open. |
|
|
285
285
|
| `doctor` | Run diagnostics: build info, LLM status, stale daemon cleanup, optional repair. Supports `--verbose` and `--fix`. |
|
|
286
286
|
| `doctor log [name]` | List, view, tail, or grep backend log files. Supports `--tail`, grep-style flags, and `doctor log <name> grep <pattern>`. |
|
|
287
287
|
| `doctor metrics [filter]` | List, filter, or grep backend metrics. Supports `doctor metrics grep <pattern>`. |
|
|
@@ -522,15 +522,17 @@ The `co` prefix is accepted as an alias for `swarm`.
|
|
|
522
522
|
| `crawl [url]` | Crawl from a URL or seed file. Supports `--seed-file`, `--sql`, `--sql-stdin`, `--sql-base64`, `--format`, `--output`, `-d/--depth`, `-ol/--out-link-selector`, `-olp/--out-link-pattern`, `-tl/--top-links`, `-a/--args`, `--refresh`, `--parse`, `--expires`, `-p/--priority`, `--page-load-timeout`, `--ignore-url-query`, `--no-norm`, `--readonly`, `-bg/--background`. |
|
|
523
523
|
| `crawl status <id>` | Check crawl task status. |
|
|
524
524
|
| `crawl result <id>` | Fetch crawl results. |
|
|
525
|
-
| `crawl cancel <id>` | Cancel a running crawl. |
|
|
526
|
-
| `crawl
|
|
527
|
-
| `crawl
|
|
525
|
+
| `crawl cancel <id>` | Cancel a running crawl (the checkpoint is kept, so it can be resumed). |
|
|
526
|
+
| `crawl resume <id>` | Continue an interrupted crawl from its checkpoint: same task id, no repeat requests for URLs that already succeeded. `--retry-failed` re-fetches terminal failures; automatic resume at startup is off by default (`crawl.autoResume`). See [Crawl checkpoint & resume](docs/crawl-checkpoint-resume.md). |
|
|
527
|
+
| `crawl clear` | Remove terminal-state crawl tasks; supports force-style cleanup options. Resumable checkpoints are kept until `crawl clear --all`. |
|
|
528
|
+
| `crawl list` | List tracked crawl tasks (`--status interrupted` shows the resumable ones). |
|
|
528
529
|
|
|
529
530
|
```bash
|
|
530
531
|
browser4-cli swarm create --max-open-tabs 12 --display-mode HEADLESS
|
|
531
532
|
browser4-cli swarm query --seed-file urls.txt --sql @query.sql --refresh
|
|
532
533
|
browser4-cli crawl "https://example.com" --depth 2 --out-link-selector "a[href]"
|
|
533
534
|
browser4-cli crawl list
|
|
535
|
+
browser4-cli crawl resume <task-id> # continue an interrupted crawl
|
|
534
536
|
```
|
|
535
537
|
|
|
536
538
|
#### Bundled skill files vs installed runtime skills
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/package.json
CHANGED
package/scripts/README.md
CHANGED
|
@@ -85,6 +85,7 @@ Runs a full CLI smoke test suitable for CI and local development:
|
|
|
85
85
|
| `tests/install-browser4-cli.tests.sh` | Unit tests for the Unix install script |
|
|
86
86
|
| `tests/install-browser4-cli.tests.ps1` | Unit tests for the Windows install script (Pester) |
|
|
87
87
|
| `tests/wait-for-npm-version.tests.sh` | Unit tests for `wait-for-npm-version.sh` (stubbed `npm`, no network) |
|
|
88
|
+
| `tests/wait-for-oss-sync.tests.sh` | Unit tests for `wait-for-oss-sync.sh` (stubbed `gh`, no network) |
|
|
88
89
|
|
|
89
90
|
## Publish (npm)
|
|
90
91
|
|
|
@@ -153,6 +154,39 @@ This makes the npm package README match the repository root README without leavi
|
|
|
153
154
|
|
|
154
155
|
`postinstall.js` runs automatically after `npm install browser4-cli`. It detects the platform and downloads the matching native binary to `bin/`. On global installs, it also patches npm's bin shims to invoke the native binary directly.
|
|
155
156
|
|
|
157
|
+
## Publish (release workflows)
|
|
158
|
+
|
|
159
|
+
| Script | Platform | Purpose |
|
|
160
|
+
|--------|----------|---------|
|
|
161
|
+
| `wait-for-oss-sync.sh` | Linux/macOS | Triggers the `sync-to-oss.yml` workflow for a release tag and waits for **that** run; called by `release.yml` and `release-cli.yml` |
|
|
162
|
+
|
|
163
|
+
### Verifying the OSS CDN sync
|
|
164
|
+
|
|
165
|
+
GitHub Releases and the Alibaba Cloud OSS mirror are separate distribution channels: the OSS
|
|
166
|
+
mirror is what `install-browser4-cli.sh` prefers in the China mainland, and its `latest`
|
|
167
|
+
symlinks are what a plain "install the latest" resolves to. So the release should not report
|
|
168
|
+
success while the mirror is still on the previous version — and it should not report failure
|
|
169
|
+
just because the mirror is slow.
|
|
170
|
+
|
|
171
|
+
`wait-for-oss-sync.sh` dispatches `sync-to-oss.yml` for the tag and waits for the run it
|
|
172
|
+
**caused**, under a budget of its own:
|
|
173
|
+
|
|
174
|
+
```shell
|
|
175
|
+
bash scripts/wait-for-oss-sync.sh --tag v4.13.21 # 45 min budget, 15 s interval
|
|
176
|
+
bash scripts/wait-for-oss-sync.sh --tag v4.13.21 --timeout 600 --interval 5
|
|
177
|
+
bash scripts/wait-for-oss-sync.sh --tag v4.13.21 --repo platonai/Browser4
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
Exit code 0 = the run it triggered concluded `success`; 1 = that run failed, was cancelled,
|
|
181
|
+
never appeared, or was still running when the budget expired. On failure and on timeout the
|
|
182
|
+
message names the run, its URL, the step it is stuck on, and the commands to re-check
|
|
183
|
+
(`gh run view <id>`) and re-trigger (`gh workflow run sync-to-oss.yml -f tag_name=<tag>`)
|
|
184
|
+
it. It does **not** retry the sync itself: a re-triggered sync re-uploads every asset.
|
|
185
|
+
|
|
186
|
+
Runs that already existed before the dispatch are excluded by id, so a previous release's
|
|
187
|
+
successful sync can never be mistaken for the current one — the failure mode the old inline
|
|
188
|
+
`gh run list --limit 1` loop had (see `docs-dev/copilot/ci-stabilization-4.13.x.md` §29).
|
|
189
|
+
|
|
156
190
|
## Documentation
|
|
157
191
|
|
|
158
192
|
| Script | Platform | Purpose |
|
|
@@ -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,314 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# ---------------------------------------------------------------------------
|
|
3
|
+
# wait-for-oss-sync.sh -- trigger the Aliyun OSS sync for a release tag and
|
|
4
|
+
# wait for that exact run to finish
|
|
5
|
+
# ---------------------------------------------------------------------------
|
|
6
|
+
# Usage:
|
|
7
|
+
# wait-for-oss-sync.sh --tag <tag> [--repo <owner/name>] [--workflow <name>]
|
|
8
|
+
# [--timeout <seconds>] [--interval <seconds>]
|
|
9
|
+
# [--discovery-timeout <seconds>]
|
|
10
|
+
#
|
|
11
|
+
# Defaults: --workflow sync-to-oss.yml, --timeout 3900 s, --interval 15 s,
|
|
12
|
+
# --discovery-timeout 120 s.
|
|
13
|
+
#
|
|
14
|
+
# release.yml and release-cli.yml used to inline this whole dance:
|
|
15
|
+
#
|
|
16
|
+
# gh workflow run sync-to-oss.yml -f tag_name="$TAG"
|
|
17
|
+
# sleep 3
|
|
18
|
+
# RUN_ID=$(gh run list --workflow=sync-to-oss.yml --limit 1 ...) # nothing but air for 20 tries
|
|
19
|
+
# gh run watch "$RUN_ID" --exit-status
|
|
20
|
+
#
|
|
21
|
+
# under a step `timeout-minutes: 15`. Three things were wrong with it:
|
|
22
|
+
#
|
|
23
|
+
# 1. The run it waited on was not necessarily the run it triggered. `gh run
|
|
24
|
+
# list` answers newest-first, so while the freshly dispatched run was not
|
|
25
|
+
# indexed yet the loop's first non-empty answer was the PREVIOUS release's
|
|
26
|
+
# sync run -- already completed, conclusion "success". The step then
|
|
27
|
+
# reported the CDN as updated without having waited for the current sync
|
|
28
|
+
# at all. Same defect class as bd18d2b7d8, which fixed the identical
|
|
29
|
+
# run-discovery race in monitor-release.ps1.
|
|
30
|
+
# 2. The wait had no budget of its own. The only bound was the step timeout,
|
|
31
|
+
# which kills the step and reports
|
|
32
|
+
# "The action 'Sync to Aliyun OSS CDN' has timed out after 15 minutes."
|
|
33
|
+
# -- no run id, no run URL, and no statement of what the sync was doing.
|
|
34
|
+
# 3. `gh run watch` re-renders the entire run every 3 s: a wall of text that
|
|
35
|
+
# still never says which step the sync is stuck on.
|
|
36
|
+
#
|
|
37
|
+
# v4.13.21 (release.yml run 35860129386) hit all three at once. The sync run's
|
|
38
|
+
# "Upload to OSS" step stalled -- 18+ minutes and still counting, against ~2
|
|
39
|
+
# minutes for the same 11 assets in every earlier release -- the 15-minute cap
|
|
40
|
+
# fired, and the release job went red after everything had in fact been
|
|
41
|
+
# published to GitHub, npm and the container registries.
|
|
42
|
+
#
|
|
43
|
+
# So this script:
|
|
44
|
+
#
|
|
45
|
+
# 1. records the sync runs that already exist BEFORE dispatching, and only
|
|
46
|
+
# ever accepts a run that is not one of them (or, when that listing
|
|
47
|
+
# failed, one created after the newest of them) -- a stale run can never
|
|
48
|
+
# be mistaken for ours,
|
|
49
|
+
# 2. waits under an explicit budget of its own, never sleeping past the
|
|
50
|
+
# deadline, and
|
|
51
|
+
# 3. reports the run id, the run URL, the elapsed time and the step the run
|
|
52
|
+
# is currently on -- on success, on failure, and on timeout alike.
|
|
53
|
+
#
|
|
54
|
+
# It deliberately does not retry the sync: a re-triggered sync re-uploads every
|
|
55
|
+
# asset. The verdict belongs to the caller; re-run the workflow (or this
|
|
56
|
+
# script) once the run it names has been looked at.
|
|
57
|
+
#
|
|
58
|
+
# Exit codes:
|
|
59
|
+
# 0 the sync run this script triggered completed successfully
|
|
60
|
+
# 1 that run failed, was cancelled, never appeared, or was still running
|
|
61
|
+
# when the budget expired -- or bad usage
|
|
62
|
+
#
|
|
63
|
+
# Designed for CI (GitHub Actions) but also runs on developer machines.
|
|
64
|
+
# Requires `gh`, authenticated via GH_TOKEN/GITHUB_TOKEN or `gh auth login`.
|
|
65
|
+
# ---------------------------------------------------------------------------
|
|
66
|
+
set -euo pipefail
|
|
67
|
+
|
|
68
|
+
TAG=''
|
|
69
|
+
REPO=''
|
|
70
|
+
WORKFLOW='sync-to-oss.yml'
|
|
71
|
+
# 65 min: above sync-to-oss.yml's worst case (job 60 min) plus queue time, so the
|
|
72
|
+
# waiter reports the sync's verdict instead of expiring first. Sized for a
|
|
73
|
+
# re-sync, where the destination objects already exist and the transfer runs at
|
|
74
|
+
# roughly a tenth of a first sync's speed.
|
|
75
|
+
TIMEOUT_SECONDS=3900
|
|
76
|
+
POLL_INTERVAL_SECONDS=15
|
|
77
|
+
DISCOVERY_TIMEOUT_SECONDS=120
|
|
78
|
+
|
|
79
|
+
usage() {
|
|
80
|
+
local code="${1:-1}"
|
|
81
|
+
{
|
|
82
|
+
echo "Usage: $0 --tag <tag> [--repo <owner/name>] [--workflow <name>]"
|
|
83
|
+
echo " [--timeout <seconds>] [--interval <seconds>]"
|
|
84
|
+
echo " [--discovery-timeout <seconds>]"
|
|
85
|
+
} >&2
|
|
86
|
+
exit "$code"
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
while [ $# -gt 0 ]; do
|
|
90
|
+
case "${1:-}" in
|
|
91
|
+
--tag)
|
|
92
|
+
TAG="${2:-}"
|
|
93
|
+
shift 2 || usage
|
|
94
|
+
;;
|
|
95
|
+
--repo)
|
|
96
|
+
REPO="${2:-}"
|
|
97
|
+
shift 2 || usage
|
|
98
|
+
;;
|
|
99
|
+
--workflow)
|
|
100
|
+
WORKFLOW="${2:-}"
|
|
101
|
+
shift 2 || usage
|
|
102
|
+
;;
|
|
103
|
+
--timeout)
|
|
104
|
+
TIMEOUT_SECONDS="${2:-}"
|
|
105
|
+
shift 2 || usage
|
|
106
|
+
;;
|
|
107
|
+
--interval)
|
|
108
|
+
POLL_INTERVAL_SECONDS="${2:-}"
|
|
109
|
+
shift 2 || usage
|
|
110
|
+
;;
|
|
111
|
+
--discovery-timeout)
|
|
112
|
+
DISCOVERY_TIMEOUT_SECONDS="${2:-}"
|
|
113
|
+
shift 2 || usage
|
|
114
|
+
;;
|
|
115
|
+
-h|--help)
|
|
116
|
+
usage 0
|
|
117
|
+
;;
|
|
118
|
+
*)
|
|
119
|
+
echo "ERROR: unknown argument '$1'" >&2
|
|
120
|
+
usage
|
|
121
|
+
;;
|
|
122
|
+
esac
|
|
123
|
+
done
|
|
124
|
+
|
|
125
|
+
[ -n "$TAG" ] || { echo "ERROR: --tag is required" >&2; usage; }
|
|
126
|
+
[ -n "$WORKFLOW" ] || { echo "ERROR: --workflow must not be empty" >&2; usage; }
|
|
127
|
+
case "$TIMEOUT_SECONDS" in ''|*[!0-9]*) echo "ERROR: --timeout must be a whole number of seconds" >&2; usage ;; esac
|
|
128
|
+
case "$POLL_INTERVAL_SECONDS" in ''|*[!0-9]*) echo "ERROR: --interval must be a whole number of seconds" >&2; usage ;; esac
|
|
129
|
+
case "$DISCOVERY_TIMEOUT_SECONDS" in ''|*[!0-9]*) echo "ERROR: --discovery-timeout must be a whole number of seconds" >&2; usage ;; esac
|
|
130
|
+
[ "$TIMEOUT_SECONDS" -gt 0 ] || { echo "ERROR: --timeout must be at least 1 s" >&2; usage; }
|
|
131
|
+
[ "$POLL_INTERVAL_SECONDS" -gt 0 ] || { echo "ERROR: --interval must be at least 1 s" >&2; usage; }
|
|
132
|
+
|
|
133
|
+
repo_label="${REPO:-<current repo>}"
|
|
134
|
+
|
|
135
|
+
# gh reads --repo per subcommand, so append it rather than building an argument
|
|
136
|
+
# list: an empty array under `set -u` is a portability trap older bash and
|
|
137
|
+
# GitHub's runners disagree about.
|
|
138
|
+
gh_ws() {
|
|
139
|
+
if [ -n "$REPO" ]; then
|
|
140
|
+
gh "$@" --repo "$REPO"
|
|
141
|
+
else
|
|
142
|
+
gh "$@"
|
|
143
|
+
fi
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
# The most recent runs of the sync workflow, newest first, as TSV.
|
|
147
|
+
# `limit` is a parameter because the pre-dispatch listing wants a long memory
|
|
148
|
+
# (every run that can be mistaken for ours) while the discovery poll only ever
|
|
149
|
+
# looks at the newest few. Fails (non-zero) when gh cannot answer, so the
|
|
150
|
+
# caller can tell "no runs yet" from "the listing is unavailable".
|
|
151
|
+
list_runs_tsv() {
|
|
152
|
+
gh_ws run list --workflow="$WORKFLOW" --limit "$1" \
|
|
153
|
+
--json databaseId,createdAt -q '.[] | [.databaseId, .createdAt] | @tsv' 2>/dev/null
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
# status, conclusion and the step the run is currently on, as TSV.
|
|
157
|
+
#
|
|
158
|
+
# "--" stands for "no value": a bare empty tab-separated field is not
|
|
159
|
+
# survivable here, because tab is IFS whitespace, so `read` collapses a run of
|
|
160
|
+
# tabs and an empty conclusion would silently swallow the step name (which is
|
|
161
|
+
# exactly the diagnostic this script exists to report).
|
|
162
|
+
view_run_tsv() {
|
|
163
|
+
local run_id="$1"
|
|
164
|
+
gh_ws run view "$run_id" --json status,conclusion,jobs \
|
|
165
|
+
-q '[.status, (.conclusion // "--"), ([.jobs[].steps[]? | select(.status == "in_progress") | .name] | join(",") | if . == "" then "--" else . end)] | @tsv' 2>/dev/null || true
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
# ---- 1. what the sync workflow looks like before we add to it --------------
|
|
169
|
+
|
|
170
|
+
baseline_ok=true
|
|
171
|
+
baseline_tsv=''
|
|
172
|
+
if ! baseline_tsv="$(list_runs_tsv 50)"; then
|
|
173
|
+
baseline_ok=false
|
|
174
|
+
baseline_tsv=''
|
|
175
|
+
fi
|
|
176
|
+
|
|
177
|
+
baseline_ids=' '
|
|
178
|
+
watermark_created=''
|
|
179
|
+
while IFS=$'\t' read -r _id _created; do
|
|
180
|
+
[ -n "${_id:-}" ] || continue
|
|
181
|
+
baseline_ids="${baseline_ids}${_id} "
|
|
182
|
+
done <<< "$baseline_tsv"
|
|
183
|
+
|
|
184
|
+
if [ "$baseline_ok" != true ]; then
|
|
185
|
+
# No id baseline to compare against, so our run is identified by creation
|
|
186
|
+
# time alone: anything created after this dispatch watermark is a
|
|
187
|
+
# candidate. The watermark is local, so a stale run could only slip
|
|
188
|
+
# through by being created in the same second as our dispatch -- which
|
|
189
|
+
# would be a sync of the same release anyway.
|
|
190
|
+
watermark_created="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
|
191
|
+
echo "WARN: could not list existing ${WORKFLOW} runs; identifying our run by creation time alone (after ${watermark_created})"
|
|
192
|
+
fi
|
|
193
|
+
echo "Existing ${WORKFLOW} runs to exclude: $(printf '%s' "$baseline_ids" | wc -w)"
|
|
194
|
+
|
|
195
|
+
# ---- 2. dispatch ----------------------------------------------------------
|
|
196
|
+
|
|
197
|
+
echo "Triggering ${WORKFLOW} for ${TAG}"
|
|
198
|
+
if ! gh_ws workflow run "$WORKFLOW" -f tag_name="$TAG"; then
|
|
199
|
+
echo "::error::Could not dispatch ${WORKFLOW} for ${TAG} in ${repo_label} -- check that the workflow exists on the default branch and that the token may run it."
|
|
200
|
+
exit 1
|
|
201
|
+
fi
|
|
202
|
+
|
|
203
|
+
# ---- 3. find OUR run, never an older one ----------------------------------
|
|
204
|
+
|
|
205
|
+
RUN_ID=''
|
|
206
|
+
RUN_URL=''
|
|
207
|
+
discovery_started=$SECONDS
|
|
208
|
+
attempt=0
|
|
209
|
+
|
|
210
|
+
while :; do
|
|
211
|
+
attempt=$((attempt + 1))
|
|
212
|
+
rows="$(list_runs_tsv 20)" || rows=''
|
|
213
|
+
|
|
214
|
+
while IFS=$'\t' read -r run_id created_at; do
|
|
215
|
+
[ -n "${run_id:-}" ] || continue
|
|
216
|
+
# Never accept a run that existed before we dispatched. With the id
|
|
217
|
+
# baseline in hand that test is exact and involves no clock at all;
|
|
218
|
+
# only when that listing failed do we fall back to the creation-time
|
|
219
|
+
# watermark taken just before the dispatch.
|
|
220
|
+
if [ "$baseline_ok" = true ]; then
|
|
221
|
+
if [[ "$baseline_ids" == *" ${run_id} "* ]]; then
|
|
222
|
+
continue
|
|
223
|
+
fi
|
|
224
|
+
elif [[ "$created_at" < "$watermark_created" ]]; then
|
|
225
|
+
continue
|
|
226
|
+
fi
|
|
227
|
+
RUN_ID="$run_id"
|
|
228
|
+
RUN_URL="https://github.com/${REPO:-platonai/Browser4}/actions/runs/${run_id} (created ${created_at})"
|
|
229
|
+
break
|
|
230
|
+
done <<< "$rows"
|
|
231
|
+
|
|
232
|
+
if [ -n "$RUN_ID" ]; then
|
|
233
|
+
break
|
|
234
|
+
fi
|
|
235
|
+
|
|
236
|
+
elapsed=$((SECONDS - discovery_started))
|
|
237
|
+
remaining=$((DISCOVERY_TIMEOUT_SECONDS - elapsed))
|
|
238
|
+
if [ "$remaining" -le 0 ]; then
|
|
239
|
+
break
|
|
240
|
+
fi
|
|
241
|
+
echo "Waiting for the ${WORKFLOW} run to appear (attempt ${attempt}, ${remaining}s left)"
|
|
242
|
+
if [ "$POLL_INTERVAL_SECONDS" -gt "$remaining" ]; then
|
|
243
|
+
sleep "$remaining"
|
|
244
|
+
else
|
|
245
|
+
sleep "$POLL_INTERVAL_SECONDS"
|
|
246
|
+
fi
|
|
247
|
+
done
|
|
248
|
+
|
|
249
|
+
if [ -z "$RUN_ID" ]; then
|
|
250
|
+
echo "::error::${WORKFLOW} was dispatched for ${TAG} but no new run appeared within ${DISCOVERY_TIMEOUT_SECONDS}s."
|
|
251
|
+
echo "::error::Check https://github.com/${REPO:-platonai/Browser4}/actions/workflows/${WORKFLOW} -- the run may have been refused before it was queued."
|
|
252
|
+
exit 1
|
|
253
|
+
fi
|
|
254
|
+
|
|
255
|
+
RUN_URL="https://github.com/${REPO:-platonai/Browser4}/actions/runs/${RUN_ID}"
|
|
256
|
+
echo "Watching ${WORKFLOW} run ${RUN_ID} for ${TAG}: ${RUN_URL}"
|
|
257
|
+
|
|
258
|
+
# ---- 4. wait for it, under our own budget ---------------------------------
|
|
259
|
+
|
|
260
|
+
status='unknown'
|
|
261
|
+
conclusion=''
|
|
262
|
+
stuck_on=''
|
|
263
|
+
last_state=''
|
|
264
|
+
last_reported_at=-60
|
|
265
|
+
wait_started=$SECONDS
|
|
266
|
+
|
|
267
|
+
while :; do
|
|
268
|
+
now=$((SECONDS - wait_started))
|
|
269
|
+
|
|
270
|
+
line="$(view_run_tsv "$RUN_ID")"
|
|
271
|
+
if [ -n "$line" ]; then
|
|
272
|
+
status=''; conclusion=''; stuck_on=''
|
|
273
|
+
IFS=$'\t' read -r status conclusion stuck_on <<< "$line" || true
|
|
274
|
+
[ "$conclusion" != '--' ] || conclusion=''
|
|
275
|
+
[ "$stuck_on" != '--' ] || stuck_on=''
|
|
276
|
+
fi
|
|
277
|
+
# An unreadable poll (rate limit, network blip) keeps the last known state
|
|
278
|
+
# and simply counts against the budget -- it must not abort the wait.
|
|
279
|
+
|
|
280
|
+
state="${status}|${stuck_on}"
|
|
281
|
+
remaining=$((TIMEOUT_SECONDS - now))
|
|
282
|
+
if [ "$state" != "$last_state" ] || [ $((now - last_reported_at)) -ge 60 ]; then
|
|
283
|
+
echo "run ${RUN_ID}: status=${status:-unknown}${stuck_on:+, step=${stuck_on}} (${now}s elapsed, ${remaining}s left)"
|
|
284
|
+
last_state="$state"
|
|
285
|
+
last_reported_at="$now"
|
|
286
|
+
fi
|
|
287
|
+
|
|
288
|
+
if [ "$status" = 'completed' ]; then
|
|
289
|
+
if [ "$conclusion" = 'success' ]; then
|
|
290
|
+
echo "Aliyun OSS CDN sync for ${TAG} completed successfully"
|
|
291
|
+
echo "Run URL: ${RUN_URL}"
|
|
292
|
+
exit 0
|
|
293
|
+
fi
|
|
294
|
+
echo "::error::${WORKFLOW} run ${RUN_ID} concluded '${conclusion:-unknown}' -- the OSS CDN may still serve an older release."
|
|
295
|
+
echo "::error::Run URL: ${RUN_URL}"
|
|
296
|
+
echo "::error::Re-check it, then re-trigger with: gh workflow run ${WORKFLOW} -f tag_name=${TAG}"
|
|
297
|
+
exit 1
|
|
298
|
+
fi
|
|
299
|
+
|
|
300
|
+
if [ "$remaining" -le 0 ]; then
|
|
301
|
+
break
|
|
302
|
+
fi
|
|
303
|
+
if [ "$POLL_INTERVAL_SECONDS" -gt "$remaining" ]; then
|
|
304
|
+
sleep "$remaining"
|
|
305
|
+
else
|
|
306
|
+
sleep "$POLL_INTERVAL_SECONDS"
|
|
307
|
+
fi
|
|
308
|
+
done
|
|
309
|
+
|
|
310
|
+
echo "::error::${WORKFLOW} run ${RUN_ID} was still ${status:-unknown}${stuck_on:+ (step: ${stuck_on})} after ${TIMEOUT_SECONDS}s -- the OSS CDN was not confirmed updated for ${TAG}."
|
|
311
|
+
echo "::error::Run URL: ${RUN_URL}"
|
|
312
|
+
echo "::error::Re-check it with: gh run view ${RUN_ID}${REPO:+ --repo $REPO}"
|
|
313
|
+
echo "::error::Then re-trigger with: gh workflow run ${WORKFLOW}${REPO:+ --repo $REPO} -f tag_name=${TAG}"
|
|
314
|
+
exit 1
|