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
4 changes: 3 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ Orchestrator: `scripts/build-vite.js`. Flags: `--local`, `--headless`.
- `scripts/build-vite.js` — Build orchestrator (4 steps: prepare, hoist, copy assets, astro build)
- `scripts/fetch-apps.js` — GitHub Release artifact downloader. Only *obtains* an artifact; installing it is one shared path in `build-vite.js`
- `scripts/artifacts.js` — Safe tarball extraction + tree copy, shared by both fetch paths. Validates archive members (no traversal, no absolute paths, no symlinks) before anything is written, and replaces the old `cp -r`/`tar` shell-outs so the build runs on Windows
- `scripts/check-artifact.js` — Runs the publish action's contract checker (`actions/lib/check.js`) on every installed artifact, before hoisting; logs findings grouped by rule, and fails a strict build on an error. `check.js` resolves its parsers from `actions/node_modules` or the root, which pins the same versions
- `scripts/hoist-inline-scripts.js` — Moves inline `<script>` bodies in sub-app HTML into files before the Astro build, so the deployment can serve `script-src 'self'`. Needed because bundles published before the action stopped emitting an inline mermaid bootstrap still contain one. A sub-app's dark-mode bootstrap is deleted here rather than hoisted — light only, and hoisting would put it beyond the reach of `transform.js`
- `actions/publish-single-page-docs/` — Reusable GitHub Action that turns a repo's markdown into a single-page bundle
- `skills/kb-docs-add/` — Agent skill (Claude Code, GitHub Copilot, `npx skills add`) that walks an agent through onboarding a docs repo (classify, write only the contract-required files, verify, troubleshoot) and through auditing one already onboarded (`references/audit.md`: run `actions/lib/check-cli.js` from a scratch clone, fix findings by rule ID at the source, report what the checker cannot see). Guidance only — no scripts; `examples/` are the contract's own code blocks and `tests/skill.spec.js` fails if they drift. Eval fixtures live in `tests/fixtures/kb-docs-add/`
Expand Down Expand Up @@ -188,6 +189,7 @@ Every embedded test runs twice, as Playwright projects `chromium` (:4201) and
route; a narrow route tears the fragment down) — and what "smooth" means: no host reload,
the reframed iframe survives, one `astro:after-swap` and one host-document view transition
per hop, one `wf-html`, no duplicated stylesheet across a → b → c → a.
- `artifact-checks.spec.js` — the build-side checker: root and `actions/` pin the same parser versions, findings are grouped by rule, a strict build refuses an error finding.
- `artifact-safety.spec.js` — tarball extraction guards (traversal, absolute paths, symlinks).
- `nginx-config.spec.js` — static assertions on `nginx.conf`/`nginx.headers.conf`, including
that the CSP the Express mirror serves is byte-identical to nginx's.
Expand Down Expand Up @@ -219,7 +221,7 @@ in the committed `apps.json` without breaking CI, which only has this repo.

