Repository navigation
Add release candidate app channel #4512
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Closed
Closed
Changes from all commits
Commits
Show all changes
18 commits
Select commit
Hold shift + click to select a range
4cf162a
Add release candidate app channel
lawrencecchen 2c85fe2
Merge remote-tracking branch 'origin/main' into task-release-candidat…
lawrencecchen 21dff8a
Harden release candidate workflows
lawrencecchen b523408
Guard release candidate promotion ordering
lawrencecchen 89faaf7
Merge remote-tracking branch 'origin/main' into task-release-candidat…
lawrencecchen f70fc82
Scope RC app runtime identity
lawrencecchen b1073b6
Support RC auth and Python client routing
lawrencecchen c639eba
Harden RC promotion guards
lawrencecchen 2f5476b
Verify stable tag before RC promotion
lawrencecchen 301c83d
Harden RC workflow retry guards
lawrencecchen f93f7dc
Route RC theme reloads to RC bundle
lawrencecchen bf90bd2
Keep RC promotion side effects retryable
lawrencecchen 2b94c2b
Keep RC appcast latest checks consistent
lawrencecchen 2041aab
Harden RC socket autodiscovery tests
lawrencecchen 1edaf4b
Merge remote-tracking branch 'origin/main' into task-release-candidat…
lawrencecchen b2fa86d
fix: prefer channel socket fallback over stale marker
lawrencecchen f374c8b
fix: record remote daemon signer workflow
lawrencecchen b1cc8c8
fix: keep rc tags out of stable release workflow
lawrencecchen File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,285 @@ | ||
| name: Promote release candidate | ||
|
|
||
| on: | ||
| workflow_dispatch: | ||
| inputs: | ||
| rc_tag: | ||
| description: RC tag to promote, for example v0.64.0-rc.1 | ||
| required: true | ||
| type: string | ||
|
|
||
| concurrency: | ||
| group: promote-release-candidate | ||
| cancel-in-progress: false | ||
|
|
||
| permissions: | ||
| contents: write | ||
| actions: write | ||
|
|
||
| jobs: | ||
| promote: | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| - name: Checkout | ||
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | ||
|
|
||
| - name: Resolve tags | ||
| id: vars | ||
| env: | ||
| INPUT_RC_TAG: ${{ inputs.rc_tag }} | ||
| run: | | ||
| set -euo pipefail | ||
| RC_TAG="$INPUT_RC_TAG" | ||
| if ! [[ "$RC_TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+-rc\.[0-9]+$ ]]; then | ||
| echo "rc_tag must look like v0.64.0-rc.1, got: $RC_TAG" >&2 | ||
| exit 1 | ||
| fi | ||
| STABLE_TAG="${RC_TAG%%-rc.*}" | ||
| VERSION="${STABLE_TAG#v}" | ||
| git fetch origin "refs/tags/${RC_TAG}:refs/tags/${RC_TAG}" | ||
| CANDIDATE_SHA="$(git rev-list -n 1 "$RC_TAG")" | ||
| { | ||
| echo "RC_TAG=$RC_TAG" | ||
| echo "STABLE_TAG=$STABLE_TAG" | ||
| echo "VERSION=$VERSION" | ||
| echo "CANDIDATE_SHA=$CANDIDATE_SHA" | ||
| } >> "$GITHUB_ENV" | ||
| { | ||
| echo "rc_tag=$RC_TAG" | ||
| echo "stable_tag=$STABLE_TAG" | ||
| echo "version=$VERSION" | ||
| echo "candidate_sha=$CANDIDATE_SHA" | ||
| } >> "$GITHUB_OUTPUT" | ||
| { | ||
| echo "### Promote release candidate" | ||
| echo | ||
| echo "- rc tag: \`$RC_TAG\`" | ||
| echo "- stable tag: \`$STABLE_TAG\`" | ||
| echo "- candidate sha: \`$CANDIDATE_SHA\`" | ||
| } >> "$GITHUB_STEP_SUMMARY" | ||
|
|
||
| - name: Guard immutable stable release assets | ||
| id: guard_stable_assets | ||
| uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 | ||
| env: | ||
| STABLE_TAG: ${{ steps.vars.outputs.stable_tag }} | ||
| CANDIDATE_SHA: ${{ steps.vars.outputs.candidate_sha }} | ||
| with: | ||
| script: | | ||
| const { | ||
| IMMUTABLE_RELEASE_ASSETS, | ||
| evaluateReleaseAssetGuard, | ||
| } = require('./scripts/release_asset_guard'); | ||
| const { owner, repo } = context.repo; | ||
| const stableTag = process.env.STABLE_TAG; | ||
| const candidateSha = process.env.CANDIDATE_SHA; | ||
|
|
||
| async function resolveTagSha(tagName) { | ||
| const ref = await github.rest.git.getRef({ | ||
| owner, | ||
| repo, | ||
| ref: `tags/${tagName}`, | ||
| }); | ||
| if (ref.data.object.type !== 'tag') { | ||
| return ref.data.object.sha; | ||
| } | ||
| const tag = await github.rest.git.getTag({ | ||
| owner, | ||
| repo, | ||
| tag_sha: ref.data.object.sha, | ||
| }); | ||
| return tag.data.object.sha; | ||
| } | ||
|
|
||
| core.setOutput('skip_release_upload', 'false'); | ||
| core.setOutput('appcast_source', 'candidate'); | ||
| try { | ||
| const release = await github.rest.repos.getReleaseByTag({ | ||
| owner, | ||
| repo, | ||
| tag: stableTag, | ||
| }); | ||
| let existingTagSha; | ||
| try { | ||
| existingTagSha = await resolveTagSha(stableTag); | ||
| } catch (tagError) { | ||
| core.setFailed( | ||
| `Release ${stableTag} exists but refs/tags/${stableTag} could not be resolved: ${tagError.message}` | ||
| ); | ||
| return; | ||
| } | ||
| if (existingTagSha !== candidateSha) { | ||
| core.setFailed( | ||
| `Release ${stableTag} already points to ${existingTagSha}, ` + | ||
| `not candidate ${candidateSha}. Publish a new stable version instead of replacing it.` | ||
| ); | ||
| return; | ||
| } | ||
| const existingAssetNames = (release.data.assets || []).map((asset) => asset.name); | ||
| const { | ||
| conflicts, | ||
| missingImmutableAssets, | ||
| hasPartialConflict, | ||
| shouldSkipBuildAndUpload, | ||
| } = evaluateReleaseAssetGuard({ | ||
| existingAssetNames, | ||
| immutableAssetNames: IMMUTABLE_RELEASE_ASSETS, | ||
| }); | ||
|
|
||
| if (hasPartialConflict) { | ||
| core.setFailed( | ||
| `Release ${stableTag} has partial immutable assets. Existing: ${conflicts.join(', ')}. ` + | ||
| `Missing: ${missingImmutableAssets.join(', ')}. Resolve release assets manually.` | ||
| ); | ||
| return; | ||
| } | ||
|
|
||
| if (shouldSkipBuildAndUpload) { | ||
| core.notice(`Release ${stableTag} already has complete immutable assets. Reusing release appcast for post-publish steps.`); | ||
| core.setOutput('skip_release_upload', 'true'); | ||
| core.setOutput('appcast_source', 'release'); | ||
| return; | ||
|
cursor[bot] marked this conversation as resolved.
|
||
| } | ||
| } catch (error) { | ||
| if (error.status === 404) { | ||
| try { | ||
| const existingTagSha = await resolveTagSha(stableTag); | ||
| if (existingTagSha !== candidateSha) { | ||
| core.setFailed( | ||
| `Tag ${stableTag} already points to ${existingTagSha}, ` + | ||
| `not candidate ${candidateSha}. Publish a new stable version instead of replacing it.` | ||
| ); | ||
| return; | ||
| } | ||
| core.notice(`Tag ${stableTag} already points to candidate ${candidateSha}; safe to create the release.`); | ||
| } catch (tagError) { | ||
| if (tagError.status !== 404) { | ||
| throw tagError; | ||
| } | ||
| core.notice(`Release ${stableTag} and refs/tags/${stableTag} do not exist yet; safe to publish.`); | ||
| } | ||
| return; | ||
| } | ||
| throw error; | ||
| } | ||
|
|
||
| - name: Download stable candidate assets from RC release | ||
| if: steps.guard_stable_assets.outputs.skip_release_upload != 'true' | ||
| env: | ||
| GH_TOKEN: ${{ github.token }} | ||
| run: | | ||
| set -euo pipefail | ||
| mkdir -p rc-assets remote-daemon-assets | ||
| gh release download "$RC_TAG" \ | ||
| --repo "$GITHUB_REPOSITORY" \ | ||
| --pattern 'stable-candidate-*' \ | ||
| --dir rc-assets | ||
|
|
||
| mv rc-assets/stable-candidate-cmux-macos.dmg cmux-macos.dmg | ||
| mv rc-assets/stable-candidate-appcast.xml appcast.xml | ||
| for asset in \ | ||
| cmuxd-remote-darwin-arm64 \ | ||
| cmuxd-remote-darwin-amd64 \ | ||
| cmuxd-remote-linux-arm64 \ | ||
| cmuxd-remote-linux-amd64 \ | ||
| cmuxd-remote-checksums.txt \ | ||
| cmuxd-remote-manifest.json | ||
| do | ||
| mv "rc-assets/stable-candidate-${asset}" "remote-daemon-assets/${asset}" | ||
| done | ||
|
|
||
| grep -F "releases/download/${STABLE_TAG}/cmux-macos.dmg" appcast.xml >/dev/null | ||
| if grep -F "$RC_TAG" appcast.xml >/dev/null; then | ||
| echo "Stable appcast unexpectedly references $RC_TAG" >&2 | ||
| exit 1 | ||
| fi | ||
|
|
||
| - name: Publish stable release from candidate assets | ||
| if: steps.guard_stable_assets.outputs.skip_release_upload != 'true' | ||
| env: | ||
| GH_TOKEN: ${{ github.token }} | ||
| run: | | ||
| set -euo pipefail | ||
| FILES=( | ||
| cmux-macos.dmg | ||
| appcast.xml | ||
| remote-daemon-assets/cmuxd-remote-darwin-arm64 | ||
| remote-daemon-assets/cmuxd-remote-darwin-amd64 | ||
| remote-daemon-assets/cmuxd-remote-linux-arm64 | ||
| remote-daemon-assets/cmuxd-remote-linux-amd64 | ||
| remote-daemon-assets/cmuxd-remote-checksums.txt | ||
| remote-daemon-assets/cmuxd-remote-manifest.json | ||
| ) | ||
|
|
||
| EXISTING_STABLE_TAGS="$(gh release list --repo "$GITHUB_REPOSITORY" --exclude-drafts --exclude-pre-releases \ | ||
| --json tagName -q '.[].tagName')" | ||
| LATEST_STABLE_TAG="$(printf '%s\n%s\n' "$EXISTING_STABLE_TAGS" "$STABLE_TAG" | sed '/^$/d' | sort -V | tail -1)" | ||
| LATEST_FLAG=(--latest=false) | ||
|
cubic-dev-ai[bot] marked this conversation as resolved.
|
||
| if [ "$LATEST_STABLE_TAG" = "$STABLE_TAG" ]; then | ||
| LATEST_FLAG=(--latest) | ||
| fi | ||
|
lawrencecchen marked this conversation as resolved.
|
||
|
|
||
| if gh release view "$STABLE_TAG" --repo "$GITHUB_REPOSITORY" >/dev/null 2>&1; then | ||
| gh release upload "$STABLE_TAG" "${FILES[@]}" \ | ||
| --repo "$GITHUB_REPOSITORY" | ||
| else | ||
| gh release create "$STABLE_TAG" "${FILES[@]}" \ | ||
| --repo "$GITHUB_REPOSITORY" \ | ||
| --target "$CANDIDATE_SHA" \ | ||
| --title "$STABLE_TAG" \ | ||
| --generate-notes \ | ||
| "${LATEST_FLAG[@]}" | ||
| fi | ||
|
lawrencecchen marked this conversation as resolved.
|
||
|
|
||
| - name: Upload stable appcast to R2 | ||
| env: | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
| GH_TOKEN: ${{ github.token }} | ||
| 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" | ||
| APPCAST_SOURCE: ${{ steps.guard_stable_assets.outputs.appcast_source }} | ||
| run: | | ||
| set -euo pipefail | ||
| command -v aws >/dev/null 2>&1 || { echo "Installing AWS CLI..."; python3 -m pip install --user awscli; } | ||
| export PATH="$HOME/.local/bin:$PATH" | ||
| echo "$HOME/.local/bin" >> "$GITHUB_PATH" | ||
| if [ "${APPCAST_SOURCE:-candidate}" = "release" ]; then | ||
| rm -f appcast.xml | ||
| gh release download "$STABLE_TAG" \ | ||
| --repo "$GITHUB_REPOSITORY" \ | ||
| --pattern appcast.xml \ | ||
| --dir . | ||
| fi | ||
| test -s appcast.xml | ||
|
|
||
| STABLE_RELEASE_TAGS="$(gh release list --repo "$GITHUB_REPOSITORY" --exclude-drafts --exclude-pre-releases \ | ||
| --json tagName -q '.[].tagName')" | ||
| LATEST="$(printf '%s\n%s\n' "$STABLE_RELEASE_TAGS" "$STABLE_TAG" | sed '/^$/d' | sort -V | tail -1)" | ||
| if [ -n "$LATEST" ] && [ "$LATEST" != "$STABLE_TAG" ]; then | ||
| echo "Skipping R2 stable upload: $STABLE_TAG is not the latest release ($LATEST)" | ||
| exit 0 | ||
| fi | ||
|
|
||
| aws s3 cp appcast.xml \ | ||
| "s3://cmux-binaries/stable/appcast.xml" \ | ||
| --endpoint-url "$R2_ENDPOINT" \ | ||
| --cache-control "no-cache, no-store, must-revalidate" | ||
|
|
||
| - name: Trigger Homebrew cask update | ||
| continue-on-error: true | ||
| env: | ||
| GH_TOKEN: ${{ github.token }} | ||
| run: | | ||
| set -euo pipefail | ||
| STABLE_RELEASE_TAGS="$(gh release list --repo "$GITHUB_REPOSITORY" --exclude-drafts --exclude-pre-releases \ | ||
| --json tagName -q '.[].tagName')" | ||
| LATEST="$(printf '%s\n%s\n' "$STABLE_RELEASE_TAGS" "$STABLE_TAG" | sed '/^$/d' | sort -V | tail -1)" | ||
| if [ -n "$LATEST" ] && [ "$LATEST" != "$STABLE_TAG" ]; then | ||
| echo "Skipping Homebrew update: $STABLE_TAG is not the latest release ($LATEST)" | ||
| exit 0 | ||
| fi | ||
|
|
||
| gh workflow run update-homebrew.yml \ | ||
| --repo "$GITHUB_REPOSITORY" \ | ||
| -f version="$VERSION" | ||
|
cursor[bot] marked this conversation as resolved.
|
||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.