Skip to content

Release

Release #2

Workflow file for this run

name: Release
# Tagging v* builds, signs, notarizes and publishes the app. The same five steps
# that are documented for local releases in STRUCTURE.md, so the two stay in sync.
# A v* tag builds and publishes. A manual run does everything except publish,
# so the signing and notarization path can be rehearsed without shipping.
on:
push:
tags:
- 'v*'
workflow_dispatch:
permissions:
contents: write
jobs:
release:
runs-on: macos-15
steps:
- uses: actions/checkout@v7
- name: Select the newest Xcode
run: |
set -euo pipefail
NEWEST=$(ls -d /Applications/Xcode*.app | sort -V | tail -1)
sudo xcode-select -s "$NEWEST"
xcodebuild -version
- name: Import Developer ID certificate
env:
CERT_P12_BASE64: ${{ secrets.DEVELOPER_ID_CERT_P12_BASE64 }}
CERT_PASSWORD: ${{ secrets.DEVELOPER_ID_CERT_PASSWORD }}
run: |
set -euo pipefail
# A throwaway keychain, unlocked only for this job. The password is
# generated here, so it never needs to exist as a repository secret.
KEYCHAIN_PASSWORD="$(uuidgen)"
KEYCHAIN=$RUNNER_TEMP/build.keychain-db
# Strip any whitespace the secret picked up on its way in, then make
# sure we actually decoded a PKCS12 before blaming the password.
printf '%s' "$CERT_P12_BASE64" | tr -d '[:space:]' | base64 --decode > "$RUNNER_TEMP/cert.p12"
if [ ! -s "$RUNNER_TEMP/cert.p12" ]; then
echo "::error::DEVELOPER_ID_CERT_P12_BASE64 decoded to an empty file"
exit 1
fi
security create-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN"
security set-keychain-settings -lut 3600 "$KEYCHAIN"
security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN"
# Not validated with openssl first: Keychain Access exports .p12 files
# using RC2-40-CBC, which OpenSSL 3 dropped from its default provider.
# `security` handles it, so let it be the judge.
if ! security import "$RUNNER_TEMP/cert.p12" -k "$KEYCHAIN" -P "$CERT_PASSWORD" \
-T /usr/bin/codesign -T /usr/bin/security; then
echo "::error::Could not import the certificate. A 'MAC verification failed' above means DEVELOPER_ID_CERT_PASSWORD does not match the password used when exporting the .p12."
exit 1
fi
# Lets codesign use the key without an interactive prompt.
security set-key-partition-list -S apple-tool:,apple:,codesign: \
-s -k "$KEYCHAIN_PASSWORD" "$KEYCHAIN" > /dev/null
security list-keychain -d user -s "$KEYCHAIN" login.keychain-db
rm -f "$RUNNER_TEMP/cert.p12"
security find-identity -v -p codesigning "$KEYCHAIN"
- name: Archive
run: |
set -euo pipefail
xcodebuild -scheme Cleankey -configuration Release \
-destination 'platform=macOS' \
-archivePath "$RUNNER_TEMP/Cleankey.xcarchive" \
archive
- name: Export with Developer ID
run: |
set -euo pipefail
xcodebuild -exportArchive \
-archivePath "$RUNNER_TEMP/Cleankey.xcarchive" \
-exportOptionsPlist ExportOptions.plist \
-exportPath "$RUNNER_TEMP/export"
- name: Verify signature and entitlements
run: |
set -euo pipefail
APP="$RUNNER_TEMP/export/Cleankey.app"
codesign -dv --verbose=4 "$APP" 2>&1 | grep "Authority=Developer ID Application"
# A debugger-attachable build must never ship.
if codesign -d --entitlements - --xml "$APP" 2>/dev/null | grep -q "get-task-allow"; then
echo "::error::get-task-allow present in exported app"
exit 1
fi
- name: Notarize and staple
env:
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
APPLE_APP_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
run: |
set -euo pipefail
APP="$RUNNER_TEMP/export/Cleankey.app"
ditto -c -k --keepParent "$APP" "$RUNNER_TEMP/submit.zip"
xcrun notarytool submit "$RUNNER_TEMP/submit.zip" \
--apple-id "$APPLE_ID" \
--team-id "$APPLE_TEAM_ID" \
--password "$APPLE_APP_PASSWORD" \
--wait
xcrun stapler staple "$APP"
xcrun stapler validate "$APP"
spctl -a -vv "$APP"
- name: Package
id: package
run: |
set -euo pipefail
APP="$RUNNER_TEMP/export/Cleankey.app"
# On a manual run there is no tag, so take the version from the built
# app and mark it, rather than naming the zip after the branch.
if [[ "$GITHUB_REF" == refs/tags/* ]]; then
VERSION="${GITHUB_REF_NAME#v}"
else
BUILT=$(/usr/libexec/PlistBuddy -c 'Print CFBundleShortVersionString' "$APP/Contents/Info.plist")
VERSION="$BUILT-dev"
fi
# Zip after stapling, so the ticket travels inside the app.
ditto -c -k --keepParent "$APP" "$RUNNER_TEMP/Cleankey-$VERSION.zip"
echo "zip=$RUNNER_TEMP/Cleankey-$VERSION.zip" >> "$GITHUB_OUTPUT"
echo "name=Cleankey-$VERSION.zip" >> "$GITHUB_OUTPUT"
- name: Upload build artifact
uses: actions/upload-artifact@v7
with:
name: ${{ steps.package.outputs.name }}
path: ${{ steps.package.outputs.zip }}
if-no-files-found: error
- name: Publish release
# Tags only. A manual run stops here, having proved the whole signing and
# notarization path without creating a release named after the branch.
if: startsWith(github.ref, 'refs/tags/')
env:
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
gh release create "$GITHUB_REF_NAME" "${{ steps.package.outputs.zip }}" \
--repo "$GITHUB_REPOSITORY" \
--title "Cleankey ${GITHUB_REF_NAME#v}" \
--generate-notes \
--latest \
|| gh release upload "$GITHUB_REF_NAME" "${{ steps.package.outputs.zip }}" \
--repo "$GITHUB_REPOSITORY" --clobber
- name: Clean up keychain
if: always()
run: security delete-keychain "$RUNNER_TEMP/build.keychain-db" || true