cf2854a0291012525f5d4151367c9f2135d2df95
13
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
c2fdc05e5c |
release: a tag builds nothing and carries a changelog instead
Android / Build, or is the channel already serving this? (push) Successful in 3s
CI & Build / Build now, or wait for Android? (push) Successful in 3s
CI & Build / Python lint (push) Successful in 4s
Desktop (Tauri) / Build, or is the channel already serving this? (push) Successful in 4s
Desktop (Tauri) / Tauri desktop (Linux) (push) Successful in 5m17s
Desktop (Tauri) / Update manifest (push) Successful in 4s
CI & Build / TypeScript typecheck (push) Successful in 7s
CI & Build / Python tests (push) Successful in 12s
CI & Build / integration (push) Successful in 18s
CI & Build / Build & push image (push) Skipped
Desktop (Tauri) / Windows installer (cross-compiled) (push) Successful in 3m0s
Android / Kotlin + Rust (APK) (push) Successful in 8m4s
Step 7 of M314, the last one. Rule 22 — the old path comes out completely. ## A release stops building `desktop.yml` no longer triggers on `v*`, and its two `Publish release` steps are gone. `ci.yml` lost its tag trigger in step 6. So a tag now reaches exactly one lane: the new `release.yml`, which builds nothing. That is not a simplification for its own sake. The merge to `main` already published everything a user can receive — `:latest` + `:<sha>`, both channel feeds, the updater manifest. A tag rebuilding that source produces identical artifacts under identical names and re-pushes `:<sha>` with different bytes, which rule 145 forbids even when they match. ## So what a release is FOR The changelog (note 3127 §5). Two halves to "what am I running", and the version answers only the first: which build is this (the footer, /api/config, the APK's versionName) and what is in it that was not in the one I ran last month (nothing, until now). `packaging/release-notes.sh` derives it from git rather than a hand-maintained CHANGELOG, which drifts into recording what someone MEANT to ship. Capped at 60 entries with the omitted count stated — the first dated release spans 181 commits since `v0.1.0`, and a truncated list that does not say it is truncated is a lie. It publishes through `publish-release.sh` rather than making its own API calls, for the create-or-PATCH-on-409 path: a fixed-tag release that only ever POSTs keeps whatever body its first run wrote, which is #2182, and reimplementing that correctly in a second place is how it comes back. ## Retired `MANIFEST_TAG` and the whole branch behind it. It let the manifest live on a `stable` pointer release while the bundles sat on a versioned one — a split step 3 removed when `stable` started holding its own bundles. Nothing had passed it since; a parameter that can only ever receive its own default is a branch nobody exercises and a comment that goes stale, and its stale text was still telling readers the installable builds live on the versioned releases. `desktop/src-tauri/Cargo.toml`'s version and `thoughtsync/__init__.py`'s both now say out loud that they are not shipped values. The Cargo one carries the history worth keeping: the old scheme took its base from that line, so `0.2.<run>` on dev outranked a bare `0.2.0` on main, and the remedy was "remember to bump the minor before tagging" — documented in a comment, enforced nowhere. #2183 is what that looked like in the field. **That ritual is now formally dead**, and this is the deliberate act of killing it rather than a side effect. ## Still there on purpose `install.sh`'s transitional stable fallback. It cannot go until `main` has published to `stable` at least once, and that is gated on an operator request. Removing it now would break the DEFAULT install channel. #3147 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
c268ae4f23 |
ci: main publishes, so a tag stops being required — and :latest stops shipping a dev client
CI & Build / Build now, or wait for Android? (push) Successful in 3s
CI & Build / Python lint (push) Successful in 3s
CI & Build / TypeScript typecheck (push) Successful in 7s
CI & Build / Python tests (push) Successful in 11s
CI & Build / integration (push) Successful in 17s
CI & Build / Build & push image (push) Skipped
Desktop (Tauri) / Windows installer (cross-compiled) (push) Successful in 3m21s
Desktop (Tauri) / Tauri desktop (Linux) (push) Successful in 6m36s
Desktop (Tauri) / Update manifest (push) Successful in 4s
Android / Kotlin + Rust (APK) (push) Successful in 9m24s
Step 3 of M314. Note 3127 §0's diagnostic is "is `main` publishing sufficient for a user to receive the build" — and here it was not. The desktop and Android lanes BUILT on main and published nothing: `Publish release` was gated on `refs/tags/v*`, the channel publishes on `refs/heads/dev`, the manifest job on dev-or-tag. So the stable channel moved only when somebody cut a tag, which made a `v*` tag load-bearing rather than the optional bookmark the model wants. Both channels are rolling fixed-tag releases now. `dev` from dev, `stable` from main, same machinery — `publish-release.sh` already took RELEASE_TAG, `write-manifest.sh` already pruned, and both already PATCHed a stale description on 409 (#2182). This is wiring, not new mechanism. ## The defect this carried `ci.yml`'s "Fetch the Android client to bake in" read `releases/download/dev` UNCONDITIONALLY, on every branch. Every image baked in the dev APK — `:latest` included — so a stable server served a dev-channel client to anyone who downloaded it from there. That has nothing to do with versioning; it is fixed here because this is the step that finally gives `stable` an APK to point at. It also means Android needs no channel machinery of its own. The APK is served FROM the image, so the channel is already a property of which image you run — note 3127 §7's "nothing to hand off" shape, arrived at here by accident. One branch-conditional line, not a second channel in `client_dist.py` as this milestone first assumed. ## The break this nearly shipped `install.sh --channel stable` read the version out of `stable/latest.json` and then fetched `releases/tags/v<version>` for the bundles — correct while stable was a manifest-only pointer, and broken the moment stable holds its own. Stable is the DEFAULT channel, so `curl … | sh` would have failed for everyone between this commit and the first merge to main. Both channels are one lookup now: fetch the fixed-tag release, install what is on it. A transitional fallback covers the window where `stable` still has no bundles, marked for deletion in step 7 — without it the default channel is broken for however long it takes to merge, and that window is gated on an operator request rather than on this lane. ## The two writers problem `stable`'s manifest was written by tag builds. It is written by main now, and the tag path stops writing it — two writers for one channel is a race with no winner worth having. A `v*` tag still writes its own versioned manifest; its build consequence goes entirely in step 7. Also corrected: `update.rs`'s header still described stable as following `v*` tags. Nothing in that file moved — it only ever read `<channel>/latest.json` — but the comment was a lie, and it is the file somebody reads to understand the feed. #3143 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
e6da720e6b |
packaging: drop assets that aren't there, instead of trusting nullglob
`d77a798` added the Android client to publish-release.sh's asset list and broke the desktop lane's publish, which had been working (run 4094, curl exit 26 — "couldn't read local file"). The Android lane published fine, which is what made the shape of the mistake clear. `shopt -s nullglob` drops PATTERNS that match nothing. The two entries I added — `android/dist/thoughtsync.apk` and its sidecar — contain no wildcard, so they are not patterns at all: globbing leaves them in the array verbatim and curl is handed a path to a file that does not exist. In the Android job those files are there, so it worked; in the desktop job they never are, so it did not. Every entry is now filtered on existence, which is what the array has always meant. That covers the literal paths and the globs alike, rather than relying on each future entry containing a `*` to be safe — the trap that just cost a run. Verified both ways before pushing: a literal missing path survives nullglob and is removed by the filter, and an all-empty result still exits cleanly under `set -u`. |
||
|
|
d77a79859c |
server: hand out the Android client this server syncs with (2726)
CI & Build / Python tests (push) Successful in 11s
CI & Build / Python lint (push) Successful in 3s
CI & Build / TypeScript typecheck (push) Successful in 6s
CI & Build / Build & push image (push) Successful in 50s
Desktop (Tauri) / Windows installer (cross-compiled) (push) Failing after 3m8s
Desktop (Tauri) / Tauri desktop (Linux) (push) Failing after 5m32s
Desktop (Tauri) / Update manifest (push) Skipped
Android / Kotlin + Rust (APK) (push) Successful in 8m8s
A self-hoster should not need an account on someone else's forge to get the app
for their own notes. The Fabled-Git instance is private — which is why
`install.sh` already cannot fetch for anyone but the operator — so a release page
is no use as a distribution point. The server holding the notes is something the
person already trusts and already reaches.
It also keeps the pair in step by construction. Client and server negotiate a
sync protocol version before linking, so a server that also serves the client
cannot hand out a phone it is unable to talk to.
**Two files, and both must be present**: `thoughtsync.apk` and a
`thoughtsync-android.json` sidecar carrying `{version_name, version_code, size,
sha256}`. The sidecar exists because an APK keeps its version in a binary AXML
manifest, which Python cannot read and which is not worth putting `aapt` on a
Quart server to reach. CI writes it beside the APK, where the values are already
known — including the digest, computed over the same bytes it uploads, so a
phone can tell a truncated download from a complete one before handing it to the
installer. Not a trust anchor; the signature is that.
**Under DATA_DIR, not baked into the image.** Baking charges ~55 MiB to every
self-hoster including everyone who never touches Android. `/var/thoughtsync` is
already the mounted volume that holds attachments, so a build dropped there
survives container recreation.
**Absence is an ordinary state, not an error.** No APK means the key is absent
from `/api/config` — absent rather than null, so a client testing for it cannot
confuse "this server has no client" with "this server predates the field" — the
web UI hides the card instead of offering a button that 404s, and the metadata
route answers 404. A server whose owner does not use Android is not misconfigured.
**A mismatched pair also counts as no client.** If the sidecar's recorded size
does not match the file on disk, the two did not arrive together; serving one
build while advertising another is worse than serving none, because the phone
would compare versions against a promise the bytes do not keep. That makes the
copy order in docs/android-distribution.md load-bearing, and it is written down
there: APK first, sidecar last.
**The version is public, the bytes are not.** An updater has to be able to ask
"is there something newer?" cheaply and before it has done anything; 55 MiB is
not for anyone who can reach the port. `login_required` already accepts either a
session cookie or a device bearer token, so the browser and a linked phone both
work with no second auth path.
The Android lane now publishes both files to the same rolling `dev` release the
desktop bundles use, reusing `publish-release.sh` — its nullglob asset list was
already built for several jobs in separate workspaces publishing to one release,
which is exactly this. Signed builds only: publishing an unsigned APK would offer
people something they cannot install over what they already have.
Nine tests, DB-free like the rest of the suite — this lane runs no Postgres, so
the advertisement is asserted through `advertisement()` rather than through
`/api/config`, whose other half needs a database. Both routes ARE exercised,
because neither opens a session.
|
||
|
|
0a7480cf9b |
core: extract the store and sync engine into a shared crate (M12 step 1)
Android becomes a native Kotlin client over this same code (Scribe note 2730), so
the local store and sync engine stop being modules of the desktop app and become
`thoughtsync-core`, a crate with no UI framework in it at all.
This is a move, not a rewrite, and the measurement is why: every file in local/
and sync/ already carried ZERO Tauri references — 4,980 of 6,372 lines. The
coupling was 473 lines of command shim, which stays behind in the desktop crate
as src/commands/. Kept as git renames so history follows the files.
The desktop imports them under their old names (`use thoughtsync_core::{local,
sync}`) so every call site reads exactly as before. What moved is where they
live, not what they are.
Two things a workspace changes that are easy to miss, both caught before pushing:
[profile.release] now lives at the workspace ROOT. Cargo silently ignores
profiles declared by a non-root member — leaving it in the desktop crate would
have dropped lto/strip/opt-level from every release build with only a warning.
And a workspace shares ONE target dir, so the bundles moved from
desktop/src-tauri/target to target/. Thirteen references across publish-release,
debundle-graphics, verify.sh, package-prebuilt and the workflow now point there.
Pinning target-dir back would have been the smaller diff, but the Android lane
also produces Rust artifacts and they do not belong under desktop/.
Also retires the Tauri Android lane in the same push rather than leaving a path
that is being replaced: gen/android, android.yml and docs/android-dev.md are
gone, the mobile_entry_point attribute with them, and the lib drops to rlib —
staticlib/cdylib existed for Tauri mobile, and the .so Android loads will be
built from the core crate instead. Rule 22, no parallel path.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
13e48672c0 |
packaging: bare backticks — a heredoc's backslash isn't the JSON's
Run 2981 built everything and then died posting the release: HTTP 422, "invalid escape sequence \`". The body's other backticks are written \` because they sit in an UNQUOTED heredoc, where that backslash is the shell's and is gone before any JSON exists. Copying the idiom into a single-quoted variable changed what it meant — single quotes already stop substitution, so the backslash survived into the body as an escape JSON has no rule for. bash -n passes either way; it checks syntax, not what a string becomes. So parse the assembled body for every branch it can take instead, and write down the recipe next to the one for formatting Rust. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MKsUY9Z45KQd34V956hZ9Q |
||
|
|
d8b0cd9b96 |
packaging: the installer learns the same two channels the app updates on
install.sh asked /releases/latest and installed whatever came back. That is v0.1.0 today, which predates the updater, and it was about to get worse: the `stable` pointer release write-manifest.sh creates is non-prerelease and holds only latest.json, so from the next v* tag onward it would have WON /releases/latest and the installer would have found nothing to install. So resolve a channel instead of a "latest". `--channel stable|dev` (or TS_CHANNEL), default stable, named to match update.rs's Channel exactly. dev reads /releases/tags/dev. stable reads the pointer's own latest.json, takes its version, and installs that v* release — the same file the app reads, so the installer and the updater cannot disagree about what stable means. Two things found on the way. The dev release's description still told people to run the stable command, and always would have: publish-release.sh writes a body only when it CREATES a release, and a fixed-tag release is only created once, so the text froze at the first build. The 409 path now PATCHes it. And the asset greps were unanchored, so a .AppImage.sig URL could match as the bundle URL — harmless by coincidence, not by construction. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MKsUY9Z45KQd34V956hZ9Q |
||
|
|
d6734cf7a0 |
desktop: in-app updates, two channels, signed, fed by fixed-tag releases
CI & Build / Python lint (push) Successful in 3s
CI & Build / TypeScript typecheck (push) Successful in 6s
CI & Build / Python tests (push) Successful in 10s
CI & Build / Build & push image (push) Successful in 30s
Desktop (Tauri) / Tauri desktop (Linux) (push) Failing after 1m59s
Desktop (Tauri) / Windows installer (cross-compiled) (push) Successful in 2m23s
Desktop (Tauri) / Update manifest (push) Has been skipped
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 |
||
|
|
ab961f13ce |
desktop: cross-compiled Windows NSIS installer lane (task 2015)
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 |
||
|
|
dc68386d1a |
desktop: point the install command at a branch that exists
Desktop (Tauri) / Tauri desktop (Linux) (push) Successful in 3m27s
The advertised curl URL referenced raw/branch/main, but main has never been created (creating it is rejected by a branch-protection rule that matches the name even with no branch behind it), so the one-command install 404'd. dev is currently the repo's only branch and serves the script fine now that the repo is public — verified 200, with all three v0.1.0 release assets resolving and the AppImage downloading in full. Flagged in the header to move back to main once that branch exists, so the public install command stops tracking day-to-day work. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SreJkbxB4gx8pPsu8QbLPi |
||
|
|
85ca7c2a2d |
desktop: drop the redundant deb depends, correct the pacman docs
CI & Build / Python lint (push) Successful in 2s
CI & Build / TypeScript typecheck (push) Successful in 7s
CI & Build / Python tests (push) Successful in 8s
CI & Build / Build & push image (push) Successful in 17s
Desktop (Tauri) / Tauri desktop (Linux) (push) Successful in 3m28s
Follow-up to
|
||
|
|
8a8b2b17e6 |
desktop: prebuilt pacman package + verified .deb (tasks 2022, 2074)
Desktop (Tauri) / Tauri desktop (Linux) (push) Successful in 3m41s
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 |
||
|
|
36c05f5029 |
desktop: release-publish pipeline + one-command Linux installer
Desktop (Tauri) / Tauri desktop (Linux) (push) Successful in 3m45s
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 |