Skip to content

This guide covers how a new Patrimoine version gets from a git tag to a published image, and how the changelog and in-app version display are derived.


  1. A maintainer bumps the version and tags the commit:

    Terminal window
    git tag vX.Y.Z && git push --tags
  2. The tag push triggers .forgejo/workflows/backend.yml, .forgejo/workflows/frontend.yml and .forgejo/workflows/web.yml (all match on push: tags: v*). Each first calls its matching ci-*.yml (see CI/CD Pipeline) as a pre-publish gate — a tag can never publish an image that skips a check normal CI would have caught.

  3. Buildah builds and pushes semver-tagged images to the registry — see Docker Images for the build mechanics.

  4. An ntfy notification fires on success, failure, or cancellation via niniyas/ntfy-action.

Android ships on its own schedule, unrelated to the v* tag above (backend/frontend/web deploy together; Android ships to app stores/sideload gated by different things — review, signing — entirely).

Run android/scripts/release.sh X.Y.Z to cut a release. It sets versionName and versionCode in android/app/build.gradle.kts, commits the change as chore(android): bump version to X.Y.Z (signed) and creates the signed tag android-vX.Y.Z. versionCode goes up by one unless you pass --code N, and --dry-run only prints the plan. The script never pushes: push the commit, then the tag, to trigger .forgejo/workflows/android.yml:

Terminal window
git push origin HEAD
git push origin android-vX.Y.Z

The workflow:

  1. First runs android/scripts/check-release-version.sh, which fails the run when the tag does not match versionName, or when versionCode is not higher than on every earlier android-v* tag.
  2. Also calls ci-android.yml.
  3. Builds a signed release APK.
  4. Generates a CycloneDX SBOM with Syft.
  5. Generates release notes with android/scripts/release-notes.sh: the breaking changes, features and fixes (conventional commits) that touched android/ since the previous android-v* tag. chore, test, docs and refactor commits are left out, and the first release reads “Initial release.”.
  6. Publishes the Forgejo release for the tag with forgejo-release, attaching the APK and the SBOM and using those notes. A re-run replaces the existing release. This needs a FORGEJO_TOKEN repo secret with the write:repository scope.

No container image is built for Android; there is nothing to push to the registry.

The release APK is signed with a keystore that you create once, with keytool:

Terminal window
keytool -genkeypair -v \
-keystore ~/patrimoine-release.keystore \
-alias patrimoine \
-keyalg RSA -keysize 4096 -validity 10000

keytool creates a PKCS12 keystore. It has a single password for the store and the key, so keytool asks for only one. Use that same password for storePassword and keyPassword below.

Keep the keystore file outside the repository (the repository ignores *.keystore and keystore.properties) and back it up first. Android refuses an update signed with a different key, and a lost key cannot be recovered.

For a local release build, create android/keystore.properties:

keyAlias=patrimoine
keyPassword=<password>
storeFile=/home/you/patrimoine-release.keystore
storePassword=<password>

Then run cd android && ./gradlew :app:assembleRelease. The APK is app/build/outputs/apk/release/app-release.apk. Without keystore.properties, a local release build falls back to debug signing.

For releases built by the workflow, add four repository secrets (Actions secrets in the repository settings):

Secret Value
ANDROID_KEYSTORE_BASE64 the output of base64 -w0 ~/patrimoine-release.keystore
ANDROID_KEYSTORE_PASSWORD the keystore password
ANDROID_KEY_ALIAS patrimoine
ANDROID_KEY_PASSWORD the same password as ANDROID_KEYSTORE_PASSWORD

Check the key with keytool -list -v -keystore ~/patrimoine-release.keystore. The certificate fingerprint it prints is the app’s signing identity.


CHANGELOG.md is auto-generated by git-cliff from Conventional Commits, per cliff.toml. Changelog content is never hand-written — it is generated, not described here; do not expect this page to reproduce specific entries.

Notable cliff.toml behaviors relevant to an admin reading release history:

  • Only tags matching v[0-9]* (tag_pattern) count as releases — a stray non-version tag does not shift the release boundary.
  • cliff.toml keeps breaking_always_bump_major = false, which only affects 0.x versions; from 1.0.0 on (the project is at 8.x) a breaking commit (! or BREAKING CHANGE:) bumps the major version.

frontend/scripts/generate-version.mjs populates the in-app version display by writing src/generated/version.json. It resolves the version in this priority order:

  1. The PATRIMOINE_VERSION env var — set by the Forgejo build workflow from the pushed tag.
  2. git describe --tags --exact-match HEAD — for local builds sitting exactly on a tag.
  3. A "dev" fallback otherwise.

The docs site shows the latest release in its header, linking to that release in the repository. web/documentation/scripts/generate-version.mjs writes src/generated/version.json for it, using the PATRIMOINE_VERSION env var when set (the Forgejo web workflow sets it from the pushed tag), otherwise the most recent tag reachable from HEAD (git describe --tags --abbrev=0). Inside the image build, where git is unavailable, it keeps the file podman.sh generated on the host; with no tag at all, the header shows the repository link without a version.


This repository tracks two independent version numbers — do not conflate them:

Version Where it lives What bumps it
App-wide release version Git tags (vX.Y.Z) + CHANGELOG.md Managed by git-cliff from Conventional Commits, cut by a maintainer pushing a tag
Backend package version backend/pyproject.toml’s version field Bumped only on commits that touch backend/app/routers/*.py, using the same feat=minor/fix=patch/BREAKING=major mapping, scoped to the API surface only — a router change bumps it; a service-layer refactor or test-only change does not (see Git Commit Conventions)

The backend package version is not published as a container tag and does not drive the release flow above — it exists purely to version the API surface independently of when a release is actually cut.