diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9bb48da..1b23925 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,187 +1,59 @@ -name: Release +name: Release Artifact on: - push: - tags: ["v*"] + release: + types: [published] permissions: - contents: write + contents: read jobs: - release: + verify-and-update-homebrew: runs-on: macos-26 steps: - - uses: actions/checkout@v4 - - name: Install pinned XcodeGen - run: | - archive="$RUNNER_TEMP/xcodegen-2.44.1.zip" - install_root="$RUNNER_TEMP/xcodegen-2.44.1" - bin="$install_root/xcodegen/bin" - curl -fsSL "https://github.com/yonaskolb/XcodeGen/releases/download/2.44.1/xcodegen.zip" -o "$archive" - echo "a2e905fb68446e9bb4008cdfe2e13e3f176d0cbcca828b71770f8e53fca91b73 $archive" | shasum -a 256 -c - - ditto -x -k "$archive" "$install_root" - test -x "$bin/xcodegen" - "$bin/xcodegen" --version | grep -Fx "Version: 2.44.1" - echo "$bin" >> "$GITHUB_PATH" - - name: Verify generated Xcode project and Info plists are current - run: | - ./script/generate_xcode_project.sh - git diff --exit-code -- \ - SortingHat.xcodeproj \ - Configuration/SortingHatApp-Info.plist \ - Configuration/SendToSortingHatAction-Info.plist + - name: Check out the published release tag + uses: actions/checkout@v4 + with: + ref: ${{ github.event.release.tag_name }} - name: Verify tag matches the shared release identity - run: ./script/release_identity.sh --verify "${GITHUB_REF_NAME#v}" - - name: Import Developer ID certificate - env: - CERTIFICATE_BASE64: ${{ secrets.DEVELOPER_ID_APPLICATION_P12 }} - CERTIFICATE_PASSWORD: ${{ secrets.DEVELOPER_ID_APPLICATION_PASSWORD }} - KEYCHAIN_PASSWORD: ${{ secrets.BUILD_KEYCHAIN_PASSWORD }} - run: | - test -n "$CERTIFICATE_BASE64" - test -n "$CERTIFICATE_PASSWORD" - test -n "$KEYCHAIN_PASSWORD" - certificate_path="$RUNNER_TEMP/sorting-hat-developer-id.p12" - keychain_path="$RUNNER_TEMP/sorting-hat-signing.keychain-db" - echo -n "$CERTIFICATE_BASE64" | base64 --decode > "$certificate_path" - 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 "$certificate_path" -P "$CERTIFICATE_PASSWORD" -A -t cert -f pkcs12 -k "$keychain_path" - security set-key-partition-list -S apple-tool:,apple: -k "$KEYCHAIN_PASSWORD" "$keychain_path" - security list-keychains -d user -s "$keychain_path" - - name: Build and sign app from the extension outward - env: - BUILD_CONFIGURATION: Release - SORTING_HAT_SIGN_IDENTITY: "Developer ID Application: Thomas Ballard (R8HXTBY3NM)" - run: ./script/build_and_run.sh package - - name: Verify nested signatures and entitlements - run: | - app="dist/Sorting Hat.app" - extension="dist/Sorting Hat.app/Contents/PlugIns/Send to Sorting Hat.appex" - app_info="$app/Contents/Info.plist" - extension_info="$extension/Contents/Info.plist" - version="$(./script/release_identity.sh --version)" - build="$(./script/release_identity.sh --build)" - test -d "$extension" - codesign --verify --strict --deep --verbose=2 "$extension" - codesign --verify --strict --deep --verbose=2 "$app" - codesign -dvvv "$extension" 2>&1 | grep -Fx "Authority=Developer ID Application: Thomas Ballard (R8HXTBY3NM)" - codesign -dvvv "$extension" 2>&1 | grep -Fx "TeamIdentifier=R8HXTBY3NM" - codesign -dvvv "$app" 2>&1 | grep -Fx "Authority=Developer ID Application: Thomas Ballard (R8HXTBY3NM)" - codesign -dvvv "$app" 2>&1 | grep -Fx "TeamIdentifier=R8HXTBY3NM" - /usr/libexec/PlistBuddy -c "Print :CFBundleIdentifier" "$app_info" | grep -Fx com.tcballard.sortinghat - /usr/libexec/PlistBuddy -c "Print :CFBundleIdentifier" "$extension_info" | grep -Fx com.tcballard.sortinghat.finder-action - /usr/libexec/PlistBuddy -c "Print :CFBundleShortVersionString" "$app_info" | grep -Fx "$version" - /usr/libexec/PlistBuddy -c "Print :CFBundleVersion" "$app_info" | grep -Fx "$build" - /usr/libexec/PlistBuddy -c "Print :CFBundleShortVersionString" "$extension_info" | grep -Fx "$version" - /usr/libexec/PlistBuddy -c "Print :CFBundleVersion" "$extension_info" | grep -Fx "$build" - codesign -d --entitlements :- "$extension" > "$RUNNER_TEMP/extension-entitlements.plist" 2>/dev/null - codesign -d --entitlements :- "$app" > "$RUNNER_TEMP/app-entitlements.plist" 2>/dev/null - /usr/libexec/PlistBuddy -c "Print :com.apple.security.application-groups:0" "$RUNNER_TEMP/extension-entitlements.plist" | grep -Fx R8HXTBY3NM.com.tcballard.sortinghat - /usr/libexec/PlistBuddy -c "Print :com.apple.security.application-groups:0" "$RUNNER_TEMP/app-entitlements.plist" | grep -Fx R8HXTBY3NM.com.tcballard.sortinghat - /usr/libexec/PlistBuddy -c "Print :com.apple.security.app-sandbox" "$RUNNER_TEMP/extension-entitlements.plist" | grep -Fx true - /usr/libexec/PlistBuddy -c "Print :com.apple.security.files.user-selected.read-only" "$RUNNER_TEMP/extension-entitlements.plist" | grep -Fx true - /usr/libexec/PlistBuddy -c "Print :NSExtension:NSExtensionPointIdentifier" "$extension_info" | grep -Fx com.apple.services - /usr/libexec/PlistBuddy -c "Print :NSExtension:NSExtensionPrincipalClass" "$extension_info" | grep -Fx SendToSortingHatAction.ActionRequestHandler - /usr/libexec/PlistBuddy -c "Print :NSExtension:NSExtensionAttributes:NSExtensionServiceFinderPreviewLabel" "$extension_info" | grep -Fx "Send to Sorting Hat" - /usr/libexec/PlistBuddy -c "Print :NSExtension:NSExtensionAttributes:NSExtensionServiceRoleType" "$extension_info" | grep -Fx NSExtensionServiceRoleTypeEditor - /usr/libexec/PlistBuddy -c "Print :NSExtension:NSExtensionAttributes:NSExtensionServiceAllowsFinderPreviewItem" "$extension_info" | grep -Fx true - /usr/libexec/PlistBuddy -c "Print :NSExtension:NSExtensionAttributes:NSExtensionActivationRule" "$extension_info" | grep -Fx 'SUBQUERY (extensionItems, $extensionItem, SUBQUERY ($extensionItem.attachments, $attachment, ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "public.item").@count == $extensionItem.attachments.@count).@count == 1' - - name: Notarize and staple app env: - NOTARY_APPLE_ID: ${{ secrets.NOTARY_APPLE_ID }} - NOTARY_PASSWORD: ${{ secrets.NOTARY_PASSWORD }} - run: | - test -n "$NOTARY_APPLE_ID" - test -n "$NOTARY_PASSWORD" - submission="$RUNNER_TEMP/Sorting-Hat-notarization.zip" - ditto -c -k --sequesterRsrc --keepParent "dist/Sorting Hat.app" "$submission" - xcrun notarytool submit "$submission" \ - --apple-id "$NOTARY_APPLE_ID" \ - --password "$NOTARY_PASSWORD" \ - --team-id R8HXTBY3NM \ - --wait - xcrun stapler staple "dist/Sorting Hat.app" - xcrun stapler validate "dist/Sorting Hat.app" - spctl --assess --type execute --verbose=2 "dist/Sorting Hat.app" - - name: Archive verified app - run: ditto -c -k --sequesterRsrc --keepParent "dist/Sorting Hat.app" "Sorting-Hat-${GITHUB_REF_NAME}.zip" - - name: Verify final release archive - run: | - archive="Sorting-Hat-${GITHUB_REF_NAME}.zip" - verification_root="$RUNNER_TEMP/final-release-verification" - app="$verification_root/Sorting Hat.app" - extension="$app/Contents/PlugIns/Send to Sorting Hat.appex" - app_info="$app/Contents/Info.plist" - extension_info="$extension/Contents/Info.plist" - app_entitlements="$RUNNER_TEMP/final-app-entitlements.plist" - extension_entitlements="$RUNNER_TEMP/final-extension-entitlements.plist" - version="$(./script/release_identity.sh --version)" - build="$(./script/release_identity.sh --build)" - - test -f "$archive" - rm -rf "$verification_root" - mkdir -p "$verification_root" - ditto -x -k "$archive" "$verification_root" - test -d "$app" - test -d "$extension" - - codesign --verify --strict --deep --verbose=2 "$extension" - codesign --verify --strict --deep --verbose=2 "$app" - codesign -dvvv "$extension" 2>&1 | grep -Fx "Authority=Developer ID Application: Thomas Ballard (R8HXTBY3NM)" - codesign -dvvv "$extension" 2>&1 | grep -Fx "TeamIdentifier=R8HXTBY3NM" - codesign -dvvv "$app" 2>&1 | grep -Fx "Authority=Developer ID Application: Thomas Ballard (R8HXTBY3NM)" - codesign -dvvv "$app" 2>&1 | grep -Fx "TeamIdentifier=R8HXTBY3NM" - xcrun stapler validate "$app" - spctl --assess --type execute --verbose=2 "$app" - - /usr/libexec/PlistBuddy -c "Print :CFBundleIdentifier" "$app_info" | grep -Fx com.tcballard.sortinghat - /usr/libexec/PlistBuddy -c "Print :CFBundleIdentifier" "$extension_info" | grep -Fx com.tcballard.sortinghat.finder-action - /usr/libexec/PlistBuddy -c "Print :CFBundleShortVersionString" "$app_info" | grep -Fx "$version" - /usr/libexec/PlistBuddy -c "Print :CFBundleVersion" "$app_info" | grep -Fx "$build" - /usr/libexec/PlistBuddy -c "Print :CFBundleShortVersionString" "$extension_info" | grep -Fx "$version" - /usr/libexec/PlistBuddy -c "Print :CFBundleVersion" "$extension_info" | grep -Fx "$build" - - codesign -d --entitlements :- "$app" > "$app_entitlements" 2>/dev/null - codesign -d --entitlements :- "$extension" > "$extension_entitlements" 2>/dev/null - /usr/libexec/PlistBuddy -c "Print :com.apple.security.application-groups:0" "$app_entitlements" | grep -Fx R8HXTBY3NM.com.tcballard.sortinghat - /usr/libexec/PlistBuddy -c "Print :com.apple.security.application-groups:0" "$extension_entitlements" | grep -Fx R8HXTBY3NM.com.tcballard.sortinghat - /usr/libexec/PlistBuddy -c "Print :com.apple.security.app-sandbox" "$extension_entitlements" | grep -Fx true - /usr/libexec/PlistBuddy -c "Print :com.apple.security.files.user-selected.read-only" "$extension_entitlements" | grep -Fx true - - /usr/libexec/PlistBuddy -c "Print :NSExtension:NSExtensionPointIdentifier" "$extension_info" | grep -Fx com.apple.services - /usr/libexec/PlistBuddy -c "Print :NSExtension:NSExtensionPrincipalClass" "$extension_info" | grep -Fx SendToSortingHatAction.ActionRequestHandler - /usr/libexec/PlistBuddy -c "Print :NSExtension:NSExtensionAttributes:NSExtensionServiceFinderPreviewLabel" "$extension_info" | grep -Fx "Send to Sorting Hat" - /usr/libexec/PlistBuddy -c "Print :NSExtension:NSExtensionAttributes:NSExtensionServiceRoleType" "$extension_info" | grep -Fx NSExtensionServiceRoleTypeEditor - /usr/libexec/PlistBuddy -c "Print :NSExtension:NSExtensionAttributes:NSExtensionServiceAllowsFinderPreviewItem" "$extension_info" | grep -Fx true - /usr/libexec/PlistBuddy -c "Print :NSExtension:NSExtensionAttributes:NSExtensionActivationRule" "$extension_info" | grep -Fx 'SUBQUERY (extensionItems, $extensionItem, SUBQUERY ($extensionItem.attachments, $attachment, ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "public.item").@count == $extensionItem.attachments.@count).@count == 1' - - name: Publish signed pre-release + TAG: ${{ github.event.release.tag_name }} + run: ./script/release_identity.sh --verify "${TAG#v}" + - name: Download the locally produced release archive env: GH_TOKEN: ${{ github.token }} + TAG: ${{ github.event.release.tag_name }} run: | - gh release create "$GITHUB_REF_NAME" "Sorting-Hat-${GITHUB_REF_NAME}.zip" \ - --prerelease \ - --generate-notes \ - --title "Sorting Hat $GITHUB_REF_NAME" \ - --notes "Signed and notarized macOS pre-release. Enable the bundled Send to Sorting Hat action in System Settings → General → Login Items & Extensions → Finder." - - name: Calculate release checksum + version="${TAG#v}" + mkdir -p dist/releases + gh release download "$TAG" \ + --repo "$GITHUB_REPOSITORY" \ + --pattern "Sorting-Hat-$TAG.zip" \ + --dir dist/releases + - name: Verify the exact published artifact id: release + env: + TAG: ${{ github.event.release.tag_name }} run: | - echo "version=${GITHUB_REF_NAME#v}" >> "$GITHUB_OUTPUT" - echo "sha256=$(shasum -a 256 "Sorting-Hat-${GITHUB_REF_NAME}.zip" | cut -d ' ' -f 1)" >> "$GITHUB_OUTPUT" + version="${TAG#v}" + archive="dist/releases/Sorting-Hat-$TAG.zip" + ./script/verify_release_archive.sh "$archive" "$version" + echo "version=$version" >> "$GITHUB_OUTPUT" + echo "sha256=$(shasum -a 256 "$archive" | cut -d ' ' -f 1)" >> "$GITHUB_OUTPUT" - name: Check out Homebrew tap uses: actions/checkout@v4 with: repository: tcballard/homebrew-tap token: ${{ secrets.HOMEBREW_TAP_TOKEN }} path: homebrew-tap - - name: Update Homebrew cask + - name: Update Homebrew cask from the verified archive working-directory: homebrew-tap env: VERSION: ${{ steps.release.outputs.version }} SHA256: ${{ steps.release.outputs.sha256 }} run: | + test -n "$VERSION" + test -n "$SHA256" sed -i '' -E "s/version \"[^\"]+\"/version \"$VERSION\"/" Casks/sorting-hat.rb sed -i '' -E "s/sha256 \"[^\"]+\"/sha256 \"$SHA256\"/" Casks/sorting-hat.rb git config user.name "github-actions[bot]" diff --git a/Configuration/Release.xcconfig b/Configuration/Release.xcconfig index c3dddb6..d05a72d 100644 --- a/Configuration/Release.xcconfig +++ b/Configuration/Release.xcconfig @@ -1,3 +1,3 @@ // One release identity for every signed Sorting Hat delivery channel. -MARKETING_VERSION = 0.1.0 -CURRENT_PROJECT_VERSION = 2 +MARKETING_VERSION = 0.1.1 +CURRENT_PROJECT_VERSION = 3 diff --git a/README.md b/README.md index b9aaa2b..10c9f75 100644 --- a/README.md +++ b/README.md @@ -136,7 +136,10 @@ swift test ./script/generate_xcode_project.sh ./script/preflight_app_store.sh # After one-time signing and notarization setup: -./script/release_candidate.sh +./script/release_candidate.sh 0.1.1 3 +# Verify the notarized ZIP and, only when explicitly intended, create a draft: +./script/create_release_draft.sh +./script/create_release_draft.sh --create ``` Inference sits behind `FileAnalyzing`, so filesystem safety can be tested without a live model. The Foundation Models decision path can also be shared by a future iPhone or iPad client, but iOS cannot behave like a continuously watched Mac folder. The [iOS client boundary](docs/ios-client-architecture.md) documents what is reusable and what needs a Files or Share-extension workflow. @@ -148,7 +151,8 @@ built from the same source commit and share the version and build number in `Configuration/Release.xcconfig`; release tooling rejects a mismatched tag or artifact. Their security contracts remain intentionally different. -- **Mac App Store:** local-only build `0.1.0 (2)` passed Apple validation, processed as `VALID`, and is selected in App Store Connect. It has not been submitted for review or published. Pricing, App Privacy, export compliance, content rights, and installed-build verification still need owner sign-off. -- **GitHub and Homebrew:** the existing downloadable `v0.1.0` artifact predates the unified release tooling and remains an experimental, ad-hoc-signed pre-release. The next release candidate is Developer ID signed, notarised, stapled, and verified from its extracted ZIP before publication. Issue [#24](https://github.com/tcballard/SortingHat/issues/24) tracks that first signed publication. +- **Release target:** `v0.1.1 (3)` is configured as the first unified candidate. Its [release notes](docs/releases/v0.1.1.md) are prepared, but no candidate has been notarised, uploaded, submitted, or published yet. Signing and notarization happen only on the maintainer's Mac; GitHub verifies the published artifact and updates Homebrew without receiving private signing credentials. +- **Mac App Store:** the earlier local-only build `0.1.0 (2)` passed Apple validation, processed as `VALID`, and remains selected in App Store Connect until the matching `0.1.1 (3)` candidate is uploaded and verified. Nothing has been submitted for review or published. Pricing, App Privacy, export compliance, content rights, and installed-build verification still need owner sign-off. +- **GitHub and Homebrew:** the existing downloadable `v0.1.0` artifact predates the unified release tooling and remains an experimental, ad-hoc-signed pre-release. Issue [#24](https://github.com/tcballard/SortingHat/issues/24) tracks replacing it with the signed and notarised `v0.1.1` release. Issue [#29](https://github.com/tcballard/SortingHat/issues/29) tracks the remaining App Store work. The [distribution guide](docs/distribution.md), [privacy policy](docs/privacy.md), and [support page](docs/support.md) hold the channel-specific details. diff --git a/docs/distribution.md b/docs/distribution.md index ff26d1b..26f35c2 100644 --- a/docs/distribution.md +++ b/docs/distribution.md @@ -17,30 +17,37 @@ verifies the Developer ID ZIP, builds and verifies the Apple Distribution archive and package, and records their checksums and source commit in one manifest. It does not tag, publish, upload, or submit either artifact. +The maintainer's Mac is the signing authority. Developer ID and Apple +Distribution private keys remain in the local Keychain, and notarization uses a +local Keychain profile or App Store Connect API key. GitHub never receives +those credentials. + +The current unified release target is **0.1.1 (3)**. The candidate must come +from the eventual release commit; the existing GitHub `v0.1.0` artifact and +App Store Connect build `0.1.0 (2)` remain historical channel artifacts until +their replacements are independently verified. + ## Developer ID and Homebrew — Issue #24 -The tag-triggered release workflow already fails closed while it: +The local release path fails closed while it: -1. imports a Developer ID Application certificate into an ephemeral keychain; -2. builds with hardened runtime and signs the Finder Action before the app; +1. builds with hardened runtime and signs the Finder Action before the app; +2. verifies the local Developer ID Application identity; 3. verifies identities, App Group entitlements, bundle identifiers, and the extension contract; 4. notarizes, staples, and validates with Gatekeeper; -5. extracts and re-verifies the final ZIP before publishing it; -6. updates the Homebrew cask with the final archive checksum. +5. extracts and re-verifies the final ZIP; +6. creates an owner-reviewable draft GitHub release only when explicitly asked. -The GitHub workflow expects these repository secrets. Store them only as -GitHub repository secrets, never in source, issues, workflow logs, or pull -requests: +After the owner publishes that draft, GitHub downloads and verifies the exact +locally produced ZIP, then updates the Homebrew cask with its checksum. The +repository needs only: -- `DEVELOPER_ID_APPLICATION_P12` -- `DEVELOPER_ID_APPLICATION_PASSWORD` -- `BUILD_KEYCHAIN_PASSWORD` -- `NOTARY_APPLE_ID` -- `NOTARY_PASSWORD` +- `HOMEBREW_TAP_TOKEN` -`HOMEBREW_TAP_TOKEN` is already configured. A successful signed release and a -fresh downloaded-artifact verification remain required before closing Issue #24. +A successful signed release and a fresh downloaded-artifact verification remain +required before closing Issue #24. Publishing the GitHub draft is the explicit +owner gate; there is no tag-triggered cloud signing job. The local Mac already has the Developer ID Application identity. Store reusable notarization credentials once in the login Keychain (the password is requested @@ -67,6 +74,18 @@ The script also accepts an App Store Connect API key through the `SORTING_HAT_NOTARY_ISSUER_ID` environment variables instead of a Keychain profile. Credentials are never read from the repository. +After the unified candidate finishes, preflight the exact ZIP and create a +draft release with separate commands: + +```sh +./script/create_release_draft.sh +./script/create_release_draft.sh --create +``` + +The first command is read-only. The second creates a draft GitHub release and +uploads the verified ZIP; it still does not publish it. Publishing the draft +triggers GitHub's artifact-verification and Homebrew-update workflow. + ## Mac App Store — Issue #29 The `SortingHatAppStore` scheme uses the release-like `AppStore` configuration. @@ -109,7 +128,8 @@ scheme, checks the nested signatures and shared release identity, and exports with `Configuration/AppStoreExportOptions.plist`. It does not upload or submit. App Store Connect build **0.1.0 (2)** passed Apple validation, processed as -`VALID`, and is selected for version 0.1.0. The listing, screenshots, +`VALID`, and remains selected for version 0.1.0. The matching **0.1.1 (3)** +candidate has not been uploaded. The listing, screenshots, categories, age rating, review contact, and review notes are saved. Before submission, choose pricing, attest **Data Not Collected**, complete export compliance and content rights, and run the installed-build smoke test. The diff --git a/docs/releases/v0.1.1.md b/docs/releases/v0.1.1.md new file mode 100644 index 0000000..21ca108 --- /dev/null +++ b/docs/releases/v0.1.1.md @@ -0,0 +1,51 @@ +# Sorting Hat 0.1.1 + +- Release status: prepared, not published +- Build: 3 +- Channels: Developer ID download and Homebrew; sandboxed Mac App Store + +Sorting Hat 0.1.1 is the first release prepared through one signed release +train. Both delivery channels come from the same source commit and carry the +same version and build number, while retaining the security boundaries each +channel requires. + +## What is included + +- Describe a filing plan in plain language and review the generated rules + before applying them. +- Rename files meaningfully and route them into explicit rule-specific folders + outside the intake-only Inbox. +- Extract bounded context from searchable PDFs, scanned PDFs, receipts, images, + and common documents before routing. +- Review, retry, reveal, or remove files that cannot be sorted safely. +- Inspect Inbox and activity state inside Sorting Hat without using Finder as a + second application entry point. +- Send files from Finder through the bundled **Send to Sorting Hat** action. +- Use Apple's on-device Foundation Model or Ollama on the same Mac in the Mac + App Store build. + +## Safety and privacy + +- Sorting Hat validates every destination, filename, extension, and tag before + changing the filesystem. +- Files remain in the Inbox for manual review when analysis abstains or returns + an unsafe decision. +- The Mac App Store build is sandboxed and excludes OpenAI, remote Ollama, LAN + Ollama, and command-line inference adapters. +- API credentials and signing material are not included in the application or + repository. + +## Distribution + +- The direct-download build requires Developer ID signing, Apple notarization, + stapling, Gatekeeper assessment, and verification after extracting the final + ZIP. +- Signing and notarization happen on the maintainer's Mac. GitHub receives only + the final verified ZIP, then independently validates it before updating + Homebrew. +- The Mac App Store build requires Apple Distribution signing, matching + profiles, App Store Connect processing, and an installed sandbox smoke test. +- Publication and App Review submission remain explicit maintainer actions. + +The previous `v0.1.0` GitHub artifact was experimental and ad-hoc signed. It is +not representative of the trust state required for this release. diff --git a/script/create_release_draft.sh b/script/create_release_draft.sh new file mode 100755 index 0000000..499c2ab --- /dev/null +++ b/script/create_release_draft.sh @@ -0,0 +1,57 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd -P)" +VERSION="$($ROOT_DIR/script/release_identity.sh --version)" +BUILD="$($ROOT_DIR/script/release_identity.sh --build)" +TAG="v$VERSION" +ARCHIVE="$ROOT_DIR/dist/releases/Sorting-Hat-$TAG.zip" +CREATE=false + +if [[ "${1:-}" == "--create" ]]; then + CREATE=true + shift +fi +if [[ $# -gt 1 ]]; then + echo "usage: $0 [--create] [ARCHIVE]" >&2 + exit 2 +fi +if [[ $# -eq 1 ]]; then + ARCHIVE="$1" +fi + +"$ROOT_DIR/script/release_identity.sh" --verify "$VERSION" "$BUILD" +if [[ -n "$(git -C "$ROOT_DIR" status --porcelain --untracked-files=all)" ]]; then + echo "Release drafts must be created from a clean committed worktree." >&2 + exit 2 +fi +if ! git -C "$ROOT_DIR" show-ref --verify --quiet refs/remotes/origin/main; then + echo "Missing origin/main. Fetch the current default branch before continuing." >&2 + exit 2 +fi +if [[ "$(git -C "$ROOT_DIR" rev-parse HEAD)" != "$(git -C "$ROOT_DIR" rev-parse refs/remotes/origin/main)" ]]; then + echo "Release drafts must be created from the exact origin/main commit." >&2 + exit 2 +fi + +NOTES="$ROOT_DIR/docs/releases/$TAG.md" +test -f "$NOTES" +"$ROOT_DIR/script/verify_release_archive.sh" "$ARCHIVE" "$VERSION" "$BUILD" +gh auth status >/dev/null + +if [[ "$CREATE" != true ]]; then + echo "Release draft preflight passed for $TAG." + echo "Run '$0 --create "$ARCHIVE"' to create the owner-reviewable draft." + exit 0 +fi + +gh release create "$TAG" "$ARCHIVE" \ + --repo tcballard/SortingHat \ + --target "$(git -C "$ROOT_DIR" rev-parse HEAD)" \ + --draft \ + --prerelease \ + --title "Sorting Hat $TAG" \ + --notes-file "$NOTES" + +echo "Draft GitHub release created for $TAG." +echo "Publishing it remains a separate maintainer action." diff --git a/script/release_local.sh b/script/release_local.sh index 5d0e215..cf4db1b 100755 --- a/script/release_local.sh +++ b/script/release_local.sh @@ -17,7 +17,6 @@ APP="$ROOT_DIR/dist/Sorting Hat.app" EXTENSION="$APP/Contents/PlugIns/Send to Sorting Hat.appex" ARCHIVE="$OUTPUT_DIR/Sorting-Hat-v$VERSION.zip" SUBMISSION="${TMPDIR%/}/Sorting-Hat-v$VERSION-notarization.zip" -VERIFY_ROOT="${TMPDIR%/}/Sorting-Hat-v$VERSION-verification" IDENTITY="Developer ID Application: Thomas Ballard (R8HXTBY3NM)" NOTARY_ARGS=() @@ -32,7 +31,7 @@ else fi mkdir -p "$OUTPUT_DIR" -rm -rf "$DERIVED_DATA" "$VERIFY_ROOT" +rm -rf "$DERIVED_DATA" rm -f "$ARCHIVE" "$SUBMISSION" if ! security find-identity -v -p codesigning | grep -Fq "\"$IDENTITY\""; then @@ -61,19 +60,7 @@ xcrun stapler validate "$APP" spctl --assess --type execute --verbose=2 "$APP" ditto -c -k --sequesterRsrc --keepParent "$APP" "$ARCHIVE" -mkdir -p "$VERIFY_ROOT" -ditto -x -k "$ARCHIVE" "$VERIFY_ROOT" -FINAL_APP="$VERIFY_ROOT/Sorting Hat.app" -FINAL_EXTENSION="$FINAL_APP/Contents/PlugIns/Send to Sorting Hat.appex" -codesign --verify --strict --verbose=2 "$FINAL_EXTENSION" -codesign --verify --deep --strict --verbose=2 "$FINAL_APP" -xcrun stapler validate "$FINAL_APP" -spctl --assess --type execute --verbose=2 "$FINAL_APP" -test "$(/usr/libexec/PlistBuddy -c 'Print :CFBundleShortVersionString' "$FINAL_APP/Contents/Info.plist")" = "$CONTRACT_VERSION" -test "$(/usr/libexec/PlistBuddy -c 'Print :CFBundleVersion' "$FINAL_APP/Contents/Info.plist")" = "$CONTRACT_BUILD" -test "$(/usr/libexec/PlistBuddy -c 'Print :CFBundleShortVersionString' "$FINAL_EXTENSION/Contents/Info.plist")" = "$CONTRACT_VERSION" -test "$(/usr/libexec/PlistBuddy -c 'Print :CFBundleVersion' "$FINAL_EXTENSION/Contents/Info.plist")" = "$CONTRACT_BUILD" +"$ROOT_DIR/script/verify_release_archive.sh" "$ARCHIVE" "$CONTRACT_VERSION" "$CONTRACT_BUILD" echo "Signed, notarized, stapled, and extracted-archive verified for $CONTRACT_VERSION ($CONTRACT_BUILD):" echo "$ARCHIVE" -shasum -a 256 "$ARCHIVE" diff --git a/script/verify_release_archive.sh b/script/verify_release_archive.sh new file mode 100755 index 0000000..eca1553 --- /dev/null +++ b/script/verify_release_archive.sh @@ -0,0 +1,65 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd -P)" +ARCHIVE="${1:-}" +VERSION="${2:-$($ROOT_DIR/script/release_identity.sh --version)}" +BUILD="${3:-$($ROOT_DIR/script/release_identity.sh --build)}" +IDENTITY="Developer ID Application: Thomas Ballard (R8HXTBY3NM)" + +if [[ -z "$ARCHIVE" ]]; then + echo "usage: $0 ARCHIVE [VERSION [BUILD]]" >&2 + exit 2 +fi +if [[ ! -f "$ARCHIVE" ]]; then + echo "Release archive not found: $ARCHIVE" >&2 + exit 2 +fi +"$ROOT_DIR/script/release_identity.sh" --verify "$VERSION" "$BUILD" + +VERIFY_ROOT="$(mktemp -d "${TMPDIR%/}/sorting-hat-release-verify.XXXXXX")" +trap 'rm -rf "$VERIFY_ROOT"' EXIT +ditto -x -k "$ARCHIVE" "$VERIFY_ROOT" + +APP="$VERIFY_ROOT/Sorting Hat.app" +EXTENSION="$APP/Contents/PlugIns/Send to Sorting Hat.appex" +APP_INFO="$APP/Contents/Info.plist" +EXTENSION_INFO="$EXTENSION/Contents/Info.plist" +APP_ENTITLEMENTS="$VERIFY_ROOT/app-entitlements.plist" +EXTENSION_ENTITLEMENTS="$VERIFY_ROOT/extension-entitlements.plist" + +test "$(find "$VERIFY_ROOT" -maxdepth 1 -type d -name '*.app' | wc -l | tr -d ' ')" = "1" +test -d "$APP" +test -d "$EXTENSION" +codesign --verify --strict --verbose=2 "$EXTENSION" +codesign --verify --deep --strict --verbose=2 "$APP" +codesign -dvvv "$EXTENSION" 2>&1 | grep -Fx "Authority=$IDENTITY" >/dev/null +codesign -dvvv "$EXTENSION" 2>&1 | grep -Fx "TeamIdentifier=R8HXTBY3NM" >/dev/null +codesign -dvvv "$APP" 2>&1 | grep -Fx "Authority=$IDENTITY" >/dev/null +codesign -dvvv "$APP" 2>&1 | grep -Fx "TeamIdentifier=R8HXTBY3NM" >/dev/null +xcrun stapler validate "$APP" +spctl --assess --type execute --verbose=2 "$APP" + +/usr/libexec/PlistBuddy -c "Print :CFBundleIdentifier" "$APP_INFO" | grep -Fx com.tcballard.sortinghat >/dev/null +/usr/libexec/PlistBuddy -c "Print :CFBundleIdentifier" "$EXTENSION_INFO" | grep -Fx com.tcballard.sortinghat.finder-action >/dev/null +/usr/libexec/PlistBuddy -c "Print :CFBundleShortVersionString" "$APP_INFO" | grep -Fx "$VERSION" >/dev/null +/usr/libexec/PlistBuddy -c "Print :CFBundleVersion" "$APP_INFO" | grep -Fx "$BUILD" >/dev/null +/usr/libexec/PlistBuddy -c "Print :CFBundleShortVersionString" "$EXTENSION_INFO" | grep -Fx "$VERSION" >/dev/null +/usr/libexec/PlistBuddy -c "Print :CFBundleVersion" "$EXTENSION_INFO" | grep -Fx "$BUILD" >/dev/null + +codesign -d --entitlements :- "$APP" > "$APP_ENTITLEMENTS" 2>/dev/null +codesign -d --entitlements :- "$EXTENSION" > "$EXTENSION_ENTITLEMENTS" 2>/dev/null +/usr/libexec/PlistBuddy -c "Print :com.apple.security.application-groups:0" "$APP_ENTITLEMENTS" | grep -Fx R8HXTBY3NM.com.tcballard.sortinghat >/dev/null +/usr/libexec/PlistBuddy -c "Print :com.apple.security.application-groups:0" "$EXTENSION_ENTITLEMENTS" | grep -Fx R8HXTBY3NM.com.tcballard.sortinghat >/dev/null +/usr/libexec/PlistBuddy -c "Print :com.apple.security.app-sandbox" "$EXTENSION_ENTITLEMENTS" | grep -Fx true >/dev/null +/usr/libexec/PlistBuddy -c "Print :com.apple.security.files.user-selected.read-only" "$EXTENSION_ENTITLEMENTS" | grep -Fx true >/dev/null + +/usr/libexec/PlistBuddy -c "Print :NSExtension:NSExtensionPointIdentifier" "$EXTENSION_INFO" | grep -Fx com.apple.services >/dev/null +/usr/libexec/PlistBuddy -c "Print :NSExtension:NSExtensionPrincipalClass" "$EXTENSION_INFO" | grep -Fx SendToSortingHatAction.ActionRequestHandler >/dev/null +/usr/libexec/PlistBuddy -c "Print :NSExtension:NSExtensionAttributes:NSExtensionServiceFinderPreviewLabel" "$EXTENSION_INFO" | grep -Fx "Send to Sorting Hat" >/dev/null +/usr/libexec/PlistBuddy -c "Print :NSExtension:NSExtensionAttributes:NSExtensionServiceRoleType" "$EXTENSION_INFO" | grep -Fx NSExtensionServiceRoleTypeEditor >/dev/null +/usr/libexec/PlistBuddy -c "Print :NSExtension:NSExtensionAttributes:NSExtensionServiceAllowsFinderPreviewItem" "$EXTENSION_INFO" | grep -Fx true >/dev/null +/usr/libexec/PlistBuddy -c "Print :NSExtension:NSExtensionAttributes:NSExtensionActivationRule" "$EXTENSION_INFO" | grep -Fx 'SUBQUERY (extensionItems, $extensionItem, SUBQUERY ($extensionItem.attachments, $attachment, ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "public.item").@count == $extensionItem.attachments.@count).@count == 1' >/dev/null + +echo "Developer ID release archive verified: $VERSION ($BUILD)" +shasum -a 256 "$ARCHIVE"