@norskvideo/ctl-dev-kit 0.1.84 → 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/check-drift.ts +37 -0
- package/conventions/checks.yml +153 -39
- package/conventions/integration-testing.md +40 -0
- package/conventions/integration.yml +435 -0
- 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/sync-drift.ts +30 -0
- package/conventions/upgrade-latest.yml +102 -26
- package/conventions/workflow-shell.ts +136 -0
- package/package.json +1 -1
|
@@ -43,36 +43,117 @@ jobs:
|
|
|
43
43
|
build:
|
|
44
44
|
runs-on: x64
|
|
45
45
|
steps:
|
|
46
|
-
#
|
|
47
|
-
#
|
|
48
|
-
#
|
|
49
|
-
#
|
|
50
|
-
#
|
|
51
|
-
- name:
|
|
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)
|
|
52
52
|
run: |
|
|
53
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.
|
|
54
77
|
tt="$GITHUB_WORKSPACE/test-temp"
|
|
55
78
|
[ -d "$tt" ] || exit 0
|
|
56
|
-
|
|
57
|
-
|
|
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
|
|
58
101
|
|
|
59
|
-
#
|
|
60
|
-
#
|
|
61
|
-
#
|
|
62
|
-
#
|
|
63
|
-
#
|
|
64
|
-
|
|
65
|
-
# time it landed on a cold runner (probe, 2026-09-01). Clear the leftovers
|
|
66
|
-
# from a root container FIRST, and the checkout can clean properly again.
|
|
67
|
-
# Best-effort by construction: no docker, no test-temp, or a failed run all
|
|
68
|
-
# fall through without failing the job.
|
|
69
|
-
- name: Clear stale root-owned test-temp (pre-checkout)
|
|
102
|
+
# Runs BEFORE checkout, and does two things: points the harness's temp base
|
|
103
|
+
# out of the workspace for every later step in this job, and clears whatever
|
|
104
|
+
# the old repo-local default left behind on this runner. The body carries the
|
|
105
|
+
# why. It fails the job only when legacy litter survives -- which would take
|
|
106
|
+
# the checkout down a step later anyway, but report nothing useful.
|
|
107
|
+
- name: Route test-temp out of the workspace (pre-checkout)
|
|
70
108
|
run: |
|
|
71
109
|
set -uo pipefail
|
|
110
|
+
# These runners are docker-OUTSIDE-of-docker, so a bind-mount source the
|
|
111
|
+
# HOST daemon has not seen before is created by dockerd, as ROOT, on the
|
|
112
|
+
# host -- even when the launch passes --container-user (reproduced
|
|
113
|
+
# 2026-09-04). The harness's default base is repo-local for an OrbStack
|
|
114
|
+
# inotify constraint that binds macOS dev and nothing here, so every run
|
|
115
|
+
# seeded the CHECKOUT with root-owned dirs a non-root `git clean -ffdx`
|
|
116
|
+
# cannot remove. That took commentary's checks AND integration red on
|
|
117
|
+
# 2026-09-03. Use the sanctioned override, per-run and outside the
|
|
118
|
+
# checkout -- as 8ff17a8d did in June for one debug workflow, and stopped.
|
|
119
|
+
base="$RUNNER_TEMP/nctt-$GITHUB_RUN_ID-$GITHUB_RUN_ATTEMPT"
|
|
120
|
+
mkdir -p "$base"
|
|
121
|
+
echo "NORSK_CTL_TEST_TMP=$base" >> "$GITHUB_ENV"
|
|
122
|
+
# Yesterday's bases hold dockerd-created root dirs, so a non-root rm
|
|
123
|
+
# EACCESes. They cannot wedge a checkout, but they do fill the disk.
|
|
124
|
+
for old in $(find "$RUNNER_TEMP" -maxdepth 1 -name 'nctt-*' -mtime +0 -printf '%f\n' 2>/dev/null || true); do
|
|
125
|
+
docker run --rm --user 0:0 -v "$RUNNER_TEMP":/t alpine sh -c 'rm -rf -- "/t/$1"' sh "$old" >/dev/null 2>&1 || true
|
|
126
|
+
done
|
|
127
|
+
# LEGACY: a runner that ran the old default still carries a workspace
|
|
128
|
+
# test-temp, and a checkout with clean:true dies on it. Reap the holders
|
|
129
|
+
# first -- rm cannot unlink a live mountpoint even as root (EBUSY), and a
|
|
130
|
+
# RESTARTING leaked container re-creates the path as root seconds after
|
|
131
|
+
# any clean. Match on mount SOURCE, not a label: the holders are untracked
|
|
132
|
+
# by construction, because a killed run labels nothing.
|
|
72
133
|
tt="$GITHUB_WORKSPACE/test-temp"
|
|
73
134
|
[ -d "$tt" ] || exit 0
|
|
74
|
-
|
|
75
|
-
|
|
135
|
+
stuck=""
|
|
136
|
+
for c in $(docker ps -aq 2>/dev/null || true); do
|
|
137
|
+
if docker inspect -f '{{range .Mounts}}{{println .Source}}{{end}}' "$c" 2>/dev/null | grep -q "^$tt/"; then
|
|
138
|
+
stuck="$stuck $c"
|
|
139
|
+
fi
|
|
140
|
+
done
|
|
141
|
+
if [ -n "$stuck" ]; then
|
|
142
|
+
echo "reaping containers holding mounts under $tt:$stuck"
|
|
143
|
+
docker rm -f $stuck || true
|
|
144
|
+
fi
|
|
145
|
+
# The DIRECTORY, not just its contents: an empty but root-owned base
|
|
146
|
+
# still EACCESes a later mkdtemp. Mounting the PARENT is what lets a root
|
|
147
|
+
# container unlink the leaf.
|
|
148
|
+
docker run --rm --user 0:0 -v "$GITHUB_WORKSPACE":/w alpine \
|
|
149
|
+
sh -c 'rm -rf /w/test-temp' || rm -rf "$tt" || true
|
|
150
|
+
# Fail HERE if it survived. The checkout fails on it either way, but
|
|
151
|
+
# reports only an EACCES rmdir with no clue what was holding the path.
|
|
152
|
+
if [ -e "$tt" ]; then
|
|
153
|
+
echo "::error::$tt survived the pre-checkout clean"
|
|
154
|
+
ls -lan "$tt" || true
|
|
155
|
+
exit 1
|
|
156
|
+
fi
|
|
76
157
|
|
|
77
158
|
- uses: actions/checkout@v5
|
|
78
159
|
with:
|
|
@@ -66,23 +66,61 @@ jobs:
|
|
|
66
66
|
# publish job gates on this output instead of on PUBLISH_IMAGE directly.
|
|
67
67
|
publish: ${{ steps.gen.outputs.publish }}
|
|
68
68
|
steps:
|
|
69
|
-
#
|
|
70
|
-
#
|
|
71
|
-
#
|
|
72
|
-
#
|
|
73
|
-
#
|
|
74
|
-
|
|
75
|
-
# time it landed on a cold runner (probe, 2026-09-01). Clear the leftovers
|
|
76
|
-
# from a root container FIRST, and the checkout can clean properly again.
|
|
77
|
-
# Best-effort by construction: no docker, no test-temp, or a failed run all
|
|
78
|
-
# fall through without failing the job.
|
|
79
|
-
- name: Clear stale root-owned test-temp (pre-checkout)
|
|
69
|
+
# Runs BEFORE checkout, and does two things: points the harness's temp base
|
|
70
|
+
# out of the workspace for every later step in this job, and clears whatever
|
|
71
|
+
# the old repo-local default left behind on this runner. The body carries the
|
|
72
|
+
# why. It fails the job only when legacy litter survives -- which would take
|
|
73
|
+
# the checkout down a step later anyway, but report nothing useful.
|
|
74
|
+
- name: Route test-temp out of the workspace (pre-checkout)
|
|
80
75
|
run: |
|
|
81
76
|
set -uo pipefail
|
|
77
|
+
# These runners are docker-OUTSIDE-of-docker, so a bind-mount source the
|
|
78
|
+
# HOST daemon has not seen before is created by dockerd, as ROOT, on the
|
|
79
|
+
# host -- even when the launch passes --container-user (reproduced
|
|
80
|
+
# 2026-09-04). The harness's default base is repo-local for an OrbStack
|
|
81
|
+
# inotify constraint that binds macOS dev and nothing here, so every run
|
|
82
|
+
# seeded the CHECKOUT with root-owned dirs a non-root `git clean -ffdx`
|
|
83
|
+
# cannot remove. That took commentary's checks AND integration red on
|
|
84
|
+
# 2026-09-03. Use the sanctioned override, per-run and outside the
|
|
85
|
+
# checkout -- as 8ff17a8d did in June for one debug workflow, and stopped.
|
|
86
|
+
base="$RUNNER_TEMP/nctt-$GITHUB_RUN_ID-$GITHUB_RUN_ATTEMPT"
|
|
87
|
+
mkdir -p "$base"
|
|
88
|
+
echo "NORSK_CTL_TEST_TMP=$base" >> "$GITHUB_ENV"
|
|
89
|
+
# Yesterday's bases hold dockerd-created root dirs, so a non-root rm
|
|
90
|
+
# EACCESes. They cannot wedge a checkout, but they do fill the disk.
|
|
91
|
+
for old in $(find "$RUNNER_TEMP" -maxdepth 1 -name 'nctt-*' -mtime +0 -printf '%f\n' 2>/dev/null || true); do
|
|
92
|
+
docker run --rm --user 0:0 -v "$RUNNER_TEMP":/t alpine sh -c 'rm -rf -- "/t/$1"' sh "$old" >/dev/null 2>&1 || true
|
|
93
|
+
done
|
|
94
|
+
# LEGACY: a runner that ran the old default still carries a workspace
|
|
95
|
+
# test-temp, and a checkout with clean:true dies on it. Reap the holders
|
|
96
|
+
# first -- rm cannot unlink a live mountpoint even as root (EBUSY), and a
|
|
97
|
+
# RESTARTING leaked container re-creates the path as root seconds after
|
|
98
|
+
# any clean. Match on mount SOURCE, not a label: the holders are untracked
|
|
99
|
+
# by construction, because a killed run labels nothing.
|
|
82
100
|
tt="$GITHUB_WORKSPACE/test-temp"
|
|
83
101
|
[ -d "$tt" ] || exit 0
|
|
84
|
-
|
|
85
|
-
|
|
102
|
+
stuck=""
|
|
103
|
+
for c in $(docker ps -aq 2>/dev/null || true); do
|
|
104
|
+
if docker inspect -f '{{range .Mounts}}{{println .Source}}{{end}}' "$c" 2>/dev/null | grep -q "^$tt/"; then
|
|
105
|
+
stuck="$stuck $c"
|
|
106
|
+
fi
|
|
107
|
+
done
|
|
108
|
+
if [ -n "$stuck" ]; then
|
|
109
|
+
echo "reaping containers holding mounts under $tt:$stuck"
|
|
110
|
+
docker rm -f $stuck || true
|
|
111
|
+
fi
|
|
112
|
+
# The DIRECTORY, not just its contents: an empty but root-owned base
|
|
113
|
+
# still EACCESes a later mkdtemp. Mounting the PARENT is what lets a root
|
|
114
|
+
# container unlink the leaf.
|
|
115
|
+
docker run --rm --user 0:0 -v "$GITHUB_WORKSPACE":/w alpine \
|
|
116
|
+
sh -c 'rm -rf /w/test-temp' || rm -rf "$tt" || true
|
|
117
|
+
# Fail HERE if it survived. The checkout fails on it either way, but
|
|
118
|
+
# reports only an EACCES rmdir with no clue what was holding the path.
|
|
119
|
+
if [ -e "$tt" ]; then
|
|
120
|
+
echo "::error::$tt survived the pre-checkout clean"
|
|
121
|
+
ls -lan "$tt" || true
|
|
122
|
+
exit 1
|
|
123
|
+
fi
|
|
86
124
|
|
|
87
125
|
- uses: actions/checkout@v5
|
|
88
126
|
with:
|
|
@@ -109,23 +147,61 @@ jobs:
|
|
|
109
147
|
arch: arm64
|
|
110
148
|
runs-on: ${{ matrix.runner }}
|
|
111
149
|
steps:
|
|
112
|
-
#
|
|
113
|
-
#
|
|
114
|
-
#
|
|
115
|
-
#
|
|
116
|
-
#
|
|
117
|
-
|
|
118
|
-
# time it landed on a cold runner (probe, 2026-09-01). Clear the leftovers
|
|
119
|
-
# from a root container FIRST, and the checkout can clean properly again.
|
|
120
|
-
# Best-effort by construction: no docker, no test-temp, or a failed run all
|
|
121
|
-
# fall through without failing the job.
|
|
122
|
-
- name: Clear stale root-owned test-temp (pre-checkout)
|
|
150
|
+
# Runs BEFORE checkout, and does two things: points the harness's temp base
|
|
151
|
+
# out of the workspace for every later step in this job, and clears whatever
|
|
152
|
+
# the old repo-local default left behind on this runner. The body carries the
|
|
153
|
+
# why. It fails the job only when legacy litter survives -- which would take
|
|
154
|
+
# the checkout down a step later anyway, but report nothing useful.
|
|
155
|
+
- name: Route test-temp out of the workspace (pre-checkout)
|
|
123
156
|
run: |
|
|
124
157
|
set -uo pipefail
|
|
158
|
+
# These runners are docker-OUTSIDE-of-docker, so a bind-mount source the
|
|
159
|
+
# HOST daemon has not seen before is created by dockerd, as ROOT, on the
|
|
160
|
+
# host -- even when the launch passes --container-user (reproduced
|
|
161
|
+
# 2026-09-04). The harness's default base is repo-local for an OrbStack
|
|
162
|
+
# inotify constraint that binds macOS dev and nothing here, so every run
|
|
163
|
+
# seeded the CHECKOUT with root-owned dirs a non-root `git clean -ffdx`
|
|
164
|
+
# cannot remove. That took commentary's checks AND integration red on
|
|
165
|
+
# 2026-09-03. Use the sanctioned override, per-run and outside the
|
|
166
|
+
# checkout -- as 8ff17a8d did in June for one debug workflow, and stopped.
|
|
167
|
+
base="$RUNNER_TEMP/nctt-$GITHUB_RUN_ID-$GITHUB_RUN_ATTEMPT"
|
|
168
|
+
mkdir -p "$base"
|
|
169
|
+
echo "NORSK_CTL_TEST_TMP=$base" >> "$GITHUB_ENV"
|
|
170
|
+
# Yesterday's bases hold dockerd-created root dirs, so a non-root rm
|
|
171
|
+
# EACCESes. They cannot wedge a checkout, but they do fill the disk.
|
|
172
|
+
for old in $(find "$RUNNER_TEMP" -maxdepth 1 -name 'nctt-*' -mtime +0 -printf '%f\n' 2>/dev/null || true); do
|
|
173
|
+
docker run --rm --user 0:0 -v "$RUNNER_TEMP":/t alpine sh -c 'rm -rf -- "/t/$1"' sh "$old" >/dev/null 2>&1 || true
|
|
174
|
+
done
|
|
175
|
+
# LEGACY: a runner that ran the old default still carries a workspace
|
|
176
|
+
# test-temp, and a checkout with clean:true dies on it. Reap the holders
|
|
177
|
+
# first -- rm cannot unlink a live mountpoint even as root (EBUSY), and a
|
|
178
|
+
# RESTARTING leaked container re-creates the path as root seconds after
|
|
179
|
+
# any clean. Match on mount SOURCE, not a label: the holders are untracked
|
|
180
|
+
# by construction, because a killed run labels nothing.
|
|
125
181
|
tt="$GITHUB_WORKSPACE/test-temp"
|
|
126
182
|
[ -d "$tt" ] || exit 0
|
|
127
|
-
|
|
128
|
-
|
|
183
|
+
stuck=""
|
|
184
|
+
for c in $(docker ps -aq 2>/dev/null || true); do
|
|
185
|
+
if docker inspect -f '{{range .Mounts}}{{println .Source}}{{end}}' "$c" 2>/dev/null | grep -q "^$tt/"; then
|
|
186
|
+
stuck="$stuck $c"
|
|
187
|
+
fi
|
|
188
|
+
done
|
|
189
|
+
if [ -n "$stuck" ]; then
|
|
190
|
+
echo "reaping containers holding mounts under $tt:$stuck"
|
|
191
|
+
docker rm -f $stuck || true
|
|
192
|
+
fi
|
|
193
|
+
# The DIRECTORY, not just its contents: an empty but root-owned base
|
|
194
|
+
# still EACCESes a later mkdtemp. Mounting the PARENT is what lets a root
|
|
195
|
+
# container unlink the leaf.
|
|
196
|
+
docker run --rm --user 0:0 -v "$GITHUB_WORKSPACE":/w alpine \
|
|
197
|
+
sh -c 'rm -rf /w/test-temp' || rm -rf "$tt" || true
|
|
198
|
+
# Fail HERE if it survived. The checkout fails on it either way, but
|
|
199
|
+
# reports only an EACCES rmdir with no clue what was holding the path.
|
|
200
|
+
if [ -e "$tt" ]; then
|
|
201
|
+
echo "::error::$tt survived the pre-checkout clean"
|
|
202
|
+
ls -lan "$tt" || true
|
|
203
|
+
exit 1
|
|
204
|
+
fi
|
|
129
205
|
|
|
130
206
|
- uses: actions/checkout@v5
|
|
131
207
|
with:
|
|
@@ -165,23 +241,61 @@ jobs:
|
|
|
165
241
|
if: ${{ needs.label.outputs.publish == 'true' && !inputs.dry_run }}
|
|
166
242
|
runs-on: x64
|
|
167
243
|
steps:
|
|
168
|
-
#
|
|
169
|
-
#
|
|
170
|
-
#
|
|
171
|
-
#
|
|
172
|
-
#
|
|
173
|
-
|
|
174
|
-
# time it landed on a cold runner (probe, 2026-09-01). Clear the leftovers
|
|
175
|
-
# from a root container FIRST, and the checkout can clean properly again.
|
|
176
|
-
# Best-effort by construction: no docker, no test-temp, or a failed run all
|
|
177
|
-
# fall through without failing the job.
|
|
178
|
-
- name: Clear stale root-owned test-temp (pre-checkout)
|
|
244
|
+
# Runs BEFORE checkout, and does two things: points the harness's temp base
|
|
245
|
+
# out of the workspace for every later step in this job, and clears whatever
|
|
246
|
+
# the old repo-local default left behind on this runner. The body carries the
|
|
247
|
+
# why. It fails the job only when legacy litter survives -- which would take
|
|
248
|
+
# the checkout down a step later anyway, but report nothing useful.
|
|
249
|
+
- name: Route test-temp out of the workspace (pre-checkout)
|
|
179
250
|
run: |
|
|
180
251
|
set -uo pipefail
|
|
252
|
+
# These runners are docker-OUTSIDE-of-docker, so a bind-mount source the
|
|
253
|
+
# HOST daemon has not seen before is created by dockerd, as ROOT, on the
|
|
254
|
+
# host -- even when the launch passes --container-user (reproduced
|
|
255
|
+
# 2026-09-04). The harness's default base is repo-local for an OrbStack
|
|
256
|
+
# inotify constraint that binds macOS dev and nothing here, so every run
|
|
257
|
+
# seeded the CHECKOUT with root-owned dirs a non-root `git clean -ffdx`
|
|
258
|
+
# cannot remove. That took commentary's checks AND integration red on
|
|
259
|
+
# 2026-09-03. Use the sanctioned override, per-run and outside the
|
|
260
|
+
# checkout -- as 8ff17a8d did in June for one debug workflow, and stopped.
|
|
261
|
+
base="$RUNNER_TEMP/nctt-$GITHUB_RUN_ID-$GITHUB_RUN_ATTEMPT"
|
|
262
|
+
mkdir -p "$base"
|
|
263
|
+
echo "NORSK_CTL_TEST_TMP=$base" >> "$GITHUB_ENV"
|
|
264
|
+
# Yesterday's bases hold dockerd-created root dirs, so a non-root rm
|
|
265
|
+
# EACCESes. They cannot wedge a checkout, but they do fill the disk.
|
|
266
|
+
for old in $(find "$RUNNER_TEMP" -maxdepth 1 -name 'nctt-*' -mtime +0 -printf '%f\n' 2>/dev/null || true); do
|
|
267
|
+
docker run --rm --user 0:0 -v "$RUNNER_TEMP":/t alpine sh -c 'rm -rf -- "/t/$1"' sh "$old" >/dev/null 2>&1 || true
|
|
268
|
+
done
|
|
269
|
+
# LEGACY: a runner that ran the old default still carries a workspace
|
|
270
|
+
# test-temp, and a checkout with clean:true dies on it. Reap the holders
|
|
271
|
+
# first -- rm cannot unlink a live mountpoint even as root (EBUSY), and a
|
|
272
|
+
# RESTARTING leaked container re-creates the path as root seconds after
|
|
273
|
+
# any clean. Match on mount SOURCE, not a label: the holders are untracked
|
|
274
|
+
# by construction, because a killed run labels nothing.
|
|
181
275
|
tt="$GITHUB_WORKSPACE/test-temp"
|
|
182
276
|
[ -d "$tt" ] || exit 0
|
|
183
|
-
|
|
184
|
-
|
|
277
|
+
stuck=""
|
|
278
|
+
for c in $(docker ps -aq 2>/dev/null || true); do
|
|
279
|
+
if docker inspect -f '{{range .Mounts}}{{println .Source}}{{end}}' "$c" 2>/dev/null | grep -q "^$tt/"; then
|
|
280
|
+
stuck="$stuck $c"
|
|
281
|
+
fi
|
|
282
|
+
done
|
|
283
|
+
if [ -n "$stuck" ]; then
|
|
284
|
+
echo "reaping containers holding mounts under $tt:$stuck"
|
|
285
|
+
docker rm -f $stuck || true
|
|
286
|
+
fi
|
|
287
|
+
# The DIRECTORY, not just its contents: an empty but root-owned base
|
|
288
|
+
# still EACCESes a later mkdtemp. Mounting the PARENT is what lets a root
|
|
289
|
+
# container unlink the leaf.
|
|
290
|
+
docker run --rm --user 0:0 -v "$GITHUB_WORKSPACE":/w alpine \
|
|
291
|
+
sh -c 'rm -rf /w/test-temp' || rm -rf "$tt" || true
|
|
292
|
+
# Fail HERE if it survived. The checkout fails on it either way, but
|
|
293
|
+
# reports only an EACCES rmdir with no clue what was holding the path.
|
|
294
|
+
if [ -e "$tt" ]; then
|
|
295
|
+
echo "::error::$tt survived the pre-checkout clean"
|
|
296
|
+
ls -lan "$tt" || true
|
|
297
|
+
exit 1
|
|
298
|
+
fi
|
|
185
299
|
|
|
186
300
|
- uses: actions/checkout@v5
|
|
187
301
|
with:
|
|
@@ -216,23 +330,61 @@ jobs:
|
|
|
216
330
|
if: ${{ !cancelled() }}
|
|
217
331
|
runs-on: x64
|
|
218
332
|
steps:
|
|
219
|
-
#
|
|
220
|
-
#
|
|
221
|
-
#
|
|
222
|
-
#
|
|
223
|
-
#
|
|
224
|
-
|
|
225
|
-
# time it landed on a cold runner (probe, 2026-09-01). Clear the leftovers
|
|
226
|
-
# from a root container FIRST, and the checkout can clean properly again.
|
|
227
|
-
# Best-effort by construction: no docker, no test-temp, or a failed run all
|
|
228
|
-
# fall through without failing the job.
|
|
229
|
-
- name: Clear stale root-owned test-temp (pre-checkout)
|
|
333
|
+
# Runs BEFORE checkout, and does two things: points the harness's temp base
|
|
334
|
+
# out of the workspace for every later step in this job, and clears whatever
|
|
335
|
+
# the old repo-local default left behind on this runner. The body carries the
|
|
336
|
+
# why. It fails the job only when legacy litter survives -- which would take
|
|
337
|
+
# the checkout down a step later anyway, but report nothing useful.
|
|
338
|
+
- name: Route test-temp out of the workspace (pre-checkout)
|
|
230
339
|
run: |
|
|
231
340
|
set -uo pipefail
|
|
341
|
+
# These runners are docker-OUTSIDE-of-docker, so a bind-mount source the
|
|
342
|
+
# HOST daemon has not seen before is created by dockerd, as ROOT, on the
|
|
343
|
+
# host -- even when the launch passes --container-user (reproduced
|
|
344
|
+
# 2026-09-04). The harness's default base is repo-local for an OrbStack
|
|
345
|
+
# inotify constraint that binds macOS dev and nothing here, so every run
|
|
346
|
+
# seeded the CHECKOUT with root-owned dirs a non-root `git clean -ffdx`
|
|
347
|
+
# cannot remove. That took commentary's checks AND integration red on
|
|
348
|
+
# 2026-09-03. Use the sanctioned override, per-run and outside the
|
|
349
|
+
# checkout -- as 8ff17a8d did in June for one debug workflow, and stopped.
|
|
350
|
+
base="$RUNNER_TEMP/nctt-$GITHUB_RUN_ID-$GITHUB_RUN_ATTEMPT"
|
|
351
|
+
mkdir -p "$base"
|
|
352
|
+
echo "NORSK_CTL_TEST_TMP=$base" >> "$GITHUB_ENV"
|
|
353
|
+
# Yesterday's bases hold dockerd-created root dirs, so a non-root rm
|
|
354
|
+
# EACCESes. They cannot wedge a checkout, but they do fill the disk.
|
|
355
|
+
for old in $(find "$RUNNER_TEMP" -maxdepth 1 -name 'nctt-*' -mtime +0 -printf '%f\n' 2>/dev/null || true); do
|
|
356
|
+
docker run --rm --user 0:0 -v "$RUNNER_TEMP":/t alpine sh -c 'rm -rf -- "/t/$1"' sh "$old" >/dev/null 2>&1 || true
|
|
357
|
+
done
|
|
358
|
+
# LEGACY: a runner that ran the old default still carries a workspace
|
|
359
|
+
# test-temp, and a checkout with clean:true dies on it. Reap the holders
|
|
360
|
+
# first -- rm cannot unlink a live mountpoint even as root (EBUSY), and a
|
|
361
|
+
# RESTARTING leaked container re-creates the path as root seconds after
|
|
362
|
+
# any clean. Match on mount SOURCE, not a label: the holders are untracked
|
|
363
|
+
# by construction, because a killed run labels nothing.
|
|
232
364
|
tt="$GITHUB_WORKSPACE/test-temp"
|
|
233
365
|
[ -d "$tt" ] || exit 0
|
|
234
|
-
|
|
235
|
-
|
|
366
|
+
stuck=""
|
|
367
|
+
for c in $(docker ps -aq 2>/dev/null || true); do
|
|
368
|
+
if docker inspect -f '{{range .Mounts}}{{println .Source}}{{end}}' "$c" 2>/dev/null | grep -q "^$tt/"; then
|
|
369
|
+
stuck="$stuck $c"
|
|
370
|
+
fi
|
|
371
|
+
done
|
|
372
|
+
if [ -n "$stuck" ]; then
|
|
373
|
+
echo "reaping containers holding mounts under $tt:$stuck"
|
|
374
|
+
docker rm -f $stuck || true
|
|
375
|
+
fi
|
|
376
|
+
# The DIRECTORY, not just its contents: an empty but root-owned base
|
|
377
|
+
# still EACCESes a later mkdtemp. Mounting the PARENT is what lets a root
|
|
378
|
+
# container unlink the leaf.
|
|
379
|
+
docker run --rm --user 0:0 -v "$GITHUB_WORKSPACE":/w alpine \
|
|
380
|
+
sh -c 'rm -rf /w/test-temp' || rm -rf "$tt" || true
|
|
381
|
+
# Fail HERE if it survived. The checkout fails on it either way, but
|
|
382
|
+
# reports only an EACCES rmdir with no clue what was holding the path.
|
|
383
|
+
if [ -e "$tt" ]; then
|
|
384
|
+
echo "::error::$tt survived the pre-checkout clean"
|
|
385
|
+
ls -lan "$tt" || true
|
|
386
|
+
exit 1
|
|
387
|
+
fi
|
|
236
388
|
|
|
237
389
|
- uses: actions/checkout@v5
|
|
238
390
|
with:
|
|
@@ -62,6 +62,20 @@ export const PRODUCT_SENTINEL = "__PRODUCT__";
|
|
|
62
62
|
// unfilled-sentinel check.
|
|
63
63
|
export const PUBLISH_IMAGE_LINE = /^(\s*PUBLISH_IMAGE:\s*).*$/gm;
|
|
64
64
|
|
|
65
|
+
// integration.yml (04c s9 #1) is universally present and was hand-owned in six
|
|
66
|
+
// shapes, so on the day it joined the conventions set the gate could not demand
|
|
67
|
+
// the canonical of every copy: sync-dev-kit commits straight to main when
|
|
68
|
+
// checks are green, and would have rewritten six tiers before the hooks their
|
|
69
|
+
// suites need existed. A copy therefore declares itself converged by carrying
|
|
70
|
+
// the canonical's own first line; one without it is hand-owned and ungated
|
|
71
|
+
// until its repo converges deliberately. When every repo carries the marker,
|
|
72
|
+
// this becomes unconditional like smoke.yml.
|
|
73
|
+
export const INTEGRATION_MARKER = "# ctl-dev-kit convention: integration";
|
|
74
|
+
|
|
75
|
+
export function carriesIntegrationMarker(text: string): boolean {
|
|
76
|
+
return text.split("\n")[0]?.trim() === INTEGRATION_MARKER;
|
|
77
|
+
}
|
|
78
|
+
|
|
65
79
|
// smoke.yml carries a second per-repo line: the matrix `runner:` label array
|
|
66
80
|
// the journey runs on ('["x64"]' for the pool, '["ARM64","nvidia"]' for the GPU
|
|
67
81
|
// box). Masked like product:; any filled value is valid, so no sentinel check.
|
|
@@ -252,6 +266,9 @@ export interface CanonicalBytes {
|
|
|
252
266
|
gitignoreCore: string;
|
|
253
267
|
buildImage: string;
|
|
254
268
|
smoke: string;
|
|
269
|
+
/** conventions/integration.yml — the integration tier. Gated only for a copy
|
|
270
|
+
* carrying INTEGRATION_MARKER on line 1; same two masked lines as smoke. */
|
|
271
|
+
integration: string;
|
|
255
272
|
/** conventions/demo.sh — the scripts/demo shim a product with tests/demo.spec.ts carries. */
|
|
256
273
|
demoShim: string;
|
|
257
274
|
/** doc-guide/with-display.sh — the X-display wrapper BOTH docs workflows invoke
|
|
@@ -546,6 +563,25 @@ export function checkDrift(repoRoot: string, canonical: CanonicalBytes, opts: Ch
|
|
|
546
563
|
);
|
|
547
564
|
}
|
|
548
565
|
|
|
566
|
+
// integration.yml: gated once a copy carries the convention marker (see
|
|
567
|
+
// INTEGRATION_MARKER for why not before); then it is smoke.yml's twin, with
|
|
568
|
+
// the `product:` key and the matrix `runner:` array as the per-repo lines.
|
|
569
|
+
const integrationPath = join(repoRoot, ".github", "workflows", "integration.yml");
|
|
570
|
+
if (existsSync(integrationPath)) {
|
|
571
|
+
const actual = readFileSync(integrationPath, "utf8");
|
|
572
|
+
if (carriesIntegrationMarker(actual)) {
|
|
573
|
+
push(
|
|
574
|
+
workflowProblem(
|
|
575
|
+
".github/workflows/integration.yml",
|
|
576
|
+
"conventions/integration.yml",
|
|
577
|
+
actual,
|
|
578
|
+
canonical.integration,
|
|
579
|
+
[PRODUCT_LINE, RUNNER_LINE],
|
|
580
|
+
),
|
|
581
|
+
);
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
|
|
549
585
|
// publish-docs.yml is OPTIONAL — a product carries it once it has an
|
|
550
586
|
// illustrated manual to publish nightly. Unlike the workflows above it has NO
|
|
551
587
|
// per-repo line: it is deliberately product-agnostic (per-repo bits live in
|
|
@@ -623,6 +659,7 @@ if (import.meta.main) {
|
|
|
623
659
|
gitignoreCore: readFileSync(join(import.meta.dir, "gitignore.core"), "utf8"),
|
|
624
660
|
buildImage: readFileSync(join(import.meta.dir, "build-image.yml"), "utf8"),
|
|
625
661
|
smoke: readFileSync(join(import.meta.dir, "smoke.yml"), "utf8"),
|
|
662
|
+
integration: readFileSync(join(import.meta.dir, "integration.yml"), "utf8"),
|
|
626
663
|
demoShim: readFileSync(join(import.meta.dir, "demo.sh"), "utf8"),
|
|
627
664
|
};
|
|
628
665
|
const report = checkDrift(repoRoot, canonical, { repoName: resolveRepoName(repoRoot) });
|