8.4 KiB
| name | description |
|---|---|
| flotilla-release | Use this skill when cutting, publishing, or debugging a flotilla release, or when changing anything under scripts/release, scripts/fdroid, scripts/desktop, fastlane/, fdroid/, zapstore.yaml or .gitea/workflows. It covers the version bump, changelog, tagging, the local and CI release runs, every distribution target (container image, gitea release and desktop update feed, Obtainium, zapstore, Google Play, App Store, F-Droid with reproducible builds, the GitHub mirror), the credentials each needs, and a release checklist with the mistakes that have bitten before. |
Releasing flotilla
A release is one version tag and two runs against it. pnpm release:local, on a Mac, does everything that needs a signing key, so no key ever sits on the server. Pushing the tag starts .gitea/workflows/release.yml, which does everything that needs only a gitea token: the container image, the Linux and Windows desktop packages, and F-Droid's build. Both runs attach to the same draft gitea release, and whichever attaches the last required file publishes it.
Where each target ships from
| target | built by | ships to | finished by |
|---|---|---|---|
| Web / self-hosting | image job in release.yml |
gitea.coracle.social/coracle/flotilla:<version> and :latest |
automatic |
| Android APK | apk step, distribution key |
flotilla-<version>.apk on the gitea release |
automatic |
| Obtainium | the gitea release (and its GitHub copy) | users' Obtainium, by source url | automatic |
| zapstore | zapstore step, zsp |
zapstore relays | automatic |
| Google Play | play step, upload key |
a draft release on the production track |
rolling it out in Play Console |
| iOS | ios step |
build uploaded and attached to the App Store version, with What's New | submitting for review in App Store Connect |
| macOS | desktop step on the Mac, signed and notarized |
gitea release + latest-mac.yml |
automatic |
| Linux, Windows | desktop step in CI (Windows in the electronuserland/builder container) |
gitea release + latest-linux.yml, latest.yml |
automatic |
| F-Droid | fdroid (CI) builds unsigned, fdroid-sign (local) signs |
flotilla-fdroid generic package on gitea, which F-Droid verifies its own build against |
F-Droid's bot, from the tag |
| GitHub mirror | mirror.yml |
tags on push, the latest published release every 3 hours | automatic |
Gitea's latest release is the desktop update feed, so a release stays a draft, hidden from updaters and Obtainium, until it has the APK and all three latest*.yml manifests.
Layout
scripts/release/local.mjsandci.mjslist their steps; each step is a module inscripts/release/steps/withmissing()(preflight),setup(how to fix it) andrun().scripts/release/lib/pipeline.mjschecks everything before anything runs: the tag exists, is pushed and is HEAD;CHANGELOG.mdhas a section for the version; the fastlane changelog matches it;node_modulesmatchespnpm-lock.yaml; and each step's credentials and tools.--checkstops there. A failed step prints the command to resume from it.lib/context.mjsreads the version frompackage.json, the version code fromandroid/app/build.gradle, and the notes fromCHANGELOG.md.shortNotesis the notes cut at the last whole line under 500 characters, for Play and F-Droid.scripts/release/bump.mjs(pnpm bump) sets the version inpackage.json, Android and iOS, bumping each platform's build number only when its marketing version changes.scripts/release/github.mjscopies gitea's latest release to GitHub, run bymirror.yml.scripts/fdroid/reproduce.shis F-Droid's build, run in their buildserver image;fdroid/metadata/social.flotilla.fdroid.ymlis the recipe, mirrored in fdroiddata.fastlane/metadata/android/en-US/is F-Droid's listing, read from the tag.
The steps are safe to rerun. play finishes a release from a bundle Play already has when the local AAB is byte-identical to it, ios reuses a build number App Store Connect already has and leaves an already-submitted version alone, gitea replaces same-named assets, and the GitHub copy skips files whose size matches.
Credentials
Local ones go in .env.local; pnpm release:check names whatever is missing and how to get it.
| variable | used by |
|---|---|
GITEA_TOKEN (write:repository, write:package) |
gitea, fdroid-sign |
ANDROID_KEYSTORE_* |
apk, fdroid-sign; the distribution key, which can never change |
PLAY_KEYSTORE_*, PLAY_SERVICE_ACCOUNT |
play |
ASC_KEY_ID, ASC_ISSUER_ID, ASC_KEY_PATH |
ios, and notarizing macOS |
CSC_NAME |
desktop on macOS (Developer ID Application certificate) |
SIGN_WITH |
zapstore |
DOCKER |
optional, e.g. podman, for container builds run from the Mac |
CI uses the job's own token for the release, and the PACKAGE_TOKEN and GH_MIRROR_TOKEN secrets for the container registry and F-Droid package, and for GitHub.
Checklist
Before tagging
git pullon dev, thenpnpm install --frozen-lockfileandnpm ci --prefix electron. The release refuses stale dependencies; building against them once shipped Capacitor 8.3.4 native code with 8.5.2 Swift.pnpm bump patch(orminor,major,x.y.z). Always pass the argument.- Write the
# <version>section ofCHANGELOG.md. The first ~500 characters are what Play and F-Droid show, so lead with what matters. pnpm release:changelogto writefastlane/.../changelogs/<versionCode>.txt.- New F-Droid screenshots or listing text, if the UI changed, go in
fastlane/now; F-Droid only reads them from the tag. - If the F-Droid build environment changed (Node major in
.nvmrc, the JDK, the fdroid scripts), update the recipe infdroid/metadata/and open an fdroiddata merge request with the same change. git add -A && git commit,pnpm release:check, thengit tag <version>andgit push origin dev <version>.
Releasing
- Start
pnpm release:localonce the tag is pushed.fdroid-signwaits for CI's F-Droid build, up to two hours. - Watch the Release workflow run for the tag in gitea's Actions tab. Its
imageandreleasejobs must both pass. - When a step fails, fix the cause and rerun the command the run prints, which resumes from that step. For a CI step, push the fix to dev and run the Release workflow by hand from dev with the steps to rerun, e.g.
fdroid; it builds the tag's commit with dev's scripts, so the tag never has to move.
After both runs
- The gitea release is published, not a draft, with
flotilla-<version>.apk, the macOS DMGs and ZIPs, the AppImage, the Windows installer, andlatest.yml,latest-linux.yml,latest-mac.yml. - Play Console: review the draft on the production track and roll it out.
- App Store Connect: submit the version for review once the build is attached.
- The
flotilla-fdroidpackage hasflotilla-fdroid-<version>.apkfor this version. - Within three hours, GitHub's latest release is this version, with the APK.
- Within a few days, F-Droid shows the version. A failed reproducibility check shows up in F-Droid's build logs for
social.flotilla.fdroid.
Mistakes that have bitten before
- Moving a tag publishes its draft. Force-pushing a tag when a draft release exists for it makes gitea clear the draft flag. It also restarts the Release workflow, which rebuilds the image and replaces the CI-built files. Once F-Droid has built a tag, never move it: F-Droid won't rebuild.
- Version codes are single-use. Play and App Store Connect never accept a version code or build number twice. When an upload of the wrong build is already there, bump
versionCodeinandroid/app/build.gradleorCURRENT_PROJECT_VERSIONin the Xcode project, not the version. - The Play service account needs its app permissions saved in Play Console, and new access can take up to a day to reach the API ("The caller does not have permission").
cap syncrewrites native files (ios/App/Podfile,AndroidManifest.xml,capacitor.settings.gradle) duringweb. On stale dependencies it points them at the wrong versions; revert them rather than committing.- Uploading a build is not releasing it. Play stays a draft and iOS waits for review until someone acts on the follow-ups the run prints.
- The F-Droid recipe lives in two places. Our CI builds from
fdroid/metadata/, F-Droid builds from fdroiddata. If they drift, our apk stops matching theirs and F-Droid won't ship it.