--- name: CI-macOS permissions: {} on: workflow_call: inputs: publish_release: required: true type: string release_commit: required: true type: string release_version: required: true type: string secrets: # email address APPLE_ID: required: false # 10-character Team ID APPLE_TEAM_ID: required: false # app-specific password in APPLE_ID's account that must be named "notarytool" # https://support.apple.com/en-us/102654 APPLE_NOTARYTOOL_PASSWORD: required: false # Developer ID Application: Full Name (TEAMIDHERE) APPLE_CODESIGN_IDENTITY: required: false # pkcs12 export from Xcode in base64 APPLE_DEVELOPER_ID_APPLICATION_CERTIFICATE_BASE64: required: false # pkcs12 password added by Xcode export APPLE_DEVELOPER_ID_APPLICATION_CERTIFICATE_P12_PASSWORD: required: false env: BRANCH: ${{ github.head_ref || github.ref_name }} BUILD_VERSION: ${{ inputs.release_version }} COMMIT: ${{ inputs.release_commit }} MACOSX_DEPLOYMENT_TARGET: 14.2 PYTHON_VERSION: '3.14' jobs: build_dmg: name: ${{ matrix.name }} permissions: contents: read runs-on: ${{ matrix.os }} outputs: notarytool_submission_id_arm64: ${{ steps.notarize_submit.outputs.submission_id_arm64 }} notarytool_submission_id_x86_64: ${{ steps.notarize_submit.outputs.submission_id_x86_64 }} strategy: fail-fast: false matrix: include: - os: "macos-14" name: "macOS-arm64" arch: "arm64" - os: "macos-15-intel" name: "macOS-x86_64" arch: "x86_64" steps: - name: Install Apple certificate uses: apple-actions/import-codesign-certs@5142e029c445c10ffc7149d172e540235a065466 # v7.0.0 if: inputs.publish_release == 'true' with: p12-file-base64: ${{ secrets.APPLE_DEVELOPER_ID_APPLICATION_CERTIFICATE_BASE64 }} p12-password: ${{ secrets.APPLE_DEVELOPER_ID_APPLICATION_CERTIFICATE_P12_PASSWORD }} - name: Checkout uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: submodules: recursive - name: Select Xcode 16.2 if: matrix.os == 'macos-14' run: sudo xcode-select --switch /Applications/Xcode_16.2.app - name: Install dependencies timeout-minutes: 5 run: | brew install --force \ cmake \ doxygen \ graphviz \ node \ pkgconf \ icu4c@78 \ miniupnpc \ openssl@3 \ opus \ qtbase \ qtsvg - name: Configure macOS screen recording uses: LizardByte/tray/.github/actions/configure-macos-screen-recording@master - name: Setup Python uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: ${{ env.PYTHON_VERSION }} - name: Setup uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 with: enable-cache: true - name: Sync Python tools run: | uv sync --locked \ --python "${PYTHON_VERSION}" \ --no-python-downloads --no-install-project - name: Configure env: APPLE_CODESIGN_IDENTITY: ${{ secrets.APPLE_CODESIGN_IDENTITY }} run: | mkdir -p build cmake \ -B build \ -S . \ -DBUILD_WERROR=ON \ -DCMAKE_BUILD_TYPE=Release \ -DICU_ROOT="$(brew --prefix icu4c@78 2>/dev/null)" \ -DOPENSSL_ROOT_DIR="$(brew --prefix openssl@3 2>/dev/null)" \ -DOpus_ROOT_DIR="$(brew --prefix opus 2>/dev/null)" \ -DSUNSHINE_PUBLISHER_NAME="${GITHUB_REPOSITORY_OWNER}" \ -DSUNSHINE_PUBLISHER_WEBSITE="https://app.lizardbyte.dev" \ -DSUNSHINE_PUBLISHER_ISSUE_URL="https://app.lizardbyte.dev/support" \ -DAPPLE_CODESIGN_IDENTITY="${APPLE_CODESIGN_IDENTITY}" - name: Build run: | echo "::add-matcher::.github/matchers/gcc.json" cmake --build build -j "$(sysctl -n hw.ncpu)" echo "::remove-matcher owner=gcc::" - name: Package DMG env: APPLE_CODESIGN_IDENTITY: ${{ secrets.APPLE_CODESIGN_IDENTITY }} MATRIX_NAME: ${{ matrix.name }} SHOULD_SIGN: ${{ inputs.publish_release }} run: | # build DMG and sign everything (see cmake/packaging/macos.cmake) # cpack can rarely fail with "hdiutil: create failed - Resource busy" # so let's allow 1 retry if ! cpack -G DragNDrop --config build/CPackConfig.cmake; then echo "cpack failed, retrying once with verbose..." if ! cpack -G DragNDrop --config build/CPackConfig.cmake --verbose; then echo "cpack failed again. Aborting." exit 1 fi fi mkdir -p artifacts cp "build/cpack_artifacts/Sunshine.dmg" "artifacts/Sunshine-${MATRIX_NAME}.dmg" - name: Submit for notarization id: notarize_submit if: inputs.publish_release == 'true' env: APPLE_ID: ${{ secrets.APPLE_ID }} APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} APPLE_NOTARYTOOL_PASSWORD: ${{ secrets.APPLE_NOTARYTOOL_PASSWORD }} MATRIX_ARCH: ${{ matrix.arch }} run: | # Notarizing allows the signed .app to run on any Mac with no prompts. # If you don't notarize, users must jump through the "Open Anyway" hoop as well as run # `xattr -cr /Applications/Sunshine.app` to remove quarantine. if [[ -n "${APPLE_NOTARYTOOL_PASSWORD}" ]]; then submission_id=$(xcrun notarytool submit build/cpack_artifacts/Sunshine.dmg \ --apple-id "${APPLE_ID}" \ --team-id "${APPLE_TEAM_ID}" \ --password "${APPLE_NOTARYTOOL_PASSWORD}" \ --output-format json \ | jq -r '.id') echo "Submission ID: ${submission_id}" echo "submission_id_${MATRIX_ARCH}=${submission_id}" >> "${GITHUB_OUTPUT}" fi - name: Test id: test working-directory: build/tests run: ./test_sunshine --gtest_color=yes --gtest_output=xml:test_results.xml - name: Upload system tray screenshots if: >- always() && (steps.test.outcome == 'success' || steps.test.outcome == 'failure') uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: sunshine-screenshots-${{ matrix.name }} path: build/tests/screenshots if-no-files-found: error - name: Generate gcov report id: test_report # any except canceled or skipped if: >- always() && (steps.test.outcome == 'success' || steps.test.outcome == 'failure') working-directory: build run: | uv run --project .. --locked --no-sync gcovr . -r ../src \ --exclude-noncode-lines \ --exclude-throw-branches \ --exclude-unreachable-branches \ --xml-pretty \ -j "$(sysctl -n hw.ncpu)" \ -o coverage.xml - name: Upload coverage artifact if: >- always() && (steps.test_report.outcome == 'success') uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: coverage-${{ matrix.name }} path: | build/coverage.xml build/tests/test_results.xml if-no-files-found: error - name: Set artifact prefix id: artifact_prefix env: INPUTS_PUBLISH_RELEASE: ${{ inputs.publish_release }} run: | if [[ "${INPUTS_PUBLISH_RELEASE}" == "true" ]]; then echo "prefix=unsigned" >> "${GITHUB_OUTPUT}" else echo "prefix=build" >> "${GITHUB_OUTPUT}" fi - name: Upload Artifacts uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: ${{ steps.artifact_prefix.outputs.prefix }}-${{ matrix.name }} path: artifacts/ if-no-files-found: error notarize_dmg: name: Notarize ${{ matrix.name }} needs: build_dmg if: inputs.publish_release == 'true' permissions: contents: read runs-on: ${{ matrix.os }} strategy: fail-fast: false matrix: include: - os: "macos-14" name: "macOS-arm64" arch: "arm64" - os: "macos-15-intel" name: "macOS-x86_64" arch: "x86_64" steps: - name: Install Apple certificate uses: apple-actions/import-codesign-certs@5142e029c445c10ffc7149d172e540235a065466 # v7.0.0 with: p12-file-base64: ${{ secrets.APPLE_DEVELOPER_ID_APPLICATION_CERTIFICATE_BASE64 }} p12-password: ${{ secrets.APPLE_DEVELOPER_ID_APPLICATION_CERTIFICATE_P12_PASSWORD }} - name: Download DMG artifact uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: name: unsigned-${{ matrix.name }} path: artifacts - name: Wait for notarization and staple env: APPLE_ID: ${{ secrets.APPLE_ID }} APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} APPLE_NOTARYTOOL_PASSWORD: ${{ secrets.APPLE_NOTARYTOOL_PASSWORD }} MATRIX_NAME: ${{ matrix.name }} SUBMISSION_ID: ${{ matrix.arch == 'arm64' && needs.build_dmg.outputs.notarytool_submission_id_arm64 || needs.build_dmg.outputs.notarytool_submission_id_x86_64 }} run: | if [[ -z "${SUBMISSION_ID}" ]]; then echo "No submission ID found; skipping notarization wait." exit 0 fi echo "Polling notarization status for submission: ${SUBMISSION_ID}" while true; do status=$(xcrun notarytool info "${SUBMISSION_ID}" \ --apple-id "${APPLE_ID}" \ --team-id "${APPLE_TEAM_ID}" \ --password "${APPLE_NOTARYTOOL_PASSWORD}" \ --output-format json \ | jq -r '.status') echo "Current status: ${status}" if [[ "${status}" == "Accepted" ]]; then echo "Notarization accepted." break elif [[ "${status}" == "Invalid" || "${status}" == "Rejected" ]]; then echo "Notarization failed with status: ${status}" # Print the full log for debugging xcrun notarytool log "${SUBMISSION_ID}" \ --apple-id "${APPLE_ID}" \ --team-id "${APPLE_TEAM_ID}" \ --password "${APPLE_NOTARYTOOL_PASSWORD}" exit 1 fi echo "Status is '${status}', waiting 30 seconds before retrying..." sleep 30 done xcrun stapler staple -v "artifacts/Sunshine-${MATRIX_NAME}.dmg" - name: Upload stapled artifact uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: build-${{ matrix.name }} path: artifacts/ if-no-files-found: error