How to cut a new release. Distinct from the contributor workflow in
CONTRIBUTING.md, which covers ordinary PRs.
Semantic versioning: MAJOR.MINOR.PATCH. Pre-1.0 milestones (0.x) may
include breaking changes between minors; once we cut v1.0 we promise
backwards compatibility for the public API and persisted database
schema.
versionCode in composeApp/build.gradle.kts
is derived from the version name as MAJOR*100 + MINOR*10 + PATCH
(e.g. 0.4.0 → 40, 1.0.0 → 100).
The release signing key lives outside the repo. Contributors and the F-Droid build server fall back to the debug keystore automatically; that is fine because F-Droid re-signs every APK with its own key anyway, and GitHub-release attachments carry the upstream key in their release notes for manual verification.
To produce an upstream-signed APK locally, create
keystore.properties in the repository root:
storeFile=path/to/release.keystore
storePassword=<store password>
keyAlias=eu-stats-release
keyPassword=<key password>keystore.properties and *.keystore are already in
.gitignore — never commit either.
To generate a fresh release keystore:
keytool -genkeypair \
-v -storetype PKCS12 \
-keystore release.keystore \
-alias eu-stats-release \
-keyalg RSA -keysize 4096 \
-validity 36500 \
-dname "CN=Roman Tsisyk, OU=EU Stats, O=EU Stats Multiplatform, L=Wroclaw, C=PL"The same identity must be used for every subsequent upstream release — Android refuses to install an upgrade signed with a different key. Back up the keystore and the passwords somewhere durable (password manager, hardware token, encrypted offsite copy). Losing the key means every existing install has to be uninstalled before the next release can be installed.
composeApp/build.gradle.kts configures compose.desktop.application.nativeDistributions
with targetFormats(Dmg, Msi, Deb) — package name "EU Stats", bundle ID
eu.eurostat.app, AGPL LICENSE bundled. Per-OS icons live in
composeApp/icons/ (app.icns, app.ico, app.png), generated from the
same store icon used for the Android/F-Droid listing.
./gradlew :composeApp:packageDmg # macOS
./gradlew :composeApp:packageMsi # Windows
./gradlew :composeApp:packageDeb # LinuxEach package* task only runs on its native OS (you cannot build a .msi
on macOS). packageDmg has been verified locally, producing
EU Stats-1.0.0.dmg (122 MB); packageMsi / packageDeb are exercised
in CI (see below) but not yet verified against a real Windows/Linux
install. All three installers are unsigned for now — macOS Gatekeeper
and Windows SmartScreen will warn on first launch; signing is deferred to
a post-grant phase (no Apple Developer / Windows code-signing cert
budgeted — see NLNET_SUBMISSION/03-milestones.md).
Local gotcha: jpackage (which the package* tasks shell out to)
needs a full JDK — Android Studio's bundled JBR does not ship it. If a
package* task fails with a jpackage-not-found error, point JAVA_HOME
at a full JDK before invoking Gradle, e.g. a Gradle-provisioned Temurin
under ~/.gradle/jdks:
JAVA_HOME=~/.gradle/jdks/<temurin-dir> ./gradlew :composeApp:packageDmg- Land everything intended for the release on
master. - Update
CHANGELOG.md: move the## [Unreleased]block under a new## [X.Y.Z] — YYYY-MM-DDheading, and start a fresh empty## [Unreleased]above it. - Bump
versionNameandversionCodeincomposeApp/build.gradle.kts. - Add a new file
fastlane/metadata/android/{en-US,pl,uk}/changelogs/<versionCode>.txtsummarising the release in 1–3 sentences per locale (F-Droid reads these in its catalogue listing). - Commit and push the release-prep changes.
- Build the upstream APK locally:
./gradlew :composeApp:assembleRelease sha256sum composeApp/build/outputs/apk/release/composeApp-release.apk
- Tag the release:
git tag -s vX.Y.Z -m "EU Stats Multiplatform vX.Y.Z" git push origin vX.Y.Z - Pushing the tag triggers
.github/workflows/release.yml, which creates the GitHub release and attaches the release APK plus the three native desktop installers (.dmg/.msi/.deb) automatically. Paste theversionCodechangelog plus the SHA-256 hash from step 6 into the release notes. The APK is signed with the real key only when theKEYSTORE_BASE64/KEYSTORE_PASSWORD/KEY_ALIAS/KEY_PASSWORDrepository secrets are configured; otherwise it is debug-signed (same fallback as local builds). - If this is a new public release: open a merge request against
fdroiddata updating
metadata/eu.eurostat.app.ymlto point at the new tag. Seedocs/FDROID.md.
GitHub Actions in .github/workflows/build.yml
runs on push to main/master/develop-v* (the develop-v* pattern was
added so the maintainer's actual working branches are covered, not just
main) and on pull requests against main/master. Three jobs: android
(assembles a debug APK, runs the full test suite, and now also runs
assembleRelease so the R8/proguard pass is exercised on every push,
falling back to the debug keystore without secrets), desktop
(smoke-tests packageUberJarForCurrentOS on Ubuntu), and ios-test (runs
the real iosSimulatorArm64Test suite — not just a compile — gated behind
android since macOS runners bill roughly 10x an Ubuntu runner).
release.yml runs on v* tag pushes:
an android job assembles the release APK (real signature when the
keystore secrets are set, debug-signed otherwise) and a three-OS matrix
packages the native installers; every job attaches its artifact to the
GitHub release for the tag. Installers ship unsigned for now —
Gatekeeper/SmartScreen warnings are expected until signing certificates
are budgeted (post-grant item).