Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 84 additions & 0 deletions .github/workflows/site-generated-docs-check.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
name: site-generated-docs-check

# PR gate for apps/site's generated-docs pipeline: verify's own `tsc` pass
# (types:check) and the root generated-artifacts drift check
# (generated-artifacts:check), both of which currently run ONLY post-merge
# (generated-artifacts:check in semantic-release.yml, gated on a version
# actually publishing) or not at all in CI (types:check/verify are never
# invoked by any workflow). That gap let two separate defects reach main
# undetected: a stale tsconfig path mapping that broke `pnpm --dir apps/site
# run verify` on a fresh checkout, and hand-edited generated output
# (apps/site/scripts/sync-spec-docs.mjs) that drifted from the TypeScript
# source it's compiled from. Neither is caught by spec-check.yml's "root/site
# spec drift" job, which only diffs generated markdown against the root
# specs.
#
# Deliberately NOT the full `verify` script: `verify` also runs the test
# suite and a production `next build`, which is real coverage but not what
# closes this specific gap and would roughly double this job's wall time for
# no additional defect coverage here. `types:check` alone reproduces the
# tsconfig defect (TS2307/TS2305-class failures); generated-artifacts:check
# alone reproduces the compiled-output drift. Both run in well under the
# `pnpm install` time this job already pays.

on:
push:
branches: [main]
paths:
- ".github/workflows/site-generated-docs-check.yml"
- "apps/site/**"
- "docs/design-system/ink-carbon/project/**"
- "scripts/check-generated-artifacts.ts"
- "package.json"
- "pnpm-lock.yaml"
pull_request:
paths:
- ".github/workflows/site-generated-docs-check.yml"
- "apps/site/**"
- "docs/design-system/ink-carbon/project/**"
- "scripts/check-generated-artifacts.ts"
- "package.json"
- "pnpm-lock.yaml"
workflow_dispatch: {}

concurrency:
group: site-generated-docs-check-${{ github.ref }}
cancel-in-progress: true

jobs:
check:
name: site types:check + generated-artifacts:check
runs-on: ubuntu-latest
timeout-minutes: 10

steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1

- name: Install pnpm
uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0

- name: Install Node.js
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: "24"
cache: "pnpm"

- name: Install dependencies
env:
PATCHRIGHT_SKIP_BROWSER_DOWNLOAD: "1"
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: "1"
run: pnpm install --frozen-lockfile

# Runs with the same PDPP_SITE_SOURCE_DIR/PDPP_SITE_TSCONFIG isolation
# as `pnpm --dir apps/site run verify`, so this job exercises the exact
# tsconfig this gate exists to protect (tsconfig.verify.json), not the
# default tsconfig.json a plain `types:check` would use.
- name: Site types:check (verify mode)
env:
PDPP_SITE_SOURCE_DIR: .source-verify
PDPP_SITE_TSCONFIG: tsconfig.verify.json
run: pnpm --dir apps/site run types:check

