@norskvideo/ctl-dev-kit 0.1.85 → 0.1.87

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.
@@ -52,17 +52,61 @@ jobs:
52
52
  - runner: '["ar-arm-vm"]'
53
53
  runs-on: ${{ fromJSON(matrix.runner) }}
54
54
  steps:
55
- # BEFORE checkout: launched instances leave root-owned bind-mount target
56
- # dirs under test-temp/ that the non-root runner user cannot remove, so
57
- # actions/checkout's own cleanup would fail the job before anything runs.
58
- # Nuke them from a throwaway root container first. Best-effort.
59
- - name: Clear stale root-owned test-temp (pre-checkout)
55
+ # Runs BEFORE checkout, and does two things: points the harness's temp base
56
+ # out of the workspace for every later step in this job, and clears whatever
57
+ # the old repo-local default left behind on this runner. The body carries the
58
+ # why. It fails the job only when legacy litter survives -- which would take
59
+ # the checkout down a step later anyway, but report nothing useful.
60
+ - name: Route test-temp out of the workspace (pre-checkout)
60
61
  run: |
61
62
  set -uo pipefail
63
+ # These runners are docker-OUTSIDE-of-docker, so a bind-mount source the
64
+ # HOST daemon has not seen before is created by dockerd, as ROOT, on the
65
+ # host -- even when the launch passes --container-user (reproduced
66
+ # 2026-09-04). The harness's default base is repo-local for an OrbStack
67
+ # inotify constraint that binds macOS dev and nothing here, so every run
68
+ # seeded the CHECKOUT with root-owned dirs a non-root `git clean -ffdx`
69
+ # cannot remove. That took commentary's checks AND integration red on
70
+ # 2026-09-03. Use the sanctioned override, per-run and outside the
71
+ # checkout -- as 8ff17a8d did in June for one debug workflow, and stopped.
72
+ base="$RUNNER_TEMP/nctt-$GITHUB_RUN_ID-$GITHUB_RUN_ATTEMPT"
73
+ mkdir -p "$base"
74
+ echo "NORSK_CTL_TEST_TMP=$base" >> "$GITHUB_ENV"
75
+ # Yesterday's bases hold dockerd-created root dirs, so a non-root rm
76
+ # EACCESes. They cannot wedge a checkout, but they do fill the disk.
77
+ for old in $(find "$RUNNER_TEMP" -maxdepth 1 -name 'nctt-*' -mtime +0 -printf '%f\n' 2>/dev/null || true); do
78
+ docker run --rm --user 0:0 -v "$RUNNER_TEMP":/t alpine sh -c 'rm -rf -- "/t/$1"' sh "$old" >/dev/null 2>&1 || true
79
+ done
80
+ # LEGACY: a runner that ran the old default still carries a workspace
81
+ # test-temp, and a checkout with clean:true dies on it. Reap the holders
82
+ # first -- rm cannot unlink a live mountpoint even as root (EBUSY), and a
83
+ # RESTARTING leaked container re-creates the path as root seconds after
84
+ # any clean. Match on mount SOURCE, not a label: the holders are untracked
85
+ # by construction, because a killed run labels nothing.
62
86
  tt="$GITHUB_WORKSPACE/test-temp"
63
87
  [ -d "$tt" ] || exit 0
