test(release): make the version derivation executable, and guard it on dev
Steps 1 and 2 of this milestone shipped with no CI coverage at all, and the
reason generalises: release.yml triggers only on main and tags, so nothing
inside it is exercised until a release is already running. That is the worst
place in the repo to be unguarded, because the failure mode is silence — a
version nobody can compare looks exactly like being up to date, and nobody
reports an update they were never offered.
The fix is not a test that reads YAML. The derivation moved into
ci/version.sh, so it can be RUN, and internal/server/release_version_test.go
runs it on every push. release.yml now calls the same script, so the thing
that ships and the thing under test are one artifact rather than two copies
that agree until they don't.
test-go.yml gains 'ci/**' and '.gitea/workflows/release.yml' in its paths.
Without that the guard exists but never fires on the changes it protects,
which is the same nothing it replaces.
What is pinned, and why each one:
- HHMM is zero-padded. A build at 00:42 must emit "0042"; a stripped
leading zero shifts the segment two orders of magnitude and reverses
comparisons against every other build that day. It only bites for a
tenth of the day, so it will not be found by chance.
- The name derives from the COMMIT and the code from the BUILD. Asserted
by holding one clock and moving the other: the name must not move, the
code must.
- The code clears 1895, the highest versionCode the retired commit-count
scheme shipped. Below that Android refuses the upgrade as a downgrade
and the channel becomes a one-way door.
- The tag is the name with a `v`, never chosen.
- release.yml still calls the script, and does not derive a commit count
again. This pins the WIRING: without it every other assertion keeps
passing while the shipped path silently drifts out of coverage.
The script rejects unusable clocks rather than emitting something plausible,
and those rejections are tested — a guard that cannot fail is worse than
none, because it reads as coverage.
Falsified before committing rather than after: ran the script against good
and broken inputs and watched all three failure paths fire; verified every
asserted value by executing it rather than by reading it; and checked the
two workflow predicates catch their regressions while staying immune to a
comment that merely names the old formula.
Step 5 of 5 — Scribe task #3812, milestone #390.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SQ31KQpYbStyK5y58UmPLH
This commit is contained in:
@@ -110,45 +110,12 @@ jobs:
|
||||
working-directory: ${{ github.workspace }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
# Two different clocks, deliberately. They answer different
|
||||
# questions, and using one for both breaks whichever it fits worse.
|
||||
#
|
||||
# The NAME answers "is this the same code?" — so it derives from
|
||||
# COMMIT time and reads identically on every lane that builds this
|
||||
# source. A dev build and a main build of one commit must report the
|
||||
# same string; build time cannot do that, it prints two numbers for
|
||||
# one thing.
|
||||
COMMIT_TS=$(git log --format=%ct -1 HEAD)
|
||||
VERSION_NAME=$(date -u -d "@${COMMIT_TS}" +%Y.%m.%d.%H%M)
|
||||
|
||||
# The ORDERING KEY answers "may this be installed over that?" — so it
|
||||
# must be monotonic BY CONSTRUCTION. Minutes since 2020-01-01: ~3.5M
|
||||
# today, ~525k/year, against a 2^31 ceiling.
|
||||
#
|
||||
# This replaced `git rev-list --count HEAD`, which was NOT monotonic
|
||||
# and was commented as if it were. A commit count runs ahead on `dev`,
|
||||
# so a dev build outranked the `main` release that superseded it and
|
||||
# Android refused the install as a downgrade — a channel you could
|
||||
# enter and not leave without uninstalling.
|
||||
#
|
||||
# Commit time would be wrong here too, for the mirror-image reason:
|
||||
# rebuild an older commit and it goes DOWN, which on a phone is a
|
||||
# refused install rather than a merely confusing label.
|
||||
VERSION_CODE=$(( ( $(date -u +%s) - 1577836800 ) / 60 ))
|
||||
|
||||
# Assert the emitted shape at the source. A malformed name still
|
||||
# builds, signs and publishes perfectly happily, and only surfaces as
|
||||
# an update nobody is ever offered — which nobody reports, because
|
||||
# "no update available" and "I cannot read this" look identical.
|
||||
if [[ ! "${VERSION_NAME}" =~ ^[0-9]{4}\.[0-9]{2}\.[0-9]{2}\.[0-9]{4}$ ]]; then
|
||||
echo "::error::version name '${VERSION_NAME}' is not YYYY.MM.DD.HHMM"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "name=${VERSION_NAME}" >> "$GITHUB_OUTPUT"
|
||||
echo "code=${VERSION_CODE}" >> "$GITHUB_OUTPUT"
|
||||
echo "::notice::APK version: ${VERSION_NAME} (code=${VERSION_CODE})"
|
||||
# The derivation lives in ci/version.sh, not here, so it can be
|
||||
# executed by a test on every push. Anything inline in this file is
|
||||
# unverifiable until a release is already running.
|
||||
out="$(ci/version.sh HEAD)"
|
||||
printf '%s\n' "${out}" >> "$GITHUB_OUTPUT"
|
||||
echo "::notice::APK $(printf '%s' "${out}" | tr '\n' ' ')"
|
||||
|
||||
# Checked BEFORE the expensive work, not after it. "Attach APK to gitea
|
||||
# Release" below resolves the release by tag and fails if it is absent —
|
||||
|
||||
Reference in New Issue
Block a user