Skip to content

Add R2 dual-write for nightly appcast and DMGs - #2335

Merged
lawrencecchen merged 4 commits into
mainfrom
feat-r2-dual-write
Mar 30, 2026
Merged

lawrencecchen merged 4 commits into
mainfrom
feat-r2-dual-write

Conversation

@lawrencecchen

@lawrencecchen lawrencecchen commented Mar 30, 2026 •

Copy link
Copy Markdown
Contributor

Summary

  • Uploads nightly DMGs and appcast.xml to Cloudflare R2 (files.cmux.com/nightly/) alongside existing GitHub Release assets
  • R2 uses atomic PutObject, so the appcast never 404s during replacement (fixes the transient SUDownloadError 2001 race condition)
  • DMGs uploaded before appcast so the feed never references a missing file
  • Existing GitHub Release upload is unchanged, no client-side changes

How it works

  1. Existing appcast generation (GitHub URLs) runs as before
  2. New step generates a second appcast with DOWNLOAD_URL_PREFIX=https://files.cmux.com/nightly/
  3. New step uploads immutable DMG, latest DMG, then appcast to R2 via AWS CLI (S3-compatible)

Secrets required

  • CF_R2_ACCOUNT_ID ✅
  • CF_R2_ACCESS_KEY_ID ✅
  • CF_R2_SECRET_ACCESS_KEY ✅

Verification

After merge, confirm:

  • curl -I https://files.cmux.com/nightly/appcast.xml returns 200
  • Appcast XML contains R2 DMG URLs
  • DMG downloads work from R2

Follow-up

Separate PR will switch SUFeedURL in the app bundle from GitHub Releases to R2 after manual verification.


Summary by cubic

Dual-write nightly and stable DMGs and appcasts to Cloudflare R2 (https://files.cmux.com/nightly/, https://files.cmux.com/stable/) alongside GitHub Releases to eliminate transient feed 404s. DMGs upload first, then the appcast; atomic writes prevent SUDownloadError 2001, and failures won’t block GitHub publishing.

  • New Features

    • Derive R2 appcast.xml by URL-replacing the GitHub download prefix (via sed); signatures remain valid.
    • Set Cache-Control headers: nightly versioned DMG is immutable (1 year), appcasts and “latest” DMGs are no-cache; upload via aws s3 with continue-on-error and AWS CLI auto-install. GitHub Release uploads are unchanged. A follow-up will switch SUFeedURL to R2.
  • Migration

    • Set secrets: CF_R2_ACCOUNT_ID, CF_R2_ACCESS_KEY_ID, CF_R2_SECRET_ACCESS_KEY.

Written for commit 7ba3ea0. Summary will update on new commits.

Summary by CodeRabbit

  • Chores
    • Publish nightly installers and feed to an additional Cloudflare R2 hosting path; immutable build artifact kept versioned, feed and latest DMG published with no-cache headers. Uploads are non-blocking.
    • Publish release installers and feed to the same R2 target; installer uploaded before the feed to ensure feed references existing files, uploads are non-blocking.

Upload nightly DMGs and appcast to Cloudflare R2 (files.cmux.com)
alongside the existing GitHub Release assets. R2 uses atomic PutObject
so the appcast never 404s during replacement, fixing the transient
SUDownloadError 2001 that occurs when GitHub Release assets are
being overwritten.

DMGs are uploaded before the appcast so the feed never references a
file that doesn't exist yet. The GitHub Release upload is unchanged,
so existing nightly users are unaffected.

A follow-up PR will switch the Sparkle feed URL in the app bundle
from GitHub Releases to R2 after manual verification.
@vercel

vercel Bot commented Mar 30, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
cmux Ready Ready Preview, Comment Mar 30, 2026 10:53am

@coderabbitai

coderabbitai Bot commented Mar 30, 2026 •

Copy link
Copy Markdown

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 4b09f088-2b08-482d-900e-979c323b02eb

📥 Commits

Reviewing files that changed from the base of the PR and between 17a1f24 and 7ba3ea0.

📒 Files selected for processing (2)
  • .github/workflows/nightly.yml
  • .github/workflows/release.yml

📝 Walkthrough

Walkthrough

Added new workflow steps to upload release and nightly DMG assets plus a rewritten Sparkle appcast to Cloudflare R2 (S3-compatible) in .github/workflows/nightly.yml and .github/workflows/release.yml. Both steps install AWS CLI if missing, rewrite appcast.xml download URLs to R2, upload DMGs then the appcast, and use continue-on-error: true.

Changes

Cohort / File(s) Summary
Nightly workflow: R2 upload
.github/workflows/nightly.yml
Added gated “Upload nightly assets to R2” step (runs when should_publish == 'true' and freshness checks pass). Installs aws CLI if needed, rewrites appcast.xml to appcast-r2.xml (prefix https://files.cmux.com/nightly/), uploads immutable versioned DMG, a latest-named DMG, then appcast-r2.xml to cmux-binaries/nightly/ on Cloudflare R2; uploads use specific cache headers and continue-on-error: true.
Release workflow: R2 upload
.github/workflows/release.yml
Added “Upload release assets to R2” step (runs on tag push unless skip_upload == 'true'). Installs aws CLI if needed, rewrites appcast.xml to appcast-r2.xml, uploads signed cmux-macos.dmg to cmux-binaries/stable/..., then uploads appcast-r2.xml (ensures appcast updates after artifact upload); uses Cloudflare R2 endpoint and continue-on-error: true.

Sequence Diagram(s)

sequenceDiagram
    participant Runner as GitHub Actions Runner
    participant Gen as Appcast generator (sparkle/appcast)
    participant CLI as AWS CLI (s3 cp)
    participant R2 as Cloudflare R2 (cmux-binaries)

    Runner->>Gen: generate appcast-r2.xml\n(rewrite DOWNLOAD_URL_PREFIX -> https://files.cmux.com/...)
    Gen-->>Runner: appcast-r2.xml
    Runner->>CLI: upload DMG(s) -> R2 (nightly/ or stable/...)
    CLI->>R2: PUT object(s) with cache headers
    Runner->>CLI: upload appcast-r2.xml -> R2 (appcast.xml)
    CLI->>R2: PUT appcast with no-cache/no-store headers
    Note right of Runner: steps gated by conditions (tags / should_publish)\ncontinue-on-error: true
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Poem

🐇
I nibble bytes and stitch the feed,
DMGs hop off at Cloudflare's mead,
appcast-r2 gleams, rewritten new—
one last upload, then a skyward view,
Nightly carrots for users, two by two.

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main change: adding R2 dual-write functionality for nightly appcast and DMGs, which is the core focus of the changeset.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Description check ✅ Passed PR description is comprehensive with clear summary, technical details, verification steps, and follow-up plan. Matches template structure with summary and testing sections.

✏️ 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 feat-r2-dual-write

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.

@cubic-dev-ai cubic-dev-ai 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.

No issues found across 1 file

@coderabbitai coderabbitai 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.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.github/workflows/nightly.yml:
- Around line 517-540: Add explicit fail-fast validation of the R2 secrets and
endpoint before any aws s3 cp calls: check that environment variables
CF_R2_ACCESS_KEY_ID, CF_R2_SECRET_ACCESS_KEY, CF_R2_ACCOUNT_ID (used to build
R2_ENDPOINT) and NIGHTLY_DMG_IMMUTABLE are non-empty and exit with a clear error
if any are missing. Locate the workflow step that sets env and runs the upload
(references: env variables AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY,
R2_ENDPOINT, the BUCKET variable, and the aws s3 cp commands) and add simple
checks after set -euo pipefail that print which variable is missing and call
exit 1 so the job fails fast with an informative message.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: 78c3c0dd-364d-435a-a6c2-bdb0da44595d

📥 Commits

Reviewing files that changed from the base of the PR and between 35cb42f and 1c51281.

📒 Files selected for processing (1)
  • .github/workflows/nightly.yml

Comment on lines +517 to +540
env:
AWS_ACCESS_KEY_ID: ${{ secrets.CF_R2_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.CF_R2_SECRET_ACCESS_KEY }}
AWS_DEFAULT_REGION: auto
R2_ENDPOINT: "https://${{ secrets.CF_R2_ACCOUNT_ID }}.r2.cloudflarestorage.com"
run: |
set -euo pipefail
BUCKET=cmux-binaries

# Upload DMGs first so the appcast never references a missing file.
aws s3 cp "$NIGHTLY_DMG_IMMUTABLE" \
"s3://${BUCKET}/nightly/${NIGHTLY_DMG_IMMUTABLE}" \
--endpoint-url "$R2_ENDPOINT"
aws s3 cp cmux-nightly-macos.dmg \
"s3://${BUCKET}/nightly/cmux-nightly-macos.dmg" \
--endpoint-url "$R2_ENDPOINT"

# Upload appcast last (atomic PutObject, no 404 window).
aws s3 cp appcast-r2.xml \
"s3://${BUCKET}/nightly/appcast.xml" \
--endpoint-url "$R2_ENDPOINT"

echo "R2 upload complete: https://files.cmux.com/nightly/appcast.xml"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Add fail-fast checks for R2 secrets before upload.

If any R2 secret is empty, this step fails later with opaque endpoint/auth errors. Validate upfront for clearer failures.

Suggested patch
       - name: Upload nightly assets to R2
         if: needs.decide.outputs.should_publish == 'true' && steps.current_head_prebuild.outputs.still_current == 'true' && steps.current_head_postbuild.outputs.still_current == 'true'
         env:
           AWS_ACCESS_KEY_ID: ${{ secrets.CF_R2_ACCESS_KEY_ID }}
           AWS_SECRET_ACCESS_KEY: ${{ secrets.CF_R2_SECRET_ACCESS_KEY }}
           AWS_DEFAULT_REGION: auto
-          R2_ENDPOINT: "https://${{ secrets.CF_R2_ACCOUNT_ID }}.r2.cloudflarestorage.com"
+          CF_R2_ACCOUNT_ID: ${{ secrets.CF_R2_ACCOUNT_ID }}
         run: |
           set -euo pipefail
+          : "${AWS_ACCESS_KEY_ID:?Missing CF_R2_ACCESS_KEY_ID secret}"
+          : "${AWS_SECRET_ACCESS_KEY:?Missing CF_R2_SECRET_ACCESS_KEY secret}"
+          : "${CF_R2_ACCOUNT_ID:?Missing CF_R2_ACCOUNT_ID secret}"
+          R2_ENDPOINT="https://${CF_R2_ACCOUNT_ID}.r2.cloudflarestorage.com"
           BUCKET=cmux-binaries
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/nightly.yml around lines 517 - 540, Add explicit fail-fast
validation of the R2 secrets and endpoint before any aws s3 cp calls: check that
environment variables CF_R2_ACCESS_KEY_ID, CF_R2_SECRET_ACCESS_KEY,
CF_R2_ACCOUNT_ID (used to build R2_ENDPOINT) and NIGHTLY_DMG_IMMUTABLE are
non-empty and exit with a clear error if any are missing. Locate the workflow
step that sets env and runs the upload (references: env variables
AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, R2_ENDPOINT, the BUCKET variable, and
the aws s3 cp commands) and add simple checks after set -euo pipefail that print
which variable is missing and call exit 1 so the job fails fast with an
informative message.

@greptile-apps

greptile-apps Bot commented Mar 30, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds a parallel upload path for nightly DMGs and the Sparkle appcast to Cloudflare R2 (files.cmux.com/nightly/), alongside the existing GitHub Releases upload. The core motivation is to eliminate a transient SUDownloadError 2001 race condition caused by the non-atomic, sequential nature of GitHub Release asset uploads — R2's atomic PutObject means the appcast is either the old version or the fully-replaced new one, never mid-write. The implementation correctly uploads the versioned and "latest" DMGs before the appcast so the feed never references a missing file.

Key changes:

  • New step: "Generate R2-targeted appcast" — re-runs sparkle_generate_appcast.sh with DOWNLOAD_URL_PREFIX=https://files.cmux.com/nightly/ to produce appcast-r2.xml
  • New step: "Upload nightly assets to R2" — uses the AWS CLI (S3-compatible API) to upload the immutable versioned DMG, the mutable latest DMG, and finally the appcast to the cmux-binaries R2 bucket
  • Both new steps are gated by should_publish == 'true' (correct — R2 upload only for real publish runs)
  • Upload ordering is intentional and correct (versioned DMG → latest DMG → appcast)

Concerns:

  • The mutable assets (cmux-nightly-macos.dmg, appcast.xml) are uploaded without --cache-control \"no-cache\" headers; if a Cloudflare CDN layer caches these, clients could receive stale feeds or DMG pointers until the TTL expires, partially undermining the race-condition fix.
  • sparkle_generate_appcast.sh clones and builds the Sparkle toolchain from source on every invocation — this PR adds a second full build, meaningfully increasing CI time.
  • The aws CLI is called without verifying it is installed on the warp-macos-26-arm64-6x runner; a missing binary would produce a cryptic failure.

Confidence Score: 4/5

Safe to merge with low risk — the GitHub Releases path is untouched, but the missing Cache-Control headers on mutable R2 assets could allow CDN-cached stale content to reach clients, partially negating the race-condition fix the PR set out to solve.

All three findings are P2, but the Cache-Control omission on mutable objects (appcast.xml and latest DMG) is a correctness concern for the stated goal of the PR. Since this PR is introducing a new CDN-backed delivery path for a Sparkle feed, getting caching headers right is important for reliability even if it isn't a hard runtime error today.

.github/workflows/nightly.yml — specifically the aws s3 cp commands in the 'Upload nightly assets to R2' step.

Important Files Changed

Filename Overview
.github/workflows/nightly.yml Adds two new steps: one to generate an R2-targeted Sparkle appcast and one to upload the immutable versioned DMG, latest mutable DMG, and appcast to Cloudflare R2. Logic and ordering (DMGs first, appcast last) are correct; minor concerns around missing Cache-Control headers, double Sparkle builds, and assumed AWS CLI availability.

Sequence Diagram

sequenceDiagram
    participant CI as GitHub Actions (nightly job)
    participant GH as GitHub Releases
    participant R2 as Cloudflare R2 (cmux-binaries)
    participant CDN as files.cmux.com (CDN)
    participant App as cmux (Sparkle)

    CI->>CI: Build & notarize DMG
    CI->>CI: Generate appcast.xml (GitHub URLs)
    CI->>GH: Upload versioned DMG
    CI->>GH: Upload latest DMG
    CI->>GH: Upload appcast.xml

    CI->>CI: Generate appcast-r2.xml (R2 URLs)
    CI->>R2: PutObject: versioned DMG (immutable)
    CI->>R2: PutObject: cmux-nightly-macos.dmg (latest)
    CI->>R2: PutObject: appcast.xml (atomic replace)

    App->>CDN: GET /nightly/appcast.xml
    CDN->>R2: Fetch appcast.xml
    R2-->>CDN: appcast.xml (R2 DMG URLs)
    CDN-->>App: appcast.xml
    App->>CDN: GET /nightly/cmux-nightly-macos-build.dmg
    CDN->>R2: Fetch versioned DMG
    R2-->>CDN: DMG binary
    CDN-->>App: DMG binary
Loading

Reviews (1): Last reviewed commit: "Add R2 dual-write for nightly appcast an..." | Re-trigger Greptile

Comment thread .github/workflows/nightly.yml Outdated
Comment on lines +529 to +537
--endpoint-url "$R2_ENDPOINT"
aws s3 cp cmux-nightly-macos.dmg \
"s3://${BUCKET}/nightly/cmux-nightly-macos.dmg" \
--endpoint-url "$R2_ENDPOINT"

# Upload appcast last (atomic PutObject, no 404 window).
aws s3 cp appcast-r2.xml \
"s3://${BUCKET}/nightly/appcast.xml" \
--endpoint-url "$R2_ENDPOINT"

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.

P2 Missing Cache-Control headers for mutable assets

cmux-nightly-macos.dmg and appcast.xml are mutable files that are replaced on every nightly build. Without explicit Cache-Control: no-cache headers, Cloudflare's CDN layer in front of files.cmux.com may cache these for its default TTL. A client checking for updates could then receive a stale appcast.xml pointing to a freshly-uploaded DMG URL that the CDN is still serving the previous build for.

The versioned/immutable DMG can take a long max-age; the two mutable objects should opt out of caching entirely:

Suggested change
--endpoint-url "$R2_ENDPOINT"
aws s3 cp cmux-nightly-macos.dmg \
"s3://${BUCKET}/nightly/cmux-nightly-macos.dmg" \
--endpoint-url "$R2_ENDPOINT"
# Upload appcast last (atomic PutObject, no 404 window).
aws s3 cp appcast-r2.xml \
"s3://${BUCKET}/nightly/appcast.xml" \
--endpoint-url "$R2_ENDPOINT"
aws s3 cp "$NIGHTLY_DMG_IMMUTABLE" \
"s3://${BUCKET}/nightly/${NIGHTLY_DMG_IMMUTABLE}" \
--endpoint-url "$R2_ENDPOINT" \
--cache-control "max-age=31536000, immutable"
aws s3 cp cmux-nightly-macos.dmg \
"s3://${BUCKET}/nightly/cmux-nightly-macos.dmg" \
--endpoint-url "$R2_ENDPOINT" \
--cache-control "no-cache, no-store, must-revalidate"
# Upload appcast last (atomic PutObject, no 404 window).
aws s3 cp appcast-r2.xml \
"s3://${BUCKET}/nightly/appcast.xml" \
--endpoint-url "$R2_ENDPOINT" \
--cache-control "no-cache, no-store, must-revalidate"

Comment thread .github/workflows/nightly.yml Outdated
Comment on lines +506 to +513
- name: Generate R2-targeted appcast
if: needs.decide.outputs.should_publish == 'true' && steps.current_head_prebuild.outputs.still_current == 'true' && steps.current_head_postbuild.outputs.still_current == 'true'
env:
SPARKLE_PRIVATE_KEY: ${{ secrets.SPARKLE_PRIVATE_KEY }}
DOWNLOAD_URL_PREFIX: "https://files.cmux.com/nightly/"
RELEASE_NOTES_URL: "https://github.com/manaflow-ai/cmux/releases/tag/nightly"
run: |
./scripts/sparkle_generate_appcast.sh "$NIGHTLY_DMG_IMMUTABLE" nightly appcast-r2.xml

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.

P2 Double Sparkle clone and build

sparkle_generate_appcast.sh does a git clone --depth 1 of the Sparkle repo and runs two full xcodebuild invocations on every call. Adding this second call roughly doubles the Sparkle build time in every nightly publish run (on top of the existing call in "Generate Sparkle appcasts (nightly)").

Consider either:

  • Caching the built Sparkle binaries between the two steps (e.g., write the binary paths to $GITHUB_ENV after the first build and pass them in as env vars), or
  • Generating both appcasts in the same script invocation so Sparkle is only built once.

This is non-blocking but may be worth optimizing given the already-long nightly build time.

Comment on lines +515 to +521
- name: Upload nightly assets to R2
if: needs.decide.outputs.should_publish == 'true' && steps.current_head_prebuild.outputs.still_current == 'true' && steps.current_head_postbuild.outputs.still_current == 'true'
env:
AWS_ACCESS_KEY_ID: ${{ secrets.CF_R2_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.CF_R2_SECRET_ACCESS_KEY }}
AWS_DEFAULT_REGION: auto
R2_ENDPOINT: "https://${{ secrets.CF_R2_ACCOUNT_ID }}.r2.cloudflarestorage.com"

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.

P2 AWS CLI availability not verified

The step calls aws s3 cp directly without installing or validating that the AWS CLI is present on the warp-macos-26-arm64-6x runner. macOS GitHub-hosted and many self-hosted macOS runners do not ship with the AWS CLI pre-installed. If it is absent the step will fail with a cryptic "command not found" error rather than a clear message.

Consider adding a guard or an explicit install:

- name: Install AWS CLI (if needed)
  run: |
    if ! command -v aws &>/dev/null; then
      brew install awscli
    fi

Or verify in the run script itself:

command -v aws >/dev/null 2>&1 || { echo "aws CLI not found; install awscli" >&2; exit 1; }

R2 upload failures should not block the existing GitHub Release
publish. This keeps the nightly pipeline safe while R2 is new.

@cubic-dev-ai cubic-dev-ai 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.

1 issue found across 1 file (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name=".github/workflows/nightly.yml">

<violation number="1" location=".github/workflows/nightly.yml:508">
P1: Fail the workflow when R2 upload fails; swallowing upload errors can mark a broken publish as successful.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Comment thread .github/workflows/nightly.yml Outdated

- name: Generate R2-targeted appcast
if: needs.decide.outputs.should_publish == 'true' && steps.current_head_prebuild.outputs.still_current == 'true' && steps.current_head_postbuild.outputs.still_current == 'true'
continue-on-error: true

@cubic-dev-ai cubic-dev-ai Bot Mar 30, 2026 •

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1: Fail the workflow when R2 upload fails; swallowing upload errors can mark a broken publish as successful.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At .github/workflows/nightly.yml, line 508:

<comment>Fail the workflow when R2 upload fails; swallowing upload errors can mark a broken publish as successful.</comment>

<file context>
@@ -505,6 +505,7 @@ jobs:
 
       - name: Generate R2-targeted appcast
         if: needs.decide.outputs.should_publish == 'true' && steps.current_head_prebuild.outputs.still_current == 'true' && steps.current_head_postbuild.outputs.still_current == 'true'
+        continue-on-error: true
         env:
           SPARKLE_PRIVATE_KEY: ${{ secrets.SPARKLE_PRIVATE_KEY }}
</file context>
Suggested change
continue-on-error: true
continue-on-error: false
Fix with Cubic

Same pattern as nightly: upload DMG then appcast to R2
(files.cmux.com/stable/) alongside the GitHub Release.
Both steps use continue-on-error so R2 failures can't
block the release.
- Add Cache-Control headers: immutable versioned DMGs get max-age=1yr,
  mutable appcast.xml and latest DMG get no-cache to prevent stale CDN
- Replace separate appcast generation step with sed URL replacement,
  avoiding a second Sparkle clone+build (signature is over DMG content,
  not the URL)
- Add AWS CLI availability check with fallback brew install
@lawrencecchen
lawrencecchen merged commit d6d9130 into main Mar 30, 2026
14 checks passed
@lawrencecchen
lawrencecchen deleted the feat-r2-dual-write branch March 30, 2026 10:54

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 7ba3ea0aa5

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

./scripts/sparkle_generate_appcast.sh cmux-macos.dmg "$GITHUB_REF_NAME" appcast.xml

- name: Upload release assets to R2
if: steps.guard_release_assets.outputs.skip_upload != 'true' && github.event_name == 'push' && startsWith(github.ref, 'refs/tags/')

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Gate stable R2 publishes to the latest release tag

In .github/workflows/release.yml, this new step runs on any pushed tag and then uploads to fixed keys under stable/ (cmux-macos.dmg and appcast.xml), so a backport or older tag pushed later can overwrite the stable feed with an older build. When clients are switched to files.cmux.com/stable/appcast.xml, that can surface a downgrade path unintentionally. Add a recency/semver guard (or equivalent latest-release check) before writing the stable objects.

Useful? React with 👍 / 👎.

bn-l pushed a commit to bn-l/cmux that referenced this pull request Apr 3, 2026
* Add R2 dual-write for nightly appcast and DMGs

Upload nightly DMGs and appcast to Cloudflare R2 (files.cmux.com)
alongside the existing GitHub Release assets. R2 uses atomic PutObject
so the appcast never 404s during replacement, fixing the transient
SUDownloadError 2001 that occurs when GitHub Release assets are
being overwritten.

DMGs are uploaded before the appcast so the feed never references a
file that doesn't exist yet. The GitHub Release upload is unchanged,
so existing nightly users are unaffected.

A follow-up PR will switch the Sparkle feed URL in the app bundle
from GitHub Releases to R2 after manual verification.

* Add continue-on-error to R2 steps

R2 upload failures should not block the existing GitHub Release
publish. This keeps the nightly pipeline safe while R2 is new.

* Add R2 dual-write for stable release appcast and DMG

Same pattern as nightly: upload DMG then appcast to R2
(files.cmux.com/stable/) alongside the GitHub Release.
Both steps use continue-on-error so R2 failures can't
block the release.

* Address review feedback: cache headers, no double build, AWS CLI guard

- Add Cache-Control headers: immutable versioned DMGs get max-age=1yr,
  mutable appcast.xml and latest DMG get no-cache to prevent stale CDN
- Replace separate appcast generation step with sed URL replacement,
  avoiding a second Sparkle clone+build (signature is over DMG content,
  not the URL)
- Add AWS CLI availability check with fallback brew install

---------

Co-authored-by: Lawrence Chen <lawrencecchen@users.noreply.github.com>

This branch was successfully deployed

1 active deployment
Preview — 7ba3ea0a Deployed Mar 30, 2026 by vercel[bot]
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