Skip to content

Iterate to get macOS CI/CD build to work #22

Iterate to get macOS CI/CD build to work

Iterate to get macOS CI/CD build to work #22

Workflow file for this run

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