From c855a1c836734fb86b7a2fa953b8747442837943 Mon Sep 17 00:00:00 2001 From: Oto Macenauer Date: Fri, 4 Sep 2026 12:45:21 +0200 Subject: [PATCH] =?UTF-8?q?docs(contract):=20define=20the=20v1=20artifact?= =?UTF-8?q?=20=E2=80=94=20kb-docs.tar.gz=20and=20kb-docs.json?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The artifact contract had grown two shapes and a misleading name. Packaged sites shipped dist.tar.gz containing dist/ plus a root marketplace.json describing one app; single-page bundles shipped dist.tar.gz containing bundle.json plus one directory per doc, describing many. dist.tar.gz is also indistinguishable from a repo's own distribution package, which a release usually carries alongside. This defines one shape for both. kb-docs.tar.gz holds kb-docs.json at its root plus one directory per app. A documentation site is a manifest listing one app; a set of markdown docs is a manifest listing several. Nothing downstream has to tell them apart. New: contract/ARTIFACT.md normative: layout, manifest, archive rules, versioning, size budget, registration contract/kb-docs.schema.json draft-07 schema, $id at its raw GitHub URL so the actions and this build validate the same document tests/contract.spec.js 30 assertions pinning what the schema accepts and rejects Removed contract/schema.json, which described the old single-app manifest. Two things the schema is deliberate about. kbVersion is a string, so the contract can reach "1.1" or "2" without changing type, and unknown fields are ignored rather than rejected so an additive change never breaks an older knowledge base. Conversely entryPoint and pages[].path are pattern-constrained against traversal and absolute paths: the archive is unpacked into a directory this deployment then serves, so a manifest must not be able to point outside the app's own tree. The size budget is new and was previously unstated: 20 MB target, warned above that, refused above 100 MB, which is also GitHub's per-asset limit. Every registered artifact is downloaded on every deployment build, so it is a shared cost rather than a private one. validate-doc-app.yml is now a loud deprecation. It validated marketplace.json against a schema this commit deletes, and it assumed `npm run build --headless`, which the Python example repo cannot run. Rather than half-migrating a workflow that #76 removes, it fails with the replacement in the message and the step summary — a green check against a contract that no longer exists is worse than no check. Contract-only. No build code changes, so the build still reads the old layout until #75 lands. The docs describing that layout move with it. Part of #73. Closes #74. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01PYtmxSeSXFg4wEXRcKCmAi --- .github/workflows/validate-doc-app.yml | 220 ++++-------------- CLAUDE.md | 13 +- README.md | 68 +++--- .../publish-single-page-docs/src/inputs.js | 4 +- contract/ARTIFACT.md | 181 ++++++++++++++ contract/HEADLESS_RULES.md | 122 ++++++---- contract/SINGLE_PAGE.md | 55 +++-- contract/kb-docs.schema.json | 108 +++++++++ contract/schema.json | 87 ------- src/utils/single-page.js | 2 +- tests/contract.spec.js | 123 ++++++++++ 11 files changed, 604 insertions(+), 379 deletions(-) create mode 100644 contract/ARTIFACT.md create mode 100644 contract/kb-docs.schema.json delete mode 100644 contract/schema.json create mode 100644 tests/contract.spec.js diff --git a/.github/workflows/validate-doc-app.yml b/.github/workflows/validate-doc-app.yml index ecca0a1..906ef8a 100644 --- a/.github/workflows/validate-doc-app.yml +++ b/.github/workflows/validate-doc-app.yml @@ -1,190 +1,70 @@ -# Reusable workflow — validate a doc app against the knowledge-base contract. +# Reusable workflow — DEPRECATED. # -# Called by doc-app repos in their own CI to verify the app meets the knowledge base -# contract before raising a PR or publishing a release. +# This workflow validated a doc app against the pre-v1 contract: a +# `marketplace.json` in the repo root, a `dist/` produced by +# `npm run build -- --headless`, and a schema fetched from +# `contract/schema.json`. # -# Usage in a doc repo (.github/workflows/validate.yml): +# None of those exist any more. The v1 contract is one release asset, +# `kb-docs.tar.gz`, carrying a `kb-docs.json` manifest — see +# `contract/ARTIFACT.md`. Validation is no longer a separate workflow a doc repo +# has to remember to call: the publishing action validates the manifest and the +# built HTML before it packs anything, so a repo that publishes is a repo that +# was checked. # -# jobs: -# validate: -# uses: AbsaOSS/knowledge-base/.github/workflows/validate-doc-app.yml@master +# Replace a call to this workflow with the publishing step itself: # -# The calling repo must have: -# - marketplace.json in the repo root -# - `npm run build -- --headless` producing dist/ +# - uses: actions/checkout@v4 +# - run: +# - uses: AbsaOSS/knowledge-base/actions/publish-docs@v1 +# with: +# manifest: kb-docs.json +# dist: dist # -name: Validate Doc App +# It fails loudly rather than silently passing, because a green check against a +# contract that no longer exists is worse than no check. +# +# Removed once the migration is done — see issue #76. +name: Validate Doc App (deprecated) on: workflow_call: inputs: node-version: - description: 'Node.js version to use' + description: 'Unused. Kept so an existing caller fails on the message below rather than on an unexpected-input error.' type: string + required: false default: '24' permissions: contents: read jobs: - validate: - name: Validate knowledge base contract + deprecated: + name: Deprecated — use actions/publish-docs runs-on: ubuntu-latest steps: - - name: Checkout - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - - - name: Setup Node.js - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 - with: - node-version: ${{ inputs.node-version }} - cache: npm - - - name: Install dependencies - run: npm ci - - # ── Validate marketplace.json ────────────────────────────────────────── - - name: Validate marketplace.json exists + - name: Explain the replacement run: | - if [ ! -f marketplace.json ]; then - echo "::error file=marketplace.json::marketplace.json not found in repo root." - echo "See https://github.com/AbsaOSS/knowledge-base/blob/master/contract/HEADLESS_RULES.md" - exit 1 - fi - echo "✓ marketplace.json found" - - - name: Validate marketplace.json schema - run: | - # ajv may not be a dependency of the doc repo — install it transiently. - npm install --no-save ajv@^8 - node --input-type=module <<'EOF' - import { readFileSync } from 'node:fs'; - import Ajv from 'ajv'; - - const manifest = JSON.parse(readFileSync('marketplace.json', 'utf8')); - const schemaUrl = - 'https://raw.githubusercontent.com/AbsaOSS/knowledge-base/master/contract/schema.json'; - - const res = await fetch(schemaUrl); - if (!res.ok) { - console.warn('⚠ Could not fetch remote schema — skipping schema validation'); - process.exit(0); - } - const schema = await res.json(); - const validate = new Ajv({ allErrors: true }).compile(schema); - - if (!validate(manifest)) { - console.error('✗ marketplace.json validation errors:'); - for (const err of validate.errors) { - console.error(` ${err.instancePath || '/'}: ${err.message}`); - } - process.exit(1); - } - console.log('✓ marketplace.json is valid'); - EOF - - # ── Build headless ───────────────────────────────────────────────────── - - name: Build headless - run: npm run build -- --headless - - # ── Check dist/ output ───────────────────────────────────────────────── - - name: Check dist/ exists - run: | - if [ ! -d dist ]; then - echo "::error::dist/ directory not found after build. Check your build script." - exit 1 - fi - echo "✓ dist/ exists" - - - name: Check entryPoint exists - run: | - ENTRY=$(node -e "const m=require('./marketplace.json'); console.log(m.entryPoint || 'index.html')") - if [ ! -f "dist/${ENTRY}" ]; then - echo "::error::dist/${ENTRY} (entryPoint) not found after headless build." - exit 1 - fi - echo "✓ dist/${ENTRY} found" - - # ── Validate headless HTML structure ─────────────────────────────────── - - name: Check data-kb-headless attribute - run: | - ENTRY=$(node -e "const m=require('./marketplace.json'); console.log(m.entryPoint || 'index.html')") - if ! grep -q 'data-kb-headless="true"' "dist/${ENTRY}"; then - echo "::error file=dist/${ENTRY}::Missing data-kb-headless=\"true\" on element." - echo "Add data-kb-headless=\"true\" to the tag when building with --headless." - exit 1 - fi - echo "✓ data-kb-headless=\"true\" found" - - - name: Check no fixed site header in headless output - run: | - VIOLATIONS=$(grep -rl 'class="[^"]*fixed[^"]*top-0[^"]*inset-x-0' dist/ || true) - if [ -n "$VIOLATIONS" ]; then - echo "::warning::Found potential fixed top-bar elements in headless output:" - echo "$VIOLATIONS" - echo "Ensure the site header is omitted when building with --headless." - else - echo "✓ No fixed site headers detected in headless output" - fi - - - name: Check all asset paths are relative - run: | - node --input-type=module <<'EOF' - import { readdirSync, statSync, readFileSync } from 'node:fs'; - import { join, extname } from 'node:path'; - - function collectHtml(dir) { - const files = []; - for (const e of readdirSync(dir)) { - const p = join(dir, e); - if (statSync(p).isDirectory()) files.push(...collectHtml(p)); - else if (extname(e) === '.html') files.push(p); - } - return files; - } - - let violations = 0; - for (const file of collectHtml('dist')) { - const html = readFileSync(file, 'utf8'); - const matches = [...html.matchAll(/(?:href|src|action)="(\/[^/"'][^"]*?)"/g)]; - for (const [, path] of matches) { - if (!path.startsWith('//') && !path.startsWith('/favicon')) { - console.error(`::warning file=${file}::Absolute path found: ${path}`); - violations++; - } - } - } - if (violations > 0) { - console.error( - `\n${violations} absolute path(s) found. The knowledge-base mounts each app under ` + - `/knowledge-base/{slug}/ and rewrites relative paths — absolute paths will 404.`, - ); - process.exit(1); - } - console.log('✓ All asset paths are relative'); - EOF - - # ── Design tokens (soft check) ───────────────────────────────────────── - - name: Check knowledge-base design tokens in CSS - run: | - CSS_FILES=$(find dist/ -name "*.css" | head -5) - if [ -z "$CSS_FILES" ]; then - echo "::warning::No CSS files found in dist/" - else - FOUND=false - for f in $CSS_FILES; do - if grep -q -- '--color-kb-500\|#af144b' "$f"; then - FOUND=true - break - fi - done - if [ "$FOUND" = "false" ]; then - echo "::warning::knowledge-base brand token (--color-kb-500 / #af144b) not found in dist CSS." - echo "See contract/STYLE_GUIDE.md for the design-token contract." - else - echo "✓ knowledge-base design tokens detected in CSS" - fi - fi - - - name: Validation summary - run: echo "✓ All contract checks passed for $(node -e "console.log(require('./marketplace.json').slug)")" + { + echo "### This workflow is deprecated" + echo + echo "It validated the pre-v1 contract (\`marketplace.json\` + \`dist/\`), which no longer exists." + echo + echo "The v1 contract is a single \`kb-docs.tar.gz\` release asset carrying a \`kb-docs.json\` manifest." + echo "Validation now happens inside the publishing action, so replace your call to this workflow with:" + echo + echo '```yaml' + echo "- uses: actions/checkout@v4" + echo "- run: " + echo "- uses: AbsaOSS/knowledge-base/actions/publish-docs@v1" + echo " with:" + echo " manifest: kb-docs.json" + echo " dist: dist" + echo '```' + echo + echo "See https://github.com/AbsaOSS/knowledge-base/blob/master/contract/ARTIFACT.md" + } >> "$GITHUB_STEP_SUMMARY" + + echo "::error::validate-doc-app.yml is deprecated. The pre-v1 contract it checked (marketplace.json + dist/) no longer exists. Use AbsaOSS/knowledge-base/actions/publish-docs@v1 instead — see contract/ARTIFACT.md." + exit 1 diff --git a/CLAUDE.md b/CLAUDE.md index 5bdcc77..a0ec676 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -64,7 +64,7 @@ Orchestrator: `scripts/build-vite.js`. Flags: `--local`, `--headless`. ### Core Data Flow -`apps.json` → `scripts/fetch-apps.js` downloads `dist.tar.gz` per app → `apps/{slug}/` → Astro's `src/pages/[...path].astro` catchall uses `getStaticPaths()` from `src/utils/apps.js` to enumerate every HTML file → `src/utils/transform.js` rewrites URLs and splits the document → `Base.astro` re-hosts the parts → static output in `dist/`. +`apps.json` → `scripts/fetch-apps.js` downloads `kb-docs.tar.gz` per app → `apps/{slug}/` → Astro's `src/pages/[...path].astro` catchall uses `getStaticPaths()` from `src/utils/apps.js` to enumerate every HTML file → `src/utils/transform.js` rewrites URLs and splits the document → `Base.astro` re-hosts the parts → static output in `dist/`. ### Key Source Files @@ -88,9 +88,9 @@ Orchestrator: `scripts/build-vite.js`. Flags: `--local`, `--headless`. An `apps.json` entry is one of: -- **default (packaged)** — a repo publishes a headless static site as `dist.tar.gz` plus `marketplace.json`. Every HTML file becomes a route. +- **default (packaged)** — a repo publishes a headless static site as `kb-docs.tar.gz` carrying a `kb-docs.json` manifest. Every HTML file becomes a route unless the manifest lists `pages`. - **`type: "iframe"`** — no artifact; a single route renders a full-viewport `