Skip to content
Merged
Show file tree
Hide file tree
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
188 changes: 30 additions & 158 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -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]"
Expand Down
4 changes: 2 additions & 2 deletions Configuration/Release.xcconfig
Original file line number Diff line number Diff line change
@@ -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
10 changes: 7 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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.
Loading
Loading