browser4-cli 4.14.0-rc.6 → 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 +66 -0
- package/scripts/reconcile-release-assets.sh +293 -0
- package/scripts/tests/reconcile-release-assets.tests.sh +421 -0
- package/scripts/tests/wait-for-oss-sync.tests.sh +382 -0
- package/scripts/wait-for-oss-sync.sh +314 -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
|