Iterate to get macOS CI/CD build to work #22
Workflow file for this run
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: Build macOS App | |
| on: | |
| push: | |
| branches: | |
| - feature/cdMacOsApp | |
| concurrency: ci-macos-${{ github.ref }} | |
| jobs: | |
| macos-build: | |
| runs-on: macos-14 | |
| permissions: | |
| contents: read | |
| defaults: | |
| run: | |
| shell: bash | |
| working-directory: ${{ github.workspace }} | |
| env: | |
| NOTARY_PROFILE: interspec-notary-${{ github.run_id }}-${{ github.run_attempt }} | |
| MAC_DEVELOPER_TEAM_ID: ${{ secrets.MAC_NOTARY_TEAM_ID }} | |
| MAC_SIGNING_IDENTITY: ${{ secrets.MAC_SIGNING_IDENTITY }} | |
| steps: | |
| - name: Check out repository code | |
| uses: actions/checkout@v4 | |
| with: | |
| submodules: true | |
| - name: Define build paths | |
| run: | | |
| echo "BUILD_DIR=${RUNNER_TEMP}/build_macos" >> "${GITHUB_ENV}" | |
| echo "DEPS_BUILD_DIR=${RUNNER_TEMP}/mac_deps_build" >> "${GITHUB_ENV}" | |
| echo "DEPS_PREFIX_DIR=${RUNNER_TEMP}/mac_deps_prefix" >> "${GITHUB_ENV}" | |
| echo "INSTALLER_DIR=${RUNNER_TEMP}/mac_artifacts" >> "${GITHUB_ENV}" | |
| echo "PAYLOAD_DIR=${RUNNER_TEMP}/mac_artifacts/InterSpec_macOS_latest" >> "${GITHUB_ENV}" | |
| - name: Prepare directories | |
| run: | | |
| mkdir -p "${DEPS_BUILD_DIR}" "${DEPS_PREFIX_DIR}" "${BUILD_DIR}" "${INSTALLER_DIR}" | |
| - name: Cache macOS dependencies | |
| id: cache-macos-deps | |
| uses: actions/cache@v4 | |
| with: | |
| path: ${{ env.DEPS_PREFIX_DIR }} | |
| key: macos-deps-${{ hashFiles('target/patches/dep_build_macOS.sh', 'target/patches/wt/3.7.1/NormalBuild/wt_3.7.1_git.patch') }} | |
| - name: Build dependency prefix | |
| if: steps.cache-macos-deps.outputs.cache-hit != 'true' | |
| run: | | |
| chmod +x target/patches/dep_build_macOS.sh | |
| ./target/patches/dep_build_macOS.sh "${{ github.workspace }}" "${DEPS_BUILD_DIR}" "${DEPS_PREFIX_DIR}" | |
| - name: Import signing certificate | |
| env: | |
| MAC_SIGNING_CERT_P12: ${{ secrets.MAC_SIGNING_CERT_P12 }} | |
| MAC_SIGNING_CERT_PASSWORD: ${{ secrets.MAC_SIGNING_CERT_PASSWORD }} | |
| run: | | |
| set -euo pipefail | |
| CERT_PATH="${RUNNER_TEMP}/signing_cert.p12" | |
| echo "${MAC_SIGNING_CERT_P12}" | base64 --decode > "${CERT_PATH}" | |
| KEYCHAIN_PATH="${RUNNER_TEMP}/interspec-build.keychain-db" | |
| KEYCHAIN_PASSWORD=$(uuidgen) | |
| 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 "${MAC_SIGNING_CERT_PASSWORD}" -T /usr/bin/codesign -T /usr/bin/security | |
| security set-key-partition-list -S apple-tool:,apple: -s -k "${KEYCHAIN_PASSWORD}" "${KEYCHAIN_PATH}" | |
| security list-keychains -d user -s "${KEYCHAIN_PATH}" ~/Library/Keychains/login.keychain-db | |
| rm -f "${CERT_PATH}" | |
| echo "SIGNING_KEYCHAIN=${KEYCHAIN_PATH}" >> "${GITHUB_ENV}" | |
| echo "KEYCHAIN_PASSWORD=${KEYCHAIN_PASSWORD}" >> "${GITHUB_ENV}" | |
| - name: Prepare notarization credentials | |
| env: | |
| MAC_NOTARY_KEY_CONTENTS: ${{ secrets.MAC_NOTARY_KEY_CONTENTS }} | |
| run: | | |
| set -euo pipefail | |
| NOTARY_KEY_PATH="${RUNNER_TEMP}/AuthKey.p8" | |
| echo "${MAC_NOTARY_KEY_CONTENTS}" | base64 --decode > "${NOTARY_KEY_PATH}" | |
| echo "NOTARY_KEY_PATH=${NOTARY_KEY_PATH}" >> "${GITHUB_ENV}" | |
| - name: Configure InterSpec (Xcode) | |
| env: | |
| LEAFLET_KEY: ${{ secrets.LEAFLET_KEY }} | |
| run: | | |
| cmake -S "${{ github.workspace }}/target/osx" \ | |
| -B "${BUILD_DIR}" \ | |
| -G Xcode \ | |
| -DCMAKE_PREFIX_PATH="${DEPS_PREFIX_DIR}" \ | |
| -DLEAFLET_MAPS_KEY="${LEAFLET_KEY}" \ | |
| -DCMAKE_OSX_ARCHITECTURES="x86_64;arm64" | |
| - name: Build InterSpec.app | |
| run: | | |
| cmake --build "${BUILD_DIR}" --config Release | |
| echo "APP_BUNDLE=${BUILD_DIR}/Release/InterSpec.app" >> "${GITHUB_ENV}" | |
| - name: Copy D3 resources | |
| run: | | |
| set -euo pipefail | |
| D3_SOURCE="${{ github.workspace }}/external_libs/SpecUtils/d3_resources" | |
| D3_DEST="${APP_BUNDLE}/Contents/Resources/InterSpec_resources" | |
| echo "Copying D3 resources from ${D3_SOURCE} to ${D3_DEST}" | |
| cp "${D3_SOURCE}/d3.v3.min.js" "${D3_DEST}/" | |
| cp "${D3_SOURCE}/SpectrumChartD3.js" "${D3_DEST}/" | |
| cp "${D3_SOURCE}/SpectrumChartD3.css" "${D3_DEST}/" | |
| ls -lh "${D3_DEST}/" | |
| - name: Codesign bundle | |
| env: | |
| MAC_SIGNING_IDENTITY: ${{ secrets.MAC_SIGNING_IDENTITY }} | |
| run: | | |
| set -euo pipefail | |
| # Sign the QuickLook plugin first (it's a nested bundle) | |
| QUICKLOOK_PLUGIN="${APP_BUNDLE}/Contents/Library/QuickLook/SpecFilePreview.qlgenerator" | |
| if [ -d "${QUICKLOOK_PLUGIN}" ]; then | |
| echo "Signing QuickLook plugin..." | |
| codesign --force --options runtime --timestamp \ | |
| --sign "${MAC_SIGNING_IDENTITY}" \ | |
| "${QUICKLOOK_PLUGIN}/Contents/MacOS/SpecFilePreview" | |
| codesign --force --options runtime --timestamp \ | |
| --sign "${MAC_SIGNING_IDENTITY}" \ | |
| "${QUICKLOOK_PLUGIN}" | |
| fi | |
| # Sign any frameworks in the bundle | |
| if [ -d "${APP_BUNDLE}/Contents/Frameworks" ]; then | |
| echo "Signing frameworks..." | |
| # Sign .dylib files first | |
| find "${APP_BUNDLE}/Contents/Frameworks" -type f -name "*.dylib" | while read -r dylib; do | |
| echo " Signing dylib: ${dylib}" | |
| codesign --force --options runtime --timestamp \ | |
| --sign "${MAC_SIGNING_IDENTITY}" \ | |
| "${dylib}" || true | |
| done | |
| # Then sign framework bundles (these are directories) | |
| find "${APP_BUNDLE}/Contents/Frameworks" -type d -name "*.framework" | while read -r framework; do | |
| echo " Signing framework: ${framework}" | |
| codesign --force --options runtime --timestamp \ | |
| --sign "${MAC_SIGNING_IDENTITY}" \ | |
| "${framework}" | |
| done | |
| fi | |
| # Sign all executables in Contents/MacOS (except the main one, which we'll sign with entitlements) | |
| if [ -d "${APP_BUNDLE}/Contents/MacOS" ]; then | |
| echo "Signing additional executables in MacOS directory..." | |
| find "${APP_BUNDLE}/Contents/MacOS" -type f -perm +111 ! -name "InterSpec" | while read -r executable; do | |
| echo " Signing: ${executable}" | |
| codesign --force --options runtime --timestamp \ | |
| --sign "${MAC_SIGNING_IDENTITY}" \ | |
| "${executable}" | |
| done | |
| fi | |
| # Sign the main app bundle (without --deep since we signed nested components) | |
| echo "Signing main app bundle..." | |
| codesign --force --options runtime --timestamp \ | |
| --entitlements "${{ github.workspace }}/target/osx/InterSpec.entitlements" \ | |
| --sign "${MAC_SIGNING_IDENTITY}" \ | |
| "${APP_BUNDLE}" | |
| # Verify the signature | |
| codesign --verify --deep --strict --verbose=2 "${APP_BUNDLE}" | |
| - name: Notarize bundle | |
| env: | |
| MAC_NOTARY_KEY_ID: ${{ secrets.MAC_NOTARY_KEY_ID }} | |
| MAC_NOTARY_KEY_ISSUER: ${{ secrets.MAC_NOTARY_KEY_ISSUER }} | |
| MAC_NOTARY_TEAM_ID: ${{ secrets.MAC_NOTARY_TEAM_ID }} | |
| run: | | |
| set -euo pipefail | |
| NOTARIZE_ARCHIVE="${INSTALLER_DIR}/InterSpec_notarize.zip" | |
| ditto -c -k --keepParent "${APP_BUNDLE}" "${NOTARIZE_ARCHIVE}" | |
| # Submit and capture the submission ID | |
| SUBMIT_OUTPUT=$(xcrun notarytool submit "${NOTARIZE_ARCHIVE}" \ | |
| --key "${NOTARY_KEY_PATH}" \ | |
| --key-id "${MAC_NOTARY_KEY_ID}" \ | |
| --issuer "${MAC_NOTARY_KEY_ISSUER}" \ | |
| --team-id "${MAC_NOTARY_TEAM_ID}" \ | |
| --wait --output-format json) | |
| echo "Notarization submission output:" | |
| echo "${SUBMIT_OUTPUT}" | |
| SUBMISSION_ID=$(echo "${SUBMIT_OUTPUT}" | grep -o '"id":"[^"]*"' | head -1 | cut -d'"' -f4) | |
| echo "Submission ID: ${SUBMISSION_ID}" | |
| # Get detailed log regardless of status | |
| if [ -n "${SUBMISSION_ID}" ]; then | |
| echo "Fetching notarization log..." | |
| xcrun notarytool log "${SUBMISSION_ID}" \ | |
| --key "${NOTARY_KEY_PATH}" \ | |
| --key-id "${MAC_NOTARY_KEY_ID}" \ | |
| --issuer "${MAC_NOTARY_KEY_ISSUER}" \ | |
| --team-id "${MAC_NOTARY_TEAM_ID}" || true | |
| fi | |
| # Check if notarization succeeded | |
| STATUS=$(echo "${SUBMIT_OUTPUT}" | grep -o '"status":"[^"]*"' | head -1 | cut -d'"' -f4) | |
| if [ "${STATUS}" != "Accepted" ]; then | |
| echo "Error: Notarization failed with status: ${STATUS}" | |
| exit 1 | |
| fi | |
| xcrun stapler staple "${APP_BUNDLE}" | |
| rm -f "${NOTARIZE_ARCHIVE}" | |
| - name: Prepare distribution payload | |
| run: | | |
| set -euo pipefail | |
| rm -rf "${PAYLOAD_DIR}" | |
| mkdir -p "${PAYLOAD_DIR}" | |
| cp -R "${APP_BUNDLE}" "${PAYLOAD_DIR}/" | |
| cp "${{ github.workspace }}/LICENSE.txt" "${PAYLOAD_DIR}/" | |
| cp "${{ github.workspace }}/NOTICE.html" "${PAYLOAD_DIR}/" | |
| { | |
| echo "This is an automated build of InterSpec built on the github infrastructure at $(date)" | |
| echo "" | |
| echo "No testing has been performed on this build." | |
| echo "" | |
| echo "InterSpec code git hash SHA: ${{ github.sha }}" | |
| echo "" | |
| echo "Contact InterSpec@sandia.gov for support" | |
| } > "${PAYLOAD_DIR}/build_info.txt" | |
| - name: Create DMG installer | |
| env: | |
| MAC_SIGNING_IDENTITY: ${{ secrets.MAC_SIGNING_IDENTITY }} | |
| run: | | |
| set -euo pipefail | |
| BUILD_DATE=$(date +%Y-%m-%d) | |
| DMG_PATH="${INSTALLER_DIR}/InterSpec_app_macOS_latest_${BUILD_DATE}.dmg" | |
| hdiutil create -volname "InterSpec" -srcfolder "${PAYLOAD_DIR}" -ov -format UDZO "${DMG_PATH}" | |
| codesign --force --options runtime --timestamp --sign "${MAC_SIGNING_IDENTITY}" "${DMG_PATH}" | |
| ls -lh "${INSTALLER_DIR}" | |
| echo "DMG_ARTIFACT=${DMG_PATH}" >> "${GITHUB_ENV}" | |
| - name: Upload macOS DMG | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: macos-app-dmg | |
| path: ${{ env.DMG_ARTIFACT }} | |
| if-no-files-found: error | |
| - name: Cleanup keychain and credentials | |
| if: ${{ always() }} | |
| run: | | |
| if [ -n "${SIGNING_KEYCHAIN:-}" ] && [ -f "${SIGNING_KEYCHAIN}" ]; then | |
| security delete-keychain "${SIGNING_KEYCHAIN}" || true | |
| fi | |
| if [ -n "${NOTARY_KEY_PATH:-}" ] && [ -f "${NOTARY_KEY_PATH}" ]; then | |
| rm -f "${NOTARY_KEY_PATH}" || true | |
| fi | |