Commit Graph
7 Commits
Author SHA1 Message Date
logan b2fe1cb1e0 ci: skip a prerelease tag in all four publishers
Their trigger is `v*`, which matches `v0.4.0-beta.1`. They guarded
`v0.0.0` -- the version floor -- and nothing else, so the first
prerelease tag would have published a beta everywhere.

Nothing produces one today. The guard is here because the thing that
would is `prerelease: true` in .releaserc.yml, a one-line change whose
blast radius is four public channels and which nothing in those four
files mentions. That is the same argument release.yml's `chore(release):`
guard is kept on: cheap, against something a future edit turns on
somewhere else entirely.

android-apk is the worst of the four twice over. Its APK goes to the
*generic* registry, which is readable without credentials so Obtainium
can poll a plain URL, so a beta would be offered to every device on it.
And its versionCode maths splits on dots: it would read "1" out of
"0-beta" and produce a wrong number rather than a failed build, which
matters because Android orders releases by that integer and refuses
anything not greater than what is installed.

Each is a clean skip rather than a failure, matching the v0.0.0 guard
beside it: a red run against a tag that was never meant to ship is noise.
2026-08-18 22:25:11 -04:00
logan 37e3373db9 docs: correct the workflow counts these comments name
CI / check (push) Skipped
CI / e2e (push) Skipped
CI / check (pull_request) Failing after 1m35s
CI / e2e (pull_request) Skipped
Adding release.yml and desktop-assets.yml made 'the three workflows a
tag fires' wrong in three files that each said it slightly differently.
2026-08-17 19:47:55 -04:00
logan 2c576fa1e8 ci(release): attach the Linux, Arch and Android builds to the release
A release page with nothing to download is one nobody can use. The
Arch package and the APK are already built and merely go unattached;
the plain Linux binary is new, and is what answers 'get the latest
version' without a package manager.

scripts/release-asset.sh waits for the release to exist first.
semantic-release pushes the tag in prepare and creates the release in
publish, so the tag push that starts these workflows happens before
there is an id to upload to -- and a capacity-1 runner serialises that
into working by accident, which is the worst kind of bug.

macOS is absent because it cannot be built here: GOOS=darwin
CGO_ENABLED=0 fails at wails/v3/pkg/mac, the darwin backend being
Objective-C behind cgo. Homebrew builds from source on the user's Mac
and stays the macOS channel. Windows cross-compiles cleanly and is
still withheld: no build of it has ever been run.

All three skip v0.0.0, which is semantic-release's version floor rather
than a shipment.
2026-08-17 18:39:06 -04:00
logan c99c8efa11 ci(android): tell a wrong password apart from a wrong keystore
The v1.5.0 run reported that the keystore did not open, and the
diagnostics could not say why. They now clear the two causes that look
identical to a wrong password.

**A password pasted with its shell quotes** is two characters longer
than the password and nothing in keytool's error says so. The step
retries with the surrounding quotes stripped and, if *that* opens the
keystore, says exactly that. It does not strip them and carry on: a
password may legitimately contain a quote, so this reports a diagnosis
rather than guessing at a fix.

**A password that is right for a different keystore** is the other one,
and it is the one currently in play -- the secret decodes to a valid
2280-byte PKCS12 and the password is the length the owner expects, which
leaves "is this the keystore I have locally?" as the open question. The
step prints the decoded file's sha256 so that is answerable by
comparing one line against sha256sum. Hashing a certificate store gives
nothing away.
2026-08-16 22:26:29 -04:00
logan b6651310ea build(android): drop the x86_64 ABI, which no Android can run
The fat APK's second half was 31 MB that cannot execute on any Android
device. modernc.org/libc's Xlstat64 issues a raw lstat syscall on
linux/amd64, and Android's seccomp policy forbids it because bionic
never issues it, so the process takes SIGSYS the first time anything
touches the database -- which for this app is startup. That is every
x86_64 Android, x86 Chromebooks included, not merely the emulator.
arm64 is structurally unaffected: the architecture has no lstat syscall
at all, so modernc routes through fstatat.

27,059,130 bytes to 15,898,465, and one lib/ entry.

Three places had to agree, and the third is what would have made this a
silent no-op: abiFilters (what Gradle packages), android:package rather
than package:fat (what Go *compiles* -- otherwise the library is still
built and then discarded), and the native-code assertion in CI. That
assertion is anchored, `native-code: 'arm64-v8a'$`, because without the
anchor it also matches the fat APK's line and would pass on exactly the
thing it exists to catch. Checked against a real artifact.

