@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
package/conventions/bot-prs.yml
CHANGED
|
@@ -13,12 +13,27 @@
|
|
|
13
13
|
# So, for every open PR whose branch a bot owns:
|
|
14
14
|
# - every check on its head has passed and it merges cleanly -> squash-merge it
|
|
15
15
|
# (the PR's checks + smoke ARE the verdict; integration then runs on main);
|
|
16
|
-
# -
|
|
17
|
-
#
|
|
18
|
-
#
|
|
16
|
+
# - it conflicts with main -> re-run the bot that owns the branch, ONCE per
|
|
17
|
+
# head, so it rebuilds the branch from today's main (see below);
|
|
18
|
+
# - a check failed, it still conflicts after that rebuild, or it is a draft
|
|
19
|
+
# (the bot's own gate was red) -> list it, and report this repo's `bot-prs`
|
|
20
|
+
# cell RED on the product dashboard, which is the Discord message people
|
|
21
|
+
# actually read;
|
|
19
22
|
# - checks still running -> leave it for the next trigger.
|
|
20
23
|
# The cell goes green again once nothing is red.
|
|
21
24
|
#
|
|
25
|
+
# Why the conflict rebuild: the three bots fire within minutes of each other
|
|
26
|
+
# each morning and all write package.json and bun.lock, each from main as it
|
|
27
|
+
# stood. Whichever lands first leaves the other's open PR conflicting, and the
|
|
28
|
+
# bot only rebuilds its branch on its next DAILY run -- so on 2026-10-09
|
|
29
|
+
# commentary, probe and reuters each had a sync-ctl-packages PR "needing a
|
|
30
|
+
# human" that a re-run of sync-ctl-packages fixed outright. Every bot rebuilds
|
|
31
|
+
# from main when run: sync-ctl-packages force-pushes a fresh branch, and
|
|
32
|
+
# upgrade-latest / sync-dev-kit either land on main and close the PR they
|
|
33
|
+
# superseded, or force-push their branch. The marker comment caps it at one
|
|
34
|
+
# rebuild per head, so a rebuild that still conflicts goes to a human instead
|
|
35
|
+
# of looping every hour.
|
|
36
|
+
#
|
|
22
37
|
# Runs when a PR's checks or smoke finish (the prompt path), hourly as a sweep,
|
|
23
38
|
# and on demand. No checkout: it only talks to the GitHub API, so it needs none
|
|
24
39
|
# of the runner-workspace steps the building workflows carry.
|
|
@@ -41,6 +56,8 @@ permissions:
|
|
|
41
56
|
contents: read
|
|
42
57
|
checks: read
|
|
43
58
|
pull-requests: read
|
|
59
|
+
# The rebuild dispatch and its marker comment go through the PAT (WRITE_TOKEN),
|
|
60
|
+
# like the merges; this token only reads.
|
|
44
61
|
|
|
45
62
|
concurrency:
|
|
46
63
|
# One at a time per repo, and never cancelled: a merge half-done by a run that
|
|
@@ -86,8 +103,19 @@ jobs:
|
|
|
86
103
|
| select(.head.ref | test("^(upgrade-latest/|sync-dev-kit/|chore/sync-ctl-packages$)"))
|
|
87
104
|
| {number, ref: .head.ref, sha: .head.sha, draft, url: .html_url}]' <<<"$pulls")"
|
|
88
105
|
|
|
106
|
+
# The workflow that owns each bot branch, i.e. the one that rebuilds it.
|
|
107
|
+
producer() {
|
|
108
|
+
case "$1" in
|
|
109
|
+
upgrade-latest/*) echo upgrade-latest.yml ;;
|
|
110
|
+
sync-dev-kit/*) echo sync-dev-kit.yml ;;
|
|
111
|
+
chore/sync-ctl-packages) echo sync-ctl-packages.yml ;;
|
|
112
|
+
*) echo "" ;;
|
|
113
|
+
esac
|
|
114
|
+
}
|
|
115
|
+
|
|
89
116
|
red=()
|
|
90
117
|
merged=()
|
|
118
|
+
rebuilt=()
|
|
91
119
|
while IFS= read -r pr; do
|
|
92
120
|
[ -n "$pr" ] || continue
|
|
93
121
|
n="$(jq -r .number <<<"$pr")"; ref="$(jq -r .ref <<<"$pr")"; sha="$(jq -r .sha <<<"$pr")"
|
|
@@ -106,7 +134,29 @@ jobs:
|
|
|
106
134
|
[ "$mergeable" != "null" ] && break
|
|
107
135
|
sleep 3
|
|
108
136
|
done
|
|
109
|
-
if [ "$mergeable" != "true" ]; then
|
|
137
|
+
if [ "$mergeable" != "true" ]; then
|
|
138
|
+
# One rebuild per head: the marker names the sha it was asked for.
|
|
139
|
+
marker="bot-prs: rebuild requested for head $sha"
|
|
140
|
+
wf="$(producer "$ref")"
|
|
141
|
+
comments="$(gh_api "$api/issues/$n/comments?per_page=100")"
|
|
142
|
+
expect "reading comments on #$n" 'type == "array"' "$comments"
|
|
143
|
+
if [ -z "$wf" ]; then
|
|
144
|
+
red+=("#$n $ref: does not merge cleanly with main, and no bot is known to rebuild it")
|
|
145
|
+
elif jq -e --arg m "$marker" 'any(.[]; .body | contains($m))' >/dev/null <<<"$comments"; then
|
|
146
|
+
red+=("#$n $ref: still does not merge cleanly with main after $wf rebuilt it")
|
|
147
|
+
else
|
|
148
|
+
code="$(gh_write -o /dev/null -w '%{http_code}' -X POST "$api/actions/workflows/$wf/dispatches" -d '{"ref":"main"}')"
|
|
149
|
+
if [ "$code" = "204" ]; then
|
|
150
|
+
body="$marker -- it conflicted with main (another bot landed first), so $wf was re-run to rebuild it from today's main. If the rebuilt branch conflicts too, this PR is reported for a human."
|
|
151
|
+
gh_write -o /dev/null -X POST "$api/issues/$n/comments" -d "$(jq -nc --arg b "$body" '{body:$b}')" || true
|
|
152
|
+
rebuilt+=("#$n")
|
|
153
|
+
echo "#$n $ref: conflicts with main; re-ran $wf to rebuild it"
|
|
154
|
+
else
|
|
155
|
+
red+=("#$n $ref: does not merge cleanly with main, and re-running $wf failed (HTTP $code)")
|
|
156
|
+
fi
|
|
157
|
+
fi
|
|
158
|
+
continue
|
|
159
|
+
fi
|
|
110
160
|
# sha pins the merge to what was checked: a bot force-pushing the
|
|
111
161
|
# branch meanwhile makes this refuse rather than land unchecked code.
|
|
112
162
|
res="$(gh_write -X PUT "$api/pulls/$n/merge" -d "$(jq -nc --arg sha "$sha" '{merge_method:"squash", sha:$sha}')")"
|
|
@@ -128,7 +178,9 @@ jobs:
|
|
|
128
178
|
state=success
|
|
129
179
|
# Never empty: the dashboard step reads an empty detail as missing.
|
|
130
180
|
merged_detail="${merged[*]:+merged ${merged[*]}}"
|
|
131
|
-
|
|
181
|
+
rebuilt_detail="${rebuilt[*]:+rebuilding ${rebuilt[*]}}"
|
|
182
|
+
detail="$(printf '%s' "$merged_detail${merged_detail:+${rebuilt_detail:+; }}$rebuilt_detail")"
|
|
183
|
+
detail="${detail:-no bot PRs waiting}"
|
|
132
184
|
fi
|
|
133
185
|
echo "state=$state" >> "$GITHUB_OUTPUT"
|
|
134
186
|
echo "detail=${detail%; }" >> "$GITHUB_OUTPUT"
|
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
// gated file uses the loosest mechanism that still pins what is shared:
|
|
4
4
|
// - byte-verbatim: CLAUDE.md fenced core, nix/ctl-shell.nix, biome.json,
|
|
5
5
|
// tsconfig.base.json, the .gitignore core block, scripts/demo (only a
|
|
6
|
-
// product with tests/demo.spec.ts is asked for it)
|
|
6
|
+
// product with tests/demo.spec.ts is asked for it), publish-docs-site.yml
|
|
7
|
+
// (only a repo with a docs site may carry it, and never a turnkey)
|
|
7
8
|
// - masked per-repo line: checks.yml + upgrade-latest.yml (`product:` key)
|
|
8
9
|
// - base + sanctioned extension block: dprint.json (repo-specific excludes)
|
|
9
10
|
// - must import the shared module: flake.nix (otherwise the product's own)
|
|
@@ -272,6 +273,10 @@ export interface CanonicalBytes {
|
|
|
272
273
|
publishDocs: string;
|
|
273
274
|
/** conventions/build-docs.yml — the embedded docs variant (bundle -> image, no upload). */
|
|
274
275
|
buildDocs: string;
|
|
276
|
+
/** conventions/publish-docs-site.yml — the docs SITE's publish (website/ ->
|
|
277
|
+
* Cloudflare Pages on every push to main). Verbatim; allowed only where
|
|
278
|
+
* there is a site to publish and never in a turnkey. */
|
|
279
|
+
publishDocsSite: string;
|
|
275
280
|
checks: string;
|
|
276
281
|
biome: string;
|
|
277
282
|
tsconfigBase: string;
|
|
@@ -506,6 +511,10 @@ export function resolveRepoName(repoRoot: string, env: NodeJS.ProcessEnv = proce
|
|
|
506
511
|
}
|
|
507
512
|
|
|
508
513
|
const PUBLIC_DOCS_REPO = /^norsk-ctl-product-/;
|
|
514
|
+
const TURNKEY_REPO = /^norsk-ctl-turnkey-/;
|
|
515
|
+
|
|
516
|
+
/** The docs site's publish workflow, where a repo carries one. */
|
|
517
|
+
export const DOCS_SITE_WORKFLOW = ".github/workflows/publish-docs-site.yml";
|
|
509
518
|
|
|
510
519
|
/** ADR-0012: exposure is declared by which docs workflow a repo carries, so the
|
|
511
520
|
* gate is what makes that declaration safe — exactly one variant; the public
|
|
@@ -530,6 +539,37 @@ export function docsExposureProblems(hasPublish: boolean, hasBuild: boolean, rep
|
|
|
530
539
|
return problems;
|
|
531
540
|
}
|
|
532
541
|
|
|
542
|
+
/** Where a PUBLIC docs site may exist. The rule is negative on the repo name
|
|
543
|
+
* where publish-docs.yml's is positive, because sites live outside
|
|
544
|
+
* norsk-ctl-product-* (norsk-relay has one, and so do norsk-ctl and the
|
|
545
|
+
* Manager); what is certain is that a turnkey has none -- its documentation
|
|
546
|
+
* reaches one customer, inside the image they run (ADR-0012). And a site has
|
|
547
|
+
* to exist to be published: a repo without website/astro.config.mjs carrying
|
|
548
|
+
* the workflow would deploy nothing, or whatever a stale dist held. */
|
|
549
|
+
export function docsSiteExposureProblems(
|
|
550
|
+
hasWorkflow: boolean,
|
|
551
|
+
hasSite: boolean,
|
|
552
|
+
repoName: string | undefined,
|
|
553
|
+
): string[] {
|
|
554
|
+
if (!hasWorkflow) return [];
|
|
555
|
+
const problems: string[] = [];
|
|
556
|
+
if (!hasSite) {
|
|
557
|
+
problems.push(
|
|
558
|
+
`${DOCS_SITE_WORKFLOW} publishes a docs site, but this repo has none (no ${DOCS_SITE_DIR}/astro.config.mjs). Add the site (conventions/docs-site.md) or delete the workflow.`,
|
|
559
|
+
);
|
|
560
|
+
}
|
|
561
|
+
if (repoName === undefined) {
|
|
562
|
+
problems.push(
|
|
563
|
+
`${DOCS_SITE_WORKFLOW} publishes this repo's docs site publicly, and the gate cannot tell which repo this is (no GITHUB_REPOSITORY, no origin remote). A turnkey (norsk-ctl-turnkey-*) may not carry it.`,
|
|
564
|
+
);
|
|
565
|
+
} else if (TURNKEY_REPO.test(repoName)) {
|
|
566
|
+
problems.push(
|
|
567
|
+
`${DOCS_SITE_WORKFLOW} publishes a public docs site, but this repo is ${repoName}, a turnkey. A turnkey has no public site: its documentation reaches one customer, inside the image they run (ADR-0012). Delete the workflow.`,
|
|
568
|
+
);
|
|
569
|
+
}
|
|
570
|
+
return problems;
|
|
571
|
+
}
|
|
572
|
+
|
|
533
573
|
export function checkDrift(repoRoot: string, canonical: CanonicalBytes, opts: CheckDriftOptions = {}): DriftReport {
|
|
534
574
|
const problems: string[] = [];
|
|
535
575
|
const push = (problem: string | undefined) => {
|
|
@@ -808,6 +848,20 @@ export function checkDrift(repoRoot: string, canonical: CanonicalBytes, opts: Ch
|
|
|
808
848
|
// checked, and one clause covers both.
|
|
809
849
|
for (const p of docsThemeProblems(repoRoot, opts.themeCanonical)) push(p);
|
|
810
850
|
|
|
851
|
+
// publish-docs-site.yml: the site's publish, verbatim (no per-repo line: the
|
|
852
|
+
// project name is a repo variable, the credentials are secrets), and only
|
|
853
|
+
// where docsSiteExposureProblems allows a public site at all.
|
|
854
|
+
const siteWorkflowPath = join(repoRoot, DOCS_SITE_WORKFLOW);
|
|
855
|
+
if (existsSync(siteWorkflowPath)) {
|
|
856
|
+
const actual = readFileSync(siteWorkflowPath, "utf8");
|
|
857
|
+
if (actual !== canonical.publishDocsSite) {
|
|
858
|
+
push(
|
|
859
|
+
`${DOCS_SITE_WORKFLOW} has drifted from @norskvideo/ctl-dev-kit conventions/publish-docs-site.yml (${firstDiffLine(actual, canonical.publishDocsSite)}). ${RESYNC}`,
|
|
860
|
+
);
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
for (const p of docsSiteExposureProblems(existsSync(siteWorkflowPath), hasDocsSite(repoRoot), opts.repoName)) push(p);
|
|
864
|
+
|
|
811
865
|
for (const p of demoProblems(repoRoot, canonical.demoShim)) push(p);
|
|
812
866
|
for (const p of productLogsProblems(repoRoot)) push(p);
|
|
813
867
|
for (const p of ctlPinProblems(repoRoot)) push(p);
|
|
@@ -827,6 +881,7 @@ if (import.meta.main) {
|
|
|
827
881
|
botPrs: readFileSync(join(import.meta.dir, "bot-prs.yml"), "utf8"),
|
|
828
882
|
publishDocs: readFileSync(join(import.meta.dir, "publish-docs.yml"), "utf8"),
|
|
829
883
|
buildDocs: readFileSync(join(import.meta.dir, "build-docs.yml"), "utf8"),
|
|
884
|
+
publishDocsSite: readFileSync(join(import.meta.dir, "publish-docs-site.yml"), "utf8"),
|
|
830
885
|
withDisplay: readFileSync(join(import.meta.dir, "..", "doc-guide", "with-display.sh"), "utf8"),
|
|
831
886
|
checks: readFileSync(join(import.meta.dir, "checks.yml"), "utf8"),
|
|
832
887
|
biome: readFileSync(join(import.meta.dir, "biome.base.json"), "utf8"),
|
package/conventions/docs-site.md
CHANGED
|
@@ -8,7 +8,7 @@ Like [`docs.md`](./docs.md) and [`personas.md`](./personas.md) this is a **shipp
|
|
|
8
8
|
reference**, not a drift-gated copy: read it from
|
|
9
9
|
`node_modules/@norskvideo/ctl-dev-kit/conventions/docs-site.md`. What IS gated is
|
|
10
10
|
the theme itself — see "The theme is a copy, and the gate is what makes that
|
|
11
|
-
safe" below.
|
|
11
|
+
safe" below — and the publish workflow, see "Publishing".
|
|
12
12
|
|
|
13
13
|
**`create-product` renders all of it.** A repo generated by
|
|
14
14
|
`bunx @norskvideo/ctl-dev-kit create-product` comes out with a `website/` that
|
|
@@ -78,10 +78,24 @@ each section serves, in the vocabulary of [`personas.md`](./personas.md):
|
|
|
78
78
|
|
|
79
79
|
- **Start here** — the Evaluator. What this does, and where to go next.
|
|
80
80
|
- **Guides** — the Builder. One task, start to finish, in the order it is done.
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
81
|
+
**A Guides page is the recorded chapter where one exists**: a symlink from
|
|
82
|
+
`website/src/content/docs/guides/<slug>` into the `docs/generated/<slug>`
|
|
83
|
+
tree a doc-guide writes, with at most a prose introduction above it. That
|
|
84
|
+
is what the Manager's site does for every guide and what makes the page
|
|
85
|
+
unable to describe a screen that does not exist; see [`docs.md`](./docs.md)
|
|
86
|
+
for the toolchain, the `capturedAgainst` front matter the theme renders at
|
|
87
|
+
the foot of such a page, and the placeholder the dev-kit writes so the site
|
|
88
|
+
builds on a checkout with no captures. **When a product has more than one
|
|
89
|
+
surface** (a console and an API, or a CLI as well), show the same task on
|
|
90
|
+
each: `<task>`, `<task>-ui`, `<task>-api`, grouped under the task in the
|
|
91
|
+
sidebar, as the Manager does for launching a show format, draining a node
|
|
92
|
+
and API tokens. Not every task needs every surface; the first task on the
|
|
93
|
+
Builder's route should have them all.
|
|
84
94
|
- **How-to** — the Administrator. Looked up, repeatedly, while running it.
|
|
95
|
+
Its first page, **"What this product adds to the host"** (images, ports,
|
|
96
|
+
volumes, GPU, state), is generated from the manifest and the product
|
|
97
|
+
template, where those facts already live, and never hand-written; the
|
|
98
|
+
rest of the section ends with a link into norsk-ctl's manual.
|
|
85
99
|
- **Concepts** — the vocabulary the rest of the manual uses. Not architecture.
|
|
86
100
|
- **Reference** — the Integrator. Exhaustive and generated from the source of
|
|
87
101
|
truth where possible.
|
|
@@ -89,6 +103,16 @@ each section serves, in the vocabulary of [`personas.md`](./personas.md):
|
|
|
89
103
|
Add sections below these if a product genuinely needs them. Do not rename or
|
|
90
104
|
reorder these five: the shape is the shared part.
|
|
91
105
|
|
|
106
|
+
**Start here and Guides are the light half**, written for readers who do not
|
|
107
|
+
know our engineering vocabulary: one command per step, no flags, the result
|
|
108
|
+
before the prerequisites, a single "Next" link at the foot of each page, and a
|
|
109
|
+
word budget. `docs/light-pages.ts` lints those pages (see `docs.md`, "Voice").
|
|
110
|
+
How-to and Reference may be as exhaustive as the Administrator and the
|
|
111
|
+
Integrator want. The install section of Start here is the theme's
|
|
112
|
+
`components/InstallProduct.astro`: the product's one-line install, one sentence
|
|
113
|
+
on what it leaves behind, and one link into norsk-ctl's manual. It is repeated
|
|
114
|
+
on every product site on purpose and generated so the wording cannot drift.
|
|
115
|
+
|
|
92
116
|
## The theme is a copy, and the gate is what makes that safe
|
|
93
117
|
|
|
94
118
|
The theme reaches a site as **files copied into it**, not as an import, and
|
|
@@ -138,6 +162,27 @@ Astro derives a component's scoped-style class from its path, so moving
|
|
|
138
162
|
Then `bun install --cwd website` (the repo-root install does NOT cover a
|
|
139
163
|
standalone package) and `bun run docs:site`.
|
|
140
164
|
|
|
165
|
+
### Adding a recorded guide
|
|
166
|
+
|
|
167
|
+
Two lines. A symlink from the site's Guides into the tree the doc-guide
|
|
168
|
+
writes, and a sidebar entry:
|
|
169
|
+
|
|
170
|
+
```sh
|
|
171
|
+
ln -s ../../../../../docs/generated/<slug> website/src/content/docs/guides/<slug>
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
```js
|
|
175
|
+
{ label: "<Title>", slug: "guides/<slug>" }
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
`docs:site` runs the dev-kit's `docs/guide-stubs.ts stub` first, so the site
|
|
179
|
+
builds on a checkout that has never captured; `publish-docs-site.yml` runs
|
|
180
|
+
`guide-stubs.ts check` after the build and refuses to deploy while any guide
|
|
181
|
+
is still a placeholder. That refusal is deliberate: a product that declares
|
|
182
|
+
recorded guides must also restore (or regenerate) its captures in that
|
|
183
|
+
workflow before the build, and until it does, the publish stops rather than
|
|
184
|
+
shipping an empty page.
|
|
185
|
+
|
|
141
186
|
## The two builds
|
|
142
187
|
|
|
143
188
|
One content collection, two outputs, selected by `ASTRO_BUILD`:
|
|
@@ -159,22 +204,56 @@ of compressed docs on a ~68 MB binary.
|
|
|
159
204
|
|
|
160
205
|
## Publishing
|
|
161
206
|
|
|
162
|
-
|
|
207
|
+
The site publishes from **`.github/workflows/publish-docs-site.yml`**,
|
|
208
|
+
single-sourced in this package (`conventions/publish-docs-site.yml`) and
|
|
209
|
+
**drift-gated** the way the bundle's `publish-docs.yml` is: `bun run check:drift`
|
|
210
|
+
holds the copy byte-identical, and `bun run sync:drift` writes it — creating it
|
|
211
|
+
where the repo has a site and is not a turnkey. On every push to `main` (and on
|
|
212
|
+
demand) it installs `website/`, builds the public variant and deploys
|
|
213
|
+
`website/dist-public` to Cloudflare Pages with `wrangler`. It never cancels a
|
|
214
|
+
run in flight, because a cancelled deploy is a half-published site. It restores
|
|
215
|
+
no capture cache: a product site builds from the committed prose, so there is
|
|
216
|
+
nothing a nightly run would have that a push does not.
|
|
217
|
+
|
|
218
|
+
Where it may exist is the gate's business (ADR-0012, and the documentation
|
|
219
|
+
plan): a repo with `website/astro.config.mjs` may carry it; a repo without a
|
|
220
|
+
site may not, since there is nothing to publish; a turnkey
|
|
221
|
+
(`norsk-ctl-turnkey-*`) may not, since a turnkey has no public site. The rule
|
|
222
|
+
is negative on the repo name where `publish-docs.yml`'s is positive, because
|
|
223
|
+
sites exist outside `norsk-ctl-product-*` — Relay has one.
|
|
224
|
+
|
|
225
|
+
What the repo needs, per site, and none of it to build or review one locally:
|
|
163
226
|
|
|
164
227
|
1. **A Cloudflare Pages project**, e.g. `norsk-acme-docs`.
|
|
165
228
|
2. **A custom domain** on it — `<product>.norsk.video` — and the DNS record.
|
|
166
|
-
3. **
|
|
167
|
-
|
|
168
|
-
the account, or issue a second token
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
229
|
+
3. **Two repo secrets.** `CLOUDFLARE_API_TOKEN`, a Pages:Edit token — the
|
|
230
|
+
existing one is deliberately narrow and must cover the new project: widen it
|
|
231
|
+
to all Pages projects in the account, or issue a second token — and
|
|
232
|
+
`CLOUDFLARE_ACCOUNT_ID`. The workflow verifies the token at the API (both the
|
|
233
|
+
user-owned and the account-owned endpoint), never with `wrangler whoami`,
|
|
234
|
+
which a Pages:Edit token cannot answer.
|
|
235
|
+
4. **One repo variable.** `NORSK_DOCS_SITE_PROJECT`, the project name from
|
|
236
|
+
step 1. It has **no default**: unset, the workflow still builds the site,
|
|
237
|
+
prints exactly these steps and exits 0 — so `main` is green before
|
|
238
|
+
Cloudflare exists and publishes the day the variable is set, with no change
|
|
239
|
+
to the workflow.
|
|
240
|
+
|
|
241
|
+
A `workflow_dispatch` from a branch other than `main` deploys a **preview** of
|
|
242
|
+
that branch (`--branch` is the ref that ran), never over production.
|
|
243
|
+
|
|
244
|
+
### Wiring it into a repo that has a site
|
|
245
|
+
|
|
246
|
+
Two steps, once a dev-kit carrying the workflow is published:
|
|
247
|
+
|
|
248
|
+
```
|
|
249
|
+
bun update @norskvideo/ctl-dev-kit # bump the pin (or let sync-dev-kit.yml do it)
|
|
250
|
+
bun run sync:drift # writes .github/workflows/publish-docs-site.yml
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
Commit the result; `bun run check:drift` passes. The nightly `sync-dev-kit.yml`
|
|
254
|
+
does both hands-free, so a product with a site acquires the workflow on its
|
|
255
|
+
next sync without anyone touching the repo. Then create the Pages project, set
|
|
256
|
+
the two secrets and the variable, and the next push to `main` publishes.
|
|
178
257
|
|
|
179
258
|
## Search does not cross products
|
|
180
259
|
|
package/conventions/docs.md
CHANGED
|
@@ -52,6 +52,14 @@ product's manual in one voice:
|
|
|
52
52
|
- **No internal/test vocabulary.** Never `engine`, `fixture`, `Playwright`,
|
|
53
53
|
`assertion`, `harness`, `instance`, `SSR`, a slug, or a DOM id in reader-facing
|
|
54
54
|
copy. The reader does not know or care how the shot was produced.
|
|
55
|
+
**Scoped and checked (October 2026):** the rule applies to the light half of
|
|
56
|
+
a manual, which is a site's Start here pages (`index`, `quickstart`) and its
|
|
57
|
+
`guides/`, plus a repo's `docs/quickstart.md`. `docs/light-pages.ts` in this
|
|
58
|
+
package lints exactly those pages for the deny list and a word budget, skipping
|
|
59
|
+
code, comments and imports, and runs from `bun run docs:check`. A repo whose
|
|
60
|
+
own vocabulary is on the list allows the word (`--allow`); `instance` is not
|
|
61
|
+
on it, because it is the label on norsk-ctl's own screens. How-to and
|
|
62
|
+
Reference may use any of it.
|
|
55
63
|
- **Lead with the capability, not the UI state.** "A commentator joins from a
|
|
56
64
|
browser with a link" — not "the connections page shows a slot in the pending
|
|
57
65
|
state."
|
|
@@ -89,11 +97,44 @@ Per-step treatments (`CaptureTreatment`, from the doc-guide library):
|
|
|
89
97
|
`live` on the first decoded frame, or `error` with the reason in
|
|
90
98
|
`data-moq-error`. The player mounts at the moment the source connects, which
|
|
91
99
|
is also when a guide's "it's running" assertion passes — so a capture taken
|
|
92
|
-
then is of a spinner, and a sleep only hides a preview that is broken.
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
100
|
+
then is of a spinner, and a sleep only hides a preview that is broken. The
|
|
101
|
+
phase alone is not enough, though: Commentary's standby slate and its "Enter
|
|
102
|
+
the booth" modal both passed it (2026-10-07), because the player underneath
|
|
103
|
+
was live. So the gate judges the visible composite as well: the element at
|
|
104
|
+
the tile's centre (`document.elementFromPoint`) must be the tile or inside
|
|
105
|
+
it, nothing matching the slate/modal selectors (`[data-slate]`,
|
|
106
|
+
`[role="dialog"]`, `.modal`, or visible text matching
|
|
107
|
+
`/temporarily unavailable|standby/i`) may be visible inside it, and two
|
|
108
|
+
32x18 luma samples of its video or canvas taken 500 ms apart must differ —
|
|
109
|
+
a slate, however bright, is still. Name the tile here, or call
|
|
110
|
+
`awaitLivePicture(page, selector, gate?)` before the capture (the verify run
|
|
111
|
+
skips captures, so an explicit wait is also the assertion); either way a
|
|
112
|
+
tile that never shows a picture fails the guide, naming the check it never
|
|
113
|
+
passed. Add a product's own slate markers with `livePictureGate.
|
|
114
|
+
occluderSelectors`. A tile with no video or canvas in it (an audio-only
|
|
115
|
+
player) cannot pass, so do not name one.
|
|
116
|
+
|
|
117
|
+
**Captures are shot at 2x in a `DOCS_GENERATE` run.** `guidesConfig` defaults
|
|
118
|
+
`deviceScaleFactor` from `captureScaleFactor()`: 2 when `DOCS_GENERATE=1`, 1
|
|
119
|
+
otherwise, and `launchLiveBrowser` (the engine tier) reads the same default.
|
|
120
|
+
The video spike measured the manual's component crops at 383 to 778 px wide;
|
|
121
|
+
blown up to sit in a 1080p frame they were legible but soft, and no renderer
|
|
122
|
+
step recovers detail that was never captured. A plain run of a guide writes no
|
|
123
|
+
captures, so it stays at 1x and nothing else about it changes; pass
|
|
124
|
+
`deviceScaleFactor` explicitly to pin either. Expect the PNGs to grow with the
|
|
125
|
+
pixel count.
|
|
126
|
+
|
|
127
|
+
### When a guide runs
|
|
128
|
+
|
|
129
|
+
A fixture-tier guide costs seconds and needs no licence, so it runs in the
|
|
130
|
+
**PR check** (`checks.yml`), where a broken capture is found by its author;
|
|
131
|
+
the engine tier runs **nightly** in the docs workflow, where a licence and a
|
|
132
|
+
real launch are affordable. A site is never hostage to a capture: on a
|
|
133
|
+
checkout with no `docs/generated`, the dev-kit's `docs/guide-stubs.ts stub`
|
|
134
|
+
writes a placeholder page for every guide the site's symlinks name, so
|
|
135
|
+
`docs:site` builds, and `docs/guide-stubs.ts check` **refuses to publish** a
|
|
136
|
+
placeholder, in every publish workflow the dev-kit ships. The placeholder
|
|
137
|
+
sentence is the marker the gate matches on; do not write it by hand.
|
|
97
138
|
|
|
98
139
|
## The toolchain
|
|
99
140
|
|
|
@@ -103,6 +144,12 @@ they do not copy it:
|
|
|
103
144
|
- `@norskvideo/ctl-dev-kit/doc-guide` — `DocGuide` (a guide narrates itself into
|
|
104
145
|
`docs/generated/<slug>/`), `screenshotFor`, `CaptureTreatment`. Writes under
|
|
105
146
|
`NORSK_DOCS_ROOT` (the repo-level `docs/generated`); the config factory sets it.
|
|
147
|
+
Step text is also the video narration, so in a `DOCS_GENERATE` run `step()`
|
|
148
|
+
(heading plus description) and `text()` fail the guide over 50 spoken words,
|
|
149
|
+
on a raw URL (say "the address"), or on a slash between words (write "or" or
|
|
150
|
+
"and"); the failure names the guide, the step and the count. `captionWords`
|
|
151
|
+
on `guidesConfig` or on the `DocGuide` options tightens the budget. A plain
|
|
152
|
+
run is unchanged.
|
|
106
153
|
- `@norskvideo/ctl-dev-kit/doc-guide/guides-config` — `guidesConfig({ port,
|
|
107
154
|
docsRoot, … })`, the Playwright config for the fixture/frontend tiers, carrying
|
|
108
155
|
the CI defaults (bind+probe `127.0.0.1`, 120s webServer timeout, piped output).
|
package/conventions/personas.md
CHANGED
|
@@ -131,3 +131,28 @@ Reconcile them like this:
|
|
|
131
131
|
Map each product's named audiences (from its `PRODUCT.md`) onto the four
|
|
132
132
|
personas above; don't invent a parallel scheme. Keep the per-product mapping in
|
|
133
133
|
the product's `docs/personas.md`.
|
|
134
|
+
|
|
135
|
+
## The journey table
|
|
136
|
+
|
|
137
|
+
A persona is who you write for; a journey is where they go. Each product's
|
|
138
|
+
`docs/personas.md` carries one table, in this shape, and `docs/journeys.ts` in
|
|
139
|
+
this package checks it from `bun run docs:check`: every persona has one row,
|
|
140
|
+
every page it names exists, no route is longer than five pages, and every route
|
|
141
|
+
ends in a hand-off.
|
|
142
|
+
|
|
143
|
+
| Persona | Starts at | Then, in order | Hands off to |
|
|
144
|
+
| ------------- | ----------------------- | ----------------------------------------------------- | ---------------------------------------------------- |
|
|
145
|
+
| Evaluator | `/docs/` | `/docs/quickstart/`, `/docs/guides/gallery/` | Guides (build with it) or ctl (run it for real) |
|
|
146
|
+
| Builder | `/docs/guides/first-…/` | the task pages, `/docs/guides/troubleshooting/` | Reference, for the settings they just used |
|
|
147
|
+
| Integrator | `/docs/reference/` | config, CLI subtree, API, recipes | norsk-ctl's CLI and API reference |
|
|
148
|
+
| Administrator | `/docs/how-to/` | what this product adds to the host, known limitations | norsk-ctl's manual, for everything about the machine |
|
|
149
|
+
|
|
150
|
+
The Integrator's route may start at a recorded API walkthrough (the `-api`
|
|
151
|
+
variant of the Builder's first task) rather than at Reference; the Manager's
|
|
152
|
+
"API tokens, HTTP API" is the pattern. Reference stays where it ends.
|
|
153
|
+
|
|
154
|
+
Site paths are `/docs/...` (the embedded base; the public build strips it) and
|
|
155
|
+
resolve to `website/src/content/docs`; a bare `name.md` resolves under `docs/`.
|
|
156
|
+
The hand-off is where "see norsk-ctl for details" lives, once per route, so no
|
|
157
|
+
page needs the caveat sprinkled through it. A repo with no site and no
|
|
158
|
+
`docs/quickstart.md` (a backend-only turnkey) is not asked for the table.
|