Skip to content

feat(site): serve downloads via Cloudflare R2 mirror with GitHub fallback#1000

Merged
Astro-Han merged 4 commits into
devfrom
worktree-r2-download-mirror
May 30, 2026
Merged

feat(site): serve downloads via Cloudflare R2 mirror with GitHub fallback#1000
Astro-Han merged 4 commits into
devfrom
worktree-r2-download-mirror

Conversation

@Astro-Han

@Astro-Han Astro-Han commented May 30, 2026

Copy link
Copy Markdown
Owner

Goal

Make the landing page download buttons serve installers from a stable, China-accessible, Cloudflare-CDN-cached URL instead of linking to the GitHub Releases page (mainland-China users without a VPN download from GitHub poorly).

Design

Chosen over "fixed filename + cache purge" to avoid release-day stale/corrupt downloads of large installers:

  • Version-stamped objects are immutable → the edge cache is always correct, can be cached aggressively, full CDN acceleration.
  • A tiny no-cache latest.json pointer is the single switch that makes a release live.
  • No Worker — the page reads the pointer client-side and swaps button hrefs. Marketing drives users to pawwork.ai; no fixed shareable short link needed.

Changes

  • site/: download buttons gain data-dl keys; on load the page fetches a no-store latest.json from R2 and swaps hrefs to per-platform direct links. If the manifest is unreachable, the GitHub Releases fallback stands, so buttons always work. Until R2 is configured, live behavior is exactly today's GitHub fallback.
  • .github/workflows/mirror-release-to-r2.yml + packages/desktop-electron/scripts/mirror-release-to-r2.ts: on release: published (or manual tag dispatch), gate on the existing verify-release.ts (fail closed if incomplete) → upload immutable versioned artifacts first with HEAD size checks → then updater latest*.yml → finally latest.json. A failure leaves the site on the previous good release, never half-mirrored. concurrency guard prevents event/manual-rerun races; reuses releaseAssetNames() so mirrored names track the release pipeline.

Required before this goes live (Cloudflare + secrets, maintainer)

  1. R2 bucket pawwork-downloads.
  2. Custom domain dl.pawwork.ai bound to the bucket (public).
  3. Bucket CORS allowing GET from https://pawwork.ai (so the page can read latest.json).
  4. R2 API token (Object Read & Write, scoped to the bucket) → repo secrets R2_ACCESS_KEY_ID / R2_SECRET_ACCESS_KEY. Account id reuses existing CLOUDFLARE_ACCOUNT_ID.

Verification

  • site builds; buttons render with data-dl + GitHub fallback href; manifest URL inlined.
  • mirror script imports resolve; usage and missing-env paths exit correctly; releaseAssetNames() output matches real release assets.
  • @opencode-ai/desktop-electron typecheck green; bun-version-workflow guard updated and green.
  • Live href-swap is verifiable only after the Cloudflare prerequisites above.

Follow-up

