Files
wehub-resource-sync 869b84f27c
Build / build (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:29:30 +08:00

334 lines
13 KiB
YAML

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