package server import ( "os" "os/exec" "path/filepath" "regexp" "strconv" "strings" "testing" ) // Guards the version derivation that stamps every build. // // These assertions used to be impossible to run. The derivation lived inline // in release.yml, which triggers only on `main` and on tags — so a mistake in // it could not surface until a release was already under way, and its 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. // // Moving it to ci/version.sh made it executable, so this runs on every push // that touches the release machinery. That is the whole point of the file; the // specific assertions below matter less than the fact that they run at all. // // The tests EXECUTE the script rather than asserting on its text, so they // break when the behaviour changes rather than when the wording does. // highestOldSchemeCode is the largest versionCode ever shipped under the // retired commit-count scheme (v2026.09.09 shipped 1895). Every code the new // scheme emits must clear it, or Android would refuse the upgrade as a // downgrade and the update channel would be a one-way door. const highestOldSchemeCode = 1895 func repoRoot(t *testing.T) string { t.Helper() dir, err := os.Getwd() if err != nil { t.Fatal(err) } for { if _, err := os.Stat(filepath.Join(dir, "go.mod")); err == nil { return dir } parent := filepath.Dir(dir) if parent == dir { t.Fatalf("no go.mod above %s", dir) } dir = parent } } // runVersion executes ci/version.sh with both clocks pinned, so the result is // deterministic. Returns the parsed KEY=VALUE output. func runVersion(t *testing.T, commitEpoch, nowEpoch string) map[string]string { t.Helper() out, err := versionScript(t, commitEpoch, nowEpoch) if err != nil { t.Fatalf("ci/version.sh failed: %v\n%s", err, out) } parsed := map[string]string{} for _, line := range strings.Split(strings.TrimSpace(out), "\n") { if k, v, ok := strings.Cut(line, "="); ok { parsed[k] = v } } return parsed } func versionScript(t *testing.T, commitEpoch, nowEpoch string) (string, error) { t.Helper() root := repoRoot(t) cmd := exec.Command(filepath.Join(root, "ci", "version.sh")) cmd.Dir = root cmd.Env = append(os.Environ(), "MINSTREL_COMMIT_EPOCH="+commitEpoch, "MINSTREL_NOW_EPOCH="+nowEpoch, ) out, err := cmd.CombinedOutput() return string(out), err } func TestVersionName_IsCommitTimeToTheMinute(t *testing.T) { // 2025-09-09T18:48:56Z got := runVersion(t, "1757443736", "1789000920") if want := "2025.09.09.1848"; got["name"] != want { t.Errorf("name = %q, want %q", got["name"], want) } } // HHMM is the segment most likely to be silently mangled, and it only bites // for about a tenth of the day — a build just after midnight must emit "0042", // never "42". A stripped leading zero shifts the segment by two orders of // magnitude and reverses comparisons against every other build that day. func TestVersionName_PadsTheMinuteSegment(t *testing.T) { // 2026-09-10T00:42:00Z got := runVersion(t, "1789000920", "1789000920") if want := "2026.09.10.0042"; got["name"] != want { t.Errorf("name = %q, want %q — leading zero lost?", got["name"], want) } } func TestVersionName_DerivesFromCommitNotBuildClock(t *testing.T) { // Same commit, two different build clocks: the NAME must not move, or a // dev build and a main build of one commit would report different strings // and the channel field would stop being the only thing separating them. a := runVersion(t, "1757443736", "1789000920") b := runVersion(t, "1757443736", "1789500000") if a["name"] != b["name"] { t.Errorf("name moved with the build clock: %q vs %q", a["name"], b["name"]) } if a["code"] == b["code"] { t.Errorf("code did NOT move with the build clock (%q) — it is not build-derived", a["code"]) } } func TestVersionCode_IsMinutesSince2020AndClearsTheOldScheme(t *testing.T) { got := runVersion(t, "1789000920", "1789000920") code, err := strconv.Atoi(got["code"]) if err != nil { t.Fatalf("code %q is not an integer: %v", got["code"], err) } if want := (1789000920 - 1577836800) / 60; code != want { t.Errorf("code = %d, want %d", code, want) } if code <= highestOldSchemeCode { t.Errorf("code %d does not clear the retired commit-count scheme (%d) — "+ "Android would refuse the upgrade as a downgrade", code, highestOldSchemeCode) } if int64(code) > 2147483647 { t.Errorf("code %d overflows versionCode's int32 ceiling", code) } } // The tag is not chosen, it is the name with a `v`. Anything else reintroduces // the mismatch between what a tag claims and what the artifact reports. func TestTag_IsTheNameWithAPrefix(t *testing.T) { got := runVersion(t, "1757443736", "1789000920") if want := "v" + got["name"]; got["tag"] != want { t.Errorf("tag = %q, want %q", got["tag"], want) } } // A guard that cannot fail is worse than no guard. These prove the script // rejects the shapes it claims to reject, rather than emitting something // plausible and letting it ship. func TestVersionScript_RejectsUnusableClocks(t *testing.T) { for _, tc := range []struct{ name, commit, now string }{ {"unreadable commit timestamp", "notanumber", "1789000920"}, {"non-numeric build clock", "1789000920", "abc"}, {"build clock before 2020", "1789000920", "1000000000"}, } { t.Run(tc.name, func(t *testing.T) { out, err := versionScript(t, tc.commit, tc.now) if err == nil { t.Errorf("script succeeded on %s, output: %s", tc.name, out) } }) } } // Pins the wiring, not the formula: if release.yml stops calling the script, // every assertion above keeps passing while the thing that actually ships goes // unguarded again. That silent decoupling is the specific regression here. func TestReleaseWorkflow_UsesTheSharedDerivation(t *testing.T) { body, err := os.ReadFile(filepath.Join(repoRoot(t), ".gitea", "workflows", "release.yml")) if err != nil { t.Fatal(err) } yaml := string(body) if !strings.Contains(yaml, "ci/version.sh") { t.Error("release.yml no longer calls ci/version.sh — the derivation has drifted out of test coverage") } // The retired scheme, which must not come back. A presence check is safe // against prose; the historical note in that file names the old formula // only in comments, so match the executable form. if strings.Contains(yaml, "$(git rev-list --count") { t.Error("release.yml derives a commit count again — that is not monotonic across branches") } } // jobBoundary matches a blank line followed by job-level (two-space) // indentation — the end of the last step in a job. var jobBoundary = regexp.MustCompile(`\n\n [^ \n]`) // stepBody returns one workflow step's text, from its `- name:` line to the // start of the next step or the next job. // // The naive cut — "up to the next `- name:`" — silently returns an EMPTY body // for the last step in a job, and every assertion over it then passes // vacuously. That is the failure rule 167 names: a check that reads as // coverage while asserting on nothing. Cutting at a blank line followed by // job-level indentation handles the last-step case, which is exactly where // `Build and push` sits. func stepBody(t *testing.T, yaml, name string) string { t.Helper() i := strings.Index(yaml, "- name: "+name) if i < 0 { t.Fatalf("no %q step in release.yml", name) } body := yaml[i:] end := len(body) if j := strings.Index(body, "\n - name:"); j >= 0 { end = j } // A blank line followed by EXACTLY two spaces and then content starts a // new job or job-level comment. The "exactly" matters: a blank line inside // a `run:` block is followed by ten-space indentation and would otherwise // match, truncating the step mid-body — which is how this helper first // sliced the verify step down to its first two lines. if loc := jobBoundary.FindStringIndex(body); loc != nil && loc[0] < end { end = loc[0] } body = body[:end] if strings.TrimSpace(strings.TrimPrefix(body, "- name: "+name)) == "" { t.Fatalf("step %q sliced to an empty body — the assertions below would pass vacuously", name) } return body } // releaseYAML reads the workflow once per test. func releaseYAML(t *testing.T) string { t.Helper() body, err := os.ReadFile(filepath.Join(repoRoot(t), ".gitea", "workflows", "release.yml")) if err != nil { t.Fatal(err) } return string(body) } // imageTagArms splits "Compute image tags" into its three ref branches. The // whole tag policy lives in that if/elif/else, so the arms are the unit worth // asserting on — a tag published from the wrong arm reaches the wrong // audience, and every such mistake still builds and still pushes a valid // image. func imageTagArms(t *testing.T, yaml string) (tagArm, devArm, mainArm string) { t.Helper() const ( tagMarker = `if [[ "${GITHUB_REF}" == refs/tags/v* ]]; then` devMarker = `elif [[ "${GITHUB_REF}" == "refs/heads/dev" ]]; then` mainMarker = "\n else" endMarker = "\n fi" ) iTag := strings.Index(yaml, tagMarker) iDev := strings.Index(yaml, devMarker) iMain := -1 if iDev >= 0 { if k := strings.Index(yaml[iDev:], mainMarker); k >= 0 { iMain = iDev + k } } if iTag < 0 || iDev < 0 || iMain < 0 { t.Fatal("Compute image tags no longer has a tag/dev/main arm — the tag policy has been restructured, so these guards are pinning nothing") } iEnd := iMain if k := strings.Index(yaml[iMain:], endMarker); k >= 0 { iEnd = iMain + k } else { t.Fatal("no closing fi after the main arm") } return yaml[iTag:iDev], yaml[iDev:iMain], yaml[iMain:iEnd] } // The worst regression this wiring can produce: a dev push that also moves // :latest would ship untested code to every stable operator, silently, on the // next pull. Nothing else in the suite would notice — the build stays green // and the image is valid, it is simply the wrong audience. func TestDevChannel_PublishesDevAloneAndNeverLatest(t *testing.T) { _, arm, _ := imageTagArms(t, releaseYAML(t)) if !strings.Contains(arm, "${IMAGE}:dev") { t.Errorf("dev arm does not publish :dev\n%s", arm) } if strings.Contains(arm, ":latest") { t.Errorf("dev arm moves :latest — that ships dev code to every stable operator\n%s", arm) } // Rule 145: a rolling channel gets no commit-addressable tag. if strings.Contains(arm, "GITHUB_SHA") { t.Errorf("dev arm publishes a per-commit tag; a rolling channel should not\n%s", arm) } } // The two bundling paths must stay mutually exclusive. If the rebundle step's // condition were relaxed back to "not a tag", a dev push would run BOTH: stage // its freshly-built APK, then overwrite it with the previous release's. The // image would still build and the sidecar would still parse — it would just // quietly serve stale art to the channel whose whole job is being current. func TestDevChannel_RebundlePathIsMainOnly(t *testing.T) { body, err := os.ReadFile(filepath.Join(repoRoot(t), ".gitea", "workflows", "release.yml")) if err != nil { t.Fatal(err) } yaml := string(body) i := strings.Index(yaml, "- name: Bundle latest release APK") if i < 0 { t.Fatal("no 'Bundle latest release APK' step") } step := yaml[i:] if j := strings.Index(step, "\n - name:"); j >= 0 { step = step[:j] } if !strings.Contains(step, "github.ref == 'refs/heads/main'") { t.Errorf("the rebundle step is not gated to main; a dev push would overwrite its own APK\n%s", step) } } // The runner invokes `shell: bash` as `bash -e -o pipefail`. That makes any // command substitution whose grep matches NOTHING fatal at the assignment — // pipefail reports the grep's non-zero status even though `head` succeeded — // so the step dies before reaching the `if` written to handle the empty case. // // This is not hypothetical. It took down the first `main` build after the // version rework: the newest release predated sidecar assets, its // `.apk.version` grep matched nothing, and the step aborted instead of // falling through to the name-only branch that exists precisely for it. The // same latent hazard sat on the other two assignments and had simply never // fired, because their greps always matched. // // Pins the property that makes every "degrades gracefully" branch in that // step reachable at all. func TestBundleStep_GrepsCannotKillTheStep(t *testing.T) { body, err := os.ReadFile(filepath.Join(repoRoot(t), ".gitea", "workflows", "release.yml")) if err != nil { t.Fatal(err) } yaml := string(body) i := strings.Index(yaml, "- name: Bundle latest release APK") if i < 0 { t.Fatal("no 'Bundle latest release APK' step") } step := yaml[i:] if j := strings.Index(step, "\n - name:"); j >= 0 { step = step[:j] } for _, line := range strings.Split(step, "\n") { trimmed := strings.TrimSpace(line) if strings.HasPrefix(trimmed, "#") || !strings.Contains(trimmed, "grep") { continue } // Only assignments from a command substitution can abort the step. if !strings.Contains(trimmed, `="$(`) { continue } if !strings.HasSuffix(trimmed, "|| true") { t.Errorf("this assignment dies under pipefail when its grep matches nothing, "+ "skipping the empty-case branch below it:\n %s", trimmed) } } } // The rollback unit, and the reason it is worth a guard: it is invisible until // the moment it is needed. Nothing pulls : during normal operation, so if // this arm stopped minting one, every build would stay green and every image // would be valid — and the absence would surface only during an incident, as // "there is nothing to roll back to." // // Rules 145 and 147: main push → :latest + :. func TestMainChannel_PublishesLatestAndTheRollbackUnit(t *testing.T) { _, _, arm := imageTagArms(t, releaseYAML(t)) if !strings.Contains(arm, "${IMAGE}:latest") { t.Errorf("main arm does not move :latest — production would stop tracking main's tip\n%s", arm) } if !strings.Contains(arm, "${IMAGE}:${GITHUB_SHA}") { t.Errorf("main arm publishes no commit-addressable image; there is no rollback target for production commits\n%s", arm) } // Rule 147: :latest tracks main's tip, and a second name for the same // image sends readers looking for a distinction that does not exist. if strings.Contains(arm, "${IMAGE}:main") { t.Errorf("main arm publishes :main — rule 147 says that tag should not exist\n%s", arm) } } // A release refreshes the CHANNEL and mints nothing else (rules 145 + 146). // // The specific regression: re-adding : here. The tag build rebuilds the // SAME SOURCE as main's build minutes earlier, differing only in which APK is // baked in — so a : minted here would overwrite main's immutable rollback // target with different contents, under the same name. That is the exact thing // rule 145's immutability clause exists to prevent, and it is the half with // the incidents behind it. func TestReleaseBuild_RefreshesTheChannelAndMintsNothingElse(t *testing.T) { arm, _, _ := imageTagArms(t, releaseYAML(t)) if !strings.Contains(arm, "${IMAGE}:latest") { t.Errorf("tag arm does not refresh :latest — the channel would keep serving the PREVIOUS release's APK until someone pushed to main\n%s", arm) } if strings.Contains(arm, "GITHUB_SHA") { t.Errorf("tag arm mints a : image; that would re-push main's immutable rollback target with different bundled contents\n%s", arm) } } // No version-numbered image tags anywhere, on any arm (rule 145, and the // operator's 2026-09-10 decision to drop them across every project). // // Asserted across the whole step rather than per-arm because the mistake this // catches is re-adding one ANYWHERE, and the tag arm is only the likeliest // spot. `${VERSION}` still legitimately appears in the step as the build's // self-reported version, so the assertion has to name the image-tag form // specifically rather than the variable — otherwise it would fire on correct // code and get "fixed" by deleting the guard. func TestNoVersionNumberedImageTags(t *testing.T) { yaml := releaseYAML(t) tagArm, devArm, mainArm := imageTagArms(t, yaml) for _, tc := range []struct{ name, arm string }{ {"tag", tagArm}, {"dev", devArm}, {"main", mainArm}, } { for _, forbidden := range []string{ "${IMAGE}:${VERSION}", "${IMAGE}:v", "${IMAGE}:${GITHUB_REF#refs/tags/}", } { if strings.Contains(tc.arm, forbidden) { t.Errorf("%s arm publishes a version-numbered image tag (%q); git and the build's self-reported version answer \"which build is this\"\n%s", tc.name, forbidden, tc.arm) } } } } // The verify job asserted the : image existed. With version tags // gone that assertion would fail every release for a tag nothing mints — so // this pins that it was re-pointed rather than deleted, since deleting it is // the tempting way to make a failing guard go green. func TestVerifyJob_ChecksTheRollbackImageNotAVersionTag(t *testing.T) { yaml := releaseYAML(t) if !strings.Contains(yaml, "- name: Rollback image must exist for the tagged commit") { t.Fatal("the release-verification step that checks an image exists is gone; an image push that silently did not happen would now pass verification") } step := stepBody(t, yaml, "Rollback image must exist for the tagged commit") if !strings.Contains(step, "${IMAGE}:${GITHUB_SHA}") { t.Errorf("the verify step does not inspect the commit's rollback image\n%s", step) } if strings.Contains(step, "${IMAGE}:${TAG}") { t.Errorf("the verify step still inspects a version-numbered image, which is no longer published — this would fail every release\n%s", step) } } // The server's self-reported version is now the ONLY thing that identifies a // build, so a lane that stamps a channel word instead of a version silently // removes that ability. It used to stamp the literal "main"/"dev". func TestImageBuild_StampsADerivedVersionAndAChannel(t *testing.T) { yaml := releaseYAML(t) step := stepBody(t, yaml, "Build and push") for _, want := range []string{ "MINSTREL_VERSION=", "MINSTREL_CHANNEL=", } { if !strings.Contains(step, want) { t.Errorf("Build and push does not pass %s — the image cannot report which build it is\n%s", want, step) } } // The version must come from the shared derivation, not from the ref. // Reading it off GITHUB_REF is what produced "main" and "dev" as version // strings, which is the regression this pins. _, _, mainArm := imageTagArms(t, yaml) if strings.Contains(mainArm, `version=main`) { t.Errorf("the main arm stamps the literal string \"main\" as a version; two images months apart would be indistinguishable\n%s", mainArm) } if !strings.Contains(yaml, "ci/version.sh HEAD | sed") { t.Error("the image job no longer derives its version from ci/version.sh — the version and the APK's version can now drift apart") } }