From 0badeac8210fc5fdfbcc0c809cb826b6a9fcca1a Mon Sep 17 00:00:00 2001 From: Pawel Mikolajczuk Date: Tue, 8 Sep 2026 13:56:39 +1000 Subject: [PATCH 1/2] feat(updater): migrate releases to GitHub with a legacy bridge --- .github/workflows/release.yml | 39 +-------- Cargo.lock | 4 +- Cargo.toml | 2 +- apps/desktop/package.json | 2 +- apps/desktop/src-tauri/Cargo.lock | 2 +- apps/desktop/src-tauri/Cargo.toml | 2 +- apps/desktop/src-tauri/tauri.conf.json | 2 +- apps/marketing/README.md | 42 ++++++++++ apps/marketing/src/updater-bridge.ts | 11 +++ apps/marketing/src/updater-proxy.test.ts | 86 ++++++++++++++++++++ apps/marketing/src/updater-proxy.ts | 44 ++++++++++ apps/marketing/tsconfig.worker.json | 2 +- apps/marketing/wrangler.updater-bridge.jsonc | 12 +++ docs/public-builds.md | 25 ++++-- scripts/notices.py | 2 + third-party/inventory.json | 19 +++-- 16 files changed, 235 insertions(+), 61 deletions(-) create mode 100644 apps/marketing/src/updater-bridge.ts create mode 100644 apps/marketing/src/updater-proxy.test.ts create mode 100644 apps/marketing/src/updater-proxy.ts create mode 100644 apps/marketing/wrangler.updater-bridge.jsonc diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 2356a13..26e1877 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -20,9 +20,6 @@ jobs: build: name: macOS (aarch64) runs-on: macos-15 - env: - RELEASE_BASE_URL: ${{ vars.RELEASE_BASE_URL }} - R2_BUCKET: ${{ vars.R2_BUCKET }} steps: - uses: actions/checkout@v4 @@ -149,9 +146,6 @@ jobs: exit 1 fi endpoint="https://github.com/$GITHUB_REPOSITORY/releases/latest/download/latest.json" - if [[ -n "$RELEASE_BASE_URL" ]]; then - endpoint="${RELEASE_BASE_URL%/}/desktop/gravity/latest.json" - fi config="$(jq -cn --arg key "$UPDATER_PUBLIC_KEY" --arg endpoint "$endpoint" \ '{plugins: {updater: {pubkey: $key, endpoints: [$endpoint]}}, bundle: {createUpdaterArtifacts: true}}')" echo "GRAVITY_RELEASE_CONFIG=$config" >> "$GITHUB_ENV" @@ -194,9 +188,6 @@ jobs: run: | archive="gravity-desktop-$VERSION-aarch64.app.tar.gz" url="https://github.com/$GITHUB_REPOSITORY/releases/download/$GITHUB_REF_NAME/$archive" - if [[ -n "$RELEASE_BASE_URL" ]]; then - url="${RELEASE_BASE_URL%/}/desktop/gravity/$VERSION/$archive" - fi jq -n \ --arg version "$VERSION" \ --arg pub_date "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \ @@ -232,32 +223,6 @@ jobs: path: dist/* if-no-files-found: error - # Optional mirror; GitHub release assets are the default distribution. - - name: Publish to R2 - if: github.ref_type == 'tag' && env.R2_BUCKET != '' && env.RELEASE_BASE_URL != '' - env: - CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} - CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} - VERSION: ${{ steps.version.outputs.version }} - run: | - put() { - pnpm --dir apps/marketing exec wrangler r2 object put "$R2_BUCKET/$1" --file "$GITHUB_WORKSPACE/$2" \ - --content-type "$3" --cache-control "$4" --remote - } - immutable="public, max-age=31536000, immutable" - pnpm --dir apps/marketing install --frozen-lockfile - if [[ "$UPDATER_ENABLED" == "true" ]]; then - put "desktop/gravity/$VERSION/gravity-desktop-$VERSION-aarch64.app.tar.gz" \ - "dist/gravity-desktop-$VERSION-aarch64.app.tar.gz" application/gzip "$immutable" - fi - put "desktop/gravity/$VERSION/gravity-desktop-$VERSION-aarch64.dmg" \ - "dist/gravity-desktop-$VERSION-aarch64.dmg" application/x-apple-diskimage "$immutable" - put "gravityd/$VERSION/gravityd-$VERSION-aarch64-apple-darwin.tar.gz" \ - "dist/gravityd-$VERSION-aarch64-apple-darwin.tar.gz" application/gzip "$immutable" - if [[ "$UPDATER_ENABLED" == "true" ]]; then - put "desktop/gravity/latest.json" dist/latest.json application/json "no-cache" - fi - - name: Publish GitHub release if: github.ref_type == 'tag' env: @@ -305,4 +270,6 @@ jobs: gh release create "$GITHUB_REF_NAME" dist/* \ --title "Gravity $VERSION" \ --notes-file notes.md \ - --generate-notes + --generate-notes --draft + # Expose latest.json only after every release asset has uploaded. + gh release edit "$GITHUB_REF_NAME" --draft=false --latest diff --git a/Cargo.lock b/Cargo.lock index baa6534..4faf93b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -154,7 +154,7 @@ checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" [[package]] name = "bus" -version = "0.12.3" +version = "0.12.4" dependencies = [ "chrono", "serde", @@ -516,7 +516,7 @@ dependencies = [ [[package]] name = "gravityd" -version = "0.12.3" +version = "0.12.4" dependencies = [ "anyhow", "axum", diff --git a/Cargo.toml b/Cargo.toml index 7c31f09..b8a0400 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,7 +3,7 @@ resolver = "2" members = ["crates/bus", "crates/gravityd"] [workspace.package] -version = "0.12.3" +version = "0.12.4" edition = "2021" license = "MIT" diff --git a/apps/desktop/package.json b/apps/desktop/package.json index 2d2cb3e..dbf9f9a 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -1,6 +1,6 @@ { "name": "gravity-desktop", - "version": "0.12.3", + "version": "0.12.4", "private": true, "license": "MIT", "type": "module", diff --git a/apps/desktop/src-tauri/Cargo.lock b/apps/desktop/src-tauri/Cargo.lock index 769e709..6cf127a 100644 --- a/apps/desktop/src-tauri/Cargo.lock +++ b/apps/desktop/src-tauri/Cargo.lock @@ -1219,7 +1219,7 @@ dependencies = [ [[package]] name = "gravity-desktop" -version = "0.12.3" +version = "0.12.4" dependencies = [ "semver", "serde", diff --git a/apps/desktop/src-tauri/Cargo.toml b/apps/desktop/src-tauri/Cargo.toml index 152b9ef..a10f953 100644 --- a/apps/desktop/src-tauri/Cargo.toml +++ b/apps/desktop/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "gravity-desktop" -version = "0.12.3" +version = "0.12.4" description = "Gravity desktop client" license = "MIT" edition = "2021" diff --git a/apps/desktop/src-tauri/tauri.conf.json b/apps/desktop/src-tauri/tauri.conf.json index cf68caf..8752b4a 100644 --- a/apps/desktop/src-tauri/tauri.conf.json +++ b/apps/desktop/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "Gravity", - "version": "0.12.3", + "version": "0.12.4", "identifier": "in.mikolajczuk.gravity", "build": { "beforeDevCommand": "pnpm dev", diff --git a/apps/marketing/README.md b/apps/marketing/README.md index e0fec83..772eab2 100644 --- a/apps/marketing/README.md +++ b/apps/marketing/README.md @@ -24,3 +24,45 @@ Wrangler. Set `vars.RELEASE_MANIFEST_URL` to your HTTPS updater manifest to enab `/download/latest`; it returns 503 without configuration. Add a `routes` entry with your custom domain if desired. Review the canonical URLs, sitemap, and branding when forking. + +## Legacy updater bridge + +`wrangler.updater-bridge.jsonc` builds a separate, dependency-free compatibility +Worker. It serves only GET/HEAD `/desktop/gravity/latest.json`, returning the +configured upstream manifest without a redirect or content changes. Other paths +and methods are rejected. Upstream errors/invalid manifests return 502; responses +are not cached. The bridge does not forward caller headers or query parameters. + +The default upstream is empty and there are no production routes in source. +Keep deployment configuration specific to your distribution. From this directory: + +```sh +pnpm exec wrangler deploy --config wrangler.updater-bridge.jsonc --dry-run +pnpm exec wrangler deploy --config wrangler.updater-bridge.jsonc \ + --var "RELEASE_MANIFEST_URL:https://github.com/OWNER/REPO/releases/latest/download/latest.json" +``` + +Verify the Worker on its `workers.dev` URL first. Publish and verify the transition +release's GitHub assets, manifest and updater signature before attaching the exact +legacy manifest route. Do not replace the R2 custom domain or route the entire +host: existing versioned downloads must continue reaching their original objects. +Attach a route with a trailing `*` to include requests with query parameters; the +handler still rejects every pathname except the exact manifest path: + +```sh +pnpm exec wrangler deploy --config wrangler.updater-bridge.jsonc \ + --var "RELEASE_MANIFEST_URL:https://github.com/OWNER/REPO/releases/latest/download/latest.json" \ + --route "downloads.example.com/desktop/gravity/latest.json*" +``` + +Set the marketing Worker's runtime **and automatic-build** `RELEASE_MANIFEST_URL` +to the GitHub latest manifest. Preserve the explicit `--var` deploy argument and +the empty committed default. Test an existing updater's check/download/signature +verification through the legacy URL, and the transition client's GitHub endpoint. +Keep the original signing keys and the bridge for users who upgrade much later. + +For rollback, repoint the bridge to a verified version-specific GitHub manifest, +or remove only its route to expose the retained R2 manifest again. Keep a private +copy of the prior route/settings/manifest before activation. Removing a route or +repointing a manifest affects future checks; it does not downgrade installed apps. +Never replace a published version's signed archives to roll back. diff --git a/apps/marketing/src/updater-bridge.ts b/apps/marketing/src/updater-bridge.ts new file mode 100644 index 0000000..811af2d --- /dev/null +++ b/apps/marketing/src/updater-bridge.ts @@ -0,0 +1,11 @@ +import { proxyUpdateManifest } from "./updater-proxy"; + +interface Env { + readonly RELEASE_MANIFEST_URL?: string; +} + +export default { + fetch(request: Request, env: Env): Promise { + return proxyUpdateManifest(request, env.RELEASE_MANIFEST_URL); + }, +} satisfies ExportedHandler; diff --git a/apps/marketing/src/updater-proxy.test.ts b/apps/marketing/src/updater-proxy.test.ts new file mode 100644 index 0000000..ce6da39 --- /dev/null +++ b/apps/marketing/src/updater-proxy.test.ts @@ -0,0 +1,86 @@ +import { afterEach, describe, expect, it, vi } from "vitest"; + +import { proxyUpdateManifest } from "./updater-proxy"; + +const upstream = "https://github.com/example/app/releases/latest/download/latest.json"; +const legacy = "https://updates.example/desktop/gravity/latest.json"; +const body = JSON.stringify({ + version: "0.12.4", + platforms: { + "darwin-aarch64": { + url: "https://github.com/example/app/releases/download/v0.12.4/gravity.app.tar.gz", + signature: "original-signature", + }, + }, +}); + +afterEach(() => { + vi.unstubAllGlobals(); +}); + +describe("legacy update manifest bridge", () => { + it("preserves manifest bytes and does not forward caller credentials or query parameters", async () => { + const fetch = vi.fn().mockResolvedValue(new Response(body)); + vi.stubGlobal("fetch", fetch); + const response = await proxyUpdateManifest( + new Request(`${legacy}?old-client=true`, { + headers: { authorization: "private-client-token" }, + }), + upstream, + ); + + expect(response.status).toBe(200); + expect(await response.text()).toBe(body); + expect(response.headers.get("content-type")).toBe("application/json"); + expect(response.headers.get("cache-control")).toBe("no-store"); + expect(fetch).toHaveBeenCalledExactlyOnceWith(upstream, { redirect: "follow" }); + }); + + it("supports HEAD without a response body", async () => { + vi.stubGlobal("fetch", vi.fn().mockResolvedValue(new Response(body))); + const response = await proxyUpdateManifest(new Request(legacy, { method: "HEAD" }), upstream); + expect(response.status).toBe(200); + expect(await response.text()).toBe(""); + }); + + it.each([404, 403, 500])("fails without caching an upstream HTTP %i", async (status) => { + vi.stubGlobal( + "fetch", + vi.fn().mockResolvedValue(new Response("unavailable", { status })), + ); + const response = await proxyUpdateManifest(new Request(legacy), upstream); + expect(response.status).toBe(502); + expect(response.headers.get("cache-control")).toBe("no-store"); + }); + + it.each([ + "error", + "{}", + '{"platforms":{"darwin-aarch64":{"url":"bad.app.tar.gz"}}}', + ])("rejects invalid upstream manifests", async (invalid) => { + vi.stubGlobal( + "fetch", + vi.fn().mockResolvedValue(new Response(invalid)), + ); + expect((await proxyUpdateManifest(new Request(legacy), upstream)).status).toBe(502); + }); + + it("handles an upstream network failure", async () => { + vi.stubGlobal( + "fetch", + vi.fn().mockRejectedValue(new Error("offline")), + ); + expect((await proxyUpdateManifest(new Request(legacy), upstream)).status).toBe(502); + }); + + it("does not fetch for other paths, unsupported methods or missing configuration", async () => { + const fetch = vi.fn(); + vi.stubGlobal("fetch", fetch); + expect((await proxyUpdateManifest(new Request(`${legacy}/other`), upstream)).status).toBe(404); + expect( + (await proxyUpdateManifest(new Request(legacy, { method: "POST" }), upstream)).status, + ).toBe(405); + expect((await proxyUpdateManifest(new Request(legacy), "")).status).toBe(503); + expect(fetch).not.toHaveBeenCalled(); + }); +}); diff --git a/apps/marketing/src/updater-proxy.ts b/apps/marketing/src/updater-proxy.ts new file mode 100644 index 0000000..9e21ebe --- /dev/null +++ b/apps/marketing/src/updater-proxy.ts @@ -0,0 +1,44 @@ +import { resolveMacDownloadUrl } from "./release"; + +const MANIFEST_PATH = "/desktop/gravity/latest.json"; + +function unavailable(): Response { + return new Response("The update manifest is temporarily unavailable.", { + status: 502, + headers: { "cache-control": "no-store" }, + }); +} + +export async function proxyUpdateManifest( + request: Request, + manifestUrl: string | undefined, +): Promise { + if (new URL(request.url).pathname !== MANIFEST_PATH) { + return new Response("Not found", { status: 404 }); + } + if (request.method !== "GET" && request.method !== "HEAD") { + return new Response("Method not allowed", { status: 405, headers: { allow: "GET, HEAD" } }); + } + if (!manifestUrl?.trim()) { + return new Response("Updates are not configured.", { status: 503 }); + } + + try { + // Never forward caller cookies, authorization, query parameters or validators. + const upstream = await fetch(manifestUrl, { redirect: "follow" }); + if (!upstream.ok) { + return unavailable(); + } + const body = await upstream.text(); + const manifest: unknown = JSON.parse(body); + if (resolveMacDownloadUrl(manifest) === null) { + return unavailable(); + } + // Preserve the manifest bytes, especially the signed archive's signature. + return new Response(request.method === "HEAD" ? null : body, { + headers: { "content-type": "application/json", "cache-control": "no-store" }, + }); + } catch { + return unavailable(); + } +} diff --git a/apps/marketing/tsconfig.worker.json b/apps/marketing/tsconfig.worker.json index 6932afc..c096932 100644 --- a/apps/marketing/tsconfig.worker.json +++ b/apps/marketing/tsconfig.worker.json @@ -4,5 +4,5 @@ "lib": ["ES2022"], "types": ["@cloudflare/workers-types"] }, - "include": ["src/release.ts", "src/worker.ts"] + "include": ["src/release.ts", "src/worker.ts", "src/updater-bridge.ts", "src/updater-proxy.ts"] } diff --git a/apps/marketing/wrangler.updater-bridge.jsonc b/apps/marketing/wrangler.updater-bridge.jsonc new file mode 100644 index 0000000..c7af5b4 --- /dev/null +++ b/apps/marketing/wrangler.updater-bridge.jsonc @@ -0,0 +1,12 @@ +{ + "$schema": "./node_modules/wrangler/config-schema.json", + "name": "gravity-updater-bridge", + "main": "src/updater-bridge.ts", + "compatibility_date": "2026-09-02", + "workers_dev": true, + // Configure the upstream and the exact legacy manifest route when deploying. + // No route by default: verify the Worker before attaching production traffic. + "vars": { + "RELEASE_MANIFEST_URL": "", + }, +} diff --git a/docs/public-builds.md b/docs/public-builds.md index b0de232..acecad7 100644 --- a/docs/public-builds.md +++ b/docs/public-builds.md @@ -47,9 +47,6 @@ Optional repository configuration: | `UPDATER_PUBLIC_KEY` | Variable | Your updater verification key; public, not a credential | | `TAURI_SIGNING_PRIVATE_KEY` | Secret | Enables signed updater archives and `latest.json`; requires the matching public key | | `TAURI_SIGNING_PRIVATE_KEY_PASSWORD` | Secret | Password for an encrypted updater private key, if used | -| `RELEASE_BASE_URL` | Variable | Optional HTTPS download mirror base; default is GitHub release assets | -| `R2_BUCKET` | Variable | Enables the optional R2 mirror with `RELEASE_BASE_URL` | -| `CLOUDFLARE_API_TOKEN`, `CLOUDFLARE_ACCOUNT_ID` | Secrets | Credentials for that optional R2 upload | | `POSTHOG_PROJECT_TOKEN`, `POSTHOG_HOST` | Variables | Optional telemetry destination; enables capture in configured builds | | `POSTHOG_PROJECT_ID` | Variable | Optional source-map destination | | `POSTHOG_PERSONAL_API_KEY` | Secret | Optional source-map upload credential | @@ -67,13 +64,23 @@ A local signed build can supply `plugins.updater` and distributing a separate app should also choose their own app identifiers and launchd labels. -### Optional download mirror +### Download hosting and existing installations -GitHub release assets are the default download host. To use an R2 mirror, -configure `RELEASE_BASE_URL` with an HTTPS base URL, `R2_BUCKET` with the bucket -name, and the two Cloudflare secrets listed above. The workflow uploads versioned -artifacts before publishing `desktop/gravity/latest.json`. Leave the mirror -variables unset to use GitHub releases without Cloudflare credentials. +Tagged releases publish the signed updater archive, DMG, daemon archive and +`latest.json` on GitHub Releases. New official builds check +`https://github.com/ahilles107/gravity/releases/latest/download/latest.json`. +The workflow uses its own repository name for forks. It uploads all assets to a +draft release before marking it published/latest, so clients cannot discover a +manifest before its artifacts are available. Manual dry runs do not publish. + +The former R2 upload variables and credentials are no longer used. Existing R2 +objects remain available for historical links. A narrow compatibility Worker +keeps the old manifest endpoint working for installed versions through 0.12.3; +it proxies the GitHub manifest without changing its signature or artifact URLs. +After installing 0.12.4 or later, clients check GitHub directly. Keep this bridge +available for dormant installations and preserve the updater signing key pair. + +See [the bridge deployment and rollback procedure](../apps/marketing/README.md#legacy-updater-bridge). ### Marketing site diff --git a/scripts/notices.py b/scripts/notices.py index 9a92260..2259f69 100644 --- a/scripts/notices.py +++ b/scripts/notices.py @@ -28,6 +28,8 @@ "apps/desktop/pnpm-lock.yaml", "apps/desktop/vite.config.ts", "apps/marketing/package.json", "apps/marketing/pnpm-lock.yaml", "apps/marketing/src/main.ts", "apps/marketing/src/worker.ts", + "apps/marketing/src/updater-proxy.ts", "apps/marketing/src/updater-bridge.ts", + "apps/marketing/wrangler.updater-bridge.jsonc", ".github/workflows/release.yml", "scripts/prepare-sidecar.sh", "scripts/notices.py", "third-party/README.md", ] diff --git a/third-party/inventory.json b/third-party/inventory.json index 437650d..b54218f 100644 --- a/third-party/inventory.json +++ b/third-party/inventory.json @@ -1,23 +1,26 @@ { "target": "aarch64-apple-darwin", "inputs": { - "Cargo.lock": "b62879e336aa449b0aa551555803334850592f8d47aadb62dd44c56f85353df0", - "Cargo.toml": "01b5ebb6363a98162162b644364cc81ba35f6bee81271ff79f5e9440b8362baa", + "Cargo.lock": "020443b4ecd1679b611a728f4efc6288f3e99f1627578f62b64c7eb630276749", + "Cargo.toml": "ab03e60687476c99acaa1d150f83e65b4d21cfac89f048f0c5a6b3c86a9983f2", "crates/gravityd/Cargo.toml": "ae8213b9576b7ce6baa6efda1a6e8633dcbc0888bbd96f60383323ffa81702fd", "crates/bus/Cargo.toml": "c4c4f51131d78f8ae8cb6e96fcb918c245998d9c1e01312f513fb56dfc452910", - "apps/desktop/src-tauri/Cargo.lock": "9dfcf7912195856030501b846f4414b5113f05e8c2af6984601b4bb310bf21b6", - "apps/desktop/src-tauri/Cargo.toml": "b5393fae1ece64d6907cccfdeb5ddc3d33b886fd2139d79882ee52c9ff94ea0a", - "apps/desktop/src-tauri/tauri.conf.json": "dbdb2c698aa465f471498d01f26c71ea780d0ba06aa294a7ce777349c544a41f", - "apps/desktop/package.json": "3a8755f25644b7cc17378437d348be10170aa6d2d0add490e331418fac9f110b", + "apps/desktop/src-tauri/Cargo.lock": "9e094be8b0effbfc0a2e8b3b1ae9bda02b08e1a7f39574493c6881769c051c34", + "apps/desktop/src-tauri/Cargo.toml": "cd1a733eed7c7e2cf2e870153aea1fab7029560cb17f0af2ad3737f608f877d6", + "apps/desktop/src-tauri/tauri.conf.json": "200597addad29b5f58b25a4c1dd42f2a7705f72f222d2f687f5374bfa4a0e193", + "apps/desktop/package.json": "137d59807bb4ffc49013ed3eb9bcad6aa43c1f3b8693f215df8bd830dd753b4a", "apps/desktop/pnpm-lock.yaml": "ea5c8997543f43fb066b0aee187bc0f8b171607c7734c95638c02e3cd1ba6c72", "apps/desktop/vite.config.ts": "77f2fd4730e92ded079a31f0f857ef62d7a25f0b71acfe5db1eef34baccddd25", "apps/marketing/package.json": "40590a51129d5206b21bf0410fbfd8e1b5c2ee4b8c70f1249b5ea088d84169a9", "apps/marketing/pnpm-lock.yaml": "8bac14609a15069e41ebb8c00a9a53075dc1c66cd6fd351fc39dd7b1b1d4e759", "apps/marketing/src/main.ts": "6a2a0518500b2b0292c74f52a97df6653b7e3dfd4e99c9387a1ec923fd6622e0", "apps/marketing/src/worker.ts": "fbd4c3d51ed0d3c905eda4ef3e45b4267949a7482a0dffecc05079bf3afedc4e", - ".github/workflows/release.yml": "e0a6fb05f37c8deb98c416712c46d4d504e104dd4f023abe21f79d6982643f73", + "apps/marketing/src/updater-proxy.ts": "0883a34260e53b3248d60f9fa5fe9d9692f1215576c29907462bf84263989893", + "apps/marketing/src/updater-bridge.ts": "9e2cd3920e4fba8444561f2f7393fe6bca641bba528fca3051ac152d28e9e72a", + "apps/marketing/wrangler.updater-bridge.jsonc": "31c766018eef47e0b815ccfe2c69123bdb371459de3f99d566bc956780832fb8", + ".github/workflows/release.yml": "08d00202ab09a5609acc8db441b73e7a5573f29de5a0884339b0f1759416de5d", "scripts/prepare-sidecar.sh": "de31795719af5a1f23814b67095ac990cd6a97742b58724ab53512aa2330a313", - "scripts/notices.py": "966756a8ae2f837cac0e30ed4fd309e7f9cd8785a8342f29a14249f64e5d802d", + "scripts/notices.py": "1fcca820bd7b878e624a74eb24d1cd10d747a2381f08e5918aa12b3402406e65", "third-party/README.md": "60f83aa879b44f78cb90bd5c8bce90b539490bd529fc411547b54eeccaa0bc97", "third-party/overrides/0322ceec0b8f15aa2d74a6173c07cc8c6f3764b3.txt": "f5c342c49f3ac804f3e8e7bb62a8040a44c50d47bb36902b1abd13f66a1adf8b", "third-party/overrides/11069edd79019f7dafbe3138841cf289209270dd.txt": "8173d5c29b4f956d532781d2b86e4e30f83e6b7878dce18c919451d6ba707c90", From 1c9bf8fefdb83addd2b141c05d7ac0bfd29b164c Mon Sep 17 00:00:00 2001 From: Pawel Mikolajczuk Date: Tue, 8 Sep 2026 13:58:52 +1000 Subject: [PATCH 2/2] docs(release): document GitHub hosting and bridge recovery Keep the release procedure aligned with the 0.12.4 transition. This is a patch because storage changes preserve the existing update behavior and require no user configuration or data migration. --- .claude/skills/release/SKILL.md | 31 +++++++++++++++++-------------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/.claude/skills/release/SKILL.md b/.claude/skills/release/SKILL.md index 4f57085..33c5b79 100644 --- a/.claude/skills/release/SKILL.md +++ b/.claude/skills/release/SKILL.md @@ -8,10 +8,11 @@ description: Cut a new Gravity release — pick the version, land the version-bu A release is a signed `v*` tag on `main`. Pushing that tag runs `.github/workflows/release.yml` on a GitHub-hosted macOS ARM64 runner, which builds the desktop app, packages `gravityd`, and creates the GitHub release. -Signing, notarization, updater artifacts, and the R2 mirror are optional; see +Signing, notarization and updater artifacts are configurable; see `docs/public-builds.md` for the required variables and secrets. Verify that configuration before releasing. For an existing distribution, preserve the -updater signing pair and download endpoint so installed clients still update. +updater signing pair. When changing download endpoints, retain a compatibility +bridge for every endpoint embedded in existing installations. Publishing `latest.json` makes configured running apps offer the update. ## The rule that governs everything here @@ -75,7 +76,8 @@ re-check the manifests in that commit first: git show :Cargo.toml | sed -n '6p' ``` -A pushed tag publishes to GitHub releases and any configured mirror. +A pushed tag uploads all assets to a draft GitHub release, then publishes it as +latest. Existing R2 objects remain available, but new releases do not upload there. ## 4. Watch the run @@ -107,7 +109,9 @@ gh release view v0.12.0 --json name,url,assets Expect the DMG and `gravityd` tarball. With updater signing enabled, also expect `.app.tar.gz`, its `.sig`, and `latest.json`. Fetch the configured manifest and -confirm its version and artifact URL. If R2 is enabled, check that mirror too. +confirm its version and GitHub artifact URL. Check the legacy manifest bridge +and website download redirect too, and verify updater signatures against the +existing public key. Keep the bridge for dormant installations. Also check the release notes: the workflow appends a Gatekeeper/`xattr` warning when notarization was unavailable. That is expected for builds without @@ -116,18 +120,17 @@ Apple credentials; official notarized distributions should treat it as a failure ## Dry runs `workflow_dispatch` on `release.yml` builds from `tauri.conf.json`'s version -and only uploads artifacts to the run — no tag, no R2, no GitHub release. Use +and only uploads artifacts to the run — no tag or GitHub release. Use it to prove a build change before tagging. ## When a release goes wrong -Versioned R2 objects are immutable and `latest.json` is `no-cache`, so the -recovery is always forward: fix, bump to the next patch, tag again. Do not -delete or move a published tag — apps may already have the manifest. If a bad -`latest.json` is live and the fix will take a while, re-put the previous -version's manifest to stop the rollout: +Recovery is normally forward: fix, bump to the next patch, and tag again. Do not +delete or move a published tag or replace its signed archives — apps may already +have the manifest. If a bad release is latest, select the previous verified +GitHub release as latest to stop new clients discovering it. Point the legacy +bridge at that same version-specific manifest if needed. This stops future +update offers; it does not downgrade installed apps. -```bash -pnpm --dir apps/marketing exec wrangler r2 object put "$R2_BUCKET/desktop/gravity/latest.json" \ - --file latest.json --content-type application/json --cache-control no-cache --remote -``` +See `apps/marketing/README.md` for the bridge activation and rollback procedure. +Do not remove the old R2 custom domain or historical artifacts during recovery.