AI agent skill
Release
Cut a Lotti release — assemble the changelog.d/ fragments into CHANGELOG.md and the Flathub metainfo, bump the version, open the release PR, then tag. Use when asked to cut or prepare a release, assemble the changelog, publish a new version, or work out what is unreleased.
·
When to use this skill
Use Release when an AI agent needs a reusable SKILL.md workflow for this job: Cut a Lotti release — assemble the changelog.d/ fragments into CHANGELOG.md and the Flathub metainfo, bump the version, open the release PR, then tag. Use when asked to cut or prepare a release, assemble the changelog, publish a new version, or work out what is unreleased.
When not to use it
Skip Release when the task is outside the coding category, or when a more specific skill in this directory already covers the same workflow with clearer triggers.
How to install
- Personal install: create ~/.claude/skills/release/SKILL.md (and any bundled scripts) so Claude Code, Claude Desktop, and compatible agents can load it in every project.
- Project install: commit the same folder at .claude/skills/release/ so teammates get the skill with the repo.
- Restart the agent session after copying files so it re-scans the skills directory, then ask for the task in words that match the skill description.
What this skill does
# Cut a release
Release notes are written per change as **fragments** in `changelog.d/`, one new file per pull request, and assembled once per release. A release is therefore a small, boring, self-contained pull request: it turns the fragments into prose in two files, moves the version, and deletes what it consumed.
**This is the only change allowed to touch `CHANGELOG.md`, `flatpak/com.matthiasn.lotti.metainfo.xml`, or the `version:` line in `pubspec.yaml`.** That rule is the whole point — those three files are written at the top by everything, so as long as exactly one pull request per release writes them, nothing can conflict. If you are here for any other reason, you are in the wrong skill; write a fragment instead (`changelog.d/README.md`).
## Before you start
```bash git checkout main && git pull ls changelog.d/[0-9]*.md # the work queue; README.md is not a fragment fvm dart run tool/changelog/validate.dart --strict ```
`--strict` rather than `make changelog_check`: the house-style warnings the author-facing check only prints — an entry with no bold headline, a paragraph pasted as one long line — are about to become published prose, so here they block.
- Work from an up-to-date `main`. A release assembled on a stale branch drops whatever merged in the meantime. - If `changelog.d/` holds nothing but its README, there is nothing to release. Say so and stop. - Fix any malformed fragment here, in the release branch. It is cheaper than sending the author back. - A fragment describing something that was reverted before it shipped gets deleted, not published.
**Ask before committing, pushing or tagging.** Every step below writes files; the git operations are the user's call, always.
## 1. Read every fragment
Read them all before writing anything. You are producing one coherent set of release notes, not a concatenation: two fragments may describe two halves of the same user-visible change and should be merged into one entry, and a later fragment may supersede an earlier one.
## 2. Choose the version
```bash grep -m1 '^version:' pubspec.yaml # e.g. version: 1.0.13+4352 ```
- **Bug fixes alone:** keep the semantic version unchanged and increment only the build number (`1.0.21+4362` → `1.0.21+4363`). A fix-only release appends its notes to the current top changelog section and metainfo release block; it must not create a duplicate `## [1.0.21]` heading or `<release version="1.0.21">` block. - **User-facing additions or changes:** increment the patch version and the build number (`1.0.21+4362` → `1.0.22+4363`). Any publishable `Added`, `Changed`, `Deprecated`, or `Removed` entry makes this a user-facing release. - **Explicit minor release:** increment the minor version and reset the patch component to zero only when the user explicitly requests a minor release (`1.0.21+4362` → `1.1.0+4363`). The build number still increments normally. - **Build number:** always previous + 1. It only has to be unique and increasing per tag — every release lane triggers on tag push, not on merges to `main` — so it moves once per release, not once per pull request.
If the user named a version, use it. Otherwise propose one from what the fragments contain and confirm it.
## 3. Assemble the CHANGELOG section
For a user-facing release, insert one new section at the top of `CHANGELOG.md`, directly under the Keep a Changelog preamble and above the previous version:
```markdown ## [1.0.22] ### Added - **...**
### Changed - **...**
### Fixed - **...** ```
- **One section per type, in this order**: Added, Changed, Deprecated, Removed, Fixed, Security. Skip the ones with no entries. Never repeat a heading inside a version — older sections do, because entries used to be appended one PR at a time; assembling from fragments is what fixes that. - **The version heading carries no date.** That is the file's existing shape. - Keep each entry's wording as the fragment wrote it, minus edits for duplication, house voice, or an entry that reads oddly next to its neighbours. - Wrap at about 78 columns, continuation lines indented two spaces.
For a fix-only build release, add the new bullets to the current top semantic version instead. Reuse its existing `### Fixed` heading, or create that heading in the normal section order if it does not have one. Never create a second version section or a second `### Fixed` heading.
## 4. Write the Flathub release block
For a user-facing release, add one `<release>` to `flatpak/com.matthiasn.lotti.metainfo.xml`, as the **first** child of `<releases>` — newest first:
```xml <release version="1.0.22" date="2026-08-25"> <description> <p>Fixed: ...</p> </description> </release> ```
This is the same prose, mechanically transformed. AppStream renders `<p>` as plain text: markdown does not survive it, and an unescaped `&` breaks the build.
For a fix-only build release, append the new `Fixed:` paragraphs to the description of the existing first `<release>` block and update that block's date to the day the build is cut. Do not create a duplicate release block for the unchanged semantic version.
| In `CHANGELOG.md` | In the metainfo | |---|---| | one `- ` bullet | one `<p>`, same order as the section | | the `### Fixed` heading above it | the prefix `Fixed: ` on every paragraph in that section | | `- **Bold headline.** Detail…` | `Fixed: bold headline. Detail…` — bold markers dropped, first letter lowercased unless it is a proper noun (`Settings`, `Flathub`) | | hard-wrapped over several lines | one long line | | `—` or `–` | ` - ` | | `` `code` `` or a glyph like `•••` | plain words a user recognises — `"..."`, `the "Location with the name CEST doesn't exist" error` | | `&`, `<`, `>` | `&`, `<`, `>` |
Worked example, from 1.0.13:
```markdown - **Daily habit reminders arrived hours late on iPhone, iPad and Android.** The app could not work out which timezone the device was in, quietly settled for UTC, and then built the reminder's time of day in UTC — so an 08:00 habit reminder rang at 10:00 in Central European Summer Time. ```
```xml <p>Fixed: daily habit reminders arrived hours late on iPhone, iPad and Android. The app could not work out which timezone the device was in, quietly settled for UTC, and then built the reminder's time of day in UTC - so an 08:00 habit reminder rang at 10:00 in Central European Summer Time.</p> ```
The `date` is the day the release is cut, `YYYY-MM-DD`.
## 5. Bump the version
One line in `pubspec.yaml`:
```yaml version: 1.0.22+4363 ```
## 6. Delete the fragments you consumed
```bash git rm changelog.d/[0-9]*.md # fragments start with their date; README.md does not ```
Everything you just published goes. `changelog.d/` holding only its README is what "nothing unreleased" looks like; git history keeps the originals.
## 7. Verify
```bash make changelog_check # versions now agree xmllint --noout flatpak/com.matthiasn.lotti.metainfo.xml # if available git status --short ```
`git status` must show exactly four kinds of change and nothing else: `CHANGELOG.md`, the metainfo, `pubspec.yaml`, and deleted fragments. A release PR that also carries code is a release PR that can conflict.
Re-read the assembled section once as a user would. It is the text that reaches Flathub, the GitHub release and the app's What's New — it is read far more often than it is written.
## 8. Open the release pull request
Use the semantic version in the title for a user-facing release:
``` chore(release): 1.0.22 ```
Use the full version and build for a fix-only release so it is distinguishable from the release that first introduced that semantic version:
``` chore(release): 1.0.21+4363 ```
Body: the entries assembled for this release, grouped under their release headings, so review reads the new notes rather than the diff. For a fix-only release, label the body with the full version and build even though the changelog heading remains the unchanged semantic version. No screenshots — nothing visual changed.
## 9. Tag, after it merges
```bash git checkout main && git pull make tag_push # tags 1.0.22+4363 from pubspec.yaml and pushes it ```
`make tag_push` reads the version with `yq`; install it (`brew install yq`) or tag by hand with the exact `version+build` string if the target reports it missing.
The tag is what ships. Pushing it triggers, in parallel: macOS and iOS TestFlight, the Android release, the Linux GitHub release, the macOS signed `.dmg`, and the Flathub submission PR against `flathub/com.matthiasn.lotti` — which is why the metainfo has to be right *before* the tag, not after.
On Google Play the release tag stops at internal testing. Moving that build on is a separate tag from the same commit, once the internal upload has finished: `make android_closed_testing` for closed testing, `make android_release` for production. Both promote the build Play already has rather than rebuilding it; the mechanism is the *Google Play tracks* section of [`knowledge/architecture/platform-and-release.md`](../../../knowledge/architecture/platform-and-release.md).
Confirm with the user before tagging. Watch the lanes; `docs/macos-release.md` and `docs/flatpak-flathub-recovery.md` cover the two that fail in interesting ways.
## 10. Downstream, and out of scope here
The app's **What's New** modal reads from the separate `matthiasn/lotti-docs` repository (`whats-new/`), not from this one. Publishing there is an editorial step in that repo — mention it once the release is tagged; do not attempt it from here.
## Gotchas
- **The metainfo lagging a version behind is invisible until Flathub ships it.** `make changelog_check` fails on exactly that, which is why it runs in CI on every push rather than only at release time. - **Tags carry the build number** (`1.0.14+4353`), and every release lane keys on tag push. Re-tagging the same build number to fix a mistake will not give you a fresh TestFlight build; bump the build number and cut again. - **Do not fold anything else into the release PR** — not a "quick fix", not a dependency bump. The moment it carries code it can conflict, and the reason this flow exists is gone.
Intended uses
- Use Release when this documented workflow matches the task.
Related skills
Related skills in this directory, for comparison before you install another skill.
coding
Act as a Patient, Non-Technical Android Studio Guide
A reusable prompt for asking an AI assistant to work as Act as a Patient, Non-Technical Android Studio Guide.
coding
Add Ave Record
The main workflow for this repo. Adds one new AVE record end to end.
coding
Add Backend
Guide for adding a backend (Rust or Python) to the agent-sec-core security middleware. Use when creating new backends, integrating Rust or Python code into the security middleware, or extending with new backend actions.
coding
Agent Device
Drive iOS and Android devices for the Expensify App - testing, debugging, performance profiling, bug reproduction, and feature verification. Use when the developer needs to interact with the mobile app on a device.