This commit is contained in:
@@ -0,0 +1,32 @@
|
||||
name: Build
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: macos-15
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Install XcodeGen
|
||||
run: brew install xcodegen
|
||||
|
||||
- name: Generate Xcode project
|
||||
run: xcodegen generate
|
||||
|
||||
- name: Build (Universal Binary)
|
||||
run: |
|
||||
xcodebuild -project PureMac.xcodeproj \
|
||||
-scheme PureMac \
|
||||
-configuration Release \
|
||||
-derivedDataPath build \
|
||||
build \
|
||||
ARCHS="arm64 x86_64" \
|
||||
ONLY_ACTIVE_ARCH=NO \
|
||||
CODE_SIGN_IDENTITY="" \
|
||||
CODE_SIGNING_REQUIRED=NO \
|
||||
CODE_SIGNING_ALLOWED=NO
|
||||
@@ -0,0 +1,333 @@
|
||||
name: Release
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*.*.*'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: 'Version to release (e.g. 2.2.0). Must match project.yml MARKETING_VERSION.'
|
||||
required: true
|
||||
type: string
|
||||
dry_run:
|
||||
description: 'Build + sign + notarize but skip release upload + homebrew bump.'
|
||||
required: false
|
||||
default: false
|
||||
type: boolean
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
env:
|
||||
APP_NAME: PureMac
|
||||
BUNDLE_ID: com.puremac.app
|
||||
TEAM_ID: H3WXHVTP97
|
||||
SCHEME: PureMac
|
||||
PROJECT: PureMac.xcodeproj
|
||||
CONFIGURATION: Release
|
||||
|
||||
jobs:
|
||||
release:
|
||||
name: Build, sign, notarize, release
|
||||
runs-on: macos-15
|
||||
timeout-minutes: 60
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Resolve version
|
||||
id: ver
|
||||
run: |
|
||||
if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then
|
||||
VERSION="${{ inputs.version }}"
|
||||
else
|
||||
VERSION="${GITHUB_REF_NAME#v}"
|
||||
fi
|
||||
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
|
||||
echo "tag=v${VERSION}" >> "$GITHUB_OUTPUT"
|
||||
echo "Releasing version: ${VERSION}"
|
||||
|
||||
- name: Verify version matches project.yml
|
||||
run: |
|
||||
PROJ_VERSION=$(grep -E '^\s*MARKETING_VERSION:' project.yml | sed -E 's/.*"([^"]+)".*/\1/')
|
||||
if [[ "${PROJ_VERSION}" != "${{ steps.ver.outputs.version }}" ]]; then
|
||||
echo "::error::project.yml MARKETING_VERSION (${PROJ_VERSION}) != release version (${{ steps.ver.outputs.version }}). Bump project.yml first."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Select Xcode
|
||||
run: sudo xcode-select -s /Applications/Xcode_16.4.app
|
||||
|
||||
- name: Install xcodegen + create-dmg
|
||||
run: |
|
||||
brew install xcodegen create-dmg
|
||||
|
||||
- name: Generate Xcode project
|
||||
run: xcodegen generate
|
||||
|
||||
- name: Import Developer ID signing certificate
|
||||
env:
|
||||
BUILD_CERTIFICATE_BASE64: ${{ secrets.BUILD_CERTIFICATE_BASE64 }}
|
||||
P12_PASSWORD: ${{ secrets.P12_PASSWORD }}
|
||||
KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
CERT_PATH="${RUNNER_TEMP}/certificate.p12"
|
||||
KEYCHAIN_PATH="${RUNNER_TEMP}/app-signing.keychain-db"
|
||||
echo -n "${BUILD_CERTIFICATE_BASE64}" | base64 --decode -o "${CERT_PATH}"
|
||||
security create-keychain -p "${KEYCHAIN_PASSWORD}" "${KEYCHAIN_PATH}"
|
||||
security set-keychain-settings -lut 21600 "${KEYCHAIN_PATH}"
|
||||
security unlock-keychain -p "${KEYCHAIN_PASSWORD}" "${KEYCHAIN_PATH}"
|
||||
security import "${CERT_PATH}" -P "${P12_PASSWORD}" -A -t cert -f pkcs12 -k "${KEYCHAIN_PATH}"
|
||||
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "${KEYCHAIN_PASSWORD}" "${KEYCHAIN_PATH}" >/dev/null
|
||||
security list-keychain -d user -s "${KEYCHAIN_PATH}" $(security list-keychain -d user | xargs)
|
||||
# Verify identity loaded
|
||||
security find-identity -v -p codesigning "${KEYCHAIN_PATH}" | grep "Developer ID Application: Moamen Basel"
|
||||
|
||||
- name: Archive (universal arm64 + x86_64)
|
||||
run: |
|
||||
set -euo pipefail
|
||||
xcodebuild \
|
||||
-project "${PROJECT}" \
|
||||
-scheme "${SCHEME}" \
|
||||
-configuration "${CONFIGURATION}" \
|
||||
-destination 'generic/platform=macOS' \
|
||||
-archivePath build/PureMac.xcarchive \
|
||||
ARCHS="arm64 x86_64" \
|
||||
ONLY_ACTIVE_ARCH=NO \
|
||||
CODE_SIGN_STYLE=Manual \
|
||||
CODE_SIGN_IDENTITY="Developer ID Application: Moamen Basel (H3WXHVTP97)" \
|
||||
DEVELOPMENT_TEAM="${TEAM_ID}" \
|
||||
OTHER_CODE_SIGN_FLAGS="--timestamp --options=runtime" \
|
||||
archive | xcbeautify
|
||||
|
||||
- name: Export signed app
|
||||
run: |
|
||||
set -euo pipefail
|
||||
cat > build/ExportOptions.plist <<'PLIST'
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>method</key><string>developer-id</string>
|
||||
<key>teamID</key><string>H3WXHVTP97</string>
|
||||
<key>signingStyle</key><string>manual</string>
|
||||
<key>signingCertificate</key><string>Developer ID Application</string>
|
||||
</dict>
|
||||
</plist>
|
||||
PLIST
|
||||
xcodebuild -exportArchive \
|
||||
-archivePath build/PureMac.xcarchive \
|
||||
-exportPath build/export \
|
||||
-exportOptionsPlist build/ExportOptions.plist | xcbeautify
|
||||
|
||||
- name: Verify codesign + hardened runtime
|
||||
run: |
|
||||
set -euo pipefail
|
||||
APP="build/export/PureMac.app"
|
||||
codesign --verify --deep --strict --verbose=2 "${APP}"
|
||||
codesign -dvv "${APP}" 2>&1 | tee /tmp/cs-info.txt
|
||||
grep -q "flags=0x10000(runtime)" /tmp/cs-info.txt || { echo "::error::Hardened runtime missing"; exit 1; }
|
||||
spctl --assess --type execute --verbose=4 "${APP}" || true
|
||||
# Universal arch check
|
||||
lipo -archs "${APP}/Contents/MacOS/PureMac" | grep -q "x86_64" && lipo -archs "${APP}/Contents/MacOS/PureMac" | grep -q "arm64"
|
||||
|
||||
- name: Build DMG
|
||||
env:
|
||||
VERSION: ${{ steps.ver.outputs.version }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
DMG="build/PureMac-${VERSION}.dmg"
|
||||
create-dmg \
|
||||
--volname "PureMac ${VERSION}" \
|
||||
--window-size 540 360 \
|
||||
--icon-size 100 \
|
||||
--icon "PureMac.app" 140 180 \
|
||||
--hide-extension "PureMac.app" \
|
||||
--app-drop-link 400 180 \
|
||||
--no-internet-enable \
|
||||
"${DMG}" \
|
||||
build/export/PureMac.app
|
||||
codesign --sign "Developer ID Application: Moamen Basel (H3WXHVTP97)" --timestamp "${DMG}"
|
||||
codesign --verify --verbose=2 "${DMG}"
|
||||
|
||||
- name: Write App Store Connect API key
|
||||
env:
|
||||
APP_STORE_CONNECT_PRIVATE_KEY: ${{ secrets.APP_STORE_CONNECT_PRIVATE_KEY }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
KEY_PATH="${RUNNER_TEMP}/asc_api_key.p8"
|
||||
printf '%s' "${APP_STORE_CONNECT_PRIVATE_KEY}" > "${KEY_PATH}"
|
||||
chmod 600 "${KEY_PATH}"
|
||||
echo "ASC_KEY_PATH=${KEY_PATH}" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Notarize DMG
|
||||
env:
|
||||
ASC_KEY_ID: ${{ secrets.APP_STORE_CONNECT_KEY_ID }}
|
||||
ASC_ISSUER_ID: ${{ secrets.APP_STORE_CONNECT_ISSUER_ID }}
|
||||
VERSION: ${{ steps.ver.outputs.version }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
DMG="build/PureMac-${VERSION}.dmg"
|
||||
xcrun notarytool submit "${DMG}" \
|
||||
--key "${ASC_KEY_PATH}" \
|
||||
--key-id "${ASC_KEY_ID}" \
|
||||
--issuer "${ASC_ISSUER_ID}" \
|
||||
--wait \
|
||||
--timeout 30m
|
||||
xcrun stapler staple "${DMG}"
|
||||
xcrun stapler validate "${DMG}"
|
||||
spctl --assess --type install --verbose=4 "${DMG}"
|
||||
|
||||
- name: Build notarized zip
|
||||
env:
|
||||
ASC_KEY_ID: ${{ secrets.APP_STORE_CONNECT_KEY_ID }}
|
||||
ASC_ISSUER_ID: ${{ secrets.APP_STORE_CONNECT_ISSUER_ID }}
|
||||
VERSION: ${{ steps.ver.outputs.version }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
ZIP="build/PureMac-${VERSION}.zip"
|
||||
# Notarize the .app inside a zip too (homebrew cask uses zip)
|
||||
ditto -c -k --keepParent --sequesterRsrc build/export/PureMac.app build/PureMac-app.zip
|
||||
xcrun notarytool submit build/PureMac-app.zip \
|
||||
--key "${ASC_KEY_PATH}" \
|
||||
--key-id "${ASC_KEY_ID}" \
|
||||
--issuer "${ASC_ISSUER_ID}" \
|
||||
--wait --timeout 30m
|
||||
xcrun stapler staple build/export/PureMac.app
|
||||
# Final shippable zip with stapled ticket
|
||||
ditto -c -k --keepParent --sequesterRsrc build/export/PureMac.app "${ZIP}"
|
||||
|
||||
- name: Compute SHA256 checksums
|
||||
id: sha
|
||||
env:
|
||||
VERSION: ${{ steps.ver.outputs.version }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
DMG_SHA=$(shasum -a 256 "build/PureMac-${VERSION}.dmg" | awk '{print $1}')
|
||||
ZIP_SHA=$(shasum -a 256 "build/PureMac-${VERSION}.zip" | awk '{print $1}')
|
||||
echo "dmg=${DMG_SHA}" >> "$GITHUB_OUTPUT"
|
||||
echo "zip=${ZIP_SHA}" >> "$GITHUB_OUTPUT"
|
||||
{
|
||||
echo "## Checksums"
|
||||
echo ""
|
||||
echo "| Asset | SHA-256 |"
|
||||
echo "|-------|---------|"
|
||||
echo "| PureMac-${VERSION}.dmg | \`${DMG_SHA}\` |"
|
||||
echo "| PureMac-${VERSION}.zip | \`${ZIP_SHA}\` |"
|
||||
} > build/CHECKSUMS.md
|
||||
|
||||
- name: Upload artifacts (always, for inspection)
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: puremac-${{ steps.ver.outputs.version }}
|
||||
path: |
|
||||
build/PureMac-${{ steps.ver.outputs.version }}.dmg
|
||||
build/PureMac-${{ steps.ver.outputs.version }}.zip
|
||||
build/CHECKSUMS.md
|
||||
retention-days: 14
|
||||
|
||||
- name: Create or update GitHub release
|
||||
if: ${{ github.event_name == 'push' || inputs.dry_run == false }}
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
VERSION: ${{ steps.ver.outputs.version }}
|
||||
TAG: ${{ steps.ver.outputs.tag }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
NOTES_FILE=build/release-notes.md
|
||||
{
|
||||
echo "## PureMac ${VERSION}"
|
||||
echo ""
|
||||
echo "Universal binary (Intel + Apple Silicon). Signed with Developer ID + notarized + stapled by Apple."
|
||||
echo ""
|
||||
cat build/CHECKSUMS.md
|
||||
echo ""
|
||||
echo "### Install"
|
||||
echo ""
|
||||
echo "\`\`\`"
|
||||
echo "brew tap momenbasel/tap"
|
||||
echo "brew install --cask puremac"
|
||||
echo "\`\`\`"
|
||||
} > "${NOTES_FILE}"
|
||||
|
||||
if gh release view "${TAG}" --repo "${GITHUB_REPOSITORY}" >/dev/null 2>&1; then
|
||||
gh release upload "${TAG}" \
|
||||
"build/PureMac-${VERSION}.dmg" \
|
||||
"build/PureMac-${VERSION}.zip" \
|
||||
--repo "${GITHUB_REPOSITORY}" --clobber
|
||||
else
|
||||
gh release create "${TAG}" \
|
||||
"build/PureMac-${VERSION}.dmg" \
|
||||
"build/PureMac-${VERSION}.zip" \
|
||||
--title "PureMac ${TAG}" \
|
||||
--notes-file "${NOTES_FILE}" \
|
||||
--repo "${GITHUB_REPOSITORY}"
|
||||
fi
|
||||
|
||||
- name: Bump in-repo homebrew formula
|
||||
if: ${{ github.event_name == 'push' || inputs.dry_run == false }}
|
||||
env:
|
||||
VERSION: ${{ steps.ver.outputs.version }}
|
||||
ZIP_SHA: ${{ steps.sha.outputs.zip }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
FORMULA=homebrew/puremac.rb
|
||||
/usr/bin/sed -i '' -E "s/^ version \".*\"/ version \"${VERSION}\"/" "${FORMULA}"
|
||||
/usr/bin/sed -i '' -E "s/^ sha256 \".*\"/ sha256 \"${ZIP_SHA}\"/" "${FORMULA}"
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||
git add "${FORMULA}"
|
||||
if git diff --cached --quiet; then
|
||||
echo "No formula changes to commit."
|
||||
else
|
||||
git commit -m "homebrew: bump to ${VERSION} (sha256 ${ZIP_SHA:0:12})"
|
||||
git push origin HEAD:main
|
||||
fi
|
||||
|
||||
- name: Bump tap formula (momenbasel/homebrew-tap)
|
||||
# `env.HOMEBREW_TAP_TOKEN != ''` evaluated empty because env was
|
||||
# step-scoped — referencing the secret directly works in step `if`
|
||||
# per GitHub Actions docs. Also emits a loud warning when the secret
|
||||
# is absent so future runs don't silently skip again.
|
||||
if: ${{ github.event_name == 'push' || inputs.dry_run == false }}
|
||||
env:
|
||||
HOMEBREW_TAP_TOKEN: ${{ secrets.HOMEBREW_TAP_TOKEN }}
|
||||
VERSION: ${{ steps.ver.outputs.version }}
|
||||
ZIP_SHA: ${{ steps.sha.outputs.zip }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if [[ -z "${HOMEBREW_TAP_TOKEN:-}" ]]; then
|
||||
echo "::warning title=Tap bump skipped::HOMEBREW_TAP_TOKEN secret is not set. Tap will stay at its previous version. Set the secret to enable auto-bumps."
|
||||
exit 0
|
||||
fi
|
||||
TAP_DIR="${RUNNER_TEMP}/homebrew-tap"
|
||||
git clone "https://x-access-token:${HOMEBREW_TAP_TOKEN}@github.com/momenbasel/homebrew-tap.git" "${TAP_DIR}"
|
||||
cd "${TAP_DIR}"
|
||||
# Cask path may be Casks/puremac.rb or puremac.rb depending on tap layout
|
||||
FORMULA=$(find . -name 'puremac.rb' -not -path './.git/*' | head -1)
|
||||
if [[ -z "${FORMULA}" ]]; then
|
||||
echo "::error::puremac.rb not found in tap"
|
||||
exit 1
|
||||
fi
|
||||
/usr/bin/sed -i '' -E "s/^ version \".*\"/ version \"${VERSION}\"/" "${FORMULA}"
|
||||
/usr/bin/sed -i '' -E "s/^ sha256 \".*\"/ sha256 \"${ZIP_SHA}\"/" "${FORMULA}"
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||
git add "${FORMULA}"
|
||||
if git diff --cached --quiet; then
|
||||
echo "No tap formula changes."
|
||||
else
|
||||
git commit -m "puremac: bump to ${VERSION} (sha256 ${ZIP_SHA:0:12})"
|
||||
git push origin HEAD
|
||||
fi
|
||||
|
||||
- name: Cleanup keychain + ASC key
|
||||
if: always()
|
||||
run: |
|
||||
security delete-keychain "${RUNNER_TEMP}/app-signing.keychain-db" || true
|
||||
rm -f "${RUNNER_TEMP}/asc_api_key.p8" || true
|
||||
Reference in New Issue
Block a user