Working agreement for automated agents and contributors making changes in this repository. It exists so a change lands green on the first CI run instead of the third.
Read CLAUDE.md for the architecture and CONTRIBUTING.md
for the human-facing summary. This file is the mechanical checklist.
CI enforces every rule in this section. .github/workflows/pr-requirements.yml
runs AbsaOSS/check-pr-requirements on every PR event, so a PR that violates a
rule is red before any code is looked at.
^(feat|feature|fix|bugfix|hotfix|release|support|chore|docs|ci|test|dependabot)/[a-zA-Z0-9._/-]+$
Branch off master. master is the only allowed target branch.
Examples: fix/build-pipeline-hardening, docs/agents-guide, chore/asset-copy.
The branch prefixes are not the same set as the commit/PR title types.
perf,refactor,style,buildandrevertare valid title types but invalid branch prefixes — aperf/…branch fails the check. Usechore/…(orfix/…) for that work and keep theperf:prefix in the title.
<type>: <summary>
Allowed types: feat, fix, docs, style, refactor, perf, test,
build, ci, chore, revert. A scope is optional (fix(build): …).
- At least 20 characters. A one-word body fails.
- Must reference an issue. Use a closing keyword so the issue is resolved on
merge:
Closes #42. Reference every issue an aggregated PR resolves, each on its own line.
Template that satisfies the checker:
## What
One paragraph on what changed and why.
## Changes
- `path/to/file.js` — what and why
## Verification
- `npm run build:headless` — green
- `npm test` — green
- `npx playwright test --config=playwright.config.ci.js` — green
Closes #42
Closes #43Maximum 50 changed files per PR. Split larger work.
Conventional Commits, same type list as PR titles. Keep the subject under ~72 characters and explain why in the body when it is not obvious from the diff.
All three must pass locally — CI runs the same three:
npm run build:headless # hermetic build
npm test # embedded web-fragment E2E
npx playwright test --config=playwright.config.ci.js # standalone fragment layerThe build and both suites are hermetic: they use the committed
tests/fixtures/ artifacts via apps.json, so no GITHUB_TOKEN, no network and
no sibling repository is required. If a change makes any of them need network,
that is the bug — fix the change, not the test.
A fourth suite needs Docker and is therefore not part of npm test:
npm run test:container # integration tests against the real nginx imageRun it when touching nginx.conf, nginx.headers.conf or the Dockerfile. It
is the only place the shipped config is executed — the other suites run against
tests/fragment-server.mjs, an Express mirror of the nginx rewrites. That mirror
is a second implementation of the same contract and it has drifted twice
(#45, #60), so a change to nginx behaviour means changing both, and proving it
with this suite. CI runs it inside the image job, which already builds the
image.
Locally it sets reuseExistingServer, so a container left running from an
earlier run answers on :8099 and the suite passes against the old dist/.
Before trusting a green run: docker ps --filter "publish=8099", remove
anything there, rebuild, then run it.
npm audit --omit=dev --audit-level=high must also stay clean; it gates CI.
apps.json is committed and CI builds from it. Entries pointing outside this
repository must carry "optional": true so a fresh clone still builds. Never
add a registry entry that requires network or a sibling checkout without it.
package-lock.json and actions/package-lock.json carry a version and an
integrity hash per package but no resolved URL: the committed .npmrc next
to each sets omit-lockfile-registry-resolved=true. npm then fetches from
whichever registry is configured at install time — registry.npmjs.org, or an
internal mirror on a private network — and the hash still pins the tarball. A
lockfile regenerated behind a corporate ~/.npmrc therefore stays portable, no
--registry flag needed. Keep both .npmrc files;
tests/private-registry.spec.js fails if either loses the setting or a
lockfile gains a registry URL.
Every page renders through src/layouts/Base.astro. Headless (web-fragment) and
standalone differ only by data-kb-headless and the shadow-DOM compat styles —
not by a different layout. Changes that add a mode-specific code path need a
strong reason.
The knowledge base has no dark mode: no theme toggle, no persisted theme, no dark
class, no dark palette. A sub-app's own theme bootstrap is removed twice:
scripts/hoist-inline-scripts.js deletes it while it is still inline, and
src/utils/transform.js strips any that survives, along with a dark body
class. Do not reintroduce any of it, and keep both halves — hoisting a bootstrap
instead of deleting it puts it beyond the reach of the transform.
Inside a web fragment the <head> belongs to reframed, which has lost, moved
and duplicated head <link>/<style> nodes across ClientRouter swaps
(web-fragments #297). So the knowledge base stylesheet is never linked from
<head>: src/layouts/Base.astro inlines it into every <body>, where it is
replaced together with the page. And every sub-app stylesheet — CSS files in
scripts/build-vite.js, inline <style> blocks in src/utils/transform.js —
is wrapped in the kb-app cascade layer, below the knowledge base's own rules,
with the masthead and the catalog (.kb-shell) fenced by all: revert in the
kb-reset layer. A sub-app sheet that outlives its page can then not restyle
them. src/utils/css-layers.js owns the order and the wrapper; keep all three
emitters of the order statement (layout head, rewritten sub-app CSS,
knowledge-base.css) in agreement, and do not reintroduce a <link> to the
knowledge base CSS — tests/build-integrity.spec.js and
tests/css-isolation.spec.js fail if you do.
Artifacts come from other repositories' releases. Treat their HTML, CSS and archive contents as attacker-controlled: no unvalidated archive extraction, no unsanitised HTML re-hosting, no shell interpolation of registry values.
The deployment serves script-src 'self' with no 'unsafe-inline'. That holds
only because no inline <script> survives into dist/ — the publishing action
emits its mermaid init as a file, and scripts/hoist-inline-scripts.js moves any
inline script found in a sub-app artifact into one. A change that introduces an
inline script fails tests/build-integrity.spec.js before it can start breaking
pages silently in production.
The check covers <script> elements. Inline on* handlers are a known gap
(#67): they are equally blocked by the policy but nothing strips or reports them.
Inline <style> is allowed (style-src keeps 'unsafe-inline') and now relied
on: the knowledge base stylesheet itself is an inline block in every body (see
above). Tightening style-src would mean hashes or nonces for it, not dropping it.
The build runs on Linux CI and on Windows developer machines. Prefer Node APIs
(cpSync, rmSync) over shelling out to cp/rm. Where tar is unavoidable,
follow the drive-letter workaround documented in scripts/artifacts.js
(stageArtifact).
The repository has no .gitattributes, so a Windows checkout is CRLF. Two
consequences: git status reports apps.json and the generated fixtures as
modified when only line endings changed — check git diff --stat, which will
show nothing, before staging — and a $-anchored regex over file text will not
match, because . does not match \r.
contract/ is the public API for onboarding repositories. Changing
schema.json, HEADLESS_RULES.md, STYLE_GUIDE.md or SINGLE_PAGE.md is a
breaking change for every doc app — call it out explicitly in the PR
description.
Every GitHub Action is pinned to a commit SHA with the version in a trailing comment. Keep it that way; Dependabot bumps them.
Grouping related issues into one PR is fine and often better than a chain of one-line PRs. When doing so:
- Group by subsystem, not by severity — reviewers read one area at a time.
- Reference every issue with its own
Closes #Nline. - Keep the diff under the 50-file limit.
- Do not mix a security fix with an unrelated refactor; a reviewer should be able to reason about the security change in isolation.