Release macOS #11
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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[@]}" |