Skip to content

design(bloatmac): horizontal logo + wordmark lockup variants #10

design(bloatmac): horizontal logo + wordmark lockup variants

design(bloatmac): horizontal logo + wordmark lockup variants #10

name: Release BloatMac
on:
push:
tags:
- "bloatmac-v*"
permissions:
contents: write
jobs:
build:
name: Build & ship BloatMac.app
runs-on: macos-26
# Hard cap so a stalled notarytool upload never wastes the default 6 h.
# Build + .app notarize is ~3 min on the macos-26 image; .dmg notarize
# adds another minute or two — 30 leaves plenty of headroom.
timeout-minutes: 30
env:
TAG_NAME: ${{ github.ref_name }}
DERIVED_DATA: ${{ github.workspace }}/derived
APPLE_TEAM_ID: NCLSWS8Y8K
DEVELOPER_ID_APPLICATION: "Developer ID Application: Akhil Gautam (NCLSWS8Y8K)"
defaults:
run:
working-directory: bloatmac
steps:
- uses: actions/checkout@v4
- name: Select Xcode
run: |
# BloatMac targets the macOS 26 SDK (Foundation Models). The macos-26
# runner image ships Xcode 26 by default; this just makes the choice
# explicit and surfaces the version in logs.
set +e
for path in \
/Applications/Xcode_26.app \
/Applications/Xcode.app
do
if [ -d "$path" ]; then
sudo xcode-select -s "$path/Contents/Developer"
break
fi
done
xcodebuild -version
- name: Strip BloatMac version from tag
id: ver
run: |
# bloatmac-v0.2.0 → 0.2.0
VERSION="${TAG_NAME#bloatmac-v}"
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
- name: Detect signing configuration
id: signing
working-directory: ${{ github.workspace }}
env:
P12: ${{ secrets.APPLE_CERT_P12_BASE64 }}
NOTARY_KEY: ${{ secrets.NOTARY_API_KEY_P8_BASE64 }}
run: |
if [ -n "$P12" ] && [ -n "$NOTARY_KEY" ]; then
echo "configured=true" >> "$GITHUB_OUTPUT"
echo "Signing + notarization configured — will produce a Developer-ID-signed, notarized build."
else
echo "configured=false" >> "$GITHUB_OUTPUT"
echo "::warning::APPLE_CERT_P12_BASE64 / NOTARY_API_KEY_P8_BASE64 secrets missing — falling back to ad-hoc signing."
fi
- name: Import Developer ID certificate
if: steps.signing.outputs.configured == 'true'
env:
P12_BASE64: ${{ secrets.APPLE_CERT_P12_BASE64 }}
P12_PASSWORD: ${{ secrets.APPLE_CERT_P12_PASSWORD }}
run: |
set -euo pipefail
KEYCHAIN="$RUNNER_TEMP/build.keychain"
KEYCHAIN_PASSWORD="$(uuidgen)"
# Decode cert without leaking secret to logs.
CERT_PATH="$RUNNER_TEMP/cert.p12"
printf '%s' "$P12_BASE64" | base64 --decode > "$CERT_PATH"
security create-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN"
security set-keychain-settings -lut 21600 "$KEYCHAIN"
security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN"
# Prepend our keychain to the search list so xcodebuild finds the identity.
security list-keychains -d user -s "$KEYCHAIN" $(security list-keychains -d user | tr -d '"')
security default-keychain -s "$KEYCHAIN"
security import "$CERT_PATH" \
-k "$KEYCHAIN" \
-P "$P12_PASSWORD" \
-T /usr/bin/codesign \
-T /usr/bin/security
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$KEYCHAIN_PASSWORD" "$KEYCHAIN" >/dev/null
rm -f "$CERT_PATH"
# Confirm the identity is visible to codesign.
security find-identity -v -p codesigning "$KEYCHAIN"
# ─── Build (signed path) ───────────────────────────────────────────────
- name: Build — Developer ID signed
if: steps.signing.outputs.configured == 'true'
run: |
xcodebuild \
-project bloatmac.xcodeproj \
-scheme bloatmac \
-configuration Release \
-destination "generic/platform=macOS" \
-derivedDataPath "$DERIVED_DATA" \
ONLY_ACTIVE_ARCH=NO \
ARCHS="arm64 x86_64" \
CODE_SIGN_STYLE=Manual \
CODE_SIGN_IDENTITY="$DEVELOPER_ID_APPLICATION" \
CODE_SIGNING_REQUIRED=YES \
CODE_SIGNING_ALLOWED=YES \
CODE_SIGN_INJECT_BASE_ENTITLEMENTS=NO \
DEVELOPMENT_TEAM="$APPLE_TEAM_ID" \
PROVISIONING_PROFILE_SPECIFIER= \
OTHER_CODE_SIGN_FLAGS="--timestamp --options=runtime" \
MARKETING_VERSION="${{ steps.ver.outputs.version }}" \
CURRENT_PROJECT_VERSION="${{ steps.ver.outputs.version }}" \
build
# ─── Build (ad-hoc fallback) ───────────────────────────────────────────
- name: Build — ad-hoc fallback
if: steps.signing.outputs.configured == 'false'
run: |
xcodebuild \
-project bloatmac.xcodeproj \
-scheme bloatmac \
-configuration Release \
-destination "generic/platform=macOS" \
-derivedDataPath "$DERIVED_DATA" \
ONLY_ACTIVE_ARCH=NO \
ARCHS="arm64 x86_64" \
CODE_SIGN_STYLE=Manual \
CODE_SIGN_IDENTITY="-" \
CODE_SIGNING_REQUIRED=NO \
CODE_SIGNING_ALLOWED=NO \
DEVELOPMENT_TEAM= \
PROVISIONING_PROFILE_SPECIFIER= \
MARKETING_VERSION="${{ steps.ver.outputs.version }}" \
CURRENT_PROJECT_VERSION="${{ steps.ver.outputs.version }}" \
build
- name: Locate built app
id: appPath
run: |
APP="$(find "$DERIVED_DATA/Build/Products/Release" -maxdepth 1 -name 'bloatmac.app' -type d -print -quit)"
if [ -z "$APP" ]; then
echo "::error::Could not find bloatmac.app in $DERIVED_DATA"
exit 1
fi
# Rename to BloatMac.app for nicer Finder presentation
STAGED="${RUNNER_TEMP}/BloatMac.app"
rm -rf "$STAGED"
cp -R "$APP" "$STAGED"
echo "app=$STAGED" >> "$GITHUB_OUTPUT"
# ─── Ad-hoc only post-processing ───────────────────────────────────────
- name: Strip xattrs & re-sign ad-hoc
if: steps.signing.outputs.configured == 'false'
run: |
xattr -cr "${{ steps.appPath.outputs.app }}"
codesign --force --deep --sign - "${{ steps.appPath.outputs.app }}"
codesign --verify --deep --strict --verbose=2 "${{ steps.appPath.outputs.app }}" || true
# ─── Verify Developer ID signature ─────────────────────────────────────
- name: Verify Developer ID signature
if: steps.signing.outputs.configured == 'true'
run: |
codesign --verify --deep --strict --verbose=2 "${{ steps.appPath.outputs.app }}"
codesign --display --verbose=4 "${{ steps.appPath.outputs.app }}" 2>&1 | grep -E "Authority|TeamIdentifier|Identifier|Signed Time" || true
# Fail loudly if the runtime hardening flag wasn't applied — Apple
# rejects notarization without it.
if ! codesign --display --verbose=2 "${{ steps.appPath.outputs.app }}" 2>&1 | grep -q "flags=.*runtime"; then
echo "::error::App was signed without the hardened runtime — notarization will fail."
exit 1
fi
- name: Package zip
run: |
cd "$RUNNER_TEMP"
ZIP="BloatMac-${TAG_NAME#bloatmac-}-macos.zip"
ditto -c -k --keepParent BloatMac.app "$ZIP"
mv "$ZIP" "$GITHUB_WORKSPACE/$ZIP"
echo "ZIP=$ZIP" >> "$GITHUB_ENV"
# ─── Notarize (signed path only) ───────────────────────────────────────
- name: Notarize via notarytool
if: steps.signing.outputs.configured == 'true'
env:
NOTARY_API_KEY_P8_BASE64: ${{ secrets.NOTARY_API_KEY_P8_BASE64 }}
NOTARY_API_KEY_ID: ${{ secrets.NOTARY_API_KEY_ID }}
NOTARY_API_ISSUER_ID: ${{ secrets.NOTARY_API_ISSUER_ID }}
run: |
set -euo pipefail
KEY_PATH="$RUNNER_TEMP/notary_key.p8"
umask 077
printf '%s' "$NOTARY_API_KEY_P8_BASE64" | base64 --decode > "$KEY_PATH"
# Submit, capturing the submission id so we can fetch the log
# whether or not the submission succeeds.
SUBMIT_OUT="$RUNNER_TEMP/notary_submit.txt"
set +e
xcrun notarytool submit "$GITHUB_WORKSPACE/$ZIP" \
--key "$KEY_PATH" \
--key-id "$NOTARY_API_KEY_ID" \
--issuer "$NOTARY_API_ISSUER_ID" \
--wait \
--timeout 20m \
--output-format json | tee "$SUBMIT_OUT"
NOTARY_RC=$?
set -e
SUBMIT_ID="$(python3 -c "import json,sys;print(json.load(open('$SUBMIT_OUT'))['id'])" 2>/dev/null || echo '')"
STATUS="$(python3 -c "import json,sys;print(json.load(open('$SUBMIT_OUT'))['status'])" 2>/dev/null || echo '')"
echo "Submission id: $SUBMIT_ID"
echo "Status: $STATUS"
# Always fetch the developer log — even on Accepted, it sometimes
# surfaces warnings worth reading.
if [ -n "$SUBMIT_ID" ]; then
echo "─── notarytool log ───"
xcrun notarytool log "$SUBMIT_ID" \
--key "$KEY_PATH" \
--key-id "$NOTARY_API_KEY_ID" \
--issuer "$NOTARY_API_ISSUER_ID" || true
echo "─── end notarytool log ───"
fi
rm -f "$KEY_PATH"
if [ "$STATUS" != "Accepted" ]; then
echo "::error::Notarization status was '$STATUS' (rc=$NOTARY_RC) — see log above"
exit 1
fi
- name: Staple ticket
if: steps.signing.outputs.configured == 'true'
run: |
# `notarytool submit --wait` returns when Apple has accepted the
# submission, but the ticket store (CloudKit-backed) sometimes
# takes a minute longer to propagate the ticket to the staple
# endpoint. Retry with backoff before giving up.
set -e
APP="${{ steps.appPath.outputs.app }}"
for attempt in 1 2 3 4 5 6; do
if xcrun stapler staple "$APP"; then
echo "Stapled on attempt $attempt"
break
fi
if [ "$attempt" = "6" ]; then
echo "::error::stapler still failing after 6 attempts — Apple edge hasn't propagated the ticket"
exit 1
fi
echo "Stapler edge not ready (attempt $attempt) — sleeping 30s"
sleep 30
done
xcrun stapler validate "$APP"
# Re-zip to embed the stapled ticket in the artifact users download.
rm -f "$GITHUB_WORKSPACE/$ZIP"
cd "$RUNNER_TEMP"
ditto -c -k --keepParent BloatMac.app "$ZIP"
mv "$ZIP" "$GITHUB_WORKSPACE/$ZIP"
- name: Package dmg
run: |
cd "$RUNNER_TEMP"
DMG="BloatMac-${TAG_NAME#bloatmac-}-macos.dmg"
STAGING="$RUNNER_TEMP/dmg-stage"
rm -rf "$STAGING"; mkdir "$STAGING"
cp -R BloatMac.app "$STAGING/"
ln -s /Applications "$STAGING/Applications"
hdiutil create -volname "BloatMac" -srcfolder "$STAGING" -ov -format UDZO "$DMG"
mv "$DMG" "$GITHUB_WORKSPACE/$DMG"
echo "DMG=$DMG" >> "$GITHUB_ENV"
# Notarize + staple the dmg as a nice-to-have — Gatekeeper validates
# the .app inside on open, so the cask install path works either way.
# Capped at 10 min so a stalled Apple upload doesn't fail the release.
- name: Notarize & staple dmg
if: steps.signing.outputs.configured == 'true'
timeout-minutes: 10
continue-on-error: true
env:
NOTARY_API_KEY_P8_BASE64: ${{ secrets.NOTARY_API_KEY_P8_BASE64 }}
NOTARY_API_KEY_ID: ${{ secrets.NOTARY_API_KEY_ID }}
NOTARY_API_ISSUER_ID: ${{ secrets.NOTARY_API_ISSUER_ID }}
run: |
set -euo pipefail
KEY_PATH="$RUNNER_TEMP/notary_key.p8"
umask 077
printf '%s' "$NOTARY_API_KEY_P8_BASE64" | base64 --decode > "$KEY_PATH"
xcrun notarytool submit "$GITHUB_WORKSPACE/$DMG" \
--key "$KEY_PATH" \
--key-id "$NOTARY_API_KEY_ID" \
--issuer "$NOTARY_API_ISSUER_ID" \
--wait \
--timeout 6m
rm -f "$KEY_PATH"
for attempt in 1 2 3 4 5 6; do
if xcrun stapler staple "$GITHUB_WORKSPACE/$DMG"; then
echo "DMG stapled on attempt $attempt"
break
fi
if [ "$attempt" = "6" ]; then
echo "::error::DMG stapler still failing after 6 attempts"
exit 1
fi
echo "DMG stapler edge not ready (attempt $attempt) — sleeping 30s"
sleep 30
done
xcrun stapler validate "$GITHUB_WORKSPACE/$DMG"
- name: Compute SHA256 checksums
working-directory: ${{ github.workspace }}
run: |
shasum -a 256 "$ZIP" "$DMG" > "BloatMac-${TAG_NAME#bloatmac-}-checksums.txt"
cat "BloatMac-${TAG_NAME#bloatmac-}-checksums.txt"
- name: Compose release notes
id: notes
env:
SIGNED: ${{ steps.signing.outputs.configured }}
run: |
if [ "$SIGNED" = "true" ]; then
cat > "$RUNNER_TEMP/notes.md" <<'EOF'
## BloatMac ${{ steps.ver.outputs.version }}
Native SwiftUI macOS companion app for the `bloat` CLI.
### Install
**Homebrew (recommended)**
```sh
brew install --cask akhil-gautam/tap/bloatmac
```
**Manual download**
Grab the `.dmg` below, open it, and drag BloatMac.app into Applications.
### Notes
Signed with Developer ID Application and notarized by Apple — first launch should be unblocked.
EOF
else
cat > "$RUNNER_TEMP/notes.md" <<'EOF'
## BloatMac ${{ steps.ver.outputs.version }}
Native SwiftUI macOS companion app for the `bloat` CLI.
### Install
**Homebrew (recommended)**
```sh
brew install --cask akhil-gautam/tap/bloatmac
```
**Manual download**
Grab the `.dmg` below, open it, and drag BloatMac.app into Applications.
### Notes
The app is **ad-hoc signed**, not Developer-ID-signed. On first launch macOS may
warn about an unidentified developer — right-click the app → Open, or run
`xattr -dr com.apple.quarantine /Applications/BloatMac.app` once.
EOF
fi
echo "path=$RUNNER_TEMP/notes.md" >> "$GITHUB_OUTPUT"
- name: Create GitHub Release
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ env.TAG_NAME }}
name: BloatMac ${{ steps.ver.outputs.version }}
generate_release_notes: true
body_path: ${{ steps.notes.outputs.path }}
files: |
${{ github.workspace }}/BloatMac-*-macos.zip
${{ github.workspace }}/BloatMac-*-macos.dmg
${{ github.workspace }}/BloatMac-*-checksums.txt