@norskvideo/ctl-dev-kit 0.1.65 → 0.1.67
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.
|
@@ -238,6 +238,9 @@ export interface CanonicalBytes {
|
|
|
238
238
|
flake: string;
|
|
239
239
|
upgradeLatest: string;
|
|
240
240
|
syncDevKit: string;
|
|
241
|
+
/** conventions/sync-ctl-packages.yml — the ctl-* pin freshness PR. No per-repo
|
|
242
|
+
* line at all, so it is verbatim and sync-drift creates it where absent. */
|
|
243
|
+
syncCtlPackages: string;
|
|
241
244
|
publishDocs: string;
|
|
242
245
|
/** conventions/build-docs.yml — the embedded docs variant (bundle -> image, no upload). */
|
|
243
246
|
buildDocs: string;
|
|
@@ -513,6 +516,14 @@ export function checkDrift(repoRoot: string, canonical: CanonicalBytes, opts: Ch
|
|
|
513
516
|
);
|
|
514
517
|
}
|
|
515
518
|
|
|
519
|
+
// sync-ctl-packages.yml is OPTIONAL until a repo has been synced once — it is
|
|
520
|
+
// NEW, and sync-drift creates it. Unlike its neighbours it carries no per-repo
|
|
521
|
+
// line (it dispatches to no dashboard), so the whole file is canonical.
|
|
522
|
+
const syncCtlPath = join(repoRoot, ".github", "workflows", "sync-ctl-packages.yml");
|
|
523
|
+
if (existsSync(syncCtlPath) && readFileSync(syncCtlPath, "utf8") !== canonical.syncCtlPackages) {
|
|
524
|
+
push(`.github/workflows/sync-ctl-packages.yml differs from conventions/sync-ctl-packages.yml. ${RESYNC}`);
|
|
525
|
+
}
|
|
526
|
+
|
|
516
527
|
// smoke.yml is OPTIONAL until every product carries a tests/smoke.spec.ts
|
|
517
528
|
// (04b s7.2 sequences the required-file rule last). When present it is the
|
|
518
529
|
// shared, single-sourced workflow; only its `product:` dispatch key and the
|
|
@@ -569,6 +580,7 @@ if (import.meta.main) {
|
|
|
569
580
|
flake: readFileSync(join(import.meta.dir, "..", "build", "flake.nix"), "utf8"),
|
|
570
581
|
upgradeLatest: readFileSync(join(import.meta.dir, "upgrade-latest.yml"), "utf8"),
|
|
571
582
|
syncDevKit: readFileSync(join(import.meta.dir, "sync-dev-kit.yml"), "utf8"),
|
|
583
|
+
syncCtlPackages: readFileSync(join(import.meta.dir, "sync-ctl-packages.yml"), "utf8"),
|
|
572
584
|
publishDocs: readFileSync(join(import.meta.dir, "publish-docs.yml"), "utf8"),
|
|
573
585
|
buildDocs: readFileSync(join(import.meta.dir, "build-docs.yml"), "utf8"),
|
|
574
586
|
checks: readFileSync(join(import.meta.dir, "checks.yml"), "utf8"),
|
package/conventions/smoke.yml
CHANGED
|
@@ -102,37 +102,47 @@ jobs:
|
|
|
102
102
|
# gracefully, and this workflow cancels its own superseded runs -- a killed
|
|
103
103
|
# daemon reaps nothing and the port stays held for every later job here.
|
|
104
104
|
#
|
|
105
|
-
# On 2026-08-30 something held it on
|
|
105
|
+
# On 2026-08-30 something held it on ar-arm-vm from 11:18 to ~16:00 and
|
|
106
106
|
# took out the next nine smoke and integration jobs across four repos.
|
|
107
107
|
# What held it was never established: the job that last bound it PASSED
|
|
108
|
-
# and shut down cleanly,
|
|
109
|
-
#
|
|
110
|
-
# "already allocated"
|
|
111
|
-
#
|
|
112
|
-
# occurrence as much of a mystery as this one.
|
|
108
|
+
# and shut down cleanly, and every add-path failure already removes its
|
|
109
|
+
# own container -- so the holder may not have been a control plane at all.
|
|
110
|
+
# "already allocated" is also what a dangling endpoint or a stranded
|
|
111
|
+
# docker-proxy reports after an unclean removal.
|
|
113
112
|
#
|
|
114
|
-
#
|
|
115
|
-
#
|
|
116
|
-
#
|
|
113
|
+
# Hence: kill by PORT rather than by name, prune the endpoints a removal
|
|
114
|
+
# can strand, and then PROVE the port binds instead of assuming it. This
|
|
115
|
+
# branch only runs where the docker daemon is this runner's own and jobs
|
|
116
|
+
# are serial, so there is nothing else here to protect. The GPU box runs
|
|
117
|
+
# several runners as sibling CONTAINERS against one host daemon and is
|
|
118
|
+
# excluded -- there, a blanket reap would kill a concurrent job.
|
|
117
119
|
- name: Reap a stranded product control plane
|
|
118
120
|
if: ${{ env.NORSK_RUNNER_OWNS_DOCKER == 'true' }}
|
|
119
121
|
run: |
|
|
120
122
|
set -uo pipefail
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
123
|
+
port=14321
|
|
124
|
+
bindable() { docker run --rm -p "127.0.0.1:$port:1" alpine true >/dev/null 2>&1; }
|
|
125
|
+
|
|
126
|
+
held="$(docker ps -a --filter "publish=$port" --format '{{.Names}} {{.Status}} {{.Image}}')"
|
|
127
|
+
if [ -n "$held" ]; then
|
|
128
|
+
echo "::warning::$port was held on entry by: $held"
|
|
129
|
+
docker rm -f $(docker ps -aq --filter "publish=$port") || true
|
|
126
130
|
fi
|
|
127
|
-
#
|
|
128
|
-
#
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
131
|
+
# A control plane that is stopped, or one whose publish docker no
|
|
132
|
+
# longer reports, still gets removed: nothing here outlives a job.
|
|
133
|
+
leftover="$(docker ps -aq --filter 'name=^norsk-product-')"
|
|
134
|
+
[ -z "$leftover" ] || docker rm -f $leftover || true
|
|
135
|
+
|
|
136
|
+
if ! bindable; then
|
|
137
|
+
echo "::warning::$port still not bindable after removing containers -- pruning dangling endpoints"
|
|
138
|
+
docker network prune -f || true
|
|
139
|
+
fi
|
|
140
|
+
if ! bindable; then
|
|
141
|
+
echo "::error::cannot bind 127.0.0.1:$port; the journey would fail later and more obscurely"
|
|
142
|
+
docker ps -a --filter "publish=$port" || true
|
|
143
|
+
exit 1
|
|
135
144
|
fi
|
|
145
|
+
echo "$port is bindable"
|
|
136
146
|
|
|
137
147
|
- name: Write the Norsk license (from the org secret)
|
|
138
148
|
env:
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
# Keep this product fresh against the ctl-* npm family: the test harness that
|
|
2
|
+
# drives its smoke and integration tiers, the SDK, the foundation helpers and
|
|
3
|
+
# the product-template schema.
|
|
4
|
+
#
|
|
5
|
+
# sync-dev-kit already does this for the conventions package, and upgrade-latest
|
|
6
|
+
# does it for the Norsk nightly line. Nothing did it for the rest, so the pins
|
|
7
|
+
# drifted silently: on 2026-08-30 studio carried a conventions pin bumped that
|
|
8
|
+
# morning beside an SDK three releases behind, and three repos ran three
|
|
9
|
+
# different vintages of the same smoke runner. The caret ranges admit the new
|
|
10
|
+
# versions -- `--frozen-lockfile` is what holds them back, and nothing ever
|
|
11
|
+
# regenerated the lockfile.
|
|
12
|
+
#
|
|
13
|
+
# Unlike sync-dev-kit this NEVER pushes to main. Its gate is drift + lint +
|
|
14
|
+
# typecheck + unit, and a bump to the test harness changes the code that drives
|
|
15
|
+
# smoke and integration -- neither of which that gate runs, because both need
|
|
16
|
+
# Docker and a licence. A green gate therefore proves strictly less here than it
|
|
17
|
+
# does for a conventions bump, so the bump goes to a human as a PR. A red gate
|
|
18
|
+
# still opens one, as a draft: adopting a breaking change is real engineering,
|
|
19
|
+
# not a copy-paste, and the PR is where that work belongs.
|
|
20
|
+
#
|
|
21
|
+
# Single-sourced in @norskvideo/ctl-dev-kit (conventions/sync-ctl-packages.yml)
|
|
22
|
+
# and copied verbatim into each product repo; the check:drift gate fails CI if a
|
|
23
|
+
# copy diverges. Never hand-edit the copy -- edit the source and re-sync.
|
|
24
|
+
name: sync-ctl-packages
|
|
25
|
+
|
|
26
|
+
on:
|
|
27
|
+
schedule:
|
|
28
|
+
# Clear of sync-dev-kit's 04:37 so the two never race the same lockfile.
|
|
29
|
+
- cron: "52 4 * * *"
|
|
30
|
+
workflow_dispatch:
|
|
31
|
+
|
|
32
|
+
permissions:
|
|
33
|
+
contents: write
|
|
34
|
+
pull-requests: write
|
|
35
|
+
|
|
36
|
+
concurrency:
|
|
37
|
+
group: sync-ctl-packages-${{ github.ref }}
|
|
38
|
+
cancel-in-progress: true
|
|
39
|
+
|
|
40
|
+
jobs:
|
|
41
|
+
sync:
|
|
42
|
+
runs-on: x64
|
|
43
|
+
steps:
|
|
44
|
+
# CI_DISPATCH_TOKEN, not the automatic GITHUB_TOKEN: a PR opened by
|
|
45
|
+
# GITHUB_TOKEN raises no workflow runs, so the product's own build, smoke
|
|
46
|
+
# and integration would never execute against the new pins -- which is the
|
|
47
|
+
# entire reason for opening it. clean: false for the same reason as the
|
|
48
|
+
# other shared jobs: the suites leave root-owned bind-mount dirs under
|
|
49
|
+
# test-temp/ that a non-root git clean cannot remove.
|
|
50
|
+
- uses: actions/checkout@v5
|
|
51
|
+
with:
|
|
52
|
+
token: ${{ secrets.CI_DISPATCH_TOKEN }}
|
|
53
|
+
clean: false
|
|
54
|
+
|
|
55
|
+
- name: Resolve the newest published ctl-* packages and rewrite the pins
|
|
56
|
+
id: bump
|
|
57
|
+
run: |
|
|
58
|
+
set -euo pipefail
|
|
59
|
+
reg="https://registry.npmjs.org"
|
|
60
|
+
# The conventions package is deliberately absent: sync-dev-kit owns it,
|
|
61
|
+
# lands it straight to main, and re-syncs the copied files with it.
|
|
62
|
+
family="ctl-test-harness ctl-sdk ctl-foundation ctl-product-template-schema"
|
|
63
|
+
|
|
64
|
+
# Every tracked package.json, not just the root: a repo can pin the
|
|
65
|
+
# same package in a nested workspace (playout pins the harness twice),
|
|
66
|
+
# and a root-only rewrite would split the line across the two.
|
|
67
|
+
mapfile -t manifests < <(git ls-files '*package.json' ':!:**/node_modules/**')
|
|
68
|
+
|
|
69
|
+
moved=""
|
|
70
|
+
for short in $family; do
|
|
71
|
+
pkg="@norskvideo/$short"
|
|
72
|
+
enc="@norskvideo%2F$short"
|
|
73
|
+
# The runner has no npm; read the dist-tag off the registry with
|
|
74
|
+
# curl+jq, as sync-dev-kit and upgrade-latest both do.
|
|
75
|
+
new="$(curl -fsSL --retry 3 --retry-all-errors --retry-delay 2 "$reg/$enc" | jq -r '."dist-tags".latest')"
|
|
76
|
+
[ -n "$new" ] && [ "$new" != "null" ] || { echo "could not resolve $pkg latest"; exit 1; }
|
|
77
|
+
|
|
78
|
+
for m in "${manifests[@]}"; do
|
|
79
|
+
old="$(grep -oE "\"$pkg\"[[:space:]]*:[[:space:]]*\"[^\"]*\"" "$m" | head -1 | sed -E 's/.*"([^"]*)"$/\1/' || true)"
|
|
80
|
+
# Absent here, or a workspace protocol spec that names no version.
|
|
81
|
+
case "$old" in "" | workspace:*) continue ;; esac
|
|
82
|
+
old_bare="${old#^}"; old_bare="${old_bare#~}"
|
|
83
|
+
[ "$old_bare" != "$new" ] || continue
|
|
84
|
+
# Surgical single-line rewrite so the file keeps its formatting;
|
|
85
|
+
# a jq reserialize would reflow it and trip the formatter.
|
|
86
|
+
sed -i -E "s|(\"$pkg\"[[:space:]]*:[[:space:]]*\")[^\"]*(\")|\1^$new\2|" "$m"
|
|
87
|
+
echo "$m: $pkg $old -> ^$new"
|
|
88
|
+
moved="$moved- \`$pkg\` $old -> \`^$new\` ($m)"$'\n'
|
|
89
|
+
done
|
|
90
|
+
done
|
|
91
|
+
|
|
92
|
+
if [ -z "$moved" ]; then
|
|
93
|
+
echo "every ctl-* pin is already on its newest published version."
|
|
94
|
+
echo "changed=0" >> "$GITHUB_OUTPUT"
|
|
95
|
+
else
|
|
96
|
+
echo "changed=1" >> "$GITHUB_OUTPUT"
|
|
97
|
+
{ echo "moved<<PINS"; printf '%s' "$moved"; echo "PINS"; } >> "$GITHUB_OUTPUT"
|
|
98
|
+
fi
|
|
99
|
+
|
|
100
|
+
- name: Reinstall and run the product gate
|
|
101
|
+
id: gate
|
|
102
|
+
if: steps.bump.outputs.changed == '1'
|
|
103
|
+
continue-on-error: true
|
|
104
|
+
run: |
|
|
105
|
+
nix develop .#build --command bash -c '
|
|
106
|
+
set -euo pipefail
|
|
107
|
+
bun install
|
|
108
|
+
bun run check:drift
|
|
109
|
+
bun run lint
|
|
110
|
+
bun run typecheck
|
|
111
|
+
bun run test:unit
|
|
112
|
+
'
|
|
113
|
+
|
|
114
|
+
- name: Open (or update) the pin-freshness PR
|
|
115
|
+
if: steps.bump.outputs.changed == '1'
|
|
116
|
+
env:
|
|
117
|
+
GH_TOKEN: ${{ secrets.CI_DISPATCH_TOKEN }}
|
|
118
|
+
GATE: ${{ steps.gate.outcome }}
|
|
119
|
+
run: |
|
|
120
|
+
set -euo pipefail
|
|
121
|
+
branch="chore/sync-ctl-packages"
|
|
122
|
+
git config user.name "github-actions[bot]"
|
|
123
|
+
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
|
124
|
+
git checkout -B "$branch"
|
|
125
|
+
git add -A
|
|
126
|
+
git commit -m "chore: sync the ctl-* pins to their newest published versions"
|
|
127
|
+
# One branch, force-pushed: a daily schedule must refresh the standing
|
|
128
|
+
# PR rather than accumulate a new one every morning.
|
|
129
|
+
git push -f origin "$branch"
|
|
130
|
+
|
|
131
|
+
if [ "$GATE" = "success" ]; then
|
|
132
|
+
verdict="Gate green: check:drift, lint, typecheck and unit tests all pass on the new pins."
|
|
133
|
+
draft=""
|
|
134
|
+
else
|
|
135
|
+
verdict="**Gate red** — the new pins do not fit this product as-is. Adopting them is real engineering, not a merge."
|
|
136
|
+
draft="--draft"
|
|
137
|
+
fi
|
|
138
|
+
|
|
139
|
+
body="$(cat <<EOF
|
|
140
|
+
Automated by \`sync-ctl-packages\`.
|
|
141
|
+
|
|
142
|
+
## Pins moved
|
|
143
|
+
|
|
144
|
+
${{ steps.bump.outputs.moved }}
|
|
145
|
+
|
|
146
|
+
## What the gate proved
|
|
147
|
+
|
|
148
|
+
$verdict
|
|
149
|
+
|
|
150
|
+
It did **not** run smoke or integration — both need Docker and a licence.
|
|
151
|
+
A test-harness bump changes the code that drives those tiers, so the
|
|
152
|
+
runs raised on this PR are the real evidence. Read them before merging.
|
|
153
|
+
EOF
|
|
154
|
+
)"
|
|
155
|
+
|
|
156
|
+
if [ -z "$(gh pr list --head "$branch" --state open --json number --jq '.[].number')" ]; then
|
|
157
|
+
gh pr create $draft --head "$branch" --base main \
|
|
158
|
+
--title "chore: sync the ctl-* pins to their newest published versions" \
|
|
159
|
+
--body "$body"
|
|
160
|
+
else
|
|
161
|
+
gh pr edit "$branch" --body "$body"
|
|
162
|
+
fi
|
|
@@ -223,6 +223,15 @@ export function syncDrift(repoRoot: string, canonical: CanonicalBytes): SyncRepo
|
|
|
223
223
|
syncWorkflow(repoRoot, ".github/workflows/checks.yml", canonical.checks, r);
|
|
224
224
|
syncWorkflow(repoRoot, ".github/workflows/upgrade-latest.yml", canonical.upgradeLatest, r);
|
|
225
225
|
syncWorkflow(repoRoot, ".github/workflows/sync-dev-kit.yml", canonical.syncDevKit, r);
|
|
226
|
+
// No per-repo line to preserve, so it is written whole -- and CREATED where
|
|
227
|
+
// absent, unlike syncWorkflow's copies. That is how a repo acquires the
|
|
228
|
+
// workflow at all: its next sync run installs it.
|
|
229
|
+
writeIfChanged(
|
|
230
|
+
join(repoRoot, ".github/workflows/sync-ctl-packages.yml"),
|
|
231
|
+
canonical.syncCtlPackages,
|
|
232
|
+
".github/workflows/sync-ctl-packages.yml",
|
|
233
|
+
r.written,
|
|
234
|
+
);
|
|
226
235
|
syncBuildImage(repoRoot, canonical.buildImage, r);
|
|
227
236
|
syncSmoke(repoRoot, canonical.smoke, r);
|
|
228
237
|
|
|
@@ -256,6 +265,7 @@ if (import.meta.main) {
|
|
|
256
265
|
flake: readFileSync(join(dir, "..", "build", "flake.nix"), "utf8"),
|
|
257
266
|
upgradeLatest: readFileSync(join(dir, "upgrade-latest.yml"), "utf8"),
|
|
258
267
|
syncDevKit: readFileSync(join(dir, "sync-dev-kit.yml"), "utf8"),
|
|
268
|
+
syncCtlPackages: readFileSync(join(dir, "sync-ctl-packages.yml"), "utf8"),
|
|
259
269
|
publishDocs: readFileSync(join(dir, "publish-docs.yml"), "utf8"),
|
|
260
270
|
buildDocs: readFileSync(join(dir, "build-docs.yml"), "utf8"),
|
|
261
271
|
checks: readFileSync(join(dir, "checks.yml"), "utf8"),
|
package/create-product/canon.ts
CHANGED
|
@@ -18,6 +18,7 @@ export interface Canon {
|
|
|
18
18
|
flakeLock: string;
|
|
19
19
|
upgradeLatest: string;
|
|
20
20
|
syncDevKit: string;
|
|
21
|
+
syncCtlPackages: string;
|
|
21
22
|
checks: string;
|
|
22
23
|
biome: string;
|
|
23
24
|
tsconfigBase: string;
|
|
@@ -39,6 +40,7 @@ export function loadCanon(): Canon {
|
|
|
39
40
|
flakeLock: readFileSync(join(buildDir, "flake.lock"), "utf8"),
|
|
40
41
|
upgradeLatest: readFileSync(join(conventionsDir, "upgrade-latest.yml"), "utf8"),
|
|
41
42
|
syncDevKit: readFileSync(join(conventionsDir, "sync-dev-kit.yml"), "utf8"),
|
|
43
|
+
syncCtlPackages: readFileSync(join(conventionsDir, "sync-ctl-packages.yml"), "utf8"),
|
|
42
44
|
checks: readFileSync(join(conventionsDir, "checks.yml"), "utf8"),
|
|
43
45
|
buildDocs: readFileSync(join(conventionsDir, "build-docs.yml"), "utf8"),
|
|
44
46
|
biome: readFileSync(join(conventionsDir, "biome.base.json"), "utf8"),
|
|
@@ -111,6 +111,7 @@ function conventionFiles(ctx: ShapeContext, shape: ShapeModule): GeneratedFile[]
|
|
|
111
111
|
{ path: ".github/workflows/checks.yml", content: fillProduct(canon.checks) },
|
|
112
112
|
{ path: ".github/workflows/upgrade-latest.yml", content: fillProduct(canon.upgradeLatest) },
|
|
113
113
|
{ path: ".github/workflows/sync-dev-kit.yml", content: fillProduct(canon.syncDevKit) },
|
|
114
|
+
{ path: ".github/workflows/sync-ctl-packages.yml", content: canon.syncCtlPackages },
|
|
114
115
|
{ path: ".github/workflows/build-image.yml", content: fillProduct(canon.buildImage) },
|
|
115
116
|
{ path: ".github/actions/ci-status-dispatch/action.yml", content: loadAsset("ci-status-dispatch.yml") },
|
|
116
117
|
{ path: "manifest.seed.json", content: seedJson(ctx) },
|