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
15 changes: 15 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,21 @@ jobs:
'
- run: npm run selftest
working-directory: actions
# The self-tests call check-docs' entry point directly; this runs the
# composite action itself, the way a docs repo's pull request does, on a
# fixture that must come out clean.
- name: Run check-docs as a docs repo would
id: check-docs
uses: ./actions/check-docs
with:
manifest: tests/fixtures/single-page-bundle/kb-docs.json
dist: tests/fixtures/single-page-bundle
strict: 'true'
- name: check-docs reported no findings
env:
KB_ERRORS: ${{ steps.check-docs.outputs.errors }}
KB_WARNINGS: ${{ steps.check-docs.outputs.warnings }}
run: test "$KB_ERRORS" = 0 && test "$KB_WARNINGS" = 0

# ── 5. Deployment workflow dry run ─────────────────────────────────────────
#
Expand Down
1 change: 1 addition & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ Orchestrator: `scripts/build-vite.js`. Flags: `--local`, `--headless`.
- `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/check-docs/` — Reusable GitHub Action for docs repo pull requests: runs `checkWorkspace()` from `actions/lib/check.js` on the built headless output, one annotation per rule, every finding in the job summary; errors fail it (warnings too with `strict: true`). Never packs or uploads
- `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
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -426,6 +426,12 @@ jobs:
The archive layout and the manifest are specified in
[`contract/ARTIFACT.md`](contract/ARTIFACT.md).

To catch problems before a release, run the same checks on every pull request with
[`actions/check-docs`](actions/check-docs): the same build step, then
`uses: AbsaOSS/knowledge-base/actions/check-docs@v1` with the same `manifest` and `dist`.
Findings are annotated by rule ID ([`contract/RULES.md`](contract/RULES.md)); errors fail
the check.

---

## Deployment
Expand Down Expand Up @@ -482,6 +488,7 @@ knowledge-base/
├── apps.json ← Registry of doc apps
├── astro.config.mjs ← Astro SSG config (base /knowledge-base)
├── actions/publish-single-page-docs/ ← Reusable action: markdown → single-page bundle
├── actions/check-docs/ ← Reusable action: contract checks on a docs repo's pull request
├── src/
│ ├── pages/
│ │ ├── index.astro ← Landing catalog
Expand Down
61 changes: 61 additions & 0 deletions actions/check-docs/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# check-docs

Runs the knowledge base contract checks on a docs repo's built headless site, on a pull
request. It uses the same checker the [`publish-docs`](../publish-docs) action runs at
release time, but it does not release or upload anything. A finding that would fail the
publish fails the pull request instead, one release earlier.

```yaml
# .github/workflows/check-docs.yml in the docs repo
name: Check docs

on:
pull_request:

permissions:
contents: read

jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

# The same headless build the publish workflow runs.
- run: npm ci && npm run build -- --headless

