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
25 changes: 24 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,30 @@ jobs:
path: playwright-report/
retention-days: 7

# ── 4. Dependency audit ────────────────────────────────────────────────────
# ── 4. publish-single-page-docs action self-test ───────────────────────────
#
# actions/publish-single-page-docs/ ships its own pinned dependency tree, so it is not
# covered by the root `npm ci` or by the Playwright suites (which stay hermetic
# and must not depend on the action's node_modules). This job renders a sample
# markdown file through the real pipeline and pins the validation messages —
# they are the action's user interface for onboarding repos.
publish-single-page-docs:
name: publish-single-page-docs action self-test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
# Matches the node-version the composite action pins in action.yml.
node-version: '20'
cache: npm
cache-dependency-path: actions/publish-single-page-docs/package-lock.json
- run: npm ci
working-directory: actions/publish-single-page-docs
- run: npm run selftest
working-directory: actions/publish-single-page-docs

# ── 5. Dependency audit ────────────────────────────────────────────────────
audit:
name: npm audit
runs-on: ubuntu-latest
Expand Down
22 changes: 20 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,8 +72,18 @@ Orchestrator: `scripts/build-vite.js`. Flags: `--local`, `--headless`, `--path-p
- `src/components/Chrome.astro` — 56px fixed top bar (standalone mode only)
- `src/components/Masthead.astro` — Persistent Knowledge base header + Library/current-app sub-nav (all pages, both modes)
- `src/templates/chrome.js` — Inline theme script + shadow-DOM compat styles injected by the layout
- `src/utils/single-page.js` — Bundle manifest reading/validation + registry expansion, shared by both fetch paths and by Astro
- `scripts/build-vite.js` — Build orchestrator (3-step pipeline)
- `scripts/fetch-apps.js` — GitHub Release artifact downloader
- `actions/publish-single-page-docs/` — Reusable GitHub Action that turns a repo's markdown into a single-page bundle

### Three Onboarding Types

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.
- **`type: "iframe"`** — no artifact; a single route renders a full-viewport `<iframe>` for an external URL. Explicit stopgap (issue #10).
- **`type: "single-page"`** — one release artifact holding *many* docs, published by `actions/publish-single-page-docs` from plain markdown. The entry carries **no per-doc metadata** (`{ "repo": …, "type": "single-page", "version": "latest" }`); the build reads the artifact's `bundle.json` and **expands** the entry into one app per doc, extracting each into `apps/{slug}/`. The expansion is recorded in `apps/.single-page.json` and spliced back into the registry by `loadRegistry()` so Astro sees the same registry the build did. Slugs must be globally unique — `resolveRegistry()` fails the build otherwise. Rendering: masthead, no sidebar, content in a centred `main.mp-single-page` reading column. See issue #35 and `contract/SINGLE_PAGE.md`.

### Two Modes

Expand All @@ -94,9 +104,10 @@ The marketplace has no dark mode: no theme toggle, no persisted theme, no `dark`
## Contract for Doc Apps

Apps registered in `apps.json` must comply with:
- `contract/schema.json` — JSON Schema for `marketplace.json` manifest
- `contract/schema.json` — JSON Schema for `marketplace.json` manifest (packaged apps)
- `contract/HEADLESS_RULES.md` — Structural requirements (headless HTML, relative paths, `data-mp-headless` attribute)
- `contract/STYLE_GUIDE.md` — Design tokens and typography (light only — the marketplace has no dark mode)
- `contract/SINGLE_PAGE.md` — `bundle.json` format + the copy-paste onboarding workflow for single-page docs

## Testing

Expand All @@ -115,7 +126,8 @@ Self-contained Playwright E2E — `npm test` auto-starts everything (no external

Tests drive the host origin (`http://localhost:4201`). Suites (`tests/`):
- `build-integrity.spec.js` — `dist/` output: both apps enumerated, absolute URL rewriting,
headless markup, stable `dist/style.css` (the marketplace CSS the sub-app pages reference).
headless markup, stable `dist/style.css` (the marketplace CSS the sub-app pages reference),
and single-page bundle expansion (`tests/fixtures/single-page-bundle/` → two apps).
- `web-fragment.spec.js` — shadow-DOM isolation (reframed `wf-html`/`wf-body`; chrome must
not leak in), routing + smooth no-reload SPA transitions, cross-app navigation, asset
loading (no host-origin 404s), and the documented history limitation (fragment routing is
Expand All @@ -126,6 +138,12 @@ Two build-pipeline pieces support this: `apps.json` entries may carry a `prebuil
(tarball or dist dir) consumed by `scripts/build-vite.js` (`preparePrebuilt`) for hermetic
offline builds; and the build aliases the bundled marketplace CSS to a stable `dist/style.css`.

An entry may also carry `"optional": true`: the build then skips it with a warning when its
`prebuilt`/`localPath` artifact is missing, instead of failing. That is how the sibling
`knowledge-base-example-single-page` repo (a mock docs repo whose `dist.tar.gz` comes from
the real action — `npm run build:local && npm run preview` to view it) can stay registered
in the committed `apps.json` without breaking CI, which only has this repo.

## Environment Variables

- `GITHUB_TOKEN` — GitHub API auth for fetching Release artifacts
Expand Down
85 changes: 81 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,13 @@ Each entry registers one doc app. A `slug` is required; the source is one of
]
```

Add `"optional": true` to a `prebuilt`/`localPath` entry whose artifact lives
outside this repo — a sibling checkout, say. The build then **skips it with a
warning** when the artifact is absent instead of failing, so CI and fresh clones
stay green while a developer who has the sibling repo gets the app. Entries
without the flag still hard-fail on a missing artifact, so a lost fixture can
never quietly produce an empty deployment.

### iframe onboarding (temporary)

Teams that already host their docs elsewhere and can't yet produce a headless
Expand All @@ -120,9 +127,73 @@ inside the marketplace chrome and shows an **External** badge in the catalogue.
The external site must permit embedding (its CSP `frame-ancestors` /
`X-Frame-Options` must not block the marketplace origin). See issue #10.

### single-page onboarding (markdown, zero config)

Teams whose "docs" are just a markdown file or two don't need a docs site at all.
They add **one workflow file** to their repo:

```yaml
# .github/workflows/publish-docs.yml in the docs repo
on:
release:
types: [published]
workflow_dispatch:

jobs:
publish:
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v4
- uses: AbsaOSS/knowledge-base/actions/publish-single-page-docs@master
with:
docs: |
- md: docs/overview.md
title: Service Overview
description: What the service does and how to use it.
slug: my-service
```

The action ([`actions/publish-single-page-docs/`](actions/publish-single-page-docs)) renders the markdown
to headless HTML — GFM tables, task lists, footnotes, highlighted code and
vendored-mermaid diagrams — packs every doc into one `dist.tar.gz` with a
`bundle.json` manifest, and attaches it to the repo's latest release.

The registry entry then carries **no per-doc metadata at all**:

```jsonc
{
"repo": "AbsaOSS/my-service",
"type": "single-page",
"version": "latest"
}
```

The build reads `bundle.json` and expands that one entry into one app per doc —
each with its own catalogue card and URL — so adding or renaming a doc later
never touches this repository again. Pages render with the masthead and a centred
reading column, no sidebar. See [`contract/SINGLE_PAGE.md`](contract/SINGLE_PAGE.md)
and issue #35.

#### Seeing it for real

`knowledge-base-example-single-page` is a mock docs repo — three markdown files
and the workflow above, nothing else — whose `dist.tar.gz` is produced by the
real action. Check it out next to this repo and:

```bash
npm run build:local && npm run preview
```

then open <http://localhost:4321/knowledge-base/>. The committed `apps.json`
already registers it as an `optional` entry, so nothing breaks when it is absent.

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.
twice (`user-guide`, `guide-mirror`), an iframe entry, a **single-page bundle
fixture** (`platform-overview`, `release-process`), and the optional example repo
above — so the build and tests are hermetic out of the box. Replace it with your
own apps for a real deployment.

---

Expand Down Expand Up @@ -199,6 +270,10 @@ Apps must comply with the marketplace contract before they can be registered:
| [`contract/schema.json`](contract/schema.json) | JSON Schema for `marketplace.json` |
| [`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 |
| [`contract/SINGLE_PAGE.md`](contract/SINGLE_PAGE.md) | `bundle.json` format + zero-config markdown onboarding |

> The checklist and workflows below apply to **packaged** doc apps. Single-page
> docs skip all of it — the action produces a compliant artifact for you.

### Checklist
- [ ] `marketplace.json` in repo root, valid against `contract/schema.json`
Expand Down Expand Up @@ -274,6 +349,7 @@ push + ECS deploy permissions via repository secrets.
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
├── src/
│ ├── pages/
│ │ ├── index.astro ← Landing catalog
Expand All @@ -283,7 +359,8 @@ knowledge-base/
│ ├── templates/shadow-compat.js ← Shadow-DOM design-token styles
│ ├── styles/marketplace.css ← Design tokens + Tailwind
│ └── utils/
│ ├── apps.js ← getAppPages() page enumeration
│ ├── apps.js ← loadRegistry() + getAppPages() page enumeration
│ ├── single-page.js ← bundle.json parsing + registry expansion
│ └── transform.js ← URL rewriting + sub-app document splitting
├── scripts/
│ ├── build-vite.js ← Build orchestrator
Expand All @@ -296,7 +373,7 @@ knowledge-base/
│ ├── 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
│ └── fixtures/ ← Vendored docs-example dist.tar.gz + single-page bundle
├── contract/ ← marketplace.json schema + rules + style guide
├── .github/workflows/ ← ci.yml, validate-doc-app.yml
├── Dockerfile
Expand Down
49 changes: 49 additions & 0 deletions actions/publish-single-page-docs/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# `publish-single-page-docs` action

Renders plain markdown files into headless single-page docs and attaches them to
your repository's latest GitHub Release as `dist.tar.gz`, ready for the AbsaOSS
knowledge base to pick up.

```yaml
- uses: AbsaOSS/knowledge-base/actions/publish-single-page-docs@master
with:
docs: |
- md: docs/overview.md
title: Service Overview
description: What the service does and how to use it.
slug: my-service
```

**Full documentation — inputs, `bundle.json`, the copy-paste workflow and
troubleshooting — lives in [`contract/SINGLE_PAGE.md`](../../contract/SINGLE_PAGE.md).**

---

## Layout

| Path | Role |
|---|---|
| `action.yml` | Composite action: setup-node → `npm ci` → render → `gh release upload --clobber` |
| `src/index.js` | Entry point. Reads inputs from env, writes step outputs. |
| `src/inputs.js` | Parses and validates the `docs` list. Every message names the entry and the fix. |
| `src/markdown.js` | markdown-it pipeline: GFM, highlight.js, mermaid passthrough. |
| `src/template.js` | The headless document shell and `assets/doc.css`. **Dependency-free** — also imported by the knowledge base's test fixture generator, so the fixture cannot drift from real output. |
| `src/bundle.js` | Stages the doc directories, writes `bundle.json`, packs the tarball. |
| `src/selftest.js` | `npm run selftest` — renders a sample end to end and pins the validation messages. |

Dependencies are pinned and vendored here (`package.json` + `package-lock.json`)
rather than in the repository root, so the action stays self-contained and an
onboarding repo needs no toolchain of its own.

## Working on it

```bash
cd actions/publish-single-page-docs
npm ci
npm run selftest
```

The self-test is deliberately **not** part of the knowledge base's Playwright
suite: that suite must stay hermetic and must not depend on this action's
`node_modules`. The marketplace side of the feature is covered there instead, via
the `tests/fixtures/single-page-bundle/` fixture.
98 changes: 98 additions & 0 deletions actions/publish-single-page-docs/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
name: Publish docs to the knowledge base
description: >-
Render plain markdown files into headless single-page docs and publish them as
a dist.tar.gz bundle on this repository's latest GitHub Release, ready for the
AbsaOSS knowledge base to pick up.
author: AbsaOSS

branding:
icon: book-open
color: purple

inputs:
docs:
description: >-
YAML (or JSON) list of doc definitions. Each entry takes `md` (path to a
markdown file, relative to the repository root), `title`, `description`
and `slug` (lowercase kebab-case), plus optional `icon` and `tags`.
required: true
release-tag:
description: >-
Release to attach the bundle to. Defaults to the release that triggered the
workflow, falling back to the repository's latest release.
required: false
default: ${{ github.event.release.tag_name }}
github-token:
description: Token used to upload the release asset. Needs `contents: write`.
required: false
default: ${{ github.token }}

outputs:
slugs:
description: Comma-separated list of the slugs that were published.
value: ${{ steps.build.outputs.slugs }}
count:
description: Number of docs in the published bundle.
value: ${{ steps.build.outputs.count }}
artifact:
description: Absolute path of the packed dist.tar.gz.
value: ${{ steps.build.outputs.artifact }}
release-tag:
description: Tag of the release the bundle was uploaded to.
value: ${{ steps.upload.outputs.release-tag }}

runs:
using: composite
steps:
# The action ships its own pinned dependency tree so onboarding repos need no
# toolchain, no package.json and no lockfile of their own.
- name: Set up Node.js
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: '20'

- name: Install publisher dependencies
shell: bash
working-directory: ${{ github.action_path }}
run: npm ci --omit=dev --no-audit --no-fund

- name: Render markdown and pack the bundle
id: build
shell: bash
run: node "$GITHUB_ACTION_PATH/src/index.js"
env:
KB_DOCS: ${{ inputs.docs }}
KB_WORKSPACE: ${{ github.workspace }}
KB_STAGE: ${{ runner.temp }}/kb-single-page/stage
KB_ARTIFACT: ${{ runner.temp }}/kb-single-page/dist.tar.gz

- name: Upload dist.tar.gz to the release
id: upload
shell: bash
env:
GH_TOKEN: ${{ inputs.github-token }}
KB_RELEASE_TAG: ${{ inputs.release-tag }}
KB_ARTIFACT: ${{ steps.build.outputs.artifact }}
run: |
set -euo pipefail

tag="${KB_RELEASE_TAG:-}"

# /releases/latest excludes drafts and pre-releases, so fall back to the
# most recently created release of any kind before giving up.
if [ -z "$tag" ]; then
tag="$(gh api "repos/$GITHUB_REPOSITORY/releases/latest" --jq '.tag_name' 2>/dev/null || true)"
fi
if [ -z "$tag" ] || [ "$tag" = "null" ]; then
tag="$(gh api "repos/$GITHUB_REPOSITORY/releases?per_page=1" --jq '.[0].tag_name // empty' 2>/dev/null || true)"
fi
if [ -z "$tag" ] || [ "$tag" = "null" ]; then
echo "::error::$GITHUB_REPOSITORY has no GitHub Release to attach the docs bundle to. Create a release (any tag) and re-run, or pass release-tag explicitly."
exit 1
fi

echo "Uploading $(basename "$KB_ARTIFACT") to release $tag (replacing any existing asset)…"
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"
Loading