Add R2 dual-write for nightly appcast and DMGs (#2335)
* 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 commit is contained in:
parent
90a9edb761
commit
d6d9130c72
2 changed files with 68 additions and 0 deletions
37
.github/workflows/nightly.yml
vendored
37
.github/workflows/nightly.yml
vendored
|
|
@ -503,6 +503,43 @@ jobs:
|
|||
# installs to migrate onto the unified nightly appcast.
|
||||
cp appcast.xml appcast-universal.xml
|
||||
|
||||
- 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'
|
||||
continue-on-error: 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"
|
||||
run: |
|
||||
set -euo pipefail
|
||||
command -v aws >/dev/null 2>&1 || { echo "Installing AWS CLI..."; brew install awscli; }
|
||||
BUCKET=cmux-binaries
|
||||
|
||||
# Derive R2 appcast from the GitHub one by replacing the download URL prefix.
|
||||
# EdDSA signature is over the DMG content, not the URL, so this is safe.
|
||||
sed 's|https://github.com/manaflow-ai/cmux/releases/download/nightly/|https://files.cmux.com/nightly/|g' \
|
||||
appcast.xml > appcast-r2.xml
|
||||
|
||||
# Upload immutable versioned DMG (cacheable forever).
|
||||
aws s3 cp "$NIGHTLY_DMG_IMMUTABLE" \
|
||||
"s3://${BUCKET}/nightly/${NIGHTLY_DMG_IMMUTABLE}" \
|
||||
--endpoint-url "$R2_ENDPOINT" \
|
||||
--cache-control "max-age=31536000, immutable"
|
||||
# Upload mutable latest DMG (no cache).
|
||||
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"
|
||||
|
||||
echo "R2 upload complete: https://files.cmux.com/nightly/appcast.xml"
|
||||
|
||||
- name: Attest remote daemon nightly assets
|
||||
if: needs.decide.outputs.should_publish != 'true' || (steps.current_head_prebuild.outputs.still_current == 'true' && steps.current_head_postbuild.outputs.still_current == 'true')
|
||||
uses: actions/attest-build-provenance@a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32 # v4.1.0
|
||||
|
|
|
|||
31
.github/workflows/release.yml
vendored
31
.github/workflows/release.yml
vendored
|
|
@ -336,6 +336,37 @@ jobs:
|
|||
fi
|
||||
./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/')
|
||||
continue-on-error: 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"
|
||||
run: |
|
||||
set -euo pipefail
|
||||
command -v aws >/dev/null 2>&1 || { echo "Installing AWS CLI..."; brew install awscli; }
|
||||
BUCKET=cmux-binaries
|
||||
|
||||
# Derive R2 appcast by replacing the download URL prefix.
|
||||
sed "s|https://github.com/manaflow-ai/cmux/releases/download/${GITHUB_REF_NAME}/|https://files.cmux.com/stable/|g" \
|
||||
appcast.xml > appcast-r2.xml
|
||||
|
||||
# Upload DMG first so the appcast never references a missing file.
|
||||
aws s3 cp cmux-macos.dmg \
|
||||
"s3://${BUCKET}/stable/cmux-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}/stable/appcast.xml" \
|
||||
--endpoint-url "$R2_ENDPOINT" \
|
||||
--cache-control "no-cache, no-store, must-revalidate"
|
||||
|
||||
echo "R2 upload complete: https://files.cmux.com/stable/appcast.xml"
|
||||
|
||||
- name: Attest remote daemon release assets
|
||||
if: steps.guard_release_assets.outputs.skip_all != 'true'
|
||||
uses: actions/attest-build-provenance@a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32 # v4.1.0
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue