Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 82 additions & 0 deletions .github/workflows/publish-cli.yml
Original file line number Diff line number Diff line change
Expand Up @@ -118,10 +118,92 @@ jobs:
include: cli/assets/usage.1
codesign: "Developer ID Application: Jeffrey Dickey (4993Y37DX6)"
codesign_prefix: dev.jdx.
# Apple will not notarize a binary without the hardened runtime.
codesign_options: runtime
dry-run: ${{ github.event_name == 'workflow_dispatch' }}
env:
CARGO_PROFILE_RELEASE_CODEGEN_UNITS: 1
CARGO_PROFILE_RELEASE_LTO: true
# Signing settles who built the binary; notarization is what Gatekeeper
# demands once an archive carries the quarantine bit, which is to say
# whenever someone downloads it in a browser rather than with curl.
# Without a ticket it is blocked behind a "cannot be verified" dialog that
# only a deliberate override gets past.
#
# The ticket lives on Apple's side and is keyed to the binary's cdhash, so
# nothing here rewrites what was already built or uploaded -- stapling
# would, but `stapler` only writes into bundles, disk images, and
# installer packages, and this is a bare Mach-O in an archive. Gatekeeper
# resolves the ticket online instead.
- name: Notarize macOS binary
if: matrix.os == 'macos-latest'
shell: bash
env:
APPLE_API_KEY_P8: ${{ secrets.APPLE_API_KEY_P8 }}
APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }}
APPLE_API_ISSUER_ID: ${{ secrets.APPLE_API_ISSUER_ID }}
BINARIES: target/${{ matrix.target }}/release/usage
run: |
set -euo pipefail

if [ -z "$APPLE_API_KEY_P8" ] || [ -z "$APPLE_API_KEY_ID" ] || [ -z "$APPLE_API_ISSUER_ID" ]; then
echo "::warning title=Not notarized::App Store Connect credentials are unset, so this release ships signed but un-notarized macOS binaries. Gatekeeper will block them for anyone who downloads an archive through a browser."
exit 0
fi

# Outside the workspace, so no later step can sweep the key into an
# artifact, and removed even when notarization fails.
work="$(mktemp -d)"
trap 'rm -rf "$work"' EXIT
printf '%s' "$APPLE_API_KEY_P8" | base64 --decode > "$work/key.p8"

# notarytool takes a container, never a bare executable. Copies are
# fine: a Mach-O carries its signature inline, so the cdhash the
# ticket is keyed to is the one already shipped.
mkdir "$work/stage"
for binary in $BINARIES; do
cp "$binary" "$work/stage/"
done
# Apple rejects a binary without the hardened runtime or a secure
# timestamp. Naming a missing one here turns an opaque Invalid verdict
# into a diagnosis; Apple stays the authority on whether to accept.
for binary in $BINARIES; do
details="$(codesign --display --verbose=4 "$binary" 2>&1 || true)"
case "$details" in
*runtime*) ;;
*) echo "::warning title=No hardened runtime::$binary is signed without --options runtime, which Apple requires; expect an Invalid verdict below." ;;
esac
case "$details" in
*Timestamp=*) ;;
*) echo "::warning title=No secure timestamp::$binary carries no secure timestamp, which Apple requires; expect an Invalid verdict below." ;;
esac
done

ditto -c -k --norsrc --noextattr "$work/stage" "$work/submission.zip"

# `--wait` is not a gate on its own: it can return zero on an Invalid
# submission, so the reported status is what decides. plutil reads
# JSON and ships with macOS, which keeps this off jq.
result="$(xcrun notarytool submit "$work/submission.zip" \
--key "$work/key.p8" \
--key-id "$APPLE_API_KEY_ID" \
--issuer "$APPLE_API_ISSUER_ID" \
--output-format json \
--timeout 30m \
--wait)"
echo "$result"

status="$(printf '%s' "$result" | plutil -extract status raw -o - -)"
id="$(printf '%s' "$result" | plutil -extract id raw -o - -)"
if [ "$status" != "Accepted" ]; then
echo "::error::notarization returned $status for submission $id"
xcrun notarytool log "$id" \
--key "$work/key.p8" \
--key-id "$APPLE_API_KEY_ID" \
--issuer "$APPLE_API_ISSUER_ID" || true
exit 1
fi
echo "notarized: submission $id"
release:
runs-on: ubuntu-latest
timeout-minutes: 10
Expand Down