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
5 changes: 5 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,11 @@ jobs:
# Also proves the Dockerfile's dist/ sanity checks pass on a real build.
- name: Build image
run: docker build -t knowledge-base:ci .
# The CSP checks in the container suite need a real browser: a policy that
# blocks something the page needs fails silently, and only a browser
# reports the violation.
- name: Install Playwright browser
run: npx playwright install --with-deps chromium
# The only place nginx.conf itself is executed. The other suites run
# against tests/fragment-server.mjs, an Express mirror of the rewrites —
# see playwright.config.docker.js for why that is not sufficient.
Expand Down
12 changes: 12 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,18 @@ Artifacts come from other repositories' releases. Treat their HTML, CSS and
archive contents as attacker-controlled: no unvalidated archive extraction, no
unsanitised HTML re-hosting, no shell interpolation of registry values.

### Nothing inline in the output

The deployment serves `script-src 'self'` with no `'unsafe-inline'`. That holds
only because no inline `<script>` survives into `dist/` — the publishing action
emits its mermaid init as a file, and `scripts/hoist-inline-scripts.js` moves any
inline script found in a sub-app artifact into one. A change that introduces an
inline script fails `tests/build-integrity.spec.js` before it can start breaking
pages silently in production.

Inline `<style>` is still allowed (`style-src` keeps `'unsafe-inline'`); tightening
that is a separate piece of work.

### Portability

The build runs on Linux CI and on Windows developer machines. Prefer Node APIs
Expand Down
3 changes: 3 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ docker run -p 8080:8080 knowledge-base
```
apps.json (registry)
→ [1] Fetch Release artifacts OR build local repos → apps/{slug}/
→ [1b] Hoist inline <script> bodies → apps/{slug}/_kb-inline/*.js
(keeps script-src 'self' viable for already-published bundles)
→ [2] Copy non-HTML assets → public/{slug}/ (Astro serves as static)
→ [3] astro build: [...path].astro enumerates all HTML via getStaticPaths
→ transformSubAppHtml() rewrites URLs + splits the document,
Expand Down Expand Up @@ -76,6 +78,7 @@ Orchestrator: `scripts/build-vite.js`. Flags: `--local`, `--headless`, `--path-p
- `scripts/build-vite.js` — Build orchestrator (3-step pipeline)
- `scripts/fetch-apps.js` — GitHub Release artifact downloader
- `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/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
- `actions/publish-single-page-docs/` — Reusable GitHub Action that turns a repo's markdown into a single-page bundle

### Three Onboarding Types
Expand Down
8 changes: 7 additions & 1 deletion actions/publish-single-page-docs/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,12 @@ runs:
GH_TOKEN: ${{ inputs.github-token }}
KB_RELEASE_TAG: ${{ inputs.release-tag }}
KB_ARTIFACT: ${{ steps.build.outputs.artifact }}
# Read through the environment, never interpolated into the script body:
# a ${{ }} expression is substituted into the shell text before bash sees
# it, so any metacharacter in the value would execute rather than quote.
# These come from the consuming repo's `docs` input.
KB_COUNT: ${{ steps.build.outputs.count }}
KB_SLUGS: ${{ steps.build.outputs.slugs }}
run: |
set -euo pipefail

Expand All @@ -96,4 +102,4 @@ runs:
gh release upload "$tag" "$KB_ARTIFACT" --clobber --repo "$GITHUB_REPOSITORY"

echo "release-tag=$tag" >> "$GITHUB_OUTPUT"
echo "Published ${{ steps.build.outputs.count }} doc(s): ${{ steps.build.outputs.slugs }}" >> "$GITHUB_STEP_SUMMARY"
echo "Published $KB_COUNT doc(s): $KB_SLUGS" >> "$GITHUB_STEP_SUMMARY"
190 changes: 188 additions & 2 deletions actions/publish-single-page-docs/package-lock.json

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

1 change: 1 addition & 0 deletions actions/publish-single-page-docs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"markdown-it-footnote": "4.0.0",
"markdown-it-task-lists": "2.1.1",
"mermaid": "11.4.1",
"sanitize-html": "2.17.0",
"yaml": "2.7.1"
}
}
15 changes: 12 additions & 3 deletions actions/publish-single-page-docs/src/bundle.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@
* bundle.json ← manifest listing every doc in this bundle
* {slug}/index.html ← headless document
* {slug}/assets/doc.css
* {slug}/assets/mermaid.min.js (only when that doc uses mermaid)
* {slug}/assets/mermaid.min.js (only when that doc uses mermaid)
* {slug}/assets/mermaid-init.js (ditto — kept out of the HTML so the
* marketplace can run script-src 'self')
*
* One release asset carries every doc from the repo; the knowledge base expands
* the manifest into one marketplace app per doc (see contract/SINGLE_PAGE.md).
Expand All @@ -18,7 +20,9 @@ import { createRequire } from 'node:module';
import { basename, dirname, join } from 'node:path';

import { renderMarkdown } from './markdown.js';
import { CSS_PATH, DOC_CSS, MERMAID_PATH, renderDocument } from './template.js';
import {
CSS_PATH, DOC_CSS, MERMAID_INIT_JS, MERMAID_INIT_PATH, MERMAID_PATH, renderDocument,
} from './template.js';

const require = createRequire(import.meta.url);

Expand Down Expand Up @@ -56,7 +60,12 @@ export function buildBundle(docs, stageDir) {
}));
writeFileSync(join(docDir, CSS_PATH), DOC_CSS);

if (usesMermaid) copyFileSync(resolveMermaidBundle(), join(docDir, MERMAID_PATH));
if (usesMermaid) {
copyFileSync(resolveMermaidBundle(), join(docDir, MERMAID_PATH));
// The init runs from a file, not an inline <script>, so the marketplace
// can serve these pages under script-src 'self'.
writeFileSync(join(docDir, MERMAID_INIT_PATH), MERMAID_INIT_JS);
}

rendered.push({ ...doc, usesMermaid });
}
Expand Down
Loading