Skip to content

Release macOS

Release macOS #11

Workflow file for this run

name: Release macOS
on:
workflow_dispatch:
inputs:
tag:
description: Release tag to create and publish, for example v0.1.0
required: true
type: string
target_ref:
description: Branch or commit SHA to release
required: false
default: main
type: string
prerelease:
description: Mark the GitHub Release as a prerelease
required: false
default: false
type: boolean
permissions:
contents: write
concurrency:
group: release-macos-${{ github.event.inputs.tag }}
cancel-in-progress: false
jobs:
prepare:
name: Prepare release tag
runs-on: ubuntu-latest
env:
GH_TOKEN: ${{ github.token }}
outputs:
package_version: ${{ steps.package_version.outputs.value }}
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
ref: ${{ github.event.inputs.target_ref }}
fetch-depth: 0
- name: Validate release tag
shell: bash
run: |
set -euo pipefail
TAG="${{ github.event.inputs.tag }}"
if [[ ! "$TAG" =~ ^v[0-9]+(\.[0-9]+)*([.-][A-Za-z0-9._-]+)?$ ]]; then
echo "Release tag must look like v0.1.0 or v0.1.0-rc1" >&2
exit 1
fi
- name: Derive package version
id: package_version
shell: bash
run: |
set -euo pipefail
PACKAGE_VERSION="${{ github.event.inputs.tag }}"
PACKAGE_VERSION="${PACKAGE_VERSION#v}"
echo "value=${PACKAGE_VERSION}" >> "$GITHUB_OUTPUT"
- name: Check for existing release
shell: bash
run: |
set -euo pipefail
TAG="${{ github.event.inputs.tag }}"
if gh release view "$TAG" >/dev/null 2>&1; then
echo "Release $TAG already exists" >&2
exit 1
fi
- name: Create tag if needed
shell: bash
run: |
set -euo pipefail
TAG="${{ github.event.inputs.tag }}"
TARGET_REF="${{ github.event.inputs.target_ref }}"
git fetch --tags origin
if ! git ls-remote --exit-code --tags origin "refs/tags/$TAG" >/dev/null 2>&1; then
git tag "$TAG" "$TARGET_REF"
git push origin "refs/tags/$TAG"
fi
build:
name: Build, sign, and notarize (${{ matrix.arch }})
needs: prepare
runs-on: ${{ matrix.runner }}
environment: release-macos
strategy:
fail-fast: false
matrix:
include:
- arch: arm64
runner: macos-15
cmake_arch: arm64
dmg_name: Platypus-macos-arm64.dmg
checksum_name: Platypus-macos-arm64.sha256
photoshop_dmg_name: Platypus-photoshop-macos-arm64.dmg
photoshop_checksum_name: Platypus-photoshop-macos-arm64.sha256
- arch: x86_64
runner: macos-15-intel
cmake_arch: x86_64
dmg_name: Platypus-macos-x86_64.dmg
checksum_name: Platypus-macos-x86_64.sha256
photoshop_dmg_name: Platypus-photoshop-macos-x86_64.dmg
photoshop_checksum_name: Platypus-photoshop-macos-x86_64.sha256
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
ref: ${{ github.event.inputs.target_ref }}
- name: Install build dependencies
shell: bash
run: |
set -euo pipefail
brew install ninja qt opencv
echo "$(brew --prefix qt)/bin" >> "$GITHUB_PATH"
- name: Configure app CMake
shell: bash
run: |
set -euo pipefail
QT_PREFIX="$(brew --prefix qt)"
OPENCV_PREFIX="$(brew --prefix opencv)"
cmake -B build -S . -G Ninja \
-DCMAKE_BUILD_TYPE=Release \
-DBUILD_APPLE_APP=ON \
-DBUILD_TESTING=OFF \
-DCMAKE_OSX_ARCHITECTURES="${{ matrix.cmake_arch }}" \
-DPLATYPUS_PACKAGE_VERSION_OVERRIDE="${{ needs.prepare.outputs.package_version }}" \
-DCMAKE_PREFIX_PATH="${QT_PREFIX};${OPENCV_PREFIX}" \
-DOpenCV_DIR="${OPENCV_PREFIX}/lib/cmake/opencv4"
- name: Build PlatypusGui
shell: bash
run: cmake --build build --config Release --target PlatypusGui
- name: Verify app architecture
shell: bash
run: |
set -euo pipefail
APP_BIN="build/PlatypusGui.app/Contents/MacOS/PlatypusGui"
APP_ARCHS="$(lipo -archs "$APP_BIN")"
echo "App architectures: $APP_ARCHS"
[[ "$APP_ARCHS" == *"${{ matrix.cmake_arch }}"* ]] || {
echo "Missing expected architecture ${{ matrix.cmake_arch }}" >&2
exit 1
}
- name: Install staged app bundle
shell: bash
run: |
set -euo pipefail
STAGE_DIR="${GITHUB_WORKSPACE}/build/stage/${{ matrix.arch }}"
RELEASE_DIR="${GITHUB_WORKSPACE}/build/release/${{ matrix.arch }}"
VERIFY_DIR="${GITHUB_WORKSPACE}/build/mac-verification/${{ matrix.arch }}"
rm -rf "${STAGE_DIR}" "${RELEASE_DIR}" "${VERIFY_DIR}"
mkdir -p "${RELEASE_DIR}" "${VERIFY_DIR}"
cmake --install build --prefix "${STAGE_DIR}"
echo "APP_PATH=${STAGE_DIR}/PlatypusGui.app" >> "$GITHUB_ENV"
echo "DMG_PATH=${RELEASE_DIR}/${{ matrix.dmg_name }}" >> "$GITHUB_ENV"
echo "CHECKSUM_PATH=${RELEASE_DIR}/${{ matrix.checksum_name }}" >> "$GITHUB_ENV"
echo "VERIFY_DIR=${VERIFY_DIR}" >> "$GITHUB_ENV"
- name: Configure Photoshop CMake
shell: bash
run: |
set -euo pipefail
QT_PREFIX="$(brew --prefix qt)"
OPENCV_PREFIX="$(brew --prefix opencv)"
cmake -B build-photoshop -S . -G Ninja \
-DCMAKE_BUILD_TYPE=Release \
-DBUILD_PHOTOSHOP_PLUGIN=ON \
-DBUILD_TESTING=OFF \
-DCMAKE_OSX_ARCHITECTURES="${{ matrix.cmake_arch }}" \
-DPLATYPUS_PACKAGE_VERSION_OVERRIDE="${{ needs.prepare.outputs.package_version }}" \
-DCMAKE_PREFIX_PATH="${QT_PREFIX};${OPENCV_PREFIX}" \
-DOpenCV_DIR="${OPENCV_PREFIX}/lib/cmake/opencv4"
- name: Build Photoshop release payload
shell: bash
run: cmake --build build-photoshop --config Release --target stage_photoshop_release
- name: Set Photoshop release paths
shell: bash
run: |
set -euo pipefail
PHOTOSHOP_RELEASE_DIR="${GITHUB_WORKSPACE}/build/release/${{ matrix.arch }}"
PHOTOSHOP_VERIFY_DIR="${GITHUB_WORKSPACE}/build/mac-photoshop-verification/${{ matrix.arch }}"
PHOTOSHOP_PAYLOAD_DIR="${GITHUB_WORKSPACE}/build-photoshop/photoshop-release/photoshop/Platypus v${{ needs.prepare.outputs.package_version }}"
rm -rf "${PHOTOSHOP_VERIFY_DIR}"
mkdir -p "${PHOTOSHOP_VERIFY_DIR}"
echo "PHOTOSHOP_PAYLOAD_DIR=${PHOTOSHOP_PAYLOAD_DIR}" >> "$GITHUB_ENV"
echo "PHOTOSHOP_APP_PATH=${PHOTOSHOP_PAYLOAD_DIR}/Platypus.app" >> "$GITHUB_ENV"
echo "PHOTOSHOP_PLUGIN_PATH=${PHOTOSHOP_PAYLOAD_DIR}/Platypus.plugin" >> "$GITHUB_ENV"
echo "PHOTOSHOP_DMG_PATH=${PHOTOSHOP_RELEASE_DIR}/${{ matrix.photoshop_dmg_name }}" >> "$GITHUB_ENV"
echo "PHOTOSHOP_CHECKSUM_PATH=${PHOTOSHOP_RELEASE_DIR}/${{ matrix.photoshop_checksum_name }}" >> "$GITHUB_ENV"
echo "PHOTOSHOP_VERIFY_DIR=${PHOTOSHOP_VERIFY_DIR}" >> "$GITHUB_ENV"
- name: Verify Photoshop payload architecture
shell: bash
run: |
set -euo pipefail
PS_APP_BIN="${PHOTOSHOP_APP_PATH}/Contents/MacOS/Platypus"
PS_PLUGIN_BIN="${PHOTOSHOP_PLUGIN_PATH}/Contents/MacOS/Platypus"
PS_APP_ARCHS="$(lipo -archs "$PS_APP_BIN")"
PS_PLUGIN_ARCHS="$(lipo -archs "$PS_PLUGIN_BIN")"
echo "Photoshop app architectures: $PS_APP_ARCHS"
echo "Photoshop plugin architectures: $PS_PLUGIN_ARCHS"
[[ "$PS_APP_ARCHS" == *"${{ matrix.cmake_arch }}"* ]] || {
echo "Photoshop companion app is missing expected architecture ${{ matrix.cmake_arch }}" >&2
exit 1
}
[[ "$PS_PLUGIN_ARCHS" == *"${{ matrix.cmake_arch }}"* ]] || {
echo "Photoshop plugin is missing expected architecture ${{ matrix.cmake_arch }}" >&2
exit 1
}
- name: Import Apple signing certificate
shell: bash
env:
MACOS_CERT_P12_BASE64: ${{ secrets.MACOS_CERT_P12_BASE64 }}
MACOS_CERT_P12_PASSWORD: ${{ secrets.MACOS_CERT_P12_PASSWORD }}
run: |
set -euo pipefail
CERT_PATH="$RUNNER_TEMP/dev_app.p12"
KEYCHAIN_PATH="$RUNNER_TEMP/platypus-build.keychain-db"
KEYCHAIN_PASSWORD="$(uuidgen)"
echo "$MACOS_CERT_P12_BASE64" | 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 "$MACOS_CERT_P12_PASSWORD" -T /usr/bin/codesign -T /usr/bin/security
security list-keychains -d user -s "$KEYCHAIN_PATH" $(security list-keychains -d user | tr -d '"')
security default-keychain -d user -s "$KEYCHAIN_PATH"
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
echo "KEYCHAIN_PATH=$KEYCHAIN_PATH" >> "$GITHUB_ENV"
- name: Materialize App Store Connect API key
shell: bash
env:
APPSTORE_CONNECT_API_KEY_BASE64: ${{ secrets.APPSTORE_CONNECT_API_KEY_BASE64 }}
run: |
set -euo pipefail
API_KEY_PATH="$RUNNER_TEMP/AuthKey_${{ secrets.APPSTORE_CONNECT_API_KEY_ID }}.p8"
echo "$APPSTORE_CONNECT_API_KEY_BASE64" | base64 --decode > "$API_KEY_PATH"
echo "APPSTORE_CONNECT_API_KEY_PATH=$API_KEY_PATH" >> "$GITHUB_ENV"
- name: Sign app bundle
shell: bash
run: |
set -euo pipefail
bash scripts/mac_sign_app.sh \
--app "${APP_PATH}" \
--identity "${{ vars.APPLE_SIGNING_IDENTITY }}" \
--skip-deploy
- name: Sign Photoshop companion app
shell: bash
run: |
set -euo pipefail
bash scripts/mac_sign_bundle.sh \
--bundle "${PHOTOSHOP_APP_PATH}" \
--identity "${{ vars.APPLE_SIGNING_IDENTITY }}"
- name: Sign Photoshop plugin bundle
shell: bash
run: |
set -euo pipefail
bash scripts/mac_sign_bundle.sh \
--bundle "${PHOTOSHOP_PLUGIN_PATH}" \
--identity "${{ vars.APPLE_SIGNING_IDENTITY }}"
- name: Verify app bundle
shell: bash
run: |
set -euo pipefail
bash scripts/mac_verify_release.sh \
--phase pre-notarize \
--app "${APP_PATH}" \
--report-dir "${VERIFY_DIR}/pre-notarize"
- name: Verify Photoshop payload
shell: bash
run: |
set -euo pipefail
bash scripts/mac_verify_photoshop_release.sh \
--phase pre-notarize \
--payload-dir "${PHOTOSHOP_PAYLOAD_DIR}" \
--report-dir "${PHOTOSHOP_VERIFY_DIR}/pre-notarize"
- name: Create signed DMG
shell: bash
run: |
set -euo pipefail
bash scripts/mac_package_dmg.sh \
--app "${APP_PATH}" \
--dmg "${DMG_PATH}" \
--identity "${{ vars.APPLE_SIGNING_IDENTITY }}" \
--volname "Platypus"
- name: Create signed Photoshop DMG
shell: bash
run: |
set -euo pipefail
bash scripts/mac_package_dir_dmg.sh \
--source-dir "${PHOTOSHOP_PAYLOAD_DIR}" \
--dmg "${PHOTOSHOP_DMG_PATH}" \
--identity "${{ vars.APPLE_SIGNING_IDENTITY }}" \
--volname "Platypus Photoshop"
- name: Notarize DMG
shell: bash
run: |
set -euo pipefail
xcrun notarytool submit "${DMG_PATH}" \
--key "$APPSTORE_CONNECT_API_KEY_PATH" \
--key-id "${{ secrets.APPSTORE_CONNECT_API_KEY_ID }}" \
--issuer "${{ secrets.APPSTORE_CONNECT_API_ISSUER_ID }}" \
--wait
- name: Notarize Photoshop DMG
shell: bash
run: |
set -euo pipefail
xcrun notarytool submit "${PHOTOSHOP_DMG_PATH}" \
--key "$APPSTORE_CONNECT_API_KEY_PATH" \
--key-id "${{ secrets.APPSTORE_CONNECT_API_KEY_ID }}" \
--issuer "${{ secrets.APPSTORE_CONNECT_API_ISSUER_ID }}" \
--wait
- name: Staple DMG
shell: bash
run: |
set -euo pipefail
xcrun stapler staple "${DMG_PATH}"
- name: Staple Photoshop DMG
shell: bash
run: |
set -euo pipefail
xcrun stapler staple "${PHOTOSHOP_DMG_PATH}"
- name: Verify final release artifacts
shell: bash
run: |
set -euo pipefail
bash scripts/mac_verify_release.sh \
--phase post-staple \
--app "${APP_PATH}" \
--dmg "${DMG_PATH}" \
--report-dir "${VERIFY_DIR}/post-staple"
- name: Verify final Photoshop release artifacts
shell: bash
run: |
set -euo pipefail
bash scripts/mac_verify_photoshop_release.sh \
--phase post-staple \
--payload-dir "${PHOTOSHOP_PAYLOAD_DIR}" \
--dmg "${PHOTOSHOP_DMG_PATH}" \
--report-dir "${PHOTOSHOP_VERIFY_DIR}/post-staple"
- name: Generate checksum
shell: bash
run: |
set -euo pipefail
shasum -a 256 "${DMG_PATH}" > "${CHECKSUM_PATH}"
- name: Generate Photoshop checksum
shell: bash
run: |
set -euo pipefail
shasum -a 256 "${PHOTOSHOP_DMG_PATH}" > "${PHOTOSHOP_CHECKSUM_PATH}"
- name: Upload workflow artifacts
uses: actions/upload-artifact@v4
with:
name: macos-${{ matrix.arch }}-release
path: |
build/release/${{ matrix.arch }}/${{ matrix.dmg_name }}
build/release/${{ matrix.arch }}/${{ matrix.checksum_name }}
build/mac-verification/${{ matrix.arch }}
build/release/${{ matrix.arch }}/${{ matrix.photoshop_dmg_name }}
build/release/${{ matrix.arch }}/${{ matrix.photoshop_checksum_name }}
build/mac-photoshop-verification/${{ matrix.arch }}
- name: Cleanup temporary keychain
if: always()
shell: bash
run: |
set +e
if [[ -n "${KEYCHAIN_PATH:-}" ]]; then
security delete-keychain "$KEYCHAIN_PATH"
fi
publish:
name: Publish GitHub Release
needs:
- prepare
- build
runs-on: ubuntu-latest
env:
GH_TOKEN: ${{ github.token }}
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
ref: ${{ github.event.inputs.target_ref }}
- name: Download release artifacts
uses: actions/download-artifact@v4
with:
path: dist
- name: Create GitHub Release
shell: bash
run: |
set -euo pipefail
TAG="${{ github.event.inputs.tag }}"
mapfile -t RELEASE_FILES < <(find dist -type f \( -name '*.dmg' -o -name '*.sha256' \) | sort)
if [[ "${#RELEASE_FILES[@]}" -eq 0 ]]; then
echo "No release artifacts were downloaded." >&2
exit 1
fi
RELEASE_ARGS=()
if [[ "${{ github.event.inputs.prerelease }}" == "true" ]]; then
RELEASE_ARGS+=(--prerelease)
fi
gh release create "$TAG" \
"${RELEASE_FILES[@]}" \
--target "${{ github.event.inputs.target_ref }}" \
--title "$TAG" \
--generate-notes \
"${RELEASE_ARGS[@]}"