Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -144,3 +144,44 @@ jobs:
dist/Capsule-*.zip
dist/appcast.xml
generate_release_notes: true

# ── Keep the Homebrew tap current ─────────────────────────────────────────
- name: Determine Homebrew tap availability
id: tap
env:
TAP_TOKEN: ${{ secrets.HOMEBREW_TAP_TOKEN }}
run: |
# A PAT (Contents: read+write) scoped to capsule-native/homebrew-tap — the job's own
# GITHUB_TOKEN can only write to *this* repo. Absent on forks → skip cleanly, never fail.
if [ -n "$TAP_TOKEN" ]; then
echo "enabled=true" >> "$GITHUB_OUTPUT"
else
echo "enabled=false" >> "$GITHUB_OUTPUT"
echo "::warning::HOMEBREW_TAP_TOKEN absent — skipping the Homebrew cask bump."
fi

- name: Bump the Homebrew cask
if: steps.signing.outputs.enabled == 'true' && steps.tap.outputs.enabled == 'true' && startsWith(github.ref, 'refs/tags/')
env:
TAP_TOKEN: ${{ secrets.HOMEBREW_TAP_TOKEN }}
TAP_REPO: capsule-native/homebrew-tap
run: |
# Version comes from the app, not the tag (the tag guard already proved they agree).
version="$(/usr/libexec/PlistBuddy -c 'Print :CFBundleShortVersionString' App/Info.plist)"
dmg="dist/Capsule-${version}.dmg"
[ -f "$dmg" ] || { echo "::error::expected $dmg after 'make release'"; exit 1; }

work="$(mktemp -d)"
git clone --depth 1 "https://x-access-token:${TAP_TOKEN}@github.com/${TAP_REPO}.git" "$work"
Scripts/release/update-homebrew-cask.sh "$version" "$dmg" > "$work/Casks/capsule.rb"

cd "$work"
if git diff --quiet -- Casks/capsule.rb; then
echo "Homebrew cask already at ${version} — nothing to bump."
exit 0
fi
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git commit -aqm "Update capsule cask to ${version}"
git push origin HEAD:main
echo "Bumped the Homebrew cask → ${version}."
7 changes: 7 additions & 0 deletions Scripts/release/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,13 @@ unsandboxed to drive the `container` CLI).
**<https://capsule-native.github.io/appcast.xml>** — the app's `SUFeedURL`. It runs on a
schedule (~15 min) and on demand (Actions ▸ Sync appcast ▸ Run workflow) for an instant
publish right after a release.
4. The release job also **bumps the Homebrew cask**: it recomputes the DMG's SHA-256, renders
`Casks/capsule.rb` via [`update-homebrew-cask.sh`](update-homebrew-cask.sh), and pushes it to
[`capsule-native/homebrew-tap`](https://github.com/capsule-native/homebrew-tap) so that
`brew install --cask capsule-native/tap/capsule` serves the new version. Gated on the
`HOMEBREW_TAP_TOKEN` secret (a fine-grained PAT with **Contents: read+write** on the tap);
absent on forks, it's skipped with a warning. `auto_updates true` means *existing* installs are
updated in place by Sparkle regardless — the bump keeps **fresh** installs current.

> The code repo stays **public** so Sparkle can download the update zip from the release assets
> without authentication.
Expand Down
57 changes: 57 additions & 0 deletions Scripts/release/update-homebrew-cask.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
#!/usr/bin/env bash
#
# update-homebrew-cask.sh
# Capsule
#
# Copyright © 2026 Capsule. All rights reserved.
#
# Renders the Homebrew cask for a released build to stdout, pinning the SHA-256 of the release
# DMG. Pure and network-free: the CI release job redirects this into a checkout of
# capsule-native/homebrew-tap and pushes the result (see .github/workflows/release.yml). Keeping
# the template here makes the main repo the single source of truth for the cask.
#
# Usage: update-homebrew-cask.sh <version> <dmg-path>
# e.g. update-homebrew-cask.sh 0.1.0 dist/Capsule-0.1.0.dmg

set -euo pipefail

version="${1:?usage: update-homebrew-cask.sh <version> <dmg-path>}"
dmg="${2:?usage: update-homebrew-cask.sh <version> <dmg-path>}"

[ -f "$dmg" ] || { echo "update-homebrew-cask.sh: missing DMG: $dmg" >&2; exit 1; }

sha256="$(shasum -a 256 "$dmg" | awk '{print $1}')"

# The `#{version}` tokens below are Ruby interpolation for Homebrew and must survive verbatim;
# only ${version}/${sha256} are expanded by the shell here.
cat <<EOF
cask "capsule" do
version "${version}"
sha256 "${sha256}"

url "https://github.com/capsule-native/capsule/releases/download/v#{version}/Capsule-#{version}.dmg",
verified: "github.com/capsule-native/capsule/"
name "Capsule"
desc "GUI for Apple's container CLI"
homepage "https://capsule-native.github.io/"

livecheck do
url :url
strategy :github_latest
end

auto_updates true
depends_on macos: :tahoe
depends_on arch: :arm64

app "Capsule.app"

zap trash: [
"~/Library/Application Support/com.capsule.app",
"~/Library/Caches/com.capsule.app",
"~/Library/HTTPStorages/com.capsule.app",
"~/Library/Preferences/com.capsule.app.plist",
"~/Library/Saved Application State/com.capsule.app.savedState",
]
end
EOF
Loading