@norskvideo/ctl-dev-kit 0.1.56 → 0.1.58
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 +147 -0
- package/conventions/check-drift.ts +70 -2
- package/conventions/docs.md +25 -16
- package/conventions/sync-drift.ts +7 -0
- package/create-product/assets/ci-status-dispatch.yml +1 -1
- package/doc-guide/build-manual.d.ts +14 -2
- package/doc-guide/build-manual.js +38 -10
- package/doc-guide/bundle.d.ts +1 -1
- package/doc-guide/bundle.js +22 -14
- package/package.json +3 -2
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
# Nightly: regenerate this product's docs bundle and bake it into the product
|
|
2
|
+
# image. The EMBEDDED variant of the docs workflow (ADR-0012): the bundle is
|
|
3
|
+
# built, put in the image the way a release build puts it there, and read back
|
|
4
|
+
# out of that image as the proof. NOTHING LEAVES THE REPO -- no release asset,
|
|
5
|
+
# no upload, no write permission on the repo, no token held. A turnkey's documentation
|
|
6
|
+
# reaches exactly one customer, inside the image they already run.
|
|
7
|
+
#
|
|
8
|
+
# THE TWO VARIANTS (a repo carries exactly one; the drift gate enforces it):
|
|
9
|
+
# - build-docs.yml (this file) embedded: bundle -> image, full stop.
|
|
10
|
+
# Turnkeys (norsk-ctl-turnkey-*) carry this.
|
|
11
|
+
# - publish-docs.yml public: the same build, then the bundle is
|
|
12
|
+
# published for the norsk-ctl docs site to
|
|
13
|
+
# ingest. ONLY a norsk-ctl-product-* repo may
|
|
14
|
+
# carry it -- the gate refuses it elsewhere.
|
|
15
|
+
#
|
|
16
|
+
# This workflow is SINGLE-SOURCED in @norskvideo/ctl-dev-kit
|
|
17
|
+
# (conventions/build-docs.yml) and copied verbatim, same as checks.yml. It is
|
|
18
|
+
# deliberately product-agnostic: everything per-repo lives in repo-local scripts
|
|
19
|
+
# it calls --
|
|
20
|
+
# - scripts/doc-guide/regen-manual.sh : the guide tiers + slug list + assemble
|
|
21
|
+
# - scripts/doc-guide/prepull-extra.sh : OPTIONAL, any images beyond media+studio
|
|
22
|
+
# - deployment/build-image.sh : the product image wrapper (dev-kit driver)
|
|
23
|
+
# so the drift gate can keep every copy byte-identical. A turnkey with prose-only
|
|
24
|
+
# pages (no doc-guides yet) has a regen-manual.sh that just assembles.
|
|
25
|
+
#
|
|
26
|
+
# Nightly-only by design, mirroring publish-docs.yml: a guide's engine tier
|
|
27
|
+
# stands up real instances (slow, licensed), so it does not ride every push.
|
|
28
|
+
name: build-docs
|
|
29
|
+
|
|
30
|
+
on:
|
|
31
|
+
schedule:
|
|
32
|
+
- cron: "30 1 * * *" # 01:30 UTC — ahead of ctl's nightly docs build
|
|
33
|
+
workflow_dispatch:
|
|
34
|
+
|
|
35
|
+
permissions:
|
|
36
|
+
contents: read # builds and bakes; nothing leaves the repo
|
|
37
|
+
|
|
38
|
+
concurrency:
|
|
39
|
+
group: build-docs-${{ github.ref }}
|
|
40
|
+
cancel-in-progress: true
|
|
41
|
+
|
|
42
|
+
jobs:
|
|
43
|
+
build:
|
|
44
|
+
runs-on: x64
|
|
45
|
+
steps:
|
|
46
|
+
# A guide's engine tier launches the same docker instances the integration
|
|
47
|
+
# suite does, leaving root-owned bind-mount target dirs under test-temp/
|
|
48
|
+
# that the non-root runner can't remove — which fails actions/checkout's
|
|
49
|
+
# own cleanup before anything runs. Nuke them from a throwaway root
|
|
50
|
+
# container first. Best-effort. (Mirrors integration.yml.)
|
|
51
|
+
- name: Clear stale root-owned test-temp (pre-checkout)
|
|
52
|
+
run: |
|
|
53
|
+
set -uo pipefail
|
|
54
|
+
tt="$GITHUB_WORKSPACE/test-temp"
|
|
55
|
+
[ -d "$tt" ] || exit 0
|
|
56
|
+
docker run --rm --user 0:0 -v "$tt":/t alpine sh \
|
|
57
|
+
-c 'rm -rf /t/* /t/.[!.]* 2>/dev/null || true' || rm -rf "$tt"/* 2>/dev/null || true
|
|
58
|
+
|
|
59
|
+
- uses: actions/checkout@v5
|
|
60
|
+
with:
|
|
61
|
+
clean: false
|
|
62
|
+
|
|
63
|
+
- name: Write the Norsk license (from the org secret)
|
|
64
|
+
env:
|
|
65
|
+
NORSK_LICENSE_V2: ${{ secrets.NORSK_LICENSE_V2 }}
|
|
66
|
+
run: printf '%s' "$NORSK_LICENSE_V2" > "$RUNNER_TEMP/norsk-license.json"
|
|
67
|
+
|
|
68
|
+
- name: Download the released norsk-ctl binary (latest channel)
|
|
69
|
+
run: |
|
|
70
|
+
set -euo pipefail
|
|
71
|
+
S3="https://s3.eu-west-1.amazonaws.com/norsk.video/norsk-ctl"
|
|
72
|
+
ver="$(curl -fsSL "$S3/latest")"
|
|
73
|
+
echo "norsk-ctl latest channel -> $ver"
|
|
74
|
+
curl -fsSL "$S3/$ver/norsk-ctl-$ver-linux-x64" -o "$RUNNER_TEMP/norsk-ctl"
|
|
75
|
+
chmod +x "$RUNNER_TEMP/norsk-ctl"
|
|
76
|
+
"$RUNNER_TEMP/norsk-ctl" --version || true
|
|
77
|
+
|
|
78
|
+
# Cold-runner first pulls overrun the harness's per-test launch timeouts;
|
|
79
|
+
# pre-pull the universal media + studio images (every product pins them in
|
|
80
|
+
# manifest.seed.json) so compose/run hit local images. A product needing
|
|
81
|
+
# more (e.g. a WHIP driver) pulls them in scripts/doc-guide/prepull-extra.sh.
|
|
82
|
+
- name: Pre-pull the media + studio images
|
|
83
|
+
run: |
|
|
84
|
+
set -euo pipefail
|
|
85
|
+
for img in "$(jq -r '.latest.media' manifest.seed.json)" "$(jq -r '.latest.studio' manifest.seed.json)"; do
|
|
86
|
+
echo "pre-pulling $img"
|
|
87
|
+
docker pull "$img"
|
|
88
|
+
done
|
|
89
|
+
if [ -x scripts/doc-guide/prepull-extra.sh ]; then
|
|
90
|
+
echo "running scripts/doc-guide/prepull-extra.sh"
|
|
91
|
+
./scripts/doc-guide/prepull-extra.sh
|
|
92
|
+
fi
|
|
93
|
+
|
|
94
|
+
# Rebuild every workspace the bundle depends on (a guide's engine tier packs
|
|
95
|
+
# the freshly-built dashboard + frontend dist into the launched template),
|
|
96
|
+
# then run the product's doc-guide tiers and assemble the bundle.
|
|
97
|
+
# regen-manual.sh owns the slug list and which tiers run; with-display.sh
|
|
98
|
+
# wraps the whole run in an X display for headless chromium (the engine tier
|
|
99
|
+
# drives a raw chromium that needs one).
|
|
100
|
+
- name: Regenerate the docs bundle (all tiers)
|
|
101
|
+
env:
|
|
102
|
+
NORSK_CTL_BINARY: ${{ runner.temp }}/norsk-ctl
|
|
103
|
+
NORSK_LICENSE_FILE: ${{ runner.temp }}/norsk-license.json
|
|
104
|
+
# This runner launches Studio as a HOST sibling (DooD), so the harness
|
|
105
|
+
# reaches host-published ports via the host-gateway alias, not
|
|
106
|
+
# localhost — same as the integration suite.
|
|
107
|
+
NORSK_TEST_HOST: host.docker.internal
|
|
108
|
+
# Reach launched instances over norsk-net by service DNS (as the
|
|
109
|
+
# integration tier already does) AND, on a ctl that supports it, launch
|
|
110
|
+
# them binding no host ports — so concurrent doc-guide instances can't
|
|
111
|
+
# collide on the product's deterministic host-port bands.
|
|
112
|
+
NORSK_TEST_NET: direct
|
|
113
|
+
run: |
|
|
114
|
+
nix develop .#build --command bash -c '
|
|
115
|
+
set -euo pipefail
|
|
116
|
+
# clean:false persists node_modules between runs for speed, but a
|
|
117
|
+
# workspace dep whose version moved leaves stale copies that
|
|
118
|
+
# --frozen-lockfile does not reliably relink, so a new export is "not
|
|
119
|
+
# found". Nuke EVERY node_modules — including nested per-workspace ones
|
|
120
|
+
# — for a deterministic install. A nightly can afford it.
|
|
121
|
+
find . -name node_modules -type d -prune -exec rm -rf {} + 2>/dev/null || true
|
|
122
|
+
bun install --frozen-lockfile
|
|
123
|
+
bun run build:no-lint
|
|
124
|
+
bash scripts/with-display.sh scripts/doc-guide/regen-manual.sh
|
|
125
|
+
'
|
|
126
|
+
|
|
127
|
+
# The proof: the bundle the tiers just built goes into the product image
|
|
128
|
+
# the way a release build puts it there (deployment/build-image.sh stages
|
|
129
|
+
# docs/generated/bundle/ as the image's docs/), and the image is asked for
|
|
130
|
+
# its manifest. A bundle that does not build, or does not reach the image,
|
|
131
|
+
# fails here — on the commit that broke it. The image is local to the
|
|
132
|
+
# runner: nothing is pushed, nothing is uploaded, no token is held.
|
|
133
|
+
- name: Bake the bundle into the product image and read it back
|
|
134
|
+
env:
|
|
135
|
+
IMAGE_TAG: build-docs-bake:${{ github.sha }}
|
|
136
|
+
run: |
|
|
137
|
+
nix develop .#build --command bash -c '
|
|
138
|
+
set -euo pipefail
|
|
139
|
+
man="docs/generated/bundle/bundle.json"
|
|
140
|
+
[ -s "$man" ] || { echo "::error::bundle was not generated at $man"; exit 1; }
|
|
141
|
+
bash deployment/build-image.sh
|
|
142
|
+
docker run --rm --entrypoint /usr/local/bin/bun "$IMAGE_TAG" -e "
|
|
143
|
+
const m = require(\"/usr/src/app/docs/bundle.json\");
|
|
144
|
+
console.log(\"image carries docs bundle schema\", m.schemaVersion, \"with\", m.pages.length, \"pages, built\", m.provenance.builtAt);
|
|
145
|
+
"
|
|
146
|
+
docker image rm -f "$IMAGE_TAG" >/dev/null
|
|
147
|
+
'
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
// dev-kit forces a re-sync. Canonical bytes ship alongside this script
|
|
18
18
|
// (conventions/* + build/*), resolved package-relative so they work both
|
|
19
19
|
// workspace-symlinked and installed from the published tarball.
|
|
20
|
+
import { execFileSync } from "node:child_process";
|
|
20
21
|
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
21
22
|
import { join } from "node:path";
|
|
22
23
|
import { parseManifestSeed } from "@norskvideo/ctl-sdk/manifest-seed";
|
|
@@ -238,6 +239,8 @@ export interface CanonicalBytes {
|
|
|
238
239
|
upgradeLatest: string;
|
|
239
240
|
syncDevKit: string;
|
|
240
241
|
publishDocs: string;
|
|
242
|
+
/** conventions/build-docs.yml — the embedded docs variant (bundle -> image, no upload). */
|
|
243
|
+
buildDocs: string;
|
|
241
244
|
checks: string;
|
|
242
245
|
biome: string;
|
|
243
246
|
tsconfigBase: string;
|
|
@@ -320,7 +323,59 @@ function verbatimProblem(
|
|
|
320
323
|
return undefined;
|
|
321
324
|
}
|
|
322
325
|
|
|
323
|
-
export
|
|
326
|
+
export interface CheckDriftOptions {
|
|
327
|
+
/** The GitHub repo name (`norsk-ctl-product-playout`), which decides whether the
|
|
328
|
+
* public docs variant may be carried. The CLI resolves it ({@link resolveRepoName});
|
|
329
|
+
* a fixture passes it. Unknown is allowed only while nothing depends on it. */
|
|
330
|
+
repoName?: string;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/** `owner/name` from GITHUB_REPOSITORY (Actions), else the last path segment of
|
|
334
|
+
* the origin remote. Undefined when neither is available. */
|
|
335
|
+
export function resolveRepoName(repoRoot: string, env: NodeJS.ProcessEnv = process.env): string | undefined {
|
|
336
|
+
const fromEnv = env.GITHUB_REPOSITORY?.split("/").pop();
|
|
337
|
+
if (fromEnv) return fromEnv;
|
|
338
|
+
try {
|
|
339
|
+
const url = execFileSync("git", ["-C", repoRoot, "remote", "get-url", "origin"], {
|
|
340
|
+
encoding: "utf8",
|
|
341
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
342
|
+
}).trim();
|
|
343
|
+
const name = url
|
|
344
|
+
.split(/[/:]/)
|
|
345
|
+
.pop()
|
|
346
|
+
?.replace(/\.git$/, "");
|
|
347
|
+
return name || undefined;
|
|
348
|
+
} catch {
|
|
349
|
+
return undefined;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
const PUBLIC_DOCS_REPO = /^norsk-ctl-product-/;
|
|
354
|
+
|
|
355
|
+
/** ADR-0012: exposure is declared by which docs workflow a repo carries, so the
|
|
356
|
+
* gate is what makes that declaration safe — exactly one variant; the public
|
|
357
|
+
* one only where GitHub calls the repo norsk-ctl-product-*; a name it cannot
|
|
358
|
+
* determine is a finding, never a silent pass. */
|
|
359
|
+
export function docsExposureProblems(hasPublish: boolean, hasBuild: boolean, repoName: string | undefined): string[] {
|
|
360
|
+
const problems: string[] = [];
|
|
361
|
+
if (hasPublish && hasBuild) {
|
|
362
|
+
problems.push(
|
|
363
|
+
"a repo carries exactly one docs workflow: .github/workflows/publish-docs.yml (public: the bundle is published for the norsk-ctl docs site) OR .github/workflows/build-docs.yml (embedded: the bundle goes into the image and nowhere else). Both are present; delete one.",
|
|
364
|
+
);
|
|
365
|
+
}
|
|
366
|
+
if (hasPublish && repoName === undefined) {
|
|
367
|
+
problems.push(
|
|
368
|
+
".github/workflows/publish-docs.yml publishes this repo's docs publicly, and the gate cannot tell which repo this is (no GITHUB_REPOSITORY, no origin remote). Only a norsk-ctl-product-* repo may carry the public variant.",
|
|
369
|
+
);
|
|
370
|
+
} else if (hasPublish && !PUBLIC_DOCS_REPO.test(repoName as string)) {
|
|
371
|
+
problems.push(
|
|
372
|
+
`.github/workflows/publish-docs.yml publishes this repo's docs publicly, but this repo is ${repoName}, not a norsk-ctl-product-* repo. A turnkey's documentation never leaves the box: carry build-docs.yml (the embedded variant) instead.`,
|
|
373
|
+
);
|
|
374
|
+
}
|
|
375
|
+
return problems;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
export function checkDrift(repoRoot: string, canonical: CanonicalBytes, opts: CheckDriftOptions = {}): DriftReport {
|
|
324
379
|
const problems: string[] = [];
|
|
325
380
|
const push = (problem: string | undefined) => {
|
|
326
381
|
if (problem) problems.push(problem);
|
|
@@ -490,6 +545,18 @@ export function checkDrift(repoRoot: string, canonical: CanonicalBytes): DriftRe
|
|
|
490
545
|
}
|
|
491
546
|
}
|
|
492
547
|
|
|
548
|
+
// build-docs.yml is the embedded twin of publish-docs.yml: optional, verbatim.
|
|
549
|
+
const buildDocsPath = join(repoRoot, ".github", "workflows", "build-docs.yml");
|
|
550
|
+
if (existsSync(buildDocsPath)) {
|
|
551
|
+
const actual = readFileSync(buildDocsPath, "utf8");
|
|
552
|
+
if (actual !== canonical.buildDocs) {
|
|
553
|
+
push(
|
|
554
|
+
`.github/workflows/build-docs.yml has drifted from @norskvideo/ctl-dev-kit conventions/build-docs.yml (${firstDiffLine(actual, canonical.buildDocs)}). ${RESYNC}`,
|
|
555
|
+
);
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
for (const p of docsExposureProblems(existsSync(publishDocsPath), existsSync(buildDocsPath), opts.repoName)) push(p);
|
|
559
|
+
|
|
493
560
|
for (const p of demoProblems(repoRoot, canonical.demoShim)) push(p);
|
|
494
561
|
|
|
495
562
|
return { ok: problems.length === 0, problems };
|
|
@@ -503,6 +570,7 @@ if (import.meta.main) {
|
|
|
503
570
|
upgradeLatest: readFileSync(join(import.meta.dir, "upgrade-latest.yml"), "utf8"),
|
|
504
571
|
syncDevKit: readFileSync(join(import.meta.dir, "sync-dev-kit.yml"), "utf8"),
|
|
505
572
|
publishDocs: readFileSync(join(import.meta.dir, "publish-docs.yml"), "utf8"),
|
|
573
|
+
buildDocs: readFileSync(join(import.meta.dir, "build-docs.yml"), "utf8"),
|
|
506
574
|
checks: readFileSync(join(import.meta.dir, "checks.yml"), "utf8"),
|
|
507
575
|
biome: readFileSync(join(import.meta.dir, "biome.base.json"), "utf8"),
|
|
508
576
|
tsconfigBase: readFileSync(join(import.meta.dir, "tsconfig.base.json"), "utf8"),
|
|
@@ -513,7 +581,7 @@ if (import.meta.main) {
|
|
|
513
581
|
smoke: readFileSync(join(import.meta.dir, "smoke.yml"), "utf8"),
|
|
514
582
|
demoShim: readFileSync(join(import.meta.dir, "demo.sh"), "utf8"),
|
|
515
583
|
};
|
|
516
|
-
const report = checkDrift(repoRoot, canonical);
|
|
584
|
+
const report = checkDrift(repoRoot, canonical, { repoName: resolveRepoName(repoRoot) });
|
|
517
585
|
if (report.ok) {
|
|
518
586
|
console.log("drift-check: all shared-convention copies match @norskvideo/ctl-dev-kit.");
|
|
519
587
|
process.exit(0);
|
package/conventions/docs.md
CHANGED
|
@@ -128,19 +128,28 @@ pulls them in an optional `scripts/doc-guide/prepull-extra.sh`.
|
|
|
128
128
|
|
|
129
129
|
## The publish contract
|
|
130
130
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
`
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
131
|
+
The unit is the **docs bundle** (`buildBundle` -> `docs/generated/bundle/`:
|
|
132
|
+
`bundle.json`, `pages/*.md`, `captures/`, and the derived `manual.html`).
|
|
133
|
+
Exposure is declared by which docs workflow the repo carries — exactly one,
|
|
134
|
+
single-sourced here and drift-gated:
|
|
135
|
+
|
|
136
|
+
- **`build-docs.yml` — embedded.** Nightly: regenerate the bundle, bake it into
|
|
137
|
+
the product image through `deployment/build-image.sh`, read it back out of the
|
|
138
|
+
image as the proof. No upload, no write permission on the repo, no token. A
|
|
139
|
+
turnkey (`norsk-ctl-turnkey-*`) carries this and nothing else: its
|
|
140
|
+
documentation reaches exactly one customer, inside the image they already run.
|
|
141
|
+
- **`publish-docs.yml` — public.** The same build, then `manual.html` is
|
|
142
|
+
published as the asset of the moving `docs-latest` GitHub Release
|
|
143
|
+
(`https://github.com/<owner>/<repo>/releases/download/docs-latest/manual.html`)
|
|
144
|
+
for norsk-ctl's docs site to fetch and mount at `/products/<slug>/`. Versioned
|
|
145
|
+
bundle assets behind a `latest` pointer, and ingestion of `pages/*.md` into the
|
|
146
|
+
site, come with the public-plane work. **Only a repo GitHub calls
|
|
147
|
+
`norsk-ctl-product-*` may carry it.** `check:drift` refuses it anywhere else,
|
|
148
|
+
refuses a repo carrying both, and refuses the public variant when it cannot
|
|
149
|
+
tell the repo's name (`GITHUB_REPOSITORY` in CI, the origin remote locally).
|
|
150
|
+
|
|
151
|
+
Either way the image serves the bundle at `/docs` (SDK `serveDocs`), reached
|
|
152
|
+
through the daemon at `/products/<name>/docs/` behind its auth guard and linked
|
|
153
|
+
from the product's hub via `ui.sidebarEntries`. That is the sole channel for a
|
|
154
|
+
turnkey and the version-true channel for a product; the public site is the
|
|
155
|
+
faster-cadence copy, for products only.
|
|
@@ -232,6 +232,12 @@ export function syncDrift(repoRoot: string, canonical: CanonicalBytes): SyncRepo
|
|
|
232
232
|
if (existsSync(join(repoRoot, publishDocsRel))) {
|
|
233
233
|
writeIfChanged(join(repoRoot, publishDocsRel), canonical.publishDocs, publishDocsRel, r.written);
|
|
234
234
|
}
|
|
235
|
+
// build-docs.yml, the embedded variant, the same way: carrying it is the
|
|
236
|
+
// repo's declaration (ADR-0012); sync only keeps the copy verbatim.
|
|
237
|
+
const buildDocsRel = ".github/workflows/build-docs.yml";
|
|
238
|
+
if (existsSync(join(repoRoot, buildDocsRel))) {
|
|
239
|
+
writeIfChanged(join(repoRoot, buildDocsRel), canonical.buildDocs, buildDocsRel, r.written);
|
|
240
|
+
}
|
|
235
241
|
|
|
236
242
|
syncFlake(repoRoot, canonical.flake, r);
|
|
237
243
|
syncClaude(repoRoot, canonical.core, r);
|
|
@@ -251,6 +257,7 @@ if (import.meta.main) {
|
|
|
251
257
|
upgradeLatest: readFileSync(join(dir, "upgrade-latest.yml"), "utf8"),
|
|
252
258
|
syncDevKit: readFileSync(join(dir, "sync-dev-kit.yml"), "utf8"),
|
|
253
259
|
publishDocs: readFileSync(join(dir, "publish-docs.yml"), "utf8"),
|
|
260
|
+
buildDocs: readFileSync(join(dir, "build-docs.yml"), "utf8"),
|
|
254
261
|
checks: readFileSync(join(dir, "checks.yml"), "utf8"),
|
|
255
262
|
biome: readFileSync(join(dir, "biome.base.json"), "utf8"),
|
|
256
263
|
tsconfigBase: readFileSync(join(dir, "tsconfig.base.json"), "utf8"),
|
|
@@ -19,7 +19,7 @@ inputs:
|
|
|
19
19
|
description: "Product key: commentary | playout | probe | studio | funke | reuters."
|
|
20
20
|
required: true
|
|
21
21
|
pipeline:
|
|
22
|
-
description: "Pipeline key: build-image | integration |
|
|
22
|
+
description: "Pipeline key: build-image | integration | checks | nightly | dev-kit | smoke | docs."
|
|
23
23
|
required: true
|
|
24
24
|
status:
|
|
25
25
|
description: "success or failure."
|
|
@@ -20,8 +20,9 @@ export interface ManualOverview {
|
|
|
20
20
|
headline: string;
|
|
21
21
|
/** The lead paragraph under it. */
|
|
22
22
|
lead: string;
|
|
23
|
-
/** The hero capture (a representative shot) and its caption.
|
|
24
|
-
|
|
23
|
+
/** The hero capture (a representative shot) and its caption. A prose-only
|
|
24
|
+
* manual (a turnkey handover) has none. */
|
|
25
|
+
hero?: {
|
|
25
26
|
slug: string;
|
|
26
27
|
file: string;
|
|
27
28
|
alt: string;
|
|
@@ -41,6 +42,15 @@ export interface ManualLink {
|
|
|
41
42
|
label: string;
|
|
42
43
|
href: string;
|
|
43
44
|
}
|
|
45
|
+
/** A prose page: hand-written markdown carried as-is (a turnkey's handover
|
|
46
|
+
* document, an operator runbook). Rendered to HTML in the manual, verbatim in
|
|
47
|
+
* the bundle. Captures are optional per page; a document has none. */
|
|
48
|
+
export interface ManualDocument {
|
|
49
|
+
id: string;
|
|
50
|
+
nav: string;
|
|
51
|
+
title: string;
|
|
52
|
+
markdown: string;
|
|
53
|
+
}
|
|
44
54
|
export interface ManualSpec {
|
|
45
55
|
/** Sidebar wordmark + document brand. */
|
|
46
56
|
brand: string;
|
|
@@ -49,6 +59,8 @@ export interface ManualSpec {
|
|
|
49
59
|
overview: ManualOverview;
|
|
50
60
|
functionality: ManualPage[];
|
|
51
61
|
examples: ManualPage[];
|
|
62
|
+
/** Prose pages, listed after the walkthroughs. */
|
|
63
|
+
documents?: ManualDocument[];
|
|
52
64
|
/** Outbound links for the footer. Optional, but every published manual should
|
|
53
65
|
* carry at least one. */
|
|
54
66
|
links?: ManualLink[];
|
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
import { spawnSync } from "node:child_process";
|
|
27
27
|
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
28
28
|
import { join } from "node:path";
|
|
29
|
+
import { marked } from "marked";
|
|
29
30
|
/** Provenance as a GitHub Actions run sees it (GITHUB_SHA / GITHUB_RUN_ID), or
|
|
30
31
|
* as much of it as a local build has. */
|
|
31
32
|
export function provenanceFromEnv(env = process.env) {
|
|
@@ -111,6 +112,7 @@ export function buildManual(spec, opts) {
|
|
|
111
112
|
};
|
|
112
113
|
const FN = spec.functionality;
|
|
113
114
|
const EX = spec.examples;
|
|
115
|
+
const DOCS = spec.documents ?? [];
|
|
114
116
|
const ALL = [...FN, ...EX];
|
|
115
117
|
const byId = new Map(ALL.map((p) => [p.id, p]));
|
|
116
118
|
const render = (uri) => {
|
|
@@ -137,22 +139,36 @@ export function buildManual(spec, opts) {
|
|
|
137
139
|
<p class="lead">${esc(p.intro)}</p>
|
|
138
140
|
<ol class="steps">${steps}</ol>
|
|
139
141
|
${thin}${links}
|
|
142
|
+
</article>`;
|
|
143
|
+
}
|
|
144
|
+
function documentHtml(d) {
|
|
145
|
+
return `<article class="page doc" id="${d.id}">
|
|
146
|
+
<div class="phead"><span class="kind doc">Document</span><h1>${esc(d.title)}</h1></div>
|
|
147
|
+
<div class="prose">${marked.parse(d.markdown, { async: false })}</div>
|
|
140
148
|
</article>`;
|
|
141
149
|
}
|
|
142
150
|
const ov = spec.overview;
|
|
151
|
+
const hero = ov.hero
|
|
152
|
+
? `\n <div class="heroshot"><div class="shot"><img src="${uri(ov.hero.slug, ov.hero.file)}" alt="${esc(ov.hero.alt)}"></div><p class="hcap">${esc(ov.hero.caption)}</p></div>`
|
|
153
|
+
: "";
|
|
154
|
+
const linklist = (pages) => `<div class="linklist">${pages.map((p) => `<a href="#${p.id}">${esc(p.nav)}</a>`).join("")}</div>`;
|
|
155
|
+
const cols = [
|
|
156
|
+
FN.length ? `<div><h2>${esc(ov.byFunctionHeading ?? "By function")}</h2>${linklist(FN)}</div>` : "",
|
|
157
|
+
EX.length
|
|
158
|
+
? `<div><h2>${esc(ov.byExampleHeading ?? "By example")}</h2><p class="mini">${esc(ov.byExampleIntro)}</p>${linklist(EX)}</div>`
|
|
159
|
+
: "",
|
|
160
|
+
DOCS.length ? `<div><h2>Documents</h2>${linklist(DOCS)}</div>` : "",
|
|
161
|
+
].filter(Boolean);
|
|
143
162
|
const overview = `<article class="page" id="overview">
|
|
144
163
|
<div class="phead"><span class="kind ov">Overview</span><h1>${esc(ov.headline)}</h1></div>
|
|
145
|
-
<p class="lead">${esc(ov.lead)}</p
|
|
146
|
-
|
|
147
|
-
<div class="twocol">
|
|
148
|
-
<div><h2>${esc(ov.byFunctionHeading ?? "By function")}</h2><div class="linklist">${FN.map((p) => `<a href="#${p.id}">${esc(p.nav)}</a>`).join("")}</div></div>
|
|
149
|
-
<div><h2>${esc(ov.byExampleHeading ?? "By example")}</h2><p class="mini">${esc(ov.byExampleIntro)}</p><div class="linklist">${EX.map((p) => `<a href="#${p.id}">${esc(p.nav)}</a>`).join("")}</div></div>
|
|
150
|
-
</div>
|
|
164
|
+
<p class="lead">${esc(ov.lead)}</p>${hero}
|
|
165
|
+
${cols.length ? `<div class="twocol">\n ${cols.join("\n ")}\n </div>` : ""}
|
|
151
166
|
</article>`;
|
|
167
|
+
const navGroup = (head, pages) => pages.length
|
|
168
|
+
? `\n <div class="ngroup"><div class="ghead">${head}</div>${pages.map((p) => `<a href="#${p.id}" data-nav>${esc(p.nav)}</a>`).join("")}</div>`
|
|
169
|
+
: "";
|
|
152
170
|
const nav = `
|
|
153
|
-
<a href="#overview" data-nav class="ovlink">Overview</a
|
|
154
|
-
<div class="ngroup"><div class="ghead">Functionality</div>${FN.map((p) => `<a href="#${p.id}" data-nav>${esc(p.nav)}</a>`).join("")}</div>
|
|
155
|
-
<div class="ngroup"><div class="ghead">Examples <span class="ct">${EX.length}</span></div>${EX.map((p) => `<a href="#${p.id}" data-nav>${esc(p.nav)}</a>`).join("")}</div>`;
|
|
171
|
+
<a href="#overview" data-nav class="ovlink">Overview</a>${navGroup("Functionality", FN)}${navGroup(`Examples <span class="ct">${EX.length}</span>`, EX)}${navGroup("Documents", DOCS)}`;
|
|
156
172
|
const html = `<style>
|
|
157
173
|
:root{--bg:#0a0d12;--bg2:#0c1118;--surf:#131923;--ink:#eaeff5;--dim:#939dab;--faint:#5f6a79;
|
|
158
174
|
--line:#212a36;--accent:#54d6cf;--measure:64ch}
|
|
@@ -184,7 +200,18 @@ main{min-width:0;padding:0 clamp(20px,5vw,72px)}
|
|
|
184
200
|
@media (prefers-reduced-motion:reduce){.page{animation:none}}
|
|
185
201
|
.phead{margin-bottom:20px}
|
|
186
202
|
.kind{display:inline-block;font-family:ui-monospace,Menlo,monospace;font-size:10px;letter-spacing:.14em;text-transform:uppercase;color:var(--accent);margin-bottom:12px}
|
|
187
|
-
.kind.ex{color:var(--dim)}.kind.ov{color:var(--faint)}
|
|
203
|
+
.kind.ex{color:var(--dim)}.kind.ov{color:var(--faint)}.kind.doc{color:var(--dim)}
|
|
204
|
+
.prose{max-width:var(--measure)}
|
|
205
|
+
.prose h2{font-size:22px;margin:36px 0 10px;letter-spacing:-.015em}
|
|
206
|
+
.prose h3{font-size:17px;margin:26px 0 8px}
|
|
207
|
+
.prose p,.prose li{color:var(--dim);font-size:15px}
|
|
208
|
+
.prose code{font-family:ui-monospace,Menlo,monospace;font-size:.92em;background:var(--surf);border:1px solid var(--line);border-radius:4px;padding:1px 5px}
|
|
209
|
+
.prose pre{background:var(--surf);border:1px solid var(--line);border-radius:9px;padding:14px 16px;overflow-x:auto;font-size:13px}
|
|
210
|
+
.prose pre code{background:none;border:none;padding:0}
|
|
211
|
+
.prose table{border-collapse:collapse;width:100%;font-size:14px;margin:16px 0}
|
|
212
|
+
.prose th,.prose td{text-align:left;padding:8px 10px;border-bottom:1px solid var(--line);vertical-align:top}
|
|
213
|
+
.prose th{color:var(--ink);font-weight:600}
|
|
214
|
+
.prose a{color:var(--accent)}
|
|
188
215
|
.phead h1{font-size:clamp(26px,4vw,42px);line-height:1.05;letter-spacing:-.028em;margin:0;font-weight:730;text-wrap:balance}
|
|
189
216
|
.ptag{margin-top:12px;font-size:12px;color:var(--faint)}
|
|
190
217
|
.lead{font-size:clamp(15px,1.9vw,18px);color:var(--dim);max-width:var(--measure);margin:0 0 8px}
|
|
@@ -227,6 +254,7 @@ main{min-width:0;padding:0 clamp(20px,5vw,72px)}
|
|
|
227
254
|
<main>
|
|
228
255
|
${overview}
|
|
229
256
|
${ALL.map(pageHtml).join("\n")}
|
|
257
|
+
${DOCS.map(documentHtml).join("\n")}
|
|
230
258
|
${footerHtml(spec, provenance)}
|
|
231
259
|
</main>
|
|
232
260
|
</div>
|
package/doc-guide/bundle.d.ts
CHANGED
package/doc-guide/bundle.js
CHANGED
|
@@ -28,6 +28,7 @@ export function buildBundle(spec, opts) {
|
|
|
28
28
|
// Pages live one level down, so a capture href is reached via `../`.
|
|
29
29
|
const captureRef = (slug, file) => `../${hrefs[`${slug}/${file}`]}`;
|
|
30
30
|
const all = [...spec.functionality, ...spec.examples];
|
|
31
|
+
const docs = spec.documents ?? [];
|
|
31
32
|
const byId = new Map(all.map((p) => [p.id, p]));
|
|
32
33
|
const pageLink = (id) => {
|
|
33
34
|
const t = byId.get(id);
|
|
@@ -57,28 +58,35 @@ export function buildBundle(spec, opts) {
|
|
|
57
58
|
"",
|
|
58
59
|
ov.lead,
|
|
59
60
|
"",
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
...spec.
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
61
|
+
...(ov.hero
|
|
62
|
+
? [`})`, "", `_${ov.hero.caption}_`, ""]
|
|
63
|
+
: []),
|
|
64
|
+
...(spec.functionality.length
|
|
65
|
+
? [`## ${ov.byFunctionHeading ?? "By function"}`, "", ...spec.functionality.map((p) => `- ${pageLink(p.id)}`), ""]
|
|
66
|
+
: []),
|
|
67
|
+
...(spec.examples.length
|
|
68
|
+
? [
|
|
69
|
+
`## ${ov.byExampleHeading ?? "By example"}`,
|
|
70
|
+
"",
|
|
71
|
+
ov.byExampleIntro,
|
|
72
|
+
"",
|
|
73
|
+
...spec.examples.map((p) => `- ${pageLink(p.id)}`),
|
|
74
|
+
"",
|
|
75
|
+
]
|
|
76
|
+
: []),
|
|
77
|
+
...(docs.length ? ["## Documents", "", ...docs.map((d) => `- [${d.nav}](${d.id}.md)`), ""] : []),
|
|
74
78
|
].join("\n");
|
|
79
|
+
const documentMarkdown = (d) => ["---", `title: ${yamlString(d.title)}`, `nav: ${yamlString(d.nav)}`, "kind: doc", "---", "", d.markdown].join("\n");
|
|
75
80
|
const pages = [
|
|
76
81
|
{ id: "overview", nav: "Overview", title: ov.headline, kind: "overview", file: "pages/overview.md" },
|
|
77
82
|
...all.map((p) => ({ id: p.id, nav: p.nav, title: p.title, kind: p.kind, file: `pages/${p.id}.md` })),
|
|
83
|
+
...docs.map((d) => ({ id: d.id, nav: d.nav, title: d.title, kind: "doc", file: `pages/${d.id}.md` })),
|
|
78
84
|
];
|
|
79
85
|
writeFileSync(join(outDir, "pages/overview.md"), overviewMarkdown);
|
|
80
86
|
for (const p of all)
|
|
81
87
|
writeFileSync(join(outDir, `pages/${p.id}.md`), pageMarkdown(p));
|
|
88
|
+
for (const d of docs)
|
|
89
|
+
writeFileSync(join(outDir, `pages/${d.id}.md`), documentMarkdown(d));
|
|
82
90
|
const exports = { manual: "manual.html", manualLinked: "manual-linked.html" };
|
|
83
91
|
writeFileSync(join(outDir, exports.manual), manual.standaloneHtml);
|
|
84
92
|
writeFileSync(join(outDir, exports.manualLinked), manual.standaloneAssetHtml ?? manual.standaloneHtml);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@norskvideo/ctl-dev-kit",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.58",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"exports": {
|
|
6
6
|
"./package.json": "./package.json",
|
|
@@ -19,7 +19,8 @@
|
|
|
19
19
|
"ctl-dev-kit": "./create-product/cli.ts"
|
|
20
20
|
},
|
|
21
21
|
"dependencies": {
|
|
22
|
-
"@norskvideo/ctl-sdk": "^0.1.0"
|
|
22
|
+
"@norskvideo/ctl-sdk": "^0.1.0",
|
|
23
|
+
"marked": "18.0.11"
|
|
23
24
|
},
|
|
24
25
|
"peerDependencies": {
|
|
25
26
|
"@playwright/test": "*"
|