Follows #50, which unpacks and repacks every AppImage to demote the bundled Wayland client. This adds the metadata an AppImage manager needs while the image is already open, so the cost is a few lines rather than a second pass. unbundle-wayland-client.sh becomes finalize-appimage.sh, since it now does more than unbundle.
What was missing
An AppImage manager (Gear Lever, AppImageLauncher and similar) can adopt the current build but can never update it:
No update information. This is the string embedded in the image that says where to look for a newer build. Without it there is nothing for a manager to check, and updating is manual forever.
No AppStream metadata, so a manager has only a filename to show. appimagetool has been warning about this on every build.
Categories= is empty in the shipped .desktop, because linuxdeploy leaves it so. That is what a desktop menu and most managers use to file the app, so it currently lands nowhere. Now Development;Utility;.
The update URL is a fixed tag, not releases/latest
Updates are pulled from the GitHub mirror, so that is where the URL points — at a fixed linux-latest tag holding exactly two files, replaced on every release.
releases/latest was the obvious choice and is the wrong one. It follows whichever release is newest, and the Gitea→GitHub backfill creates one GitHub release per Gitea tag — including the -win and -mac tags, which carry no AppImage. A URL that can resolve to a release with no AppImage in it does not fail in CI or on a maintainer's machine; it fails on every installed copy, silently, at the moment it matters. A fixed tag cannot be pointed at a release that has none, and is equally immune to a release marked prerelease.
scripts/publish-update-channel.sh maintains that tag, and verifies both published URLs return 200 before it reports success — a silent failure here stops every installed copy updating.
Why the artifact is named for the tag
Not cosmetic. zsync records a relative filename which a client resolves against the URL it fetched the .zsync from:
A versioned name there would send every client after the build it already has. The stable name is what makes the fixed tag work.
This is also where a wrong guard earned its place. My first assertion checked the .zsync for the update tag and failed. The tag is not there — it lives in the image's own .upd_info ELF section:
The two files carry different halves of the mechanism, and the guard found that distinction instead of shipping past it.
Deltas, confirmed before building on them
Range requests against the mirror return 206 with a correct content-range, so an update pulls the changed blocks rather than re-downloading 85 MB.
The .deb and .rpm are dropped
Two more artifacts to build, publish and keep working, for an audience already served by the one file that runs on every distribution — and neither could ever self-update, which is the difference that now matters. Older releases keep theirs.
The Linux job passes --bundles appimage rather than editing tauri.conf.json, so macOS and Windows are untouched. HOW-TO-USE.md's install table collapses to a single Linux row.
Verification
Against the real 0.4.19 artifact, not a synthetic AppDir:
repacks; the AppStream file and the filled-in Categories land inside the image
the embedded update string resolves to the fixed tag
the #50 wayland fallback still holds — library demoted, fallback copy and hook intact
both publisher failure paths refuse rather than half-publishing: no GH_PAT, and missing artifacts
The script's post-repack assertions are the only test this has, which is deliberate — each one is a way the build could look like it worked while shipping an image that is silently unmanageable.
Not verifiable until CI runs: that the pipeline produces a working AppImage end to end, and that linux-latest publishes. The publisher checks its own URLs, so a failure there fails the build rather than going quiet.
Follows #50, which unpacks and repacks every AppImage to demote the bundled Wayland client. This adds the metadata an AppImage manager needs while the image is already open, so the cost is a few lines rather than a second pass. `unbundle-wayland-client.sh` becomes `finalize-appimage.sh`, since it now does more than unbundle.
## What was missing
An AppImage manager (Gear Lever, AppImageLauncher and similar) can adopt the current build but can never update it:
- **No update information.** This is the string embedded in the image that says where to look for a newer build. Without it there is nothing for a manager to check, and updating is manual forever.
- **No AppStream metadata**, so a manager has only a filename to show. `appimagetool` has been warning about this on every build.
- **`Categories=` is empty** in the shipped `.desktop`, because linuxdeploy leaves it so. That is what a desktop menu and most managers use to file the app, so it currently lands nowhere. Now `Development;Utility;`.
## The update URL is a fixed tag, not `releases/latest`
Updates are pulled from the GitHub mirror, so that is where the URL points — at a fixed `linux-latest` tag holding exactly two files, replaced on every release.
`releases/latest` was the obvious choice and is the wrong one. It follows whichever release is newest, and the Gitea→GitHub backfill creates **one GitHub release per Gitea tag** — including the `-win` and `-mac` tags, which carry no AppImage. A URL that can resolve to a release with no AppImage in it does not fail in CI or on a maintainer's machine; it fails on every installed copy, silently, at the moment it matters. A fixed tag cannot be pointed at a release that has none, and is equally immune to a release marked prerelease.
`scripts/publish-update-channel.sh` maintains that tag, and verifies both published URLs return `200` before it reports success — a silent failure here stops every installed copy updating.
## Why the artifact is named for the tag
Not cosmetic. zsync records a **relative** filename which a client resolves against the URL it fetched the `.zsync` from:
```
Filename: Triple-C_x86_64.AppImage
URL: Triple-C_x86_64.AppImage
```
A versioned name there would send every client after the build it already has. The stable name is what makes the fixed tag work.
**This is also where a wrong guard earned its place.** My first assertion checked the `.zsync` for the update tag and failed. The tag is not there — it lives in the image's own `.upd_info` ELF section:
```
zsync|https://github.com/shadowdao/triple-c/releases/download/linux-latest/Triple-C_x86_64.AppImage.zsync
```
The two files carry different halves of the mechanism, and the guard found that distinction instead of shipping past it.
## Deltas, confirmed before building on them
Range requests against the mirror return `206` with a correct `content-range`, so an update pulls the changed blocks rather than re-downloading 85 MB.
## The .deb and .rpm are dropped
Two more artifacts to build, publish and keep working, for an audience already served by the one file that runs on every distribution — and neither could ever self-update, which is the difference that now matters. Older releases keep theirs.
The Linux job passes `--bundles appimage` rather than editing `tauri.conf.json`, so macOS and Windows are untouched. `HOW-TO-USE.md`'s install table collapses to a single Linux row.
## Verification
Against the **real 0.4.19 artifact**, not a synthetic AppDir:
- repacks; the AppStream file and the filled-in `Categories` land inside the image
- the embedded update string resolves to the fixed tag
- the #50 wayland fallback still holds — library demoted, fallback copy and hook intact
- both publisher failure paths refuse rather than half-publishing: no `GH_PAT`, and missing artifacts
The script's post-repack assertions are the only test this has, which is deliberate — each one is a way the build could look like it worked while shipping an image that is silently unmanageable.
**Not verifiable until CI runs:** that the pipeline produces a working AppImage end to end, and that `linux-latest` publishes. The publisher checks its own URLs, so a failure there fails the build rather than going quiet.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_011YPqHpjV4EL6RNEwrRKqQm
An AppImage manager can adopt the current build but never update it: the image
carries no update information, which is the string that tells such a tool where
to look for a newer one. It also carries no AppStream metadata, so a manager
has nothing to show but a filename — appimagetool has been warning about that
on every build — and linuxdeploy leaves `Categories=` empty, which files the
app nowhere in a desktop menu.
All three are fixed while the image is already unpacked for the wayland fix, so
the cost is a few lines rather than a second pass. `unbundle-wayland-client.sh`
is now `finalize-appimage.sh`, since it does more than unbundle.
The update URL is a **fixed** `linux-latest` tag on the GitHub mirror, which is
where updates are pulled from — deliberately not `releases/latest`. `latest`
follows whichever release is newest, and the Gitea-to-GitHub backfill creates
one GitHub release per Gitea tag, including the `-win` and `-mac` tags that
carry no AppImage. A URL that can resolve to a release with no AppImage in it
fails on users' machines and nowhere else.
The output is named for that tag too, and that is not cosmetic: zsync records
a *relative* filename which a client resolves against the .zsync URL it
fetched, so a versioned name would send every client after the build it already
has. Verified by reading the generated header — `Filename: Triple-C_x86_64
.AppImage` — and the image's own `.upd_info` section, which is where the tag
actually lives. My first guard checked the .zsync for the tag and failed
correctly, which is how that distinction got found rather than shipped.
Range requests were confirmed against the mirror before building on them: 206
with a correct content-range, so updates are real deltas rather than an 85 MB
re-download.
The .deb and .rpm go. They are two more artifacts to build, publish and keep
working for an audience already served by the one file that runs on every
distribution, and neither could ever self-update — which is now the difference
that matters. Older releases keep theirs. The Linux job passes
`--bundles appimage` rather than changing `tauri.conf.json`, so macOS and
Windows are untouched.
Verified against the real 0.4.19 artifact: it repacks, the AppStream file and
filled-in Categories land inside the image, the update string resolves to the
fixed tag, and the wayland fallback still holds. Both publisher failure paths
refuse rather than half-publishing — no token, and missing artifacts.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011YPqHpjV4EL6RNEwrRKqQm
jknapp
merged commit 670450ccfd into main2026-09-03 00:28:10 +00:00
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.
Follows #50, which unpacks and repacks every AppImage to demote the bundled Wayland client. This adds the metadata an AppImage manager needs while the image is already open, so the cost is a few lines rather than a second pass.
unbundle-wayland-client.shbecomesfinalize-appimage.sh, since it now does more than unbundle.What was missing
An AppImage manager (Gear Lever, AppImageLauncher and similar) can adopt the current build but can never update it:
appimagetoolhas been warning about this on every build.Categories=is empty in the shipped.desktop, because linuxdeploy leaves it so. That is what a desktop menu and most managers use to file the app, so it currently lands nowhere. NowDevelopment;Utility;.The update URL is a fixed tag, not
releases/latestUpdates are pulled from the GitHub mirror, so that is where the URL points — at a fixed
linux-latesttag holding exactly two files, replaced on every release.releases/latestwas the obvious choice and is the wrong one. It follows whichever release is newest, and the Gitea→GitHub backfill creates one GitHub release per Gitea tag — including the-winand-mactags, which carry no AppImage. A URL that can resolve to a release with no AppImage in it does not fail in CI or on a maintainer's machine; it fails on every installed copy, silently, at the moment it matters. A fixed tag cannot be pointed at a release that has none, and is equally immune to a release marked prerelease.scripts/publish-update-channel.shmaintains that tag, and verifies both published URLs return200before it reports success — a silent failure here stops every installed copy updating.Why the artifact is named for the tag
Not cosmetic. zsync records a relative filename which a client resolves against the URL it fetched the
.zsyncfrom:A versioned name there would send every client after the build it already has. The stable name is what makes the fixed tag work.
This is also where a wrong guard earned its place. My first assertion checked the
.zsyncfor the update tag and failed. The tag is not there — it lives in the image's own.upd_infoELF section:The two files carry different halves of the mechanism, and the guard found that distinction instead of shipping past it.
Deltas, confirmed before building on them
Range requests against the mirror return
206with a correctcontent-range, so an update pulls the changed blocks rather than re-downloading 85 MB.The .deb and .rpm are dropped
Two more artifacts to build, publish and keep working, for an audience already served by the one file that runs on every distribution — and neither could ever self-update, which is the difference that now matters. Older releases keep theirs.
The Linux job passes
--bundles appimagerather than editingtauri.conf.json, so macOS and Windows are untouched.HOW-TO-USE.md's install table collapses to a single Linux row.Verification
Against the real 0.4.19 artifact, not a synthetic AppDir:
Categoriesland inside the imageGH_PAT, and missing artifactsThe script's post-repack assertions are the only test this has, which is deliberate — each one is a way the build could look like it worked while shipping an image that is silently unmanageable.
Not verifiable until CI runs: that the pipeline produces a working AppImage end to end, and that
linux-latestpublishes. The publisher checks its own URLs, so a failure there fails the build rather than going quiet.🤖 Generated with Claude Code
https://claude.ai/code/session_011YPqHpjV4EL6RNEwrRKqQm