mirror of
https://github.com/h3pdesign/Neon-Vision-Editor
synced 2026-04-21 13:27:16 +00:00
252 lines
9.2 KiB
YAML
252 lines
9.2 KiB
YAML
name: Release macOS app (notarized)
|
|
run-name: notarized release ${{ inputs.tag }}
|
|
|
|
on:
|
|
workflow_dispatch:
|
|
inputs:
|
|
tag:
|
|
description: "Existing Git tag to release (e.g. v0.4.5)"
|
|
required: true
|
|
type: string
|
|
|
|
permissions:
|
|
contents: write
|
|
|
|
jobs:
|
|
release:
|
|
concurrency:
|
|
group: release-${{ inputs.tag }}
|
|
cancel-in-progress: true
|
|
runs-on: macos-15
|
|
|
|
steps:
|
|
- name: Checkout tag
|
|
env:
|
|
TAG_NAME: ${{ inputs.tag }}
|
|
REPO: ${{ github.repository }}
|
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
run: |
|
|
set -euo pipefail
|
|
git init
|
|
git remote add origin "https://x-access-token:${GH_TOKEN}@github.com/${REPO}.git"
|
|
git fetch --depth=1 origin "refs/tags/${TAG_NAME}"
|
|
git checkout FETCH_HEAD
|
|
|
|
- name: Select/verify Xcode 17+
|
|
run: |
|
|
set -euo pipefail
|
|
if [[ -x scripts/ci/select_xcode17.sh ]]; then
|
|
scripts/ci/select_xcode17.sh
|
|
else
|
|
xcodebuild -version
|
|
XCODE_MAJOR="$(xcodebuild -version | awk '/Xcode/ {split($2, v, "."); print v[1]}')"
|
|
if [[ "${XCODE_MAJOR:-0}" -lt 17 ]]; then
|
|
echo "Xcode 17+ required for AppIcon.icon builds." >&2
|
|
exit 1
|
|
fi
|
|
fi
|
|
|
|
- name: Import signing certificate
|
|
env:
|
|
MACOS_CERT_P12: ${{ secrets.MACOS_CERT_P12 }}
|
|
MACOS_CERT_PASSWORD: ${{ secrets.MACOS_CERT_PASSWORD }}
|
|
KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }}
|
|
run: |
|
|
set -euo pipefail
|
|
KEYCHAIN="$RUNNER_TEMP/build.keychain-db"
|
|
|
|
echo "$MACOS_CERT_P12" | base64 --decode > cert.p12
|
|
|
|
security create-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN"
|
|
security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN"
|
|
security import cert.p12 -k "$KEYCHAIN" -P "$MACOS_CERT_PASSWORD" -T /usr/bin/codesign
|
|
security list-keychains -d user -s "$KEYCHAIN"
|
|
|
|
- name: Build archive
|
|
env:
|
|
TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
|
ARCHIVE_PATH: ${{ runner.temp }}/NeonVisionEditor.xcarchive
|
|
run: |
|
|
set -euo pipefail
|
|
xcodebuild \
|
|
-project "Neon Vision Editor.xcodeproj" \
|
|
-scheme "Neon Vision Editor" \
|
|
-configuration Release \
|
|
-destination "generic/platform=macOS" \
|
|
-archivePath "$ARCHIVE_PATH" \
|
|
CODE_SIGN_STYLE=Manual \
|
|
DEVELOPMENT_TEAM="$TEAM_ID" \
|
|
CODE_SIGN_IDENTITY="Developer ID Application" \
|
|
archive
|
|
|
|
- name: Export app
|
|
env:
|
|
TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
|
ARCHIVE_PATH: ${{ runner.temp }}/NeonVisionEditor.xcarchive
|
|
EXPORT_PATH: ${{ runner.temp }}/export
|
|
run: |
|
|
set -euo pipefail
|
|
mkdir -p "$EXPORT_PATH"
|
|
|
|
cat > export.plist <<EOF
|
|
<?xml version="1.0" encoding="UTF-8"?>
|
|
<plist version="1.0">
|
|
<dict>
|
|
<key>method</key><string>developer-id</string>
|
|
<key>teamID</key><string>${TEAM_ID}</string>
|
|
</dict>
|
|
</plist>
|
|
EOF
|
|
|
|
xcodebuild -exportArchive \
|
|
-archivePath "$ARCHIVE_PATH" \
|
|
-exportPath "$EXPORT_PATH" \
|
|
-exportOptionsPlist export.plist
|
|
|
|
- name: Verify app icon payload
|
|
env:
|
|
EXPORT_PATH: ${{ runner.temp }}/export
|
|
run: |
|
|
set -euo pipefail
|
|
APP="$EXPORT_PATH/Neon Vision Editor.app"
|
|
if [[ -x scripts/ci/verify_icon_payload.sh ]]; then
|
|
scripts/ci/verify_icon_payload.sh "$APP"
|
|
else
|
|
INFO="$APP/Contents/Info.plist"
|
|
CAR="$APP/Contents/Resources/Assets.car"
|
|
ICON_NAME="$(/usr/libexec/PlistBuddy -c 'Print :CFBundleIconName' "$INFO" 2>/dev/null || true)"
|
|
if [[ "$ICON_NAME" != "AppIcon" ]]; then
|
|
echo "Unexpected CFBundleIconName: '$ICON_NAME' (expected 'AppIcon')." >&2
|
|
exit 1
|
|
fi
|
|
TMP_JSON="$(mktemp)"
|
|
xcrun --sdk macosx assetutil --info "$CAR" > "$TMP_JSON"
|
|
grep -Eq '"RenditionName" : "AppIcon\.iconstack"' "$TMP_JSON"
|
|
grep -Eq '"Name" : "AppIcon"' "$TMP_JSON"
|
|
rm -f "$TMP_JSON"
|
|
fi
|
|
|
|
- name: Notarize
|
|
env:
|
|
EXPORT_PATH: ${{ runner.temp }}/export
|
|
APPLE_ID: ${{ secrets.APPLE_ID }}
|
|
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
|
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
|
|
run: |
|
|
set -euo pipefail
|
|
APP="$EXPORT_PATH/Neon Vision Editor.app"
|
|
|
|
ditto -c -k --keepParent "$APP" submit.zip
|
|
|
|
xcrun notarytool submit submit.zip \
|
|
--apple-id "$APPLE_ID" \
|
|
--team-id "$APPLE_TEAM_ID" \
|
|
--password "$APPLE_APP_SPECIFIC_PASSWORD" \
|
|
--wait
|
|
|
|
xcrun stapler staple "$APP"
|
|
|
|
- name: Zip notarized app
|
|
env:
|
|
EXPORT_PATH: ${{ runner.temp }}/export
|
|
run: |
|
|
set -euo pipefail
|
|
ditto -c -k --keepParent "$EXPORT_PATH/Neon Vision Editor.app" Neon.Vision.Editor.app.zip
|
|
|
|
- name: Extract changelog section
|
|
env:
|
|
TAG_NAME: ${{ inputs.tag }}
|
|
run: |
|
|
set -euo pipefail
|
|
./scripts/extract_changelog_section.sh CHANGELOG.md "$TAG_NAME" > release-notes.md
|
|
|
|
- name: Validate release docs are in sync
|
|
env:
|
|
TAG_NAME: ${{ inputs.tag }}
|
|
run: |
|
|
set -euo pipefail
|
|
if grep -nE "^- TODO$" release-notes.md >/dev/null; then
|
|
echo "CHANGELOG section for ${TAG_NAME} still contains TODO entries." >&2
|
|
exit 1
|
|
fi
|
|
grep -nE "^> Latest release: \\*\\*${TAG_NAME}\\*\\*$" README.md >/dev/null
|
|
grep -nE "^- Latest release: \\*\\*${TAG_NAME}\\*\\*$" README.md >/dev/null
|
|
grep -nE "^### ${TAG_NAME} \\(summary\\)$" README.md >/dev/null
|
|
|
|
- name: Create or Update GitHub Release
|
|
id: publish_release
|
|
env:
|
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
TAG_NAME: ${{ inputs.tag }}
|
|
run: |
|
|
set -euo pipefail
|
|
|
|
if gh release view "$TAG_NAME" >/dev/null 2>&1; then
|
|
gh release upload "$TAG_NAME" Neon.Vision.Editor.app.zip --clobber
|
|
gh release edit "$TAG_NAME" --title "Neon Vision Editor $TAG_NAME" --notes-file release-notes.md
|
|
else
|
|
gh release create "$TAG_NAME" Neon.Vision.Editor.app.zip \
|
|
--title "Neon Vision Editor $TAG_NAME" \
|
|
--notes-file release-notes.md
|
|
fi
|
|
|
|
- name: Verify published release asset payload
|
|
env:
|
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
TAG_NAME: ${{ inputs.tag }}
|
|
run: |
|
|
set -euo pipefail
|
|
if [[ -x scripts/ci/verify_release_asset.sh ]]; then
|
|
scripts/ci/verify_release_asset.sh "$TAG_NAME"
|
|
else
|
|
WORK_DIR="$(mktemp -d)"
|
|
gh release download "$TAG_NAME" -p Neon.Vision.Editor.app.zip -D "$WORK_DIR"
|
|
ditto -x -k "$WORK_DIR/Neon.Vision.Editor.app.zip" "$WORK_DIR/extracted"
|
|
APP="$WORK_DIR/extracted/Neon Vision Editor.app"
|
|
INFO="$APP/Contents/Info.plist"
|
|
CAR="$APP/Contents/Resources/Assets.car"
|
|
ICON_NAME="$(/usr/libexec/PlistBuddy -c 'Print :CFBundleIconName' "$INFO" 2>/dev/null || true)"
|
|
if [[ "$ICON_NAME" != "AppIcon" ]]; then
|
|
echo "Unexpected CFBundleIconName: '$ICON_NAME' (expected 'AppIcon')." >&2
|
|
exit 1
|
|
fi
|
|
TMP_JSON="$(mktemp)"
|
|
xcrun --sdk macosx assetutil --info "$CAR" > "$TMP_JSON"
|
|
grep -Eq '"RenditionName" : "AppIcon\.iconstack"' "$TMP_JSON"
|
|
grep -Eq '"Name" : "AppIcon"' "$TMP_JSON"
|
|
rm -rf "$WORK_DIR" "$TMP_JSON"
|
|
fi
|
|
|
|
- name: Roll back broken published release asset
|
|
if: ${{ failure() && steps.publish_release.outcome == 'success' }}
|
|
env:
|
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
TAG_NAME: ${{ inputs.tag }}
|
|
run: |
|
|
set -euo pipefail
|
|
gh release delete-asset "$TAG_NAME" "Neon.Vision.Editor.app.zip" -y || true
|
|
gh release edit "$TAG_NAME" --draft true || true
|
|
echo "Rolled back release asset and marked release as draft due to post-publish verification failure."
|
|
|
|
- name: Create release-failure alert issue
|
|
if: ${{ failure() }}
|
|
continue-on-error: true
|
|
env:
|
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
TAG_NAME: ${{ inputs.tag }}
|
|
run: |
|
|
set -euo pipefail
|
|
gh issue create \
|
|
--title "Release failure: ${TAG_NAME} (run ${GITHUB_RUN_ID})" \
|
|
--body "Automated release failed for ${TAG_NAME}. Run URL: ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}" \
|
|
--label "release-alert"
|
|
|
|
- name: Trigger homebrew-tap update
|
|
env:
|
|
GH_TOKEN: ${{ secrets.TAP_BOT_TOKEN }}
|
|
TAG_NAME: ${{ inputs.tag }}
|
|
run: |
|
|
set -euo pipefail
|
|
gh api repos/h3pdesign/homebrew-tap/dispatches \
|
|
-f event_type=notarized_release \
|
|
-f client_payload[tag]="$TAG_NAME"
|