587 lines
22 KiB
YAML
587 lines
22 KiB
YAML
name: Release
|
|
|
|
on:
|
|
push:
|
|
tags:
|
|
- "v*.*.*"
|
|
- "v*.*.*-*"
|
|
workflow_dispatch:
|
|
inputs:
|
|
tag:
|
|
description: "Tag to release, for example v0.9.2 or v0.9.2-beta.1"
|
|
required: true
|
|
type: string
|
|
|
|
permissions:
|
|
contents: write
|
|
|
|
concurrency:
|
|
group: release-${{ github.event.inputs.tag || github.ref_name }}
|
|
cancel-in-progress: false
|
|
|
|
env:
|
|
PROJECT_NAME: MacTools
|
|
SCHEME: MacTools
|
|
DERIVED_DATA: build/DerivedData
|
|
ARTIFACT_DIR: build/release
|
|
|
|
jobs:
|
|
release:
|
|
name: Build, sign, notarize, and release
|
|
runs-on: macos-26
|
|
timeout-minutes: 60
|
|
|
|
steps:
|
|
- name: Checkout
|
|
uses: actions/checkout@v5
|
|
with:
|
|
ref: ${{ github.event.inputs.tag || github.ref }}
|
|
fetch-depth: 0
|
|
|
|
- name: Set release metadata
|
|
run: |
|
|
TAG="${{ github.event.inputs.tag || github.ref_name }}"
|
|
if [[ ! "$TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+([.-][0-9A-Za-z.-]+)?$ ]]; then
|
|
echo "Release tag must look like v1.2.3 or v1.2.3-beta.1" >&2
|
|
exit 1
|
|
fi
|
|
|
|
VERSION="$(awk '$1 == "MARKETING_VERSION" && $2 == "=" { print $3; exit }' Configs/AppVersion.xcconfig)"
|
|
BUILD_NUMBER="$(awk '$1 == "CURRENT_PROJECT_VERSION" && $2 == "=" { print $3; exit }' Configs/AppVersion.xcconfig)"
|
|
TAG_VERSION="${TAG#v}"
|
|
|
|
if [[ -z "$VERSION" || -z "$BUILD_NUMBER" ]]; then
|
|
echo "MARKETING_VERSION and CURRENT_PROJECT_VERSION must be set in Configs/AppVersion.xcconfig" >&2
|
|
exit 1
|
|
fi
|
|
if [[ "$TAG_VERSION" != "$VERSION" ]]; then
|
|
echo "Release tag $TAG does not match Configs/AppVersion.xcconfig MARKETING_VERSION $VERSION" >&2
|
|
exit 1
|
|
fi
|
|
if [[ ! "$BUILD_NUMBER" =~ ^[0-9]+$ ]]; then
|
|
echo "Configs/AppVersion.xcconfig CURRENT_PROJECT_VERSION must be numeric" >&2
|
|
exit 1
|
|
fi
|
|
|
|
PRERELEASE="false"
|
|
if [[ "$VERSION" == *"-"* || "$VERSION" == *"beta"* || "$VERSION" == *"alpha"* || "$VERSION" == *"rc"* ]]; then
|
|
PRERELEASE="true"
|
|
fi
|
|
|
|
{
|
|
echo "TAG=$TAG"
|
|
echo "VERSION=$VERSION"
|
|
echo "BUILD_NUMBER=$BUILD_NUMBER"
|
|
echo "PRERELEASE=$PRERELEASE"
|
|
echo "DMG_PATH=${ARTIFACT_DIR}/${TAG}/${PROJECT_NAME}.dmg"
|
|
echo "SHA256_PATH=${ARTIFACT_DIR}/${TAG}/${PROJECT_NAME}.sha256"
|
|
} >> "$GITHUB_ENV"
|
|
|
|
- name: Validate required secrets
|
|
env:
|
|
APPLE_DEVELOPMENT_TEAM: ${{ secrets.APPLE_DEVELOPMENT_TEAM }}
|
|
BUNDLE_IDENTIFIER_PREFIX: ${{ secrets.BUNDLE_IDENTIFIER_PREFIX }}
|
|
DEVELOPER_ID_CERT_P12: ${{ secrets.DEVELOPER_ID_CERT_P12 }}
|
|
DEVELOPER_ID_CERT_PASSWORD: ${{ secrets.DEVELOPER_ID_CERT_PASSWORD }}
|
|
ASC_API_KEY_P8_BASE64: ${{ secrets.ASC_API_KEY_P8_BASE64 }}
|
|
ASC_API_KEY_ID: ${{ secrets.ASC_API_KEY_ID }}
|
|
ASC_API_ISSUER_ID: ${{ secrets.ASC_API_ISSUER_ID }}
|
|
SPARKLE_PRIVATE_KEY: ${{ secrets.SPARKLE_PRIVATE_KEY }}
|
|
run: |
|
|
missing=0
|
|
for name in \
|
|
APPLE_DEVELOPMENT_TEAM \
|
|
BUNDLE_IDENTIFIER_PREFIX \
|
|
DEVELOPER_ID_CERT_P12 \
|
|
DEVELOPER_ID_CERT_PASSWORD \
|
|
ASC_API_KEY_P8_BASE64 \
|
|
ASC_API_KEY_ID \
|
|
ASC_API_ISSUER_ID \
|
|
SPARKLE_PRIVATE_KEY; do
|
|
if [[ -z "${!name}" ]]; then
|
|
echo "Missing required secret: $name" >&2
|
|
missing=1
|
|
fi
|
|
done
|
|
exit "$missing"
|
|
|
|
- name: Show Xcode version
|
|
run: xcodebuild -version
|
|
|
|
- name: Install XcodeGen
|
|
run: |
|
|
if ! command -v xcodegen >/dev/null 2>&1; then
|
|
brew install xcodegen
|
|
fi
|
|
|
|
- name: Write release local config
|
|
env:
|
|
APPLE_DEVELOPMENT_TEAM: ${{ secrets.APPLE_DEVELOPMENT_TEAM }}
|
|
BUNDLE_IDENTIFIER_PREFIX: ${{ secrets.BUNDLE_IDENTIFIER_PREFIX }}
|
|
run: |
|
|
umask 077
|
|
cat > LocalConfig.xcconfig <<EOF
|
|
DEVELOPMENT_TEAM = ${APPLE_DEVELOPMENT_TEAM}
|
|
BUNDLE_IDENTIFIER_PREFIX = ${BUNDLE_IDENTIFIER_PREFIX}
|
|
EOF
|
|
|
|
- name: Import Developer ID certificate
|
|
env:
|
|
DEVELOPER_ID_CERT_P12: ${{ secrets.DEVELOPER_ID_CERT_P12 }}
|
|
DEVELOPER_ID_CERT_PASSWORD: ${{ secrets.DEVELOPER_ID_CERT_PASSWORD }}
|
|
run: |
|
|
CERT_PATH="$RUNNER_TEMP/developer-id.p12"
|
|
KEYCHAIN_PATH="$RUNNER_TEMP/build.keychain-db"
|
|
KEYCHAIN_PASSWORD="$(openssl rand -hex 24)"
|
|
|
|
echo "$DEVELOPER_ID_CERT_P12" | base64 --decode > "$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" \
|
|
-k "$KEYCHAIN_PATH" \
|
|
-P "$DEVELOPER_ID_CERT_PASSWORD" \
|
|
-T /usr/bin/codesign \
|
|
-T /usr/bin/security
|
|
security set-key-partition-list \
|
|
-S apple-tool:,apple:,codesign: \
|
|
-s \
|
|
-k "$KEYCHAIN_PASSWORD" \
|
|
"$KEYCHAIN_PATH"
|
|
security list-keychains -d user -s "$KEYCHAIN_PATH"
|
|
|
|
SIGNING_IDENTITY="$(security find-identity -v -p codesigning "$KEYCHAIN_PATH" | awk -F'"' '/Developer ID Application/ { print $2; exit }')"
|
|
if [[ -z "$SIGNING_IDENTITY" ]]; then
|
|
echo "Developer ID Application identity was not found in imported certificate." >&2
|
|
exit 1
|
|
fi
|
|
|
|
rm -f "$CERT_PATH"
|
|
echo "KEYCHAIN_PATH=$KEYCHAIN_PATH" >> "$GITHUB_ENV"
|
|
echo "SIGNING_IDENTITY=$SIGNING_IDENTITY" >> "$GITHUB_ENV"
|
|
|
|
- name: Generate Xcode project
|
|
run: make generate
|
|
|
|
- name: Build unsigned Release app
|
|
run: |
|
|
./scripts/xcodebuild-filtered.sh \
|
|
-project "${PROJECT_NAME}.xcodeproj" \
|
|
-scheme "${SCHEME}" \
|
|
-configuration Release \
|
|
-destination "generic/platform=macOS" \
|
|
-derivedDataPath "${DERIVED_DATA}" \
|
|
MARKETING_VERSION="${VERSION}" \
|
|
CURRENT_PROJECT_VERSION="${BUILD_NUMBER}" \
|
|
CODE_SIGNING_ALLOWED=NO \
|
|
CODE_SIGNING_REQUIRED=NO \
|
|
CODE_SIGN_IDENTITY= \
|
|
build \
|
|
-quiet
|
|
|
|
- name: Sign app bundle
|
|
run: |
|
|
APP_PATH="${DERIVED_DATA}/Build/Products/Release/${PROJECT_NAME}.app"
|
|
ENTITLEMENTS="Configs/MacTools.entitlements"
|
|
FINDER_SYNC_ENTITLEMENTS="Sources/Extensions/RightClickFinderSync/RightClickFinderSync.entitlements"
|
|
SPARKLE_FRAMEWORK="$APP_PATH/Contents/Frameworks/Sparkle.framework"
|
|
SPARKLE_CURRENT="$SPARKLE_FRAMEWORK/Versions/Current"
|
|
|
|
sign_path() {
|
|
/usr/bin/codesign \
|
|
--force \
|
|
--sign "$SIGNING_IDENTITY" \
|
|
--keychain "$KEYCHAIN_PATH" \
|
|
--options runtime \
|
|
--timestamp \
|
|
"$1"
|
|
}
|
|
|
|
sign_path_with_entitlements() {
|
|
/usr/bin/codesign \
|
|
--force \
|
|
--sign "$SIGNING_IDENTITY" \
|
|
--keychain "$KEYCHAIN_PATH" \
|
|
--options runtime \
|
|
--timestamp \
|
|
--entitlements "$2" \
|
|
"$1"
|
|
}
|
|
|
|
sign_path_preserving_entitlements() {
|
|
/usr/bin/codesign \
|
|
--force \
|
|
--sign "$SIGNING_IDENTITY" \
|
|
--keychain "$KEYCHAIN_PATH" \
|
|
--options runtime \
|
|
--timestamp \
|
|
--preserve-metadata=entitlements \
|
|
"$1"
|
|
}
|
|
|
|
if [[ -d "$SPARKLE_FRAMEWORK" ]]; then
|
|
if [[ -d "$SPARKLE_CURRENT/XPCServices/Installer.xpc" ]]; then
|
|
sign_path "$SPARKLE_CURRENT/XPCServices/Installer.xpc"
|
|
fi
|
|
if [[ -d "$SPARKLE_CURRENT/XPCServices/Downloader.xpc" ]]; then
|
|
sign_path_preserving_entitlements "$SPARKLE_CURRENT/XPCServices/Downloader.xpc"
|
|
fi
|
|
if [[ -f "$SPARKLE_CURRENT/Autoupdate" ]]; then
|
|
sign_path "$SPARKLE_CURRENT/Autoupdate"
|
|
fi
|
|
if [[ -d "$SPARKLE_CURRENT/Updater.app" ]]; then
|
|
sign_path "$SPARKLE_CURRENT/Updater.app"
|
|
fi
|
|
fi
|
|
|
|
while IFS= read -r binary; do
|
|
[[ -n "$binary" ]] || continue
|
|
sign_path "$binary"
|
|
done < <(find "$APP_PATH/Contents" -type f \( -name "*.dylib" -o -name "*.so" \) -print)
|
|
|
|
while IFS= read -r bundle; do
|
|
[[ -n "$bundle" ]] || continue
|
|
if [[ "$bundle" == *"/Sparkle.framework" || "$bundle" == *"/Sparkle.framework/"* ]]; then
|
|
continue
|
|
fi
|
|
if [[ "$(basename "$bundle")" == "RightClickFinderSync.appex" ]]; then
|
|
sign_path_with_entitlements "$bundle" "$FINDER_SYNC_ENTITLEMENTS"
|
|
else
|
|
sign_path "$bundle"
|
|
fi
|
|
done < <(find "$APP_PATH/Contents" -depth -type d \( -name "*.framework" -o -name "*.app" -o -name "*.xpc" -o -name "*.appex" \) -print)
|
|
|
|
if [[ -d "$SPARKLE_FRAMEWORK" ]]; then
|
|
sign_path "$SPARKLE_FRAMEWORK"
|
|
fi
|
|
|
|
/usr/bin/codesign \
|
|
--force \
|
|
--sign "$SIGNING_IDENTITY" \
|
|
--keychain "$KEYCHAIN_PATH" \
|
|
--options runtime \
|
|
--timestamp \
|
|
--entitlements "$ENTITLEMENTS" \
|
|
"$APP_PATH"
|
|
|
|
FINDER_SYNC_APPEX="$APP_PATH/Contents/PlugIns/RightClickFinderSync.appex"
|
|
FINDER_SYNC_EXTENSION_POINT="$(/usr/libexec/PlistBuddy -c "Print :NSExtension:NSExtensionPointIdentifier" "$FINDER_SYNC_APPEX/Contents/Info.plist")"
|
|
if [[ "$FINDER_SYNC_EXTENSION_POINT" != "com.apple.FinderSync" ]]; then
|
|
echo "Finder Sync extension point is invalid: $FINDER_SYNC_EXTENSION_POINT" >&2
|
|
exit 1
|
|
fi
|
|
FINDER_SYNC_SIGNED_ENTITLEMENTS="$(/usr/bin/codesign -d --entitlements :- "$FINDER_SYNC_APPEX" 2>/dev/null || true)"
|
|
if [[ "$FINDER_SYNC_SIGNED_ENTITLEMENTS" != *"com.apple.security.app-sandbox"* ]]; then
|
|
echo "Finder Sync extension is missing app sandbox entitlement" >&2
|
|
exit 1
|
|
fi
|
|
if [[ "$FINDER_SYNC_SIGNED_ENTITLEMENTS" != *"com.apple.security.temporary-exception.files.home-relative-path.read-only"* ]]; then
|
|
echo "Finder Sync extension is missing right-click config read entitlement" >&2
|
|
exit 1
|
|
fi
|
|
|
|
/usr/bin/codesign --verify --deep --strict --verbose=2 "$APP_PATH"
|
|
|
|
- name: Create and sign DMG
|
|
env:
|
|
BUNDLE_IDENTIFIER_PREFIX: ${{ secrets.BUNDLE_IDENTIFIER_PREFIX }}
|
|
run: |
|
|
APP_PATH="${DERIVED_DATA}/Build/Products/Release/${PROJECT_NAME}.app"
|
|
STAGE_DIR="$(mktemp -d "${RUNNER_TEMP}/mactools-dmg.XXXXXX")"
|
|
mkdir -p "$(dirname "$DMG_PATH")"
|
|
|
|
ditto "$APP_PATH" "$STAGE_DIR/${PROJECT_NAME}.app"
|
|
ln -s /Applications "$STAGE_DIR/Applications"
|
|
|
|
hdiutil create \
|
|
-volname "$PROJECT_NAME" \
|
|
-srcfolder "$STAGE_DIR" \
|
|
-ov \
|
|
-format UDZO \
|
|
"$DMG_PATH" >/dev/null
|
|
|
|
/bin/rm -rf "$STAGE_DIR"
|
|
|
|
/usr/bin/codesign \
|
|
--force \
|
|
--sign "$SIGNING_IDENTITY" \
|
|
--keychain "$KEYCHAIN_PATH" \
|
|
--timestamp \
|
|
--identifier "${BUNDLE_IDENTIFIER_PREFIX}.mactools.disk-image" \
|
|
"$DMG_PATH"
|
|
|
|
/usr/bin/codesign --verify --verbose=2 "$DMG_PATH"
|
|
|
|
- name: Notarize and staple DMG
|
|
env:
|
|
ASC_API_KEY_P8_BASE64: ${{ secrets.ASC_API_KEY_P8_BASE64 }}
|
|
ASC_API_KEY_ID: ${{ secrets.ASC_API_KEY_ID }}
|
|
ASC_API_ISSUER_ID: ${{ secrets.ASC_API_ISSUER_ID }}
|
|
run: |
|
|
API_KEY_PATH="$RUNNER_TEMP/AuthKey_${ASC_API_KEY_ID}.p8"
|
|
echo "$ASC_API_KEY_P8_BASE64" | base64 --decode > "$API_KEY_PATH"
|
|
|
|
xcrun notarytool submit "$DMG_PATH" \
|
|
--key "$API_KEY_PATH" \
|
|
--key-id "$ASC_API_KEY_ID" \
|
|
--issuer "$ASC_API_ISSUER_ID" \
|
|
--wait \
|
|
--timeout 30m
|
|
|
|
xcrun stapler staple "$DMG_PATH"
|
|
spctl -a -t open --context context:primary-signature -v "$DMG_PATH"
|
|
rm -f "$API_KEY_PATH"
|
|
|
|
- name: Compute SHA256
|
|
run: |
|
|
shasum -a 256 "$DMG_PATH" | tee "$SHA256_PATH"
|
|
|
|
- name: Extract release notes from CHANGELOG
|
|
run: |
|
|
RELEASE_NOTES="$RUNNER_TEMP/release-notes.md"
|
|
HIGHLIGHTS_PATH=".github/release-highlights/${TAG}.md"
|
|
GENERATED_NOTES="$RUNNER_TEMP/release-notes.generated.md"
|
|
|
|
python3 scripts/changelog.py extract \
|
|
--tag "$TAG" \
|
|
--output "$GENERATED_NOTES"
|
|
|
|
if [[ -f "$HIGHLIGHTS_PATH" ]]; then
|
|
cat "$HIGHLIGHTS_PATH" > "$RELEASE_NOTES"
|
|
printf '\n\n' >> "$RELEASE_NOTES"
|
|
cat "$GENERATED_NOTES" >> "$RELEASE_NOTES"
|
|
else
|
|
cat "$GENERATED_NOTES" > "$RELEASE_NOTES"
|
|
fi
|
|
|
|
SPARKLE_RELEASE_NOTES="$RUNNER_TEMP/sparkle-release-notes.md"
|
|
python3 scripts/changelog.py compose-sparkle \
|
|
--tag "$TAG" \
|
|
--app-notes "$RELEASE_NOTES" \
|
|
--output "$SPARKLE_RELEASE_NOTES"
|
|
|
|
{
|
|
echo "RELEASE_NOTES_PATH=$RELEASE_NOTES"
|
|
echo "SPARKLE_RELEASE_NOTES_PATH=$SPARKLE_RELEASE_NOTES"
|
|
} >> "$GITHUB_ENV"
|
|
|
|
- name: Generate Sparkle appcast
|
|
env:
|
|
SPARKLE_PRIVATE_KEY: ${{ secrets.SPARKLE_PRIVATE_KEY }}
|
|
run: |
|
|
SIGN_UPDATE="${DERIVED_DATA}/SourcePackages/artifacts/sparkle/Sparkle/bin/sign_update"
|
|
if [[ ! -x "$SIGN_UPDATE" ]]; then
|
|
SIGN_UPDATE="$(find "${DERIVED_DATA}/SourcePackages" -path "*/Sparkle/bin/sign_update" -type f -perm -111 | head -n 1)"
|
|
fi
|
|
if [[ -z "$SIGN_UPDATE" || ! -x "$SIGN_UPDATE" ]]; then
|
|
echo "Sparkle sign_update tool was not found." >&2
|
|
exit 1
|
|
fi
|
|
|
|
SPARKLE_KEY_PATH="$RUNNER_TEMP/sparkle_ed25519_key"
|
|
printf '%s' "$SPARKLE_PRIVATE_KEY" > "$SPARKLE_KEY_PATH"
|
|
SIGN_OUTPUT="$("$SIGN_UPDATE" "$DMG_PATH" --ed-key-file "$SPARKLE_KEY_PATH")"
|
|
rm -f "$SPARKLE_KEY_PATH"
|
|
|
|
ED_SIGNATURE="$(printf '%s' "$SIGN_OUTPUT" | sed -n 's/.*sparkle:edSignature="\([^"]*\)".*/\1/p')"
|
|
if [[ -z "$ED_SIGNATURE" ]]; then
|
|
echo "Failed to extract Sparkle EdDSA signature." >&2
|
|
exit 1
|
|
fi
|
|
|
|
FILE_SIZE="$(stat -f '%z' "$DMG_PATH")"
|
|
DOWNLOAD_URL="https://github.com/${GITHUB_REPOSITORY}/releases/download/${TAG}/${PROJECT_NAME}.dmg"
|
|
RELEASE_NOTES_URL="https://github.com/${GITHUB_REPOSITORY}/releases/tag/${TAG}"
|
|
FULL_RELEASE_NOTES_URL="https://github.com/${GITHUB_REPOSITORY}/releases"
|
|
PUB_DATE="$(LC_ALL=C date -u '+%a, %d %b %Y %H:%M:%S +0000')"
|
|
MINIMUM_SYSTEM_VERSION="14.0"
|
|
export ED_SIGNATURE FILE_SIZE DOWNLOAD_URL RELEASE_NOTES_URL FULL_RELEASE_NOTES_URL PUB_DATE MINIMUM_SYSTEM_VERSION
|
|
|
|
python3 <<'PY_APPCAST'
|
|
import os
|
|
import xml.sax.saxutils as xml
|
|
from pathlib import Path
|
|
|
|
values = {
|
|
key: os.environ[key]
|
|
for key in [
|
|
"PROJECT_NAME",
|
|
"VERSION",
|
|
"BUILD_NUMBER",
|
|
"DOWNLOAD_URL",
|
|
"RELEASE_NOTES_URL",
|
|
"FULL_RELEASE_NOTES_URL",
|
|
"PUB_DATE",
|
|
"FILE_SIZE",
|
|
"ED_SIGNATURE",
|
|
"MINIMUM_SYSTEM_VERSION",
|
|
]
|
|
}
|
|
release_notes = Path(os.environ["SPARKLE_RELEASE_NOTES_PATH"]).read_text(encoding="utf-8")
|
|
cdata_release_notes = release_notes.replace("]]>", "]]]]><![CDATA[>")
|
|
|
|
content = f'''<?xml version="1.0" encoding="utf-8"?>
|
|
<rss version="2.0" xmlns:sparkle="http://www.andymatuschak.org/xml-namespaces/sparkle" xmlns:dc="http://purl.org/dc/elements/1.1/">
|
|
<channel>
|
|
<title>{xml.escape(values["PROJECT_NAME"])} Releases</title>
|
|
<description>Latest release metadata for {xml.escape(values["PROJECT_NAME"])}.</description>
|
|
<language>en</language>
|
|
<item>
|
|
<title>Version {xml.escape(values["VERSION"])}</title>
|
|
<link>{xml.escape(values["RELEASE_NOTES_URL"])}</link>
|
|
<sparkle:version>{xml.escape(values["BUILD_NUMBER"])}</sparkle:version>
|
|
<sparkle:shortVersionString>{xml.escape(values["VERSION"])}</sparkle:shortVersionString>
|
|
<description sparkle:format="markdown"><![CDATA[{cdata_release_notes}]]></description>
|
|
<sparkle:fullReleaseNotesLink>{xml.escape(values["FULL_RELEASE_NOTES_URL"])}</sparkle:fullReleaseNotesLink>
|
|
<pubDate>{xml.escape(values["PUB_DATE"])}</pubDate>
|
|
<enclosure url="{xml.escape(values["DOWNLOAD_URL"])}" length="{xml.escape(values["FILE_SIZE"])}" type="application/octet-stream" sparkle:edSignature="{xml.escape(values["ED_SIGNATURE"])}" />
|
|
<sparkle:minimumSystemVersion>{xml.escape(values["MINIMUM_SYSTEM_VERSION"])}</sparkle:minimumSystemVersion>
|
|
</item>
|
|
</channel>
|
|
</rss>
|
|
'''
|
|
Path("docs/appcast.xml").write_text(content, encoding="utf-8")
|
|
PY_APPCAST
|
|
|
|
python3 -c 'import xml.etree.ElementTree as ET; ET.parse("docs/appcast.xml")'
|
|
|
|
- name: Upload notarized DMG artifact
|
|
uses: actions/upload-artifact@v4
|
|
with:
|
|
name: MacTools-${{ env.TAG }}
|
|
path: |
|
|
${{ env.DMG_PATH }}
|
|
${{ env.SHA256_PATH }}
|
|
if-no-files-found: error
|
|
retention-days: 30
|
|
|
|
- name: Create or update GitHub Release
|
|
env:
|
|
GH_TOKEN: ${{ github.token }}
|
|
run: |
|
|
RELEASE_ARGS=(--title "${PROJECT_NAME} ${VERSION}" --notes-file "$RELEASE_NOTES_PATH")
|
|
if [[ "$PRERELEASE" == "true" ]]; then
|
|
RELEASE_ARGS+=(--prerelease)
|
|
fi
|
|
|
|
if gh release view "$TAG" >/dev/null 2>&1; then
|
|
gh release upload "$TAG" "$DMG_PATH#${PROJECT_NAME}.dmg" "$SHA256_PATH#${PROJECT_NAME}.sha256" --clobber
|
|
gh release edit "$TAG" "${RELEASE_ARGS[@]}"
|
|
else
|
|
gh release create "$TAG" "$DMG_PATH#${PROJECT_NAME}.dmg" "$SHA256_PATH#${PROJECT_NAME}.sha256" "${RELEASE_ARGS[@]}"
|
|
fi
|
|
|
|
- name: Open Homebrew tap update PR
|
|
env:
|
|
HOMEBREW_GITHUB_API_TOKEN: ${{ secrets.HOMEBREW_GITHUB_API_TOKEN }}
|
|
run: |
|
|
TAP_REPOSITORY="ggbond268/homebrew-mactools"
|
|
|
|
if [[ "$PRERELEASE" == "true" ]]; then
|
|
echo "Skipping Homebrew tap update for prerelease ${TAG}."
|
|
exit 0
|
|
fi
|
|
|
|
if [[ -z "$HOMEBREW_GITHUB_API_TOKEN" ]]; then
|
|
echo "Skipping Homebrew tap update because HOMEBREW_GITHUB_API_TOKEN is not configured."
|
|
exit 0
|
|
fi
|
|
|
|
export GH_TOKEN="$HOMEBREW_GITHUB_API_TOKEN"
|
|
|
|
SHA256="$(awk '{ print $1; exit }' "$SHA256_PATH")"
|
|
if [[ -z "$SHA256" ]]; then
|
|
echo "Unable to read SHA256 from ${SHA256_PATH}." >&2
|
|
exit 1
|
|
fi
|
|
DOWNLOAD_URL="https://github.com/${GITHUB_REPOSITORY}/releases/download/${TAG}/${PROJECT_NAME}.dmg"
|
|
export SHA256 DOWNLOAD_URL
|
|
TAP_DIR="$(mktemp -d)"
|
|
BRANCH="update-mactools-${VERSION}"
|
|
|
|
gh repo clone "$TAP_REPOSITORY" "$TAP_DIR"
|
|
cd "$TAP_DIR"
|
|
gh auth setup-git
|
|
|
|
git config user.name "github-actions[bot]"
|
|
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
|
git checkout -B "$BRANCH"
|
|
|
|
mkdir -p Casks
|
|
ruby -e 'File.write("Casks/mactools.rb", <<~CASK)
|
|
cask "mactools" do
|
|
version "#{ENV.fetch("VERSION")}"
|
|
sha256 "#{ENV.fetch("SHA256")}"
|
|
|
|
url "#{ENV.fetch("DOWNLOAD_URL")}"
|
|
name "MacTools"
|
|
desc "Menu bar toolbox"
|
|
homepage "https://github.com/#{ENV.fetch("GITHUB_REPOSITORY")}"
|
|
|
|
livecheck do
|
|
url :url
|
|
strategy :github_latest
|
|
end
|
|
|
|
auto_updates true
|
|
depends_on macos: :sonoma
|
|
|
|
app "MacTools.app"
|
|
end
|
|
CASK'
|
|
|
|
brew style Casks/mactools.rb
|
|
|
|
if git diff --quiet -- Casks/mactools.rb; then
|
|
echo "Homebrew tap cask is already up to date."
|
|
exit 0
|
|
fi
|
|
|
|
git add Casks/mactools.rb
|
|
git commit -m "Update mactools to ${VERSION}"
|
|
git push --force-with-lease origin "$BRANCH"
|
|
|
|
pr_number="$(gh pr list --repo "$TAP_REPOSITORY" --head "$BRANCH" --json number --jq '.[0].number // empty')"
|
|
if [[ -n "$pr_number" ]]; then
|
|
echo "Homebrew tap update PR already exists: #${pr_number}"
|
|
else
|
|
pr_url="$(gh pr create \
|
|
--repo "$TAP_REPOSITORY" \
|
|
--base main \
|
|
--head "$BRANCH" \
|
|
--title "Update mactools to ${VERSION}" \
|
|
--body "Updates the MacTools cask for ${TAG}.")"
|
|
pr_number="${pr_url##*/}"
|
|
echo "Created Homebrew tap update PR: #${pr_number}"
|
|
fi
|
|
|
|
gh pr merge "$pr_number" \
|
|
--repo "$TAP_REPOSITORY" \
|
|
--squash \
|
|
--delete-branch \
|
|
--subject "Update mactools to ${VERSION}"
|
|
|
|
- name: Commit appcast to main
|
|
run: |
|
|
cp docs/appcast.xml "$RUNNER_TEMP/appcast.xml"
|
|
git config user.name "github-actions[bot]"
|
|
git config user.email "github-actions[bot]@users.noreply.github.com"
|
|
git fetch origin main
|
|
git checkout -B main origin/main
|
|
cp "$RUNNER_TEMP/appcast.xml" docs/appcast.xml
|
|
git add docs/appcast.xml
|
|
if git diff --cached --quiet; then
|
|
echo "No appcast changes to commit."
|
|
else
|
|
git commit -m "ci: update appcast for ${TAG}"
|
|
git push origin main
|
|
fi
|
|
|
|
- name: Cleanup keychain
|
|
if: always()
|
|
run: |
|
|
if [[ -n "${KEYCHAIN_PATH:-}" ]]; then
|
|
security delete-keychain "$KEYCHAIN_PATH" 2>/dev/null || true
|
|
fi
|