Simplify R2 to appcast-only (keep DMGs on GitHub) (#2363)

* Simplify R2 upload to appcast-only (keep DMGs on GitHub)

DMGs are immutable per-build on GitHub Releases (unique filenames,
no overwrite), so there's no race condition for them. Only the
appcast.xml needs atomic replacement, which R2 PutObject provides.

Upload the original appcast.xml as-is (GitHub Release DMG URLs)
to R2. No sed URL rewriting, no DMG uploads, less storage/bandwidth.

* Move R2 appcast upload after GitHub Release publish

The R2 appcast references GitHub Release DMG URLs, so it must be
uploaded after the DMGs exist on GitHub. Previously the R2 upload
ran before the publish step, creating a brief window where the
appcast pointed to a not-yet-existing DMG.

* Add semver guard to stable R2 appcast upload

Prevents a backport tag (e.g. v0.62.1 pushed after v0.63.1) from
overwriting the stable appcast with an older version. Uses sort -V
to compare all non-prerelease tags and only uploads if the current
tag is the highest.

---------

Co-authored-by: Lawrence Chen <lawrencecchen@users.noreply.github.com>
This commit is contained in:
Lawrence Chen 2026-03-30 04:50:18 -07:00 committed by GitHub
parent f6c949add7
commit 27aab3a035
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 54 additions and 68 deletions

View file

@ -503,43 +503,6 @@ jobs:
# installs to migrate onto the unified nightly appcast. # installs to migrate onto the unified nightly appcast.
cp appcast.xml appcast-universal.xml 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 - 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') 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 uses: actions/attest-build-provenance@a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32 # v4.1.0
@ -598,6 +561,28 @@ jobs:
appcast-universal.xml appcast-universal.xml
overwrite_files: true overwrite_files: true
- name: Upload nightly appcast 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; }
# Upload after GitHub Release publish so the appcast never references
# a DMG that doesn't exist yet. R2 PutObject is atomic, so the appcast
# is either the old version or the new one, never missing.
aws s3 cp appcast.xml \
"s3://cmux-binaries/nightly/appcast.xml" \
--endpoint-url "$R2_ENDPOINT" \
--cache-control "no-cache, no-store, must-revalidate"
echo "R2 appcast upload complete: https://files.cmux.com/nightly/appcast.xml"
- name: Cleanup keychain - name: Cleanup keychain
if: always() if: always()
run: | run: |

View file

@ -336,37 +336,6 @@ jobs:
fi fi
./scripts/sparkle_generate_appcast.sh cmux-macos.dmg "$GITHUB_REF_NAME" appcast.xml ./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 - name: Attest remote daemon release assets
if: steps.guard_release_assets.outputs.skip_all != 'true' if: steps.guard_release_assets.outputs.skip_all != 'true'
uses: actions/attest-build-provenance@a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32 # v4.1.0 uses: actions/attest-build-provenance@a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32 # v4.1.0
@ -406,6 +375,38 @@ jobs:
generate_release_notes: true generate_release_notes: true
overwrite_files: false overwrite_files: false
- name: Upload release appcast 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; }
# Guard: only upload if this tag is the highest semver release.
# Prevents a backport tag (e.g. v0.62.1 after v0.63.1) from
# overwriting the stable appcast with an older version.
LATEST=$(gh release list --exclude-drafts --exclude-pre-releases \
--json tagName -q '.[].tagName' | sort -V | tail -1)
if [ -n "$LATEST" ] && [ "$LATEST" != "$GITHUB_REF_NAME" ]; then
echo "Skipping R2 stable upload: $GITHUB_REF_NAME is not the latest release ($LATEST)"
exit 0
fi
# Upload after GitHub Release publish so the appcast never references
# a DMG that doesn't exist yet. R2 PutObject is atomic, so the appcast
# is either the old version or the new one, never missing.
aws s3 cp appcast.xml \
"s3://cmux-binaries/stable/appcast.xml" \
--endpoint-url "$R2_ENDPOINT" \
--cache-control "no-cache, no-store, must-revalidate"
echo "R2 appcast upload complete: https://files.cmux.com/stable/appcast.xml"
- name: Cleanup keychain - name: Cleanup keychain
if: always() if: always()
run: | run: |