Repository navigation
Add R2 dual-write for nightly appcast and DMGs - #2335
Conversation
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.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Caution Review failedThe pull request is closed. ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (2)
📝 WalkthroughWalkthroughAdded new workflow steps to upload release and nightly DMG assets plus a rewritten Sparkle appcast to Cloudflare R2 (S3-compatible) in Changes
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
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
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
📒 Files selected for processing (1)
.github/workflows/nightly.yml
| 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" | ||
|
|
There was a problem hiding this comment.
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 SummaryThis PR adds a parallel upload path for nightly DMGs and the Sparkle appcast to Cloudflare R2 ( Key changes:
Concerns:
Confidence Score: 4/5Safe 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 Important Files Changed
Sequence DiagramsequenceDiagram
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
Reviews (1): Last reviewed commit: "Add R2 dual-write for nightly appcast an..." | Re-trigger Greptile |
| --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" |
There was a problem hiding this comment.
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:
| --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" |
| - 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 |
There was a problem hiding this comment.
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_ENVafter 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.
| - 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" |
There was a problem hiding this comment.
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
fiOr 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.
There was a problem hiding this comment.
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.
|
|
||
| - 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 |
There was a problem hiding this comment.
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>
| continue-on-error: true | |
| continue-on-error: false |
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
There was a problem hiding this comment.
💡 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/') |
There was a problem hiding this comment.
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 👍 / 👎.
* 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>
Summary
files.cmux.com/nightly/) alongside existing GitHub Release assetsSUDownloadError 2001race condition)How it works
DOWNLOAD_URL_PREFIX=https://files.cmux.com/nightly/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.xmlreturns 200Follow-up
Separate PR will switch
SUFeedURLin 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
appcast.xmlby URL-replacing the GitHub download prefix (viased); signatures remain valid.aws s3withcontinue-on-errorand AWS CLI auto-install. GitHub Release uploads are unchanged. A follow-up will switchSUFeedURLto R2.Migration
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