- uses: AbsaOSS/knowledge-base/actions/check-docs@v1
with:
manifest: kb-docs.json
dist: dist
```

Every rule it checks is in [`contract/RULES.md`](../../contract/RULES.md), by ID.

- **Error findings fail the check.** These are what `publish-docs` refuses to release.
- **Warning findings do not fail it**, unless `strict: true`. They are annotated on the
run, one annotation per rule with a count and the first example.
- **The job summary** tables every rule and lists every finding.

This is for **packaged** sites only. A repo that uses
[`publish-single-page-docs`](../publish-single-page-docs) has no built output of its own
to check: the action renders the markdown itself.

## Inputs

| Input | Default | |
|---|---|---|
| `manifest` | `kb-docs.json` | Path to the manifest, relative to the workspace. |
| `dist` | `dist` | The built headless output, relative to the workspace. Use the same value the publish workflow passes. |
| `strict` | `false` | `true` fails on warnings as well as errors. |
| `npm-registry`, `npm-token`, `node-mirror`, `node-mirror-token` | | For runners without public internet. The same inputs as `publish-docs`; see [`contract/DEPLOYMENT.md`](../../contract/DEPLOYMENT.md) → "Private networks". |

## Outputs

| Output | |
|---|---|
| `errors` | Number of error findings. |
| `warnings` | Number of warning findings. |

The same checks run locally with `node actions/lib/check-cli.js` from a checkout of this
repository; see `contract/RULES.md` → "Running the checks".
108 changes: 108 additions & 0 deletions actions/check-docs/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
name: Check docs against the knowledge base contract
description: >-
Run the knowledge base contract checks on a repository's built headless
documentation site, on a pull request, without releasing or uploading
anything. Findings are annotated by rule ID and summarised on the run.
author: AbsaOSS

branding:
icon: check-circle
color: purple

inputs:
manifest:
description: >-
Path to the repository's kb-docs.json, relative to the workspace.
required: false
default: kb-docs.json
dist:
description: >-
The built headless output, relative to the workspace — the same value the
publish-docs workflow passes. With one app in the manifest this is that
app's directory; with several, it holds one subdirectory per slug.
required: false
default: dist
strict:
description: >-
"true" fails the check on warnings as well as errors. Errors always fail
it: they are what the publish-docs action refuses to release.
required: false
default: 'false'
npm-registry:
description: >-
npm registry to install the action's own dependencies from, for runners
that cannot reach registry.npmjs.org — an Artifactory npm remote, say.
Empty means whatever npm resolves on the runner, which is the public
registry unless the machine's npm configuration says otherwise.
required: false
default: ''
npm-token:
description: >-
Bearer token for `npm-registry`. Leave empty for a registry that allows
anonymous reads.
required: false
default: ''
node-mirror:
description: >-
Mirror of https://nodejs.org to download Node from, for runners that can
reach neither nodejs.org nor the github.com release assets `setup-node`
tries first. Not needed when the runner image already carries Node in its
tool cache.
required: false
default: ''
node-mirror-token:
description: Authorization header value for `node-mirror`.
required: false
default: ''

outputs:
errors:
description: Number of error findings.
value: ${{ steps.check.outputs.errors }}
warnings:
description: Number of warning findings.
value: ${{ steps.check.outputs.warnings }}

runs:
using: composite
steps:
# The same pinned toolchain and dependency tree the publishing actions use,
# so a pull request is checked by exactly the code that will check the
# release.
- name: Set up Node.js
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: '20'
mirror: ${{ inputs.node-mirror }}
mirror-token: ${{ inputs.node-mirror-token }}

# See publish-docs/action.yml: project-level npm config next to the
# lockfile, reaching nothing outside this install. Skipped when unset.
- name: Point npm at a private registry
if: ${{ inputs.npm-registry != '' }}
shell: bash
working-directory: ${{ github.action_path }}/..
env:
KB_NPM_REGISTRY: ${{ inputs.npm-registry }}
KB_NPM_TOKEN: ${{ inputs.npm-token }}
run: bash "$GITHUB_ACTION_PATH/../lib/npm-registry.sh"

- name: Install checker dependencies
shell: bash
working-directory: ${{ github.action_path }}/..
env:
# Expanded by npm from the .npmrc the previous step wrote; the token is
# never written to disk.
KB_NPM_TOKEN: ${{ inputs.npm-token }}
run: npm ci --omit=dev --no-audit --no-fund

# Runs in the workspace, and the paths go in as the user wrote them, so the
# findings name `kb-docs.json` and `dist/…` rather than runner paths.
- name: Check the built output
id: check
shell: bash
run: node "$GITHUB_ACTION_PATH/src/index.js"
env:
KB_MANIFEST: ${{ inputs.manifest }}
KB_DIST: ${{ inputs.dist }}
KB_CHECK_STRICT: ${{ inputs.strict }}
77 changes: 77 additions & 0 deletions actions/check-docs/src/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
/**
* index.js — entry point of the check-docs action.
*
* Runs the contract checks (lib/check.js, rules in contract/RULES.md) on a
* docs repo's manifest and built output, the way publish-docs will at release
* time, but on a pull request and without packing or uploading anything. A
* finding that would fail the publish then fails the pull request instead, a
* release earlier.
*
* Annotations are one per rule, not one per finding: a docs site repeats one
* template on every page, and GitHub shows only the first few annotations of a
* step. The job summary carries every finding.
*
* Env:
* KB_MANIFEST path to kb-docs.json, relative to the workspace (default: kb-docs.json)
* KB_DIST built output, relative to the workspace (default: dist)
* KB_CHECK_STRICT "true" fails on warnings too
*/

import { checkWorkspace, summarise } from '../../lib/check.js';
import { PublishError } from '../../lib/manifest.js';
import { RULES_DOC, formatFinding } from '../../lib/rules.js';
import { annotate, run, setOutput, summary } from '../../lib/runner.js';

const RULES_URL = 'https://github.com/AbsaOSS/knowledge-base/blob/master/contract/RULES.md';

/** A table cell: pipes escaped, newlines flattened. */
const cell = (text) => String(text).replace(/\|/g, '\\|').replace(/\s*\n\s*/g, ' ');

function report(findings, strict) {
const errors = findings.filter((f) => f.severity === 'error').length;
const warnings = findings.length - errors;
const groups = summarise(findings);

for (const g of groups) annotate(g.line, { level: g.severity, title: g.id });

const verdict = errors > 0 || (strict && warnings > 0) ? 'fails' : 'passes';
let md = `### Knowledge base contract: ${errors} error(s), ${warnings} warning(s)\n\n`;
if (findings.length === 0) {
md += 'No findings — the built output satisfies the knowledge base contract.\n';
} else {
md += `The check ${verdict}${strict ? ' (strict: warnings fail too)' : ''}. Every rule is explained in [${RULES_DOC}](${RULES_URL}).\n\n`;
md += '| Rule | Severity | Count | First finding |\n|---|---|---|---|\n';
for (const g of groups) {
md += `| ${g.id} | ${g.severity} | ${g.count} | ${cell(`${g.first.where}: ${g.first.message}`)} |\n`;
}
md += `\n<details><summary>All ${findings.length} finding(s)</summary>\n\n`;
md += findings.map((f) => `- \`${f.severity}\` ${cell(formatFinding(f))}`).join('\n');
md += '\n\n</details>\n';
}
summary(md);

setOutput('errors', String(errors));
setOutput('warnings', String(warnings));
return { errors, warnings };
}

function main() {
const strict = process.env.KB_CHECK_STRICT === 'true';
const findings = checkWorkspace({
manifest: process.env.KB_MANIFEST || 'kb-docs.json',
dist: process.env.KB_DIST || 'dist',
});

for (const f of findings) console.log(`${f.severity.padEnd(7)} ${formatFinding(f)}`);
const { errors, warnings } = report(findings, strict);
console.log(`\n${errors} error(s), ${warnings} warning(s). Each rule is explained in ${RULES_DOC}.`);

if (errors > 0) {
throw new PublishError(`${errors} error finding(s): the publish-docs action would refuse this output. See the annotations and the job summary.`);
}
if (strict && warnings > 0) {
throw new PublishError(`${warnings} warning finding(s), and "strict" is on. See the annotations and the job summary.`);
}
}

run(main);
Loading
Loading