Adding the ABI back, if modernc ever fixes Xlstat64, is those same
three edits.
2026-08-16 22:26:11 -04:00
logan 01706c6053 ci(android): say why the keystore did not open
CI / check (push) Canceled after 0s
CI / e2e (push) Canceled after 0s
Search index maintenance / maintain-index (push) Canceled after 0s
Build & publish Arch package / arch-package (push) Successful in 2m41s
"the keystore did not open — is ANDROID_KEYSTORE_PASSWORD right?" is a
guess, and there are three quite different reasons behind it. The step
distinguishes them now.

**A secret pasted into a web form very often carries a trailing
newline**, and a password is compared byte for byte, so the run failed
with a password that was correct. Reproduced exactly: keytool rejects
`Correct123\n` against a keystore whose password is `Correct123`. CR
and LF are stripped from the password, the alias and the key password
now, and the step says when that mattered.

**A wrong alias failed a minute later, inside Gradle.** It defaults to
`yellowjacket`, so any keystore created with another alias got there.
The alias is checked up front and the failure lists the aliases the
keystore actually holds.

**And a truncated or mis-pasted base64 is a different problem from a
bad password**, so the artifact is described before it is opened: size
and its first four bytes, named as PKCS12 or legacy JKS, with a warning
when the header is neither. A truncation shows up as 300 bytes against
2564.

Verified against real keystores for all five cases: correct, trailing
newline, wrong password, wrong alias, truncated base64.

Decode and build are one step now. Splitting them would mean either
handing the password to a later step through $GITHUB_ENV -- where the
env dump is only masked for values that are verbatim a secret, so a
trimmed one could print in clear -- or repeating the trimming in both.
The failure message also prints the password's length, which is the
one thing that distinguishes "wrong value" from "invisible whitespace",
and only on failure.
2026-08-16 17:00:36 -04:00
logan 0c6ca72cf1 ci(android): publish a signed APK on every version tag
Builds the fat APK and puts it in Gitea's *generic* package registry,
which unlike the repository is readable without credentials -- the
reason an Obtainium client can poll a plain URL with no token and no
public mirror of the source. A versioned copy for history, a fixed
`latest` URL to watch.

**Its own workflow, not a job in ci.yml.** That workflow runs on every
branch push and is the one that gates; this takes tens of minutes on a
cold cache and the runner has capacity 1, so hanging it off the gate
would put every push behind an SDK download.

**Keyed on the tag.** The ljos pipeline this is modelled on computes a
version in CI and cuts the release itself, then gates its Android job
on needs.release.outputs.version with an always() whose absence
silently kills the manual path. This repo has no release automation --
tags are pushed by hand and homebrew-formula.yml already keys on v* --
so the tag is the version and none of that machinery, or its failure
modes, is needed.

**No continue-on-error**, which that pipeline does carry: there the
Android job shares a workflow with a server deploy that must never go
red over a phone build. Here it is standalone and can neither delay nor
redden anything, so a release step that fails silently would be
strictly worse than one that fails visibly.

Four gates before anything is published, each checked against a real
APK: a non-empty artifact, both ABIs present, a versionCode equal to
the one derived from the tag, and -- verified by pointing it at a
deliberately debug-signed build, which it refused -- **not signed with
the debug key**. Android refuses to update an app whose signing
certificate changed and the only remedy is an uninstall that takes the
user's library with it, so the job also refuses to *build* without the
keystore secret rather than falling through to Gradle's debug default.

The keystore is opened with `keytool -list` before Gradle runs, because
Gradle only notices a bad password at :app:validateSigningRelease, a
minute of build time in, and reports it as a missing file. And nothing
pipes into `head`: under pipefail it exits after one line, the producer
takes SIGPIPE and the step fails with 141 having already printed a
perfectly good APK.

Two secrets, not four. keytool has produced PKCS12 by default since
JDK 9 regardless of the .jks extension, and PKCS12 cannot hold a key
password distinct from the store password -- given one it says so and
ignores it. So ANDROID_KEY_PASSWORD defaults to the store password and
the alias to a documented default.

The Wails CLI needs no caching hack here: it is a vendored `go tool`
and the runner already bind-mounts GOCACHE for every job, so it is warm
from ci.yml's own bindings-check. A fourth cache volume for
GRADLE_USER_HOME saves ~700MB a run.
2026-08-16 15:31:18 -04:00