diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index 1b47b2e3..64dc72a6 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -503,43 +503,6 @@ 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 @@ -598,6 +561,28 @@ jobs: appcast-universal.xml 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 if: always() run: | diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 5e50e61d..2f4be207 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -336,37 +336,6 @@ 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 @@ -406,6 +375,38 @@ jobs: generate_release_notes: true 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 if: always() run: |