Two halves of one mistake, both visible on the dev release right now: the
manifest said 0.1.134 and pointed at ThoughtSync_0.1.132_amd64.AppImage.
The rolling channel accumulates every build's assets, and the manifest picked
its bundle by file extension with `head -1` — the OLDEST match. A client would
have been told 0.1.134 was available, downloaded 0.1.132, installed it, and
been offered 0.1.134 again. Forever.
Signature verification could not have caught it. The old bundle's signature is
perfectly valid for the old bundle; nothing about it says "this isn't the build
the manifest claims". Selection is now matched on the build's own version
string, so the manifest can only ever describe the binary it was written for.
The accumulation is the other half. Nothing can reach a superseded build once
the manifest moves on, and an AppImage is ~100 MB — three pushes had already
left 300 MB of unreachable binaries on the Git host. A rolling channel now
prunes everything but the current build once the manifest points at it.
Versioned releases are untouched: that IS the archive, and the stable pointer's
URLs aim into it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SreJkbxB4gx8pPsu8QbLPi
The de-bundle step deletes the AppImage and repackages it without the host
graphics libraries — necessary, and it runs AFTER tauri signed the original.
So the .sig published on the release described a file that no longer existed,
and every Linux in-app update would have failed signature verification.
Worth naming the failure mode: the error would have said the signature didn't
match, which points at the key, the manifest, or the download — anywhere except
"a later build step rewrote the file after signing it". The Windows lane hid it
too, because nothing post-processes the NSIS installer, so the one platform
already verified working was the one platform that couldn't reveal the bug.
Signs the file that actually ships, and fails the build if no .sig comes out
rather than quietly publishing an unverifiable bundle.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SreJkbxB4gx8pPsu8QbLPi
Three things, all needed before the update loop can be tested.
The public signing key is committed. Verified before trusting it: algorithm
`Ed`, key ID 90E96FEA2F6D9B6A matching its own comment, 32-byte Ed25519 key.
Dev builds now carry a version that RISES. Every build took its version from
Cargo.toml, so each one was 0.1.0 — an installed 0.1.0 would read a manifest
advertising 0.1.0, conclude it was current, and never update. The rolling
channel would have looked broken while working exactly as written. Dev builds
are now 0.1.<ci-run-number>, from one helper shared by both bundle jobs and the
manifest writer, because three separate derivations of "what version is this"
is three chances for the binary and the manifest to disagree.
Plain semver, not a `-dev.N` prerelease: prerelease versions sort BELOW the
release they qualify, so a tagged build would never update to a newer dev one,
and Windows installer metadata wants a numeric X.Y.Z regardless. Bumping the
minor still beats any dev build on the old line — 0.2.0 > 0.1.2932.
The Windows job also gets the signing environment it was missing, so its NSIS
installer is signed too. Without that the manifest would have had a Linux entry
and nothing for the platform actually being tested.
docker-compose.yml is now the production stack, per request: it pulls the
published image instead of building, keeps Postgres OFF the host network, sets
restart policies, health checks and log rotation, and refuses to start without
a POSTGRES_PASSWORD rather than shipping a known one. Volume names are
deliberately unchanged so an existing deployment upgrades in place instead of
silently coming up against an empty database. Development keeps its own
clearly-named file.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SreJkbxB4gx8pPsu8QbLPi
There was no in-place update anywhere. The app never checked, downloaded or
applied anything, and the only published release predates the whole sync arc —
so `install.sh` would hand out a build with no sync in it. Installing from
per-run CI artifacts, which is what's been happening, is not something an
updater can point at: ephemeral, auth-gated, no stable URL.
Two channels, switchable in the app: `stable` follows tagged releases, `dev`
follows every green push.
The feed is a Fabled-Git release asset, not a ThoughtSync server route. This
reverses the lean recorded in task 1998, and the reason matters — a
server-hosted feed can only reach a desktop that has linked a server, and
local-first-with-no-server is the whole premise. An unlinked install has to be
able to update itself.
Each channel reads a `latest.json` on a release whose TAG NEVER MOVES.
That's forced, not stylistic: Forgejo has no /releases/latest/download/<asset>
route (verified — it 404s with no redirect), so "newest" cannot be named in a
URL. `dev` carries the rolling bundles; `stable` is a pointer release holding
only the manifest, whose URLs aim at the versioned release's assets, so nothing
is duplicated.
The manifest is written by a third job that runs after both bundle jobs. They
build in separate workspaces and neither can see the other's output, but one
manifest has to describe both platforms — generating it inside either job would
silently omit the other, and a missing platform reads to a user as "no update
available" rather than as a broken feed. It reads what actually landed on the
release, so it can never advertise a bundle that failed to upload.
Signing is gated on the secret existing, in the script rather than an `if:`
(the secrets context isn't reliably available to step conditions). No key means
no updater artifacts and no publish: a feed the app would refuse to verify is
worse than no feed, because it looks like it works. CI stays green until the
key lands.
On Linux the updater can only replace an AppImage — a deb or pacman install is
owned by its package manager and must never be overwritten underneath it. The
app detects that case up front and says so, instead of failing halfway through
with a permissions error nobody can read.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SreJkbxB4gx8pPsu8QbLPi
Two macro-argument splits and a stray blank line, applied verbatim from run
2900's cargo fmt --check diff.
Also reorders the Linux job so `cargo fmt --check` runs AFTER clippy and the
tests. Fail-fast ordering would normally put the cheapest check first, but
there is no Rust toolchain on the workstation, so this lane is verified
entirely in CI — and a formatting nit failing first SKIPS clippy and the
tests, making a whole cycle teach nothing but whitespace. That has now cost
four cycles in this session alone. It still runs before the 20-40 minute
bundle build, so a fmt failure doesn't burn that either.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SreJkbxB4gx8pPsu8QbLPi
tauri-build needs icons/icon.ico to emit the Windows Resource file, and the
repo only carries the PNG set the Linux bundles use — run 2881 failed with
"icons/icon.ico not found".
Generated in-job from the committed 1024px app-icon.png rather than committing
a hand-made .ico, so there stays one icon of record that can't silently drift
from the brand art. Scoped to the windows job; the Linux bundles don't need it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SreJkbxB4gx8pPsu8QbLPi
Adds a `windows` job to desktop.yml on the new ci-tauri-win image, producing a
Windows -setup.exe without any Windows hardware. A Windows container can't run
on a Linux host, so cross-compilation is the only route: --runner cargo-xwin
supplies the MSVC CRT/SDK (pre-warmed into the image) and links with lld-link,
and makensis builds the installer.
NSIS only. .msi needs WiX v3, a Windows program — per Tauri, ".msi installers
can only be created on Windows". It comes back if a Windows node ever exists.
Kept as a separate job so a Windows-side failure can never block the Linux
artifacts, which are the primary product today. publish-release.sh now globs
the windows target root too; nullglob means each job uploads only what its own
workspace contains, and the release is created once and reused via the 409
path, so both jobs can publish to the same release safely.
No app code changes were needed. The AppImage self-integration UI already
gates on is_appimage (AccountView.vue:131, DesktopIntegrationPrompt.vue:22),
and $APPIMAGE is never set on Windows, so the OOBE prompt and Settings toggle
hide themselves.
Recorded plainly in ci-requirements.md that this is the weakest-verified lane
we have: Tauri calls Linux->Windows cross-compilation "not tested as much" and
a last resort, and a Linux runner cannot execute a Windows binary. Green means
it built. A real Windows machine check is mandatory before trusting a release,
and installers are unsigned until a certificate exists.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SreJkbxB4gx8pPsu8QbLPi
Follow-up to 8a8b2b1, driven by what run 2872's new checks actually printed.
The .deb verification did its job on its first run: tauri already infers
exactly libwebkit2gtk-4.1-0 + libgtk-3-0, so declaring the same two in
tauri.conf.json produced a control file listing each of them twice. Removed
the declaration — verify.sh is the real guard, and it fails the build if
inference ever stops covering what the binary links.
The pacman step revealed ci-tauri carries neither zstd nor bsdtar, so packages
currently ship as .pkg.tar.xz with no .MTREE. Both are working outcomes
(pacman reads xz; only `pacman -Qkk` needs .MTREE), but the docs promised
.zst, so the README, the release notes and ci-requirements.md now describe
what the build actually produces.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SreJkbxB4gx8pPsu8QbLPi
Native packages the installer can actually fetch, before task 2014 wires up
the fetching.
Arch (task 2022, re-scoped): the source PKGBUILD is gone — asking every user
to install rust+node and compile for minutes isn't distribution. Replaced by
desktop/packaging/arch/package-prebuilt.sh, which wraps the binary the Linux
job already built into a .pkg.tar.zst. No second Rust build, no Arch CI image:
the binary bundles nothing and resolves webkit/gtk/soup by soname, identical
on both distros, with SQLite compiled in and glibc used in the safe
built-old/run-new direction. CI is Debian and has no pacman, so the step logs
.PKGINFO plus the full file listing for audit instead of pretending to verify.
Debian (task 2074): install.sh hands the .deb to every Debian/Ubuntu user and
nothing had ever inspected it. tauri.conf.json now declares
libwebkit2gtk-4.1-0 + libgtk-3-0 explicitly rather than trusting inference —
and deliberately declares no appindicator or sqlite dep, since tauri is built
with features=[] and rusqlite is "bundled". desktop/packaging/deb/verify.sh
prints the generated control file, cross-checks it against what the ELF
actually needs via dpkg-shlibdeps, confirms every declared dep exists in apt,
and clean-container installs when a docker CLI is available.
Both artifacts join the run artifact and the tagged release; install.sh grows
a pacman branch so Arch/CachyOS gets a native install instead of the AppImage
fallback. Still no release cut (rule 2).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SreJkbxB4gx8pPsu8QbLPi
Phase A of the desktop Release + install path (M10 / tasks 2014, 1998):
- .forgejo/workflows/desktop.yml: tag-gated "Publish release" step +
contents:write. On a v* tag the build now publishes a Fabled-Git Release
with the de-bundled AppImage + .deb attached — a stable, versioned fetch
target (Actions artifacts are ephemeral/test-only). Dormant on dev/main.
- desktop/packaging/publish-release.sh: creates/reuses the Release via the
Forgejo API using the runner-injected token; idempotent asset replace.
- desktop/packaging/install.sh: curl|sh one-command installer — native .deb
on Debian/Ubuntu, de-bundled AppImage everywhere else (installed to
~/Applications/ThoughtSync.AppImage, matching src/integration.rs so the app
sees itself integrated). AppImage path needs no sudo.
Plumbing only — no release cut (rule 2); activates on the operator's first
v* tag. In-app self-update (tauri-plugin-updater + signed latest.json) is
Phase B, gated on the operator's signing key.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FRgehjoz7Yv8LkUfADxACm
adapters/local.ts implements the repository seam over the M10.4 Tauri commands
via invoke (camelCase args -> the commands' snake_case params); adapters/index.ts
now selects local when running in the desktop shell, rest on web. bridge.ts
exports invoke for it.
This is the commit that resolves the black screen: a fresh desktop launch reads
config + session + notes from the on-device SQLite core, so the router's auth
gate passes with a synthetic local user and the board renders with zero server
and zero account. Account auth, device linking, attachment upload, URL unfurl and
file import reject with a "connect a server" message (no offline meaning yet);
everything else — board, editor, capture, search, filters, labels, checklists,
reminders — works fully offline.
desktop.yml also now rebuilds the app on frontend adapter/bridge changes, since
the desktop bundle embeds the frontend.
Task 1994.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FRgehjoz7Yv8LkUfADxACm
Tauri's AppImage bundles the build host's graphics/display stack
(libEGL/libGL/libdrm/libgbm/libwayland-* + Mesa dri drivers) into usr/lib.
On many end-user systems (Arch/CachyOS, NVIDIA, Wayland) those clash with
the running kernel driver + Mesa and abort with EGL_BAD_PARAMETER -> a black
window (issue 2021). They load before any renderer choice, so the runtime
env fallbacks can't rescue it; per the AppImage excludelist they must come
from the host.
Add a CI-only post-build step (desktop/packaging/appimage/debundle-graphics.sh)
that extracts the built AppImage, strips exactly that graphics/display subset
(keeping webkit/gtk bundled for portability), and repackages in place so the
app falls through to the system's graphics libs. Wired into desktop.yml
between the Tauri build and the artifact upload.
Task 2023. Makes the AppImage the zero-install taste-test vehicle.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FRgehjoz7Yv8LkUfADxACm
Native-first fix for the AppImage black window: a PKGBUILD that builds from source,
linked against SYSTEM graphics libs, so it renders on the host driver.
- desktop/packaging/arch/{PKGBUILD,thoughtsync.desktop,README.md}: `makepkg -si`
installs /usr/bin/thoughtsync + .desktop + icon; deps webkit2gtk-4.1/gtk3/...;
builds the frontend + `cargo build --release` (no tauri-cli). Uses the host
graphics stack -> avoids EGL_BAD_PARAMETER.
- Commit the icon set (desktop/src-tauri/icons/*.png, un-gitignored) so BOTH
`cargo build` (pacman) and `cargo tauri build` (deb/appimage) work without a
generate step; bundle.icon -> the 4 committed PNGs; drop the `cargo tauri icon`
step from desktop.yml.
- Broaden the WebKit software-render hardening (lib.rs) to ALL Linux (was
AppImage-scoped) so the native build also renders if system WebKit is finicky.
Can't CI-test the PKGBUILD (Arch-only; CI is Debian) -- operator builds locally.
desktop.yml re-verifies the deb+AppImage build with the committed icons.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FRgehjoz7Yv8LkUfADxACm
The build produces the .deb + .AppImage fine; upload-artifact@v4 failed with
GHESNotSupportedError (Forgejo has no v4 artifact API). v3 uses the older protocol
the instance accepts, so the bundles become downloadable from the run for
hand-testing.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FRgehjoz7Yv8LkUfADxACm
The Linux AppImage can now install itself into the applications menu, so it behaves
like an installed app instead of a loose file.
- Rust (desktop/src-tauri/src/integration.rs): integration_status / integrate_desktop
/ unintegrate_desktop commands — detect $APPIMAGE, copy the AppImage to
~/Applications, write ~/.local/share/applications/thoughtsync.desktop + embedded
icon, update-desktop-database. Registered in lib.rs.
- Frontend: withGlobalTauri exposes window.__TAURI__.core.invoke; desktop/bridge.ts
(isDesktop + typed invoke, NO @tauri-apps/api dep -> web bundle unaffected);
DesktopIntegrationPrompt (first-run OOBE, remembered) mounted in App.vue;
AccountView "Desktop app" add/remove control. All desktop-guarded -> no-ops on web.
- desktop.yml: upload the .deb + .AppImage as a run artifact (continue-on-error) so
the build is downloadable for hand-testing.
Verified by CI: ci.yml (vue-tsc) for the frontend, desktop.yml (cargo + tauri build)
for the Rust + AppImage.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FRgehjoz7Yv8LkUfADxACm
Separate workflow (not ci.yml) so the ~20-40min Rust/AppImage build only runs on
desktop/** changes, not every backend/frontend push. Builds on the ci-tauri:1.97
image: frontend build -> `cargo tauri icon app-icon.png` (from a committed 1024px
source PNG, sidestepping SVG-input questions) -> cargo fmt --check -> clippy -D
warnings -> cargo test -> `cargo tauri build` (deb + AppImage).
APPIMAGE_EXTRACT_AND_RUN=1 for FUSE-less CI containers. Verifies the M10.2 scaffold
end-to-end and is the foundation for the integrated-AppImage work (task 2013).
Also updates ci-requirements.md with the desktop lane.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FRgehjoz7Yv8LkUfADxACm