Skip to content

ci(release): produce the update feed the app has been polling for - #259

Merged
oratis merged 1 commit into
mainfrom
ci/updater-feed
Aug 9, 2026
Merged

ci(release): produce the update feed the app has been polling for#259
oratis merged 1 commit into
mainfrom
ci/updater-feed

Conversation

@oratis

@oratis oratis commented Aug 9, 2026

Copy link
Copy Markdown
Owner

Stacked on #252.

The app has been asking for a file nobody makes

tauri.conf.json has carried this since the desktop app shipped:

"updater": {
  "active": true,
  "endpoints": ["https://github.com/oratis/deepcode/releases/latest/download/latest.json"],
  "pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6…"
}

Nothing ever produced latest.json. So the app polls, gets a 404, and silently never updates — which from the user's side is indistinguishable from an app that is already current.

Change

Step Job
Detect TAURI_SIGNING_PRIVATE_KEYhas_updater validate
Flip bundle.createUpdaterArtifacts on build-mac, before the build
Export the signing key to tauri build build-mac
Find *.app.tar.gz + .sig, write latest.json build-mac, after signing
Attach all three github-release

Everything is gated on the key existing. createUpdaterArtifacts stays false in the committed config because tauri build fails outright when it is true with no key in the environment — leaving it on would break every credential-less release, which is precisely the trap RELEASING.md already warned about. CI flips it, and only when the key is there.

When the key is absent, the release body says so — same treatment the missing DMG and missing npm publish already get. An updater polling a 404 forever looks exactly like one that found no update, so silence is the wrong default.

The manifest is a script with tests, not inline YAML

Its shape is a contract with the updater, and a wrong field name fails the way a missing file does: quietly, in the user's app, long after the release was cut.

  • Refuses an empty signature. The plausible mistake is globbing up a .sig that was never written because the key was missing — that manifest is rejected by every client, so failing the release is better than publishing an update nobody can install.
  • Pins the URL at the tag, not /latest/. A client that already fetched the manifest must keep resolving to the build its signature was made for.
  • The bundle is located by glob, not a hardcoded filename, with a loud error and a directory listing if it is absent. The exact name is Tauri's to choose and a wrong guess would produce a manifest pointing at a file that was never uploaded.

Verification, honestly

This needs a signing key and a macOS runner, so I could not execute the build steps. What I did verify:

  • both workflow files parse (js-yaml), and the step ordering is asserted — Enable updater artifacts before the build, Stage updater bundle + manifest after;
  • the manifest builder is unit-tested (5 tests);
  • typecheck, lint, format, docs clean; full suite green.

The build steps themselves are first exercised by the next tagged release. That is a real limit, and it is the same class of thing that produced the five consecutive v0.3.0 release failures — which is why every new step fails loudly with a diagnostic rather than proceeding on a guess.

Still yours

Generate the key pair and add two secrets:

pnpm --filter @deepcode/desktop exec tauri signer generate -w deepcode-updater.key

⚠️ The committed public key is what existing installs verify against. If the private half is lost, regenerating changes the public key and every installed copy will refuse updates signed by the new one — a clean break needing a manual re-download. Treat it as unrecoverable-if-lost.

🤖 Generated with Claude Code

@oratis

oratis commented Aug 9, 2026

Copy link
Copy Markdown
Owner Author

Review — approve, no changes

Confirmed the premise against the committed config: plugins.updater.active is true, the endpoint is .../releases/latest/download/latest.json, a public key is committed, and bundle.createUpdaterArtifacts is absent. So the app has been polling a 404 since it shipped, and from the user's side that is indistinguishable from being current. Good catch.

The gating is the part that makes this safe:

  • createUpdaterArtifacts stays off in the committed config because tauri build fails when it is true without a key — flipping it in the repo would break every credential-less release. Flipping it in CI, only when the key exists, is the right shape, and the verify-after-write node -e guards against a silently failed edit.
  • tauri.conf.json is plain JSON (the // occurrences are inside URLs) and c.bundle exists, so the rewrite is safe.
  • Saying so on the release page when the feed is missing matches the existing treatment for a missing DMG and a skipped npm publish. Silence would leave users to conclude the updater works.

On the manifest generator:

  • Refusing an empty signature is the right failure. The plausible mistake is globbing up a .sig that was never written, and that manifest is rejected by every client — so failing the release beats publishing an update nobody can install.
  • Pinning the bundle URL at the tag rather than /latest/ is correct: a client that already fetched the manifest must keep resolving to the build its signature was made for. (The endpoint stays /latest/, which is also right.)
  • Glob rather than a hardcoded filename, with a directory listing on failure. The exact name is Tauri's to choose.
  • I checked find "$dir" -name '*.app.tar.gz' -maxdepth 1 on macOS — BSD find accepts -maxdepth in that position, so the build-mac runner is fine.

One small thing

if-no-files-found: ignore now covers the whole path: block, including DeepCode-*.dmg. The default was warn, so a missing DMG used to leave a warning in the log and now leaves nothing. The DMG is staged by an unconditional cp that would fail first, so this is unlikely to matter — just noting that the observability went away as a side effect of making the updater files optional. Splitting into two upload steps would keep both, if you ever want it.

@oratis
oratis changed the base branch from fix/git-env-test-isolation to main August 9, 2026 15:38
@oratis
oratis force-pushed the ci/updater-feed branch 2 times, most recently from 05a6cd7 to 8dd3b29 Compare August 9, 2026 16:12
`tauri.conf.json` has carried `updater.active: true`, a committed public key and
an endpoint at `releases/latest/download/latest.json` since the desktop app
shipped. Nothing ever produced that file. The app polls, 404s, and silently
never updates — which is indistinguishable, from the user's side, from an app
that is already up to date.

The pipeline now closes the loop: `validate` detects the signing key,
`build-mac` flips `createUpdaterArtifacts` on and exports the key to `tauri
build`, then locates the `.app.tar.gz` and its `.sig`, writes `latest.json`, and
`github-release` attaches all three.

Every part of it is gated on the key existing. `createUpdaterArtifacts` stays
false in the committed config because `tauri build` fails outright when it is
true with no key in the environment — leaving it on would break every
credential-less release, which is the failure mode RELEASING.md already warned
about. And when the key is absent the release body says the feed is missing,
for the same reason the DMG and npm skips already say so.

The manifest comes from a script with tests, not inline YAML. Its shape is a
contract with the updater, and a wrong field name fails the way a missing file
does: quietly, in the user's app, long after the release was cut. It refuses to
emit an empty signature — the plausible mistake is globbing up a `.sig` that was
never written — and pins the download URL at the tag rather than `/latest/`, so
a manifest a client already fetched keeps resolving to the build its signature
was made for.

Not verifiable here: this needs a signing key and a macOS runner. The YAML
parses, the step ordering is asserted, and the manifest builder is unit-tested;
the build steps themselves are first exercised by the next tagged release.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@oratis
oratis merged commit 4d56f44 into main Aug 9, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant