@norskvideo/ctl-dev-kit 0.2.99 → 0.2.101
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/conventions/bot-prs.yml
CHANGED
|
@@ -35,7 +35,15 @@
|
|
|
35
35
|
# actually read;
|
|
36
36
|
# - checks or a tier still running -> leave it for the next trigger; a tier
|
|
37
37
|
# that has not started an hour after checks finished never will (its
|
|
38
|
-
# workflow is broken, or does not run on PRs), so that is red too
|
|
38
|
+
# workflow is broken, or does not run on PRs), so that is red too;
|
|
39
|
+
# - a tier integration.yml cancelled because the host was too contended to
|
|
40
|
+
# judge it (its job carries a `ctl-contention-uncertain` annotation) -> not
|
|
41
|
+
# yet judged, so neither red nor merged: re-run once a backoff has passed
|
|
42
|
+
# (vars.BOT_PRS_CONTENDED_BACKOFF_MINUTES, default 30, doubling per
|
|
43
|
+
# attempt), up to vars.BOT_PRS_CONTENDED_ATTEMPTS attempts (default 4);
|
|
44
|
+
# after that it is red as "still contended after N attempts: needs a human
|
|
45
|
+
# or a quieter runner", never as a failure. Any other cancelled check is
|
|
46
|
+
# red, and a cancelled tier is re-run up to its third attempt, as before.
|
|
39
47
|
# The cell goes green again once nothing is red.
|
|
40
48
|
#
|
|
41
49
|
# Why the conflict rebuild: when the three bots fired within minutes of each
|
|
@@ -111,6 +119,14 @@ jobs:
|
|
|
111
119
|
WRITE_TOKEN: ${{ secrets.CI_DISPATCH_TOKEN }}
|
|
112
120
|
REPO: ${{ github.repository }}
|
|
113
121
|
BASE: ${{ inputs.base || 'main' }}
|
|
122
|
+
# A run integration.yml cancelled because the host was too contended
|
|
123
|
+
# to judge it carries this annotation title on its job. Such a run is
|
|
124
|
+
# not yet judged: re-run with backoff, up to the attempt budget.
|
|
125
|
+
# Repo variables, so the verbatim copy needs no edit; unset or not a
|
|
126
|
+
# number takes the default (4 attempts, 30 minutes doubling).
|
|
127
|
+
CONTENDED_MARK: ctl-contention-uncertain
|
|
128
|
+
CONTENDED_ATTEMPTS: ${{ vars.BOT_PRS_CONTENDED_ATTEMPTS }}
|
|
129
|
+
CONTENDED_BACKOFF_MINUTES: ${{ vars.BOT_PRS_CONTENDED_BACKOFF_MINUTES }}
|
|
114
130
|
run: |
|
|
115
131
|
set -euo pipefail
|
|
116
132
|
api="https://api.github.com/repos/$REPO"
|
|
@@ -181,9 +197,17 @@ jobs:
|
|
|
181
197
|
# guides queue on one concurrency group per repo and GitHub keeps one
|
|
182
198
|
# pending run per group, cancelling the older, so three bot PRs opening
|
|
183
199
|
# within the hour can cancel a guides run with nothing wrong in it.
|
|
200
|
+
# CONTENDED (optional): the run ids contended_runs found. A cancelled
|
|
201
|
+
# tier among them was never judged: it is re-run once the backoff
|
|
202
|
+
# since its cancel has passed (CONTENDED_BACKOFF_MINUTES, doubling per
|
|
203
|
+
# attempt), up to CONTENDED_ATTEMPTS attempts, then reported as still
|
|
204
|
+
# contended. It never reads ok.
|
|
184
205
|
tier_verdict() {
|
|
185
|
-
local required="$1" runs="$2" now="$3" f run status conclusion checks_age
|
|
206
|
+
local required="$1" runs="$2" now="$3" contended="${4:-}" f run id status conclusion checks_age
|
|
207
|
+
local attempt max="${CONTENDED_ATTEMPTS:-}" base="${CONTENDED_BACKOFF_MINUTES:-}" backoff age
|
|
186
208
|
local -a waiting=() failed=() rerun=()
|
|
209
|
+
[[ "$max" =~ ^[1-9][0-9]*$ ]] || max=4
|
|
210
|
+
[[ "$base" =~ ^[0-9]+$ ]] || base=30
|
|
187
211
|
checks_age="$(jq -r --argjson now "$now" '
|
|
188
212
|
[.workflow_runs[] | select((.path | split("@")[0]) == ".github/workflows/checks.yml"
|
|
189
213
|
and .event == "pull_request" and .status == "completed")] | max_by(.id)
|
|
@@ -202,8 +226,22 @@ jobs:
|
|
|
202
226
|
fi
|
|
203
227
|
status="$(jq -r .status <<<"$run")"
|
|
204
228
|
conclusion="$(jq -r '.conclusion // ""' <<<"$run")"
|
|
229
|
+
id="$(jq -r .id <<<"$run")"
|
|
230
|
+
attempt="$(jq -r '.run_attempt // 1' <<<"$run")"
|
|
205
231
|
if [ "$status" != "completed" ]; then
|
|
206
232
|
waiting+=("${f%.yml} $status")
|
|
233
|
+
elif [ "$conclusion" = "cancelled" ] && grep -qxF "$id" <<<"$contended"; then
|
|
234
|
+
if [ "$attempt" -ge "$max" ]; then
|
|
235
|
+
failed+=("${f%.yml} still contended after $attempt attempts: needs a human or a quieter runner")
|
|
236
|
+
continue
|
|
237
|
+
fi
|
|
238
|
+
backoff=$((base * 60 * (1 << (attempt - 1))))
|
|
239
|
+
age=$((now - $(jq -r '.updated_at | fromdateiso8601' <<<"$run")))
|
|
240
|
+
if [ "$age" -ge "$backoff" ]; then
|
|
241
|
+
rerun+=("$id")
|
|
242
|
+
else
|
|
243
|
+
waiting+=("${f%.yml} not yet judged, the host was contended (attempt $attempt of $max); re-running in $(((backoff - age + 59) / 60))m")
|
|
244
|
+
fi
|
|
207
245
|
elif [ "$conclusion" = "cancelled" ] && [ "$(jq -r '.run_attempt // 1' <<<"$run")" -lt 3 ]; then
|
|
208
246
|
rerun+=("$(jq -r .id <<<"$run")")
|
|
209
247
|
elif [ "$conclusion" != "success" ]; then
|
|
@@ -221,6 +259,41 @@ jobs:
|
|
|
221
259
|
fi
|
|
222
260
|
}
|
|
223
261
|
|
|
262
|
+
# contended_runs CHECK_RUNS -> the workflow run ids (one per line) that
|
|
263
|
+
# integration.yml cancelled because the host was too contended to
|
|
264
|
+
# judge them: a CANCELLED job of the run carries the CONTENDED_MARK
|
|
265
|
+
# annotation. The marker on a job that failed (its cancel never
|
|
266
|
+
# landed) is not one: that run is red. Annotations it cannot read
|
|
267
|
+
# leave the run red, as every cancel was before.
|
|
268
|
+
contended_runs() {
|
|
269
|
+
local checks="$1" id notes
|
|
270
|
+
while IFS= read -r id; do
|
|
271
|
+
[ -n "$id" ] || continue
|
|
272
|
+
notes="$(gh_api "$api/check-runs/$id/annotations?per_page=100")"
|
|
273
|
+
if ! jq -e 'type == "array"' >/dev/null 2>&1 <<<"$notes"; then
|
|
274
|
+
echo "::warning::could not read the annotations of check run $id; a cancel there counts as red" >&2
|
|
275
|
+
continue
|
|
276
|
+
fi
|
|
277
|
+
jq -e --arg m "$CONTENDED_MARK" 'any(.[]; .title == $m)' >/dev/null <<<"$notes" || continue
|
|
278
|
+
jq -r --argjson id "$id" '.check_runs[] | select(.id == $id)
|
|
279
|
+
| (.details_url // "") | (capture("/actions/runs/(?<r>[0-9]+)").r // empty)' <<<"$checks"
|
|
280
|
+
done < <(jq -r '.check_runs[] | select(.conclusion == "cancelled" and (.output.annotations_count // 0) > 0) | .id' <<<"$checks") | sort -u
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
# failed_checks CHECK_RUNS CONTENDED -> the names of the checks that
|
|
284
|
+
# failed, comma separated. A cancelled job of a contended run (every
|
|
285
|
+
# one of them: GitHub cancels the run's other jobs with it) is left to
|
|
286
|
+
# tier_verdict, which re-runs it; any other cancel is red, as before.
|
|
287
|
+
failed_checks() {
|
|
288
|
+
jq -r --arg c "$2" '($c | split("\n") | map(select(. != ""))) as $contended
|
|
289
|
+
| [.check_runs[]
|
|
290
|
+
| select(.conclusion | IN("failure","cancelled","timed_out","action_required","startup_failure"))
|
|
291
|
+
| select(.conclusion != "cancelled"
|
|
292
|
+
or (((.details_url // "") | (capture("/actions/runs/(?<r>[0-9]+)").r // "")) as $r
|
|
293
|
+
| any($contended[]; . == $r) | not))
|
|
294
|
+
| .name] | unique | join(", ")' <<<"$1"
|
|
295
|
+
}
|
|
296
|
+
|
|
224
297
|
# The workflow that rebuilds each bot branch: bump.yml, for the retired
|
|
225
298
|
# bots' branches too (their workflows went when bump.yml arrived, and
|
|
226
299
|
# bump.yml supersedes their PRs for every component it moves).
|
|
@@ -276,7 +349,8 @@ jobs:
|
|
|
276
349
|
fi
|
|
277
350
|
runs="$(gh_api "$api/commits/$sha/check-runs?per_page=100")"
|
|
278
351
|
expect "reading checks for #$n" '.check_runs | type == "array"' "$runs"
|
|
279
|
-
|
|
352
|
+
contended="$(contended_runs "$runs")"
|
|
353
|
+
failed="$(failed_checks "$runs" "$contended")"
|
|
280
354
|
pending="$(jq -r '[.check_runs[] | select(.status != "completed")] | length' <<<"$runs")"
|
|
281
355
|
passed="$(jq -r '[.check_runs[] | select(.conclusion == "success")] | length' <<<"$runs")"
|
|
282
356
|
if [ -n "$failed" ]; then red+=("#$n $ref: failing $failed"); continue; fi
|
|
@@ -286,14 +360,18 @@ jobs:
|
|
|
286
360
|
required="$(tiers_for "$sha")"
|
|
287
361
|
wf_runs="$(gh_api "$api/actions/runs?head_sha=$sha&per_page=100")"
|
|
288
362
|
expect "reading workflow runs for #$n" '.workflow_runs | type == "array"' "$wf_runs"
|
|
289
|
-
verdict="$(tier_verdict "$required" "$wf_runs" "$(date +%s)")"
|
|
363
|
+
verdict="$(tier_verdict "$required" "$wf_runs" "$(date +%s)" "$contended")"
|
|
290
364
|
case "$verdict" in
|
|
291
365
|
ok) ;;
|
|
292
366
|
red\ *) red+=("#$n $ref: ${verdict#red }"); continue ;;
|
|
293
367
|
rerun\ *)
|
|
294
368
|
for id in ${verdict#rerun }; do
|
|
295
369
|
code="$(gh_actions -o /dev/null -w '%{http_code}' -X POST "$api/actions/runs/$id/rerun")"
|
|
296
|
-
|
|
370
|
+
if grep -qxF "$id" <<<"$contended"; then
|
|
371
|
+
echo "#$n $ref: a gating tier (run $id) was cancelled as uncertain, the host was contended; re-running it (HTTP $code)"
|
|
372
|
+
else
|
|
373
|
+
echo "#$n $ref: a gating tier (run $id) was cancelled; re-running it (HTTP $code)"
|
|
374
|
+
fi
|
|
297
375
|
done
|
|
298
376
|
continue ;;
|
|
299
377
|
*) echo "#$n $ref: ${verdict#wait }; next time"; continue ;;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# ctl-dev-kit convention: the integration suite under contention verdicts.
|
|
3
|
+
#
|
|
4
|
+
# integration.yml runs this, inside the dev shell and from the repo root, in
|
|
5
|
+
# place of `bun run test:integration` when the repo has opted in with
|
|
6
|
+
# scripts/integration/contention.json (thresholds; `{}` takes every default).
|
|
7
|
+
# It runs the suite under @norskvideo/ctl-test-harness's ctl-contention, which
|
|
8
|
+
# samples host pressure (Linux PSI) alongside it and classifies the result:
|
|
9
|
+
#
|
|
10
|
+
# exit 0 the suite passed
|
|
11
|
+
# exit N a real failure: the suite's own status, as before
|
|
12
|
+
# exit 75 uncertain: every failure overlapped host contention, or the
|
|
13
|
+
# suite was stopped early because the host was starved
|
|
14
|
+
#
|
|
15
|
+
# An uncertain attempt is rerun up to rerun.max times (default 1), once the
|
|
16
|
+
# host has gone quiet or rerun.maxWaitSec has passed. Each attempt's timeline,
|
|
17
|
+
# verdict and summary land in $NORSK_CONTENTION_DIR/attempt-<n>/, the last
|
|
18
|
+
# verdict at $NORSK_CONTENTION_DIR/verdict.json for the workflow to read.
|
|
19
|
+
#
|
|
20
|
+
# Anything missing (a harness without ctl-contention, a config it cannot
|
|
21
|
+
# read) runs the suite exactly as before, with a warning: opting in must
|
|
22
|
+
# never be what turns a tier red.
|
|
23
|
+
set -uo pipefail
|
|
24
|
+
|
|
25
|
+
config=scripts/integration/contention.json
|
|
26
|
+
bin=node_modules/.bin/ctl-contention
|
|
27
|
+
dir="${NORSK_CONTENTION_DIR:-test-results/contention}"
|
|
28
|
+
|
|
29
|
+
as_before() {
|
|
30
|
+
echo "::warning title=contention verdicts off::$1; the suite runs without them, exactly as before."
|
|
31
|
+
exec bun run test:integration
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
[ -x "$bin" ] || as_before "$config is present but this repo's @norskvideo/ctl-test-harness has no ctl-contention (bump it)"
|
|
35
|
+
if ! max="$("$bin" config rerun.max --config "$config")"; then
|
|
36
|
+
as_before "ctl-contention could not read $config"
|
|
37
|
+
fi
|
|
38
|
+
|
|
39
|
+
# Between attempts: the containers the last attempt launched. Its teardown may
|
|
40
|
+
# never have run (an abort kills the suite), and a held port or a leftover
|
|
41
|
+
# engine would starve or break the rerun. The same two rules the job-end reap
|
|
42
|
+
# steps use: instances the harness recorded, and anything mounting this job's
|
|
43
|
+
# test-temp.
|
|
44
|
+
reap_leftovers() {
|
|
45
|
+
command -v docker >/dev/null 2>&1 || return 0
|
|
46
|
+
local m="${NORSK_LAUNCHED_MANIFEST:-}" base="${NORSK_CTL_TEST_TMP:-}" id ids c src
|
|
47
|
+
if [ -n "$m" ] && [ -f "$m" ]; then
|
|
48
|
+
while IFS= read -r id; do
|
|
49
|
+
[ -n "$id" ] || continue
|
|
50
|
+
ids="$(docker ps -aq --filter "label=norsk-ctl.instance=$id" 2>/dev/null || true)"
|
|
51
|
+
[ -z "$ids" ] || docker rm -f $ids >/dev/null 2>&1 || true
|
|
52
|
+
done < <(sort -u "$m")
|
|
53
|
+
fi
|
|
54
|
+
[ -n "$base" ] || return 0
|
|
55
|
+
for c in $(docker ps -aq 2>/dev/null || true); do
|
|
56
|
+
src="$(docker inspect -f '{{range .Mounts}}{{println .Source}}{{end}}' "$c" 2>/dev/null)" || continue
|
|
57
|
+
if grep -q "^$base/" <<<"$src"; then docker rm -f "$c" >/dev/null 2>&1 || true; fi
|
|
58
|
+
done
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
attempt=1
|
|
62
|
+
while :; do
|
|
63
|
+
out="$dir/attempt-$attempt"
|
|
64
|
+
"$bin" run --config "$config" --out "$out" -- bun run test:integration
|
|
65
|
+
rc=$?
|
|
66
|
+
[ -f "$out/verdict.json" ] && cp "$out/verdict.json" "$dir/verdict.json"
|
|
67
|
+
if [ -n "${GITHUB_STEP_SUMMARY:-}" ] && [ -f "$out/summary.md" ]; then
|
|
68
|
+
{ echo "#### Attempt $attempt"; echo; cat "$out/summary.md"; echo; } >>"$GITHUB_STEP_SUMMARY"
|
|
69
|
+
fi
|
|
70
|
+
if [ "$rc" -ne 75 ] || [ "$attempt" -gt "$max" ]; then break; fi
|
|
71
|
+
echo "::warning title=integration attempt $attempt uncertain::the host was contended; waiting for it to go quiet, then rerunning (attempt $((attempt + 1)) of $((max + 1)))"
|
|
72
|
+
reap_leftovers
|
|
73
|
+
"$bin" wait-quiet --config "$config" || true
|
|
74
|
+
attempt=$((attempt + 1))
|
|
75
|
+
done
|
|
76
|
+
exit "$rc"
|
|
@@ -116,6 +116,89 @@ runner without the mount runs unserialised and says so in the log; a suite that
|
|
|
116
116
|
has waited an hour runs anyway with a warning. A hand-owned `integration.yml`
|
|
117
117
|
does not take the lock until it converges onto the canonical.
|
|
118
118
|
|
|
119
|
+
### Contention verdicts: a starved host is not a red
|
|
120
|
+
|
|
121
|
+
The lock stops two suites starving each other, but not everything else on the
|
|
122
|
+
host. On test2, commentary has failed with browser main threads starved for
|
|
123
|
+
6-80% of a test window, the engine 50 s behind realtime, a 0.55 s switch taking
|
|
124
|
+
5-20 s, and jobs killed mid-run by SIGTERM. Those failures are the host's, not
|
|
125
|
+
the product's. A product can opt in to having the tier say so.
|
|
126
|
+
|
|
127
|
+
**Opting in.** Add `scripts/integration/contention.json` (and a
|
|
128
|
+
`@norskvideo/ctl-test-harness` new enough to ship `ctl-contention`). `{}` takes
|
|
129
|
+
every default; any key below overrides one, and a misspelt key is an error. With
|
|
130
|
+
the file present, the canonical workflow runs `bun run test:integration` through
|
|
131
|
+
`conventions/contention-suite.sh`, which runs it under the harness's
|
|
132
|
+
`ctl-contention run`:
|
|
133
|
+
|
|
134
|
+
- A sampler reads host pressure once a second for the whole suite: Linux PSI
|
|
135
|
+
(`/proc/pressure/{cpu,memory,io}`, host-wide, falling back to the cgroup v2
|
|
136
|
+
root's `*.pressure`), the load average and `MemAvailable`. The timeline is
|
|
137
|
+
`pressure.jsonl` in the job's `integration-attempt-N` artifact.
|
|
138
|
+
- Each failing test's window is read off bun's own `(fail) ... [duration]` line
|
|
139
|
+
as it streams past, so the product's `test:integration` script needs no
|
|
140
|
+
reporter flag.
|
|
141
|
+
- When the suite fails and **every** failure overlapped contention, the run is
|
|
142
|
+
**uncertain**: the job writes a `::warning::` and a job summary (the verdict,
|
|
143
|
+
each failure with its numbers, pressure per minute), plus a
|
|
144
|
+
`::notice title=ctl-contention-uncertain::` marker, then cancels its own run.
|
|
145
|
+
A cancelled run is not reported to the CI board, and bot-prs merges only a
|
|
146
|
+
success. One failure on a quiet stretch keeps the run red, as before.
|
|
147
|
+
- On a bot PR, bot-prs reads that marker off the cancelled job's annotations
|
|
148
|
+
and treats the run as **not yet judged**: it re-runs it once a backoff has
|
|
149
|
+
passed since the cancel (30 minutes, doubling per attempt), up to 4 attempts,
|
|
150
|
+
then reports it as "still contended after N attempts: needs a human or a
|
|
151
|
+
quieter runner", distinct from a failure. Repo variables
|
|
152
|
+
`BOT_PRS_CONTENDED_ATTEMPTS` and `BOT_PRS_CONTENDED_BACKOFF_MINUTES` override
|
|
153
|
+
the budget. A run cancelled for any other reason (a person, a concurrency
|
|
154
|
+
group) is treated exactly as before.
|
|
155
|
+
- If the host is starved early in the suite, or for a long stretch anywhere, the
|
|
156
|
+
suite is stopped there rather than run for 40 minutes to an uncertain end.
|
|
157
|
+
- An uncertain attempt is rerun in the same job (`rerun.max`, default 1), after
|
|
158
|
+
waiting up to `rerun.maxWaitSec` for the host to go quiet.
|
|
159
|
+
|
|
160
|
+
**When nothing changes.** No `contention.json`: the suite runs exactly as it did.
|
|
161
|
+
No readable PSI (an old kernel, `psi=0`, a runner that cannot read it): the
|
|
162
|
+
summary says "pressure unavailable" and every verdict is the suite's own. A
|
|
163
|
+
harness without `ctl-contention`, or a config it cannot read: a warning, then the
|
|
164
|
+
suite runs as before. Every job prints a "Probe whether this runner can read
|
|
165
|
+
host pressure (PSI)" step whether or not the repo has opted in, so a runner's
|
|
166
|
+
first log answers whether the signal exists there.
|
|
167
|
+
|
|
168
|
+
**Defaults** (`classify`, judged on the failure's window widened by `marginSec`):
|
|
169
|
+
|
|
170
|
+
| Key | Default | Why |
|
|
171
|
+
| -------------------------- | ------- | ---------------------------------------------------------------------------------------------------------------------- |
|
|
172
|
+
| `classify.cpuSomeMean` | 0.25 | cpu `some` bounds any one thread's wait from above; passing runs saw at most ~5% browser starvation, so 25% is 5x that |
|
|
173
|
+
| `classify.cpuSomePeak` | 0.60 | over any `peakWindowSec` (10 s): a stall long enough to break a seconds-scale assertion that a long test's mean hides |
|
|
174
|
+
| `classify.memSomeMean` | 0.10 | reclaim as a constant cost of running |
|
|
175
|
+
| `classify.memFullPeak` | 0.05 | half a second in any 10 in which nothing ran at all |
|
|
176
|
+
| `classify.minCoverage` | 0.5 | a window the sampler barely saw is not judged (and so stays red) |
|
|
177
|
+
| `classify.killWindowSec` | 120 | a suite killed from outside is judged on its last two minutes |
|
|
178
|
+
| `abort.cpuSome` | 0.70 | mean over the rule's window; with `memSome` 0.30 and `memFull` 0.10 |
|
|
179
|
+
| `abort.earlyWindowSec` | 60 | starved for a minute within the first `earlyWithinSec` (300): stop |
|
|
180
|
+
| `abort.sustainedWindowSec` | 300 | starved for five minutes anywhere: stop |
|
|
181
|
+
| `rerun.max` | 1 | reruns after an uncertain attempt; 0 turns them off |
|
|
182
|
+
|
|
183
|
+
Calling a real failure uncertain costs a rerun; calling a starved one red costs a
|
|
184
|
+
human an investigation of nothing. The bars are set where the first is unlikely,
|
|
185
|
+
and they are config so test2's recorded timelines can move them.
|
|
186
|
+
|
|
187
|
+
**From a test.** `hostContendedDuring({ fromMs, toMs })` from
|
|
188
|
+
`@norskvideo/ctl-test-harness/contention` answers for any window of the running
|
|
189
|
+
suite (it reads the timeline the wrapper names in `NORSK_CONTENTION_TIMELINE`),
|
|
190
|
+
with the numbers. Outside the wrapper it answers "unavailable", never
|
|
191
|
+
"contended".
|
|
192
|
+
|
|
193
|
+
**Exit statuses** of `ctl-contention run`: 0 pass; the suite's own status on a
|
|
194
|
+
real failure; 75 (EX_TEMPFAIL) when uncertain, which the workflow believes only
|
|
195
|
+
alongside a `verdict.json` that says so.
|
|
196
|
+
|
|
197
|
+
**Caveats.** A `ctl-candidate` dispatch that is cancelled is still scored red by
|
|
198
|
+
release-collect (the gate is advisory). bot-prs re-runs an uncertain cancel
|
|
199
|
+
only while the annotation is readable with the job token (`checks: read`); if
|
|
200
|
+
it cannot read it, the cancel counts as red, as every cancel did before.
|
|
201
|
+
|
|
119
202
|
## Reaching an image product's control plane
|
|
120
203
|
|
|
121
204
|
`product add --image` loopback-publishes the control plane on a
|
|
@@ -255,7 +338,9 @@ NON-PRODUCTION` warning is expected in CI, not an error. The media engine's
|
|
|
255
338
|
(`conventions/integration.yml`) and drift-gated like `smoke.yml`, with the same
|
|
256
339
|
two per-repo lines: the `product:` dashboard key and the matrix `runner:` label
|
|
257
340
|
array. Everything else a repo used to hand-edit into the workflow now lives in
|
|
258
|
-
three optional hooks the workflow calls if they are executable
|
|
341
|
+
three optional hooks the workflow calls if they are executable (and one opt-in
|
|
342
|
+
file, `scripts/integration/contention.json`, described under "Contention
|
|
343
|
+
verdicts" below):
|
|
259
344
|
|
|
260
345
|
| Hook | When | What goes there |
|
|
261
346
|
| -------------------------------------- | --------------------------------- | ----------------------------------------------------------------------------------------- |
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
# green, and ctl's (advisory) release gate, whose ctl-candidate dispatch runs it
|
|
18
18
|
# and shows the result on the CI board.
|
|
19
19
|
#
|
|
20
|
-
# Per-repo variation lives in
|
|
20
|
+
# Per-repo variation lives in four optional hooks, not in this file:
|
|
21
21
|
# scripts/integration/prepull-extra.sh extra images the suite launches (a
|
|
22
22
|
# driver, a sink, a sidecar)
|
|
23
23
|
# scripts/integration/prepare.sh built before the suite runs (a
|
|
@@ -26,6 +26,11 @@
|
|
|
26
26
|
# scripts/integration/verify.sh after the suite; absent, a product
|
|
27
27
|
# with tests/demo.spec.ts gets
|
|
28
28
|
# `bun run demo -- check`
|
|
29
|
+
# scripts/integration/contention.json opts the suite into contention verdicts
|
|
30
|
+
# (thresholds; {} takes the defaults): a
|
|
31
|
+
# failure the starved HOST caused cancels
|
|
32
|
+
# the run instead of failing it. See
|
|
33
|
+
# contention-suite.sh and the steps below.
|
|
29
34
|
#
|
|
30
35
|
# The suite is piped into tee so its log survives as an artifact, and the step
|
|
31
36
|
# sets pipefail FIRST: the default `bash -e {0}` has none, so without it the
|
|
@@ -102,6 +107,11 @@ jobs:
|
|
|
102
107
|
include:
|
|
103
108
|
- runner: '["x64"]'
|
|
104
109
|
runs-on: ${{ fromJSON(matrix.runner) }}
|
|
110
|
+
# actions: write is for one call only: the last step cancelling this run
|
|
111
|
+
# when the host was too contended to judge it.
|
|
112
|
+
permissions:
|
|
113
|
+
contents: read
|
|
114
|
+
actions: write
|
|
105
115
|
steps:
|
|
106
116
|
# Runs BEFORE checkout, and does two things: points the harness's temp base
|
|
107
117
|
# out of the workspace for every later step in this job, and clears whatever
|
|
@@ -192,6 +202,38 @@ jobs:
|
|
|
192
202
|
echo "NORSK_RUNNER_OWNS_DOCKER=true" >> "$GITHUB_ENV"
|
|
193
203
|
fi
|
|
194
204
|
|
|
205
|
+
# Contention verdicts (scripts/integration/contention.json) read host
|
|
206
|
+
# pressure from Linux PSI, and whether a runner can is a property of its
|
|
207
|
+
# kernel (CONFIG_PSI, the psi= boot flag) and of its container (cgroup v2,
|
|
208
|
+
# what is masked). This says which, on every run and whether or not the
|
|
209
|
+
# repo has opted in, so the log answers "does the signal exist on this
|
|
210
|
+
# host?" before anything depends on it. It only reports.
|
|
211
|
+
- name: Probe whether this runner can read host pressure (PSI)
|
|
212
|
+
run: |
|
|
213
|
+
set -uo pipefail
|
|
214
|
+
echo "host=$(hostname) kernel=$(uname -r) cpus=$(nproc) of $(grep -c ^processor /proc/cpuinfo 2>/dev/null || echo '?')"
|
|
215
|
+
echo "loadavg: $(cat /proc/loadavg 2>/dev/null || echo unreadable)"
|
|
216
|
+
grep -E '^(MemTotal|MemAvailable):' /proc/meminfo 2>/dev/null || true
|
|
217
|
+
readable=""
|
|
218
|
+
for f in /proc/pressure/cpu /proc/pressure/memory /proc/pressure/io /sys/fs/cgroup/cpu.pressure /sys/fs/cgroup/memory.pressure; do
|
|
219
|
+
if out="$(cat "$f" 2>&1)" && grep -q '^some avg10=' <<<"$out"; then
|
|
220
|
+
echo "$f: readable"
|
|
221
|
+
sed 's/^/ /' <<<"$out"
|
|
222
|
+
readable="$readable $f"
|
|
223
|
+
else
|
|
224
|
+
echo "$f: NOT readable (${out:-empty})"
|
|
225
|
+
fi
|
|
226
|
+
done
|
|
227
|
+
if [ -f /sys/fs/cgroup/cgroup.controllers ]; then echo "cgroup v2 at /sys/fs/cgroup; this process: $(cat /proc/self/cgroup 2>/dev/null)"; else echo "no cgroup v2 at /sys/fs/cgroup"; fi
|
|
228
|
+
case "$readable" in
|
|
229
|
+
*"/proc/pressure/cpu"*"/proc/pressure/memory"*) verdict="host-wide PSI readable (/proc/pressure)" ;;
|
|
230
|
+
*"/sys/fs/cgroup/cpu.pressure"*"/sys/fs/cgroup/memory.pressure"*) verdict="only this cgroup's PSI readable: blind to the engine containers, which are host siblings" ;;
|
|
231
|
+
*) verdict="PSI unavailable: contention verdicts would change nothing here" ;;
|
|
232
|
+
esac
|
|
233
|
+
if [ -f scripts/integration/contention.json ]; then optin="on (scripts/integration/contention.json)"; else optin="off (no scripts/integration/contention.json)"; fi
|
|
234
|
+
echo "PSI on $(hostname): $verdict; contention verdicts $optin"
|
|
235
|
+
echo "PSI on \`$(hostname)\`: $verdict; contention verdicts $optin" >> "$GITHUB_STEP_SUMMARY"
|
|
236
|
+
|
|
195
237
|
# Integration needs this host's docker to be able to create a network: the
|
|
196
238
|
# harness makes norsk-net and a network per instance. On 2026-10-02
|
|
197
239
|
# ashton-nuc's filter table had lost DOCKER-ISOLATION-STAGE-2, so every
|
|
@@ -409,6 +451,9 @@ jobs:
|
|
|
409
451
|
# and the first artifact built that way carried ten of another product's
|
|
410
452
|
# instances alongside its own three. Same shape the test-temp base uses.
|
|
411
453
|
NORSK_CI_LOG_DIR: ${{ runner.temp }}/norsk-logs-${{ github.run_id }}-${{ github.run_attempt }}
|
|
454
|
+
# Where contention-suite.sh puts each attempt's pressure timeline,
|
|
455
|
+
# verdict and summary, keyed like the log dir for the same reason.
|
|
456
|
+
NORSK_CONTENTION_DIR: ${{ runner.temp }}/contention-${{ github.run_id }}-${{ github.run_attempt }}
|
|
412
457
|
run: |
|
|
413
458
|
set -euo pipefail
|
|
414
459
|
suite() {
|
|
@@ -424,7 +469,11 @@ jobs:
|
|
|
424
469
|
echo "running scripts/integration/prepare.sh"
|
|
425
470
|
./scripts/integration/prepare.sh
|
|
426
471
|
fi
|
|
427
|
-
|
|
472
|
+
if [ -f scripts/integration/contention.json ]; then
|
|
473
|
+
bash node_modules/@norskvideo/ctl-dev-kit/conventions/contention-suite.sh
|
|
474
|
+
else
|
|
475
|
+
bun run test:integration
|
|
476
|
+
fi
|
|
428
477
|
if [ -x scripts/integration/verify.sh ]; then
|
|
429
478
|
echo "running scripts/integration/verify.sh"
|
|
430
479
|
./scripts/integration/verify.sh
|
|
@@ -460,7 +509,30 @@ jobs:
|
|
|
460
509
|
else
|
|
461
510
|
echo "no writable /srv/ci-scratch on this runner: the suite runs without the host lock"
|
|
462
511
|
fi
|
|
512
|
+
set +e
|
|
463
513
|
suite 9>&- 2>&1 | tee "$RUNNER_TEMP/integration.log"
|
|
514
|
+
rc=$?
|
|
515
|
+
set -e
|
|
516
|
+
# 75 is contention-suite.sh saying "uncertain": every failure
|
|
517
|
+
# overlapped host contention, or the suite was stopped because the
|
|
518
|
+
# host was starved. Believed only with the verdict file that says so,
|
|
519
|
+
# since a suite could exit 75 for its own reasons. Not a failure:
|
|
520
|
+
# the last step of this job cancels the run instead, so it reads
|
|
521
|
+
# cancelled (not red) on the board, and bot-prs re-runs it later
|
|
522
|
+
# rather than merging it or reporting it red.
|
|
523
|
+
# Every other status, a real failure on a quiet box included, exits
|
|
524
|
+
# exactly as it did. The %2C is a comma: workflow-command properties
|
|
525
|
+
# are comma separated, so a bare one cut the title short.
|
|
526
|
+
verdict="$NORSK_CONTENTION_DIR/verdict.json"
|
|
527
|
+
if [ "$rc" -eq 75 ] && [ "$(jq -r .status "$verdict" 2>/dev/null)" = uncertain ]; then
|
|
528
|
+
echo "::warning title=integration uncertain%2C the host was contended::$(jq -r .reason "$verdict"). This run will be cancelled rather than failed; the pressure timeline and per-test classification are in the job summary and the integration artifact."
|
|
529
|
+
# The marker bot-prs reads off this job's annotations to tell this
|
|
530
|
+
# cancel from any other (bot-prs-contended.test.ts).
|
|
531
|
+
echo "::notice title=ctl-contention-uncertain::cancelled as uncertain, not failed: bot-prs re-runs it once the host may be quieter"
|
|
532
|
+
echo "NORSK_CONTENTION_UNCERTAIN=1" >> "$GITHUB_ENV"
|
|
533
|
+
exit 0
|
|
534
|
+
fi
|
|
535
|
+
exit "$rc"
|
|
464
536
|
|
|
465
537
|
# The log AND the harness evidence. Playwright and the harness write
|
|
466
538
|
# screenshots, traces and captured container logs under test-results/;
|
|
@@ -475,6 +547,7 @@ jobs:
|
|
|
475
547
|
${{ runner.temp }}/integration.log
|
|
476
548
|
test-results/
|
|
477
549
|
${{ runner.temp }}/norsk-logs-${{ github.run_id }}-${{ github.run_attempt }}
|
|
550
|
+
${{ runner.temp }}/contention-${{ github.run_id }}-${{ github.run_attempt }}
|
|
478
551
|
retention-days: 7
|
|
479
552
|
if-no-files-found: warn
|
|
480
553
|
|
|
@@ -514,6 +587,31 @@ jobs:
|
|
|
514
587
|
docker rm -f $s
|
|
515
588
|
fi
|
|
516
589
|
|
|
590
|
+
# Last, so the evidence is uploaded and the host reaped first. The suite
|
|
591
|
+
# step flagged this run uncertain: its failures were the starved host's,
|
|
592
|
+
# not the product's, so it is cancelled rather than failed. A cancelled
|
|
593
|
+
# run is not reported to the CI board (notify is !cancelled()), and
|
|
594
|
+
# bot-prs merges only a success, so a bump cannot land on it (it re-runs
|
|
595
|
+
# a run whose job carries the suite step's ctl-contention-uncertain notice).
|
|
596
|
+
# Cancellation is asynchronous: the step holds the job until it lands,
|
|
597
|
+
# and if it is refused or never lands the job FAILS, because an uncertain
|
|
598
|
+
# run must never finish reading green. contention-wired.test.ts.
|
|
599
|
+
- name: Cancel this run, the host was too contended to judge it
|
|
600
|
+
if: ${{ always() && env.NORSK_CONTENTION_UNCERTAIN == '1' }}
|
|
601
|
+
env:
|
|
602
|
+
GH_TOKEN: ${{ github.token }}
|
|
603
|
+
run: |
|
|
604
|
+
set -uo pipefail
|
|
605
|
+
code="$(curl -sS -o /dev/null -w '%{http_code}' -X POST \
|
|
606
|
+
-H "Authorization: Bearer $GH_TOKEN" \
|
|
607
|
+
-H "Accept: application/vnd.github+json" \
|
|
608
|
+
"$GITHUB_API_URL/repos/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID/cancel")"
|
|
609
|
+
[ "$code" = "202" ] || { echo "::error::could not cancel this uncertain run (HTTP $code), so it fails instead: read the contention summary before treating it as a product failure"; exit 1; }
|
|
610
|
+
echo "cancel accepted; waiting for it to land"
|
|
611
|
+
sleep 300
|
|
612
|
+
echo "::error::still running five minutes after the cancel was accepted, so it fails instead: read the contention summary before treating it as a product failure"
|
|
613
|
+
exit 1
|
|
614
|
+
|
|
517
615
|
# Report this pipeline's result to the aggregated product CI dashboard
|
|
518
616
|
# (id3as/ci-workflows). !cancelled() so a real pass/fail reports, but a
|
|
519
617
|
# cancelled/superseded run does NOT — a cancellation dispatched as failure
|