ci: main builds and bundles its own signed APK, so a merge ships the app without a release (#6465)
release / web (push) Successful in 1m51s
release / go (push) Successful in 2m6s
release / govulncheck (push) Successful in 20s
release / integration (push) Successful in 6m53s
release / android (push) Successful in 5m41s
release / Build signed APK (push) Successful in 5m38s
release / Attach APK to the Release (tag releases only) (push) Skipped
release / Build + push container image (push) Successful in 18s
release / Verify release artifacts (tag releases only) (push) Skipped
release / web (push) Successful in 1m51s
release / go (push) Successful in 2m6s
release / govulncheck (push) Successful in 20s
release / integration (push) Successful in 6m53s
release / android (push) Successful in 5m41s
release / Build signed APK (push) Successful in 5m38s
release / Attach APK to the Release (tag releases only) (push) Skipped
release / Build + push container image (push) Successful in 18s
release / Verify release artifacts (tag releases only) (push) Skipped
android-release now runs on main as well as dev and tags, and image-release requires its success on every ref. :latest carries the stable APK built from main's own commit instead of the newest release's, so a merged app change reaches stable phones on the next pull. Releases stay as optional bookmarks that attach the APK for download. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -272,73 +272,41 @@ func TestDevChannel_PublishesDevAloneAndNeverLatest(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// 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)
|
||||
// A merge to main is enough to put a new app in front of stable phones: main
|
||||
// builds the signed APK from its own commit and bakes it into :latest, as dev
|
||||
// does for :dev. The regression to guard is main falling back to a tag-only
|
||||
// APK build, which leaves every merged app change waiting on a release while
|
||||
// each build stays green.
|
||||
func TestEveryBranchShipsTheAppBuiltFromItsOwnCommit(t *testing.T) {
|
||||
jobs := releaseJobs(t)
|
||||
|
||||
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
|
||||
build := strings.Join(strings.Fields(jobs["android-release"].If), " ")
|
||||
for _, ref := range []string{"refs/heads/main", "refs/heads/dev", "refs/tags/v"} {
|
||||
if !strings.Contains(build, ref) {
|
||||
t.Errorf("android-release does not build on %s, so that ref's image carries no app of its own\n if: %s", ref, build)
|
||||
}
|
||||
// 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)
|
||||
}
|
||||
|
||||
image := strings.Join(strings.Fields(jobs["image-release"].If), " ")
|
||||
if !strings.Contains(image, "needs.android-release.result == 'success'") || strings.Contains(image, "'skipped'") {
|
||||
t.Errorf("image-release can publish without a freshly-built APK\n if: %s", image)
|
||||
}
|
||||
|
||||
yaml := releaseYAML(t)
|
||||
if strings.Contains(yaml, "releases/latest") {
|
||||
t.Error("release.yml fetches the latest release; an image bundling a release's APK ships an app older than its own commit")
|
||||
}
|
||||
|
||||
// Both bundling steps run on every ref. Sliced from image-release on,
|
||||
// because release-assets has a step of the same name.
|
||||
i := strings.Index(yaml, "\n image-release:")
|
||||
if i < 0 {
|
||||
t.Fatal("no image-release job")
|
||||
}
|
||||
for _, name := range []string{"Download signed APK artifact", "Stage bundled APK + version sidecar"} {
|
||||
step := stepBody(t, yaml[i:], name)
|
||||
if strings.Contains(step, "github.ref") {
|
||||
t.Errorf("%q is gated by ref, so some ref's image ships without its APK\n%s", name, step)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -376,7 +344,7 @@ 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)
|
||||
t.Errorf("tag arm does not refresh :latest — the server would hand out a different APK build from the one attached to the Release\n%s", arm)
|
||||
}
|
||||
if strings.Contains(arm, "GITHUB_SHA") {
|
||||
t.Errorf("tag arm mints a :<sha> image; that would re-push main's immutable rollback target with different bundled contents\n%s", arm)
|
||||
|
||||
Reference in New Issue
Block a user