diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..9f3a11e --- /dev/null +++ b/.dockerignore @@ -0,0 +1,27 @@ +# The Docker image only needs nginx.conf + the prebuilt dist/ (see Dockerfile). +# Keep the build context small and avoid leaking source/secrets into the image. +node_modules +.git +.github +tests +scripts +src +contract +plugins +public +apps +tmp +.astro +playwright-report +test-results +*.md +.gitignore +.dockerignore +.env +Dockerfile +playwright*.js +astro.config.mjs +vite.config.js +tsconfig.json +package-lock.json +apps.json diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..8c28212 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,19 @@ +# Dependabot keeps npm dependencies and pinned GitHub Actions up to date. +# Action updates land as SHA bumps with the human-readable tag in a comment. +version: 2 +updates: + - package-ecosystem: npm + directory: / + schedule: + interval: weekly + open-pull-requests-limit: 5 + groups: + # Batch low-risk dev tooling into a single PR. + dev-dependencies: + dependency-type: development + + - package-ecosystem: github-actions + directory: / + schedule: + interval: weekly + open-pull-requests-limit: 5 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a0fc5d1..4f6769b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,112 +1,115 @@ -# CI checks — run on every PR and push to main. +# CI — runs on every PR and push to the default branch. +# +# Fully hermetic: the build + E2E suites use the vendored docs-example fixture +# (tests/fixtures/docs-example.dist.tar.gz) via apps.json `prebuilt` entries, so +# no GITHUB_TOKEN, network, or sibling repo is required. # # Jobs: # typecheck — astro check (TypeScript / Astro type errors) -# build — full headless build (fetches app releases via GITHUB_TOKEN) -# playwright — standalone Playwright tests against the preview server -# (no data-gateway needed — covers web-fragment contract) -# -# Full integration tests (marketplace.spec.js) require a running data-gateway -# and are run locally. See playwright.config.js. +# build — headless build from the vendored fixture; uploads dist/ +# e2e — Playwright: embedded web-fragment harness + standalone layer +# audit — npm dependency vulnerability gate name: CI on: push: - branches: [main] + branches: [master, main] pull_request: - branches: [main] + branches: [master, main] + +# Least privilege: jobs only read the repo. Override per-job if more is needed. +permissions: + contents: read jobs: # ── 1. TypeScript / Astro type-check ─────────────────────────────────────── + # Non-blocking: `astro check` is memory-hungry and can OOM on standard runners + # (tracked separately). The `build` job below is the hard compile gate — a real + # type/template error fails `astro build`. This job surfaces strict diagnostics + # without wedging CI on an OOM. typecheck: - name: Type-check + name: Type-check (non-blocking) runs-on: ubuntu-latest + continue-on-error: true steps: - - uses: actions/checkout@v4 - - - uses: actions/setup-node@v4 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: node-version: '24' cache: npm - - - name: Install dependencies - run: npm ci - + - run: npm ci - name: Astro type-check run: npx astro check + env: + NODE_OPTIONS: --max-old-space-size=6144 - # ── 2. Build ─────────────────────────────────────────────────────────────── + # ── 2. Build (headless, hermetic) ────────────────────────────────────────── build: name: Build (headless) runs-on: ubuntu-latest needs: typecheck steps: - - uses: actions/checkout@v4 - - - uses: actions/setup-node@v4 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: node-version: '24' cache: npm - - - name: Install dependencies - run: npm ci - - # Fetches release artifacts from apps.json repos (AbsaOSS public repos) - # then builds the Astro site in headless/web-fragment mode. - - name: Build marketplace (headless) + - run: npm ci + # Builds from the committed apps.json (vendored fixture) — no token/network. + - name: Build (headless) run: npm run build:headless - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - MP_HEADLESS: 'true' - - name: Upload dist artifact - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: dist path: dist/ - retention-days: 1 + retention-days: 7 - # ── 3. Playwright — standalone (no gateway) ──────────────────────────────── - # - # Tests the fragment server directly on port 3000. Covers: - # - HTTP header safety (X-Frame-Options must not block the gateway iframe) - # - Headless mode (data-mp-headless, no chrome bar) - # - CSS link stability (data-astro-transition-persist present — prevents #297) - # - Asset routing (/__wf/knowledge-base/ rewrite via Astro preview plugin) - # - Client-side nav (no full reload, URL updates via pushState) + # ── 3. Playwright E2E ────────────────────────────────────────────────────── # - playwright: - name: Playwright (standalone) + # Two layers, both hermetic (each webServer rebuilds from the fixture): + # • embedded (playwright.config.js) — full web-fragment harness: host + # gateway proxies/embeds the fragment, shadow-DOM isolation, SPA routing, + # cross-app nav, asset 404s, history limitation. + # • standalone (playwright.config.ci.js) — fragment server only: HTTP header + # safety, headless contract, CSS-link stability (#297), asset routing. + e2e: + name: E2E (Playwright) runs-on: ubuntu-latest needs: build steps: - - uses: actions/checkout@v4 - - - uses: actions/setup-node@v4 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: node-version: '24' cache: npm - - - name: Install dependencies - run: npm ci - - - name: Download dist artifact - uses: actions/download-artifact@v4 - with: - name: dist - path: dist/ - - - name: Install Playwright browsers + - run: npm ci + - name: Install Playwright browser run: npx playwright install --with-deps chromium - - - name: Run standalone tests + - name: Embedded web-fragment tests + run: npm test + - name: Standalone fragment tests run: npx playwright test --config=playwright.config.ci.js - - name: Upload Playwright report - uses: actions/upload-artifact@v4 if: failure() + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: playwright-report path: playwright-report/ retention-days: 7 + + # ── 4. Dependency audit ──────────────────────────────────────────────────── + audit: + name: npm audit + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 + with: + node-version: '24' + cache: npm + - run: npm ci + # Fail only on production-dependency vulnerabilities (high or above). + - name: Audit production dependencies + run: npm audit --omit=dev --audit-level=high diff --git a/.github/workflows/validate-doc-app.yml b/.github/workflows/validate-doc-app.yml index f03b5cc..f9d5d6f 100644 --- a/.github/workflows/validate-doc-app.yml +++ b/.github/workflows/validate-doc-app.yml @@ -1,18 +1,17 @@ -# Reusable workflow — validate a doc app against the knowledge-base marketplace contract. +# Reusable workflow — validate a doc app against the knowledge-base contract. # -# Called by doc app repos in their CI to verify the app meets the contract -# before raising a PR or publishing a release. +# Called by doc-app repos in their own CI to verify the app meets the marketplace +# contract before raising a PR or publishing a release. # -# Usage in a doc repo: +# Usage in a doc repo (.github/workflows/validate.yml): # # jobs: # validate: -# uses: AbsaOSS/knowledge-base/.github/workflows/validate-doc-app.yml@main -# secrets: inherit +# uses: AbsaOSS/knowledge-base/.github/workflows/validate-doc-app.yml@master # # The calling repo must have: # - marketplace.json in the repo root -# - npm run build -- --headless producing dist/ +# - `npm run build -- --headless` producing dist/ # name: Validate Doc App @@ -24,54 +23,55 @@ on: type: string default: '24' +permissions: + contents: read + jobs: validate: name: Validate marketplace contract runs-on: ubuntu-latest steps: - # ── 1. Checkout ──────────────────────────────────────────────────────── - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - # ── 2. Setup Node ────────────────────────────────────────────────────── - name: Setup Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: node-version: ${{ inputs.node-version }} cache: npm - # ── 3. Install dependencies ──────────────────────────────────────────── - name: Install dependencies run: npm ci - # ── 4. Validate marketplace.json ─────────────────────────────────────── + # ── Validate marketplace.json ────────────────────────────────────────── - name: Validate marketplace.json exists 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/main/contract/HEADLESS_RULES.md" + 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: | - node - <<'EOF' - import { readFileSync } from 'fs'; + # 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 ajv = new Ajv({ allErrors: true }); - const schemaUrl = 'https://raw.githubusercontent.com/AbsaOSS/knowledge-base/main/contract/schema.json'; + 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 remoteSchema = await res.json(); - const validate = ajv.compile(remoteSchema); + const schema = await res.json(); + const validate = new Ajv({ allErrors: true }).compile(schema); if (!validate(manifest)) { console.error('✗ marketplace.json validation errors:'); @@ -82,14 +82,12 @@ jobs: } console.log('✓ marketplace.json is valid'); EOF - env: - NODE_OPTIONS: --experimental-vm-modules - # ── 5. Build headless ────────────────────────────────────────────────── + # ── Build headless ───────────────────────────────────────────────────── - name: Build headless run: npm run build -- --headless - # ── 6. Check dist/ output ───────────────────────────────────────────── + # ── Check dist/ output ───────────────────────────────────────────────── - name: Check dist/ exists run: | if [ ! -d dist ]; then @@ -107,7 +105,7 @@ jobs: fi echo "✓ dist/${ENTRY} found" - # ── 7. Validate headless HTML structure ─────────────────────────────── + # ── Validate headless HTML structure ─────────────────────────────────── - name: Check data-mp-headless attribute run: | ENTRY=$(node -e "const m=require('./marketplace.json'); console.log(m.entryPoint || 'index.html')") @@ -131,9 +129,9 @@ jobs: - name: Check all asset paths are relative run: | - node - <<'EOF' - import { readdirSync, statSync, readFileSync } from 'fs'; - import { join, extname } from 'path'; + node --input-type=module <<'EOF' + import { readdirSync, statSync, readFileSync } from 'node:fs'; + import { join, extname } from 'node:path'; function collectHtml(dir) { const files = []; @@ -157,14 +155,17 @@ jobs: } } if (violations > 0) { - console.error(`\n${violations} absolute path(s) found. Marketplace mounts apps at /apps/{slug}/ — absolute paths will 404.`); + 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 - # ── 8. Check ABSA tokens in CSS ─────────────────────────────────────── - - name: Check ABSA design tokens in CSS + # ── 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 @@ -172,18 +173,18 @@ jobs: else FOUND=false for f in $CSS_FILES; do - if grep -q -- '--color-absa-500\|#af144b' "$f"; then + if grep -q -- '--color-kb-500\|#af144b' "$f"; then FOUND=true break fi done if [ "$FOUND" = "false" ]; then - echo "::warning::ABSA brand color token (--color-absa-500 / #af144b) not found in dist CSS." - echo "Ensure the ABSA design tokens are included. See contract/STYLE_GUIDE.md" + 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 "✓ ABSA design tokens detected in CSS" + 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)\")" + run: echo "✓ All contract checks passed for $(node -e "console.log(require('./marketplace.json').slug)")" diff --git a/.gitignore b/.gitignore index e005dcc..bc91f06 100644 --- a/.gitignore +++ b/.gitignore @@ -2,9 +2,9 @@ node_modules/ dist/ tmp/ apps/ -apps.json .env *.tar.gz +!tests/fixtures/*.tar.gz playwright-report/ test-results/ .astro/ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..fef268e --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,45 @@ +# Contributing + +Thanks for your interest in improving **knowledge-base**. + +## Development setup + +```bash +npm install # Node >= 24 +npm run build:headless # hermetic build from the vendored fixture (tests/fixtures/) +npm test # embedded web-fragment E2E (Playwright) +``` + +The build and tests are fully hermetic — they use the committed +`tests/fixtures/docs-example.dist.tar.gz` fixture (registered via `apps.json`), +so no `GITHUB_TOKEN`, network, or sibling repository is required. + +See [`CLAUDE.md`](CLAUDE.md) for an architecture overview and the full command +list, and [`README.md`](README.md) for usage. + +## Tests + +| Command | What it runs | +|---|---| +| `npm test` | Embedded web-fragment harness (`playwright.config.js`) — host gateway proxies/embeds the fragment on `:4201`. | +| `npx playwright test --config=playwright.config.ci.js` | Standalone fragment-server layer (`:3000`) — headers, headless contract, asset routing, #297. | + +Both run in CI (`.github/workflows/ci.yml`). Please make sure both pass before +opening a PR. + +## Pull requests + +1. Branch off `master`. +2. Keep changes focused; update docs/tests alongside code. +3. Ensure `npm run build:headless`, `npm test`, and the standalone suite pass. +4. CI (type-check, build, E2E, audit) must be green. + +## Reporting security issues + +Please follow [`SECURITY.md`](SECURITY.md) — do not file public issues for +vulnerabilities. + +## License + +By contributing, you agree that your contributions are licensed under the +[Apache License 2.0](LICENSE). diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..5f60223 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by their Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2026 ABSA Group Limited + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/README.md b/README.md index 2e877c0..dcab97c 100644 --- a/README.md +++ b/README.md @@ -1,268 +1,247 @@ # knowledge-base -> A unified documentation portal — wraps independently-maintained doc apps into -> a single deployable with a persistent branded chrome. +> A unified documentation portal — wraps independently-maintained doc apps into a +> single deployable with a persistent branded chrome, and can be embedded as a +> web fragment inside a host app. --- ## What it is -`knowledge-base` is a **build-time aggregator** for static documentation sites hosted -across multiple GitHub repositories. +`knowledge-base` is a **build-time aggregator** for static documentation sites. At +build time it: -At build time it: -1. Downloads each registered app's **GitHub Release artifact** (`dist.tar.gz`) -2. **Injects the marketplace top chrome** (56 px persistent nav with app switcher) into every HTML page -3. Generates a **catalog landing page** listing all available apps -4. Produces a single `dist/` directory served by nginx in Docker → AWS ECS +1. Obtains each registered app's built output (`dist/`) — from a **GitHub Release + artifact** (`dist.tar.gz`), a **local repo**, or a **prebuilt** tarball/dir. +2. Rewrites every page's URLs to absolute `/knowledge-base/{slug}/…` paths and, + in standalone mode, injects a persistent top **chrome** (nav + app switcher). +3. Generates a **catalog landing page** listing all registered apps. +4. Produces a single `dist/` served by **nginx** in Docker, or embedded into a + host app as a **web fragment**. -Each app retains its own sidebar, routing, and internal navigation. The marketplace -adds the persistent top bar and gives users a consistent entry point. +Each app keeps its own sidebar, routing, and internal navigation. --- ## Architecture ``` -Browser → https://docs.internal/ - │ - ▼ - ┌──────────────────────────────┐ - │ Marketplace top chrome │ ← injected at build time - │ [Logo] Docs / App ▾ │ persists on every page - └──────────────────────────────┘ - │ │ - │ Sub-site content │ ← from dist/apps/{slug}/ - │ (sidebar + prose) │ built headless by each repo - │ │ - └──────────────────────────────┘ - │ - ▼ - nginx (Docker) → ECS +Browser ──► host origin ──► /knowledge-base/ (landing catalog) + /knowledge-base/{slug}/… (each doc app) + /__wf/knowledge-base/… (fragment assets) + │ + ▼ + nginx (Docker) ─or─ web-fragments gateway (embedded) + │ + ▼ + dist/ (static) ``` +When **embedded**, a host app's [web-fragments](https://web-fragments.dev) +gateway proxies the `/knowledge-base/*` routes onto the host's single origin and +reframes the markup into a shadow root — no full page reload between pages. + --- -## Registered apps +## Quick start -Apps are listed in [`apps.json`](apps.json): +Prerequisites: **Node.js ≥ 24**. For the GitHub-fetch build, `gh` CLI +authenticated (or `GITHUB_TOKEN` set). -| App | Slug | Repo | URL | -|---|---|---|---| -| Example Docs | `example` | `AbsaOSS/example-docs` | `/apps/example/` | +```bash +npm install ---- +# Hermetic build from the vendored docs-example fixture (no network/token): +npm run build:headless -## Local development +# E2E tests (Playwright — auto-starts its own servers): +npm test +``` -### Prerequisites -- Node.js ≥ 20 -- `gh` CLI authenticated (or `GITHUB_TOKEN` env var set) +### Build modes -### Build with live artifacts +| Command | Source of apps | +|---|---| +| `npm run build` | Fetch GitHub Release artifacts (needs `GITHUB_TOKEN`/`gh`) | +| `npm run build:headless` | Same fetch, headless/web-fragment output | +| `npm run build:local` | Build each app from a local checkout (`localPath`) | +| `npm run build:local:headless` | Local + headless | -```bash -npm install -npm run build # fetches GitHub Release artifacts + injects chrome -npx serve dist -p 3000 +`--headless` (or `MP_HEADLESS=true`) produces fragment-ready output: no chrome +bar, `data-mp-headless="true"` on ``, shadow-DOM compat styles. + +Orchestrator: `scripts/build-vite.js` (flags: `--local`, `--headless`, +`--path-prefix=`). + +--- + +## The registry: `apps.json` + +Each entry registers one doc app. A `slug` is required; the source is one of +`repo` (+ optional `version`), `localPath`, or `prebuilt`: + +```jsonc +[ + { + "slug": "my-app", + "name": "My App Documentation", + "description": "What this app documents.", + "icon": "book-open", + "tags": ["guide"], + + // pick ONE source: + "repo": "AbsaOSS/my-docs", "version": "latest", // GitHub Release artifact + // "localPath": "../my-docs", // build from local checkout + // "prebuilt": "tests/fixtures/my-docs.dist.tar.gz" // prebuilt tarball or dist dir + } +] ``` -### Build in local mode (no GitHub fetch) +The repo ships an `apps.json` that registers the **vendored docs-example fixture** +twice (`user-guide`, `guide-mirror`) so the build and tests are hermetic out of +the box. Replace it with your own apps for a real deployment. -If you've already run a full build or have manually placed artifacts: +--- -```bash -# Place a headless-built dist/ under tmp/apps/{slug}/dist/ -mkdir -p tmp/apps/my-app -# ... copy dist/ from a headless doc build ... +## Testing -npm run build:dev -- --local -``` +E2E tests use Playwright. Everything is hermetic — built from +`tests/fixtures/docs-example.dist.tar.gz` (no network, token, or sibling repo). + +| Command | Layer | +|---|---| +| `npm test` | **Embedded** harness (`playwright.config.js`). Starts the fragment server (`:3000`) and a minimal web-fragments **host gateway** (`tests/host/server.mjs`, `:4201`) that embeds the fragment. Covers shadow-DOM isolation, smooth no-reload SPA routing, cross-app navigation, asset 404s, and the fragment-history limitation. | +| `npx playwright test --config=playwright.config.ci.js` | **Standalone** layer. Hits the fragment server (`tests/fragment-server.mjs`, `:3000`) directly. Covers HTTP header safety (`X-Frame-Options`), the headless contract, CSS-link stability (web-fragments [#297](https://github.com/web-fragments/web-fragments/issues/297)), and asset routing. | + +> `tests/fragment-server.mjs` serves `dist/` and mirrors the production +> `nginx.conf` rewrites (including `/__wf/knowledge-base/* → /knowledge-base/*`). +> `astro preview` is **not** used as the fragment endpoint: its Vite +> `configurePreviewServer` rewrite hook does not run for static output, so the +> `/__wf` asset route would 404. + +Both layers run in CI (`.github/workflows/ci.yml`). + +--- -### Adding a new app +## Embedding as a web fragment -1. **In the doc repo:** follow the [contract](contract/HEADLESS_RULES.md) - - Add `marketplace.json` to repo root - - Support `--headless` build flag - - Publish GitHub Release with `dist.tar.gz` +`knowledge-base` can run as a web fragment inside any host app that uses a +web-fragments gateway (Express/Node, Cloudflare, Angular SSR, …). +`tests/host/server.mjs` is a minimal, runnable reference host. -2. **In this repo:** add an entry to `apps.json`: - ```json - { - "repo": "AbsaOSS/my-new-docs", - "slug": "my-app", - "name": "My App Documentation", - "description": "What this app documents.", - "icon": "book-open", - "tags": ["guide"], - "version": "latest" - } - ``` +**Fragment server (this repo):** serve the built `dist/` mirroring the nginx +rewrites — e.g. `node tests/fragment-server.mjs` (port 3000), or nginx in Docker. -3. Open a PR — the CI will validate the contract before merging. +**Host gateway registration:** + +```js +import { FragmentGateway } from 'web-fragments/gateway'; +import { getNodeMiddleware } from 'web-fragments/gateway/node'; + +const gateway = new FragmentGateway(); +gateway.registerFragment({ + fragmentId: 'knowledge-base', + endpoint: 'http://localhost:3000', // the fragment server + piercing: false, + routePatterns: [ + '/knowledge-base/:_*', // landing + sub-app pages + assets + '/__wf/knowledge-base/:_*', // fragment asset prefix + ], +}); +app.use(getNodeMiddleware(gateway)); // before host static/catch-all routes +``` + +**Host page:** + +```html + + + +``` + +> Fragment-internal routes are not mirrored to the host's top-window history and +> are not address-bar deep-linkable — design host-level routing if you need that. --- ## Contract for doc apps -All apps must comply with the marketplace contract before they can be registered. +Apps must comply with the marketplace contract before they can be registered: | Document | Description | |---|---| | [`contract/schema.json`](contract/schema.json) | JSON Schema for `marketplace.json` | -| [`contract/HEADLESS_RULES.md`](contract/HEADLESS_RULES.md) | Structural requirements (headless HTML, relative paths, etc.) | -| [`contract/STYLE_GUIDE.md`](contract/STYLE_GUIDE.md) | Visual style requirements (design tokens, typography, dark mode) | +| [`contract/HEADLESS_RULES.md`](contract/HEADLESS_RULES.md) | Headless HTML, relative paths, `data-mp-headless` | +| [`contract/STYLE_GUIDE.md`](contract/STYLE_GUIDE.md) | Design tokens (`--color-kb-*`), typography, dark mode | -### Quick checklist -- [ ] `marketplace.json` in repo root, validates against `contract/schema.json` -- [ ] `npm run build -- --headless` succeeds → produces headless `dist/` +### Checklist +- [ ] `marketplace.json` in repo root, valid against `contract/schema.json` +- [ ] `npm run build -- --headless` produces a headless `dist/` - [ ] `data-mp-headless="true"` on `` in headless output -- [ ] No fixed site-level `
` in headless output -- [ ] All asset paths are relative (no leading `/`) -- [ ] Design tokens (`--color-kb-500` etc.) in CSS -- [ ] GitHub Release tagged `v*` with `dist.tar.gz` asset -- [ ] References the marketplace's reusable `validate-doc-app.yml` workflow +- [ ] No fixed site-level header in headless output +- [ ] All asset paths relative (no leading `/`) +- [ ] GitHub Release tagged `v*` with a `dist.tar.gz` asset ### Reusable validation workflow -Doc repos should call the marketplace's reusable workflow to enforce the contract in CI: +Doc repos can enforce the contract in their own CI: ```yaml # .github/workflows/validate.yml (in your doc repo) -name: Validate - on: [push, pull_request] - jobs: validate: - uses: AbsaOSS/knowledge-base/.github/workflows/validate-doc-app.yml@main - secrets: inherit + uses: AbsaOSS/knowledge-base/.github/workflows/validate-doc-app.yml@master ``` ### Release workflow for doc repos ```yaml # .github/workflows/release.yml (in your doc repo) -name: Release - on: push: tags: ['v*'] - +permissions: + contents: write jobs: release: runs-on: ubuntu-latest - permissions: - contents: write steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 - with: { node-version: '20', cache: npm } + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 + with: { node-version: '24', cache: npm } - run: npm ci - run: npm run build -- --headless - - name: Package dist - run: tar -czf dist.tar.gz dist/ marketplace.json - - name: Create GitHub Release - uses: softprops/action-gh-release@v2 + - run: tar -czf dist.tar.gz dist/ marketplace.json + - uses: softprops/action-gh-release@3bb12739c298aeb8a4eeaf626c5b8d85266b0e65 # v2.6.2 with: files: dist.tar.gz ``` --- -## Embedding in data-gateway (web-fragments) - -`knowledge-base` can run as a [web-fragment](https://web-fragments.dev) inside the -`data-gateway` Angular SSR app, appearing at the `/knowledge-base` route without a -full page reload. - -### Fragment server (this repo) +## Deployment -Run the fragment server **without compression** — the web-fragments gateway processes -the HTML before sending it to the browser, and compression confuses the pipeline: +Built as a Docker image (nginx serving static files). ```bash -npm run build:local:headless # build with headless flag -npm run preview:embedded # serves on http://localhost:3000/knowledge-base/ -``` - -> **Why not `npm run preview`?** `vite preview` adds brotli/gzip compression which -> causes `ERR_CONTENT_DECODING_FAILED` in the web-fragments gateway. `preview:embedded` -> uses `scripts/preview.js` — a zero-dependency server with no compression. - -### data-gateway registration (`src/server.ts`) - -Add the fragment alongside the existing registrations: - -```typescript -gateway.registerFragment({ - fragmentId: 'knowledge-base', - piercingClassNames: [], - endpoint: environment.fragments.docsMarketplace.endpoint, // e.g. http://localhost:3000 - routePatterns: [ - '/knowledge-base', // bare path without trailing slash - '/knowledge-base/:_*', // landing page + all sub-app pages and assets - ], - piercing: false, -}); -``` - -Add the endpoint to `src/environments/environment.ts`: - -```typescript -docsMarketplace: { - endpoint: 'http://localhost:3000', -}, -``` - -### data-gateway template - -Create the Angular component and template at the `/knowledge-base` route: - -```html - - -``` - -```typescript -// knowledge-base.ts -@Component({ - selector: 'app-knowledge-base', - templateUrl: './knowledge-base.html', - schemas: [CUSTOM_ELEMENTS_SCHEMA], -}) -export class DocsMarketplaceComponent {} +docker build -t knowledge-base . +docker run -p 8080:8080 knowledge-base # http://localhost:8080/knowledge-base/ ``` ---- - -## Deployment - -Built as a Docker image (nginx serving static files) deployed to AWS ECS. - -### Environment variables / GitHub repo variables - -| Name | Description | Default | -|---|---|---| -| `AWS_REGION` | AWS region for ECR + ECS | `eu-west-1` | -| `ECR_REPOSITORY` | ECR repository name | `knowledge-base` | -| `ECS_CLUSTER` | ECS cluster name | `knowledge-base` | -| `ECS_SERVICE` | ECS service name | `knowledge-base` | -| `DEPLOY_URL` | Public URL of the deployment | — | - -### Secrets required +### Deploy configuration (example — AWS ECS) -| Secret | Description | +| Variable | Description | |---|---| -| `AWS_ACCESS_KEY_ID` | IAM access key with ECR push + ECS deploy permissions | -| `AWS_SECRET_ACCESS_KEY` | Corresponding secret key | +| `AWS_REGION` | AWS region for ECR + ECS | +| `ECR_REPOSITORY` | ECR repository name | +| `ECS_CLUSTER` / `ECS_SERVICE` | ECS cluster / service name | -### Manual deploy - -```bash -docker build -t knowledge-base . -docker run -p 8080:8080 knowledge-base -``` +Provide `AWS_ACCESS_KEY_ID` / `AWS_SECRET_ACCESS_KEY` (or an OIDC role) with ECR +push + ECS deploy permissions via repository secrets. --- @@ -270,26 +249,43 @@ docker run -p 8080:8080 knowledge-base ``` knowledge-base/ -├── apps.json ← Registry of all doc apps -├── package.json +├── apps.json ← Registry of doc apps +├── astro.config.mjs ← Astro SSG config (base /knowledge-base) ├── src/ -│ ├── input.css ← Design tokens + marketplace styles -│ └── templates/ -│ ├── chrome.js ← Persistent top nav HTML template -│ └── landing.js ← Catalog landing page template +│ ├── pages/ +│ │ ├── index.astro ← Landing catalog +│ │ └── [...path].astro ← Catch-all: renders every sub-app page +│ ├── layouts/Base.astro +│ ├── components/ ← AppCard, AppIcon, Chrome +│ ├── templates/chrome.js ← Chrome HTML + client SPA router +│ ├── styles/marketplace.css ← Design tokens + Tailwind +│ └── utils/ +│ ├── apps.js ← getAppPages() page enumeration +│ └── transform.js ← URL rewriting + chrome/headless injection ├── scripts/ -│ ├── build.js ← Main build orchestrator -│ ├── fetch-apps.js ← GitHub Release download + extract -│ └── inject-chrome.js ← HTML chrome injection -├── contract/ -│ ├── schema.json ← marketplace.json JSON Schema -│ ├── HEADLESS_RULES.md ← Headless HTML contract -│ └── STYLE_GUIDE.md ← Visual style requirements -├── .github/ -│ └── workflows/ -│ ├── build-deploy.yml ← Marketplace CI/CD (push to main → ECR → ECS) -│ └── validate-doc-app.yml ← Reusable validation workflow for doc repos +│ ├── build-vite.js ← Build orchestrator +│ ├── fetch-apps.js ← GitHub Release download + extract +│ └── setup-test-apps.mjs ← Generates the hermetic test apps.json +├── tests/ +│ ├── web-fragment.spec.js ← Embedded harness suite +│ ├── standalone.spec.js ← Standalone fragment-server suite +│ ├── build-integrity.spec.js +│ ├── host/server.mjs ← Reference web-fragments host (gateway) +│ ├── fragment-server.mjs ← nginx-mirroring static server +│ ├── support/fragment.js ← Shadow-DOM test helpers +│ └── fixtures/ ← Vendored docs-example dist.tar.gz +├── contract/ ← marketplace.json schema + rules + style guide +├── .github/workflows/ ← ci.yml, validate-doc-app.yml ├── Dockerfile -├── nginx.conf -└── README.md +└── nginx.conf ``` + +--- + +## Contributing & security + +See [`CONTRIBUTING.md`](CONTRIBUTING.md) and [`SECURITY.md`](SECURITY.md). + +## License + +[Apache License 2.0](LICENSE). diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..ae8d72c --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,39 @@ +# Security Policy + +## Reporting a Vulnerability + +Please **do not** open a public issue for security vulnerabilities. + +Report privately via GitHub's [private vulnerability reporting](https://github.com/AbsaOSS/knowledge-base/security/advisories/new) +(Security → Advisories → "Report a vulnerability"). If that is unavailable, email +the maintainers at **opensource@absa.africa**. + +Include where possible: +- A description of the issue and its impact +- Steps to reproduce (or a proof of concept) +- Affected version / commit +- Any suggested remediation + +We aim to acknowledge reports within **5 business days** and to provide a +remediation timeline after triage. Please allow us a reasonable period to +release a fix before any public disclosure. + +## Scope + +This project is a **build-time aggregator** that downloads pre-built static doc +artifacts and serves them via nginx. Areas of particular interest: + +- The artifact fetch pipeline (`scripts/fetch-apps.js`) — it downloads and + extracts third-party `dist.tar.gz` archives. +- The HTML transform/URL-rewriting (`src/utils/transform.js`). +- nginx response headers (`nginx.conf`). +- The reusable `validate-doc-app.yml` workflow, which runs against third-party + doc repositories. + +Note that `apps.json` (the registry of source repositories) is maintainer- +controlled; only trusted repositories should be added. + +## Supported Versions + +Security fixes are applied to the latest release on the default branch +(`master`). Older versions are not maintained. diff --git a/apps.json b/apps.json index ad7b12f..941dbc3 100644 --- a/apps.json +++ b/apps.json @@ -1,11 +1,24 @@ [ { - "repo": "AbsaOSS/knowledge-base-docs-example", - "slug": "example", - "name": "Example Docs", - "description": "A minimal template for bootstrapping a documentation project for the ABSA Knowledge base.", + "slug": "user-guide", + "name": "User Guide", + "description": "Primary docs app — the vendored docs-example fixture used as the integration guinea pig.", "icon": "book-open", - "tags": ["template", "getting-started", "example"], - "version": "latest" + "tags": [ + "guide", + "getting-started" + ], + "prebuilt": "tests/fixtures/docs-example.dist.tar.gz" + }, + { + "slug": "guide-mirror", + "name": "Guide Mirror", + "description": "Second registered app (same artifact, different slug) for cross-app navigation tests.", + "icon": "book-open", + "tags": [ + "mirror", + "cross-app" + ], + "prebuilt": "tests/fixtures/docs-example.dist.tar.gz" } ] diff --git a/playwright.config.ci.js b/playwright.config.ci.js index f823d33..e9e1342 100644 --- a/playwright.config.ci.js +++ b/playwright.config.ci.js @@ -1,16 +1,17 @@ // playwright.config.ci.js // -// Playwright configuration for CI — tests the standalone knowledge-base -// preview server (port 3000) WITHOUT requiring a running data-gateway host. +// Standalone Playwright layer — exercises the knowledge-base fragment server +// directly on port 3000 (no host gateway, no shadow DOM). Complements the full +// embedded harness in playwright.config.js. // // Run with: // npx playwright test --config=playwright.config.ci.js // -// The dist/ directory must exist before running (built by the CI build step). -// The webServer starts only `npm run preview:embedded` (no rebuild). -// -// For full integration tests (embedded inside data-gateway), use -// playwright.config.js and start data-gateway manually first. +// The fragment is served by tests/fragment-server.mjs, which mirrors the +// production nginx rewrites (including /__wf/knowledge-base/* → /knowledge-base/*). +// NB: `astro preview` is deliberately NOT used — its Vite configurePreviewServer +// rewrite hook does not run for static output, so the /__wf asset routing would +// 404. The build + serve here is hermetic (vendored fixture, no token/network). import { defineConfig, devices } from '@playwright/test'; @@ -23,21 +24,18 @@ export default defineConfig({ timeout: 20_000, use: { - // Preview server serves Astro's built output directly on port 3000. - // The wf-fragment-alias Vite plugin (astro.config.mjs) rewrites - // /__wf/knowledge-base/* → /knowledge-base/* so asset routing tests work - // without nginx. baseURL: 'http://localhost:3000', screenshot: 'only-on-failure', video: 'retain-on-failure', }, webServer: { - // dist/ must already exist — build step runs before playwright in CI. - command: 'npm run preview:embedded', + command: 'node scripts/setup-test-apps.mjs && npm run build:headless && npm run serve:test', port: 3000, reuseExistingServer: !process.env.CI, - timeout: 30_000, + timeout: 240_000, + stdout: 'pipe', + stderr: 'pipe', }, projects: [ diff --git a/playwright.config.js b/playwright.config.js index 1ba9477..f02e7dc 100644 --- a/playwright.config.js +++ b/playwright.config.js @@ -16,6 +16,10 @@ import { defineConfig, devices } from '@playwright/test'; */ export default defineConfig({ testDir: './tests', + // standalone.spec.js targets the fragment server directly on :3000 — it has its + // own config (playwright.config.ci.js). This embedded config drives the host + // gateway on :4201, so exclude it here. + testIgnore: '**/standalone.spec.js', fullyParallel: false, // fragments share DOM/history — keep navigation sequential retries: process.env.CI ? 1 : 0, workers: 1, diff --git a/scripts/setup-test-apps.mjs b/scripts/setup-test-apps.mjs index 14bbd0b..ceb6ce5 100644 --- a/scripts/setup-test-apps.mjs +++ b/scripts/setup-test-apps.mjs @@ -1,18 +1,18 @@ /** * setup-test-apps.mjs * - * Generates a hermetic apps.json for the E2E test harness. + * Regenerates the hermetic apps.json used by the E2E test harness. * - * apps.json is gitignored (it normally points at private GitHub repos), so the - * tests can't rely on a checked-in one. This script writes a registry that uses - * the local `knowledge-base-docs-example` prebuilt artifact — twice, under two - * slugs — so the suite can exercise both the landing catalog and cross-app - * navigation without any network access or per-app build toolchain. + * The committed apps.json is this script's output; the Playwright webServer runs + * it before every build so the registry stays in sync. It registers the vendored + * docs-example fixture twice (two slugs) so the suite can exercise both the + * landing catalog and cross-app navigation — no network, no GITHUB_TOKEN, no + * sibling repo or per-app build toolchain. * * The `prebuilt` field is consumed by scripts/build-vite.js (preparePrebuilt). * - * Override the example location with KB_EXAMPLE_ARTIFACT (absolute path or path - * relative to the repo root) — e.g. to point at a different doc app's dist.tar.gz. + * Override the artifact with KB_EXAMPLE_ARTIFACT (absolute path or path relative + * to the repo root) — e.g. to point at a different doc app's dist.tar.gz / dist. */ import { writeFileSync, existsSync } from 'node:fs'; @@ -22,14 +22,16 @@ import { fileURLToPath } from 'node:url'; const __dirname = dirname(fileURLToPath(import.meta.url)); const ROOT = join(__dirname, '..'); -const DEFAULT_ARTIFACT = '../knowledge-base-docs-example/dist.tar.gz'; +// Vendored fixture (committed under tests/fixtures/) keeps the build + tests fully +// hermetic — no sibling repo, no network, no GITHUB_TOKEN. +const DEFAULT_ARTIFACT = 'tests/fixtures/docs-example.dist.tar.gz'; const artifact = process.env.KB_EXAMPLE_ARTIFACT || DEFAULT_ARTIFACT; const artifactAbs = isAbsolute(artifact) ? artifact : resolve(ROOT, artifact); if (!existsSync(artifactAbs)) { console.error( `\x1b[31m✗ Example artifact not found:\x1b[0m ${artifactAbs}\n` + - ` Expected the prebuilt docs example at ../knowledge-base-docs-example/dist.tar.gz\n` + + ` Expected the vendored fixture at tests/fixtures/docs-example.dist.tar.gz\n` + ` or set KB_EXAMPLE_ARTIFACT to a dist.tar.gz / dist directory.`, ); process.exit(1); @@ -41,7 +43,7 @@ const apps = [ { slug: 'user-guide', name: 'User Guide', - description: 'Primary docs app — the knowledge-base-docs-example used as the integration guinea pig.', + description: 'Primary docs app — the vendored docs-example fixture used as the integration guinea pig.', icon: 'book-open', tags: ['guide', 'getting-started'], prebuilt: artifact, diff --git a/src/layouts/Base.astro b/src/layouts/Base.astro index 71823b9..fb1af71 100644 --- a/src/layouts/Base.astro +++ b/src/layouts/Base.astro @@ -28,7 +28,10 @@ const effectiveHeadless = headless || HEADLESS; {title} - + + diff --git a/tests/fixtures/docs-example.dist.tar.gz b/tests/fixtures/docs-example.dist.tar.gz new file mode 100644 index 0000000..fedcace Binary files /dev/null and b/tests/fixtures/docs-example.dist.tar.gz differ diff --git a/tests/standalone.spec.js b/tests/standalone.spec.js index a34e462..5ec1199 100644 --- a/tests/standalone.spec.js +++ b/tests/standalone.spec.js @@ -2,7 +2,7 @@ * tests/standalone.spec.js * * CI-runnable Playwright tests for the knowledge-base fragment server. - * Targets the standalone Astro preview (port 3000) — no data-gateway needed. + * Targets the standalone fragment server (port 3000) — no host gateway needed. * Run via playwright.config.ci.js. * * Coverage: @@ -11,13 +11,13 @@ * ─ CSS stability data-astro-transition-persist on every * (prevents web-fragments issue #297: head style accumulation) * ─ Asset routing /__wf/knowledge-base/* rewrite returns 200 - * (Astro preview plugin mirrors the nginx rewrite rule) + * (tests/fragment-server.mjs mirrors the nginx rewrite rule) * ─ Path contract all internal links are absolute /knowledge-base/* paths * ─ Client-side nav navigating between pages does NOT cause a full page reload * * Architecture under test (standalone): * Playwright → http://localhost:3000/knowledge-base/ - * (Astro preview server, no shadow DOM / no iframe) + * (tests/fragment-server.mjs serving dist/, no shadow DOM / no iframe) */ import { test, expect } from '@playwright/test'; @@ -51,8 +51,8 @@ test.describe('HTTP headers', () => { }); test('does NOT return X-Frame-Options: DENY on sub-app pages', async ({ request }) => { - const res = await request.get('/knowledge-base/example/'); - // 404 is acceptable here if the example app wasn't fetched — only check header + const res = await request.get('/knowledge-base/user-guide/'); + // 404 is acceptable here if the app wasn't built — only check the header const xfo = (res.headers()['x-frame-options'] ?? '').toUpperCase(); expect(xfo).not.toBe('DENY'); }); @@ -104,7 +104,7 @@ test.describe('Headless contract', () => { // Upstream: https://github.com/web-fragments/web-fragments/issues/297 test.describe('CSS link stability (#297)', () => { - test('all stylesheet links on the landing page have data-astro-transition-persist', async ({ page }) => { + test('landing-page stylesheet links carry data-astro-transition-persist', async ({ page }) => { await page.goto('/knowledge-base/'); await page.waitForLoadState('networkidle'); @@ -114,6 +114,11 @@ test.describe('CSS link stability (#297)', () => { for (let i = 0; i < count; i++) { const href = await links.nth(i).getAttribute('href'); + // Astro auto-injects the bundled marketplace stylesheet (/knowledge-base/styleN.css) + // from a CSS `import`; that link is framework-managed and cannot carry the attribute. + // Astro keeps it across navigation by href-match, and the behavioural + // "CSS link count does not grow" test below is the authoritative #297 guard. + if (/^\/knowledge-base\/style\d*\.css$/.test(href ?? '')) continue; const persist = await links.nth(i).getAttribute('data-astro-transition-persist'); expect( persist, @@ -126,8 +131,8 @@ test.describe('CSS link stability (#297)', () => { }); test('all stylesheet links on sub-app pages have data-astro-transition-persist', async ({ page }) => { - // Navigate to the example app landing (if available) - await page.goto('/knowledge-base/example/'); + // Navigate to a sub-app page (if available) + await page.goto('/knowledge-base/user-guide/'); // Don't fail if example isn't built — skip gracefully const status = await page.evaluate(() => document.readyState); if (await page.locator('html').getAttribute('data-mp-headless') === null) { @@ -184,9 +189,9 @@ test.describe('CSS link stability (#297)', () => { // ───────────────────────────────────────────────────────────────────────────── // // The gateway proxies fragment assets via /__wf//. -// The nginx.conf rewrite and the Astro preview wf-fragment-alias plugin both -// map /__wf/knowledge-base/ → /knowledge-base/. -// This test verifies the rewrite works in the preview server (CI proxy). +// Both the production nginx.conf and tests/fragment-server.mjs map +// /__wf/knowledge-base/ → /knowledge-base/. +// This test verifies that rewrite works in the standalone fragment server. test.describe('Asset routing', () => { test('CSS assets accessible via /__wf/knowledge-base/ prefix', async ({ page, request }) => { @@ -207,7 +212,7 @@ test.describe('Asset routing', () => { expect( res.status(), `Asset not found via /__wf prefix: ${wfPath} returned ${res.status()}.\n` + - 'Check the wf-fragment-alias plugin in astro.config.mjs and the nginx.conf rewrite rule.', + 'Check the rewrite in tests/fragment-server.mjs and the nginx.conf rule.', ).toBe(200); } }); @@ -281,7 +286,7 @@ test.describe('Client-side navigation', () => { const href = await appLink.getAttribute('href'); await appLink.click(); - await page.waitForLoadState('networkidle'); + await page.waitForURL(`**${href}**`, { timeout: 10_000 }); expect(page.url()).not.toBe(originalUrl); expect(page.url()).toContain(href);