diff --git a/.github/workflows/site.yml b/.github/workflows/site.yml index 8fe87d8c..cdf8a6cf 100644 --- a/.github/workflows/site.yml +++ b/.github/workflows/site.yml @@ -30,6 +30,11 @@ on: # The docs pin the released CLI version; a release bump must re-run that # guard in the same push rather than failing the next unrelated change. - 'package.json' + # The decision portal at /decisions/ is built by the committed Action + # bundle and checked by this script; a change to either must re-run it. + - 'packages/ci/portal/**' + - 'packages/ci/dist/portal-action.js' + - 'scripts/check-portal-bundle.ts' workflow_dispatch: # Least-privilege baseline. Pages deploy privileges are scoped to the deploy job. @@ -44,6 +49,17 @@ concurrency: jobs: build: runs-on: ubuntu-latest + # Read by the deploy job, which turns the run red after deploying when a + # step that is non-blocking on `main` failed (see the portal and rendered + # steps below). On pull requests those steps fail this job directly. + outputs: + portal_failed: >- + ${{ steps.portal_route.outcome == 'failure' || + steps.portal.outcome == 'failure' || + steps.portal_check.outcome == 'failure' }} + rendered_failed: >- + ${{ steps.rendered.outcome == 'failure' || + steps.rendered_self_test.outcome == 'failure' }} # This job now drives a browser, which is the first step here that can hang # rather than error. The concurrency group never cancels, so an unbounded # hang would freeze every later deploy invisibly. @@ -119,6 +135,62 @@ jobs: - name: Build site run: bun run build + # This repository's own decision portal, served at adrkit.dev/decisions/ + # as ADR-0054's rung-2 dogfood (its action item 8). It is built by the + # nested Action from this checkout, so it runs the committed + # `packages/ci/dist/portal-action.js` exactly as a consumer pinning a + # release does. `uses:` ignores the job's `working-directory`; the Action + # resolves `dir` and `out` against the workspace. + # + # It writes into `site/dist` after `astro build`, not into `site/public` + # before it. Astro empties `dist` when it builds, so nothing stale + # survives; the portal is a directory the Action replaces whole and no + # site page reads, unlike the badge files, which `astro dev` must serve; + # and Astro has finished, so the guard below can prove no Starlight route + # already owns `/decisions/` before the Action replaces it. No `base-url`: + # the page needs none, and building without one is what shows the bundle + # works at a subpath unchanged (ADR-0054 clause 8). + # + # The visibility gate passes on `push` and `workflow_dispatch` because + # this repository is public and both payloads carry `repository` with + # `private: false`. Never set `publish-private` here. + # + # Exit 1 (corpus errors) succeeds with a warning and publishes, as the + # badge step does (clause 9). A refusal or exit 2 writes nothing and fails + # the step; on `main` that is non-blocking for the reason stated above the + # rendered checks: a missing portal must not withhold the canonical schema + # at its `$id` (ADR-0011). The site then deploys without `/decisions/` + # (or with an unchecked one), and the deploy job turns the run red after + # deploying. + - name: Guard — no site route owns /decisions/ + id: portal_route + continue-on-error: ${{ github.event_name != 'pull_request' }} + run: | + if [ -e dist/decisions ]; then + echo "::error::site/dist/decisions exists after astro build; a site page claims /decisions/, which the portal would replace" + exit 1 + fi + + - name: Build the decision portal + id: portal + if: steps.portal_route.outcome == 'success' + continue-on-error: ${{ github.event_name != 'pull_request' }} + uses: ./packages/ci/portal + with: + dir: docs/adr + out: site/dist/decisions + + # The Action exiting 0 says it wrote something. This checks what: the page + # files, `portal-data.json` at `version: "1"`, as many records as + # `adr lint` checked in the badge step above, and no asset reference that + # would break at a subpath. Its unit test sabotages each assertion. + - name: Check the decision portal bundle + id: portal_check + if: steps.portal.outcome == 'success' + continue-on-error: ${{ github.event_name != 'pull_request' }} + working-directory: . + run: bun run scripts/check-portal-bundle.ts --bundle site/dist/decisions --lint site/public/lint.json + # `astro build` exiting 0 says the pages compile; it says nothing about how # they render. Every defect fixed in #150 — a rule that silently lost a # specificity contest, a badge at 1.1:1, a page title ~180px out of line — @@ -140,8 +212,10 @@ jobs: # the canonical schema at its `$id` (ADR-0011). Withholding the artifact # would leave adrkit.dev serving the *previous* schema bytes to every # `$ref` consumer because a colour moved. The run still goes red and - # annotates, so the signal survives; only the veto over the schema is - # removed. + # annotates, from the deploy job after `deploy-pages`, so the signal + # survives; only the veto over the schema is removed. A failing step in + # this job would fail the job, skip the upload, and through + # `needs: build` skip the deploy, which is exactly the veto. - name: Guard — the rendered checks can still fail id: rendered_self_test continue-on-error: ${{ github.event_name != 'pull_request' }} @@ -152,15 +226,6 @@ jobs: continue-on-error: ${{ github.event_name != 'pull_request' }} run: bun run check:rendered - - name: Annotate a rendered failure that did not block the deploy - if: >- - github.event_name != 'pull_request' && - (steps.rendered.outcome == 'failure' || - steps.rendered_self_test.outcome == 'failure') - run: | - echo "::error title=Rendered check failed on main::The site deployed anyway so the canonical schema at its \$id stays current (ADR-0011). Fix the rendered regression, or revert — see site/DEPLOYMENT.md." - exit 1 - # Only publish an artifact for real deployments (push to main / manual run). - name: Upload Pages artifact if: github.event_name != 'pull_request' @@ -186,3 +251,20 @@ jobs: - name: Deploy to GitHub Pages id: deployment uses: actions/deploy-pages@368f82528645a54fb793d4d04e342629a3f51346 # v5.0.1 + + # After the deploy, not in the build job: a failing step there fails the + # job, and `needs: build` would then withhold the deploy, schema included. + # The second step uses `always()` (still only after a successful + # deploy) so that a rendered failure, which fails this job first, does + # not hide a portal failure. + - name: Annotate a rendered failure that did not block the deploy + if: needs.build.outputs.rendered_failed == 'true' + run: | + echo "::error title=Rendered check failed on main::The site deployed anyway so the canonical schema at its \$id stays current (ADR-0011). Fix the rendered regression, or revert — see site/DEPLOYMENT.md." + exit 1 + + - name: Annotate a portal failure that did not block the deploy + if: always() && needs.build.outputs.portal_failed == 'true' && steps.deployment.outcome == 'success' + run: | + echo "::error title=Decision portal failed on main::The site deployed anyway so the canonical schema at its \$id stays current (ADR-0011); /decisions/ is missing or unchecked. Fix it, or revert — see site/DEPLOYMENT.md." + exit 1 diff --git a/AGENTS.md b/AGENTS.md index 0d278033..85e72cd5 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1126,7 +1126,9 @@ nothing else: no token, no API call, `contents: read`. The consumer workflow deploys with `upload-pages-artifact` and `deploy-pages` in a separate job; the documented workflow is in `site/src/content/docs/ci.mdx`. **Rung 1**: unit, contract, and mutation tests, and a Node run of the committed bundle against a -git fixture. Load-bearing: +git fixture. `site.yml` builds this repository's corpus with it into +`/decisions/` on adrkit.dev and checks the bundle (`scripts/check-portal-bundle.ts`); +that is rung 2 only once a deploy has been observed serving it. Load-bearing: - **Logic is `src/portal-action.ts`, wiring is the entrypoint.** Only `portal-action-entrypoint.ts` imports `@actions/*`, as for the queue Action, diff --git a/CHANGELOG.md b/CHANGELOG.md index 1e8c217b..7e562662 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,18 @@ Until `1.0.0`, minor releases may include breaking changes Starlight 0.42's reworked mobile menu, which is meant to open without JavaScript. +### Added + +- adrkit.dev serves this repository's own decision portal at + [`/decisions/`](https://adrkit.dev/decisions/), built on every site deploy by + the nested portal Action from the same checkout, and linked from the + decision-records index and the CI guide. A new check fails the site build + when the bundle is missing a page file, `portal-data.json` is not version + `"1"`, its record count differs from `adr lint`'s, or an asset reference + would break under a subpath + ([ADR-0054](docs/adr/0054-publish-a-static-decision-portal-for-github-pages-built-outside-the-cli-from-the.md) + action item 8, #294). + ## [0.19.0] - 2026-10-10 This release ships the decision portal: a third, build-only nested Action, diff --git a/scripts/check-portal-bundle.test.ts b/scripts/check-portal-bundle.test.ts new file mode 100644 index 00000000..9dbb42c6 --- /dev/null +++ b/scripts/check-portal-bundle.test.ts @@ -0,0 +1,104 @@ +import { describe, expect, test } from 'bun:test'; +import { inspectBundle, lintRecordCount, REQUIRED_FILES } from './check-portal-bundle.ts'; + +const HTML = + '' + + 'RecordsGraph'; + +function data(overrides: Record = {}): string { + return JSON.stringify({ version: '1', commit: 'abc', records: [{ id: '0001' }, { id: '0002' }], issues: [], ...overrides }); +} + +function bundle(files: Record = {}) { + const all: Record = { + 'index.html': HTML, + 'app.css': 'body{}', + 'page/app.mjs': 'export {};', + 'vendor/board-layout.mjs': 'export {};', + 'vendor/board-view.mjs': 'export {};', + 'portal-data.json': data(), + ...files, + }; + return (rel: string) => all[rel] ?? null; +} + +describe('inspectBundle', () => { + test('accepts a complete version-1 bundle with the expected record count', () => { + const report = inspectBundle(bundle(), 2); + expect(report.problems).toEqual([]); + expect(report.warnings).toEqual([]); + expect(report.summary).toBe('version "1", 2 record(s), abc'); + }); + + test('fails on a missing directory: every required file is reported', () => { + expect(inspectBundle(() => null, 2).problems).toEqual(REQUIRED_FILES.map((f) => `${f} is missing from the bundle`)); + }); + + test.each(REQUIRED_FILES.map((f) => [f]))('fails when %s is missing', (file) => { + expect(inspectBundle(bundle({ [file]: null }), 2).problems).toContain(`${file} is missing from the bundle`); + }); + + test('fails on a data file that does not parse', () => { + expect(inspectBundle(bundle({ 'portal-data.json': '{' }), 2).problems).toEqual(['portal-data.json does not parse as JSON']); + }); + + test.each([['2'], [1], [null], [undefined]])('fails on version %p', (version) => { + expect(inspectBundle(bundle({ 'portal-data.json': data({ version }) }), 2).problems[0]).toContain('expected "1"'); + }); + + test('fails on a record count that differs from adr lint', () => { + expect(inspectBundle(bundle(), 3).problems).toEqual(['portal-data.json has 2 record(s); adr lint checked 3']); + }); + + test('fails when there is no lint count to compare against', () => { + expect(inspectBundle(bundle(), null).problems[0]).toContain('no record count from adr lint'); + }); + + test('fails on a data file with no records array', () => { + expect(inspectBundle(bundle({ 'portal-data.json': data({ records: {} }) }), 2).problems).toEqual([ + 'portal-data.json has no records array', + ]); + }); + + // Clause 9: an exit-1 corpus still publishes, and a record that does not parse + // is counted by lint but cannot be a portal record. + test('downgrades a count mismatch to a warning when the corpus has error findings', () => { + const errored = data({ issues: [{ code: 'frontmatter-parse', severity: 'error', message: 'x', path: 'docs/adr/0003-x.md' }] }); + const report = inspectBundle(bundle({ 'portal-data.json': errored }), 3); + expect(report.problems).toEqual([]); + expect(report.warnings).toHaveLength(1); + }); + + test('a warn-severity issue does not excuse a count mismatch', () => { + const warned = data({ issues: [{ code: 'x', severity: 'warn', message: 'x', path: null }] }); + expect(inspectBundle(bundle({ 'portal-data.json': warned }), 3).problems).toHaveLength(1); + }); + + // Clause 8: the bundle must load at /decisions/, not only at a domain root. + test.each([['/app.css'], ['//cdn.example/app.css'], ['https://adrkit.dev/decisions/app.css']])( + 'fails on a non-relative asset reference %p', + (href) => { + const html = HTML.replace('./app.css', href); + expect(inspectBundle(bundle({ 'index.html': html }), 2).problems).toEqual([ + `index.html references "${href}", which is not relative or a hash route; it breaks at a subpath`, + ]); + }, + ); +}); + +test('accepts a bare relative reference', () => { + expect(inspectBundle(bundle({ 'index.html': HTML.replace('./app.css', 'app.css') }), 2).problems).toEqual([]); +}); + +describe('lintRecordCount', () => { + test('reads the integer checked field', () => { + expect(lintRecordCount('{"checked":59,"findings":[]}')).toBe(59); + }); + + test.each([[null], ['not json'], ['{}'], ['{"checked":true}'], ['{"checked":"59"}'], ['{"checked":-1}'], ['{"checked":1.5}'], ['null']])( + 'returns null for %p', + (text) => { + expect(lintRecordCount(text)).toBeNull(); + }, + ); +}); diff --git a/scripts/check-portal-bundle.ts b/scripts/check-portal-bundle.ts new file mode 100644 index 00000000..d970ced0 --- /dev/null +++ b/scripts/check-portal-bundle.ts @@ -0,0 +1,157 @@ +/** + * Fail when the decision portal that `site.yml` builds for adrkit.dev is not the + * bundle it claims to be + * ([ADR-0054](../docs/adr/0054-publish-a-static-decision-portal-for-github-pages-built-outside-the-cli-from-the.md) + * action item 8, the rung-2 dogfood). + * + * The portal Action exiting 0 says it wrote something into `out`. It does not say + * the page files are there, that the data file is the version-1 contract, that it + * carries this repository's records, or that the page still loads from a subpath. + * This checks those four things against the built directory, before upload: + * + * 1. `index.html`, `app.css`, `page/app.mjs`, and `portal-data.json` exist. + * 2. `portal-data.json` parses and its `version` is exactly `"1"`. + * 3. Its `records` count equals the record count `adr lint --json` reported for + * the same corpus in the same job (`checked`). The CLI is the oracle rather + * than a filename glob here, because a glob would restate core's record + * grammar, which is the drift `check:site-grammar` exists to stop. When the + * data file carries an error-severity issue, a mismatch is a warning: an + * exit-1 corpus still publishes (clause 9), and a record that does not parse + * can be counted by lint and absent from the portal. + * 4. No `src` or `href` in `index.html` is root-absolute (`/…`), scheme-relative + * (`//…`), or absolute (`https:…`). Such a reference loads at a domain root + * and breaks at `/decisions/` (clause 8); relative paths and hash routes + * (`#/…`) work at any path. + * + * Imports only Node builtins. + * + * bun run scripts/check-portal-bundle.ts --bundle site/dist/decisions --lint site/public/lint.json + */ + +import { readFileSync } from 'node:fs'; +import { join } from 'node:path'; + +/** Files the page needs to load; a bundle missing any of them renders nothing. */ +// page/app.mjs imports the two vendor modules statically (packages/portal/src/page/app.mjs); +// if either is missing the browser aborts the page module and the portal renders nothing. +export const REQUIRED_FILES = [ + 'index.html', + 'app.css', + 'page/app.mjs', + 'vendor/board-layout.mjs', + 'vendor/board-view.mjs', + 'portal-data.json', +] as const; + +export interface BundleReport { + readonly problems: string[]; + readonly warnings: string[]; + /** One line for the log when there are no problems. */ + readonly summary: string; +} + +/** `read(rel)` returns the file's text, or `null` when it does not exist. */ +export type ReadFile = (relativePath: string) => string | null; + +const ATTRIBUTE = /\b(?:src|href)\s*=\s*"([^"]*)"/gi; +/** `https:`, `data:`, `javascript:`…: anything that names a scheme is not a relative reference. */ +const SCHEME = /^[a-z][a-z0-9+.-]*:/i; + +/** Pure: the problems and warnings for one bundle, given the CLI's record count. */ +export function inspectBundle(read: ReadFile, expectedRecords: number | null): BundleReport { + const problems: string[] = []; + const warnings: string[] = []; + + const contents = new Map(); + for (const file of REQUIRED_FILES) { + const text = read(file); + if (text === null) problems.push(`${file} is missing from the bundle`); + else contents.set(file, text); + } + + const html = contents.get('index.html'); + if (html !== undefined) { + for (const match of html.matchAll(ATTRIBUTE)) { + const value = match[1] as string; + if (value.startsWith('/') || SCHEME.test(value)) { + problems.push(`index.html references "${value}", which is not relative or a hash route; it breaks at a subpath`); + } + } + } + + let summary = ''; + const raw = contents.get('portal-data.json'); + if (raw !== undefined) { + let data: unknown; + try { + data = JSON.parse(raw); + } catch { + problems.push('portal-data.json does not parse as JSON'); + } + if (data !== undefined) { + const record = (data ?? {}) as { version?: unknown; records?: unknown; issues?: unknown; commit?: unknown }; + if (record.version !== '1') { + problems.push(`portal-data.json has version ${JSON.stringify(record.version)}, expected "1"`); + } + if (!Array.isArray(record.records)) { + problems.push('portal-data.json has no records array'); + } else if (expectedRecords === null) { + problems.push('no record count from adr lint to compare against; the corpus report was not published'); + } else if (record.records.length !== expectedRecords) { + const hasErrors = + Array.isArray(record.issues) && + record.issues.some((issue) => (issue as { severity?: unknown } | null)?.severity === 'error'); + const message = `portal-data.json has ${record.records.length} record(s); adr lint checked ${expectedRecords}`; + if (hasErrors) warnings.push(`${message} (the corpus has error findings, so a record can be absent)`); + else problems.push(message); + } + if (Array.isArray(record.records)) { + const commit = typeof record.commit === 'string' ? record.commit : 'no commit'; + summary = `version ${JSON.stringify(record.version)}, ${record.records.length} record(s), ${commit}`; + } + } + } + + return { problems, warnings, summary }; +} + +/** The integer `checked` of an `adr lint --json` report, or `null`. */ +export function lintRecordCount(text: string | null): number | null { + if (text === null) return null; + try { + const checked = (JSON.parse(text) as { checked?: unknown } | null)?.checked; + return typeof checked === 'number' && Number.isSafeInteger(checked) && checked >= 0 ? checked : null; + } catch { + return null; + } +} + +function readOrNull(path: string): string | null { + try { + return readFileSync(path, 'utf8'); + } catch { + return null; + } +} + +function argument(name: string): string { + const index = process.argv.indexOf(name); + const value = index === -1 ? undefined : process.argv[index + 1]; + if (value === undefined || value.startsWith('--')) { + console.error(`check-portal-bundle: ${name} is required`); + process.exit(2); + } + return value; +} + +if (import.meta.main) { + const bundle = argument('--bundle'); + const lint = argument('--lint'); + const report = inspectBundle((rel) => readOrNull(join(bundle, rel)), lintRecordCount(readOrNull(lint))); + for (const warning of report.warnings) console.log(`::warning::check-portal-bundle: ${warning}`); + if (report.problems.length > 0) { + for (const problem of report.problems) console.error(`check-portal-bundle: ${problem}`); + process.exit(1); + } + console.log(`check-portal-bundle: ok — ${bundle}: ${report.summary}`); +} diff --git a/site/DEPLOYMENT.md b/site/DEPLOYMENT.md index ba2982b5..850956a0 100644 --- a/site/DEPLOYMENT.md +++ b/site/DEPLOYMENT.md @@ -37,6 +37,25 @@ committed under `schema/versions/` (ADR-0011 action item 6). When `SCHEMA_VERSION` moves, copy the outgoing `schema/adr.schema.json` into `schema/versions/v/` in the same change; the planner refuses a canonical version that would repoint a retained one. +- **Decision portal:** after `astro build`, the nested portal Action + (`uses: ./packages/ci/portal`, so the committed + `packages/ci/dist/portal-action.js` runs exactly as a consumer's pinned copy + does) builds this repository's `docs/adr` into `dist/decisions/`, served at + `https://adrkit.dev/decisions/` + ([ADR-0054](../docs/adr/0054-publish-a-static-decision-portal-for-github-pages-built-outside-the-cli-from-the.md), + action item 8). It goes into `dist` after the build rather than `public` + before it because Astro empties `dist` when it builds, and a guard step fails + if a site page already produced `dist/decisions/`, which the Action would + replace. Assets are relative and routes are hash routes (`#/adr/0054`), so no + `base-url` is passed. `scripts/check-portal-bundle.ts` then checks the page + files, `portal-data.json` at `version: "1"`, its record count against the + `checked` count in `lint.json`, and that no asset reference is absolute. A + corpus with error findings (exit 1) still publishes, with a banner. On pull + requests a portal failure fails the build; on `main` it does not withhold the + schema deploy: the site deploys without (or with an unchecked) `/decisions/` + and the deploy job turns the run red afterwards. Because that step runs in + the `github-pages` environment's job, the Deployments page then shows the + deployment as failed even though the site is live. - **Custom domain persistence:** [`public/CNAME`](./public/CNAME) contains `adrkit.dev`, so the custom domain survives every deploy. @@ -120,6 +139,10 @@ curl -s https://adrkit.dev/schema/adr/v0.2.0/adr.schema.json | head -3 curl -s https://adrkit.dev/schema/adr/v0.2.0/adr.schema.json \ | grep '"$id"' +# The decision portal loads its data file from its own subpath +curl -s https://adrkit.dev/decisions/portal-data.json \ + | python3 -c 'import json,sys; d=json.load(sys.stdin); print(d["version"], d["commit"], len(d["records"]))' + # A retained prior version still serves its published bytes curl -s https://adrkit.dev/schema/adr/v0.1.0/adr.schema.json | shasum -a 256 # 1e1841151174cc5a8ed22dadae070f087477e5068bd928d5292c3acd2e2681cc @@ -147,8 +170,14 @@ git push origin HEAD:refs/heads/main # if you have push access to main required status checks, bypassable by repository role), so if a direct push is rejected, open the revert as a pull request and merge it normally. Note that the site `build` job is **not** in `main`'s required-checks list, so a red rendered -check does not block the merge — but `deploy` needs `build`, so it does hold the -publish. +check does not block the merge. On `main` a failed rendered check (or portal +step) does not hold the publish either: those steps are `continue-on-error` +there, the build job passes their outcomes to the deploy job as outputs, and the +deploy job reports them with an `exit 1` only after `deploy-pages` has +published. The run is red and the `github-pages` deployment shows as failed +while the new site, schema included, is live. A failure in any other build step +(install, schema sync, `astro build`) still fails `build`, and `deploy` needs +`build`, so that does hold the publish. Two things the timing depends on, neither obvious: @@ -162,7 +191,8 @@ Two things the timing depends on, neither obvious: If the build job itself is red for an environment reason — no browser on the runner, a `playwright-core` or Chrome bump — the rendered steps are already -non-blocking on `main` and annotate instead, so the deploy proceeds. Do not +non-blocking on `main`: the deploy proceeds and the deploy job annotates +afterwards. On pull requests they fail the build directly. Do not delete the steps to unblock a release; that removes the check silently and nothing records it. @@ -175,6 +205,8 @@ on the ref it ran on*. Deploying an older ref therefore also republishes: - an **older schema** at the `$id` that other tools fetch over HTTP, and - **stale badge projections**, which degrade to plausible-but-wrong numbers rather than to an error. +- a **stale decision portal** at `/decisions/`, built from the older corpus + and by the older portal Action. Neither failure announces itself. `check:schema` compares the served copy against the canonical file *within the deployed ref*, so it passes on an old ref by diff --git a/site/scripts/gen-adr-pages.ts b/site/scripts/gen-adr-pages.ts index a744e98a..14b41490 100644 --- a/site/scripts/gen-adr-pages.ts +++ b/site/scripts/gen-adr-pages.ts @@ -344,7 +344,7 @@ function renderIndex(docs: AdrDoc[]): string { .join('\n'); const yaml = stringifyYaml(frontmatter).trimEnd(); - return `---\n${yaml}\n---\n\nadrkit dogfoods its own format: every decision in this project is a typed\ndecision record in [\`docs/adr/\`](${GITHUB_TREE}).\nThese pages are generated from those source files — the records themselves are\nnever edited to fit this site.\n\n| # | Decision | Status |\n| --- | --- | --- |\n${rows}\n\nSee the [ADR template](/adr/0000-template/) for the copy-ready starting point.\n`; + return `---\n${yaml}\n---\n\nadrkit dogfoods its own format: every decision in this project is a typed\ndecision record in [\`docs/adr/\`](${GITHUB_TREE}).\nThese pages are generated from those source files — the records themselves are\nnever edited to fit this site.\n\nThe same corpus is also served as a [decision portal](/decisions/): the\nsupersession graph, the review queue, and which decisions govern each path,\nbuilt by adrkit's own portal Action on every deploy.\n\n| # | Decision | Status |\n| --- | --- | --- |\n${rows}\n\nSee the [ADR template](/adr/0000-template/) for the copy-ready starting point.\n`; } function main(): void { diff --git a/site/src/content/docs/ci.mdx b/site/src/content/docs/ci.mdx index 92f51101..0f0e81e4 100644 --- a/site/src/content/docs/ci.mdx +++ b/site/src/content/docs/ci.mdx @@ -252,6 +252,11 @@ So a corpus with error findings still publishes, and says so on every page, while a corpus that cannot be read publishes nothing and leaves the last deployed site in place. +adrkit builds its own corpus this way on every deploy of this site, from the +committed Action in the same checkout, and serves it at +[adrkit.dev/decisions/](/decisions/). The bundle needs no `base-url` to work +under a subpath: its assets are relative and its routes are hash routes. +