Repository navigation
How Knit numbers its releases
Knit derives each build's Android versionCode from its versionName. Nobody picks the code by hand. The build computes it from the name with this formula:
versionCode = major × 10^7 + minor × 10^5 + patch × 10^3 + stage × 10^2 + n
stage: 0 = alpha, 1 = beta, 2 = rc, 9 = stable
So 2.11.1 is 21101900 and 2.11.0-alpha.4 is 21100004. The About screen shows both, as Version 2.11.1 (21101900). The scheme replaced a hand-bumped counter in November 2026 (ADR 2026-11.7t9z).
Checked against Knit 2.11.1.
Android, Google Play and F-Droid all order Knit builds by versionCode and ignore the name.
Android's package manager installs an APK over an existing one when the code is equal or higher, and refuses a lower one with INSTALL_FAILED_VERSION_DOWNGRADE. An update keeps the app's data. Going down means uninstalling first, which doesn't.
Google Play serves each user the highest code across production and every testing track they're enrolled in. Promoting a build from the alpha track to beta reuses its code, so a promoted build keeps its place in the order.
F-Droid reads the code straight out of gradle.properties through the UpdateCheckData line in .fdroid.yml. Its UpdateCheckMode only matches tags of the form ^v[0-9.]+$, so F-Droid ignores pre-release tags entirely.
The name is there for people. Before this scheme nothing tied the two numbers together, and the order Play applied was whatever order the codes had been handed out in.
Each field has a fixed width, counted from the right:
| Field | Digits from the right | Values | Weight |
|---|---|---|---|
n, the build within a stage |
1–2 | 0–99 | 1 |
| stage | 3 | 0, 1, 2, 9 | 100 |
| patch | 4–5 | 0–99 | 1,000 |
| minor | 6–7 | 0–99 | 100,000 |
| major | 8–9 | 0–99 | 10,000,000 |
A stable release has no suffix, so it gets stage 9 and n = 0. Stages 3 to 8 are unused.
Fixed widths mean names that look alike can't collide. 2.10.2 would derive 21002900 and 21.0.2 derives 210002900.
Here's the formula applied to a run of names, in ascending order. Some are real releases and some are only there to show where a build would land:
| versionName | versionCode |
|---|---|
| 2.11.0-alpha.4 | 21100004 |
| 2.11.0-beta.0 | 21100100 |
| 2.11.0-rc.1 | 21100201 |
| 2.11.0 | 21100900 |
| 2.11.1-alpha.0 | 21101000 |
| 2.11.1 | 21101900 |
| 2.11.99 | 21199900 |
| 2.12.0-alpha.0 | 21200000 |
A stable release sorts above all of its own pre-releases. The stage digit puts alpha below beta, beta below rc and rc below stable, and n orders builds within a stage. A tester on 2.11.0-beta.0 (21100100) gets 2.11.0 (21100900) as an ordinary update. A patch can have pre-releases too: 2.11.1-alpha.0 lands between 2.11.0 and 2.11.1.
Every fix to a release line sorts below every pre-release of the next line. Minor and patch sit above the stage digit, so the highest possible 2.11 fix, 2.11.99 at 21199900, is still under 2.12.0-alpha.0 at 21200000.
The second guarantee is the reason the scheme exists. Play hands testers the highest code it has for them. If a production hotfix outranked the alpha track, every alpha tester would be pulled back onto the older line, and that line may not be able to open a database the alpha has already migrated. The same holds outside Play: no later fix to the stable line can outrank a pre-release someone installed from GitHub.
Every field stops at 99, major included. Play's own cap of 2,100,000,000 would allow a major version up to 209, but the watch modules build on the phone's code. :wear and :watchface set their code to 1,000,000,000 plus the phone's knit.versionCode. With major capped at 99 the largest phone code is 999,999,900, for 99.99.99, so phone and watch codes never overlap, and the largest watch code, 1,999,999,900, stays under Play's cap.
A name the grammar can't place fails the build: anything outside X.Y.Z or X.Y.Z-(alpha|beta|rc).N, any field above 99, and labels like -preview.1 or -dev. The build won't guess a stage.
knit.versionName in gradle.properties is the source of truth. app/build.gradle.kts computes knitVersionCode from it and compares the result with the stored knit.versionCode. If they disagree, the build stops:
knit.versionCode=38 but 2.11.0-alpha.4 derives 21100004 (ADR 2026-11.7t9z): run scripts/version-code.py --write
The stored copy has to stay, because F-Droid's update check reads it from that file and the watch modules add it to their base. Rather than prefer one value over the other, the build refuses a mismatch. The old -Pknit.versionCode override is gone too, since an override was the last way to break the order.
scripts/version-code.py is a stdlib-only copy of the same function for scripts and CI:
python3 scripts/version-code.py 2.12.0-alpha.0 # prints 21200000
python3 scripts/version-code.py --write # sets knit.versionCode from knit.versionName
python3 scripts/version-code.py --check # exits 1 on a mismatchTwo implementations of one formula can drift apart. scripts/test_version_code.py pins the mapping, the stage order, the hotfix-below-next-line rule and the refusals, and greps app/build.gradle.kts for the same weights. CI's doc-lint job runs those tests and --check on every pull request and every push to main. The preflight job in release.yml refuses a tag whose stored code isn't the derived one.
A few other things key off the code. F-Droid's per-release notes are fastlane/metadata/android/en-US/changelogs/<versionCode>.txt, so 2.11.0's are in 21100900.txt, and .fdroid.yml's CurrentVersionCode holds the full number. The slim APK gets versionNameSuffix = "-slim" after the derivation, so it shares the full APK's code and either one installs over the other.
- ADR 2026-11.7t9z is the decision record.
- Distribution: Google Play and F-Droid walks through cutting a stable release, a pre-release and the slim APK.
-
release.ymlholds the preflight checks a tag has to pass. - Releases lists every version with its notes.
Knit is free and ad-free, funded by its supporters.
☕ Ko-fi ·
💖 GitHub Sponsors ·
Liberapay ·
Other ways to help
Supporting Knit on Ko-fi or GitHub Sponsors comes with Knit Anywhere. Liberapay goes to the developer personally.