Thanks for considering a contribution. This guide covers the setup, the conventions that are easy to miss, and how to get a change reviewed quickly.
Prerequisites: Node.js >= 24, pnpm 9.15+, Docker with Compose v2, and Go 1.25+
(only if you build packages/cel-evaluator outside Docker).
pnpm install
pnpm run build
pnpm run test # unit tests, no Docker requiredIntegration tests need the stack running:
cd deploy && cp .env.example .env && cd ..
docker compose -f deploy/docker-compose.yaml -f deploy/docker-compose.test.yaml up -d --wait
pnpm run test:integrationThe docker-compose.test.yaml override mounts a fixtures pack and sets
SEED_TENANT=default so seeded reference data is readable through the API.
Three suites exercise behaviour the default stack cannot show, because dev mode runs allow-all stubs. Each owns the Docker lifecycle — it tears the stack down and reboots it in a non-default mode on the same ports — so run them one at a time, never alongside the standard suite:
SECURITY_E2E=1 pnpm --filter @openfoundry/integration-tests exec vitest run security-enforcement
CAPABILITY_E2E=1 pnpm --filter @openfoundry/integration-tests exec vitest run capability-gating
CONSENT_VOCAB_E2E=1 pnpm --filter @openfoundry/integration-tests exec vitest run open-consent-vocabThey leave the stack down; bring the dev stack back up before running
anything else. CI runs all three in a dedicated enforcement-e2e job.
These are the ones reviewers actually raise.
Fail closed. Security-relevant configuration is opt-in, never defaulted to a
permissive value. If an environment variable is unset, the platform should refuse
or deny rather than pick a convenient default. For example OIDC_DEFAULT_TENANT
is passed through unset so a token without a tenant claim is rejected, rather
than defaulting every claimless token into a shared tenant. A change that makes
something permissive by default will be asked to invert.
Blank env vars are unset. Compose and Helm pass unset knobs through as empty
strings, so process.env.X ?? 'default' does not catch them. Use
process.env.X?.trim() || 'default' when a fallback is intended.
Domain pack permission overrides replace the whole type block. A pack's
permissions/*.fga does not merge relation-by-relation, so an override must
re-declare every relation the runtime checks — chiefly viewer on each object
type and can_<verb> for each action. Boot validation enforces this: a missing
viewer stops production startup. See
docs/external-domain-packs.md.
Don't hardcode counts in docs. Test counts and line counts decay silently. Describe coverage qualitatively.
No NHS-specific defaults in platform code. Healthcare behaviour is opt-in through domain packs, capabilities, and deployment policy env vars. Platform defaults stay generic.
- Branch from
main. - Keep commits focused; the body should explain why, not restate the diff.
- Add or update tests. A bug fix should include a test that fails without it — say so in the PR, since it is the strongest signal a fix is real.
- Update docs in the same PR when behaviour or configuration changes.
Before pushing:
pnpm run build && pnpm run typecheck && pnpm run testtypecheck covers test files that build excludes, so run both.
Conventional-commit style: fix(api):, feat(odl):, docs(packs):,
test(integration):, chore(deps):. Please don't add Co-Authored-By or other
attribution trailers.
-
Choose the next version using SemVer. Before 1.0, breaking changes require a minor release; compatible fixes and dependency maintenance use a patch.
-
In one
chore(release): X.Y.Zcommit, move the relevant changelog entries out ofUnreleasedand align the version inpackage.json, the chartversion/appVersion, the default chart image tag, and the install example indeploy/README.md. Rebuild and regenerate the checked-in API contracts:pnpm run build pnpm --filter @openfoundry/api spec:all
-
Land that commit on
main, then verify the full CI workflow succeeded for its exact SHA. Do not infer this from the latest run:sha=$(git rev-parse HEAD) gh run list --workflow CI --commit "$sha" --branch main --event push \ --json databaseId,headSha,status,conclusion
-
Create an annotated
vX.Y.Ztag on that verified commit and push only the tag. The release workflow repeats the exact-SHA CI check before publishing.
The workflow refuses tags whose commit is not on main or whose tag, package,
chart, image default, changelog, and install example disagree. It then builds
the release assets from the checked-in version, verifies that the generated
OpenAPI and AsyncAPI contracts advertise that version, creates a draft, attests
the artifacts, and makes the release visible only after provenance succeeds.
Open an issue using the template. The three fields that most often decide whether
a report is actionable are NODE_ENV, the loaded DOMAIN_PACKS, and the
gateway boot log — dev mode disables enforcement entirely, so a behaviour that
looks like a bug in one mode is often expected in the other.
For security vulnerabilities, do not open a public issue — see
SECURITY.md.
Maintainers aim to respond within a week. Expect questions about fail-closed behaviour and test coverage on anything touching auth, authorisation, or consent; that scrutiny is the point of the project, not a reflection on the contribution.
By contributing you agree your work is licensed under Apache 2.0.