@norskvideo/ctl-dev-kit 0.2.100 → 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.
@@ -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
- failed="$(jq -r '[.check_runs[] | select(.conclusion | IN("failure","cancelled","timed_out","action_required","startup_failure")) | .name] | unique | join(", ")' <<<"$runs")"
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
- echo "#$n $ref: a gating tier (run $id) was cancelled; re-running it (HTTP $code)"
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 ;;
@@ -140,9 +140,18 @@ the file present, the canonical workflow runs `bun run test:integration` through
140
140
  reporter flag.
141
141
  - When the suite fails and **every** failure overlapped contention, the run is
142
142
  **uncertain**: the job writes a `::warning::` and a job summary (the verdict,
143
- each failure with its numbers, pressure per minute), then cancels its own run.
143
+ each failure with its numbers, pressure per minute), plus a
144
+ `::notice title=ctl-contention-uncertain::` marker, then cancels its own run.
144
145
  A cancelled run is not reported to the CI board, and bot-prs merges only a
145
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.
146
155
  - If the host is starved early in the suite, or for a long stretch anywhere, the
147
156
  suite is stopped there rather than run for 40 minutes to an uncertain end.
148
157
  - An uncertain attempt is rerun in the same job (`rerun.max`, default 1), after
@@ -186,10 +195,9 @@ real failure; 75 (EX_TEMPFAIL) when uncertain, which the workflow believes only
186
195
  alongside a `verdict.json` that says so.
187
196
 
188
197
  **Caveats.** A `ctl-candidate` dispatch that is cancelled is still scored red by
189
- release-collect (the gate is advisory). bot-prs reports a PR whose integration
190
- check was cancelled as needing a human rather than re-running it, because its
191
- check-run filter counts `cancelled` before the tier rerun logic sees it; the
192
- in-job rerun is what retries.
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.
193
201
 
194
202
  ## Reaching an image product's control plane
195
203
 
@@ -518,12 +518,17 @@ jobs:
518
518
  # host was starved. Believed only with the verdict file that says so,
519
519
  # since a suite could exit 75 for its own reasons. Not a failure:
520
520
  # the last step of this job cancels the run instead, so it reads
521
- # cancelled (not red) on the board and bot-prs does not merge it.
521
+ # cancelled (not red) on the board, and bot-prs re-runs it later
522
+ # rather than merging it or reporting it red.
522
523
  # Every other status, a real failure on a quiet box included, exits
523
- # exactly as it did.
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.
524
526
  verdict="$NORSK_CONTENTION_DIR/verdict.json"
525
527
  if [ "$rc" -eq 75 ] && [ "$(jq -r .status "$verdict" 2>/dev/null)" = uncertain ]; then
526
- echo "::warning title=integration uncertain, 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."
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"
527
532
  echo "NORSK_CONTENTION_UNCERTAIN=1" >> "$GITHUB_ENV"
528
533
  exit 0
529
534
  fi
@@ -586,7 +591,8 @@ jobs:
586
591
  # step flagged this run uncertain: its failures were the starved host's,
587
592
  # not the product's, so it is cancelled rather than failed. A cancelled
588
593
  # run is not reported to the CI board (notify is !cancelled()), and
589
- # bot-prs merges only a success, so a bump cannot land on it.
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).
590
596
  # Cancellation is asynchronous: the step holds the job until it lands,
591
597
  # and if it is refused or never lands the job FAILS, because an uncertain
592
598
  # run must never finish reading green. contention-wired.test.ts.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@norskvideo/ctl-dev-kit",
3
- "version": "0.2.100",
3
+ "version": "0.2.101",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  "./create-product": "./create-product/create-product.ts",