diff --git a/.github/actions/build-macos-package/action.yml b/.github/actions/build-macos-package/action.yml new file mode 100644 index 0000000..ce5ce53 --- /dev/null +++ b/.github/actions/build-macos-package/action.yml @@ -0,0 +1,219 @@ +name: Build macOS Package +description: Build macOS installer with optional signing and notarization + +inputs: + version: + description: Version of the application + required: true + app-name: + description: Name of the application + required: true + python-version: + description: Python version to use + required: true + pyoxidizer-version: + description: PyOxidizer version to use + required: true + should-sign: + description: Whether to sign and notarize the binaries + default: true + required: false + apple-certificate: + description: Apple Developer ID Application Certificate + required: false + apple-private-key: + description: Apple Developer ID Application Private Key + required: false + apple-api-key: + description: Apple App Store Connect API Key + required: false + +runs: + using: composite + steps: + - name: Checkout code + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + + - name: Set up Python ${{ inputs.python-version }} + uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v.6.0.0 + with: + python-version: ${{ inputs.python-version }} + cache: pip + + - name: Install PyOxidizer ${{ inputs.pyoxidizer-version }} + shell: bash + run: pip install pyoxidizer==${{ inputs.pyoxidizer-version }} + + - name: Install create-dmg + shell: bash + run: brew install create-dmg + + - name: Install rcodesign + shell: bash + run: cargo install apple-codesign + + - name: Download staged binaries + uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0 + with: + pattern: staged-*apple* + path: archives + merge-multiple: true + + - name: Extract staged binaries + shell: bash + run: |- + mkdir bin + for f in archives/*; do + tar -xzf "$f" -C bin + done + + - name: Write credentials + if: inputs.should-sign == 'true' + shell: bash + env: + APPLE_DEVELOPER_ID_APPLICATION_CERTIFICATE: ${{ inputs.apple-certificate }} + APPLE_DEVELOPER_ID_APPLICATION_PRIVATE_KEY: ${{ inputs.apple-private-key }} + APPLE_APP_STORE_CONNECT_API_DATA: ${{ inputs.apple-api-key }} + run: |- + echo "$APPLE_DEVELOPER_ID_APPLICATION_CERTIFICATE" > /tmp/certificate.pem + echo "$APPLE_DEVELOPER_ID_APPLICATION_PRIVATE_KEY" > /tmp/private-key.pem + echo "$APPLE_APP_STORE_CONNECT_API_DATA" > /tmp/app-store-connect.json + + # https://developer.apple.com/documentation/security/hardened_runtime + - name: Sign binaries + if: inputs.should-sign == 'true' + shell: bash + run: |- + for f in bin/*; do + rcodesign sign -vv \ + --pem-source /tmp/certificate.pem \ + --pem-source /tmp/private-key.pem \ + --code-signature-flags runtime \ + "$f" + done + + # https://developer.apple.com/documentation/security/notarizing_macos_software_before_distribution + - name: Notarize binaries + if: inputs.should-sign == 'true' + shell: bash + run: |- + mkdir notarize-bin + + cd bin + for f in *; do + zip "../notarize-bin/$f.zip" "$f" + done + + cd ../notarize-bin + for f in *; do + rcodesign notary-submit -vv \ + --api-key-path /tmp/app-store-connect.json \ + "$f" + done + + - name: Archive binaries + shell: bash + run: |- + rm archives/* + cd bin + + for f in *; do + tar -czf "../archives/$f.tar.gz" "$f" + done + + # bin/-- -> targets// + - name: Prepare binaries + shell: bash + run: |- + mkdir targets + for f in bin/*; do + if [[ "$f" =~ ${{ inputs.version }}-(.+)$ ]]; then + target="${BASH_REMATCH[1]}" + mkdir "targets/$target" + mv "$f" "targets/$target/${{ inputs.app-name }}" + fi + done + + - name: Build app bundle + shell: bash + run: >- + pyoxidizer build macos_app_bundle + --release + --var version ${{ inputs.version }} + + - name: Stage app bundle + id: stage + shell: bash + run: |- + mkdir staged + mkdir signed + mv build/*/release/*/*.app staged + app_bundle="$(ls staged)" + app_name="${app_bundle:0:${#app_bundle}-4}" + + echo "app-bundle=$app_bundle" >> "$GITHUB_OUTPUT" + echo "app-name=$app_name-${{ inputs.version }}.dmg" >> "$GITHUB_OUTPUT" + echo "dmg-file=$app_name-${{ inputs.version }}.dmg" >> "$GITHUB_OUTPUT" + + - name: Sign app bundle + if: inputs.should-sign == 'true' + shell: bash + run: >- + rcodesign sign -vv + --pem-source /tmp/certificate.pem + --pem-source /tmp/private-key.pem + "staged/${{ steps.stage.outputs.app-bundle }}" + "signed/${{ steps.stage.outputs.app-bundle }}" + + - name: Copy unsigned app bundle + if: inputs.should-sign != 'true' + shell: bash + run: >- + cp -R + "staged/${{ steps.stage.outputs.app-bundle }}" + "signed/${{ steps.stage.outputs.app-bundle }}" + + - name: Create DMG + shell: bash + run: >- + create-dmg + --volname "${{ steps.stage.outputs.app-name }}" + --hide-extension "${{ steps.stage.outputs.app-bundle }}" + --window-pos 200 120 + --window-size 800 400 + --icon-size 100 + --app-drop-link 600 185 + "${{ steps.stage.outputs.dmg-file }}" + signed + + - name: Sign DMG + if: inputs.should-sign == 'true' + shell: bash + run: >- + rcodesign sign -vv + --pem-source /tmp/certificate.pem + --pem-source /tmp/private-key.pem + "${{ steps.stage.outputs.dmg-file }}" + "${{ steps.stage.outputs.dmg-file }}" + + - name: Notarize DMG + if: inputs.should-sign == 'true' + shell: bash + run: >- + rcodesign notary-submit + --api-key-path /tmp/app-store-connect.json + --staple + "${{ steps.stage.outputs.dmg-file }}" + + - name: Upload binaries + uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 + with: + name: standalone-mac + path: archives/* + if-no-files-found: error + + - name: Upload installer + uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 + with: + name: installers-mac + path: ${{ steps.stage.outputs.dmg-file }} diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 6a64a70..85dcd5a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -267,11 +267,13 @@ jobs: name: installers-win path: installers/* - macos-packaging: - name: Build macOS installer and sign/notarize artifacts - if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name == github.repository + macos-packaging-signed: + name: Build macOS installer and sign/notarize artifacts on protected branch/tag + if: github.event_name == 'push' needs: binaries runs-on: macos-15 + environment: + name: protected env: VERSION: ${{ needs.binaries.outputs.version }} @@ -279,173 +281,37 @@ jobs: steps: - name: Checkout code uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 - - - name: Set up Python ${{ env.PYTHON_VERSION }} - uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v.6.1.0 + - name: Build and sign macOS package + uses: ./.github/actions/build-macos-package with: + version: ${{ env.VERSION }} + app-name: ${{ env.APP_NAME }} python-version: ${{ env.PYTHON_VERSION }} + pyoxidizer-version: ${{ env.PYOXIDIZER_VERSION }} + apple-certificate: ${{ secrets.APPLE_DEVELOPER_ID_APPLICATION_CERTIFICATE }} + apple-private-key: ${{ secrets.APPLE_DEVELOPER_ID_APPLICATION_PRIVATE_KEY }} + apple-api-key: ${{ secrets.APPLE_APP_STORE_CONNECT_API_DATA }} + + macos-packaging-unsigned: + name: Build macOS installer without signing + if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository + needs: binaries + runs-on: macos-15 - - name: Install PyOxidizer ${{ env.PYOXIDIZER_VERSION }} - run: pip install pyoxidizer==${{ env.PYOXIDIZER_VERSION }} - - - name: Install create-dmg - run: brew install create-dmg - - - name: Install rcodesign - run: cargo install apple-codesign - - - name: Download staged binaries - uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0 - with: - pattern: staged-*apple* - path: archives - merge-multiple: true - - - name: Extract staged binaries - run: |- - mkdir bin - for f in archives/*; do - tar -xzf "$f" -C bin - done - - - name: Write credentials - if: github.event_name == 'push' - env: - APPLE_DEVELOPER_ID_APPLICATION_CERTIFICATE: "${{ secrets.APPLE_DEVELOPER_ID_APPLICATION_CERTIFICATE }}" - APPLE_DEVELOPER_ID_APPLICATION_PRIVATE_KEY: "${{ secrets.APPLE_DEVELOPER_ID_APPLICATION_PRIVATE_KEY }}" - APPLE_APP_STORE_CONNECT_API_DATA: "${{ secrets.APPLE_APP_STORE_CONNECT_API_DATA }}" - run: |- - echo "$APPLE_DEVELOPER_ID_APPLICATION_CERTIFICATE" > /tmp/certificate.pem - echo "$APPLE_DEVELOPER_ID_APPLICATION_PRIVATE_KEY" > /tmp/private-key.pem - echo "$APPLE_APP_STORE_CONNECT_API_DATA" > /tmp/app-store-connect.json - - # https://developer.apple.com/documentation/security/hardened_runtime - - name: Sign binaries - if: github.event_name == 'push' - run: |- - for f in bin/*; do - rcodesign sign -vv \ - --pem-source /tmp/certificate.pem \ - --pem-source /tmp/private-key.pem \ - --code-signature-flags runtime \ - "$f" - done - - # https://developer.apple.com/documentation/security/notarizing_macos_software_before_distribution - - name: Notarize binaries - if: github.event_name == 'push' - run: |- - mkdir notarize-bin - - cd bin - for f in *; do - zip "../notarize-bin/$f.zip" "$f" - done - - cd ../notarize-bin - for f in *; do - rcodesign notary-submit -vv \ - --api-key-path /tmp/app-store-connect.json \ - "$f" - done - - - name: Archive binaries - run: |- - rm archives/* - cd bin - - for f in *; do - tar -czf "../archives/$f.tar.gz" "$f" - done - - # bin/-- -> targets// - - name: Prepare binaries - run: |- - mkdir targets - for f in bin/*; do - if [[ "$f" =~ ${{ env.VERSION }}-(.+)$ ]]; then - target="${BASH_REMATCH[1]}" - mkdir "targets/$target" - mv "$f" "targets/$target/${{ env.APP_NAME }}" - fi - done - - - name: Build app bundle - run: >- - pyoxidizer build macos_app_bundle - --release - --var version ${{ env.VERSION }} - - - name: Stage app bundle - id: stage - run: |- - mkdir staged - mkdir signed - mv build/*/release/*/*.app staged - app_bundle="$(ls staged)" - app_name="${app_bundle:0:${#app_bundle}-4}" - - echo "app-bundle=$app_bundle" >> "$GITHUB_OUTPUT" - echo "app-name=$app_name-${{ env.VERSION }}.dmg" >> "$GITHUB_OUTPUT" - echo "dmg-file=$app_name-${{ env.VERSION }}.dmg" >> "$GITHUB_OUTPUT" - - - name: Sign app bundle - if: github.event_name == 'push' - run: >- - rcodesign sign -vv - --pem-source /tmp/certificate.pem - --pem-source /tmp/private-key.pem - "staged/${{ steps.stage.outputs.app-bundle }}" - "signed/${{ steps.stage.outputs.app-bundle }}" - - - name: Copy unsigned app bundle - if: github.event_name != 'push' - run: >- - cp -R - "staged/${{ steps.stage.outputs.app-bundle }}" - "signed/${{ steps.stage.outputs.app-bundle }}" - - - name: Create DMG - run: >- - create-dmg - --volname "${{ steps.stage.outputs.app-name }}" - --hide-extension "${{ steps.stage.outputs.app-bundle }}" - --window-pos 200 120 - --window-size 800 400 - --icon-size 100 - --app-drop-link 600 185 - "${{ steps.stage.outputs.dmg-file }}" - signed - - - name: Sign DMG - if: github.event_name == 'push' - run: >- - rcodesign sign -vv - --pem-source /tmp/certificate.pem - --pem-source /tmp/private-key.pem - "${{ steps.stage.outputs.dmg-file }}" - "${{ steps.stage.outputs.dmg-file }}" - - - name: Notarize DMG - if: github.event_name == 'push' - run: >- - rcodesign notary-submit - --api-key-path /tmp/app-store-connect.json - --staple - "${{ steps.stage.outputs.dmg-file }}" - - - name: Upload binaries - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 - with: - name: standalone-mac - path: archives/* - if-no-files-found: error + env: + VERSION: ${{ needs.binaries.outputs.version }} - - name: Upload installer - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 + steps: + - name: Checkout code + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + - name: Build macOS package + uses: ./.github/actions/build-macos-package with: - name: installers-mac - path: ${{ steps.stage.outputs.dmg-file }} + version: ${{ env.VERSION }} + app-name: ${{ env.APP_NAME }} + python-version: ${{ env.PYTHON_VERSION }} + pyoxidizer-version: ${{ env.PYOXIDIZER_VERSION }} + should-sign: 'false' publish: name: Publish release @@ -454,7 +320,7 @@ jobs: - python-artifacts - binaries - windows-packaging - - macos-packaging + - macos-packaging-signed runs-on: ubuntu-latest permissions: id-token: write