Root, CI, docs and packaging describe the project to a stranger (#6225)
Android / Build, or is the channel already serving this? (push) Successful in 3s
CI & Build / Python lint (push) Successful in 2s
CI & Build / Build now, or wait for Android? (push) Successful in 2s
Desktop (Tauri) / Build, or is the channel already serving this? (push) Successful in 2s
CI & Build / Web typecheck and unit tests (push) Successful in 12s
CI & Build / Python tests (push) Successful in 23s
Android / Core and FFI clippy and tests (push) Successful in 51s
CI & Build / integration (push) Successful in 1m49s
CI & Build / Build & push image (push) Skipped
Desktop (Tauri) / Web tests, clippy, Rust tests and rustfmt (push) Successful in 2m20s
Desktop (Tauri) / Windows installer (cross-compiled) (push) Successful in 3m6s
Desktop (Tauri) / Tauri desktop (Linux) (push) Successful in 3m46s
Desktop (Tauri) / Update manifest (push) Successful in 3s
Android / Kotlin + Rust (APK) (push) Successful in 8m31s
Android / Build the server image (push) Successful in 1s

#6225 "Comment standard sweep: root, config, CI workflows, docs and
packaging", milestone 546 "Bring the repo to the comment standard (rule 32)
and compose defaults (rule 198)".

- README restructured in newcomer order: what it is, quick start,
  configuration, setup pitfalls, updating and backups, features,
  troubleshooting, development, licence. Its quick-start compose carries
  no restart policy.
- Dockerfile and .env.example trimmed to one or two lines per setting.
- Workflows, packaging scripts and docs lose issue, run, note, rule and
  milestone references and their history narration. Comments that still
  described path filters and "step 6" as future work now state what the
  workflows do.
- ci-requirements.md rewritten against the current workflows. It said the
  build gated on typecheck and lint only, the integration lane did not
  gate, a v* tag published :latest, and ci-tauri lacked bsdtar and zstd;
  none of those is true now (the last checked against run 9630's log).
- Echo strings in guard-forward.sh and version.sh drop their note
  citations; every string a test asserts on is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-21 20:44:02 -04:00
co-authored by Claude Opus 5.5
parent 01b53118b9
commit f16224b07c
18 changed files with 666 additions and 843 deletions
+60 -84
View File
@@ -2,15 +2,12 @@
#
# What version an artifact carries, derived from its OWN shipped files.
#
# Replaces desktop/packaging/build-version.sh, which was one generator feeding the
# desktop bundles AND the Android APK off `GITHUB_RUN_NUMBER`. A Kotlin-only commit
# re-versioned the desktop; a Rust-only commit re-versioned the phone. It read as
# tidy — one definition, no drift — which is exactly why it survived review. One
# definition of HOW to derive is right; one VALUE for unrelated artifacts is not.
# (Note 3127 §3, which cites this repo as its example of the failure.)
# One definition of HOW to derive, applied per artifact. One VALUE for unrelated
# artifacts would re-version the desktop on a Kotlin-only commit and the phone on a
# Rust-only one.
#
# Lives at the repo root, not under desktop/, because it now serves three artifacts
# and a shared thing filed under one consumer is how it ends up owned by that one.
# Lives at the repo root, not under desktop/, because it serves three artifacts and
# a shared thing filed under one consumer ends up owned by that one.
#
# version.sh display <artifact> the human-readable version — 2026.08.28.1815
# version.sh key <artifact> the ordering key a comparator reads
@@ -20,7 +17,7 @@
#
# "is this the same code?" -> display. A dev build and the main build of one
# commit read identically, because they ARE the
# same bytes (note 3127 §2, reason 4).
# same bytes.
# "may this replace that?" -> key. What an updater or an install gate
# compares, and never shown to a person.
#
@@ -29,14 +26,12 @@
# the spec allows three, and `08` is a leading zero, which it forbids outright. A
# non-semver string does not sort low: the feed fails to DESERIALIZE and every client
# reports "no update available" forever. So the platform's field takes an opaque key
# and the display version lives beside it. See #3142's spike.
# and the display version lives beside it.
#
# WHY NOT A `-dev.N` PRERELEASE for the dev channel — carried over from the script
# this replaces, because it is a real finding and the reasoning is not obvious:
# a prerelease sorts BELOW the release it qualifies (`0.1.0-dev.5` < `0.1.0`), so a
# dev build could never be offered as an update to a tagged one, and Windows
# installer metadata wants a numeric X.Y.Z anyway. The channel goes in a sibling
# field, never in the version — note 3127 §7, and rule 149.
# WHY NOT A `-dev.N` PRERELEASE for the dev channel: a prerelease sorts BELOW the
# release it qualifies (`0.1.0-dev.5` < `0.1.0`), so a dev build could never be
# offered as an update to a tagged one, and Windows installer metadata wants a
# numeric X.Y.Z anyway. The channel goes in a sibling field, never in the version.
set -eu
# ANCHOR AT THE REPO ROOT BEFORE ANYTHING ELSE.
@@ -46,47 +41,37 @@ set -eu
# `desktop/src-tauri`, the Android build from `android`, the manifest job from the
# root — so without this the same request answers differently per caller.
#
# It is not a tidy failure. Measured on run 4796, one push produced THREE versions:
# the desktop build (cwd `desktop/src-tauri`) said 1.0.3494522, while the pacman
# packager and the manifest job both said 1.0.3502131. The build's pathspec had
# matched `desktop/src-tauri/Cargo.toml` — a real file — so git returned the newest
# commit touching THAT, six days stale. Non-empty, so the guard below could not fire;
# the manifest then found no bundle matching its own answer and the lane went red for
# a reason two steps removed from the cause.
#
# The Android job failed loudly in the same run only because its pathspec happened to
# match nothing from `android/`. Same bug, louder symptom, pure luck.
# It is not a tidy failure. From `desktop/src-tauri`, the pathspec `Cargo.toml`
# matches `desktop/src-tauri/Cargo.toml`, a real file, so git returns the newest
# commit touching THAT: a stale but non-empty answer the guard below cannot catch,
# and a version that disagrees with what every other job derives for the same push.
cd "$(git rev-parse --show-toplevel)"
# A SHALLOW CLONE IS FATAL, and asked directly rather than inferred.
#
# Landmine §6.1: depth-1 sees one commit, so `git log -- <paths>` answers about
# whatever happens to be in that commit and the result is a too-LOW version — the
# unrecoverable direction, arrived at silently with every lane green.
# Depth-1 sees one commit, so `git log -- <paths>` answers about whatever happens to
# be in that commit and the result is a too-LOW version — the unrecoverable
# direction, arrived at silently with every lane green.
#
# The empty-result guard below catches only the case where NOTHING matches. It missed
# the worse one: on run 4796 a partial match returned a real, six-days-stale answer.
# `--is-shallow-repository` tests the actual hazard instead of a symptom of it, costs
# no network, and covers every artifact including the ones with no published value to
# compare against.
# The empty-result guard below catches only the case where NOTHING matches, not a
# partial match returning a real, stale answer. `--is-shallow-repository` tests the
# actual hazard instead of a symptom of it, costs no network, and covers every
# artifact including the ones with no published value to compare against.
if [ "$(git rev-parse --is-shallow-repository)" = "true" ]; then
echo "version.sh: this is a SHALLOW clone — any version derived here would be" >&2
echo " too low, silently. Add 'fetch-depth: 0' to the checkout." >&2
echo " (note 3127 §6.1)" >&2
exit 1
fi
# 2020-01-01T00:00:00Z. The counter epoch, and it must NEVER move: shifting it
# renumbers every artifact downwards, which is the one direction you cannot recover
# from (note 3127 §6.4).
# from.
EPOCH=1577836800
# --- the shipped file sets ---------------------------------------------------
#
# ONE definition, read by every consumer. The `paths:` filters in the three
# workflows are a second, independent statement of the same fact today; they come
# out in step 6 when skip-if-exists replaces them. Until then, a change here that is
# not mirrored there means a lane that does not fire — check both.
# ONE definition, read by every consumer: `should-build.sh` decides from it whether
# a client lane builds, and the workflows carry no `paths:` filters of their own.
#
# Read off what actually PACKAGES each artifact, not off intuition. Miss a file and
# a stale build keeps its version; include one that does not ship and you re-version
@@ -94,10 +79,11 @@ EPOCH=1577836800
#
# THE BUILD DEFINITION IS IN THE SET, and it is the part that is easy to leave out.
# A workflow file is not "shipped" — but change a Gradle flag or a `cargo tauri
# build` argument and the bytes change while the source does not. Once step 6 skips
# a build whose version already exists, that combination serves the OLD artifact on
# a green run: exactly the "miss a file and a stale build keeps its version" failure,
# arriving through the build recipe rather than the source. Same reason `packaging/`
# build` argument and the bytes change while the source does not. `should-build.sh`
# skips a build whose version the channel already serves, so that combination would
# serve the OLD artifact on a green run: exactly the "miss a file and a stale build
# keeps its version" failure, arriving through the build recipe rather than the
# source. Same reason `packaging/`
# is in every set: this script decides identity, so a change to it is a change to
# what each artifact claims to be.
paths_for() {
@@ -110,14 +96,11 @@ paths_for() {
# BUNDLED ARTIFACT: the image bakes in the Android client (ci.yml fetches the APK
# from the channel release and copies it into the package). So the image's set
# must contain the APK's set — an APK-only change genuinely changes what this
# image ships. Note 3127 §3 names this trap; FC's web image embeds the extension
# the same way.
# image ships.
#
# The base images are NOT listed and do not need to be: `Dockerfile` is in the
# set, so pinning `FROM` by digest (step 6) puts the base inside the set for
# free. Resolving a digest at derive time would work too and is WRONG — it is an
# external lookup, which §7's corollary forbids because it makes the value depend
# on when it was computed.
# The base images are NOT listed. `Dockerfile` is in the set, so a `FROM` pinned
# by digest would bring the base in for free. Resolving a digest at derive time
# is WRONG: an external lookup makes the value depend on when it was computed.
server) echo "src frontend alembic alembic.ini Dockerfile pyproject.toml .forgejo/workflows/ci.yml android core Cargo.toml Cargo.lock .forgejo/workflows/android.yml packaging" ;;
*) echo "version.sh: unknown artifact '$1'" >&2; exit 2 ;;
esac
@@ -126,23 +109,23 @@ paths_for() {
# Sets TS to the newest commit timestamp touching this artifact's files, or exits.
#
# EMPTY IS FATAL, deliberately. A shallow clone sees one commit and derives a
# too-low value with every lane green — the failure landmine §6.1 exists for, and
# the unrecoverable direction. Every job that calls this needs `fetch-depth: 0`;
# too-low value with every lane green — the unrecoverable direction. Every job that
# calls this needs `fetch-depth: 0`;
# this is what turns forgetting it into a red lane instead of a stranded channel.
#
# SETS A GLOBAL RATHER THAN ECHOING, and that is not a style preference. Written as
# `$(commit_ts desktop)` the function runs in a SUBSHELL, so its `exit` ends only
# that subshell and the caller continues with an empty string. Measured before this
# was fixed: `key desktop` on a repo with no matching history printed the error to
# stderr and then emitted `1.0.-26297280` and exited ZERO. A guard that reports a
# problem and does not stop is worse than none — it looks like it is working.
# `$(commit_ts desktop)` the function would run in a SUBSHELL, so its `exit` would end
# only that subshell and the caller would continue with an empty string: `key
# desktop` on a repo with no matching history would print the error and then emit
# `1.0.-26297280` and exit ZERO. A guard that reports a problem and does not stop is
# worse than none — it looks like it is working.
resolve_ts() {
# Unquoted on purpose: the path list is several words.
# shellcheck disable=SC2046
TS="$(git log --format=%ct -1 HEAD -- $(paths_for "$1"))"
if [ -z "$TS" ]; then
echo "version.sh: no commit touches $1's file set — is this a shallow clone?" >&2
echo " (needs fetch-depth: 0; see note 3127 §6.1)" >&2
echo " (needs fetch-depth: 0)" >&2
exit 1
fi
}
@@ -154,18 +137,12 @@ artifact="${2:?usage: version.sh <display|key|paths> <desktop|android|server>}"
# VALIDATED HERE, in the parent shell, and not left to `paths_for`'s default arm.
#
# Third instance of one trap in this script, so it is worth stating plainly: `exit`
# inside a function called as `$(...)` ends the SUBSHELL, not the script. `paths_for`
# is reached through `$(paths_for "$1")`, so its `exit 2` printed the error and
# returned an EMPTY pathspec — and an empty pathspec matches everything, so
# `version.sh display nope` answered `2026.08.28.0900` and exited 0. A confident
# version for an artifact that does not exist.
#
# The other two were the shallow-clone guard on the `key` path (emitted
# `1.0.-26297280`, exit 0) and the same guard on `display` (which failed only because
# `date` then choked on an empty string — luck, not design). Each was found by a
# different mechanism; none by reading the code. If you add a guard to this file,
# make sure it runs where the script does.
# The trap this whole script works around: `exit` inside a function called as
# `$(...)` ends the SUBSHELL, not the script. `paths_for` is reached through
# `$(paths_for "$1")`, so its `exit 2` alone would print the error and return an
# EMPTY pathspec — and an empty pathspec matches everything, so `version.sh display
# nope` would answer a confident version for an artifact that does not exist. If you
# add a guard to this file, make sure it runs where the script does.
case "$artifact" in
desktop|android|server) : ;;
*)
@@ -181,7 +158,7 @@ case "$what" in
display)
# One shape for every human-readable version in this repo, and for the release
# tag: YYYY.MM.DD.HHMM, zero-padded, UTC (note 3127 §1). Padded so it sorts as
# tag: YYYY.MM.DD.HHMM, zero-padded, UTC. Padded so it sorts as
# text as well as numerically, and so two lanes cannot emit forms one character
# apart.
resolve_ts "$artifact"
@@ -193,18 +170,17 @@ case "$what" in
desktop)
# COMMIT time. The desktop is a one-value system to Tauri — its comparator
# reads the version name — so this key is also what lands in bundle
# filenames and .deb metadata. Commit time buys the property in §2 reason
# (4): the last dev build before a PR and the main build from it are the
# same bytes and derive the same key, so the artifact is reused rather than
# rebuilt and re-signed under a new name.
# filenames and .deb metadata. With commit time, the last dev build before
# a PR and the main build from it are the same bytes and derive the same
# key, so the artifact is reused rather than rebuilt and re-signed under a
# new name.
#
# Commit time CAN go backwards (rebuild an older commit). The backwards
# guard in step 5 is the whole mitigation, and the desktop's failure there
# is soft: an update is not offered. Contrast Android below.
# Commit time CAN go backwards (rebuild an older commit). guard-forward.sh
# is the whole mitigation, and the desktop's failure there is soft: an
# update is not offered. Contrast Android below.
#
# `1.0.` and not `0.0.`: the minor must clear the installed `0.2.<run>` line
# or every dev user is stranded on "up to date" permanently. Checked against
# the live feed (0.2.466), not against what we thought we had published.
# `1.0.` and not `0.0.`: the minor must clear the `0.2.<run>` line older
# installs carry, or they are stranded on "up to date" permanently.
resolve_ts desktop
echo "1.0.$(minutes_since_epoch "$TS")"
;;
@@ -222,8 +198,8 @@ case "$what" in
server)
# NO ORDERING KEY. Nothing compares the server image: no updater, no install
# gate, and `:latest` is moved by the registry rather than chosen by a
# client. §2 is explicit that an artifact with nothing to compare needs only
# a name — do not add one because the other two have one.
# client. An artifact with nothing to compare needs only a name — do not
# add one because the other two have one.
echo "version.sh: the server has no ordering key; use 'display'" >&2
exit 2
;;