@norskvideo/ctl-dev-kit 0.2.64 → 0.2.65
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/flake.nix +1 -1
- package/build/flake.sdk-app.nix +2 -2
- package/build/refresh-ctl-pin.sh +1 -1
- package/conventions/CLAUDE.core.md +10 -7
- package/conventions/bot-prs.yml +91 -61
- package/conventions/build-docs.yml +2 -2
- package/conventions/build-image.yml +8 -7
- package/conventions/bump.sdk-app.yml +808 -0
- package/conventions/bump.yml +916 -0
- package/conventions/check-drift.ts +52 -60
- package/conventions/ctl-pin-agreement.ts +3 -4
- package/conventions/docs-site.md +3 -3
- package/conventions/integration-testing.md +1 -1
- package/conventions/integration.yml +6 -5
- package/conventions/publish-docs.yml +2 -2
- package/conventions/smoke.yml +2 -2
- package/conventions/sync-drift.ts +61 -24
- package/create-product/canon.ts +2 -6
- package/create-product/create-product.ts +1 -3
- package/create-product/turnkey.ts +2 -2
- package/docs-theme/README.md +1 -1
- package/package.json +1 -1
- package/conventions/sync-ctl-packages.yml +0 -309
- package/conventions/sync-dev-kit.yml +0 -404
- package/conventions/upgrade-latest.sdk-app.yml +0 -456
- package/conventions/upgrade-latest.yml +0 -567
|
@@ -1,404 +0,0 @@
|
|
|
1
|
-
# Keep this product's shared conventions fresh against @norskvideo/ctl-dev-kit.
|
|
2
|
-
# The dev-kit single-sources the files every ctl product would otherwise
|
|
3
|
-
# copy-and-drift (the fenced CLAUDE.md core, nix/ctl-shell.nix, the biome/tsconfig/dprint
|
|
4
|
-
# bases, the shared workflows, the build bootstrap). check:drift fails CI when a
|
|
5
|
-
# copy diverges from the *installed* dev-kit — so a product goes stale two ways:
|
|
6
|
-
# its pin lags the newest published dev-kit, and once the pin bumps its copies
|
|
7
|
-
# need re-syncing. This workflow closes the first gap and surfaces the second.
|
|
8
|
-
#
|
|
9
|
-
# It bumps the @norskvideo/ctl-dev-kit pin to the newest published version,
|
|
10
|
-
# reinstalls, re-syncs this product's forced copies from the new canonical
|
|
11
|
-
# (sync-drift), runs the gate (check:drift + lint + typecheck + unit tests), and
|
|
12
|
-
# ALWAYS opens (or updates) a PR on sync-dev-kit/<version> -- never a push to
|
|
13
|
-
# main. bot-prs merges it once checks, smoke, integration and the guides gate
|
|
14
|
-
# are green on its head. The gate's verdict heads the PR body: red means the new
|
|
15
|
-
# dev-kit genuinely does not fit this product (a convention that fails
|
|
16
|
-
# lint/typecheck/tests, or a copy sync-drift could not converge), and adopting
|
|
17
|
-
# it is real engineering on the branch, not a copy-paste.
|
|
18
|
-
#
|
|
19
|
-
# Why a PR even when the gate is green: a new dev-kit changes the copied
|
|
20
|
-
# workflows themselves, and those are exactly what the gate does not run. On
|
|
21
|
-
# 2026-10-09 a newer copied build-image.yml, never run against oracle-reasoning's
|
|
22
|
-
# set-up, broke it on main after a green gate landed it (ci-workflows
|
|
23
|
-
# docs/dashboard/ci-fragility.md, proposal B). On the PR, build-image's build
|
|
24
|
-
# and guides run against the new copy before main has it.
|
|
25
|
-
#
|
|
26
|
-
# sync-drift re-derives each copy from the canonical (never a second opinion that
|
|
27
|
-
# can disagree), preserving the per-repo bits the gate masks -- the product: keys,
|
|
28
|
-
# the product's own flake.nix, the dprint/gitignore/CLAUDE repo content. The gate runs AFTER
|
|
29
|
-
# it, so a bad rewrite fails check:drift and a breaking convention fails the build,
|
|
30
|
-
# rather than landing silently. Structural per-repo files it never touches.
|
|
31
|
-
#
|
|
32
|
-
# Single-sourced in @norskvideo/ctl-dev-kit (conventions/sync-dev-kit.yml) and
|
|
33
|
-
# copied verbatim into each product repo; the check:drift gate fails CI if a copy
|
|
34
|
-
# diverges. Only the `product:` value below is per-repo (set it to this repo's
|
|
35
|
-
# dashboard key); everything else must match the dev-kit source. Never hand-edit
|
|
36
|
-
# the copy -- edit the dev-kit source and re-sync.
|
|
37
|
-
name: sync-dev-kit
|
|
38
|
-
|
|
39
|
-
on:
|
|
40
|
-
schedule:
|
|
41
|
-
- cron: "37 4 * * *"
|
|
42
|
-
workflow_dispatch:
|
|
43
|
-
|
|
44
|
-
permissions:
|
|
45
|
-
contents: write
|
|
46
|
-
pull-requests: write
|
|
47
|
-
|
|
48
|
-
jobs:
|
|
49
|
-
sync:
|
|
50
|
-
runs-on: x64
|
|
51
|
-
steps:
|
|
52
|
-
# Runs BEFORE checkout, and does two things: points the harness's temp base
|
|
53
|
-
# out of the workspace for every later step in this job, and clears whatever
|
|
54
|
-
# the old repo-local default left behind on this runner. The body carries the
|
|
55
|
-
# why. It fails the job only when legacy litter survives -- which would take
|
|
56
|
-
# the checkout down a step later anyway, but report nothing useful.
|
|
57
|
-
- name: Route test-temp out of the workspace (pre-checkout)
|
|
58
|
-
run: |
|
|
59
|
-
set -uo pipefail
|
|
60
|
-
# These runners are docker-OUTSIDE-of-docker, so a bind-mount source the
|
|
61
|
-
# HOST daemon has not seen before is created by dockerd, as ROOT, on the
|
|
62
|
-
# host -- even when the launch passes --container-user (reproduced
|
|
63
|
-
# 2026-09-04). The harness's default base is repo-local for an OrbStack
|
|
64
|
-
# inotify constraint that binds macOS dev and nothing here, so every run
|
|
65
|
-
# seeded the CHECKOUT with root-owned dirs a non-root `git clean -ffdx`
|
|
66
|
-
# cannot remove. That took commentary's checks AND integration red on
|
|
67
|
-
# 2026-09-03. Use the sanctioned override, per-run and outside the
|
|
68
|
-
# checkout -- as 8ff17a8d did in June for one debug workflow, and stopped.
|
|
69
|
-
base="$RUNNER_TEMP/nctt-$GITHUB_RUN_ID-$GITHUB_RUN_ATTEMPT"
|
|
70
|
-
mkdir -p "$base"
|
|
71
|
-
echo "NORSK_CTL_TEST_TMP=$base" >> "$GITHUB_ENV"
|
|
72
|
-
# Yesterday's bases hold dockerd-created root dirs, so a non-root rm
|
|
73
|
-
# EACCESes. They cannot wedge a checkout, but they do fill the disk.
|
|
74
|
-
for old in $(find "$RUNNER_TEMP" -maxdepth 1 -name 'nctt-*' -mtime +0 -printf '%f\n' 2>/dev/null || true); do
|
|
75
|
-
docker run --rm --user 0:0 -v "$RUNNER_TEMP":/t alpine sh -c 'rm -rf -- "/t/$1"' sh "$old" >/dev/null 2>&1 || true
|
|
76
|
-
done
|
|
77
|
-
# LEGACY: a runner that ran the old default still carries a workspace
|
|
78
|
-
# test-temp, and a checkout with clean:true dies on it. Reap the holders
|
|
79
|
-
# first -- rm cannot unlink a live mountpoint even as root (EBUSY), and a
|
|
80
|
-
# RESTARTING leaked container re-creates the path as root seconds after
|
|
81
|
-
# any clean. Match on mount SOURCE, not a label: the holders are untracked
|
|
82
|
-
# by construction, because a killed run labels nothing.
|
|
83
|
-
tt="$GITHUB_WORKSPACE/test-temp"
|
|
84
|
-
[ -d "$tt" ] || exit 0
|
|
85
|
-
stuck=""
|
|
86
|
-
for c in $(docker ps -aq 2>/dev/null || true); do
|
|
87
|
-
if docker inspect -f '{{range .Mounts}}{{println .Source}}{{end}}' "$c" 2>/dev/null | grep -q "^$tt/"; then
|
|
88
|
-
stuck="$stuck $c"
|
|
89
|
-
fi
|
|
90
|
-
done
|
|
91
|
-
if [ -n "$stuck" ]; then
|
|
92
|
-
echo "reaping containers holding mounts under $tt:$stuck"
|
|
93
|
-
docker rm -f $stuck || true
|
|
94
|
-
fi
|
|
95
|
-
# The DIRECTORY, not just its contents: an empty but root-owned base
|
|
96
|
-
# still EACCESes a later mkdtemp. Mounting the PARENT is what lets a root
|
|
97
|
-
# container unlink the leaf.
|
|
98
|
-
docker run --rm --user 0:0 -v "$GITHUB_WORKSPACE":/w alpine \
|
|
99
|
-
sh -c 'rm -rf /w/test-temp' || rm -rf "$tt" || true
|
|
100
|
-
# Fail HERE if it survived. The checkout fails on it either way, but
|
|
101
|
-
# reports only an EACCES rmdir with no clue what was holding the path.
|
|
102
|
-
if [ -e "$tt" ]; then
|
|
103
|
-
echo "::error::$tt survived the pre-checkout clean"
|
|
104
|
-
ls -lan "$tt" || true
|
|
105
|
-
exit 1
|
|
106
|
-
fi
|
|
107
|
-
|
|
108
|
-
- uses: actions/checkout@v5
|
|
109
|
-
with:
|
|
110
|
-
token: ${{ secrets.CI_DISPATCH_TOKEN }}
|
|
111
|
-
clean: true
|
|
112
|
-
|
|
113
|
-
- name: Resolve the newest published dev-kit and bump the pin
|
|
114
|
-
id: sync
|
|
115
|
-
run: |
|
|
116
|
-
set -euo pipefail
|
|
117
|
-
reg="https://registry.npmjs.org"
|
|
118
|
-
pkg="@norskvideo/ctl-dev-kit"
|
|
119
|
-
enc="@norskvideo%2Fctl-dev-kit"
|
|
120
|
-
|
|
121
|
-
# The runner has no npm; read the dist-tag off the registry with curl+jq
|
|
122
|
-
# (same as upgrade-latest.yml resolves the Norsk nightly).
|
|
123
|
-
new="$(curl -fsSL --retry 3 --retry-all-errors --retry-delay 2 "$reg/$enc" | jq -r '."dist-tags".latest')"
|
|
124
|
-
[ -n "$new" ] && [ "$new" != "null" ] || { echo "could not resolve $pkg latest"; exit 1; }
|
|
125
|
-
|
|
126
|
-
# Every tracked package.json, not just the root. A nested workspace can
|
|
127
|
-
# pin the dev-kit too, and a root-only rewrite left it behind: playout's
|
|
128
|
-
# tests/ sat on 0.1.x after the root moved to 0.2, ran a guide proxy
|
|
129
|
-
# that built the wrong Studio prefix, and first-light failed every
|
|
130
|
-
# night from 2026-09-07. Same walk as sync-ctl-packages.yml.
|
|
131
|
-
mapfile -t manifests < <(git ls-files '*package.json' ':!:**/node_modules/**')
|
|
132
|
-
moved=""
|
|
133
|
-
for m in "${manifests[@]}"; do
|
|
134
|
-
old="$(grep -oE "\"$pkg\"[[:space:]]*:[[:space:]]*\"[^\"]*\"" "$m" | head -1 | sed -E 's/.*"([^"]*)"$/\1/' || true)"
|
|
135
|
-
# Absent here, or a workspace protocol spec that names no version.
|
|
136
|
-
case "$old" in "" | workspace:*) continue ;; esac
|
|
137
|
-
old_bare="${old#^}"; old_bare="${old_bare#~}"
|
|
138
|
-
[ "$old_bare" != "$new" ] || continue
|
|
139
|
-
# Surgical single-line rewrite so the file keeps its formatting
|
|
140
|
-
# (a jq reserialize would reflow it and trip the formatter).
|
|
141
|
-
sed -i -E "s|(\"$pkg\"[[:space:]]*:[[:space:]]*\")[^\"]*(\")|\1^$new\2|" "$m"
|
|
142
|
-
echo "dev-kit: $m $old -> ^$new"
|
|
143
|
-
moved="$moved $old_bare"
|
|
144
|
-
done
|
|
145
|
-
if [ -z "$moved" ]; then
|
|
146
|
-
echo "every dev-kit pin is already on the newest dev-kit ($new)."
|
|
147
|
-
echo "changed=0" >> "$GITHUB_OUTPUT"
|
|
148
|
-
exit 0
|
|
149
|
-
fi
|
|
150
|
-
# `old` names the lowest pin moved, for the commit message and PR title.
|
|
151
|
-
old_bare="$(printf '%s\n' $moved | sort -V | head -1)"
|
|
152
|
-
echo "changed=1" >> "$GITHUB_OUTPUT"
|
|
153
|
-
echo "old=$old_bare" >> "$GITHUB_OUTPUT"
|
|
154
|
-
echo "new=$new" >> "$GITHUB_OUTPUT"
|
|
155
|
-
|
|
156
|
-
- name: Reinstall against the new pin
|
|
157
|
-
if: steps.sync.outputs.changed == '1'
|
|
158
|
-
# NOT continue-on-error, unlike the gate below. An install that dies
|
|
159
|
-
# part-way (the runner OOM-killed sync-ctl-packages' one on 2026-10-03)
|
|
160
|
-
# would otherwise read as a red gate and open a reconciliation PR around
|
|
161
|
-
# a stale lockfile. Fail here instead, before anything is committed.
|
|
162
|
-
run: |
|
|
163
|
-
nix develop .#build --command bash -c '
|
|
164
|
-
set -euo pipefail
|
|
165
|
-
# Per install root, as sync-ctl-packages.yml does: a manifest outside
|
|
166
|
-
# the root workspace owns its own lockfile. Then move the locked
|
|
167
|
-
# dev-kit to the newest in range: bun install keeps a locked version
|
|
168
|
-
# that still satisfies its range, so a nested workspace could
|
|
169
|
-
# otherwise stay on an old dev-kit under a new pin. Only where the
|
|
170
|
-
# lockfile already holds the dev-kit: bun update ADDS a package it
|
|
171
|
-
# does not find, and put the dev-kit into funke proxy/ui that way.
|
|
172
|
-
for lock in $(git ls-files "*bun.lock"); do
|
|
173
|
-
(cd "$(dirname "$lock")" && bun install)
|
|
174
|
-
if grep -q "\"@norskvideo/ctl-dev-kit@" "$lock"; then
|
|
175
|
-
(cd "$(dirname "$lock")" && bun update @norskvideo/ctl-dev-kit)
|
|
176
|
-
fi
|
|
177
|
-
done
|
|
178
|
-
'
|
|
179
|
-
|
|
180
|
-
- name: Re-sync the copies from canonical and run the product gate
|
|
181
|
-
id: gate
|
|
182
|
-
if: steps.sync.outputs.changed == '1'
|
|
183
|
-
continue-on-error: true
|
|
184
|
-
run: |
|
|
185
|
-
nix develop .#build --command bash -c '
|
|
186
|
-
set -euo pipefail
|
|
187
|
-
bun run node_modules/@norskvideo/ctl-dev-kit/conventions/sync-drift.ts
|
|
188
|
-
bun run check:drift
|
|
189
|
-
bun run lint
|
|
190
|
-
bun run typecheck
|
|
191
|
-
bun run test:unit
|
|
192
|
-
'
|
|
193
|
-
|
|
194
|
-
# The bump's only way to main is this PR. bot-prs merges it once every tier
|
|
195
|
-
# that runs on a bot PR -- checks, smoke, integration and build-image's
|
|
196
|
-
# guides gate, wherever this repo has them -- is green on its head
|
|
197
|
-
# (ci-workflows docs/dashboard/ci-fragility.md, proposal B). Landing on a
|
|
198
|
-
# green gate kept lint, typecheck and unit failures off main, but the tiers
|
|
199
|
-
# that break on an upstream change ran only after landing, so a bad
|
|
200
|
-
# upstream release still turned main red in every repo that took it. The
|
|
201
|
-
# gate above still runs: its verdict heads the PR body, and a red one
|
|
202
|
-
# shows on the PR's own checks too. Never a draft: bot-prs reads red from
|
|
203
|
-
# the tiers, and a draft could never become ready again by PATCH.
|
|
204
|
-
- name: Open (or update) the bump PR
|
|
205
|
-
if: steps.sync.outputs.changed == '1'
|
|
206
|
-
env:
|
|
207
|
-
API_TOKEN: ${{ secrets.CI_DISPATCH_TOKEN }}
|
|
208
|
-
GATE: ${{ steps.gate.outcome }}
|
|
209
|
-
run: |
|
|
210
|
-
set -euo pipefail
|
|
211
|
-
branch="sync-dev-kit/${{ steps.sync.outputs.new }}"
|
|
212
|
-
prefix="sync-dev-kit/"
|
|
213
|
-
title="Sync dev-kit ${{ steps.sync.outputs.old }} -> ${{ steps.sync.outputs.new }}"
|
|
214
|
-
repo="${{ github.repository }}"; owner="${repo%%/*}"
|
|
215
|
-
api="https://api.github.com/repos/$repo/pulls"
|
|
216
|
-
# An older PR of this bot's family is obsolete once this one is open:
|
|
217
|
-
# left open they piled up (nine were closed by hand on 2026-10-03).
|
|
218
|
-
# Best-effort: a failure here must not fail the bump.
|
|
219
|
-
close_superseded() {
|
|
220
|
-
local keep="$1" n
|
|
221
|
-
auth=(-H "Authorization: Bearer $API_TOKEN" -H "Accept: application/vnd.github+json")
|
|
222
|
-
for n in $(curl -sS "${auth[@]}" "$api?state=open&per_page=100" \
|
|
223
|
-
| jq -r --arg repo "$repo" --arg prefix "$prefix" --argjson keep "$keep" \
|
|
224
|
-
'.[] | select(.head.repo.full_name == $repo and (.head.ref | startswith($prefix)) and .number != $keep) | .number'); do
|
|
225
|
-
curl -sS -o /dev/null "${auth[@]}" -X POST "https://api.github.com/repos/$repo/issues/$n/comments" \
|
|
226
|
-
-d "$(jq -nc --arg b "Superseded by #$keep, a later run of this bot." '{body:$b}')"
|
|
227
|
-
curl -sS -o /dev/null "${auth[@]}" -X PATCH "$api/$n" -d '{"state":"closed"}'
|
|
228
|
-
echo "closed superseded PR #$n"
|
|
229
|
-
done
|
|
230
|
-
}
|
|
231
|
-
git config user.name "github-actions[bot]"
|
|
232
|
-
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
|
233
|
-
git checkout -B "$branch"
|
|
234
|
-
git add -A
|
|
235
|
-
git commit -m "chore: sync dev-kit ${{ steps.sync.outputs.old }} -> ${{ steps.sync.outputs.new }}
|
|
236
|
-
|
|
237
|
-
@norskvideo/ctl-dev-kit pin bumped to the newest published version and the
|
|
238
|
-
forced copies re-synced from the new canonical (sync-drift). Lockfile
|
|
239
|
-
regenerated."
|
|
240
|
-
# One branch per bump, force-pushed from today's main -- but only when
|
|
241
|
-
# its tree changed. A red bump PR left standing is rebuilt by every run
|
|
242
|
-
# of this bot, and pushing the same tree again would re-run every slow
|
|
243
|
-
# tier on it for nothing.
|
|
244
|
-
if git fetch -q origin "$branch" 2>/dev/null && [ "$(git rev-parse 'HEAD^{tree}')" = "$(git rev-parse 'FETCH_HEAD^{tree}')" ]; then
|
|
245
|
-
echo "$branch already holds this bump; not re-pushing it"
|
|
246
|
-
else
|
|
247
|
-
git push -f origin "$branch"
|
|
248
|
-
fi
|
|
249
|
-
if [ "$GATE" = "success" ]; then
|
|
250
|
-
verdict="Gate green: check:drift, lint, typecheck and unit tests pass against the new dev-kit."
|
|
251
|
-
else
|
|
252
|
-
verdict="**Gate red**: the new dev-kit does not fit this product yet. Run the gate to see which part: \`bun install\`, then \`bun run check:drift\` (a copy sync-drift could not converge) and \`bun run lint && bun run typecheck && bun run test:unit\` (a convention that breaks real code). Fix it on this branch until green."
|
|
253
|
-
fi
|
|
254
|
-
body="$(printf '%s\n' \
|
|
255
|
-
"Automated dev-kit sync: the @norskvideo/ctl-dev-kit pin moved to the newest published version and sync-drift re-synced the forced copies from the new canonical." \
|
|
256
|
-
"" \
|
|
257
|
-
"$verdict" \
|
|
258
|
-
"" \
|
|
259
|
-
"bot-prs merges this once checks, smoke, integration and the guides gate" \
|
|
260
|
-
"(build-image) have all passed on its head, wherever this repo runs them." \
|
|
261
|
-
"Until then main does not move; a red tier keeps it open and reports it on" \
|
|
262
|
-
"the product dashboard.")"
|
|
263
|
-
# gh is not on the self-hosted runner PATH (it lives only inside
|
|
264
|
-
# `nix develop`, and this step runs outside it), so create/update the PR
|
|
265
|
-
# straight against the REST API with the same PAT that pushed the branch.
|
|
266
|
-
# The PAT (not GITHUB_TOKEN) is what lets the PR's own workflows run,
|
|
267
|
-
# which is the whole point: the bump is judged before it lands.
|
|
268
|
-
curl_api=(--fail-with-body -sS -H "Authorization: Bearer $API_TOKEN" -H "Accept: application/vnd.github+json")
|
|
269
|
-
existing="$(curl "${curl_api[@]}" "$api?head=$owner:$branch&state=open" | jq -r '.[0].number // empty')"
|
|
270
|
-
if [ -n "$existing" ]; then
|
|
271
|
-
curl "${curl_api[@]}" -X PATCH "$api/$existing" \
|
|
272
|
-
--data "$(jq -nc --arg t "$title" --arg b "$body" '{title:$t, body:$b}')" > /dev/null
|
|
273
|
-
number="$existing"
|
|
274
|
-
echo "updated PR #$number"
|
|
275
|
-
else
|
|
276
|
-
number="$(curl "${curl_api[@]}" -X POST "$api" \
|
|
277
|
-
--data "$(jq -nc --arg t "$title" --arg b "$body" --arg h "$branch" \
|
|
278
|
-
'{title:$t, body:$b, head:$h, base:"main"}')" | jq -r .number)"
|
|
279
|
-
echo "opened PR #$number"
|
|
280
|
-
fi
|
|
281
|
-
close_superseded "$number" || true
|
|
282
|
-
|
|
283
|
-
- name: Report "already fresh"
|
|
284
|
-
if: steps.sync.outputs.changed != '1'
|
|
285
|
-
run: echo "dev-kit pin already current -- nothing to sync."
|
|
286
|
-
- name: Reap the containers this job launched
|
|
287
|
-
# Whatever the tests did. A teardown cut short by a test timeout leaves
|
|
288
|
-
# its instance running on the shared Docker daemon (see JOB_REAP_STEP in
|
|
289
|
-
# conventions/workflow-shell.ts); everything this job launched mounts its
|
|
290
|
-
# own test-temp, so that is what goes.
|
|
291
|
-
if: always()
|
|
292
|
-
run: |
|
|
293
|
-
base="${NORSK_CTL_TEST_TMP:-}"
|
|
294
|
-
tt="$GITHUB_WORKSPACE/test-temp"
|
|
295
|
-
s=""
|
|
296
|
-
for c in $(docker ps -aq); do
|
|
297
|
-
src="$(docker inspect -f '{{range .Mounts}}{{println .Source}}{{end}}' "$c" 2>/dev/null)" || continue
|
|
298
|
-
if { [ -n "$base" ] && grep -q "^$base/" <<<"$src"; } || grep -q "^$tt/" <<<"$src"; then s="$s $c"; fi
|
|
299
|
-
done
|
|
300
|
-
if [ -n "$s" ]; then
|
|
301
|
-
echo "::warning::removing containers this job left running:$s"
|
|
302
|
-
docker rm -f $s
|
|
303
|
-
fi
|
|
304
|
-
|
|
305
|
-
# Report the sync result to the aggregated product CI dashboard
|
|
306
|
-
# (id3as/ci-workflows). !cancelled() so a failed sync shows a sad pony but a
|
|
307
|
-
# cancelled/superseded run does not false-red the board. A gate-red run that
|
|
308
|
-
# opened a PR is still a successful sync -- the red lives on the PR's own checks
|
|
309
|
-
# run, not here.
|
|
310
|
-
notify:
|
|
311
|
-
needs: sync
|
|
312
|
-
if: ${{ !cancelled() }}
|
|
313
|
-
runs-on: x64
|
|
314
|
-
steps:
|
|
315
|
-
# Runs BEFORE checkout, and does two things: points the harness's temp base
|
|
316
|
-
# out of the workspace for every later step in this job, and clears whatever
|
|
317
|
-
# the old repo-local default left behind on this runner. The body carries the
|
|
318
|
-
# why. It fails the job only when legacy litter survives -- which would take
|
|
319
|
-
# the checkout down a step later anyway, but report nothing useful.
|
|
320
|
-
- name: Route test-temp out of the workspace (pre-checkout)
|
|
321
|
-
run: |
|
|
322
|
-
set -uo pipefail
|
|
323
|
-
# These runners are docker-OUTSIDE-of-docker, so a bind-mount source the
|
|
324
|
-
# HOST daemon has not seen before is created by dockerd, as ROOT, on the
|
|
325
|
-
# host -- even when the launch passes --container-user (reproduced
|
|
326
|
-
# 2026-09-04). The harness's default base is repo-local for an OrbStack
|
|
327
|
-
# inotify constraint that binds macOS dev and nothing here, so every run
|
|
328
|
-
# seeded the CHECKOUT with root-owned dirs a non-root `git clean -ffdx`
|
|
329
|
-
# cannot remove. That took commentary's checks AND integration red on
|
|
330
|
-
# 2026-09-03. Use the sanctioned override, per-run and outside the
|
|
331
|
-
# checkout -- as 8ff17a8d did in June for one debug workflow, and stopped.
|
|
332
|
-
base="$RUNNER_TEMP/nctt-$GITHUB_RUN_ID-$GITHUB_RUN_ATTEMPT"
|
|
333
|
-
mkdir -p "$base"
|
|
334
|
-
echo "NORSK_CTL_TEST_TMP=$base" >> "$GITHUB_ENV"
|
|
335
|
-
# Yesterday's bases hold dockerd-created root dirs, so a non-root rm
|
|
336
|
-
# EACCESes. They cannot wedge a checkout, but they do fill the disk.
|
|
337
|
-
for old in $(find "$RUNNER_TEMP" -maxdepth 1 -name 'nctt-*' -mtime +0 -printf '%f\n' 2>/dev/null || true); do
|
|
338
|
-
docker run --rm --user 0:0 -v "$RUNNER_TEMP":/t alpine sh -c 'rm -rf -- "/t/$1"' sh "$old" >/dev/null 2>&1 || true
|
|
339
|
-
done
|
|
340
|
-
# LEGACY: a runner that ran the old default still carries a workspace
|
|
341
|
-
# test-temp, and a checkout with clean:true dies on it. Reap the holders
|
|
342
|
-
# first -- rm cannot unlink a live mountpoint even as root (EBUSY), and a
|
|
343
|
-
# RESTARTING leaked container re-creates the path as root seconds after
|
|
344
|
-
# any clean. Match on mount SOURCE, not a label: the holders are untracked
|
|
345
|
-
# by construction, because a killed run labels nothing.
|
|
346
|
-
tt="$GITHUB_WORKSPACE/test-temp"
|
|
347
|
-
[ -d "$tt" ] || exit 0
|
|
348
|
-
stuck=""
|
|
349
|
-
for c in $(docker ps -aq 2>/dev/null || true); do
|
|
350
|
-
if docker inspect -f '{{range .Mounts}}{{println .Source}}{{end}}' "$c" 2>/dev/null | grep -q "^$tt/"; then
|
|
351
|
-
stuck="$stuck $c"
|
|
352
|
-
fi
|
|
353
|
-
done
|
|
354
|
-
if [ -n "$stuck" ]; then
|
|
355
|
-
echo "reaping containers holding mounts under $tt:$stuck"
|
|
356
|
-
docker rm -f $stuck || true
|
|
357
|
-
fi
|
|
358
|
-
# The DIRECTORY, not just its contents: an empty but root-owned base
|
|
359
|
-
# still EACCESes a later mkdtemp. Mounting the PARENT is what lets a root
|
|
360
|
-
# container unlink the leaf.
|
|
361
|
-
docker run --rm --user 0:0 -v "$GITHUB_WORKSPACE":/w alpine \
|
|
362
|
-
sh -c 'rm -rf /w/test-temp' || rm -rf "$tt" || true
|
|
363
|
-
# Fail HERE if it survived. The checkout fails on it either way, but
|
|
364
|
-
# reports only an EACCES rmdir with no clue what was holding the path.
|
|
365
|
-
if [ -e "$tt" ]; then
|
|
366
|
-
echo "::error::$tt survived the pre-checkout clean"
|
|
367
|
-
ls -lan "$tt" || true
|
|
368
|
-
exit 1
|
|
369
|
-
fi
|
|
370
|
-
|
|
371
|
-
- uses: actions/checkout@v5
|
|
372
|
-
with:
|
|
373
|
-
clean: true
|
|
374
|
-
- id: meta
|
|
375
|
-
run: |
|
|
376
|
-
if [ "${{ contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') }}" = "true" ]; then
|
|
377
|
-
echo "state=failure" >> "$GITHUB_OUTPUT"
|
|
378
|
-
else
|
|
379
|
-
echo "state=success" >> "$GITHUB_OUTPUT"
|
|
380
|
-
fi
|
|
381
|
-
- uses: ./.github/actions/ci-status-dispatch
|
|
382
|
-
with:
|
|
383
|
-
token: ${{ secrets.CI_DISPATCH_TOKEN }}
|
|
384
|
-
product: __PRODUCT__
|
|
385
|
-
pipeline: dev-kit
|
|
386
|
-
status: ${{ steps.meta.outputs.state }}
|
|
387
|
-
- name: Reap the containers this job launched
|
|
388
|
-
# Whatever the tests did. A teardown cut short by a test timeout leaves
|
|
389
|
-
# its instance running on the shared Docker daemon (see JOB_REAP_STEP in
|
|
390
|
-
# conventions/workflow-shell.ts); everything this job launched mounts its
|
|
391
|
-
# own test-temp, so that is what goes.
|
|
392
|
-
if: always()
|
|
393
|
-
run: |
|
|
394
|
-
base="${NORSK_CTL_TEST_TMP:-}"
|
|
395
|
-
tt="$GITHUB_WORKSPACE/test-temp"
|
|
396
|
-
s=""
|
|
397
|
-
for c in $(docker ps -aq); do
|
|
398
|
-
src="$(docker inspect -f '{{range .Mounts}}{{println .Source}}{{end}}' "$c" 2>/dev/null)" || continue
|
|
399
|
-
if { [ -n "$base" ] && grep -q "^$base/" <<<"$src"; } || grep -q "^$tt/" <<<"$src"; then s="$s $c"; fi
|
|
400
|
-
done
|
|
401
|
-
if [ -n "$s" ]; then
|
|
402
|
-
echo "::warning::removing containers this job left running:$s"
|
|
403
|
-
docker rm -f $s
|
|
404
|
-
fi
|