Two defects found while preparing v0.9.2. Both had been shipping for months, and both were invisible because the failure mode looked like success.
1. Every release note was the same 1,050 bytes
All 35 releases from v0.0.1 to v0.9.1 published byte-identical generic install instructions, with a "What's New" section that read "See CHANGELOG.md" — a link that does not resolve from a release page. Nobody could learn what changed in any release this project has ever made.
The body now comes from the ## <version> section of CHANGELOG.md, and a missing section fails the release — notes that say nothing are worse than a build that waits for two sentences.
The 35 published release bodies have already been backfilled from the changelog via the tea CLI, so the history reads properly now.
This also corrects something I introduced two commits ago: that change generated the body from bun run release:notes. CLAUDE.md is explicit that its output is "a reviewed draft, not a final changelog", and publishing it unreviewed proved the point immediately — the v0.9.1..HEAD range contains a repo-wide prettier sweep, so every file in src/ counted as changed, their TRACES resolved to nearly the whole requirement matrix, and the draft claimed the release had added the entire application. The script now skips cosmetic commits and reports how many, but stays a local drafting tool.
2. Every release from v0.1.0 to v0.8.2 shipped every Windows installer ever built
Release
Windows installers attached
v0.5.0
12 (11 stale, back to 0.1.0)
v0.8.2
16 (13 stale)
src-tauri/target/*/release/bundle/ is not versioned, cargo never cleans it, and the runner reuses the target directory — so the copy step's bundle/**/*-setup.exe glob collected the lot.
Eight months, unnoticed, because there was nothing to notice it by: the upload loop reported success, the files were genuine, and the release page looked busy rather than wrong. It stopped only because an unrelated cargo-cache change wiped the runner's target/ — so it was dormant, not fixed, and would have resumed on the next release.
The fix
Both desktop builds clear the bundle directory before building, so a stale file cannot exist to be copied. Filtering the copy by version would have hidden it instead of removing it.
The Linux job gets the same treatment. It was never hit only because Linux packaging is newer; the glob is identical.
scripts/check-release-artifacts.sh is the backstop for whatever reintroduces one by a route nobody predicted. It runs before the SBOM, the checksums and the upload — all of which describe the file set, so a stale artifact must be caught before it is hashed and published as part of the release.
Verified against a reconstruction of the real v0.8.2 accumulation:
❌ jellytau_0.4.8_x64-setup.exe carries version 0.4.8
❌ jellytau_0.1.0_x64-setup.exe carries version 0.1.0
❌ 2 artifact(s) belong to a different version than 0.9.2.
Still on the release pages
The stale .exe files themselves are still attached to v0.1.0–v0.8.2. The backfilled notes no longer advertise them, but they remain downloadable. Removing them is destructive and deliberately not done here — say the word and I'll delete them with tea.
Verification
bun run check · 1085 frontend tests + coverage gate · format:check · lint (0 errors) · check:links · traceability 90% ≥ 89% · the guard verified on both a clean set and the real failure.
DR-219, DR-220, UT-210.
Two defects found while preparing v0.9.2. Both had been shipping for months, and both were invisible because the failure mode looked like success.
## 1. Every release note was the same 1,050 bytes
All **35 releases** from v0.0.1 to v0.9.1 published byte-identical generic install instructions, with a "What's New" section that read *"See CHANGELOG.md"* — a link that does not resolve from a release page. Nobody could learn what changed in any release this project has ever made.
The body now comes from the `## <version>` section of `CHANGELOG.md`, and a missing section **fails the release** — notes that say nothing are worse than a build that waits for two sentences.
**The 35 published release bodies have already been backfilled** from the changelog via the `tea` CLI, so the history reads properly now.
This also corrects something I introduced two commits ago: that change generated the body from `bun run release:notes`. CLAUDE.md is explicit that its output is *"a reviewed draft, not a final changelog"*, and publishing it unreviewed proved the point immediately — the `v0.9.1..HEAD` range contains a repo-wide prettier sweep, so every file in `src/` counted as changed, their TRACES resolved to nearly the whole requirement matrix, and the draft claimed the release had added **the entire application**. The script now skips cosmetic commits and reports how many, but stays a local drafting tool.
## 2. Every release from v0.1.0 to v0.8.2 shipped every Windows installer ever built
| Release | Windows installers attached |
|---|---|
| v0.5.0 | 12 (11 stale, back to 0.1.0) |
| v0.8.2 | 16 (13 stale) |
`src-tauri/target/*/release/bundle/` is not versioned, cargo never cleans it, and the runner reuses the target directory — so the copy step's `bundle/**/*-setup.exe` glob collected the lot.
Eight months, unnoticed, because there was nothing to notice it by: the upload loop reported success, the files were genuine, and the release page looked busy rather than wrong. It stopped only because an unrelated cargo-cache change wiped the runner's `target/` — so it was **dormant, not fixed**, and would have resumed on the next release.
### The fix
- **Both desktop builds clear the bundle directory before building**, so a stale file cannot exist to be copied. Filtering the copy by version would have hidden it instead of removing it.
- **The Linux job gets the same treatment.** It was never hit only because Linux packaging is newer; the glob is identical.
- **`scripts/check-release-artifacts.sh`** is the backstop for whatever reintroduces one by a route nobody predicted. It runs *before* the SBOM, the checksums and the upload — all of which describe the file set, so a stale artifact must be caught before it is hashed and published as part of the release.
Verified against a reconstruction of the real v0.8.2 accumulation:
```
❌ jellytau_0.4.8_x64-setup.exe carries version 0.4.8
❌ jellytau_0.1.0_x64-setup.exe carries version 0.1.0
❌ 2 artifact(s) belong to a different version than 0.9.2.
```
## Still on the release pages
The stale `.exe` files themselves are **still attached** to v0.1.0–v0.8.2. The backfilled notes no longer advertise them, but they remain downloadable. Removing them is destructive and deliberately not done here — say the word and I'll delete them with `tea`.
## Verification
`bun run check` · 1085 frontend tests + coverage gate · `format:check` · `lint` (0 errors) · `check:links` · traceability 90% ≥ 89% · the guard verified on both a clean set and the real failure.
DR-219, DR-220, UT-210.
Two defects found while preparing v0.9.2, both of which had been shipping
for months without anything to notice them by.
**Every release note was the same 1,050 bytes.** All 35 releases from
v0.0.1 to v0.9.1 published identical generic install instructions whose
"What's New" section read "See CHANGELOG.md" -- a link that does not
resolve from a release page. A reader learned nothing about what changed
in any release the project has ever made.
The body now comes from the `## <version>` section of CHANGELOG.md, and a
missing section fails the release: notes that say nothing are worse than
a build that waits for a maintainer to write two sentences. The 35
published bodies have been backfilled from the changelog via the tea CLI.
This also corrects something introduced two commits ago. That change
generated the body from `bun run release:notes`, which CLAUDE.md is
explicit about -- its output is "a reviewed draft, not a final
changelog". Publishing it unreviewed proved the point immediately: the
v0.9.1..HEAD range contains a repo-wide prettier sweep, so every file in
src/ counted as changed, their TRACES resolved to nearly the whole
matrix, and the draft claimed the release had added the entire
application. The script now skips cosmetic commits (chore(format),
chore(deps), style) and reports how many rather than silently returning a
smaller set, but it stays a local drafting tool.
**Every release from v0.1.0 to v0.8.2 shipped every Windows installer
ever built.** src-tauri/target/*/release/bundle/ is not versioned, cargo
never cleans it, and the runner reuses the target directory -- so the
copy step's bundle/**/*-setup.exe glob collected the lot. v0.8.2 carried
sixteen installers, thirteen of them stale; v0.5.0 offered users a
download list going back to 0.1.0. Eight months, and nothing to notice it
by: the upload loop reported success, the files were real, and the page
looked busy rather than wrong. It stopped only because an unrelated cargo
cache change wiped the runner's target dir, so it was dormant, not fixed.
Both desktop builds now remove the bundle directory before building, so a
stale file cannot exist to be copied. Filtering the copy by version would
have hidden it instead. The Linux job gets the same treatment: it was
never hit only because Linux packaging is newer, and the glob is
identical.
scripts/check-release-artifacts.sh is the backstop for whatever
reintroduces one by a route nobody predicted. It runs before the SBOM,
the checksums and the upload -- all of which describe the file set, so a
stale artifact has to be caught before it is hashed and published as part
of the release. Verified against a reconstruction of the real v0.8.2
accumulation.
DR-219, DR-220, UT-210.
Preparing v0.10.0 meant building the release locally first. It did not
build. Two separate defects were sitting on master, both invisible to
every gate this project has, for the same reason: nothing in
build-and-test.yml runs `tauri build`. Only a tag does. So the first time
anyone would have discovered either was a failed release.
**Tauri plugin versions had drifted apart.** Tauri refuses to build when a
plugin's Rust crate and npm package are on different minor versions:
tauri-plugin-log (v2.8.0) : @tauri-apps/plugin-log (v2.9.0)
tauri-plugin-updater (v2.9.0) : @tauri-apps/plugin-updater (v2.10.1)
Introduced by the updater and diagnostics work in this same branch --
`cargo add` took what the pinned toolchain allowed while `bun add` took
latest, and the caret ranges let them separate. cargo check, clippy,
cargo test and svelte-check all passed.
Matching upward pulled wry 0.53.5 -> 0.54.2 along with wasm-bindgen,
web-sys and webkit2gtk: the webview layer, which on Linux is the video
playback path. That is not a change to make while cutting a release, so
the npm packages are pinned down to the crates instead -- exactly, not by
caret, since the caret is what allowed the drift. The upgrade is worth
doing deliberately, with a playback check, and ci-operations.md says so.
CI now runs `tauri info`, which performs the same comparison without
building. Verified by reintroducing the mismatch and watching it fail.
**The AppImage target had never been built.** It was added earlier in this
branch because the release notes had advertised an AppImage for months
while tauri.conf.json never produced one. It does not work out of the
box: linuxdeploy carries its own `strip`, too old to parse the .relr.dyn
section modern toolchains emit, and it fails on every bundled library --
strip: libzstd.so.1: unknown type [0x13] section `.relr.dyn'
failed to bundle project `failed to run linuxdeploy`
Ubuntu 23.10+ links with -z pack-relative-relocs by default, so the CI
builder image fails exactly as a modern Arch host does. NO_STRIP=true is
linuxdeploy's documented escape hatch. The resulting 153 MB AppImage was
verified to be well-formed and to actually start.
Without this the release would have failed at the Linux build step --
the artifact check added earlier refuses to publish when no AppImage is
produced, which is the behaviour we want, but it would have refused a
tagged build rather than a local one.
Also: the traceability extractor now reads the tooling shell scripts that
carry TRACES comments. DR-207, DR-213 and DR-220 all had them and were
counted as uncovered because only .ts/.svelte/.rs were scanned. Listed
individually rather than globbing scripts/*.sh -- most implement nothing,
and adding one should be a decision.
DR-221.
The plugin versions could not be matched upward without this: both
tauri-plugin-log 2.9.0 and tauri-plugin-updater 2.10.1 require tauri
^2.10, and the tree was on 2.9.5. So the framework moves with them --
tauri 2.9.5 -> 2.11.5, tauri-build 2.5.3 -> 2.6.3, wry 0.53.5 -> 0.55.1
-- and every plugin's Rust crate and npm package is now pinned to the
same version on both sides.
That upgrade broke Android outright, and the breakage is the interesting
part.
Seven call sites in this crate reach JNI through
ndk_context::android_context(), which reads a process-global pair of
pointers. Nothing here ever set that global. `tao` did -- the windowing
layer under wry, three levels below anything this project names in
Cargo.toml. tao 0.34.5 called initialize_android_context() while starting
the activity and our code read what it left behind. tao 0.35.3 keeps the
same two pointers in a private struct and no longer publishes them.
The result, on every launch, was:
PANIC at ndk-context/src/lib.rs:72: android context was not initialized
8: ndk_context::android_context
9: jellytau_lib::run::{{closure}}
Not a crash in our code, and not a change to our code: an undocumented
side effect of a transitive dependency disappeared. Relying on someone
else to populate a global is a dependency that does not appear in
Cargo.toml and gives no warning when it goes.
src-tauri/src/android_context.rs now owns that invariant instead of
assuming it. JNI_OnLoad captures the JavaVM as the shared library loads
-- the earliest moment available, and nothing in tao, wry or tauri
defines one to collide with. The Context is resolved lazily via
ActivityThread.currentApplication() and pinned as a global reference for
the process lifetime, since ndk_context stores a bare pointer and does
not own it. It publishes the Application rather than the Activity:
SecureStorage.initialize() immediately reduces its argument to
applicationContext anyway, and an Application cannot outlive itself the
way a retained Activity would.
Restoring the global keeps all seven callers untouched. Threading a VM
and Context handle through five credential call sites would have been a
larger change with more risk, on the credential path.
Failure now degrades instead of aborting: it is logged and credentials
fall back to the encrypted-file path, which the app already supports.
Verified on a device, R8-minified, not merely compiled:
[INIT] Android JavaVM and Application published to ndk_context
Android SecureStorage initialized successfully
Android Keystore available via SecureStorage
[INIT] Using system keyring for credential storage
[CodecDetection] Detected 7 video codecs: av1,h263,h264,hevc,...
-- the real keystore path, not the fallback, and the app stays up. None
of this is reachable by CI: nothing there runs the app.
Also fixed here, both found the same way:
- `tauri android build --apk true` is now `--apk`. The CLI took a value
until 2.10; from 2.11 the stray `true` is a positional and the build
fails before starting. Three call sites in build-android.sh and one
in build-release.yml -- the latter builds the signed APK, by far the
most-downloaded artifact.
- scripts/build-android.sh ran `npm install` on its clean-build path in
a bun project, ignoring bun.lock and re-resolving the tree. That is
exactly how the plugin crate/package versions drift apart again.
scripts/check-tooling.sh now fails on any npm/yarn/pnpm invocation or
foreign lockfile, and runs in CI.
DR-222, DR-223.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Two defects found while preparing v0.9.2. Both had been shipping for months, and both were invisible because the failure mode looked like success.
1. Every release note was the same 1,050 bytes
All 35 releases from v0.0.1 to v0.9.1 published byte-identical generic install instructions, with a "What's New" section that read "See CHANGELOG.md" — a link that does not resolve from a release page. Nobody could learn what changed in any release this project has ever made.
The body now comes from the
## <version>section ofCHANGELOG.md, and a missing section fails the release — notes that say nothing are worse than a build that waits for two sentences.The 35 published release bodies have already been backfilled from the changelog via the
teaCLI, so the history reads properly now.This also corrects something I introduced two commits ago: that change generated the body from
bun run release:notes. CLAUDE.md is explicit that its output is "a reviewed draft, not a final changelog", and publishing it unreviewed proved the point immediately — thev0.9.1..HEADrange contains a repo-wide prettier sweep, so every file insrc/counted as changed, their TRACES resolved to nearly the whole requirement matrix, and the draft claimed the release had added the entire application. The script now skips cosmetic commits and reports how many, but stays a local drafting tool.2. Every release from v0.1.0 to v0.8.2 shipped every Windows installer ever built
src-tauri/target/*/release/bundle/is not versioned, cargo never cleans it, and the runner reuses the target directory — so the copy step'sbundle/**/*-setup.exeglob collected the lot.Eight months, unnoticed, because there was nothing to notice it by: the upload loop reported success, the files were genuine, and the release page looked busy rather than wrong. It stopped only because an unrelated cargo-cache change wiped the runner's
target/— so it was dormant, not fixed, and would have resumed on the next release.The fix
scripts/check-release-artifacts.shis the backstop for whatever reintroduces one by a route nobody predicted. It runs before the SBOM, the checksums and the upload — all of which describe the file set, so a stale artifact must be caught before it is hashed and published as part of the release.Verified against a reconstruction of the real v0.8.2 accumulation:
Still on the release pages
The stale
.exefiles themselves are still attached to v0.1.0–v0.8.2. The backfilled notes no longer advertise them, but they remain downloadable. Removing them is destructive and deliberately not done here — say the word and I'll delete them withtea.Verification
bun run check· 1085 frontend tests + coverage gate ·format:check·lint(0 errors) ·check:links· traceability 90% ≥ 89% · the guard verified on both a clean set and the real failure.DR-219, DR-220, UT-210.
The plugin versions could not be matched upward without this: both tauri-plugin-log 2.9.0 and tauri-plugin-updater 2.10.1 require tauri ^2.10, and the tree was on 2.9.5. So the framework moves with them -- tauri 2.9.5 -> 2.11.5, tauri-build 2.5.3 -> 2.6.3, wry 0.53.5 -> 0.55.1 -- and every plugin's Rust crate and npm package is now pinned to the same version on both sides. That upgrade broke Android outright, and the breakage is the interesting part. Seven call sites in this crate reach JNI through ndk_context::android_context(), which reads a process-global pair of pointers. Nothing here ever set that global. `tao` did -- the windowing layer under wry, three levels below anything this project names in Cargo.toml. tao 0.34.5 called initialize_android_context() while starting the activity and our code read what it left behind. tao 0.35.3 keeps the same two pointers in a private struct and no longer publishes them. The result, on every launch, was: PANIC at ndk-context/src/lib.rs:72: android context was not initialized 8: ndk_context::android_context 9: jellytau_lib::run::{{closure}} Not a crash in our code, and not a change to our code: an undocumented side effect of a transitive dependency disappeared. Relying on someone else to populate a global is a dependency that does not appear in Cargo.toml and gives no warning when it goes. src-tauri/src/android_context.rs now owns that invariant instead of assuming it. JNI_OnLoad captures the JavaVM as the shared library loads -- the earliest moment available, and nothing in tao, wry or tauri defines one to collide with. The Context is resolved lazily via ActivityThread.currentApplication() and pinned as a global reference for the process lifetime, since ndk_context stores a bare pointer and does not own it. It publishes the Application rather than the Activity: SecureStorage.initialize() immediately reduces its argument to applicationContext anyway, and an Application cannot outlive itself the way a retained Activity would. Restoring the global keeps all seven callers untouched. Threading a VM and Context handle through five credential call sites would have been a larger change with more risk, on the credential path. Failure now degrades instead of aborting: it is logged and credentials fall back to the encrypted-file path, which the app already supports. Verified on a device, R8-minified, not merely compiled: [INIT] Android JavaVM and Application published to ndk_context Android SecureStorage initialized successfully Android Keystore available via SecureStorage [INIT] Using system keyring for credential storage [CodecDetection] Detected 7 video codecs: av1,h263,h264,hevc,... -- the real keystore path, not the fallback, and the app stays up. None of this is reachable by CI: nothing there runs the app. Also fixed here, both found the same way: - `tauri android build --apk true` is now `--apk`. The CLI took a value until 2.10; from 2.11 the stray `true` is a positional and the build fails before starting. Three call sites in build-android.sh and one in build-release.yml -- the latter builds the signed APK, by far the most-downloaded artifact. - scripts/build-android.sh ran `npm install` on its clean-build path in a bun project, ignoring bun.lock and re-resolving the tree. That is exactly how the plugin crate/package versions drift apart again. scripts/check-tooling.sh now fails on any npm/yarn/pnpm invocation or foreign lockfile, and runs in CI. DR-222, DR-223.