- name: Generated artifacts are not drifted
run: pnpm generated-artifacts:check
26 changes: 26 additions & 0 deletions apps/site/biome.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,32 @@
}
}
},
{
// Upstream Biome resolver false-positive waiver — NOT a doctrine change.
//
// scripts/style-differ.mjs does `import { chromium } from "playwright"`.
// playwright's index.d.ts is `export * from 'playwright-core'`, and
// playwright-core is an indirect dependency here (pulled in
// transitively, not listed in this package's own package.json), so
// under pnpm's isolated node_modules it has no symlink inside
// apps/site/node_modules for Biome's resolver to follow. The import
// resolves cleanly under TypeScript (moduleResolution: "bundler") and
// at runtime (verified: `node -e "require('playwright').chromium"`
// and the ESM equivalent both return the launcher). Same upstream gap
// as the src/** waiver above (biomejs/biome#9143, #6464); this file
// sits outside src/** so that override does not reach it.
//
// Exit condition: same as the src/** override above — drop once
// Biome's resolver follows re-exports through transitive dependencies.
"includes": ["scripts/style-differ.mjs"],
"linter": {
"rules": {
"correctness": {
"noUnresolvedImports": "off"
}
}
}
},
{
// Architectural shim waiver — NOT a barrel-file pattern.
//
Expand Down
13 changes: 12 additions & 1 deletion apps/site/next.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { collectAllowedDevOrigins } from "./scripts/dev-origins.ts";
import { resolveSiteRuntime } from "./scripts/site-runtime.ts";

const __dirname = fileURLToPath(new URL(".", import.meta.url));
const { buildWorkers, distDir, isProduction, sourceDir } = resolveSiteRuntime();
const { buildWorkers, distDir, isProduction, sourceDir, tsconfigPath } = resolveSiteRuntime();
const withMDX = createMDX({ outDir: sourceDir });
const allowedDevOrigins = isProduction ? [] : collectAllowedDevOrigins();

Expand All @@ -27,6 +27,17 @@ const nextConfig = {
},
output: "standalone",
serverExternalPackages: ["fumadocs-mdx", "satteri"],
// `next build`'s own internal type-check pass reads tsconfig.json's
// "paths" directly — it does not go through the webpack/turbopack
// aliases below, which the bundler itself uses instead. Point it at the
// same per-mode tsconfig apps/site/package.json's "types:check" script
// selects via PDPP_SITE_TSCONFIG (see scripts/site-runtime.ts), so a
// `verify` build's typecheck resolves "@generated-docs/*" against
// ".source-verify" like `verify`'s own separate `tsc -p` pass does, not
// against the base config's ".source".
typescript: {
tsconfigPath,
},
turbopack: {
resolveAlias: {
"@generated-docs/*": `./${sourceDir}/*`,
Expand Down
5 changes: 3 additions & 2 deletions apps/site/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
"//dev-node-options": "NODE_OPTIONS adds Node.js diagnostic reports on fatal V8 errors and uncaught exceptions. Reports land in tmp/node-reports/ at the repo root (gitignored), excluding env/network sections so reports do not persist secrets.",
"//dev-port": "Public site defaults to :3001 (design.md §5). PDPP_SITE_PORT overrides; PDPP_WEB_PORT kept as a fallback for existing contributor env files.",
"//sync-specs": "Generates content/docs/spec-*.md from the normative root spec-*.md (single source). The generated files are gitignored; predev/prebuild regenerate them. See scripts/sync-spec-docs.mjs.",
"//types-check-tsconfig": "PDPP_SITE_TSCONFIG selects which tsconfig types:check's tsc pass uses; default tsconfig.json resolves @generated-docs/* against .source only. A mode whose PDPP_SITE_SOURCE_DIR points elsewhere (currently just verify, via .source-verify) must also set PDPP_SITE_TSCONFIG to a config whose paths match that directory, or tsc can silently resolve a stale .source left by an unrelated dev/build run instead of the output this run just generated. See tsconfig.verify.json.",
"prebuild": "node scripts/sync-spec-docs.mjs",
"build": "next build --webpack",
"check": "ultracite check && pnpm --dir ../.. generated-artifacts:check",
Expand All @@ -19,8 +20,8 @@
"start": "next start",
"sync:specs": "node scripts/sync-spec-docs.mjs",
"test": "node --import tsx ../../scripts/test-scratch/run-command.ts -- node ../../scripts/run-node-tests.mjs --test --import tsx --import ../../packages/operator-ui/css-stub-register.ts \"scripts/*.test.ts\" \"src/**/*.test.ts\"",
"types:check": "node scripts/sync-spec-docs.mjs && fumadocs-mdx source.config.ts \"${PDPP_SITE_SOURCE_DIR:-.source}\" && next typegen && tsc --noEmit",
"verify": "sh -c 'export PDPP_SITE_NEXT_DIST_DIR=.next-verify PDPP_SITE_SOURCE_DIR=.source-verify; pnpm types:check && pnpm test && pnpm check && pnpm build'"
"types:check": "node scripts/sync-spec-docs.mjs && fumadocs-mdx source.config.ts \"${PDPP_SITE_SOURCE_DIR:-.source}\" && next typegen && tsc -p \"${PDPP_SITE_TSCONFIG:-tsconfig.json}\" --noEmit",
"verify": "sh -c 'export PDPP_SITE_NEXT_DIST_DIR=.next-verify PDPP_SITE_SOURCE_DIR=.source-verify PDPP_SITE_TSCONFIG=tsconfig.verify.json; pnpm types:check && pnpm test && pnpm check && pnpm build'"
},
"dependencies": {
"@base-ui/react": "^1.8.0",
Expand Down
32 changes: 32 additions & 0 deletions apps/site/scripts/site-runtime.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,15 @@ const DEV_DIST_DIR = /PDPP_SITE_NEXT_DIST_DIR=\.next-dev/;
const DEV_SOURCE_DIR = /PDPP_SITE_SOURCE_DIR=\.source-dev/;
const VERIFY_DIST_DIR = /PDPP_SITE_NEXT_DIST_DIR=\.next-verify/;
const VERIFY_SOURCE_DIR = /PDPP_SITE_SOURCE_DIR=\.source-verify/;
const VERIFY_TSCONFIG = /PDPP_SITE_TSCONFIG=tsconfig\.verify\.json/;

test("development omits the production worker limit", () => {
assert.deepEqual(resolveSiteRuntime({ NODE_ENV: "development", PDPP_SITE_NEXT_DIST_DIR: ".next-dev" }), {
buildWorkers: undefined,
distDir: ".next-dev",
isProduction: false,
sourceDir: ".source",
tsconfigPath: "tsconfig.json",
});
});

Expand All @@ -26,6 +28,20 @@ test("production defaults to one worker and accepts an explicit override", () =>
assert.equal(resolveSiteRuntime({ NODE_ENV: "production", PDPP_WEB_BUILD_WORKERS: "8" }).buildWorkers, 8);
});

// The default sourceDir/tsconfigPath pairing must stay consistent: a mode
// that does not set PDPP_SITE_SOURCE_DIR resolves "@generated-docs/*" via
// tsconfig.json's own ".source" paths entry, so a plain resolveSiteRuntime()
// call must also default to that same tsconfig, not verify's.
test("default source directory pairs with the base tsconfig, not a per-mode override", () => {
const runtime = resolveSiteRuntime({});
assert.equal(runtime.sourceDir, ".source");
assert.equal(runtime.tsconfigPath, "tsconfig.json");
});

test("PDPP_SITE_TSCONFIG overrides the default tsconfig path", () => {
assert.equal(resolveSiteRuntime({ PDPP_SITE_TSCONFIG: "tsconfig.verify.json" }).tsconfigPath, "tsconfig.verify.json");
});

test("supported development and verification commands select distinct output directories", async () => {
const packageJson = JSON.parse(await readFile(new URL("../package.json", import.meta.url), "utf8")) as {
scripts: Record<string, string>;
Expand All @@ -40,3 +56,19 @@ test("supported development and verification commands select distinct output dir
assert.match(verify, VERIFY_SOURCE_DIR);
assert.notEqual(dev, verify);
});

// verify's own tsc pass (types:check) and next build's internal type-check
// must resolve "@generated-docs/*" against .source-verify, not the base
// tsconfig's ".source" — see tsconfig.verify.json and next.config.mjs's
// typescript.tsconfigPath wiring. A stale .source left by an unrelated
// dev/build run must never be able to mask a type error in verify's own
// freshly generated output.
test("verify selects its own tsconfig, distinct from the base config", async () => {
const packageJson = JSON.parse(await readFile(new URL("../package.json", import.meta.url), "utf8")) as {
scripts: Record<string, string>;
};

const { verify } = packageJson.scripts;
assert.ok(verify);
assert.match(verify, VERIFY_TSCONFIG);
});
7 changes: 7 additions & 0 deletions apps/site/scripts/site-runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ interface SiteRuntimeEnvironment {
NODE_ENV?: string;
PDPP_SITE_NEXT_DIST_DIR?: string;
PDPP_SITE_SOURCE_DIR?: string;
PDPP_SITE_TSCONFIG?: string;
PDPP_WEB_BUILD_WORKERS?: string;
}

Expand All @@ -27,5 +28,11 @@ export function resolveSiteRuntime(environment: SiteRuntimeEnvironment = process
distDir: environment.PDPP_SITE_NEXT_DIST_DIR ?? ".next",
isProduction,
sourceDir: environment.PDPP_SITE_SOURCE_DIR ?? ".source",
// Same PDPP_SITE_TSCONFIG that package.json's "types:check" script
// passes to `tsc -p`, so `next build`'s own internal type-check pass
// (which reads tsconfig.json directly, not through the webpack/
// turbopack "@generated-docs" aliases) resolves against the same
// directory as the rest of the current run. See next.config.mjs.
tsconfigPath: environment.PDPP_SITE_TSCONFIG ?? "tsconfig.json",
};
}
16 changes: 15 additions & 1 deletion apps/site/scripts/sync-spec-docs.mts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
// Runs from `predev` and `prebuild`. Vercel builds from apps/site with the
// monorepo root available, so the relative path to the repo root resolves.

import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";

Expand Down Expand Up @@ -155,6 +155,7 @@ function checkStatusDateDrift(header, root, spec) {
}

let generated = 0;
let staleRemoved = 0;
for (const spec of SPECS) {
const rootPath = path.join(repoRoot, `${spec}.md`);
const headerPath = path.join(headerDir, `${spec}.header.md`);
Expand All @@ -174,6 +175,16 @@ for (const spec of SPECS) {
// header (frontmatter + Callout) + blank line + normative body.
const out = `${header}\n\n${root.body.replace(/\s*$/, "")}\n`;
writeFileSync(outPath, out);
// An earlier revision of this script wrote `.md` here. A leftover from one
// of those runs is not overwritten by the `.mdx` write above, and fumadocs
// globs both, so the two resolve to the same slug and fail the build with
// "Duplicated slugs". Clearing it makes a stale checkout self-heal instead
// of failing a build that looks unrelated to anything the author changed.
const stalePath = path.join(contentDir, `${spec}.md`);
if (existsSync(stalePath)) {
rmSync(stalePath);
staleRemoved += 1;
}
generated += 1;
}

Expand Down Expand Up @@ -455,6 +466,9 @@ export const PRINCIPLES_LIST = ${JSON.stringify(principles, null, 2)} as const;
);

console.log(`[sync-spec-docs] generated ${generated} spec page(s) from root spec-*.md`);
if (staleRemoved > 0) {
console.log(`[sync-spec-docs] removed ${staleRemoved} stale .md page(s) left by an earlier revision of this script`);
}
console.log(
`[sync-spec-docs] wrote src/generated/spec-front-matter.ts (${versionMatch[1]}, ${status}, ${date}, ${editors.length} editor(s); ` +
`governance: ${governanceFrontMatter.status}; principles: v${principlesFrontMatter.version})`
Expand Down
4 changes: 4 additions & 0 deletions apps/site/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@
"name": "next"
}
],
// Resolves generated docs from ".source", the default PDPP_SITE_SOURCE_DIR.
// tsc cannot read env vars in "paths", so a mode that writes elsewhere
// needs its own tsconfig; see tsconfig.verify.json. Do not list several
// directories here: tsc picks the first that exists, which can be stale.
"paths": {
"@/*": ["./src/*"],
"@generated-docs/*": ["./.source/*"],
Expand Down
14 changes: 14 additions & 0 deletions apps/site/tsconfig.verify.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
// Used by `verify` (PDPP_SITE_SOURCE_DIR=.source-verify, selected through
// PDPP_SITE_TSCONFIG). Resolves generated docs only from the output that
// verify just wrote, so a stale ".source" from a dev or build run cannot
// hide a type error.
"extends": "./tsconfig.json",
"compilerOptions": {
"paths": {
"@/*": ["./src/*"],
"@generated-docs/*": ["./.source-verify/*"],
"collections/*": ["./.source-verify/*"]
}
}
}
Loading