Fix homebrew SHA mismatch race condition (#111)
Root cause: update-homebrew.yml triggered on release:published, which fires before softprops/action-gh-release finishes uploading assets. The workflow downloaded a 404 page instead of the DMG and committed its SHA. Fix: - Change trigger from release:published to workflow_run (fires after the release workflow completes, guaranteeing assets are uploaded) - Add download validation with retries and file size checks - Add SHA verification step before committing to the cask - Add homebrew cask update to build-sign-upload.sh for local releases - Add regression test (tests/test_homebrew_sha.sh) - Update /release and /release-local skills with homebrew verification steps Fixes #110
This commit is contained in:
parent
41639d226c
commit
fc1de08561
6 changed files with 158 additions and 8 deletions
|
|
@ -41,10 +41,15 @@ Full end-to-end release built locally. Bumps version, updates changelog, tags, t
|
|||
./scripts/build-sign-upload.sh vX.Y.Z
|
||||
```
|
||||
|
||||
This script handles: GhosttyKit build, xcodebuild, Sparkle key injection, codesigning, notarization (app + DMG), appcast generation, GitHub release upload, and cleanup.
|
||||
This script handles: GhosttyKit build, xcodebuild, Sparkle key injection, codesigning, notarization (app + DMG), appcast generation, GitHub release upload, homebrew cask update, and cleanup.
|
||||
|
||||
If the script fails, run `say "cmux release failed"`.
|
||||
|
||||
### 7. Verify homebrew cask
|
||||
|
||||
- Run `bash tests/test_homebrew_sha.sh` to confirm the cask SHA matches the release DMG
|
||||
- Update the homebrew-cmux submodule pointer: `git add homebrew-cmux && git commit -m "Update homebrew-cmux submodule to latest" && git push origin main`
|
||||
|
||||
## Changelog Guidelines
|
||||
|
||||
**Include only end-user visible changes:**
|
||||
|
|
|
|||
|
|
@ -55,7 +55,13 @@ Prepare a new release for cmux. This command updates the changelog, bumps the ve
|
|||
- Verify the release appears at: https://github.com/manaflow-ai/cmux/releases
|
||||
- Check that the DMG is attached to the release
|
||||
|
||||
12. **Notify**
|
||||
12. **Verify homebrew cask update**
|
||||
- The "Update Homebrew Cask" workflow triggers automatically after the release workflow completes
|
||||
- Watch: `gh run list --workflow=update-homebrew.yml --limit=1` and `gh run watch`
|
||||
- Verify: `cd homebrew-cmux && git pull && grep version Casks/cmux.rb`
|
||||
- Run `bash tests/test_homebrew_sha.sh` to confirm the SHA matches
|
||||
|
||||
13. **Notify**
|
||||
- On success: `say "cmux release complete"`
|
||||
- On failure: `say "cmux release failed"`
|
||||
|
||||
|
|
|
|||
49
.github/workflows/update-homebrew.yml
vendored
49
.github/workflows/update-homebrew.yml
vendored
|
|
@ -1,12 +1,15 @@
|
|||
name: Update Homebrew Cask
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
# Trigger after the release workflow completes (not on release:published,
|
||||
# which fires before assets finish uploading — causing SHA mismatch).
|
||||
workflow_run:
|
||||
workflows: ["Release macOS app"]
|
||||
types: [completed]
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: 'Version tag (e.g., v1.9.0)'
|
||||
description: 'Version (e.g., 0.58.0 or v0.58.0)'
|
||||
required: true
|
||||
|
||||
permissions:
|
||||
|
|
@ -15,6 +18,10 @@ permissions:
|
|||
jobs:
|
||||
update-cask:
|
||||
runs-on: ubuntu-latest
|
||||
# Only run if the release workflow succeeded (or manual trigger)
|
||||
if: >-
|
||||
github.event_name == 'workflow_dispatch' ||
|
||||
github.event.workflow_run.conclusion == 'success'
|
||||
steps:
|
||||
- name: Get version
|
||||
id: version
|
||||
|
|
@ -22,18 +29,40 @@ jobs:
|
|||
if [ -n "${{ github.event.inputs.version }}" ]; then
|
||||
VERSION="${{ github.event.inputs.version }}"
|
||||
else
|
||||
VERSION="${{ github.event.release.tag_name }}"
|
||||
# workflow_run: extract tag from the triggering workflow's head branch
|
||||
VERSION="${{ github.event.workflow_run.head_branch }}"
|
||||
fi
|
||||
VERSION="${VERSION#v}"
|
||||
if [ -z "$VERSION" ]; then
|
||||
echo "Could not determine version" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||
echo "Updating homebrew cask to version $VERSION"
|
||||
|
||||
- name: Download DMG and get SHA256
|
||||
id: sha
|
||||
run: |
|
||||
VERSION="${{ steps.version.outputs.version }}"
|
||||
curl -sL "https://github.com/manaflow-ai/cmux/releases/download/v${VERSION}/cmux-macos.dmg" -o cmux.dmg
|
||||
URL="https://github.com/manaflow-ai/cmux/releases/download/v${VERSION}/cmux-macos.dmg"
|
||||
MAX_RETRIES=5
|
||||
for i in $(seq 1 $MAX_RETRIES); do
|
||||
HTTP_CODE=$(curl -sL -w '%{http_code}' "$URL" -o cmux.dmg)
|
||||
FILE_SIZE=$(stat --printf="%s" cmux.dmg 2>/dev/null || stat -f%z cmux.dmg)
|
||||
if [ "$HTTP_CODE" = "200" ] && [ "$FILE_SIZE" -gt 1000000 ]; then
|
||||
echo "Download OK: HTTP $HTTP_CODE, size $FILE_SIZE bytes"
|
||||
break
|
||||
fi
|
||||
echo "Attempt $i/$MAX_RETRIES: HTTP $HTTP_CODE, size ${FILE_SIZE:-0} bytes"
|
||||
if [ "$i" -eq "$MAX_RETRIES" ]; then
|
||||
echo "Failed to download DMG after $MAX_RETRIES attempts" >&2
|
||||
exit 1
|
||||
fi
|
||||
sleep 30
|
||||
done
|
||||
SHA256=$(shasum -a 256 cmux.dmg | cut -d' ' -f1)
|
||||
echo "sha256=$SHA256" >> $GITHUB_OUTPUT
|
||||
echo "DMG SHA256: $SHA256"
|
||||
|
||||
- name: Checkout homebrew-cmux
|
||||
uses: actions/checkout@v4
|
||||
|
|
@ -76,6 +105,16 @@ jobs:
|
|||
# Remove leading whitespace from heredoc
|
||||
sed -i 's/^ //' homebrew-cmux/Casks/cmux.rb
|
||||
|
||||
- name: Verify cask SHA matches DMG
|
||||
run: |
|
||||
CASK_SHA=$(grep 'sha256' homebrew-cmux/Casks/cmux.rb | sed 's/.*"\(.*\)".*/\1/')
|
||||
ACTUAL_SHA=$(shasum -a 256 cmux.dmg | cut -d' ' -f1)
|
||||
if [ "$CASK_SHA" != "$ACTUAL_SHA" ]; then
|
||||
echo "SHA mismatch! Cask: $CASK_SHA, Actual: $ACTUAL_SHA" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "SHA verification passed: $CASK_SHA"
|
||||
|
||||
- name: Commit and push
|
||||
env:
|
||||
VERSION: ${{ steps.version.outputs.version }}
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
Subproject commit 72f1e256bed433e7f3c4d0040fd87083345e92a2
|
||||
Subproject commit 27fdfc514e21a581d363c297970c35e7b6f93b26
|
||||
|
|
@ -91,6 +91,54 @@ fi
|
|||
# --- Verify ---
|
||||
gh release view "$TAG"
|
||||
|
||||
# --- Update Homebrew cask (skip for nightlies) ---
|
||||
if [[ "$TAG" != *"-nightly"* ]]; then
|
||||
VERSION="${TAG#v}"
|
||||
DMG_SHA256=$(shasum -a 256 cmux-macos.dmg | cut -d' ' -f1)
|
||||
echo "Updating homebrew cask to $VERSION (SHA: $DMG_SHA256)..."
|
||||
CASK_FILE="homebrew-cmux/Casks/cmux.rb"
|
||||
if [ -f "$CASK_FILE" ]; then
|
||||
cat > "$CASK_FILE" << CASKEOF
|
||||
cask "cmux" do
|
||||
version "${VERSION}"
|
||||
sha256 "${DMG_SHA256}"
|
||||
|
||||
url "https://github.com/manaflow-ai/cmux/releases/download/v#{version}/cmux-macos.dmg"
|
||||
name "cmux"
|
||||
desc "Lightweight native macOS terminal with vertical tabs for AI coding agents"
|
||||
homepage "https://github.com/manaflow-ai/cmux"
|
||||
|
||||
livecheck do
|
||||
url :url
|
||||
strategy :github_latest
|
||||
end
|
||||
|
||||
depends_on macos: ">= :ventura"
|
||||
|
||||
app "cmux.app"
|
||||
|
||||
zap trash: [
|
||||
"~/Library/Application Support/cmux",
|
||||
"~/Library/Caches/cmux",
|
||||
"~/Library/Preferences/ai.manaflow.cmuxterm.plist",
|
||||
]
|
||||
end
|
||||
CASKEOF
|
||||
cd homebrew-cmux
|
||||
git add Casks/cmux.rb
|
||||
if git diff --staged --quiet; then
|
||||
echo "Homebrew cask already up to date"
|
||||
else
|
||||
git commit -m "Update cmux to ${VERSION}"
|
||||
git push
|
||||
echo "Homebrew cask updated"
|
||||
fi
|
||||
cd ..
|
||||
else
|
||||
echo "WARNING: homebrew-cmux submodule not found, skipping cask update"
|
||||
fi
|
||||
fi
|
||||
|
||||
# --- Cleanup ---
|
||||
rm -rf build/ cmux-macos.dmg appcast.xml
|
||||
echo ""
|
||||
|
|
|
|||
52
tests/test_homebrew_sha.sh
Executable file
52
tests/test_homebrew_sha.sh
Executable file
|
|
@ -0,0 +1,52 @@
|
|||
#!/bin/bash
|
||||
# Regression test: verify the homebrew cask SHA256 matches the actual release DMG.
|
||||
# Catches issues like https://github.com/manaflow-ai/cmux/issues/110 where a race
|
||||
# condition caused the cask to contain the SHA of a 404 page instead of the DMG.
|
||||
set -euo pipefail
|
||||
|
||||
CASK_FILE="$(dirname "$0")/../homebrew-cmux/Casks/cmux.rb"
|
||||
|
||||
if [ ! -f "$CASK_FILE" ]; then
|
||||
echo "SKIP: homebrew-cmux submodule not initialized"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
VERSION=$(grep 'version "' "$CASK_FILE" | head -1 | sed 's/.*"\(.*\)".*/\1/')
|
||||
CASK_SHA=$(grep 'sha256 "' "$CASK_FILE" | head -1 | sed 's/.*"\(.*\)".*/\1/')
|
||||
|
||||
if [ -z "$VERSION" ] || [ -z "$CASK_SHA" ]; then
|
||||
echo "FAIL: could not parse version/sha256 from $CASK_FILE"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Cask version: $VERSION"
|
||||
echo "Cask SHA256: $CASK_SHA"
|
||||
|
||||
URL="https://github.com/manaflow-ai/cmux/releases/download/v${VERSION}/cmux-macos.dmg"
|
||||
TMPFILE=$(mktemp)
|
||||
trap 'rm -f "$TMPFILE"' EXIT
|
||||
|
||||
HTTP_CODE=$(curl -sL -w '%{http_code}' "$URL" -o "$TMPFILE")
|
||||
FILE_SIZE=$(stat -f%z "$TMPFILE" 2>/dev/null || stat --printf="%s" "$TMPFILE")
|
||||
|
||||
if [ "$HTTP_CODE" != "200" ]; then
|
||||
echo "FAIL: download returned HTTP $HTTP_CODE (expected 200)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ "$FILE_SIZE" -lt 1000000 ]; then
|
||||
echo "FAIL: downloaded file is only $FILE_SIZE bytes (expected >1MB for a DMG)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
ACTUAL_SHA=$(shasum -a 256 "$TMPFILE" | cut -d' ' -f1)
|
||||
echo "Actual SHA256: $ACTUAL_SHA"
|
||||
|
||||
if [ "$CASK_SHA" != "$ACTUAL_SHA" ]; then
|
||||
echo "FAIL: SHA256 mismatch!"
|
||||
echo " Cask: $CASK_SHA"
|
||||
echo " Actual: $ACTUAL_SHA"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "PASS: homebrew cask SHA256 matches release DMG"
|
||||
Loading…
Add table
Add a link
Reference in a new issue