64
- docker run --rm --user 0:0 -v "$tt":/t alpine sh \
65
- -c 'rm -rf /t/* /t/.[!.]* 2>/dev/null || true' || rm -rf "$tt"/* 2>/dev/null || true
88
+ stuck=""
89
+ for c in $(docker ps -aq 2>/dev/null || true); do
90
+ if docker inspect -f '{{range .Mounts}}{{println .Source}}{{end}}' "$c" 2>/dev/null | grep -q "^$tt/"; then
91
+ stuck="$stuck $c"
92
+ fi
93
+ done
94
+ if [ -n "$stuck" ]; then
95
+ echo "reaping containers holding mounts under $tt:$stuck"
96
+ docker rm -f $stuck || true
97
+ fi
98
+ # The DIRECTORY, not just its contents: an empty but root-owned base
99
+ # still EACCESes a later mkdtemp. Mounting the PARENT is what lets a root
100
+ # container unlink the leaf.
101
+ docker run --rm --user 0:0 -v "$GITHUB_WORKSPACE":/w alpine \
102
+ sh -c 'rm -rf /w/test-temp' || rm -rf "$tt" || true
103
+ # Fail HERE if it survived. The checkout fails on it either way, but
104
+ # reports only an EACCES rmdir with no clue what was holding the path.
105
+ if [ -e "$tt" ]; then
106
+ echo "::error::$tt survived the pre-checkout clean"
107
+ ls -lan "$tt" || true
108
+ exit 1
109
+ fi
66
110
 
67
111
  - uses: actions/checkout@v5
68
112
  with:
@@ -246,16 +290,61 @@ jobs:
246
290
  if: ${{ !cancelled() && github.event_name != 'repository_dispatch' }}
247
291
  runs-on: x64
248
292
  steps:
249
- # Same reason as the first checkout in this file: clear root-owned leftovers
250
- # from a root container so the checkout below can clean properly. Best-effort
251
- # by construction -- no docker, no test-temp, or a failed run all fall through.
252
- - name: Clear stale root-owned test-temp (pre-checkout)
293
+ # Runs BEFORE checkout, and does two things: points the harness's temp base
294
+ # out of the workspace for every later step in this job, and clears whatever
295
+ # the old repo-local default left behind on this runner. The body carries the
296
+ # why. It fails the job only when legacy litter survives -- which would take
297
+ # the checkout down a step later anyway, but report nothing useful.
298
+ - name: Route test-temp out of the workspace (pre-checkout)
253
299
  run: |
254
300
  set -uo pipefail
301
+ # These runners are docker-OUTSIDE-of-docker, so a bind-mount source the
302
+ # HOST daemon has not seen before is created by dockerd, as ROOT, on the
303
+ # host -- even when the launch passes --container-user (reproduced
304
+ # 2026-09-04). The harness's default base is repo-local for an OrbStack
305
+ # inotify constraint that binds macOS dev and nothing here, so every run
306
+ # seeded the CHECKOUT with root-owned dirs a non-root `git clean -ffdx`
307
+ # cannot remove. That took commentary's checks AND integration red on
308
+ # 2026-09-03. Use the sanctioned override, per-run and outside the
309
+ # checkout -- as 8ff17a8d did in June for one debug workflow, and stopped.
310
+ base="$RUNNER_TEMP/nctt-$GITHUB_RUN_ID-$GITHUB_RUN_ATTEMPT"
311
+ mkdir -p "$base"
312
+ echo "NORSK_CTL_TEST_TMP=$base" >> "$GITHUB_ENV"
313
+ # Yesterday's bases hold dockerd-created root dirs, so a non-root rm
314
+ # EACCESes. They cannot wedge a checkout, but they do fill the disk.
315
+ for old in $(find "$RUNNER_TEMP" -maxdepth 1 -name 'nctt-*' -mtime +0 -printf '%f\n' 2>/dev/null || true); do
316
+ docker run --rm --user 0:0 -v "$RUNNER_TEMP":/t alpine sh -c 'rm -rf -- "/t/$1"' sh "$old" >/dev/null 2>&1 || true
317
+ done
318
+ # LEGACY: a runner that ran the old default still carries a workspace
319
+ # test-temp, and a checkout with clean:true dies on it. Reap the holders
320
+ # first -- rm cannot unlink a live mountpoint even as root (EBUSY), and a
321
+ # RESTARTING leaked container re-creates the path as root seconds after
322
+ # any clean. Match on mount SOURCE, not a label: the holders are untracked
323
+ # by construction, because a killed run labels nothing.
255
324
  tt="$GITHUB_WORKSPACE/test-temp"
256
325
  [ -d "$tt" ] || exit 0
257
- docker run --rm --user 0:0 -v "$tt":/t alpine sh \
258
- -c 'rm -rf /t/* /t/.[!.]* 2>/dev/null || true' || rm -rf "$tt"/* 2>/dev/null || true
326
+ stuck=""
327
+ for c in $(docker ps -aq 2>/dev/null || true); do
328
+ if docker inspect -f '{{range .Mounts}}{{println .Source}}{{end}}' "$c" 2>/dev/null | grep -q "^$tt/"; then
329
+ stuck="$stuck $c"
330
+ fi
331
+ done
332
+ if [ -n "$stuck" ]; then
333
+ echo "reaping containers holding mounts under $tt:$stuck"
334
+ docker rm -f $stuck || true
335
+ fi
336
+ # The DIRECTORY, not just its contents: an empty but root-owned base
337
+ # still EACCESes a later mkdtemp. Mounting the PARENT is what lets a root
338
+ # container unlink the leaf.
339
+ docker run --rm --user 0:0 -v "$GITHUB_WORKSPACE":/w alpine \
340
+ sh -c 'rm -rf /w/test-temp' || rm -rf "$tt" || true
341
+ # Fail HERE if it survived. The checkout fails on it either way, but
342
+ # reports only an EACCES rmdir with no clue what was holding the path.
343
+ if [ -e "$tt" ]; then
344
+ echo "::error::$tt survived the pre-checkout clean"
345
+ ls -lan "$tt" || true
346
+ exit 1
347
+ fi
259
348
 
260
349
  - uses: actions/checkout@v5
261
350
  with:
@@ -41,23 +41,61 @@ jobs:
41
41
  sync:
42
42
  runs-on: x64
43
43
  steps:
44
- # Root-owned leftovers under test-temp/ are what `clean: false` used to route
45
- # around: the integration suite bind-mounts host dirs into containers, and a
46
- # non-root git clean then EACCESes on what they wrote -- taking down whatever
47
- # innocent job checked out next. Skipping the clean cured that symptom and
48
- # caused another: every gitignored artifact survived between runs, so a test
49
- # could pass on build output an earlier job left behind and go red the first
50
- # time it landed on a cold runner (probe, 2026-09-01). Clear the leftovers
51
- # from a root container FIRST, and the checkout can clean properly again.
52
- # Best-effort by construction: no docker, no test-temp, or a failed run all
53
- # fall through without failing the job.
54
- - name: Clear stale root-owned test-temp (pre-checkout)
44
+ # Runs BEFORE checkout, and does two things: points the harness's temp base
45
+ # out of the workspace for every later step in this job, and clears whatever
46
+ # the old repo-local default left behind on this runner. The body carries the
47
+ # why. It fails the job only when legacy litter survives -- which would take
48
+ # the checkout down a step later anyway, but report nothing useful.
49
+ - name: Route test-temp out of the workspace (pre-checkout)
55
50
  run: |
56
51
  set -uo pipefail
52
+ # These runners are docker-OUTSIDE-of-docker, so a bind-mount source the
53
+ # HOST daemon has not seen before is created by dockerd, as ROOT, on the
54
+ # host -- even when the launch passes --container-user (reproduced
55
+ # 2026-09-04). The harness's default base is repo-local for an OrbStack
56
+ # inotify constraint that binds macOS dev and nothing here, so every run
57
+ # seeded the CHECKOUT with root-owned dirs a non-root `git clean -ffdx`
58
+ # cannot remove. That took commentary's checks AND integration red on
59
+ # 2026-09-03. Use the sanctioned override, per-run and outside the
60
+ # checkout -- as 8ff17a8d did in June for one debug workflow, and stopped.
61
+ base="$RUNNER_TEMP/nctt-$GITHUB_RUN_ID-$GITHUB_RUN_ATTEMPT"
62
+ mkdir -p "$base"
63
+ echo "NORSK_CTL_TEST_TMP=$base" >> "$GITHUB_ENV"
64
+ # Yesterday's bases hold dockerd-created root dirs, so a non-root rm
65
+ # EACCESes. They cannot wedge a checkout, but they do fill the disk.
66
+ for old in $(find "$RUNNER_TEMP" -maxdepth 1 -name 'nctt-*' -mtime +0 -printf '%f\n' 2>/dev/null || true); do
67
+ docker run --rm --user 0:0 -v "$RUNNER_TEMP":/t alpine sh -c 'rm -rf -- "/t/$1"' sh "$old" >/dev/null 2>&1 || true
68
+ done
69
+ # LEGACY: a runner that ran the old default still carries a workspace
70
+ # test-temp, and a checkout with clean:true dies on it. Reap the holders
71
+ # first -- rm cannot unlink a live mountpoint even as root (EBUSY), and a
72
+ # RESTARTING leaked container re-creates the path as root seconds after
73
+ # any clean. Match on mount SOURCE, not a label: the holders are untracked
74
+ # by construction, because a killed run labels nothing.
57
75
  tt="$GITHUB_WORKSPACE/test-temp"
58
76
  [ -d "$tt" ] || exit 0
59
- docker run --rm --user 0:0 -v "$tt":/t alpine sh \
60
- -c 'rm -rf /t/* /t/.[!.]* 2>/dev/null || true' || rm -rf "$tt"/* 2>/dev/null || true
77
+ stuck=""
78
+ for c in $(docker ps -aq 2>/dev/null || true); do
79
+ if docker inspect -f '{{range .Mounts}}{{println .Source}}{{end}}' "$c" 2>/dev/null | grep -q "^$tt/"; then
80
+ stuck="$stuck $c"
81
+ fi
82
+ done
83
+ if [ -n "$stuck" ]; then
84
+ echo "reaping containers holding mounts under $tt:$stuck"
85
+ docker rm -f $stuck || true
86
+ fi
87
+ # The DIRECTORY, not just its contents: an empty but root-owned base
88
+ # still EACCESes a later mkdtemp. Mounting the PARENT is what lets a root
89
+ # container unlink the leaf.
90
+ docker run --rm --user 0:0 -v "$GITHUB_WORKSPACE":/w alpine \
91
+ sh -c 'rm -rf /w/test-temp' || rm -rf "$tt" || true
92
+ # Fail HERE if it survived. The checkout fails on it either way, but
93
+ # reports only an EACCES rmdir with no clue what was holding the path.
94
+ if [ -e "$tt" ]; then
95
+ echo "::error::$tt survived the pre-checkout clean"
96
+ ls -lan "$tt" || true
97
+ exit 1
98
+ fi
61
99
 
62
100
  - uses: actions/checkout@v5
63
101
  with:
@@ -43,23 +43,61 @@ jobs:
43
43
  sync:
44
44
  runs-on: x64
45
45
  steps:
46
- # Root-owned leftovers under test-temp/ are what `clean: false` used to route
47
- # around: the integration suite bind-mounts host dirs into containers, and a
48
- # non-root git clean then EACCESes on what they wrote -- taking down whatever
49
- # innocent job checked out next. Skipping the clean cured that symptom and
50
- # caused another: every gitignored artifact survived between runs, so a test
51
- # could pass on build output an earlier job left behind and go red the first
52
- # time it landed on a cold runner (probe, 2026-09-01). Clear the leftovers
53
- # from a root container FIRST, and the checkout can clean properly again.
54
- # Best-effort by construction: no docker, no test-temp, or a failed run all
55
- # fall through without failing the job.
56
- - name: Clear stale root-owned test-temp (pre-checkout)
46
+ # Runs BEFORE checkout, and does two things: points the harness's temp base
47
+ # out of the workspace for every later step in this job, and clears whatever
48
+ # the old repo-local default left behind on this runner. The body carries the
49
+ # why. It fails the job only when legacy litter survives -- which would take
50
+ # the checkout down a step later anyway, but report nothing useful.
51
+ - name: Route test-temp out of the workspace (pre-checkout)
57
52
  run: |
58
53
  set -uo pipefail
54
+ # These runners are docker-OUTSIDE-of-docker, so a bind-mount source the
55
+ # HOST daemon has not seen before is created by dockerd, as ROOT, on the
56
+ # host -- even when the launch passes --container-user (reproduced
57
+ # 2026-09-04). The harness's default base is repo-local for an OrbStack
58
+ # inotify constraint that binds macOS dev and nothing here, so every run
59
+ # seeded the CHECKOUT with root-owned dirs a non-root `git clean -ffdx`
60
+ # cannot remove. That took commentary's checks AND integration red on
61
+ # 2026-09-03. Use the sanctioned override, per-run and outside the
62
+ # checkout -- as 8ff17a8d did in June for one debug workflow, and stopped.
63
+ base="$RUNNER_TEMP/nctt-$GITHUB_RUN_ID-$GITHUB_RUN_ATTEMPT"
64
+ mkdir -p "$base"
65
+ echo "NORSK_CTL_TEST_TMP=$base" >> "$GITHUB_ENV"
66
+ # Yesterday's bases hold dockerd-created root dirs, so a non-root rm
67
+ # EACCESes. They cannot wedge a checkout, but they do fill the disk.
68
+ for old in $(find "$RUNNER_TEMP" -maxdepth 1 -name 'nctt-*' -mtime +0 -printf '%f\n' 2>/dev/null || true); do
69
+ docker run --rm --user 0:0 -v "$RUNNER_TEMP":/t alpine sh -c 'rm -rf -- "/t/$1"' sh "$old" >/dev/null 2>&1 || true
70
+ done
71
+ # LEGACY: a runner that ran the old default still carries a workspace
72
+ # test-temp, and a checkout with clean:true dies on it. Reap the holders
73
+ # first -- rm cannot unlink a live mountpoint even as root (EBUSY), and a
74
+ # RESTARTING leaked container re-creates the path as root seconds after
75
+ # any clean. Match on mount SOURCE, not a label: the holders are untracked
76
+ # by construction, because a killed run labels nothing.
59
77
  tt="$GITHUB_WORKSPACE/test-temp"
60
78
  [ -d "$tt" ] || exit 0
61
- docker run --rm --user 0:0 -v "$tt":/t alpine sh \
62
- -c 'rm -rf /t/* /t/.[!.]* 2>/dev/null || true' || rm -rf "$tt"/* 2>/dev/null || true
79
+ stuck=""
80
+ for c in $(docker ps -aq 2>/dev/null || true); do
81
+ if docker inspect -f '{{range .Mounts}}{{println .Source}}{{end}}' "$c" 2>/dev/null | grep -q "^$tt/"; then
82
+ stuck="$stuck $c"
83
+ fi
84
+ done
85
+ if [ -n "$stuck" ]; then
86
+ echo "reaping containers holding mounts under $tt:$stuck"
87
+ docker rm -f $stuck || true
88
+ fi
89
+ # The DIRECTORY, not just its contents: an empty but root-owned base
90
+ # still EACCESes a later mkdtemp. Mounting the PARENT is what lets a root
91
+ # container unlink the leaf.
92
+ docker run --rm --user 0:0 -v "$GITHUB_WORKSPACE":/w alpine \
93
+ sh -c 'rm -rf /w/test-temp' || rm -rf "$tt" || true
94
+ # Fail HERE if it survived. The checkout fails on it either way, but
95
+ # reports only an EACCES rmdir with no clue what was holding the path.
96
+ if [ -e "$tt" ]; then
97
+ echo "::error::$tt survived the pre-checkout clean"
98
+ ls -lan "$tt" || true
99
+ exit 1
100
+ fi
63
101
 
64
102
  - uses: actions/checkout@v5
65
103
  with:
@@ -201,23 +239,61 @@ jobs:
201
239
  if: ${{ !cancelled() }}
202
240
  runs-on: x64
203
241
  steps:
204
- # Root-owned leftovers under test-temp/ are what `clean: false` used to route
205
- # around: the integration suite bind-mounts host dirs into containers, and a
206
- # non-root git clean then EACCESes on what they wrote -- taking down whatever
207
- # innocent job checked out next. Skipping the clean cured that symptom and
208
- # caused another: every gitignored artifact survived between runs, so a test
209
- # could pass on build output an earlier job left behind and go red the first
210
- # time it landed on a cold runner (probe, 2026-09-01). Clear the leftovers
211
- # from a root container FIRST, and the checkout can clean properly again.
212
- # Best-effort by construction: no docker, no test-temp, or a failed run all
213
- # fall through without failing the job.
214
- - name: Clear stale root-owned test-temp (pre-checkout)
242
+ # Runs BEFORE checkout, and does two things: points the harness's temp base
243
+ # out of the workspace for every later step in this job, and clears whatever
244
+ # the old repo-local default left behind on this runner. The body carries the
245
+ # why. It fails the job only when legacy litter survives -- which would take
246
+ # the checkout down a step later anyway, but report nothing useful.
247
+ - name: Route test-temp out of the workspace (pre-checkout)
215
248
  run: |
216
249
  set -uo pipefail
250
+ # These runners are docker-OUTSIDE-of-docker, so a bind-mount source the
251
+ # HOST daemon has not seen before is created by dockerd, as ROOT, on the
252
+ # host -- even when the launch passes --container-user (reproduced
253
+ # 2026-09-04). The harness's default base is repo-local for an OrbStack
254
+ # inotify constraint that binds macOS dev and nothing here, so every run
255
+ # seeded the CHECKOUT with root-owned dirs a non-root `git clean -ffdx`
256
+ # cannot remove. That took commentary's checks AND integration red on
257
+ # 2026-09-03. Use the sanctioned override, per-run and outside the
258
+ # checkout -- as 8ff17a8d did in June for one debug workflow, and stopped.
259
+ base="$RUNNER_TEMP/nctt-$GITHUB_RUN_ID-$GITHUB_RUN_ATTEMPT"
260
+ mkdir -p "$base"
261
+ echo "NORSK_CTL_TEST_TMP=$base" >> "$GITHUB_ENV"
262
+ # Yesterday's bases hold dockerd-created root dirs, so a non-root rm
263
+ # EACCESes. They cannot wedge a checkout, but they do fill the disk.
264
+ for old in $(find "$RUNNER_TEMP" -maxdepth 1 -name 'nctt-*' -mtime +0 -printf '%f\n' 2>/dev/null || true); do
265
+ docker run --rm --user 0:0 -v "$RUNNER_TEMP":/t alpine sh -c 'rm -rf -- "/t/$1"' sh "$old" >/dev/null 2>&1 || true
266
+ done
267
+ # LEGACY: a runner that ran the old default still carries a workspace
268
+ # test-temp, and a checkout with clean:true dies on it. Reap the holders
269
+ # first -- rm cannot unlink a live mountpoint even as root (EBUSY), and a
270
+ # RESTARTING leaked container re-creates the path as root seconds after
271
+ # any clean. Match on mount SOURCE, not a label: the holders are untracked
272
+ # by construction, because a killed run labels nothing.
217
273
  tt="$GITHUB_WORKSPACE/test-temp"
218
274
  [ -d "$tt" ] || exit 0
219
- docker run --rm --user 0:0 -v "$tt":/t alpine sh \
220
- -c 'rm -rf /t/* /t/.[!.]* 2>/dev/null || true' || rm -rf "$tt"/* 2>/dev/null || true
275
+ stuck=""
276
+ for c in $(docker ps -aq 2>/dev/null || true); do
277
+ if docker inspect -f '{{range .Mounts}}{{println .Source}}{{end}}' "$c" 2>/dev/null | grep -q "^$tt/"; then
278
+ stuck="$stuck $c"
279
+ fi
280
+ done
281
+ if [ -n "$stuck" ]; then
282
+ echo "reaping containers holding mounts under $tt:$stuck"
283
+ docker rm -f $stuck || true
284
+ fi
285
+ # The DIRECTORY, not just its contents: an empty but root-owned base
286
+ # still EACCESes a later mkdtemp. Mounting the PARENT is what lets a root
287
+ # container unlink the leaf.
288
+ docker run --rm --user 0:0 -v "$GITHUB_WORKSPACE":/w alpine \
289
+ sh -c 'rm -rf /w/test-temp' || rm -rf "$tt" || true
290
+ # Fail HERE if it survived. The checkout fails on it either way, but
291
+ # reports only an EACCES rmdir with no clue what was holding the path.
292
+ if [ -e "$tt" ]; then
293
+ echo "::error::$tt survived the pre-checkout clean"
294
+ ls -lan "$tt" || true
295
+ exit 1
296
+ fi
221
297
 
222
298
  - uses: actions/checkout@v5
223
299
  with:
@@ -45,23 +45,61 @@ jobs:
45
45
  changed: ${{ steps.bump.outputs.changed }}
46
46
  summary: ${{ steps.bump.outputs.summary }}
47
47
  steps:
48
- # Root-owned leftovers under test-temp/ are what `clean: false` used to route
49
- # around: the integration suite bind-mounts host dirs into containers, and a
50
- # non-root git clean then EACCESes on what they wrote -- taking down whatever
51
- # innocent job checked out next. Skipping the clean cured that symptom and
52
- # caused another: every gitignored artifact survived between runs, so a test
53
- # could pass on build output an earlier job left behind and go red the first
54
- # time it landed on a cold runner (probe, 2026-09-01). Clear the leftovers
55
- # from a root container FIRST, and the checkout can clean properly again.
56
- # Best-effort by construction: no docker, no test-temp, or a failed run all
57
- # fall through without failing the job.
58
- - name: Clear stale root-owned test-temp (pre-checkout)
48
+ # Runs BEFORE checkout, and does two things: points the harness's temp base
49
+ # out of the workspace for every later step in this job, and clears whatever
50
+ # the old repo-local default left behind on this runner. The body carries the
51
+ # why. It fails the job only when legacy litter survives -- which would take
52
+ # the checkout down a step later anyway, but report nothing useful.
53
+ - name: Route test-temp out of the workspace (pre-checkout)
59
54
  run: |
60
55
  set -uo pipefail
56
+ # These runners are docker-OUTSIDE-of-docker, so a bind-mount source the
57
+ # HOST daemon has not seen before is created by dockerd, as ROOT, on the
58
+ # host -- even when the launch passes --container-user (reproduced
59
+ # 2026-09-04). The harness's default base is repo-local for an OrbStack
60
+ # inotify constraint that binds macOS dev and nothing here, so every run
61
+ # seeded the CHECKOUT with root-owned dirs a non-root `git clean -ffdx`
62
+ # cannot remove. That took commentary's checks AND integration red on
63
+ # 2026-09-03. Use the sanctioned override, per-run and outside the
64
+ # checkout -- as 8ff17a8d did in June for one debug workflow, and stopped.
65
+ base="$RUNNER_TEMP/nctt-$GITHUB_RUN_ID-$GITHUB_RUN_ATTEMPT"
66
+ mkdir -p "$base"
67
+ echo "NORSK_CTL_TEST_TMP=$base" >> "$GITHUB_ENV"
68
+ # Yesterday's bases hold dockerd-created root dirs, so a non-root rm
69
+ # EACCESes. They cannot wedge a checkout, but they do fill the disk.
70
+ for old in $(find "$RUNNER_TEMP" -maxdepth 1 -name 'nctt-*' -mtime +0 -printf '%f\n' 2>/dev/null || true); do
71
+ docker run --rm --user 0:0 -v "$RUNNER_TEMP":/t alpine sh -c 'rm -rf -- "/t/$1"' sh "$old" >/dev/null 2>&1 || true
72
+ done
73
+ # LEGACY: a runner that ran the old default still carries a workspace
74
+ # test-temp, and a checkout with clean:true dies on it. Reap the holders
75
+ # first -- rm cannot unlink a live mountpoint even as root (EBUSY), and a
76
+ # RESTARTING leaked container re-creates the path as root seconds after
77
+ # any clean. Match on mount SOURCE, not a label: the holders are untracked
78
+ # by construction, because a killed run labels nothing.
61
79
  tt="$GITHUB_WORKSPACE/test-temp"
62
80
  [ -d "$tt" ] || exit 0
63
- docker run --rm --user 0:0 -v "$tt":/t alpine sh \
64
- -c 'rm -rf /t/* /t/.[!.]* 2>/dev/null || true' || rm -rf "$tt"/* 2>/dev/null || true
81
+ stuck=""
82
+ for c in $(docker ps -aq 2>/dev/null || true); do
83
+ if docker inspect -f '{{range .Mounts}}{{println .Source}}{{end}}' "$c" 2>/dev/null | grep -q "^$tt/"; then
84
+ stuck="$stuck $c"
85
+ fi
86
+ done
87
+ if [ -n "$stuck" ]; then
88
+ echo "reaping containers holding mounts under $tt:$stuck"
89
+ docker rm -f $stuck || true
90
+ fi
91
+ # The DIRECTORY, not just its contents: an empty but root-owned base
92
+ # still EACCESes a later mkdtemp. Mounting the PARENT is what lets a root
93
+ # container unlink the leaf.
94
+ docker run --rm --user 0:0 -v "$GITHUB_WORKSPACE":/w alpine \
95
+ sh -c 'rm -rf /w/test-temp' || rm -rf "$tt" || true
96
+ # Fail HERE if it survived. The checkout fails on it either way, but
97
+ # reports only an EACCES rmdir with no clue what was holding the path.
98
+ if [ -e "$tt" ]; then
99
+ echo "::error::$tt survived the pre-checkout clean"
100
+ ls -lan "$tt" || true
101
+ exit 1
102
+ fi
65
103
 
66
104
  - uses: actions/checkout@v5
67
105
  with:
@@ -323,23 +361,61 @@ jobs:
323
361
  if: ${{ !cancelled() }}
324
362
  runs-on: x64
325
363
  steps:
326
- # Root-owned leftovers under test-temp/ are what `clean: false` used to route
327
- # around: the integration suite bind-mounts host dirs into containers, and a
328
- # non-root git clean then EACCESes on what they wrote -- taking down whatever
329
- # innocent job checked out next. Skipping the clean cured that symptom and
330
- # caused another: every gitignored artifact survived between runs, so a test
331
- # could pass on build output an earlier job left behind and go red the first
332
- # time it landed on a cold runner (probe, 2026-09-01). Clear the leftovers
333
- # from a root container FIRST, and the checkout can clean properly again.
334
- # Best-effort by construction: no docker, no test-temp, or a failed run all
335
- # fall through without failing the job.
336
- - name: Clear stale root-owned test-temp (pre-checkout)
364
+ # Runs BEFORE checkout, and does two things: points the harness's temp base
365
+ # out of the workspace for every later step in this job, and clears whatever
366
+ # the old repo-local default left behind on this runner. The body carries the
367
+ # why. It fails the job only when legacy litter survives -- which would take
368
+ # the checkout down a step later anyway, but report nothing useful.
369
+ - name: Route test-temp out of the workspace (pre-checkout)
337
370
  run: |
338
371
  set -uo pipefail
372
+ # These runners are docker-OUTSIDE-of-docker, so a bind-mount source the
373
+ # HOST daemon has not seen before is created by dockerd, as ROOT, on the
374
+ # host -- even when the launch passes --container-user (reproduced
375
+ # 2026-09-04). The harness's default base is repo-local for an OrbStack
376
+ # inotify constraint that binds macOS dev and nothing here, so every run
377
+ # seeded the CHECKOUT with root-owned dirs a non-root `git clean -ffdx`
378
+ # cannot remove. That took commentary's checks AND integration red on
379
+ # 2026-09-03. Use the sanctioned override, per-run and outside the
380
+ # checkout -- as 8ff17a8d did in June for one debug workflow, and stopped.
381
+ base="$RUNNER_TEMP/nctt-$GITHUB_RUN_ID-$GITHUB_RUN_ATTEMPT"
382
+ mkdir -p "$base"
383
+ echo "NORSK_CTL_TEST_TMP=$base" >> "$GITHUB_ENV"
384
+ # Yesterday's bases hold dockerd-created root dirs, so a non-root rm
385
+ # EACCESes. They cannot wedge a checkout, but they do fill the disk.
386
+ for old in $(find "$RUNNER_TEMP" -maxdepth 1 -name 'nctt-*' -mtime +0 -printf '%f\n' 2>/dev/null || true); do
387
+ docker run --rm --user 0:0 -v "$RUNNER_TEMP":/t alpine sh -c 'rm -rf -- "/t/$1"' sh "$old" >/dev/null 2>&1 || true
388
+ done
389
+ # LEGACY: a runner that ran the old default still carries a workspace
390
+ # test-temp, and a checkout with clean:true dies on it. Reap the holders
391
+ # first -- rm cannot unlink a live mountpoint even as root (EBUSY), and a
392
+ # RESTARTING leaked container re-creates the path as root seconds after
393
+ # any clean. Match on mount SOURCE, not a label: the holders are untracked
394
+ # by construction, because a killed run labels nothing.
339
395
  tt="$GITHUB_WORKSPACE/test-temp"
340
396
  [ -d "$tt" ] || exit 0
341
- docker run --rm --user 0:0 -v "$tt":/t alpine sh \
342
- -c 'rm -rf /t/* /t/.[!.]* 2>/dev/null || true' || rm -rf "$tt"/* 2>/dev/null || true
397
+ stuck=""
398
+ for c in $(docker ps -aq 2>/dev/null || true); do
399
+ if docker inspect -f '{{range .Mounts}}{{println .Source}}{{end}}' "$c" 2>/dev/null | grep -q "^$tt/"; then
400
+ stuck="$stuck $c"
401
+ fi
402
+ done
403
+ if [ -n "$stuck" ]; then
404
+ echo "reaping containers holding mounts under $tt:$stuck"
405
+ docker rm -f $stuck || true
406
+ fi
407
+ # The DIRECTORY, not just its contents: an empty but root-owned base
408
+ # still EACCESes a later mkdtemp. Mounting the PARENT is what lets a root
409
+ # container unlink the leaf.
410
+ docker run --rm --user 0:0 -v "$GITHUB_WORKSPACE":/w alpine \
411
+ sh -c 'rm -rf /w/test-temp' || rm -rf "$tt" || true
412
+ # Fail HERE if it survived. The checkout fails on it either way, but
413
+ # reports only an EACCES rmdir with no clue what was holding the path.
414
+ if [ -e "$tt" ]; then
415
+ echo "::error::$tt survived the pre-checkout clean"
416
+ ls -lan "$tt" || true
417
+ exit 1
418
+ fi
343
419
 
344
420
  - uses: actions/checkout@v5
345
421
  with:
@@ -55,3 +55,109 @@ export function teeWithoutPipefail(yaml: string): { line: number; text: string }
55
55
  }
56
56
  return out;
57
57
  }
58
+
59
+ /** The step every convention workflow runs before `actions/checkout`, named so
60
+ * the guard below can find it. */
61
+ export const TEST_TEMP_STEP = "Route test-temp out of the workspace";
62
+
63
+ // What that step must do, and what breaks when it doesn't. All four were learned
64
+ // the hard way over three months on the shared DooD runners:
65
+ //
66
+ // route -- THE fix. These runners are docker-outside-of-docker, so the daemon
67
+ // a launch talks to is the HOST's, and a bind-mount source it has not
68
+ // seen is created by dockerd as ROOT on the host. --container-user
69
+ // does not help: it governs what the container WRITES, not what the
70
+ // daemon CREATES. Leave the harness on its repo-local default and
71
+ // every run seeds the checkout with root-owned dirs. The override
72
+ // must therefore point outside $GITHUB_WORKSPACE.
73
+ // reap -- rm cannot unlink a live mountpoint even as ROOT (EBUSY), and a
74
+ // RESTARTING leaked container re-creates the path as root seconds
75
+ // after any clean. The holders are untracked by construction (a
76
+ // killed run labels nothing), so they are found by mount SOURCE.
77
+ // base -- clearing only the CONTENTS leaves the base dir itself, and an
78
+ // empty-but-root-owned base still EACCESes a later mkdtemp.
79
+ // verify -- the old form swallowed every failure into `2>/dev/null || true`,
80
+ // so a clean that removed nothing looked identical to one that
81
+ // worked. The checkout then died with a bare "EACCES rmdir" and no
82
+ // clue what held the path. It stayed that way for a week.
83
+ // The exported base must TRACE to $RUNNER_TEMP. Checking only that the export
84
+ // exists would pass a step that dutifully exports the workspace path it was
85
+ // already defaulting to, which is the whole bug.
86
+ function routesOutOfWorkspace(body: string): boolean {
87
+ const exported = body.match(/NORSK_CTL_TEST_TMP=(\$\{?\w+\}?|[^\s"]+)[^\n]*>> "\$GITHUB_ENV"/);
88
+ if (!exported) return false;
89
+ const value = exported[1] ?? "";
90
+ if (value.startsWith("$RUNNER_TEMP")) return true;
91
+ const name = value.replace(/^\$\{?/, "").replace(/\}$/, "");
92
+ return name.length > 0 && new RegExp(`\\b${name}="\\$RUNNER_TEMP/`).test(body);
93
+ }
94
+
95
+ const TEST_TEMP_REQUIREMENTS: { name: string; ok: (body: string) => boolean }[] = [
96
+ { name: "route", ok: (b) => routesOutOfWorkspace(b) },
97
+ { name: "reap", ok: (b) => /\.Mounts\b/.test(b) && /docker rm -f/.test(b) },
98
+ { name: "base", ok: (b) => /rm -rf [^\n]*\/test-temp(?![\w/*])/.test(b) },
99
+ { name: "verify", ok: (b) => /\[ -e "\$tt" \]/.test(b) && /\bexit 1\b/.test(b) },
100
+ ];
101
+
102
+ /** The `run: |` body of the step starting at `nameLine`, de-indented. */
103
+ function stepBody(lines: string[], nameLine: number): string {
104
+ const runAt = lines.findIndex((l, i) => i > nameLine && /^\s*run: \|\s*$/.test(l));
105
+ if (runAt < 0) return "";
106
+ const indent = (lines[runAt + 1] ?? "").match(/^\s*/)?.[0].length ?? 0;
107
+ const body: string[] = [];
108
+ for (let j = runAt + 1; j < lines.length; j++) {
109
+ const line = lines[j] as string;
110
+ if (line.trim() === "") continue;
111
+ if ((line.match(/^\s*/)?.[0].length ?? 0) < indent) break;
112
+ body.push(line.trim());
113
+ }
114
+ return body.join("\n");
115
+ }
116
+
117
+ /** Pre-checkout test-temp steps that would leave the checkout exposed, each
118
+ * reported at its `- name:` line with the requirements it does not meet. */
119
+ export function testTempStepProblems(yaml: string): { line: number; missing: string[] }[] {
120
+ const lines = yaml.split("\n");
121
+ const out: { line: number; missing: string[] }[] = [];
122
+ lines.forEach((text, i) => {
123
+ if (!text.includes(`- name: ${TEST_TEMP_STEP}`)) return;
124
+ const body = stepBody(lines, i);
125
+ const missing = TEST_TEMP_REQUIREMENTS.filter((r) => !r.ok(body)).map((r) => r.name);
126
+ if (missing.length > 0) out.push({ line: i + 1, missing });
127
+ });
128
+ return out;
129
+ }
130
+
131
+ /** Jobs that check out without a {@link TEST_TEMP_STEP} of their own.
132
+ *
133
+ * The per-step guard above can only inspect steps it finds BY NAME, so
134
+ * deleting the step -- or renaming it back -- reads as "no problems". This is
135
+ * the presence half. It is per JOB because both halves of the step are
136
+ * per-job: `$GITHUB_ENV` does not cross jobs, and neither does the runner's
137
+ * workspace state. Job ids are read off the two-space indent under `jobs:`
138
+ * rather than a YAML parse, to stay consistent with the line-oriented guards
139
+ * here and to keep reporting real line numbers. */
140
+ export function jobsMissingTestTempStep(yaml: string): string[] {
141
+ const lines = yaml.split("\n");
142
+ const bad: string[] = [];
143
+ let job: string | undefined;
144
+ let checkouts = 0;
145
+ let steps = 0;
146
+ const settle = () => {
147
+ if (job !== undefined && checkouts > steps) bad.push(job);
148
+ };
149
+ for (const line of lines) {
150
+ const head = line.match(/^ {2}([A-Za-z_][\w-]*):\s*$/);
151
+ if (head) {
152
+ settle();
153
+ job = head[1];
154
+ checkouts = 0;
155
+ steps = 0;
156
+ continue;
157
+ }
158
+ if (/^\s*-?\s*uses: actions\/checkout@/.test(line)) checkouts++;
159
+ if (line.includes(`- name: ${TEST_TEMP_STEP}`)) steps++;
160
+ }
161
+ settle();
162
+ return bad;
163
+ }