Neon-Vision-Editor/.github/workflows/release-notarized.yml

278 lines
10 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
issues: write
jobs:
release:
timeout-minutes: 180
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 }}
SERVER_URL: ${{ github.server_url }}
run: |
set -euo pipefail
SERVER_HOST="${SERVER_URL#https://}"
git init
git remote add origin "https://x-access-token:${GH_TOKEN}@${SERVER_HOST}/${REPO}.git"
git fetch --depth=1 origin "refs/tags/${TAG_NAME}"
git checkout FETCH_HEAD
- name: Select Xcode 17+ (required for Tahoe AppIcon.icon)
id: xcode_mode
run: |
set -euo pipefail
if [[ -x scripts/ci/select_xcode17.sh ]]; then
scripts/ci/select_xcode17.sh
else
echo "Missing scripts/ci/select_xcode17.sh in tag; cannot guarantee Tahoe AppIcon.icon output." >&2
exit 1
fi
xcodebuild -version
XCODE_MAJOR="$(xcodebuild -version | awk '/Xcode/ {split($2,v,"."); print v[1]}' | head -n1)"
if [[ -z "${XCODE_MAJOR}" || "${XCODE_MAJOR}" -lt 17 ]]; then
echo "Xcode 17+ is required for notarized release to preserve Tahoe AppIcon.icon." >&2
exit 1
fi
echo "require_iconstack=1" >> "$GITHUB_OUTPUT"
- 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"
ORIGINAL_LIST="$RUNNER_TEMP/original-user-keychains.txt"
ORIGINAL_DEFAULT="$RUNNER_TEMP/original-default-keychain.txt"
ORIGINAL_LOGIN="$RUNNER_TEMP/original-login-keychain.txt"
echo "$MACOS_CERT_P12" | base64 --decode > cert.p12
security list-keychains -d user > "$ORIGINAL_LIST" || true
security default-keychain -d user > "$ORIGINAL_DEFAULT" || true
security login-keychain -d user > "$ORIGINAL_LOGIN" || true
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
mapfile -t EXISTING_KEYCHAINS < <(security list-keychains -d user | sed -E 's/^[[:space:]]*"?(.*)"?[[:space:]]*$/\1/' | awk 'NF')
if (( ${#EXISTING_KEYCHAINS[@]} > 0 )); then
security list-keychains -d user -s "$KEYCHAIN" "${EXISTING_KEYCHAINS[@]}"
else
security list-keychains -d user -s "$KEYCHAIN"
fi
- 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
REQUIRE_ICONSTACK: ${{ steps.xcode_mode.outputs.require_iconstack }}
run: |
set -euo pipefail
APP="$EXPORT_PATH/Neon Vision Editor.app"
scripts/ci/verify_icon_payload.sh "$APP"
- 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\r?$" 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}\\*\\*\r?$" README.md >/dev/null
grep -nE "^- Latest release: \\*\\*${TAG_NAME}\\*\\*\r?$" README.md >/dev/null
grep -nE "^### ${TAG_NAME} \\(summary\\)\r?$" 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 }}
REQUIRE_ICONSTACK: ${{ steps.xcode_mode.outputs.require_iconstack }}
run: |
set -euo pipefail
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"
scripts/ci/verify_icon_payload.sh "$APP"
rm -rf "$WORK_DIR"
- 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"
- name: Restore user keychain state
if: ${{ always() }}
run: |
set -euo pipefail
KEYCHAIN="$RUNNER_TEMP/build.keychain-db"
ORIGINAL_LIST="$RUNNER_TEMP/original-user-keychains.txt"
ORIGINAL_DEFAULT="$RUNNER_TEMP/original-default-keychain.txt"
ORIGINAL_LOGIN="$RUNNER_TEMP/original-login-keychain.txt"
if [[ -f "$ORIGINAL_LIST" ]]; then
mapfile -t RESTORE_LIST < <(sed -E 's/^[[:space:]]*"?(.*)"?[[:space:]]*$/\1/' "$ORIGINAL_LIST" | awk 'NF')
if (( ${#RESTORE_LIST[@]} > 0 )); then
security list-keychains -d user -s "${RESTORE_LIST[@]}" || true
fi
fi
if [[ -f "$ORIGINAL_DEFAULT" ]]; then
DEFAULT_KEYCHAIN="$(sed -E 's/^[[:space:]]*"?(.*)"?[[:space:]]*$/\1/' "$ORIGINAL_DEFAULT" | head -n1)"
if [[ -n "${DEFAULT_KEYCHAIN:-}" ]]; then
security default-keychain -d user -s "$DEFAULT_KEYCHAIN" || true
fi
fi
if [[ -f "$ORIGINAL_LOGIN" ]]; then
LOGIN_KEYCHAIN="$(sed -E 's/^[[:space:]]*"?(.*)"?[[:space:]]*$/\1/' "$ORIGINAL_LOGIN" | head -n1)"
if [[ -n "${LOGIN_KEYCHAIN:-}" ]]; then
security login-keychain -d user -s "$LOGIN_KEYCHAIN" || true
fi
fi
security delete-keychain "$KEYCHAIN" || true
rm -f cert.p12