Bundul
Internal
Browse docs
Waiting for review

explanation

How a change ships

Written by the build · 2 September 2026 · owner @farhan-s · reviewed 2026-09-02

How a change ships

From typing code to a customer reading about it. One page, end to end.

The shape of it

Four things move, and they move at different speeds:

What it is Updated when By whom
Reference docs docs/generated/ — jobs, env vars, API, permissions, data model every merge a script, never a person
Internal changelog CHANGELOG.md every release release-please
Internal docs site docs.bundul.io every docs change CI, mirroring this repo
Customer changelog the public page when a person publishes a person, from your one sentence

The customer one is deliberately the only one a human gates.

Step by step

1. You open a pull request

You do four things, and nothing else:

# What Why it matters
1 Write the PR title as a conventional commit The type decides the version bump and which changelog section it lands in
2 Tick one Customer impact box This alone decides whether a customer note exists, and when
3 Write one plain sentence for customers It becomes the first draft of the release note
4 Add the headline label, only if it deserves a push and an email Without it, a feature is a normal improvement
feat(payments): add dunning emails for failed collection

Customer impact: live-on-release
Customer summary: If a payment fails, we now email you straight
                  away with what to do.

Write that sentence now, not later. It takes fifteen seconds while the context is in your head, and it is archaeology in three weeks.

2. CI runs

Check Blocks?
PR title is a conventional commit yes
Typecheck, production code yes
ESLint, on the lines you changed only yes
docs/generated/ matches the code yes
Smoke: the built entrypoint exists and the app boots and answers /health yes
Backend ↔ mobile contract reports
Vocabulary drift — a live doc explaining a system the code no longer has yes
Jest no — 31 known failures

If the docs check fails, run pnpm docs:generate and commit. CI will not do it for you. That is deliberate: a bot committing to your branch is confusing, and the regenerated file is something you should look at before it ships.

3. You merge

Squash-merge, so the PR title becomes the commit message. Then, automatically:

  • dev deploys
  • docs/generated/ is re-verified
  • if docs/ changed, the docs are pushed into the docs store, which is what the docs app reads
  • release-please opens or updates a release PR carrying the version bump and the written changelog

4. You cut a release

Merge the release PR. That tags the version and publishes a GitHub Release.

Production does not deploy. Nothing automatic deploys production — not a merge, not a release, not a tag. You ship it yourself:

Actions → Deploy to Production → Run workflow → pick the tag → Run

Pick the tag release-please made, so what ships is exactly what the changelog describes. Re-running against an older tag is a rollback.

5. The customer note

Only if you ticked something other than none or silent.

Add one markdown file to apps/public/content/changelog/ in release-notes-and-docs, seeded from the sentence you wrote in step 1. Open a PR, look at the preview, merge. That one file becomes the page, the permanent URL, the RSS item and the JSON the app reads.

What determines what

Everything downstream is derived from these. Nothing else is consulted.

Input Decides
PR title type (feat, fix, perf, chore…) version bump; changelog section; whether it is customer-facing at all
Customer impact field whether a note is written, and when it can be published
Customer summary sentence what the note says
headline label push and email, or page only
files you touched which generated pages change
covers: frontmatter which hand-written docs get flagged as drifted
contract check whether a backend feature is reachable by the app yet
tools/docs/watched-terms.json which systems are checked for vocabulary drift

The Customer impact field, in full

You tick What happens When it publishes
none Internal only. No note is ever written never
live-on-release Works on the app customers already have as soon as you ship
needs-app: <ver> Needs a mobile release that has not happened held until that version ships
silent Customers are affected but we are not announcing it — security fixes, pricing changes never on the page

Unsure? Pick needs-app: TBD. A held note costs nothing. A note published before customers can use the thing costs support tickets.

What generates the content

Content Generated by From
docs/generated/*.md tools/docs/generate.ts the code itself — @Cron sites, the GraphQL schema, EmailTemplates, the push catalog, admin-permissions.ts, @Schema classes, .env.example, task definitions
CHANGELOG.md release-please commit messages since the last tag
version, tag, GitHub Release release-please the conventional commit types
internal docs site tools/sync-internal-docs.mjs then a static build this repo's docs/, mirrored — nothing is authored there
customer changelog entry you, seeded by your Customer summary one markdown file
the customer page, RSS and app feed the site build that same one file

Reference docs are never written by hand. If one is wrong, the generator is wrong — fix tools/docs/generate.ts, not the page.

Three worked examples

A fix nobody outside will notice

fix(trueup): stop reconcile force-failing live transactions · impact none.

Merge → dev deploys, generated docs unchanged, release-please adds a line under Fixed. Ship when you like. No customer note is written at all. Your cost: one checkbox.

A backend change customers feel immediately

feat(payments): add dunning emails for failed collection · impact live-on-release.

The generator notices four new entries in the EmailTemplates map, so docs/generated/customer-emails.md changes and you commit it. Internal docs site rebuilds. After you ship, you add one markdown file to the changelog. Tier is Improvement, so it goes on the page and in the feed, and no push fires.

A feature that needs the app to catch up

feat(utility-insights): bill history and price-change alerts · impact needs-app: TBD.

Internal docs update in full — new API entries, new push notifications, new required env vars. You ship the backend. The contract check reports the app cannot route the new deeplink yet.

So the customer note is written now and held. Open it as a draft PR on the changelog repo. Later releases add to that same draft. When the app ships the screen and the contract check goes green, fill in minAppVersion, merge, and announce — at the moment customers can actually use it.

That is not hypothetical: utility bill insights is in exactly this state today. The backend sends a push whose deeplink the app does not handle, so it is deliberately absent from the changelog.

What is automated, and what is not

Being honest about this, because the diagram in your head is probably tidier than the system.

Step Today
Commit format enforced automatic — local hook, plus CI on the PR title
Reference docs generated automatic to check; you run pnpm docs:generate to update
Version, tag, GitHub Release, CHANGELOG.md automatic
Dev deploy automatic
Production deploy manual, on purpose
Docs pushed to the store automatic, once DOCS_SYNC_SECRET is set
Documentation updated after a code change AI proposes, a person approves. Only when the vocabulary check finds real drift
Customer note drafted from your sentence AI drafts, a person publishes. It also recommends whether to publish at all
Customer note published manual, on purpose
Editing a written doc in the docs app — no pull request, no git
Push / email announcement not built — the page and feed are the only channels today
In-app What's New not built — the feed it reads exists; the mobile sheet does not

The two rules worth remembering

Nothing customer-facing publishes itself. Not a merge, not a release. A person writes the sentence and a person merges the note.

Nothing is announced that customers cannot reach. If the app cannot get to it, the note waits. The contract check is what tells you.