In-app updater generic provider (#219) can reuse the mirrored latest*.yml; out of scope here.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Implemented Cloudflare R2–backed download distribution for faster, more reliable release asset delivery with automatic GitHub Releases fallback when the primary source is unavailable.
  • Chores

    • Added automated release mirroring workflow and infrastructure to support the new distribution system.

Review Change Stack

…back

Make the landing page download buttons serve installers from a stable,
China-accessible, CDN-cached URL instead of the GitHub Releases page, which
mainland-China users without a VPN reach poorly.

Design (chosen over fixed-name + cache-purge to avoid release-day stale/corrupt
downloads): version-stamped objects are immutable so the edge cache is always
correct; a tiny no-cache latest.json pointer is the single switch that makes a
release live. No Worker needed — the page reads the pointer client-side.

- site/: download buttons gain data-dl keys; on load the page fetches a
  no-store latest.json from R2 and swaps button hrefs to per-platform direct
  links. If the manifest is unreachable, the GitHub Releases fallback stands,
  so buttons always work. Until R2 is configured the live behavior is exactly
  today's GitHub fallback.
- mirror-release-to-r2.yml + scripts/mirror-release-to-r2.ts: on release
  published (or manual tag dispatch), gate on the existing verify-release.ts
  (fail closed if incomplete), upload immutable versioned artifacts first with
  HEAD size checks, then the updater latest*.yml, and finally latest.json — so
  a failure leaves the site on the previous good release, never half-mirrored.
- Reuses releaseAssetNames() from verify-release.ts so mirrored names track the
  release pipeline. concurrency guard prevents event/manual-rerun races.

Verification: site builds; buttons render with data-dl + GitHub fallback href;
mirror script imports resolve and usage/missing-env paths exit correctly;
desktop-electron typecheck green; bun-version workflow guard updated and green.
Live href-swap is verifiable once the R2 bucket, dl.pawwork.ai custom domain,
CORS, and R2_ACCESS_KEY_ID/R2_SECRET_ACCESS_KEY secrets are configured.

Follow-up: in-app updater generic provider (#219) can reuse the mirrored
latest*.yml; out of scope here.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented May 30, 2026

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

@Astro-Han, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 39 minutes and 37 seconds. Learn how PR review limits work.

Your organization has run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: c2898467-b8df-4f67-8847-0630ad1a5e73

📥 Commits

Reviewing files that changed from the base of the PR and between 03f3a98 and 9310680.

📒 Files selected for processing (2)
  • .github/workflows/mirror-release-to-r2.yml
  • packages/desktop-electron/scripts/mirror-release-to-r2.test.ts
📝 Walkthrough

Walkthrough

The PR adds a complete Cloudflare R2–backed release distribution pipeline: a Bun script that downloads GitHub Release assets and uploads them to R2 in load-bearing order (versioned installers, then pointers, then manifest), a GitHub Actions workflow that triggers on release publish and runs the script with credential injection, comprehensive tests validating manifest generation and upload ordering, and site-side integration that fetches the R2 manifest to provide platform-specific download URLs with GitHub Releases as fallback.

Changes

R2-Backed Release Distribution and Site Download Integration

Layer / File(s) Summary
R2 Mirroring Script: Types, Functions, and Core Logic
packages/desktop-electron/scripts/mirror-release-to-r2.ts
Defines UploadStep type and asset classification constants, implements buildManifest() to construct the landing-page JSON with per-platform URLs, implements uploadPlan() to order assets (versioned first, mutable pointers next, latest.json last with no-cache), provides requireEnv() and run() helpers, and orchestrates the full flow: CLI arg parsing, environment variable validation, temp directory setup, asset downloading and validation from GitHub Release, upload plan execution with integrity verification via R2 head-object, and cleanup.
R2 Mirroring Script Tests
packages/desktop-electron/scripts/mirror-release-to-r2.test.ts
Tests buildManifest() output structure and trailing-slash normalization on the public base URL, and tests uploadPlan() to assert last step is latest.json with no-cache, immutable assets precede mutable pointers, correct cache headers per artifact type, and all released assets upload exactly once without duplicates.
GitHub Actions Release Mirroring Workflow and CI Integration
.github/workflows/mirror-release-to-r2.yml, packages/opencode/test/github/bun-version-workflow.test.ts
Workflow runs on release publish or manual dispatch, resolves the target tag, verifies release completeness (fail-closed), and runs the mirroring script with per-tag concurrency, pinned Bun 1.3.14, minimal permissions, and GitHub/R2 credentials. Workflow test assertion extended to validate the new mirror job step uses the audit-verified Bun version.
Site Download Integration with Manifest-Backed URLs
site/src/config.ts, site/src/pages/index.astro
Config exports DOWNLOAD_MANIFEST_URL pointing to https://dl.pawwork.ai/latest.json and documents R2 manifest as primary source with GitHub Releases fallback. Homepage imports the URL, adds data-dl identifiers to mac and Windows download buttons, and on page load fetches the manifest (no-store) to replace button hrefs with platform URLs; fetch failures silently preserve GitHub Releases hrefs.

Sequence Diagram(s)

sequenceDiagram
  participant GitHub as GitHub Release
  participant Script as mirror-release-to-r2
  participant R2
  Script->>GitHub: fetch assets by tag
  Script->>Script: stage assets in temp dir
  Script->>Script: validate all assets present
  Script->>Script: compute upload plan (versioned, pointers, latest.json)
  Script->>R2: upload versioned installers (immutable)
  Script->>R2: upload updater pointer files (no-cache)
  Script->>Script: generate latest.json manifest
  Script->>R2: upload latest.json (no-cache, final switch)
  Script->>R2: verify content size for each upload
Loading
sequenceDiagram
  participant Browser
  participant Homepage as index.astro
  participant Manifest as dl.pawwork.ai/latest.json
  participant GitHub as GitHub Releases
  Browser->>Homepage: load page with download buttons
  Homepage->>Manifest: fetch DOWNLOAD_MANIFEST_URL
  alt manifest reachable
    Manifest->>Homepage: return platform URLs
    Homepage->>Browser: replace button hrefs with manifest URLs
  else manifest unreachable
    Homepage->>Browser: keep GitHub Releases hrefs as fallback
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • Astro-Han/pawwork#990: Both PRs enforce the repo-wide Bun 1.3.14 contract—this PR's workflow test update asserts the new mirror-release-to-r2 job uses 1.3.14, aligning with the CI-hardening PR that pins setup-bun across workflows.
  • Astro-Han/pawwork#992: This PR's site download integration (manifest-backed URL replacement on index.astro) directly supports PR #992's new download landing page that centralizes and renders download links.

Poem

🐰 A manifest hops to R2's fast shore,
Installers load-ordered, one by one more,
While latest.json flips like a final switch,
And browsers fetch back the riches they wish—
Or GitHub Releases, a fallback so dear,
When the web's far away, still downloads are here! 🚀

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 28.57% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Description check ❓ Inconclusive The description covers the required sections: Goal, Design, Changes, Required setup, Verification, and Follow-up. However, the PR description template items (Type label, Routing labels, Priority label, Human Review Status, and Checklist) are not filled out. Complete the PR description template by filling out all checklist items and setting Human Review Status to Pending, Approved, or Not required with a reason.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely summarizes the main change: serving downloads from a Cloudflare R2 mirror with GitHub as fallback, which is the primary objective of this changeset.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch worktree-r2-download-mirror

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@Astro-Han Astro-Han added enhancement New feature or request ci Continuous integration / GitHub Actions labels May 30, 2026
@github-actions github-actions Bot added ui Design system and user interface platform Electron shell, OS integration, packaging, updater, signing, paths, and permissions harness Model harness, prompts, tool descriptions, and session mechanics P2 Medium priority labels May 30, 2026

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested priority: P2 (includes non-doc, non-test paths outside the low-risk bucket).

P1/P0 are reserved for maintainer confirmation. Please relabel manually if this is a release blocker, security issue, data-loss risk, or updater/runtime failure.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a script and workflow to mirror GitHub Release installers to Cloudflare R2, enabling faster, CDN-cached downloads for the China landing page. It also updates the landing page to dynamically fetch the download manifest and swap the download buttons to direct R2 links, falling back to GitHub Releases if the fetch fails. Feedback on these changes includes a recommendation to clean up the temporary directory created during the mirroring process using a try...finally block with rm to prevent disk space accumulation, and a security suggestion to validate the fetched URLs' protocols (e.g., ensuring they start with https:// or http://) before setting them as href attributes to mitigate potential DOM XSS vulnerabilities.

Comment thread packages/desktop-electron/scripts/mirror-release-to-r2.ts Outdated
Comment thread packages/desktop-electron/scripts/mirror-release-to-r2.ts
Comment thread site/src/pages/index.astro
Astro-Han and others added 2 commits May 30, 2026 19:29
- index.astro: validate manifest URLs are http(s) before setting button hrefs,
  guarding against a compromised manifest injecting javascript: hrefs.
- mirror-release-to-r2.ts: always clean up the temp download dir via try/finally
  (extracted the body into mirror()); import rm.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Address review P3: the mirror script's release-correctness semantics had no
regression test, so a future change could silently break the upload order or
manifest keys and only surface at release time.

- Extract two pure functions from mirror-release-to-r2.ts: buildManifest()
  (the { version, macArm64, macX64, winX64 } pointer) and uploadPlan() (the
  ordered, load-bearing upload sequence: immutable versioned artifacts ->
  mutable updater pointers -> latest.json last). mirror() now drives uploads
  from uploadPlan(), so the tested order is the executed order.
- Guard the CLI entry with `if (import.meta.main)` so the module can be
  imported by tests without running main().
- Add mirror-release-to-r2.test.ts locking manifest shape/URLs (incl. trailing
  slash), the versioned -> pointers -> manifest ordering, cache-control per
  class, and that every released asset is uploaded exactly once plus manifest.

Verification: bun test scripts/mirror-release-to-r2.test.ts (6 pass); CLI usage
and missing-env paths still exit non-zero.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
packages/desktop-electron/scripts/mirror-release-to-r2.ts (1)

135-161: 💤 Low value

Consider handling malformed head-object responses.

If the aws s3api head-object command returns unexpected JSON (e.g., during transient API errors), JSON.parse will throw an unhandled exception with a confusing message. A brief validation or try-catch with a clearer error would improve debuggability.

🛡️ Optional defensive parse
     const head = JSON.parse(
       await run(["aws", "s3api", "head-object", "--bucket", bucket, "--key", name, "--endpoint-url", endpoint]),
     )
+    if (typeof head.ContentLength !== "number") {
+      throw new Error(`Unexpected head-object response for ${name}: missing ContentLength`)
+    }
     const localSize = (await stat(local)).size
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/desktop-electron/scripts/mirror-release-to-r2.ts` around lines 135 -
161, The JSON.parse of the head-object output inside the upload function can
throw on malformed or non-JSON responses; wrap the call to run(["aws", "s3api",
"head-object", ...]) in a try/catch, capture the raw output string, attempt
JSON.parse inside the try, and if parse fails throw a new Error that includes
the command output and the object key (step.name) for context; also validate
that the parsed head has a numeric ContentLength before comparing to localSize
and throw a clear error if it's missing or invalid.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.github/workflows/mirror-release-to-r2.yml:
- Around line 42-52: The run commands interpolate steps.tag.outputs.tag without
quoting, risking shell injection; update the workflow step commands that call
verify-release.ts and mirror-release-to-r2.ts to quote the tag value (use single
quotes around "${{ steps.tag.outputs.tag }}" in the run lines) so the variable
is passed as a literal argument to the scripts (refer to the step id "tag" and
the scripts verify-release.ts and mirror-release-to-r2.ts).

---

Nitpick comments:
In `@packages/desktop-electron/scripts/mirror-release-to-r2.ts`:
- Around line 135-161: The JSON.parse of the head-object output inside the
upload function can throw on malformed or non-JSON responses; wrap the call to
run(["aws", "s3api", "head-object", ...]) in a try/catch, capture the raw output
string, attempt JSON.parse inside the try, and if parse fails throw a new Error
that includes the command output and the object key (step.name) for context;
also validate that the parsed head has a numeric ContentLength before comparing
to localSize and throw a clear error if it's missing or invalid.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 9ec7c20b-fb4d-4547-bf8d-6859318ecb74

📥 Commits

Reviewing files that changed from the base of the PR and between 1e0fa1f and 03f3a98.

📒 Files selected for processing (6)
  • .github/workflows/mirror-release-to-r2.yml
  • packages/desktop-electron/scripts/mirror-release-to-r2.test.ts
  • packages/desktop-electron/scripts/mirror-release-to-r2.ts
  • packages/opencode/test/github/bun-version-workflow.test.ts
  • site/src/config.ts
  • site/src/pages/index.astro

Comment thread .github/workflows/mirror-release-to-r2.yml Outdated
…rror

Address review P1: the mirror workflow interpolated ${{ ...tag... }} directly
into run: scripts, including the step that holds the R2 write secrets. A
release/dispatch tag containing shell metacharacters (e.g. $(...)) would run
arbitrary commands in that step and could exfiltrate the credentials.

- Resolve the tag once into a job env var (TAG) — env values are passed as
  data, not spliced into the script text — and reference "$TAG" in the verify
  and mirror run: steps. Drop the echo-to-GITHUB_OUTPUT resolve step entirely.
- Add a regression guard to mirror-release-to-r2.test.ts: no run: line may
  contain a ${{ }} expression, and the scripts must be invoked with "$TAG".

The concurrency group still uses the expression — that is a group-name string,
not a shell context, so it is not injectable.

Verification: bun test scripts/mirror-release-to-r2.test.ts (8 pass);
bun-version workflow guard still green; CLI missing-env path still exits 1.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@Astro-Han Astro-Han merged commit b35133d into dev May 30, 2026
28 checks passed
@Astro-Han Astro-Han deleted the worktree-r2-download-mirror branch May 30, 2026 13:08
Astro-Han added a commit that referenced this pull request May 30, 2026
…219) (#1003)

* feat(updater): route in-app updates through R2 with GitHub fallback (#219)

Mainland China users frequently hit slow, intermittent, or blocked GitHub
update checks. The prod release feed is already mirrored to Cloudflare R2
(dl.pawwork.ai, PR #1000 / mirror-release-to-r2.yml). This wires that
mirror into the desktop updater.

electron-updater does not fail over across providers and binds the download
source at check time (downloadUpdate reuses the updateInfoAndProvider from
the last checkForUpdates). So feed selection is done at runtime via
setFeedURL, not by changing the baked app-update.yml:

- new update-feed.ts: a provider-agnostic feed selector. check() tries feeds
  in order (R2 first, GitHub fallback) with a per-feed timeout; the winning
  feed becomes active and serves the download. download() retries on GitHub
  if an R2 download fails. A generation guard stops a timed-out check that
  resolves late from clobbering the active feed.
- index.ts: build the ordered feeds (R2 for prod, GitHub fallback; beta is
  GitHub-only with no mirror) and route the controller's check/download
  through the selector. setupAutoUpdater sets an initial feed (with its own
  GitHub fallback) and logs the selection.
- app-update.yml is intentionally left provider: github. The runtime always
  re-selects the feed before each check, so the baked value is only a startup
  safety net; keeping GitHub there avoids touching CI grep checks, the three
  app-update tests, and Windows/mac packaging, with no behavior difference.
- mirror-release-to-r2.ts now fails the mirror if a latest*.yml pointer
  references an asset it would not upload, guarding the R2 feed alignment.
- dev harness: PAWWORK_DEV_UPDATER=1 enables forceDevUpdateConfig under
  dev:desktop to exercise the real R2 feed without a signed build.

Verification: bun test (423 pass; the lone failure is the pre-existing
packaged-app smoke test that needs a built binary), typecheck, lint, and
electron-vite build all green. Live check against the current release
(v2026.5.29): R2 latest-mac.yml / latest.yml are byte-identical to the
GitHub release and every referenced binary HEADs 200 on dl.pawwork.ai.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(updater): remove duplicated feed config declarations (#219)

A bad edit replay during development inserted the constants.ts feed block
and the index.ts UPDATE_FEED_TIMEOUT_MS const twice, which compiled locally
in a stale state but failed CI typecheck (TS2451 redeclare) and broke the
desktop build/tests. Deduplicate to a single declaration of each.

Verified locally: typecheck, biome lint, electron-vite build, and bun test
(535 pass, 0 fail) all green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test(updater): assert the UPDATER_ACTIVE early-return guard (#219)

The source-contract test still grepped the old `!UPDATER_ENABLED` guard,
but setupAutoUpdater now early-returns on `!UPDATER_ACTIVE`. This edit was
lost in an earlier replay and never made the first commit, so unit-desktop
failed. Point the regex at the current guard.

Verified: bun test in packages/desktop-electron is 436 pass, 0 fail.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test(mirror): drop duplicated pointer-alignment describe block (#219)

Same edit-replay glitch that duplicated the constants/index declarations
also inserted the "pointer reference alignment" describe twice in the mirror
test (it passed because the cases just ran twice). Gemini flagged it; remove
the second copy.

Verified: bun test mirror-release-to-r2.test.ts is 7 pass, 0 fail; biome lint clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(updater): select feed via cancellable probe, not an uncancellable check race (#219)

Review P1: the R2-timeout fallback raced electron-updater's checkForUpdates()
with Promise.race but never cancelled the underlying check. A slow R2 check
could resolve late on the shared autoUpdater instance and rebind the provider
back to R2 *after* we had fallen back to GitHub — so "fell back to GitHub" was
a lie and the download could still go to the slow/unreachable R2 the fallback
exists to avoid. The generation guard only protected our return value, not
electron-updater's internal updateInfoAndProvider.

Fix: choose the feed with a cancellable reachability probe (HEAD on the channel
file, aborted on timeout via AbortController), then run exactly one
checkForUpdates() against the winning feed. Only one real check ever runs, bound
to the chosen feed; downloadUpdate() reuses that provider. No abandoned check
can rebind it.

Also addresses review P2: the download-fallback now compares the GitHub
re-check version against the version the controller validated and fails closed
on mismatch, so it can never mark a different release ready than it checked.

Verified: typecheck, lint, build, bun test (433 pass, 0 fail). New tests assert
single-check-on-fallback (incl. the abort/timeout path) and the version-mismatch
fail-closed. Real electron-updater fallback (point R2 at a bad host under
PAWWORK_DEV_UPDATER=1 dev:desktop) remains a manual pre-merge check.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(updater): fallback GitHub on check reject + expose UPDATER_ACTIVE to renderer (#219)

- check() now catches R2 checkForUpdates() reject and retries on the
  next feed (GitHub). Covers "R2 probe 200 but metadata fetch fails"
  — including the 60 s socket timeout built into builder-util-runtime.
- diagnostics() and getWindowConfig() now expose UPDATER_ACTIVE instead
  of UPDATER_ENABLED so the renderer reflects dev-harness updater state.
- 3 new tests: R2 check reject → GitHub fallback, both feeds reject →
  throw, download fallback re-check reject → fail closed.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs(updater): align module header with sequential check fallback (#219)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci Continuous integration / GitHub Actions enhancement New feature or request harness Model harness, prompts, tool descriptions, and session mechanics P2 Medium priority platform Electron shell, OS integration, packaging, updater, signing, paths, and permissions ui Design system and user interface

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant