@norskvideo/ctl-dev-kit 0.1.85 → 0.1.86
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/build-docs.yml +102 -21
- package/conventions/build-image.yml +204 -52
- package/conventions/checks.yml +153 -39
- package/conventions/integration.yml +102 -13
- package/conventions/publish-docs.yml +102 -21
- package/conventions/smoke.yml +102 -13
- package/conventions/sync-ctl-packages.yml +51 -13
- package/conventions/sync-dev-kit.yml +102 -26
- package/conventions/upgrade-latest.yml +102 -26
- package/conventions/workflow-shell.ts +106 -0
- package/package.json +1 -1
package/conventions/smoke.yml
CHANGED
|
@@ -52,17 +52,61 @@ jobs:
|
|
|
52
52
|
- runner: '["ar-arm-vm"]'
|
|
53
53
|
runs-on: ${{ fromJSON(matrix.runner) }}
|
|
54
54
|
steps:
|
|
55
|
-
# BEFORE checkout:
|
|
56
|
-
#
|
|
57
|
-
#
|
|
58
|
-
#
|
|
59
|
-
|
|
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
|
-
|
|
65
|
-
|
|
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
|
-
#
|
|
250
|
-
#
|
|
251
|
-
#
|
|
252
|
-
|
|
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
|
-
|
|
258
|
-
|
|
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
|
-
#
|
|
45
|
-
#
|
|
46
|
-
#
|
|
47
|
-
#
|
|
48
|
-
#
|
|
49
|
-
|
|
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
|
-
|
|
60
|
-
|
|
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
|
-
#
|
|
47
|
-
#
|
|
48
|
-
#
|
|
49
|
-
#
|
|
50
|
-
#
|
|
51
|
-
|
|
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
|
-
|
|
62
|
-
|
|
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
|
-
#
|
|
205
|
-
#
|
|
206
|
-
#
|
|
207
|
-
#
|
|
208
|
-
#
|
|
209
|
-
|
|
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
|
-
|
|
220
|
-
|
|
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
|
-
#
|
|
49
|
-
#
|
|
50
|
-
#
|
|
51
|
-
#
|
|
52
|
-
#
|
|
53
|
-
|
|
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
|
-
|
|
64
|
-
|
|
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
|
-
#
|
|
327
|
-
#
|
|
328
|
-
#
|
|
329
|
-
#
|
|
330
|
-
#
|
|
331
|
-
|
|
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
|
-
|
|
342
|
-
|
|
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
|
+
}
|