From e6309e7841f333d4b0305116606da129ca385995 Mon Sep 17 00:00:00 2001 From: Lawrence Chen <54008264+lawrencecchen@users.noreply.github.com> Date: Fri, 13 Mar 2026 04:56:19 -0700 Subject: [PATCH] Harden the nightly workflow (#1356) --- .github/workflows/nightly.yml | 68 ++++++++++++++++++++++++++--------- 1 file changed, 51 insertions(+), 17 deletions(-) diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index 41b0b04f..adcadcad 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -13,10 +13,9 @@ on: concurrency: group: nightly-build-${{ github.ref_name }} - # Queue main pushes instead of hard-canceling older runs. The decide job - # already coalesces to the current main HEAD, and we re-check HEAD before - # publishing so stale queued runs exit cleanly instead of showing up red. - cancel-in-progress: false + # Only the newest nightly matters. Cancel older runs so a fresh main push + # does not sit behind an outdated build that would be discarded anyway. + cancel-in-progress: true permissions: contents: write @@ -100,7 +99,7 @@ jobs: build-sign-notarize-nightly: needs: decide if: needs.decide.outputs.should_build == 'true' - runs-on: macos-15 + runs-on: depot-macos-latest steps: - name: Checkout build ref uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 @@ -108,7 +107,29 @@ jobs: ref: ${{ needs.decide.outputs.head_sha }} submodules: recursive + - name: Check whether build commit is still current main HEAD before build + if: needs.decide.outputs.should_publish == 'true' + id: current_head_prebuild + run: | + set -euo pipefail + CURRENT_MAIN_SHA="$(git ls-remote origin refs/heads/main | awk '{print $1}')" + BUILD_SHA="${{ needs.decide.outputs.head_sha }}" + if [ "$CURRENT_MAIN_SHA" = "$BUILD_SHA" ]; then + STILL_CURRENT=true + else + STILL_CURRENT=false + fi + echo "still_current=${STILL_CURRENT}" >> "$GITHUB_OUTPUT" + { + echo "### Pre-build publish guard" + echo + echo "- build sha: \`$BUILD_SHA\`" + echo "- current main sha: \`$CURRENT_MAIN_SHA\`" + echo "- continue build/sign/publish: \`$STILL_CURRENT\`" + } >> "$GITHUB_STEP_SUMMARY" + - name: Select Xcode + if: needs.decide.outputs.should_publish != 'true' || steps.current_head_prebuild.outputs.still_current == 'true' run: | set -euo pipefail if [ -d "/Applications/Xcode.app/Contents/Developer" ]; then @@ -128,14 +149,17 @@ jobs: xcrun --sdk macosx --show-sdk-path - name: Install build deps + if: needs.decide.outputs.should_publish != 'true' || steps.current_head_prebuild.outputs.still_current == 'true' run: | npm install --global "create-dmg@${CREATE_DMG_VERSION}" - name: Download pre-built GhosttyKit.xcframework + if: needs.decide.outputs.should_publish != 'true' || steps.current_head_prebuild.outputs.still_current == 'true' run: | ./scripts/download-prebuilt-ghosttykit.sh - name: Cache Swift packages + if: needs.decide.outputs.should_publish != 'true' || steps.current_head_prebuild.outputs.still_current == 'true' uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4 with: path: .spm-cache @@ -143,6 +167,7 @@ jobs: restore-keys: spm- - name: Derive Sparkle public key from private key + if: needs.decide.outputs.should_publish != 'true' || steps.current_head_prebuild.outputs.still_current == 'true' env: SPARKLE_PRIVATE_KEY: ${{ secrets.SPARKLE_PRIVATE_KEY }} run: | @@ -155,6 +180,7 @@ jobs: echo "SPARKLE_PUBLIC_KEY=$DERIVED_PUBLIC_KEY" >> "$GITHUB_ENV" - name: Build universal nightly app (Release) + if: needs.decide.outputs.should_publish != 'true' || steps.current_head_prebuild.outputs.still_current == 'true' run: | xcodebuild -scheme cmux -configuration Release -derivedDataPath build-universal \ -destination 'generic/platform=macOS' \ @@ -164,6 +190,7 @@ jobs: CODE_SIGNING_ALLOWED=NO ASSETCATALOG_COMPILER_APPICON_NAME=AppIcon-Nightly build - name: Verify nightly binary architectures + if: needs.decide.outputs.should_publish != 'true' || steps.current_head_prebuild.outputs.still_current == 'true' run: | set -euo pipefail APP_BINARY="build-universal/Build/Products/Release/cmux.app/Contents/MacOS/cmux" @@ -176,15 +203,16 @@ jobs: [[ "$CLI_ARCHS" == *arm64* && "$CLI_ARCHS" == *x86_64* ]] - name: Run CLI version memory guard regression + if: needs.decide.outputs.should_publish != 'true' || steps.current_head_prebuild.outputs.still_current == 'true' run: | set -euo pipefail CLI_BINARY="build-universal/Build/Products/Release/cmux.app/Contents/Resources/bin/cmux" [ -x "$CLI_BINARY" ] || { echo "cmux CLI binary not found at $CLI_BINARY" >&2; exit 1; } CMUX_CLI_BIN="$CLI_BINARY" python3 tests/test_cli_version_memory_guard.py - - name: Check whether build commit is still current main HEAD - if: needs.decide.outputs.should_publish == 'true' - id: current_head + - name: Check whether build commit is still current main HEAD after build + if: needs.decide.outputs.should_publish == 'true' && steps.current_head_prebuild.outputs.still_current == 'true' + id: current_head_postbuild run: | set -euo pipefail CURRENT_MAIN_SHA="$(git ls-remote origin refs/heads/main | awk '{print $1}')" @@ -196,7 +224,7 @@ jobs: fi echo "still_current=${STILL_CURRENT}" >> "$GITHUB_OUTPUT" { - echo "### Publish guard" + echo "### Post-build publish guard" echo echo "- build sha: \`$BUILD_SHA\`" echo "- current main sha: \`$CURRENT_MAIN_SHA\`" @@ -204,7 +232,7 @@ jobs: } >> "$GITHUB_STEP_SUMMARY" - name: Inject nightly identities and metadata - if: needs.decide.outputs.should_publish != 'true' || steps.current_head.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') run: | set -euo pipefail SHORT_SHA="${{ needs.decide.outputs.short_sha }}" @@ -259,7 +287,7 @@ jobs: echo "Commit SHA: ${SHORT_SHA}" - name: Import signing cert - if: needs.decide.outputs.should_publish != 'true' || steps.current_head.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') env: APPLE_CERTIFICATE_BASE64: ${{ secrets.APPLE_CERTIFICATE_BASE64 }} APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} @@ -283,7 +311,7 @@ jobs: security list-keychains -d user -s build.keychain - name: Codesign apps - if: needs.decide.outputs.should_publish != 'true' || steps.current_head.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') env: APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }} run: | @@ -304,7 +332,7 @@ jobs: done - name: Notarize apps and dmgs - if: needs.decide.outputs.should_publish != 'true' || steps.current_head.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') env: APPLE_ID: ${{ secrets.APPLE_ID }} APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }} @@ -369,7 +397,7 @@ jobs: "$NIGHTLY_DMG_IMMUTABLE" - name: Upload dSYMs to Sentry - if: needs.decide.outputs.should_publish != 'true' || steps.current_head.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') env: SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} SENTRY_ORG: manaflow @@ -384,7 +412,7 @@ jobs: build-universal/Build/Products/Release/ - name: Generate Sparkle appcasts (nightly) - if: needs.decide.outputs.should_publish != 'true' || steps.current_head.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') env: SPARKLE_PRIVATE_KEY: ${{ secrets.SPARKLE_PRIVATE_KEY }} run: | @@ -393,6 +421,9 @@ jobs: exit 1 fi ./scripts/sparkle_generate_appcast.sh "$NIGHTLY_DMG_IMMUTABLE" nightly appcast.xml + # Keep the legacy universal feed alive long enough for older nightly + # installs to migrate onto the unified nightly appcast. + cp appcast.xml appcast-universal.xml - name: Upload branch nightly artifacts if: needs.decide.outputs.should_publish != 'true' @@ -402,10 +433,11 @@ jobs: path: | cmux-nightly-macos*.dmg appcast.xml + appcast-universal.xml if-no-files-found: error - name: Move nightly tag to built commit - if: needs.decide.outputs.should_publish == 'true' && steps.current_head.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' run: | set -euo pipefail git config user.name "github-actions[bot]" @@ -414,7 +446,7 @@ jobs: git push origin refs/tags/nightly --force - name: Publish nightly release assets - if: needs.decide.outputs.should_publish == 'true' && steps.current_head.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: softprops/action-gh-release@a06a81a03ee405af7f2048a818ed3f03bbf83c7b # v2 with: tag_name: nightly @@ -427,12 +459,14 @@ jobs: **cmux NIGHTLY** is published as a universal app: - bundle ID `com.cmuxterm.app.nightly` - feed `appcast.xml` + - compatibility feed `appcast-universal.xml` for older universal nightlies [Download cmux-nightly-macos.dmg](https://github.com/manaflow-ai/cmux/releases/download/nightly/cmux-nightly-macos.dmg) files: | cmux-nightly-macos-${{ github.run_id }}*.dmg cmux-nightly-macos.dmg appcast.xml + appcast-universal.xml overwrite_files: true - name: Cleanup keychain