design(bloatmac): horizontal logo + wordmark lockup variants #10
Workflow file for this run
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
| 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 |