From 9d0e4d99cf5f3d799fd8c363897f59c9ac3a2e0f Mon Sep 17 00:00:00 2001 From: aodjo Date: Sat, 12 Sep 2026 19:51:43 +0900 Subject: [PATCH] Sign releases with Developer ID and notarize them build-app.sh enables the hardened runtime and a secure timestamp for Developer ID identities. scripts/release.sh builds a universal app, signs it, notarizes and staples it, and publishes the GitHub release with the zip. The signing key stays on the Mac instead of in CI secrets, so CI now only builds and tests. Claude-Session: https://claude.ai/code/session_0115d7b3mfRT6Gs8mgqtAt1x --- .github/workflows/build.yml | 30 ---------------- scripts/build-app.sh | 10 ++++-- scripts/release.sh | 72 +++++++++++++++++++++++++++++++++++++ 3 files changed, 80 insertions(+), 32 deletions(-) create mode 100755 scripts/release.sh diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 94bc1af..ac3705a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -45,33 +45,3 @@ jobs: name: MacTree path: MacTree.zip if-no-files-found: error - - release: - name: release - needs: build - if: startsWith(github.ref, 'refs/tags/v') - runs-on: ubuntu-latest - permissions: - contents: write - steps: - - uses: actions/download-artifact@v8 - with: - name: MacTree - - - name: Publish GitHub release - env: - GH_TOKEN: ${{ github.token }} - run: | - mv MacTree.zip "MacTree-${GITHUB_REF_NAME}.zip" - cat > notes.md <<'NOTES' - WizTree-style disk space analyzer for macOS 15 or later (Apple silicon and Intel). - - **Install:** unzip, move `MacTree.app` to Applications, and open it. The build is ad-hoc signed and not notarized, so macOS blocks the first launch. To allow it, go to System Settings › Privacy & Security and click **Open Anyway**. Or run `xattr -dr com.apple.quarantine /Applications/MacTree.app`. - - For complete results, allow **Full Disk Access** when MacTree asks. - NOTES - gh release create "$GITHUB_REF_NAME" "MacTree-${GITHUB_REF_NAME}.zip" \ - --repo "$GITHUB_REPOSITORY" \ - --title "MacTree ${GITHUB_REF_NAME}" \ - --notes-file notes.md \ - --generate-notes diff --git a/scripts/build-app.sh b/scripts/build-app.sh index a80be4a..0d893ad 100755 --- a/scripts/build-app.sh +++ b/scripts/build-app.sh @@ -4,7 +4,9 @@ # Signing: uses the first "Apple Development" identity in the keychain so that # macOS keeps the Full Disk Access grant across rebuilds (an ad-hoc signature # changes every build and the grant stops applying). Override with -# SIGN_IDENTITY="", or SIGN_IDENTITY=- for ad-hoc. +# SIGN_IDENTITY="", or SIGN_IDENTITY=- for ad-hoc. A +# "Developer ID Application" identity (or HARDENED_RUNTIME=1) also enables the +# hardened runtime and a secure timestamp, which notarization requires. # # UNIVERSAL=1 builds one binary for both Apple silicon and Intel Macs. set -euo pipefail @@ -45,7 +47,11 @@ if [ -z "$IDENTITY" ]; then IDENTITY=$(security find-identity -v -p codesigning 2>/dev/null | awk '/"Apple Development/ { print $2; exit }') fi IDENTITY="${IDENTITY:--}" -codesign --force --sign "$IDENTITY" "$APP" +SIGN_FLAGS=(--force --sign "$IDENTITY") +if [[ "$IDENTITY" == *"Developer ID"* || -n "${HARDENED_RUNTIME:-}" ]]; then + SIGN_FLAGS+=(--options runtime --timestamp) +fi +codesign "${SIGN_FLAGS[@]}" "$APP" if [ "$IDENTITY" = "-" ]; then echo "Built $APP (ad-hoc signed)" else diff --git a/scripts/release.sh b/scripts/release.sh new file mode 100755 index 0000000..0ae9e34 --- /dev/null +++ b/scripts/release.sh @@ -0,0 +1,72 @@ +#!/bin/bash +# Publishes a signed and notarized MacTree release from this Mac. +# +# scripts/release.sh v1.2.3 +# +# Tags main's HEAD (if the tag does not exist yet) and pushes the tag. Then it +# builds a universal app and signs it with the Developer ID Application +# identity (hardened runtime + secure timestamp). The app is notarized with +# notarytool and the ticket stapled. Finally it creates the GitHub release with +# MacTree-.zip (or replaces the zip if the release exists) and prints the +# SHA-256 for the Homebrew cask. +# +# One-time setup (stores an app-specific password in the keychain): +# xcrun notarytool store-credentials macTree --apple-id --team-id +# NOTARY_PROFILE selects another profile, SIGN_IDENTITY another identity. +set -euo pipefail +cd "$(dirname "$0")/.." + +TAG="${1:?usage: scripts/release.sh vX.Y.Z}" +[[ "$TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]] || { echo "Tag must look like v1.2.3." >&2; exit 1; } +VERSION="${TAG#v}" +PROFILE="${NOTARY_PROFILE:-macTree}" +IDENTITY="${SIGN_IDENTITY:-$(security find-identity -v -p codesigning | awk -F'"' '/Developer ID Application/ { print $2; exit }')}" +[ -n "$IDENTITY" ] || { echo "No Developer ID Application identity in the keychain." >&2; exit 1; } +if ! xcrun notarytool history --keychain-profile "$PROFILE" > /dev/null 2>&1; then + echo "Notary profile '$PROFILE' is missing. Create it once with:" >&2 + echo " xcrun notarytool store-credentials $PROFILE --apple-id --team-id " >&2 + exit 1 +fi + +[ -z "$(git status --porcelain)" ] || { echo "Commit or stash your changes first." >&2; exit 1; } +git fetch -q origin +[ "$(git rev-parse HEAD)" = "$(git rev-parse origin/main)" ] || { echo "Check out an up-to-date main first." >&2; exit 1; } +if git rev-parse -q --verify "refs/tags/$TAG" > /dev/null; then + [ "$(git rev-parse "$TAG^{commit}")" = "$(git rev-parse HEAD)" ] || { echo "$TAG exists but is not HEAD." >&2; exit 1; } +else + git tag -a "$TAG" -m "MacTree $VERSION" +fi +git push -q origin "$TAG" + +UNIVERSAL=1 VERSION="$VERSION" BUILD_NUMBER="$(git rev-list --count HEAD)" SIGN_IDENTITY="$IDENTITY" \ + ./scripts/build-app.sh + +APP=build/MacTree.app +ZIP="build/MacTree-$TAG.zip" +rm -f "$ZIP" +ditto -c -k --keepParent "$APP" "$ZIP" +xcrun notarytool submit "$ZIP" --keychain-profile "$PROFILE" --wait --output-format json > build/notary.json +if [ "$(plutil -extract status raw build/notary.json)" != "Accepted" ]; then + xcrun notarytool log "$(plutil -extract id raw build/notary.json)" --keychain-profile "$PROFILE" >&2 + exit 1 +fi +xcrun stapler staple "$APP" +spctl -a -vv -t exec "$APP" +rm -f "$ZIP" +ditto -c -k --keepParent "$APP" "$ZIP" + +NOTES=$(mktemp) +cat > "$NOTES" <<'NOTES' +WizTree-style disk space analyzer for macOS 15 or later (Apple silicon and Intel), signed with Developer ID and notarized by Apple. + +**Install:** `brew install --cask aodjo/tap/mactree`, or unzip and move `MacTree.app` to Applications. + +For complete results, allow **Full Disk Access** when MacTree asks. +NOTES +if gh release view "$TAG" > /dev/null 2>&1; then + gh release upload "$TAG" "$ZIP" --clobber +else + gh release create "$TAG" "$ZIP" --title "MacTree $TAG" --notes-file "$NOTES" --generate-notes +fi +rm -f "$NOTES" +echo "SHA-256 for the cask: $(shasum -a 256 "$ZIP" | cut -d' ' -f1)"