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
6 changes: 4 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -201,10 +201,12 @@ jobs:
run: ./scripts/check-module-deps.sh
- name: Check config key sync (src reads vs config jsons, CONFIG.md, helm env lists)
run: ./scripts/check-config-sync.sh
- name: Check changelog fragment format (changelog.d/ — fragments stay optional)
run: ./scripts/assemble-changelog.sh --check

gate-selftest:
runs-on: ubuntu-latest
# Proves the eight check-* gates BITE: per gate, copy the subtree it reads,
# Proves the nine gate scripts BITE: per gate, copy the subtree it reads,
# plant a known breakage (counted mutators; a no-op mutation is itself a
# failure), and require the gate to fail AND name what it found — after
# first PASSING the untouched copy as a control. A gate only ever run
Expand Down Expand Up @@ -260,7 +262,7 @@ jobs:
curl -fsSL https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | bash
sudo curl -fsSL -o /usr/local/bin/yq https://github.com/mikefarah/yq/releases/download/v4.44.6/yq_linux_amd64
sudo chmod +x /usr/local/bin/yq
- name: Gate selftest (18 planted breakages must be caught)
- name: Gate selftest (20 planted breakages must be caught)
if: steps.scope.outputs.relevant == 'true'
run: ./scripts/check-selftest.sh

Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/gates-nightly.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ jobs:
curl -fsSL https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | bash
sudo curl -fsSL -o /usr/local/bin/yq https://github.com/mikefarah/yq/releases/download/v4.44.6/yq_linux_amd64
sudo chmod +x /usr/local/bin/yq
- name: Gate selftest (18 planted breakages must be caught)
- name: Gate selftest (20 planted breakages must be caught)
run: ./scripts/check-selftest.sh
- name: Module dependency DAG gate
run: ./scripts/check-module-deps.sh
Expand Down
22 changes: 16 additions & 6 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,17 @@ map of all documentation; `docs/CONVENTIONS.md` is the pattern reference.
wiring): `./scripts/new-module.sh <name>`
- Migration: `make new-migration SLUG=<slug>`
- React page: `./scripts/new-react-page.sh`
- Release: `./scripts/release.sh <ver>` — after the human retitles the
CHANGELOG `[Unreleased]` heading, bumps every other version point in one
command (CMakeLists + 9 helm tag pins + 4 Chart.yaml appVersions +
- Changelog entry: drop a fragment `changelog.d/<topic>.<type>.md` (type ∈
added|changed|fixed|removed|security; bullet text WITHOUT the leading
`- `, format: `changelog.d/README.md`) — parallel-safe, two PRs never
conflict; editing `[Unreleased]` directly stays legal.
`./scripts/assemble-changelog.sh --check` gates fragment FORMAT in CI
(fragments themselves are optional).
- Release: `./scripts/release.sh <ver>` — folds `changelog.d/` fragments
into `[Unreleased]` (assemble-changelog.sh), retitles it to
`## [<ver>] — date` with a fresh empty `[Unreleased]` on top (a heading
retitled by hand is accepted too), then bumps every other version point
in one command (CMakeLists + 9 helm tag pins + 4 Chart.yaml appVersions +
`.template-version`); commits nothing (CONTRIBUTING.md "Release")
- Fork→template sync: `./scripts/sync-upstream.sh` — run IN a fork; pulls
template fixes by three-way patching between release tarballs (base =
Expand Down Expand Up @@ -98,9 +106,11 @@ gates by construction. Hand-rolled versions usually don't.
2. `./scripts/check-openapi-drift.sh && ./scripts/check-routes-registered.sh
&& ./scripts/check-test-buckets.sh && ./scripts/check-version-sync.sh
&& ./scripts/check-frontend-nginx-sync.sh && ./scripts/check-module-deps.sh
&& ./scripts/check-config-sync.sh` — seconds, no build.
Touched a `check-*` script? Also run `./scripts/check-selftest.sh` —
plants 19 breakages and requires every gate to catch and name them
&& ./scripts/check-config-sync.sh && ./scripts/assemble-changelog.sh --check`
— seconds, no build.
Touched a `check-*` script (or assemble-changelog.sh)? Also run
`./scripts/check-selftest.sh` —
plants 20 breakages and requires every gate to catch and name them
(needs helm+yq; in CI `gate-selftest` self-scopes to diffs touching
`scripts/`, `helm/` or `.github/workflows/`, with a nightly
unconditional backstop in `.github/workflows/gates-nightly.yml`)
Expand Down
45 changes: 35 additions & 10 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,28 @@ Closes #123
Types: `feat`, `fix`, `refactor`, `perf`, `docs`, `test`, `build`, `ci`,
`chore`. Scope is optional but recommended (e.g. `feat(auth):`, `fix(jobs):`).

## Changelog

`CHANGELOG.md` follows Keep a Changelog. Two ways to record a change — the
fragment is preferred, because it cannot conflict:

- **Fragment (preferred):** add one file `changelog.d/<topic>.<type>.md`,
where `<topic>` is your branch name or a short slug and `<type>` is one of
`added` / `changed` / `fixed` / `removed` / `security`. The file holds the
bullet text as ONE markdown paragraph, WITHOUT the leading `- ` (the
assembler adds it). Two parallel PRs then touch two different files
instead of the same `[Unreleased]` lines — the changelog stops being the
most-conflicted file in every integration. Format details:
`changelog.d/README.md`. CI validates fragment *format*
(`./scripts/assemble-changelog.sh --check`) but never *requires* a
fragment — PRs with nothing changelog-worthy stay clean.
- **Direct edit (legal):** append under `## [Unreleased]` in `CHANGELOG.md`
as before. Fine for cross-cutting PRs; expect merge conflicts there.

Fragments are folded into `[Unreleased]` (and deleted) at release time by
`scripts/release.sh`, or any time by a manual
`./scripts/assemble-changelog.sh`.

## Code style

- C++20. Header-only modules under `src/`, single `main.cpp` per binary.
Expand Down Expand Up @@ -112,18 +134,21 @@ Semver, tagged on `master`. The release version lives in more places than
one edit can reach by hand (that is how three releases shipped with a stale
`project(VERSION …)`), so use the script:

1. In `CHANGELOG.md`, retitle `## [Unreleased]` to `## [<ver>] — YYYY-MM-DD`
and add a fresh empty `## [Unreleased]` above it. The content stays
human-written.
2. `./scripts/release.sh <ver>` — validates semver + monotonicity, then bumps
every remaining version point in one command: `CMakeLists.txt
project(VERSION …)`, the 9 image-tag pins in
1. `./scripts/release.sh <ver>` — validates semver + monotonicity, then:
folds any `changelog.d/` fragments into `## [Unreleased]`
(`assemble-changelog.sh`, deleting the consumed fragments), retitles that
section to `## [<ver>] — YYYY-MM-DD` keeping a fresh empty
`## [Unreleased]` on top (a heading you already retitled by hand is
accepted too; an empty `[Unreleased]` with no fragments aborts — write
the notes first), and bumps every remaining version point in one command:
`CMakeLists.txt project(VERSION …)`, the 9 image-tag pins in
`helm/cpp-env/values{,-demo,-stage}.yaml`, and the 4 `Chart.yaml`
`appVersion` fields. It re-runs `scripts/check-version-sync.sh` (which
gates all of those in CI) and prints the diff. It commits nothing.
3. Commit changelog + bumps as ONE commit:
`chore(release): <ver> — <one-line summary>`.
4. After it lands on `master`, `git tag v<ver> && git push origin v<ver>`.
gates all of those in CI) and prints the diff. It commits nothing; the
changelog *content* stays human-written — the script only moves it.
2. Review `git diff`, then commit changelog + fragment deletions + bumps as
ONE commit: `chore(release): <ver> — <one-line summary>`.
3. After it lands on `master`, `git tag v<ver> && git push origin v<ver>`.
A tag matching `v*.*.*` triggers `.github/workflows/release.yml`, which
builds the app, worker and frontend images, pushes them to
`ghcr.io/<owner>/<repo>` (and `-worker` / `-frontend`) with the
Expand Down
34 changes: 34 additions & 0 deletions changelog.d/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# changelog.d — changelog fragments

`CHANGELOG.md`'s `[Unreleased]` section is the most conflict-prone file in
the repo: every PR appends to the same few lines. Instead of editing it,
drop ONE file per change here:

<topic>.<type>.md

- `<topic>` — your branch name or a short slug (`fix-login-429`,
`orgs-kit`). It only keeps file names unique across parallel PRs; it
never appears in the changelog.
- `<type>` — one of `added`, `changed`, `fixed`, `removed`, `security`:
the Keep-a-Changelog section the bullet lands in.

The file content is the bullet text: ONE markdown paragraph, WITHOUT the
leading `- ` — the assembler adds the prefix and indents continuation
lines. Backticks, links and inline markdown are fine.

Example — `changelog.d/wallet-topup.added.md`:

PayPal checkout top-ups credited to the append-only wallet ledger
(`src/billing/Wallet.hpp`), idempotent under webhook redelivery.

At release time `scripts/release.sh` (or a manual
`./scripts/assemble-changelog.sh`) folds every fragment into
`## [Unreleased]` — creating the `### Added` / `### Changed` / …
subsections in Keep-a-Changelog order when missing — and deletes the
consumed files; the deletions are committed with the release.

`./scripts/assemble-changelog.sh --check` validates fragment format
without touching anything; CI runs it in the quick gate job. Fragments are
OPTIONAL: editing `[Unreleased]` directly remains legal (a fragment is
preferred because it cannot conflict). This README is ignored by the
assembler.
7 changes: 7 additions & 0 deletions changelog.d/changelog-fragments.added.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
Changelog fragments: a PR now records its changelog entry as its own file
`changelog.d/<topic>.<type>.md` (type ∈ added|changed|fixed|removed|security,
format: `changelog.d/README.md`) instead of competing for the same
`[Unreleased]` lines — `scripts/release.sh` folds the fragments in and
retitles `[Unreleased]` itself, `./scripts/assemble-changelog.sh --check`
gates fragment format in CI (fragments stay optional, direct `[Unreleased]`
edits remain legal), and the gate selftest grows to 20 planted breakages.
7 changes: 4 additions & 3 deletions docs/INDEX.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ question instead of grepping the tree.
| [`../README.md`](../README.md) | Getting started, what's in the box, quickstart, repo layout |
| [`../CONTRIBUTING.md`](../CONTRIBUTING.md) | Pre-commit setup, dev workflow, commit-message convention, release flow |
| [`../SECURITY.md`](../SECURITY.md) | Disclosure policy + production-hardening checklist |
| [`../CHANGELOG.md`](../CHANGELOG.md) | Versioned change log (semver, edited under `## [Unreleased]`) |
| [`../CHANGELOG.md`](../CHANGELOG.md) | Versioned change log (semver; authored via `changelog.d/` fragments — parallel-safe, see [`../changelog.d/README.md`](../changelog.d/README.md) — or directly under `## [Unreleased]`) |
| [`../REMOVING-THE-DEMO.md`](../REMOVING-THE-DEMO.md) | What's reference-only (flask-base) vs the real app, and how to strip it (`init-project.sh --no-demo`) |

## Worked examples & deep-dives
Expand Down Expand Up @@ -104,15 +104,16 @@ question instead of grepping the tree.
| `check-routes-registered.sh` | Verify every controller ADD_METHOD_TO route is in `Api::get_endpoints()` (symmetric to the OpenAPI drift check) |
| `check-test-buckets.sh` | Verify test suites sit in the right bucket — classified by DIRECTORY, fails on a suite-name clash across unit/integration |
| `check-version-sync.sh` | Verify `project(VERSION …)` in CMakeLists.txt, the 9 helm image-tag pins (`helm/cpp-env/values{,-demo,-stage}.yaml`) and the 4 `Chart.yaml` `appVersion` fields all match the newest released CHANGELOG heading (`[Unreleased]` on top stays green) |
| `release.sh` | Bump every version point of a release in one command (CMakeLists + 9 helm tag pins + 4 appVersions) after the human retitles the CHANGELOG heading; verifies with check-version-sync.sh, commits nothing |
| `release.sh` | One-command release: folds `changelog.d/` fragments into `[Unreleased]`, retitles it to `## [<ver>] — date` (accepts a hand-retitled heading too), bumps every version point (CMakeLists + 9 helm tag pins + 4 appVersions); verifies with check-version-sync.sh, commits nothing |
| `assemble-changelog.sh` | Fold `changelog.d/<topic>.<type>.md` fragments into CHANGELOG's `[Unreleased]` (creates `###` sections in Keep-a-Changelog order, deletes consumed fragments); `--check` validates fragment format only — CI runs it, fragments themselves stay optional |
| `check-frontend-nginx-sync.sh` | Verify `frontend/nginx.conf` and the helm cpp-frontend ConfigMap agree on the proxied backend routes |
| `check-module-deps.sh` | Verify every cross-directory `#include` in `src/` is an edge declared in `docs/module-deps.txt`; hard-forbids `utils -> *`, non-entry-point includes of `core/Core.hpp`, and `webhooks -> email` |
| `check-config-sync.sh` | Verify every config read in `src/` (`cfg.get<T>("path", "ENV", …)` + getenv) exists in `config/config.json` AND `config/config.sample.json` and is documented in `docs/CONFIG.md`; flags stale doc rows and stale helm env lines (exceptions: `docs/config-sync-allowlist.txt`) |
| `check-helm-render.sh` | Render the cpp-env umbrella with CI values and assert deploy-path invariants (ports, hosts, empty credential defaults) |
| `check-artifact.py` | Content + leaked-template-syntax gate over ONE rendered artifact's extracted text (opt-in, for document-rendering forks — see docs/RENDER-GATE.md) |
| `render-artifacts.sh` | Loop for the opt-in artifact gate: render every `templates/render/*/fixtures/*.json` via `RENDER_CMD`/`EXTRACT_CMD`, then gate with `check-artifact.py`; zero fixtures = failure |
| `check-artifact-selftest.sh` | Mandatory selftest for the artifact gate: healthy example (incl. hostile data fixture) must PASS, two counted mutations must be caught and named |
| `check-selftest.sh` | Prove all eight `check-*` gates bite: plant 18 known breakages in scratch copies, require each gate to fail AND name it (control-pass first, counted mutators, a no-op mutation is itself a failure; needs helm+yq) |
| `check-selftest.sh` | Prove all nine gate scripts bite (the eight `check-*` gates + `assemble-changelog.sh --check`): plant 20 known breakages in scratch copies, require each gate to fail AND name it (control-pass first, counted mutators, a no-op mutation is itself a failure; needs helm+yq) |
| `prod-check.sh` | Pre-deploy assertions on a production config (auth, secrets, TLS, fail-closed) |
| `lint-openapi.sh` | Spectral lint with project ruleset |
| `make-jwt.sh` | Mint a dev HS256 JWT (no Python/Node deps) |
Expand Down
Loading
Loading