Reference for cutting a release and for editing the changelog. Read this file
when you need it — it is deliberately kept out of AGENTS.md so it doesn't
occupy context in every session.
AGENTS.md keeps only the two guardrails that must always be in force:
- Never edit
constants/changelog.tsunless the user explicitly asks. - Pushing
mainwith"easBuild": trueships to the App Store.
Everything below is the how.
The in-app "What's New" panel reads CHANGELOG (newest first) from
constants/changelog.ts.
You never edit this file unless the user explicitly asks you to. No speculative entries, no "while I'm here" additions, no entry per edit. When the user does ask, edit it as the last step before the commit — one pass covering everything in the session, not one entry per change.
All work now branches off main and returns by PR (develop and preview are
retired — see AGENTS.md). Versions in the changelog are therefore always real;
there is no .TESTFLIGHT placeholder flow any more.
Compare package.json's version to CHANGELOG[0].version:
- They DIFFER (changelog ahead) → an unreleased entry is already open. Append to it. Don't create an entry, don't change its version.
- They're EQUAL → the top entry is already released. Add ONE new entry at
the top with a patch bump only and today's date. Don't touch
package.json— the release process owns the app version, unless the user explicitly asks you to bump it.
Patch numbers are always two digits (3.8.05, never 3.8.5).
Ignore semver instinct. You never decide major vs. minor vs. patch. The only
version edit you ever make is a single patch bump in the EQUAL case on main.
A new feature does not mean a minor bump.
Use sections, not the legacy flat changes[]. Up to three sections, in this
order, omitting any that would be empty:
- New Features — user-visible capabilities and UI additions
- Bugs Fixed — corrections to broken or incorrect behavior
- Maintenance — polish, copy clarifications, reliability work users can feel
{
version: 'x.y.zz',
date: 'YYYY-MM-DD',
sections: [
{ title: 'New Features', items: ['…'] },
{ title: 'Bugs Fixed', items: ['…'] },
{ title: 'Maintenance', items: ['…'] },
],
}Older releases still use flat changes?: string[] — leave them alone. The What's
New screen renders sections when present and falls back to changes.
Keep every line short. One plain sentence written for a user — not a bug report. State what changed, and when it happens only if genuinely necessary. Skip the mechanism, the root cause, and the edge cases. If a line needs a semicolon or an em-dash aside to fit everything in, it's too long.
✅ "Fixed torrent speed limits sometimes applying the wrong units" ❌ A paragraph on the KiB/s-vs-bytes/s bug, which server versions it affected, and how the fix compares to other limit fields.
Deduplicate. If a line already covers your change, don't add a near-duplicate — fold the variant into the existing line.
No entry for internal-only work — docs, config, refactors, tests, tooling,
AGENTS.md itself.
Two GitHub Actions workflows, both in .github/workflows/.
Triggered by a push to main that touches package.json. The gate is the
"easBuild": true flag in package.json, not the version number. When it's
true the workflow: creates a GitHub Release (notes generated by parsing
constants/changelog.ts for the current version) → eas build --profile production → eas submit to App Store Connect.
This means a push to main with that flag set ships to the App Store. Treat
package.json on main as a release trigger, not just metadata.
Version plumbing:
app.config.jssetsversion: packageJson.version—package.jsonis the single source of truth for the marketing version.- Build numbers are not in git and not set by hand in Xcode.
eas.jsonusesappVersionSource: "remote"withautoIncrementon thepreviewandproductionprofiles, so EAS owns them. - OTA updates go out through EAS Update (
expo-updates), withruntimeVersion.policy: "appVersion"— an OTA update only reaches builds sharing its marketing version. Channels:development,preview,production.
Runs on push to main. Executes jest --coverage, then force-pushes a single
badge.json to the orphan coverage branch, which the README badge reads
through shields.io's endpoint type. That branch holds nothing else — don't treat
it as a real branch.