- `GITHUB_TOKEN` — GitHub API auth for fetching Release artifacts
- `KB_REGISTRY` — registry file to build from. Relative to the project root, or absolute (a deployment repo's registry is checked out beside this one). Default `apps.json`. Read through `REGISTRY_FILE` in `src/utils/config.js`, never inline.
- `KB_STRICT` — `true` rejects `prebuilt`/`localPath`/`optional` entries, an empty registry, and any entry that yields no apps. Production builds only; this repo's own registry is a fixture and fails it by design.
- `KB_STRICT` — `true` rejects `prebuilt`/`localPath`/`optional` entries, an empty registry, any entry that yields no apps, and any artifact with an `error` finding from the contract checker (`scripts/check-artifact.js`; every build logs the findings, grouped by rule, strict or not). Production builds only; this repo's own registry is a fixture and fails it by design.
- `KB_HEADLESS` — `true` produces web-fragment output; **anything else, including unset, means standalone**. `scripts/build-vite.js` always exports an explicit value, so the default only applies when `astro build`/`astro dev` runs directly. Read it through `isHeadlessBuild()`, never inline. A per-app `"headless"` in `apps.json` overrides it in either direction.
- `AWS_REGION`, `ECR_REPOSITORY`, `ECS_CLUSTER`, `ECS_SERVICE` — deployment config
- `KB_EXAMPLE_ARTIFACT` — overrides the packaged artifact `scripts/setup-test-apps.mjs` registers
Expand Down
5 changes: 5 additions & 0 deletions contract/DEPLOYMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,11 @@ A strict build enforces what a production registry may contain:
| `optional` | Permission to ship without an app nobody noticed was missing |
| an entry that produces no apps | A registered artifact that publishes nothing is a broken deploy |
| an empty registry | A knowledge base with no docs is not a successful build |
| an artifact with an `error` finding ([`RULES.md`](./RULES.md)) | The publish action refuses those; an artifact that has one reached the release some other way (hand-packed, or an action older than the rule) |

Every build, strict or not, runs the contract checks on each installed artifact and logs
the findings grouped by rule, so the registry owner can see which docs repo to chase.
Warnings never fail a build.

`iframe` entries are still allowed — they are an explicit, documented stopgap
(issue #10) and carry `"temporary": true`.
Expand Down
4 changes: 4 additions & 0 deletions contract/RULES.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@ node /tmp/knowledge-base/actions/lib/check-cli.js --manifest kb-docs.json --dist
`--json` prints the findings as a JSON array (`id`, `severity`, `where`, `message`);
`--strict` exits non-zero on warnings as well as errors.

The knowledge base build runs the same checks again on every artifact it installs and
logs the findings. A strict (production) build refuses an artifact with an error finding
— see [`DEPLOYMENT.md`](./DEPLOYMENT.md).

## Index

| ID | Severity | Rule |
Expand Down
58 changes: 41 additions & 17 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,9 @@
"dependencies": {
"@fontsource-variable/inter": "^5.3.0",
"ajv": "^8.17.1",
"parse5": "^8.0.1"
"htmlparser2": "8.0.2",
"parse5": "^8.0.1",
"postcss": "8.5.28"
},
"devDependencies": {
"@astrojs/check": "^0.9.10",
Expand Down
40 changes: 9 additions & 31 deletions scripts/build-vite.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import { collectHtmlFiles } from '../src/utils/apps.js';
import { PATH_PREFIX, REGISTRY_FILE } from '../src/utils/config.js';
import { layerSubAppCss } from '../src/utils/css-layers.js';
import { rewriteCssUrls } from '../src/utils/transform.js';
import { checkApp, reportFindings } from './check-artifact.js';
import {
ARTIFACT_NAME, MANIFEST, expandManifest, findManifestRoot, isIframe,
readManifest, resolveRegistry, sourceKey, stagingName, toRegistryEntry,
Expand Down Expand Up @@ -155,46 +156,23 @@ function installArtifact(app, stageDir, label) {
// never registered.
warnOnUnclaimedMembers(root, apps, key);

const findings = [];
for (const entry of apps) {
const destDir = join(APPS_DIR, entry.slug);
if (existsSync(destDir)) rmSync(destDir, { recursive: true });
copyDir(entry.appDir, destDir);

const html = readFileSync(join(destDir, entry.entryPoint), 'utf8');
checkHeadlessMarker(html, `${entry.slug}: ${entry.entryPoint}`);
// Checked as delivered, before the build hoists or strips anything, and on
// every source — prebuilt and local included, not only a GitHub fetch.
findings.push(...checkApp(destDir, entry));

ok(`${entry.slug} ready (${label})`);
}
return apps.map(toRegistryEntry);
}

/** The marker a headless artifact must carry on `<html>`. */
const HEADLESS_MARKER = 'data-kb-headless="true"';
/** Its pre-rename spelling — see issue #77. */
const LEGACY_HEADLESS_MARKER = 'data-mp-headless';

/**
* Warns when an artifact's entry point is not marked headless.
*
* A bundle published before the rename carries `data-mp-headless`, which is
* indistinguishable from "not headless at all" to every downstream consumer.
* Saying so explicitly is the difference between a publisher re-reading the
* contract and a publisher re-reading their build script.
*
* This used to run only on the GitHub fetch path, so the sources CI actually
* uses were never checked.
*/
function checkHeadlessMarker(html, label) {
if (html.includes(HEADLESS_MARKER)) return;
if (html.includes(LEGACY_HEADLESS_MARKER)) {
warn(
`${label} carries ${LEGACY_HEADLESS_MARKER}, which this knowledge base no longer reads. ` +
`The artifact was produced against the pre-rename contract — republish it with a current ` +
`AbsaOSS/knowledge-base action so it emits ${HEADLESS_MARKER}.`,
);
return;
}
warn(`${label} is missing ${HEADLESS_MARKER} on <html> — see contract/HEADLESS_RULES.md.`);
// The same contract checks the publish-docs action runs (contract/RULES.md).
// Warnings unless the build is strict, where an error fails it.
reportFindings(key, findings, { strict: STRICT, warn });
return apps.map(toRegistryEntry);
}

/**
Expand Down
72 changes: 72 additions & 0 deletions scripts/check-artifact.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
/**
* check-artifact.js — the contract checker, run on every artifact the build installs.
*
* The publish-docs action checks a docs repo's output before it is released
* (actions/lib/check.js, rules in contract/RULES.md). This runs the same checks
* again on what the knowledge base actually received, because not every
* artifact went through a current action: one published before a rule existed,
* one packed by hand, one from a pinned older action. The build repairs some
* findings as it re-hosts a page (it hoists inline scripts, strips dark mode,
* rebases URLs), so the report is for whoever owns the registry: which docs
* repo to chase, and for what.
*
* check.js resolves its parsers from actions/node_modules when that tree is
* installed and from the root node_modules otherwise. The root package.json
* pins htmlparser2 and postcss to the versions actions/package.json pins, so
* either way the build and the action parse the same way
* (tests/artifact-checks.spec.js holds the two in step).
*/

import { RULES_DOC, formatFinding } from '../actions/lib/rules.js';

export { checkApp } from '../actions/lib/check.js';

/**
* Groups findings by rule: one line per rule with its count and first example,
* because a docs site repeats the same template on every page and forty
* identical warnings bury the one that differs.
*/
export function summarise(findings) {
const byRule = new Map();
for (const f of findings) {
if (!byRule.has(f.id)) byRule.set(f.id, []);
byRule.get(f.id).push(f);
}
return [...byRule.values()].map((group) => {
const [first] = group;
const count = group.length > 1 ? ` ×${group.length}, e.g.` : '';
return { id: first.id, severity: first.severity, line: `${first.id}${count} ${first.where}: ${first.message}` };
});
}

/**
* Reports one artifact's findings through the build's logger.
*
* Errors are rules the publish action refuses to release. In a strict
* (production) build they fail the build: an artifact that breaks the contract
* reached the registry some other way, and a deployment should not ship it
* unnoticed. Otherwise every finding is a warning, since the build still serves
* the page. Returns the counts, for the build's summary.
*
* @param {string} key - the registry source, for the messages
* @param {Array} findings - from checkApp, across the artifact's apps
* @param {{strict: boolean, warn: (msg: string) => void}} options
*/
export function reportFindings(key, findings, { strict, warn }) {
const errors = findings.filter((f) => f.severity === 'error');
const warnings = findings.length - errors.length;

if (strict && errors.length > 0) {
throw new Error(
`${key}: the artifact breaks the knowledge base contract (see ${RULES_DOC}):\n` +
errors.map((e) => ` • ${formatFinding(e)}`).join('\n') + '\n' +
` The publish-docs action refuses these; republish with a current action.`,
);
}

for (const { severity, line } of summarise(findings)) warn(`${key}: ${severity} ${line}`);
if (findings.length > 0) {
warn(`${key}: ${errors.length} error(s), ${warnings} warning(s) against ${RULES_DOC} — the docs repo can run the same checks with actions/lib/check-cli.js.`);
}
return { errors: errors.length, warnings };
}
Loading
Loading