@norskvideo/ctl-dev-kit 0.2.64 → 0.2.65

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.
@@ -1,456 +0,0 @@
1
- # The sdk-app variant of upgrade-latest.yml: keep a bespoke-SDK product (no
2
- # Studio -- norsk-relay) fresh against the Norsk "nightly" channel (RFC 0001 --
3
- # product version freshness). Kept in lockstep with upgrade-latest.yml outside
4
- # the bump step; check-drift holds an sdk-app repo's copy to THIS file.
5
- #
6
- # An sdk-app has no Studio, so no {engine, studio} pair to take from
7
- # norsk-studio's channel manifest and no manifest.seed.json to anchor on. Its
8
- # whole norsk family is ONE version string: the @norskvideo/norsk-sdk and
9
- # norsk-api npm packages and the norskvideo/norsk engine image are built from
10
- # one commit and published under one version. The product's own exact
11
- # norsk-sdk pin is the current value; the SDK's npm nightly is the new one, and
12
- # the step confirms norsk-api and the engine image exist at it BEFORE rewriting
13
- # anything. Then every occurrence of the old string moves, lockfile aside (bun
14
- # install regenerates it) -- including the engine image ref the product's
15
- # template launches, which an sdk-app pins in its own source.
16
- #
17
- # Nightly cron + on demand. The bump runs the product's own gate (lint,
18
- # typecheck, unit tests), then ALWAYS opens (or updates) a PR on
19
- # upgrade-latest/nightly, exactly as upgrade-latest.yml does, for bot-prs to
20
- # merge once the PR's tiers are green; it never pushes to main (see that file
21
- # for why). The branch is pushed with CI_DISPATCH_TOKEN (a PAT), so the PR's
22
- # workflows fire -- a GITHUB_TOKEN push would fire nothing. It also moves
23
- # flake.nix's ctlVersion, the norsk-ctl the product's CI tiers run, to the
24
- # `latest` channel: an sdk-app's flake fetches no daemon, so it carries the
25
- # version alone (ctl-pinned-in-repo.test.ts).
26
- #
27
- # Single-sourced in @norskvideo/ctl-dev-kit (conventions/upgrade-latest.sdk-app.yml)
28
- # and copied verbatim into each sdk-app product repo as
29
- # .github/workflows/upgrade-latest.yml; the check:drift gate fails CI if a copy
30
- # diverges. Only the `product:` value below is per-repo (set it to this repo's
31
- # dashboard key); everything else must match the dev-kit source. Never hand-edit
32
- # the copy — edit the dev-kit source and re-sync.
33
- name: upgrade-latest
34
-
35
- on:
36
- schedule:
37
- - cron: "17 4 * * *"
38
- workflow_dispatch:
39
-
40
- permissions:
41
- contents: write
42
-
43
- jobs:
44
- bump:
45
- runs-on: x64
46
- outputs:
47
- changed: ${{ steps.bump.outputs.changed }}
48
- summary: ${{ steps.bump.outputs.summary }}
49
- steps:
50
- # Runs BEFORE checkout, and does two things: points the harness's temp base
51
- # out of the workspace for every later step in this job, and clears whatever
52
- # the old repo-local default left behind on this runner. The body carries the
53
- # why. It fails the job only when legacy litter survives -- which would take
54
- # the checkout down a step later anyway, but report nothing useful.
55
- - name: Route test-temp out of the workspace (pre-checkout)
56
- run: |
57
- set -uo pipefail
58
- # These runners are docker-OUTSIDE-of-docker, so a bind-mount source the
59
- # HOST daemon has not seen before is created by dockerd, as ROOT, on the
60
- # host -- even when the launch passes --container-user (reproduced
61
- # 2026-09-04). The harness's default base is repo-local for an OrbStack
62
- # inotify constraint that binds macOS dev and nothing here, so every run
63
- # seeded the CHECKOUT with root-owned dirs a non-root `git clean -ffdx`
64
- # cannot remove. That took commentary's checks AND integration red on
65
- # 2026-09-03. Use the sanctioned override, per-run and outside the
66
- # checkout -- as 8ff17a8d did in June for one debug workflow, and stopped.
67
- base="$RUNNER_TEMP/nctt-$GITHUB_RUN_ID-$GITHUB_RUN_ATTEMPT"
68
- mkdir -p "$base"
69
- echo "NORSK_CTL_TEST_TMP=$base" >> "$GITHUB_ENV"
70
- # Yesterday's bases hold dockerd-created root dirs, so a non-root rm
71
- # EACCESes. They cannot wedge a checkout, but they do fill the disk.
72
- for old in $(find "$RUNNER_TEMP" -maxdepth 1 -name 'nctt-*' -mtime +0 -printf '%f\n' 2>/dev/null || true); do
73
- docker run --rm --user 0:0 -v "$RUNNER_TEMP":/t alpine sh -c 'rm -rf -- "/t/$1"' sh "$old" >/dev/null 2>&1 || true
74
- done
75
- # LEGACY: a runner that ran the old default still carries a workspace
76
- # test-temp, and a checkout with clean:true dies on it. Reap the holders
77
- # first -- rm cannot unlink a live mountpoint even as root (EBUSY), and a
78
- # RESTARTING leaked container re-creates the path as root seconds after
79
- # any clean. Match on mount SOURCE, not a label: the holders are untracked
80
- # by construction, because a killed run labels nothing.
81
- tt="$GITHUB_WORKSPACE/test-temp"
82
- [ -d "$tt" ] || exit 0
83
- stuck=""
84
- for c in $(docker ps -aq 2>/dev/null || true); do
85
- if docker inspect -f '{{range .Mounts}}{{println .Source}}{{end}}' "$c" 2>/dev/null | grep -q "^$tt/"; then
86
- stuck="$stuck $c"
87
- fi
88
- done
89
- if [ -n "$stuck" ]; then
90
- echo "reaping containers holding mounts under $tt:$stuck"
91
- docker rm -f $stuck || true
92
- fi
93
- # The DIRECTORY, not just its contents: an empty but root-owned base
94
- # still EACCESes a later mkdtemp. Mounting the PARENT is what lets a root
95
- # container unlink the leaf.
96
- docker run --rm --user 0:0 -v "$GITHUB_WORKSPACE":/w alpine \
97
- sh -c 'rm -rf /w/test-temp' || rm -rf "$tt" || true
98
- # Fail HERE if it survived. The checkout fails on it either way, but
99
- # reports only an EACCES rmdir with no clue what was holding the path.
100
- if [ -e "$tt" ]; then
101
- echo "::error::$tt survived the pre-checkout clean"
102
- ls -lan "$tt" || true
103
- exit 1
104
- fi
105
-
106
- - uses: actions/checkout@v5
107
- with:
108
- token: ${{ secrets.CI_DISPATCH_TOKEN }}
109
- clean: true
110
-
111
- - name: Resolve the SDK nightly and rewrite the norsk family
112
- id: bump
113
- run: |
114
- set -euo pipefail
115
- reg="https://registry.npmjs.org"
116
- hub="https://hub.docker.com/v2/repositories/norskvideo/norsk/tags"
117
- tag_re='^[0-9]+\.[0-9]+\.[0-9]+-[0-9]{4}-[0-9]{2}-[0-9]{2}-[0-9a-f]+$'
118
- changed=0
119
- parts=()
120
-
121
- # NORSK-CTL: the version flake.nix's ctlVersion names is the daemon every
122
- # CI tier runs (ctl-pinned-in-repo.test.ts) as well as the one the dev
123
- # shell launches, so it moves here like the engine does and the bump
124
- # PR's tiers are what test it (ci-workflows docs/dashboard/
125
- # ci-fragility.md, proposal A). Resolved up front, with everything
126
- # else, so a refusal rewrites nothing.
127
- ctl_base="https://s3.eu-west-1.amazonaws.com/norsk.video/norsk-ctl"
128
- old_ctl="$( { grep -oE 'ctlVersion = "[^"]+"' flake.nix 2>/dev/null || true; } | head -1 | sed -E 's/.*"(.*)"/\1/')"
129
- [ -n "$old_ctl" ] || { echo "::error::flake.nix pins no ctlVersion. CI runs the norsk-ctl it names, so add ctlVersion = \"<version>\"; to flake.nix."; exit 1; }
130
- new_ctl="$(curl -fsSL --retry 3 --retry-all-errors --retry-delay 2 "$ctl_base/latest")"
131
- [ -n "$new_ctl" ] || { echo "::error::could not resolve norsk-ctl's latest channel"; exit 1; }
132
-
133
- # CURRENT: the product's exact norsk-sdk pin, wherever a tracked
134
- # package.json carries it (deps, devDeps or overrides). It is the one
135
- # string the family shares, so exactly one distinct value: none means
136
- # nothing to anchor on, two means a rewrite would leave half behind.
137
- old="$(git grep -hoE '"@norskvideo/norsk-sdk"[[:space:]]*:[[:space:]]*"[0-9]+\.[0-9]+\.[0-9]+-[0-9]{4}-[0-9]{2}-[0-9]{2}-[0-9a-f]+"' -- '*package.json' \
138
- | sed -E 's/.*"([^"]+)"$/\1/' | sort -u || true)"
139
- case "$(printf '%s' "$old" | grep -c .)" in
140
- 1) ;;
141
- 0) echo "::error::no exact @norskvideo/norsk-sdk nightly pin in any package.json: nothing to anchor the bump on"; exit 1 ;;
142
- *) echo "::error::@norskvideo/norsk-sdk pins disagree across package.json files:"; echo "$old"; exit 1 ;;
143
- esac
144
-
145
- # NEW: the SDK's npm nightly. Not norsk-studio's channel manifest: that
146
- # pairs the engine with a Studio image, and an sdk-app runs no Studio,
147
- # so it would hold the product back whenever Studio's nightly stalls.
148
- new="$(curl -fsSL --retry 3 --retry-all-errors --retry-delay 2 "$reg/@norskvideo%2Fnorsk-sdk" | jq -r '."dist-tags".nightly // empty')"
149
- printf '%s' "$new" | grep -qE "$tag_re" || { echo "::error::the norsk-sdk npm nightly is not a dated nightly version: '$new'"; exit 1; }
150
-
151
- # Everything resolved and checked BEFORE anything is rewritten, so a
152
- # refusal never leaves the SDK moved and the API or the engine not.
153
- api_has="$(curl -fsSL --retry 3 --retry-all-errors --retry-delay 2 "$reg/@norskvideo%2Fnorsk-api" | jq -r --arg v "$new" '.versions[$v] | if . == null then "" else "yes" end')"
154
- [ "$api_has" = "yes" ] || { echo "::error::@norskvideo/norsk-api has no $new: the SDK nightly is not a whole family yet"; exit 1; }
155
- curl -fsS --retry 3 --retry-delay 2 -o /dev/null "$hub/$new" ||
156
- { echo "::error::the engine image norskvideo/norsk:$new is not published: not pinning an engine nobody can pull"; exit 1; }
157
-
158
- # STRANDED: the rewrite below is a literal replace of the anchor, so a
159
- # family pin that has drifted off it is invisible to this bump and to
160
- # every later one, while the job reports success. Fail and name it
161
- # rather than move it: a pin held back may be someone's decision.
162
- stranded="$(git grep -hoE '"@norskvideo/norsk-(api|sdk|auto-manager|manager-sdk)"[[:space:]]*:[[:space:]]*"[0-9]+\.[0-9]+\.[0-9]+-[0-9]{4}-[0-9]{2}-[0-9]{2}-[0-9a-f]+"' -- '*package.json' \
163
- | sed -E 's/.*"([^"]+)"$/\1/' | sort -u | grep -vxF "$old" | grep -vxF "$new" || true)"
164
- if [ -n "$stranded" ]; then
165
- echo "::error::npm norsk pins are stranded off the family: $(printf '%s' "$stranded" | tr '\n' ' ')"
166
- echo "The family (api, sdk, auto-manager, manager-sdk) and the engine image share one version, so every pin"
167
- echo "must read $new. These do not, and no later bump can reach them either. Set them to $new."
168
- exit 1
169
- fi
170
-
171
- echo "norsk family: $old -> $new"
172
- if [ "$old" != "$new" ]; then
173
- git grep -lF "$old" -- . ':!bun.lock' | xargs -r sed -i "s|$old|$new|g"
174
- changed=1
175
- parts+=("media \`$new\`")
176
- fi
177
-
178
- # --- norsk-ctl pin (flake.nix) ------------------------------------
179
- # ctlVersion, plus the per-platform hash of each platform the flake
180
- # fetches the daemon for. A Studio product's flake fetches all four; an
181
- # sdk-app's fetches none and carries the version alone. Mirrors
182
- # packages/dev-kit/build/refresh-ctl-pin.sh in the ctl monorepo.
183
- if [ "$old_ctl" != "$new_ctl" ]; then
184
- declare -A ctlplat=([x86_64-linux]=linux-x64 [aarch64-linux]=linux-arm64 [aarch64-darwin]=darwin-arm64 [x86_64-darwin]=darwin-x64)
185
- for sys in "${!ctlplat[@]}"; do
186
- p="${ctlplat[$sys]}"
187
- grep -qF "plat = \"$p\";" flake.nix || continue
188
- hex="$(curl -fsSL --retry 3 --retry-all-errors --retry-delay 2 "$ctl_base/$new_ctl/norsk-ctl-$new_ctl-$p.sha256" | awk '{print $1}')"
189
- [ -n "$hex" ] || { echo "no sha256 sidecar for $p"; exit 1; }
190
- sri="$(nix hash convert --hash-algo sha256 --to sri "$hex")"
191
- perl -0pi -e "s{(plat = \"$p\";\\s*hash = \")[^\"]*(\")}{\${1}$sri\${2}}" flake.nix
192
- done
193
- perl -0pi -e "s{(ctlVersion = \")[^\"]*(\")}{\${1}$new_ctl\${2}}" flake.nix
194
- echo "ctl: $old_ctl -> $new_ctl"
195
- changed=1
196
- parts+=("norsk-ctl \`$new_ctl\`")
197
- fi
198
-
199
- summary=""
200
- if [ ${#parts[@]} -gt 0 ]; then
201
- summary="$(printf '%s, ' "${parts[@]}")"
202
- summary="${summary%, }"
203
- fi
204
- {
205
- echo "changed=$changed"
206
- echo "summary=$summary"
207
- } >> "$GITHUB_OUTPUT"
208
-
209
- - name: Refresh the lockfile against the new pins
210
- if: steps.bump.outputs.changed == '1'
211
- # Per INSTALL ROOT: the rewrites above walk every tracked package.json,
212
- # and a manifest outside the root workspace list owns a lockfile a
213
- # root-only install never regenerates. See lockfile-reinstall.test.ts.
214
- run: |
215
- nix develop .#build --command bash -c '
216
- set -euo pipefail
217
- for lock in $(git ls-files "*bun.lock"); do
218
- (cd "$(dirname "$lock")" && bun install)
219
- done
220
- '
221
-
222
- - name: Run the product gate against the bumped pins
223
- id: gate
224
- if: steps.bump.outputs.changed == '1'
225
- # continue-on-error so a red gate can open a PR below instead of failing
226
- # the workflow -- which is why the steps after it read `outcome` and not
227
- # `conclusion` (continue-on-error rewrites conclusion to success).
228
- continue-on-error: true
229
- run: |
230
- nix develop .#build --command bash -c '
231
- set -euo pipefail
232
- bun run lint
233
- bun run typecheck
234
- bun run test:unit
235
- '
236
-
237
- # The bump's only way to main is this PR. bot-prs merges it once every tier
238
- # that runs on a bot PR -- checks, smoke, integration and build-image's
239
- # guides gate, wherever this repo has them -- is green on its head
240
- # (ci-workflows docs/dashboard/ci-fragility.md, proposal B). Landing on a
241
- # green gate kept lint, typecheck and unit failures off main, but the tiers
242
- # that break on an upstream change ran only after landing, so a bad
243
- # upstream release still turned main red in every repo that took it. The
244
- # gate above still runs: its verdict heads the PR body, and a red one
245
- # shows on the PR's own checks too. Never a draft: bot-prs reads red from
246
- # the tiers, and a draft could never become ready again by PATCH.
247
- - name: Open (or update) the bump PR
248
- if: steps.bump.outputs.changed == '1'
249
- env:
250
- API_TOKEN: ${{ secrets.CI_DISPATCH_TOKEN }}
251
- GATE: ${{ steps.gate.outcome }}
252
- # Through env, never pasted into the script: the summary wraps each
253
- # version in backticks, and inside a double-quoted shell string those
254
- # ran as commands ("1.3.1-...: command not found"), dropping every
255
- # version from the commit message and PR body.
256
- SUMMARY: ${{ steps.bump.outputs.summary }}
257
- run: |
258
- set -euo pipefail
259
- branch="upgrade-latest/nightly"
260
- prefix="upgrade-latest/"
261
- title="Bump the Norsk family to the SDK nightly"
262
- repo="${{ github.repository }}"; owner="${repo%%/*}"
263
- api="https://api.github.com/repos/$repo/pulls"
264
- # An older PR of this bot's family is obsolete once this one is open:
265
- # left open they piled up (nine were closed by hand on 2026-10-03).
266
- # Best-effort: a failure here must not fail the bump.
267
- close_superseded() {
268
- local keep="$1" n
269
- auth=(-H "Authorization: Bearer $API_TOKEN" -H "Accept: application/vnd.github+json")
270
- for n in $(curl -sS "${auth[@]}" "$api?state=open&per_page=100" \
271
- | jq -r --arg repo "$repo" --arg prefix "$prefix" --argjson keep "$keep" \
272
- '.[] | select(.head.repo.full_name == $repo and (.head.ref | startswith($prefix)) and .number != $keep) | .number'); do
273
- curl -sS -o /dev/null "${auth[@]}" -X POST "https://api.github.com/repos/$repo/issues/$n/comments" \
274
- -d "$(jq -nc --arg b "Superseded by #$keep, a later run of this bot." '{body:$b}')"
275
- curl -sS -o /dev/null "${auth[@]}" -X PATCH "$api/$n" -d '{"state":"closed"}'
276
- echo "closed superseded PR #$n"
277
- done
278
- }
279
- git config user.name "github-actions[bot]"
280
- git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
281
- git checkout -B "$branch"
282
- git add -A
283
- git commit -m "chore: bump the Norsk family to the SDK nightly
284
-
285
- ${SUMMARY}
286
-
287
- The norsk-sdk / norsk-api pins and the engine image ref moved together
288
- via a global version-string replace; lockfile regenerated. flake.nix
289
- ctlVersion, the norsk-ctl CI runs, moved to the latest channel."
290
- # One branch per bump, force-pushed from today's main -- but only when
291
- # its tree changed. A red bump PR left standing is rebuilt by every run
292
- # of this bot, and pushing the same tree again would re-run every slow
293
- # tier on it for nothing.
294
- if git fetch -q origin "$branch" 2>/dev/null && [ "$(git rev-parse 'HEAD^{tree}')" = "$(git rev-parse 'FETCH_HEAD^{tree}')" ]; then
295
- echo "$branch already holds this bump; not re-pushing it"
296
- else
297
- git push -f origin "$branch"
298
- fi
299
- if [ "$GATE" = "success" ]; then
300
- verdict="Gate green: lint, typecheck and unit tests pass on the bumped pins."
301
- else
302
- verdict="**Gate red**: lint, typecheck or unit tests fail on the bumped pins. That usually means the new SDK nightly is bad, or genuinely changed something the product depends on. Fix it on this branch, or close this PR to skip the bump -- the next nightly run reopens it against whatever the channel holds then."
303
- fi
304
- body="$(printf '%s\n' \
305
- "Automated nightly pin bump: the Norsk SDK nightly and norsk-ctl latest moved, and the pins came with them." \
306
- "" \
307
- " ${SUMMARY}" \
308
- "" \
309
- "$verdict" \
310
- "" \
311
- "bot-prs merges this once checks, smoke, integration and the guides gate" \
312
- "(build-image) have all passed on its head, wherever this repo runs them." \
313
- "Until then main does not move; a red tier keeps it open and reports it on" \
314
- "the product dashboard.")"
315
- # gh is not on the self-hosted runner PATH (it lives only inside
316
- # `nix develop`, and this step runs outside it), so create/update the PR
317
- # straight against the REST API with the same PAT that pushed the branch.
318
- # The PAT (not GITHUB_TOKEN) is what lets the PR's own workflows run,
319
- # which is the whole point: the bump is judged before it lands.
320
- curl_api=(--fail-with-body -sS -H "Authorization: Bearer $API_TOKEN" -H "Accept: application/vnd.github+json")
321
- existing="$(curl "${curl_api[@]}" "$api?head=$owner:$branch&state=open" | jq -r '.[0].number // empty')"
322
- if [ -n "$existing" ]; then
323
- curl "${curl_api[@]}" -X PATCH "$api/$existing" \
324
- --data "$(jq -nc --arg t "$title" --arg b "$body" '{title:$t, body:$b}')" > /dev/null
325
- number="$existing"
326
- echo "updated PR #$number"
327
- else
328
- number="$(curl "${curl_api[@]}" -X POST "$api" \
329
- --data "$(jq -nc --arg t "$title" --arg b "$body" --arg h "$branch" \
330
- '{title:$t, body:$b, head:$h, base:"main"}')" | jq -r .number)"
331
- echo "opened PR #$number"
332
- fi
333
- close_superseded "$number" || true
334
-
335
- - name: Report "already fresh"
336
- if: steps.bump.outputs.changed != '1'
337
- run: echo "Nightly pins already current — nothing to bump."
338
- - name: Reap the containers this job launched
339
- # Whatever the tests did. A teardown cut short by a test timeout leaves
340
- # its instance running on the shared Docker daemon (see JOB_REAP_STEP in
341
- # conventions/workflow-shell.ts); everything this job launched mounts its
342
- # own test-temp, so that is what goes.
343
- if: always()
344
- run: |
345
- base="${NORSK_CTL_TEST_TMP:-}"
346
- tt="$GITHUB_WORKSPACE/test-temp"
347
- s=""
348
- for c in $(docker ps -aq); do
349
- src="$(docker inspect -f '{{range .Mounts}}{{println .Source}}{{end}}' "$c" 2>/dev/null)" || continue
350
- if { [ -n "$base" ] && grep -q "^$base/" <<<"$src"; } || grep -q "^$tt/" <<<"$src"; then s="$s $c"; fi
351
- done
352
- if [ -n "$s" ]; then
353
- echo "::warning::removing containers this job left running:$s"
354
- docker rm -f $s
355
- fi
356
-
357
- # Report the nightly bump result to the aggregated product CI dashboard
358
- # (id3as/ci-workflows). !cancelled() so a failed bump shows a sad pony (a
359
- # cancelled/superseded run must not false-red the board); the summary rides
360
- # along as the cell detail.
361
- notify:
362
- needs: bump
363
- if: ${{ !cancelled() }}
364
- runs-on: x64
365
- steps:
366
- # Runs BEFORE checkout, and does two things: points the harness's temp base
367
- # out of the workspace for every later step in this job, and clears whatever
368
- # the old repo-local default left behind on this runner. The body carries the
369
- # why. It fails the job only when legacy litter survives -- which would take
370
- # the checkout down a step later anyway, but report nothing useful.
371
- - name: Route test-temp out of the workspace (pre-checkout)
372
- run: |
373
- set -uo pipefail
374
- # These runners are docker-OUTSIDE-of-docker, so a bind-mount source the
375
- # HOST daemon has not seen before is created by dockerd, as ROOT, on the
376
- # host -- even when the launch passes --container-user (reproduced
377
- # 2026-09-04). The harness's default base is repo-local for an OrbStack
378
- # inotify constraint that binds macOS dev and nothing here, so every run
379
- # seeded the CHECKOUT with root-owned dirs a non-root `git clean -ffdx`
380
- # cannot remove. That took commentary's checks AND integration red on
381
- # 2026-09-03. Use the sanctioned override, per-run and outside the
382
- # checkout -- as 8ff17a8d did in June for one debug workflow, and stopped.
383
- base="$RUNNER_TEMP/nctt-$GITHUB_RUN_ID-$GITHUB_RUN_ATTEMPT"
384
- mkdir -p "$base"
385
- echo "NORSK_CTL_TEST_TMP=$base" >> "$GITHUB_ENV"
386
- # Yesterday's bases hold dockerd-created root dirs, so a non-root rm
387
- # EACCESes. They cannot wedge a checkout, but they do fill the disk.
388
- for old in $(find "$RUNNER_TEMP" -maxdepth 1 -name 'nctt-*' -mtime +0 -printf '%f\n' 2>/dev/null || true); do
389
- docker run --rm --user 0:0 -v "$RUNNER_TEMP":/t alpine sh -c 'rm -rf -- "/t/$1"' sh "$old" >/dev/null 2>&1 || true
390
- done
391
- # LEGACY: a runner that ran the old default still carries a workspace
392
- # test-temp, and a checkout with clean:true dies on it. Reap the holders
393
- # first -- rm cannot unlink a live mountpoint even as root (EBUSY), and a
394
- # RESTARTING leaked container re-creates the path as root seconds after
395
- # any clean. Match on mount SOURCE, not a label: the holders are untracked
396
- # by construction, because a killed run labels nothing.
397
- tt="$GITHUB_WORKSPACE/test-temp"
398
- [ -d "$tt" ] || exit 0
399
- stuck=""
400
- for c in $(docker ps -aq 2>/dev/null || true); do
401
- if docker inspect -f '{{range .Mounts}}{{println .Source}}{{end}}' "$c" 2>/dev/null | grep -q "^$tt/"; then
402
- stuck="$stuck $c"
403
- fi
404
- done
405
- if [ -n "$stuck" ]; then
406
- echo "reaping containers holding mounts under $tt:$stuck"
407
- docker rm -f $stuck || true
408
- fi
409
- # The DIRECTORY, not just its contents: an empty but root-owned base
410
- # still EACCESes a later mkdtemp. Mounting the PARENT is what lets a root
411
- # container unlink the leaf.
412
- docker run --rm --user 0:0 -v "$GITHUB_WORKSPACE":/w alpine \
413
- sh -c 'rm -rf /w/test-temp' || rm -rf "$tt" || true
414
- # Fail HERE if it survived. The checkout fails on it either way, but
415
- # reports only an EACCES rmdir with no clue what was holding the path.
416
- if [ -e "$tt" ]; then
417
- echo "::error::$tt survived the pre-checkout clean"
418
- ls -lan "$tt" || true
419
- exit 1
420
- fi
421
-
422
- - uses: actions/checkout@v5
423
- with:
424
- clean: true
425
- - id: meta
426
- run: |
427
- if [ "${{ contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') }}" = "true" ]; then
428
- echo "state=failure" >> "$GITHUB_OUTPUT"
429
- else
430
- echo "state=success" >> "$GITHUB_OUTPUT"
431
- fi
432
- - uses: ./.github/actions/ci-status-dispatch
433
- with:
434
- token: ${{ secrets.CI_DISPATCH_TOKEN }}
435
- product: __PRODUCT__
436
- pipeline: nightly
437
- status: ${{ steps.meta.outputs.state }}
438
- detail: ${{ needs.bump.outputs.summary }}
439
- - name: Reap the containers this job launched
440
- # Whatever the tests did. A teardown cut short by a test timeout leaves
441
- # its instance running on the shared Docker daemon (see JOB_REAP_STEP in
442
- # conventions/workflow-shell.ts); everything this job launched mounts its
443
- # own test-temp, so that is what goes.
444
- if: always()
445
- run: |
446
- base="${NORSK_CTL_TEST_TMP:-}"
447
- tt="$GITHUB_WORKSPACE/test-temp"
448
- s=""
449
- for c in $(docker ps -aq); do
450
- src="$(docker inspect -f '{{range .Mounts}}{{println .Source}}{{end}}' "$c" 2>/dev/null)" || continue
451
- if { [ -n "$base" ] && grep -q "^$base/" <<<"$src"; } || grep -q "^$tt/" <<<"$src"; then s="$s $c"; fi
452
- done
453
- if [ -n "$s" ]; then
454
- echo "::warning::removing containers this job left running:$s"
455
- docker rm -f $s
456
- fi