@norskvideo/ctl-dev-kit 0.2.54 → 0.2.56
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/bot-prs.yml +57 -5
- package/conventions/check-drift.ts +56 -1
- package/conventions/docs-site.md +96 -17
- package/conventions/docs.md +52 -5
- package/conventions/personas.md +25 -0
- package/conventions/publish-docs-site.yml +247 -0
- package/conventions/sync-drift.ts +53 -2
- package/create-product/docs-site.ts +9 -2
- package/create-product/turnkey.ts +121 -14
- package/doc-guide/doc-guide.d.ts +104 -9
- package/doc-guide/doc-guide.js +210 -25
- package/doc-guide/guides-config.d.ts +6 -0
- package/doc-guide/guides-config.js +4 -0
- package/doc-guide/live-browser.d.ts +2 -0
- package/doc-guide/live-browser.js +5 -1
- package/docs/guide-stubs.ts +304 -0
- package/docs/journeys.ts +217 -0
- package/docs/light-pages.ts +205 -0
- package/docs-theme/components/InstallProduct.astro +51 -0
- package/package.json +2 -1
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
# Publish this product's DOCS SITE -- the public manual at <product>.norsk.video
|
|
2
|
+
# (website/, an Astro + Starlight site on the shared Norsk theme) -- to
|
|
3
|
+
# Cloudflare Pages on every push to main.
|
|
4
|
+
#
|
|
5
|
+
# This is the SITE channel, not the bundle: publish-docs.yml / build-docs.yml
|
|
6
|
+
# regenerate the illustrated manual the product image serves at /docs, nightly,
|
|
7
|
+
# from a licensed Docker run. The site builds from the committed prose alone,
|
|
8
|
+
# needs no licence and no Docker, and so rides every push (conventions/
|
|
9
|
+
# docs-site.md, "Two documentation channels"). A site that also symlinks
|
|
10
|
+
# RECORDED guides (website/src/content/docs/guides/<slug> -> docs/generated/<slug>)
|
|
11
|
+
# builds against a placeholder when the capture is absent, and the gate below
|
|
12
|
+
# refuses to deploy one.
|
|
13
|
+
#
|
|
14
|
+
# This workflow is SINGLE-SOURCED in @norskvideo/ctl-dev-kit
|
|
15
|
+
# (conventions/publish-docs-site.yml) and copied verbatim: there is no per-repo
|
|
16
|
+
# line. Everything per-site lives in the repo -- website/astro.config.mjs, the
|
|
17
|
+
# root `docs:site:public` script -- or in the repo's settings:
|
|
18
|
+
#
|
|
19
|
+
# variable NORSK_DOCS_SITE_PROJECT the Cloudflare Pages project name.
|
|
20
|
+
# NO DEFAULT: unset means the project does
|
|
21
|
+
# not exist yet, and the deploy step says
|
|
22
|
+
# what to create and exits 0. So main is
|
|
23
|
+
# green before Cloudflare exists, and the
|
|
24
|
+
# site publishes the day the variable is
|
|
25
|
+
# set, with no change here.
|
|
26
|
+
# secret CLOUDFLARE_API_TOKEN a Pages:Edit token covering that project
|
|
27
|
+
# secret CLOUDFLARE_ACCOUNT_ID the account the project lives in
|
|
28
|
+
#
|
|
29
|
+
# The site is BUILT whether or not it will be deployed: if the build is only
|
|
30
|
+
# exercised on the day the Pages project appears, that day is when we find out
|
|
31
|
+
# it was broken.
|
|
32
|
+
#
|
|
33
|
+
# The drift gate (check-drift.ts) holds the copy byte-identical, allows it only
|
|
34
|
+
# in a repo that has a site (website/astro.config.mjs), and refuses it in a
|
|
35
|
+
# turnkey (norsk-ctl-turnkey-*): a turnkey has no public site, its documentation
|
|
36
|
+
# reaches one customer inside the image they run (ADR-0012).
|
|
37
|
+
name: publish-docs-site
|
|
38
|
+
|
|
39
|
+
on:
|
|
40
|
+
push:
|
|
41
|
+
branches: [main]
|
|
42
|
+
workflow_dispatch:
|
|
43
|
+
|
|
44
|
+
permissions:
|
|
45
|
+
contents: read
|
|
46
|
+
|
|
47
|
+
# Never cancel in flight: a cancelled `wrangler pages deploy` is a half-published
|
|
48
|
+
# site, and the next push rebuilds from the same inputs anyway. Queue instead.
|
|
49
|
+
concurrency:
|
|
50
|
+
group: publish-docs-site
|
|
51
|
+
cancel-in-progress: false
|
|
52
|
+
|
|
53
|
+
jobs:
|
|
54
|
+
publish:
|
|
55
|
+
runs-on: x64
|
|
56
|
+
steps:
|
|
57
|
+
# Runs BEFORE checkout, and does two things: points the harness's temp base
|
|
58
|
+
# out of the workspace for every later step in this job, and clears whatever
|
|
59
|
+
# the old repo-local default left behind on this runner. The body carries the
|
|
60
|
+
# why. It fails the job only when legacy litter survives -- which would take
|
|
61
|
+
# the checkout down a step later anyway, but report nothing useful.
|
|
62
|
+
- name: Route test-temp out of the workspace (pre-checkout)
|
|
63
|
+
run: |
|
|
64
|
+
set -uo pipefail
|
|
65
|
+
# These runners are docker-OUTSIDE-of-docker, so a bind-mount source the
|
|
66
|
+
# HOST daemon has not seen before is created by dockerd, as ROOT, on the
|
|
67
|
+
# host -- even when the launch passes --container-user (reproduced
|
|
68
|
+
# 2026-09-04). The harness's default base is repo-local for an OrbStack
|
|
69
|
+
# inotify constraint that binds macOS dev and nothing here, so every run
|
|
70
|
+
# seeded the CHECKOUT with root-owned dirs a non-root `git clean -ffdx`
|
|
71
|
+
# cannot remove. That took commentary's checks AND integration red on
|
|
72
|
+
# 2026-09-03. Use the sanctioned override, per-run and outside the
|
|
73
|
+
# checkout -- as 8ff17a8d did in June for one debug workflow, and stopped.
|
|
74
|
+
base="$RUNNER_TEMP/nctt-$GITHUB_RUN_ID-$GITHUB_RUN_ATTEMPT"
|
|
75
|
+
mkdir -p "$base"
|
|
76
|
+
echo "NORSK_CTL_TEST_TMP=$base" >> "$GITHUB_ENV"
|
|
77
|
+
# Yesterday's bases hold dockerd-created root dirs, so a non-root rm
|
|
78
|
+
# EACCESes. They cannot wedge a checkout, but they do fill the disk.
|
|
79
|
+
for old in $(find "$RUNNER_TEMP" -maxdepth 1 -name 'nctt-*' -mtime +0 -printf '%f\n' 2>/dev/null || true); do
|
|
80
|
+
docker run --rm --user 0:0 -v "$RUNNER_TEMP":/t alpine sh -c 'rm -rf -- "/t/$1"' sh "$old" >/dev/null 2>&1 || true
|
|
81
|
+
done
|
|
82
|
+
# LEGACY: a runner that ran the old default still carries a workspace
|
|
83
|
+
# test-temp, and a checkout with clean:true dies on it. Reap the holders
|
|
84
|
+
# first -- rm cannot unlink a live mountpoint even as root (EBUSY), and a
|
|
85
|
+
# RESTARTING leaked container re-creates the path as root seconds after
|
|
86
|
+
# any clean. Match on mount SOURCE, not a label: the holders are untracked
|
|
87
|
+
# by construction, because a killed run labels nothing.
|
|
88
|
+
tt="$GITHUB_WORKSPACE/test-temp"
|
|
89
|
+
[ -d "$tt" ] || exit 0
|
|
90
|
+
stuck=""
|
|
91
|
+
for c in $(docker ps -aq 2>/dev/null || true); do
|
|
92
|
+
if docker inspect -f '{{range .Mounts}}{{println .Source}}{{end}}' "$c" 2>/dev/null | grep -q "^$tt/"; then
|
|
93
|
+
stuck="$stuck $c"
|
|
94
|
+
fi
|
|
95
|
+
done
|
|
96
|
+
if [ -n "$stuck" ]; then
|
|
97
|
+
echo "reaping containers holding mounts under $tt:$stuck"
|
|
98
|
+
docker rm -f $stuck || true
|
|
99
|
+
fi
|
|
100
|
+
# The DIRECTORY, not just its contents: an empty but root-owned base
|
|
101
|
+
# still EACCESes a later mkdtemp. Mounting the PARENT is what lets a root
|
|
102
|
+
# container unlink the leaf.
|
|
103
|
+
docker run --rm --user 0:0 -v "$GITHUB_WORKSPACE":/w alpine \
|
|
104
|
+
sh -c 'rm -rf /w/test-temp' || rm -rf "$tt" || true
|
|
105
|
+
# Fail HERE if it survived. The checkout fails on it either way, but
|
|
106
|
+
# reports only an EACCES rmdir with no clue what was holding the path.
|
|
107
|
+
if [ -e "$tt" ]; then
|
|
108
|
+
echo "::error::$tt survived the pre-checkout clean"
|
|
109
|
+
ls -lan "$tt" || true
|
|
110
|
+
exit 1
|
|
111
|
+
fi
|
|
112
|
+
|
|
113
|
+
- uses: actions/checkout@v5
|
|
114
|
+
with:
|
|
115
|
+
clean: true
|
|
116
|
+
|
|
117
|
+
# website/ is a STANDALONE package -- its own bun.lock, outside the root
|
|
118
|
+
# workspaces -- so the repo-root install never covers it, and the build
|
|
119
|
+
# needs both: the root for @norskvideo/ctl-dev-kit (docs:site:public stubs
|
|
120
|
+
# the recorded guides through its docs/guide-stubs.ts, and the gate below
|
|
121
|
+
# reads them the same way), website/ for Astro. Frozen, as every CI
|
|
122
|
+
# install is: a lockfile the build rewrites is drift nobody sees. The
|
|
123
|
+
# public variant (ASTRO_BUILD=public) is base /; the embedded one is base
|
|
124
|
+
# /docs, and every link in it is wrong on the public domain -- which is
|
|
125
|
+
# what the grep at the end catches.
|
|
126
|
+
- name: Build the public docs site
|
|
127
|
+
run: |
|
|
128
|
+
nix develop .#build --command bash -c '
|
|
129
|
+
set -euo pipefail
|
|
130
|
+
bun install --frozen-lockfile
|
|
131
|
+
bun install --frozen-lockfile --cwd website
|
|
132
|
+
bun run docs:site:public
|
|
133
|
+
dist="website/dist-public"
|
|
134
|
+
[ -s "$dist/index.html" ] || { echo "::error::$dist/index.html was not built"; exit 1; }
|
|
135
|
+
if grep -q "href=./docs/" "$dist/index.html"; then
|
|
136
|
+
echo "::error::$dist is the EMBEDDED variant (links are /docs-prefixed); docs:site:public must set ASTRO_BUILD=public"
|
|
137
|
+
exit 1
|
|
138
|
+
fi
|
|
139
|
+
echo "built $dist ($(find "$dist" -name "*.html" | wc -l) pages)"
|
|
140
|
+
'
|
|
141
|
+
|
|
142
|
+
# A recorded guide whose capture is not on this runner is a placeholder
|
|
143
|
+
# page after the build above (docs:site:public stubs it so the build
|
|
144
|
+
# works). Right for a local build, wrong for a deploy: the live page would
|
|
145
|
+
# be replaced with "run the doc-guide locally", and nothing else would
|
|
146
|
+
# notice -- the build is green, the links resolve. --content reads the
|
|
147
|
+
# guides the site declares by symlink, so a site that declares none has
|
|
148
|
+
# nothing to refuse; one that does needs its captures on the runner, and
|
|
149
|
+
# until the repo restores them this step says which pages are missing.
|
|
150
|
+
- name: Refuse to publish placeholder guides
|
|
151
|
+
run: |
|
|
152
|
+
nix develop .#build --command bash -c '
|
|
153
|
+
set -euo pipefail
|
|
154
|
+
bun run node_modules/@norskvideo/ctl-dev-kit/docs/guide-stubs.ts check --content website/src/content/docs/guides
|
|
155
|
+
'
|
|
156
|
+
|
|
157
|
+
# Skipped -- loudly, and successfully -- while NORSK_DOCS_SITE_PROJECT is
|
|
158
|
+
# unset: the Pages project and the DNS record are the account owner's to
|
|
159
|
+
# create, and main must not go red waiting for them. Setting the variable
|
|
160
|
+
# is the whole switch-on.
|
|
161
|
+
#
|
|
162
|
+
# The token is verified at the API, not with `wrangler whoami`: whoami
|
|
163
|
+
# needs User Details:Read, which a Pages:Edit token deliberately lacks. Two
|
|
164
|
+
# kinds of token verify at two endpoints: a USER-owned token (My Profile ->
|
|
165
|
+
# API Tokens) answers /user/tokens/verify; an ACCOUNT-owned one (Manage
|
|
166
|
+
# Account -> API Tokens) returns 1000 "Invalid API Token" there however
|
|
167
|
+
# valid it is, and answers /accounts/<id>/tokens/verify. Checking only the
|
|
168
|
+
# first once made a valid team-owned token read like a bad secret. curl is
|
|
169
|
+
# on the host PATH; wrangler is not in the shared dev shell, so bunx fetches
|
|
170
|
+
# it inside `nix develop`, where bun is.
|
|
171
|
+
#
|
|
172
|
+
# --branch is the ref that pushed: main is the production branch of the
|
|
173
|
+
# Pages project, so a push to main publishes; a workflow_dispatch from any
|
|
174
|
+
# other branch publishes a PREVIEW deployment of that branch, never over
|
|
175
|
+
# production.
|
|
176
|
+
- name: Deploy to Cloudflare Pages
|
|
177
|
+
env:
|
|
178
|
+
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
|
179
|
+
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
|
180
|
+
NORSK_DOCS_SITE_PROJECT: ${{ vars.NORSK_DOCS_SITE_PROJECT }}
|
|
181
|
+
PAGES_BRANCH: ${{ github.ref_name }}
|
|
182
|
+
run: |
|
|
183
|
+
set -euo pipefail
|
|
184
|
+
if [ -z "${NORSK_DOCS_SITE_PROJECT:-}" ]; then
|
|
185
|
+
origin="$(sed -nE 's/.*PUBLIC_SITE_ORIGIN = "([^"]+)".*/\1/p' website/src/lib/site.ts | head -1)"
|
|
186
|
+
echo "── Docs site → SKIPPED (no Pages project configured) ──"
|
|
187
|
+
echo " The site built (website/dist-public) but NORSK_DOCS_SITE_PROJECT is unset, so"
|
|
188
|
+
echo " nothing is deployed. To turn publishing on:"
|
|
189
|
+
echo " 1. Create a Cloudflare Pages project for this site (e.g. $(basename "$GITHUB_REPOSITORY")-docs)."
|
|
190
|
+
echo " 2. Add its custom domain${origin:+ (${origin#https://})} and the DNS record."
|
|
191
|
+
echo " 3. Make sure CLOUDFLARE_API_TOKEN (Pages:Edit) covers that project, and that"
|
|
192
|
+
echo " CLOUDFLARE_ACCOUNT_ID is set as a repo secret."
|
|
193
|
+
echo " 4. Set the repo variable NORSK_DOCS_SITE_PROJECT to the project name."
|
|
194
|
+
echo " Nothing in this workflow changes. See @norskvideo/ctl-dev-kit conventions/docs-site.md, Publishing."
|
|
195
|
+
exit 0
|
|
196
|
+
fi
|
|
197
|
+
if [ -z "${CLOUDFLARE_API_TOKEN:-}" ] || [ -z "${CLOUDFLARE_ACCOUNT_ID:-}" ]; then
|
|
198
|
+
echo "::error::NORSK_DOCS_SITE_PROJECT=$NORSK_DOCS_SITE_PROJECT is set, but CLOUDFLARE_API_TOKEN and CLOUDFLARE_ACCOUNT_ID must both be repo secrets"
|
|
199
|
+
exit 1
|
|
200
|
+
fi
|
|
201
|
+
cf_verify() {
|
|
202
|
+
curl -fsS --retry 3 --retry-all-errors --retry-delay 2 \
|
|
203
|
+
-H "Authorization: Bearer $CLOUDFLARE_API_TOKEN" \
|
|
204
|
+
"https://api.cloudflare.com/client/v4/$1" 2>&1
|
|
205
|
+
}
|
|
206
|
+
if verify_body=$(cf_verify "user/tokens/verify"); then
|
|
207
|
+
token_kind="user-owned"
|
|
208
|
+
elif verify_body=$(cf_verify "accounts/$CLOUDFLARE_ACCOUNT_ID/tokens/verify"); then
|
|
209
|
+
token_kind="account-owned"
|
|
210
|
+
else
|
|
211
|
+
echo "::error::Cloudflare token verification failed at both /user/tokens/verify and /accounts/<id>/tokens/verify. Last reply: $verify_body"
|
|
212
|
+
exit 1
|
|
213
|
+
fi
|
|
214
|
+
if ! echo "$verify_body" | grep -q '"status":"active"'; then
|
|
215
|
+
echo "::error::Cloudflare token not active ($token_kind): $verify_body"
|
|
216
|
+
exit 1
|
|
217
|
+
fi
|
|
218
|
+
# The token id is an identifier, not a credential: it answers "which of
|
|
219
|
+
# our tokens is CI using?", which a write-only secret otherwise cannot.
|
|
220
|
+
token_id=$(echo "$verify_body" | sed -n 's/.*"id":"\([a-f0-9]*\)".*/\1/p' | head -1)
|
|
221
|
+
echo "Cloudflare token: active ($token_kind${token_id:+, id ${token_id}})"
|
|
222
|
+
echo "── Docs site → Cloudflare Pages ($NORSK_DOCS_SITE_PROJECT, branch $PAGES_BRANCH) ──"
|
|
223
|
+
nix develop .#build --command bash -c '
|
|
224
|
+
set -euo pipefail
|
|
225
|
+
bunx wrangler@4 pages deploy website/dist-public \
|
|
226
|
+
--project-name "$NORSK_DOCS_SITE_PROJECT" \
|
|
227
|
+
--branch "$PAGES_BRANCH" \
|
|
228
|
+
--commit-dirty=true
|
|
229
|
+
'
|
|
230
|
+
- name: Reap the containers this job launched
|
|
231
|
+
# Whatever the tests did. A teardown cut short by a test timeout leaves
|
|
232
|
+
# its instance running on the shared Docker daemon (see JOB_REAP_STEP in
|
|
233
|
+
# conventions/workflow-shell.ts); everything this job launched mounts its
|
|
234
|
+
# own test-temp, so that is what goes.
|
|
235
|
+
if: always()
|
|
236
|
+
run: |
|
|
237
|
+
base="${NORSK_CTL_TEST_TMP:-}"
|
|
238
|
+
tt="$GITHUB_WORKSPACE/test-temp"
|
|
239
|
+
s=""
|
|
240
|
+
for c in $(docker ps -aq); do
|
|
241
|
+
src="$(docker inspect -f '{{range .Mounts}}{{println .Source}}{{end}}' "$c" 2>/dev/null)" || continue
|
|
242
|
+
if { [ -n "$base" ] && grep -q "^$base/" <<<"$src"; } || grep -q "^$tt/" <<<"$src"; then s="$s $c"; fi
|
|
243
|
+
done
|
|
244
|
+
if [ -n "$s" ]; then
|
|
245
|
+
echo "::warning::removing containers this job left running:$s"
|
|
246
|
+
docker rm -f $s
|
|
247
|
+
fi
|
|
@@ -11,6 +11,8 @@
|
|
|
11
11
|
// - nix/ctl-shell.nix -> overwrite with canonical (created if absent)
|
|
12
12
|
// - flake.nix (legacy verbatim copy only) -> the starter, repo's floating ctl pin kept
|
|
13
13
|
// - marker-block (CLAUDE core, dprint, .gitignore) -> replace the shared block, keep the rest
|
|
14
|
+
// - publish-docs-site.yml -> verbatim; CREATED where the repo has a
|
|
15
|
+
// docs site and is not a turnkey
|
|
14
16
|
// Structural per-repo files (root tsconfig.json, deployment/build-image.sh,
|
|
15
17
|
// manifest.seed.json) have NO canonical bytes to apply — the gate checks their
|
|
16
18
|
// shape, not their content — so they are left untouched.
|
|
@@ -30,6 +32,7 @@ import {
|
|
|
30
32
|
CTL_VERSION_LINE,
|
|
31
33
|
carriesIntegrationMarker,
|
|
32
34
|
DOCS_SITE_DIR,
|
|
35
|
+
DOCS_SITE_WORKFLOW,
|
|
33
36
|
END,
|
|
34
37
|
GITIGNORE_BEGIN,
|
|
35
38
|
GITIGNORE_END,
|
|
@@ -40,6 +43,7 @@ import {
|
|
|
40
43
|
type ProductShape,
|
|
41
44
|
PUBLISH_IMAGE_LINE,
|
|
42
45
|
RUNNER_LINE,
|
|
46
|
+
resolveRepoName,
|
|
43
47
|
resolveShape,
|
|
44
48
|
splitAtMarkers,
|
|
45
49
|
} from "./check-drift.ts";
|
|
@@ -49,6 +53,13 @@ export interface SyncReport {
|
|
|
49
53
|
skipped: string[];
|
|
50
54
|
}
|
|
51
55
|
|
|
56
|
+
export interface SyncDriftOptions {
|
|
57
|
+
/** The GitHub repo name (`norsk-ctl-product-probe`), which decides whether the
|
|
58
|
+
* docs site's publish may be CREATED here (never in a turnkey). The CLI
|
|
59
|
+
* resolves it; a fixture passes it. Unknown ⇒ nothing is created. */
|
|
60
|
+
repoName?: string;
|
|
61
|
+
}
|
|
62
|
+
|
|
52
63
|
function writeIfChanged(path: string, content: string, rel: string, written: string[]): void {
|
|
53
64
|
const current = existsSync(path) ? readFileSync(path, "utf8") : undefined;
|
|
54
65
|
if (current !== content) {
|
|
@@ -267,6 +278,39 @@ function syncWithDisplay(repoRoot: string, canonical: string, r: SyncReport): vo
|
|
|
267
278
|
chmodSync(path, 0o755);
|
|
268
279
|
}
|
|
269
280
|
|
|
281
|
+
// publish-docs-site.yml: the docs SITE's publish. Verbatim (the project name is
|
|
282
|
+
// a repo variable, the credentials are secrets), so an existing copy is always
|
|
283
|
+
// re-derived whole -- the gate, not this writer, judges whether the repo may
|
|
284
|
+
// carry one. It is CREATED only where the gate would accept it: a repo with a
|
|
285
|
+
// site (there is something to publish), named, and not a turnkey (a turnkey has
|
|
286
|
+
// no public site, ADR-0012). That is how a product that grows a site acquires
|
|
287
|
+
// its publish: the next sync-dev-kit run installs it, green until the Pages
|
|
288
|
+
// project exists. A name this writer cannot determine creates nothing, since
|
|
289
|
+
// the gate could only refuse the result.
|
|
290
|
+
const TURNKEY_REPO = /^norsk-ctl-turnkey-/;
|
|
291
|
+
|
|
292
|
+
function syncPublishDocsSite(repoRoot: string, canonical: string, repoName: string | undefined, r: SyncReport): void {
|
|
293
|
+
const path = join(repoRoot, DOCS_SITE_WORKFLOW);
|
|
294
|
+
if (existsSync(path)) {
|
|
295
|
+
writeIfChanged(path, canonical, DOCS_SITE_WORKFLOW, r.written);
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
if (!hasDocsSite(repoRoot)) {
|
|
299
|
+
r.skipped.push(`${DOCS_SITE_WORKFLOW} (no docs site to publish)`);
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
if (repoName === undefined) {
|
|
303
|
+
r.skipped.push(`${DOCS_SITE_WORKFLOW} (cannot tell which repo this is; a turnkey may not carry it)`);
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
if (TURNKEY_REPO.test(repoName)) {
|
|
307
|
+
r.skipped.push(`${DOCS_SITE_WORKFLOW} (${repoName} is a turnkey: no public site)`);
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
311
|
+
writeIfChanged(path, canonical, DOCS_SITE_WORKFLOW, r.written);
|
|
312
|
+
}
|
|
313
|
+
|
|
270
314
|
// The docs site's theme copies — the writer behind check-drift's docsThemeProblems.
|
|
271
315
|
// Only for a repo that HAS a site: sync never creates one (a site is a nested
|
|
272
316
|
// standalone package with its own install, not a file this writer can conjure),
|
|
@@ -286,7 +330,12 @@ function syncDocsTheme(repoRoot: string, r: SyncReport): void {
|
|
|
286
330
|
for (const rel of adrift) r.written.push(`${DOCS_SITE_DIR}/${rel}`);
|
|
287
331
|
}
|
|
288
332
|
|
|
289
|
-
export function syncDrift(
|
|
333
|
+
export function syncDrift(
|
|
334
|
+
repoRoot: string,
|
|
335
|
+
canonical: CanonicalBytes,
|
|
336
|
+
shape: ProductShape = "studio",
|
|
337
|
+
opts: SyncDriftOptions = {},
|
|
338
|
+
): SyncReport {
|
|
290
339
|
const r: SyncReport = { written: [], skipped: [] };
|
|
291
340
|
// Shape-selected canonicals: an sdk-app's flake + checks are its own variants
|
|
292
341
|
// (the gate holds it to those). Everything else is shared across shapes.
|
|
@@ -349,6 +398,7 @@ export function syncDrift(repoRoot: string, canonical: CanonicalBytes, shape: Pr
|
|
|
349
398
|
syncDemoShim(repoRoot, canonical.demoShim, r);
|
|
350
399
|
syncWithDisplay(repoRoot, canonical.withDisplay, r);
|
|
351
400
|
syncDocsTheme(repoRoot, r);
|
|
401
|
+
syncPublishDocsSite(repoRoot, canonical.publishDocsSite, opts.repoName, r);
|
|
352
402
|
|
|
353
403
|
return r;
|
|
354
404
|
}
|
|
@@ -366,6 +416,7 @@ if (import.meta.main) {
|
|
|
366
416
|
botPrs: readFileSync(join(dir, "bot-prs.yml"), "utf8"),
|
|
367
417
|
publishDocs: readFileSync(join(dir, "publish-docs.yml"), "utf8"),
|
|
368
418
|
buildDocs: readFileSync(join(dir, "build-docs.yml"), "utf8"),
|
|
419
|
+
publishDocsSite: readFileSync(join(dir, "publish-docs-site.yml"), "utf8"),
|
|
369
420
|
withDisplay: readFileSync(join(dir, "..", "doc-guide", "with-display.sh"), "utf8"),
|
|
370
421
|
checks: readFileSync(join(dir, "checks.yml"), "utf8"),
|
|
371
422
|
biome: readFileSync(join(dir, "biome.base.json"), "utf8"),
|
|
@@ -381,7 +432,7 @@ if (import.meta.main) {
|
|
|
381
432
|
checksSdkApp: readFileSync(join(dir, "checks.sdk-app.yml"), "utf8"),
|
|
382
433
|
upgradeLatestSdkApp: readFileSync(join(dir, "upgrade-latest.sdk-app.yml"), "utf8"),
|
|
383
434
|
};
|
|
384
|
-
const report = syncDrift(repoRoot, canonical, resolveShape(repoRoot));
|
|
435
|
+
const report = syncDrift(repoRoot, canonical, resolveShape(repoRoot), { repoName: resolveRepoName(repoRoot) });
|
|
385
436
|
if (report.written.length === 0) {
|
|
386
437
|
console.log("sync-drift: all shared-convention copies already match @norskvideo/ctl-dev-kit.");
|
|
387
438
|
} else {
|
|
@@ -307,10 +307,17 @@ ${SITE_DIR}/dist-public/
|
|
|
307
307
|
${SITE_DIR}/.astro/
|
|
308
308
|
`;
|
|
309
309
|
|
|
310
|
+
/** Before either build: a placeholder page for every recorded guide the
|
|
311
|
+
* site symlinks from docs/generated whose capture has not run (dev-kit
|
|
312
|
+
* docs/guide-stubs.ts), so the site builds on a checkout with no captures --
|
|
313
|
+
* as ctl's and the Manager's do. A site that declares no recorded guides is
|
|
314
|
+
* a no-op. The publish workflow refuses to deploy a placeholder. */
|
|
315
|
+
const STUB_GUIDES = `bun run node_modules/@norskvideo/ctl-dev-kit/docs/guide-stubs.ts stub ${SITE_DIR}/src/content/docs/guides`;
|
|
316
|
+
|
|
310
317
|
/** The root package.json scripts that drive it. Named the same in every repo
|
|
311
318
|
* that has a site, including the two in the norsk-ctl monorepo. */
|
|
312
319
|
export const DOCS_SITE_SCRIPTS: Record<string, string> = {
|
|
313
320
|
"docs:dev": `bun run --cwd ${SITE_DIR} dev`,
|
|
314
|
-
"docs:site":
|
|
315
|
-
"docs:site:public":
|
|
321
|
+
"docs:site": `${STUB_GUIDES} && bun run --cwd ${SITE_DIR} build`,
|
|
322
|
+
"docs:site:public": `${STUB_GUIDES} && ASTRO_BUILD=public bun run --cwd ${SITE_DIR} build`,
|
|
316
323
|
};
|
|
@@ -302,8 +302,9 @@ function rootPackageJson(ctx: ShapeContext): string {
|
|
|
302
302
|
"test:unit": `bun test shared/src/ backend/src/ ${hasComponents ? "components/src/ " : ""}tests/unit/`,
|
|
303
303
|
"test:image": "bun test tests/image/",
|
|
304
304
|
"check:drift": "bun run node_modules/@norskvideo/ctl-dev-kit/conventions/check-drift.ts",
|
|
305
|
-
"docs:check":
|
|
306
|
-
|
|
305
|
+
"docs:check":
|
|
306
|
+
"bun run node_modules/@norskvideo/ctl-dev-kit/docs/path-existence.ts && bun run node_modules/@norskvideo/ctl-dev-kit/docs/light-pages.ts && bun run node_modules/@norskvideo/ctl-dev-kit/docs/journeys.ts",
|
|
307
|
+
"docs:manual": `bun run node_modules/@norskvideo/ctl-dev-kit/doc-guide/prose-bundle.js --brand "${ctx.name}" --title "${ctx.name} — Documentation" --link "norsk.video=https://norsk.video/" --out docs/generated/bundle docs/handover.md docs/operating.md docs/known-limitations.md`,
|
|
307
308
|
"docs:handover": "bash scripts/docs-handover.sh",
|
|
308
309
|
"docs:handover:check": "bash scripts/docs-handover.sh --check",
|
|
309
310
|
// The docs SITE (website/) — a standalone package, so its install is its
|
|
@@ -1896,8 +1897,8 @@ function exampleInputJson(_ctx: ShapeContext): string {
|
|
|
1896
1897
|
|
|
1897
1898
|
// ---- The canonical doc set (fleet review 02 s9, backend-only turnkey) ------
|
|
1898
1899
|
// README (developer), docs/README (the index), docs/handover (the customer's
|
|
1899
|
-
// deployment document: prose around a block norsk-ctl generates), docs/
|
|
1900
|
-
// limitations (operator). Prose only: a turnkey's bundle is built from these
|
|
1900
|
+
// deployment document: prose around a block norsk-ctl generates), docs/operating
|
|
1901
|
+
// and docs/known-limitations (operator). Prose only: a turnkey's bundle is built from these
|
|
1901
1902
|
// files by docs:manual, which the image build runs, so the image serves them.
|
|
1902
1903
|
// Bundle-internal file names are written without backticks on purpose: the
|
|
1903
1904
|
// path-existence lint would look for them in the repo.
|
|
@@ -1961,7 +1962,8 @@ norsk-ctl and the pinned Norsk SDK:
|
|
|
1961
1962
|
Two channels, deliberately:
|
|
1962
1963
|
|
|
1963
1964
|
- **The bundle the image serves.** \`bun run docs:manual\` builds it from
|
|
1964
|
-
\`docs/handover.md\` and \`docs/known-limitations.md\`;
|
|
1965
|
+
\`docs/handover.md\`, \`docs/operating.md\` and \`docs/known-limitations.md\`;
|
|
1966
|
+
the image build runs
|
|
1965
1967
|
it, so every image serves them at /docs at the version that is running
|
|
1966
1968
|
(linked from the product's page in norsk-ctl). \`bun run docs:handover\`
|
|
1967
1969
|
regenerates the handover's generated block from a daemon that has this
|
|
@@ -1985,7 +1987,8 @@ served at /docs at the version that is running.
|
|
|
1985
1987
|
|
|
1986
1988
|
| Document | Audience | What it is |
|
|
1987
1989
|
| ---------------------------- | ----------------------------------------- | --------------------------------------------------------------------------------------------------- |
|
|
1988
|
-
| \`docs/handover.md\` |
|
|
1990
|
+
| \`docs/handover.md\` | whoever gets this product running | How norsk-ctl arrives, the one command that adds this product, and the bundle route for platform teams |
|
|
1991
|
+
| \`docs/operating.md\` | operators | What a running deployment looks like from the operator's chair: the controls, the checks, the signs |
|
|
1989
1992
|
| \`docs/known-limitations.md\` | operators | What the product does not do yet, with the consequence and the workaround |
|
|
1990
1993
|
|
|
1991
1994
|
The **docs site** is a separate channel and lives in \`website/\`: the public
|
|
@@ -1995,18 +1998,84 @@ manual, on the shared Norsk theme, not built from these files. See
|
|
|
1995
1998
|
\`bun run docs:manual\` builds the bundle from these files. The tables in the
|
|
1996
1999
|
handover are generated by norsk-ctl (\`bun run docs:handover\`) — regenerate
|
|
1997
2000
|
them, never edit them.
|
|
2001
|
+
|
|
2002
|
+
\`docs/personas.md\` is for writers, not readers, and is not in the bundle: it
|
|
2003
|
+
maps this product's audiences onto the four shared personas and carries the
|
|
2004
|
+
journey table \`bun run docs:check\` verifies.
|
|
2005
|
+
`;
|
|
2006
|
+
}
|
|
2007
|
+
|
|
2008
|
+
// The per-product persona mapping and the journey table the journeys lint
|
|
2009
|
+
// reads (conventions/personas.md). The routes name the pages the skeleton
|
|
2010
|
+
// ships; a product rewrites them as its pages arrive, and docs:check keeps
|
|
2011
|
+
// every named page real.
|
|
2012
|
+
function personasMd(ctx: ShapeContext): string {
|
|
2013
|
+
return `# Who ${ctx.name}'s documentation is for
|
|
2014
|
+
|
|
2015
|
+
The four personas are shared across every Norsk product and described in
|
|
2016
|
+
\`node_modules/@norskvideo/ctl-dev-kit/conventions/personas.md\`. This page maps
|
|
2017
|
+
${ctx.name}'s own audiences onto them and says where each reader goes.
|
|
2018
|
+
|
|
2019
|
+
## The mapping
|
|
2020
|
+
|
|
2021
|
+
<!-- ctl-docs-template: fill me -->
|
|
2022
|
+
_Name this product's audiences (the people who set it up, run it, and are
|
|
2023
|
+
invited into it) and say which of the four personas each one is. Runtime
|
|
2024
|
+
actors are not personas: an operator at the console is the Builder wearing
|
|
2025
|
+
the Administrator's hat._
|
|
2026
|
+
|
|
2027
|
+
## The journey table
|
|
2028
|
+
|
|
2029
|
+
| Persona | Starts at | Then, in order | Hands off to |
|
|
2030
|
+
| ------------- | ------------------- | --------------------------------------- | ----------------------------------------------------------------------- |
|
|
2031
|
+
| Evaluator | \`/docs/\` | \`/docs/guides/\` | [norsk-ctl's manual](https://ctl.norsk.video/), to run it for real |
|
|
2032
|
+
| Builder | \`/docs/guides/\` | \`/docs/how-to/\` | \`/docs/reference/\`, for the settings they just used |
|
|
2033
|
+
| Integrator | \`/docs/reference/\` | \`/docs/concepts/\` | [norsk-ctl's CLI and API reference](https://ctl.norsk.video/) |
|
|
2034
|
+
| Administrator | \`handover.md\` | \`operating.md\`, \`known-limitations.md\` | [norsk-ctl's manual](https://ctl.norsk.video/), for everything about the machine |
|
|
2035
|
+
|
|
2036
|
+
Site paths resolve under website/src/content/docs; a bare name resolves under
|
|
2037
|
+
docs/. Each route is at most five pages and ends in a hand-off, which is the
|
|
2038
|
+
one place "see norsk-ctl for details" lives.
|
|
1998
2039
|
`;
|
|
1999
2040
|
}
|
|
2000
2041
|
|
|
2001
2042
|
function handoverMd(ctx: ShapeContext): string {
|
|
2002
|
-
return `# Deploying ${ctx.name}
|
|
2043
|
+
return `# Deploying ${ctx.name}
|
|
2044
|
+
|
|
2045
|
+
How ${ctx.name} reaches you and how to get it running. Nearly every deployment
|
|
2046
|
+
goes through norsk-ctl, and that is the first two sections. The rest of this
|
|
2047
|
+
page is for a platform team running the exported bundle in its own
|
|
2048
|
+
orchestration instead.
|
|
2049
|
+
|
|
2050
|
+
## How norsk-ctl arrives
|
|
2051
|
+
|
|
2052
|
+
\`\`\`sh
|
|
2053
|
+
curl -fsSL https://norsk.video/install/ctl | bash
|
|
2054
|
+
\`\`\`
|
|
2055
|
+
|
|
2056
|
+
When it finishes, norsk-ctl is running on the machine and its web page is where
|
|
2057
|
+
products are added, launched and watched. Everything about the machine itself
|
|
2058
|
+
is in [norsk-ctl's manual](https://ctl.norsk.video/): ports, TLS, cloud hosts,
|
|
2059
|
+
a box behind another proxy.
|
|
2060
|
+
|
|
2061
|
+
## Add this product
|
|
2062
|
+
|
|
2063
|
+
\`\`\`sh
|
|
2064
|
+
norsk-ctl product add --image <the image you were given> --license-file <file>
|
|
2065
|
+
\`\`\`
|
|
2066
|
+
|
|
2067
|
+
One command. ${ctx.name} then appears on norsk-ctl's page, ready to launch;
|
|
2068
|
+
operating.md says what to do with it once it is running.
|
|
2069
|
+
|
|
2070
|
+
## Deploying without norsk-ctl
|
|
2003
2071
|
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2072
|
+
Some platform teams run the product from a bundle norsk-ctl exports, inside
|
|
2073
|
+
their own orchestration. The tables in the generated block below come from
|
|
2074
|
+
norsk-ctl's own rendering of this product's default template
|
|
2075
|
+
(\`bun run docs:handover\`), so they are the launch truth of the version this
|
|
2076
|
+
document ships with. Regenerate them; never edit them.
|
|
2008
2077
|
|
|
2009
|
-
|
|
2078
|
+
### What you receive
|
|
2010
2079
|
|
|
2011
2080
|
A portable bundle exported by norsk-ctl: compose.yml (the flat compose, every
|
|
2012
2081
|
path relative), HANDOVER.md (the same tables as below), the working-directory
|
|
@@ -2017,13 +2086,13 @@ placeholder to replace with a production licence before the first start.
|
|
|
2017
2086
|
_Not generated yet. Run \`bun run docs:handover\` against a daemon that has this product added._
|
|
2018
2087
|
<!-- END generated -->
|
|
2019
2088
|
|
|
2020
|
-
|
|
2089
|
+
### Verifying a deployment
|
|
2021
2090
|
|
|
2022
2091
|
- The control plane answers on its port: GET /healthz returns ok.
|
|
2023
2092
|
- The media engine is licensed: its log names the licence's product entry, not a placeholder.
|
|
2024
2093
|
- The default template's outputs are reachable from where the audience is.
|
|
2025
2094
|
|
|
2026
|
-
|
|
2095
|
+
### Not in this artefact
|
|
2027
2096
|
|
|
2028
2097
|
Anything norsk-ctl-specific: the daemon, product registration, the instance
|
|
2029
2098
|
proxy and its authentication, templates-as-storage. The bundle is the launch
|
|
@@ -2031,6 +2100,42 @@ truth of one product template and needs none of them.
|
|
|
2031
2100
|
`;
|
|
2032
2101
|
}
|
|
2033
2102
|
|
|
2103
|
+
// The operator's page, in the shape of the plan's exemplar (reuters'
|
|
2104
|
+
// operating.md). Each section opens with a marker so a half-filled page is
|
|
2105
|
+
// grep-able, and the fill-me lines say what the exemplar put there.
|
|
2106
|
+
function operatingMd(ctx: ShapeContext): string {
|
|
2107
|
+
return `# Operating ${ctx.name}
|
|
2108
|
+
|
|
2109
|
+
What an operator does with a running ${ctx.name}, and where. For getting one
|
|
2110
|
+
running, see handover.md; this page assumes it is up.
|
|
2111
|
+
|
|
2112
|
+
## What it is
|
|
2113
|
+
|
|
2114
|
+
<!-- ctl-docs-template: fill me -->
|
|
2115
|
+
_One paragraph: what the deployment takes in, what it puts out, and the one
|
|
2116
|
+
thing an operator adjusts while it runs._
|
|
2117
|
+
|
|
2118
|
+
## Where the controls are
|
|
2119
|
+
|
|
2120
|
+
<!-- ctl-docs-template: fill me -->
|
|
2121
|
+
_Which screen the operator works in, and what the product's own page at /docs
|
|
2122
|
+
and its health check at /healthz are for._
|
|
2123
|
+
|
|
2124
|
+
## Checking it is doing its job
|
|
2125
|
+
|
|
2126
|
+
<!-- ctl-docs-template: fill me -->
|
|
2127
|
+
1. _The health check answers._
|
|
2128
|
+
2. _Each input is arriving. A listening input with nothing connected is idle, not failed; the sender is the thing to check._
|
|
2129
|
+
3. _Each output is connected to its destination._
|
|
2130
|
+
|
|
2131
|
+
## When something looks wrong
|
|
2132
|
+
|
|
2133
|
+
<!-- ctl-docs-template: fill me -->
|
|
2134
|
+
- _The picture is absent: which input or output to look at first._
|
|
2135
|
+
- _The picture is wrong rather than absent: what is a runtime control and what needs a rebuilt template._
|
|
2136
|
+
`;
|
|
2137
|
+
}
|
|
2138
|
+
|
|
2034
2139
|
const KNOWN_LIMITATIONS_MD = `# Known limitations
|
|
2035
2140
|
|
|
2036
2141
|
What this product does not do yet, or does with a caveat an operator must know
|
|
@@ -2105,7 +2210,9 @@ export const turnkey: ShapeModule = {
|
|
|
2105
2210
|
...docsSiteFiles(ctx),
|
|
2106
2211
|
{ path: "docs/README.md", content: docsReadmeMd(ctx) },
|
|
2107
2212
|
{ path: "docs/handover.md", content: handoverMd(ctx) },
|
|
2213
|
+
{ path: "docs/operating.md", content: operatingMd(ctx) },
|
|
2108
2214
|
{ path: "docs/known-limitations.md", content: KNOWN_LIMITATIONS_MD },
|
|
2215
|
+
{ path: "docs/personas.md", content: personasMd(ctx) },
|
|
2109
2216
|
{ path: ".github/workflows/build-docs.yml", content: ctx.canon.buildDocs },
|
|
2110
2217
|
{ path: "scripts/doc-guide/regen-manual.sh", content: REGEN_MANUAL_SH, executable: true },
|
|
2111
2218
|
{ path: "scripts/with-display.sh", content: ctx.canon.withDisplay, executable: true },
|