updated release script

This commit is contained in:
Mark Gregory
2026-11-03 15:08:34 +00:00
parent 6d4a68d219
commit 008e9bc175
6 changed files with 234 additions and 3 deletions
+1 -1
View File
@@ -105,7 +105,7 @@ Data stays under `~/Library/Application Support/KJamsClone/` for continuity. Bac
./scripts/release.sh
```
Release output: `dist/KJMixPro-unsigned-macOS.zip`. The approved icon, complete AppIcon set, high-resolution master and original generation prompt are included under Brand/assets. Bundle/module identifiers remain compatible internally; displayed branding is KJMix Pro.
Release output: a branded DMG and app ZIP. Normal `./scripts/release.sh` requires Developer ID signing and a saved notarization profile, notarizes/staples both app and DMG, and requires Gatekeeper checks before publishing the final files. Run `./scripts/setup-notarization.sh` once in Terminal to save the default `KJMixPro` profile. For a deliberately unnotarized local build use `./scripts/release.sh --signed-only`; `--unsigned` creates an unsigned preview and the legacy `dist/KJMixPro-unsigned-macOS.zip`. See [release setup](docs/RELEASING.md). The approved icon, complete AppIcon set, high-resolution master and original generation prompt are included under Brand/assets. Bundle/module identifiers remain compatible internally; displayed branding is KJMix Pro.
**73 XCTest cases passed**, including real loopback HTTP, access isolation/revocation, rotation, independent singer settings, fades, timed slides, batch atomicity, search, MP3/MP4 tag backup/media preservation and all earlier library/playback/waveform/venue/drop regressions. Release builds arm64 and x86_64, tested on Apple Silicon macOS 27.2 with Xcode 27.2/Swift 6.4.2. Deployment target macOS 13; this Xcode's XCTest runtime may require macOS 14+. Intel/older macOS execution is untested. See docs/VERIFICATION.md.
+57
View File
@@ -0,0 +1,57 @@
# Create a signed, notarized KJMix Pro DMG
Run these commands from the project folder in Terminal. Do not paste account passwords, private keys or app-specific passwords into chat or source files.
## One-time setup
Install a **Developer ID Application** certificate and its private key in your login Keychain. Apple Development certificates are for development, not this distribution workflow. View available signing identities with:
```sh
security find-identity -v -p codesigning
```
Save notarization credentials interactively:
```sh
./scripts/setup-notarization.sh
```
The helper runs `xcrun notarytool store-credentials KJMixPro`. Apple prompts for the developer Apple ID, team ID and an app-specific password, then validates and saves the credentials in Keychain. `notarytool` alone may not be on PATH: use `xcrun notarytool`, or the helper.
If you already have a profile with a different name, use that name through `KJMIX_NOTARY_PROFILE`. The profile name is a label you chose when saving the credentials; “Keychain” is not automatically a profile name.
## Verified release
```sh
./scripts/release.sh
```
The script builds Release, stages a copy, signs the app with Developer ID and hardened runtime, submits it to Apple, checks Accepted status, staples/validates its ticket, and checks app Gatekeeper assessment. It then creates the compressed DMG with the colourful app/volume/file icon and Applications shortcut, signs and notarizes the DMG, staples/validates its ticket, checks its Gatekeeper assessment and mounts the finished image to recheck the enclosed app. A fresh-Mac launch/install check is still recommended before public distribution.
Outputs are versioned, for example:
- `dist/KJMixPro-0.6.4-macOS.dmg`
- `dist/KJMixPro-0.6.4-macOS.zip` (stapled app)
- SHA-256 sidecars and `dist/KJMixPro-0.6.4-macOS-verification/` reports
The script publishes these files only after the checks succeed. A failed run returns nonzero and never labels that attempt Gatekeeper verified. Existing files from earlier builds can remain in dist; check the current report and successful script completion.
You can configure another saved profile or signing identity without editing the script:
```sh
KJMIX_NOTARY_PROFILE='your-profile' ./scripts/release.sh
KJMIX_SIGNING_IDENTITY='certificate SHA-1' ./scripts/release.sh
```
The script auto-selects a Developer ID identity if all available matching certificates share the same name; otherwise it asks for an explicit identity. It forwards remaining arguments to xcodebuild. Signing/notarization require network access and may take several minutes; reports retain the submission IDs if Apple processing times out.
## Explicit local previews
```sh
./scripts/release.sh --signed-only
./scripts/release.sh --unsigned
```
These modes have clearly marked filenames and status reports. **They are not notarized or Gatekeeper verified.** Signed-only does not need notarization credentials. Unsigned creates the compatibility ZIP as well. Neither mode changes Gatekeeper settings or bypasses macOS security.
References: [Apple notarization](https://developer.apple.com/documentation/security/notarizing-macos-software-before-distribution), [packaging for distribution](https://developer.apple.com/documentation/xcode/packaging-mac-software-for-distribution).
+6
View File
@@ -19,3 +19,9 @@ Configurable background fade times persist; tests inspect the real mixer output
Fade-out handoff regression checks inspect actual mixer gain while it decreases, assert the singer audio node has not started during the fade and starts after silence, and verify Pause/unload cancel the pending singer start. Manual background fade-out also has an intermediate gain check.
Batch waveform checks verify cache reuse versus forced regeneration, deduplication of tracks sharing audio, missing-file reporting, ShowScreen skipping, cancellation retaining completed caches, selected-track versus whole-library scope, and unchanged playback/history/library metadata. The release contains no Spotify support.
## DMG release pipeline
The release script now creates a compressed DMG with the approved app icon as both its volume icon and Finder file icon, plus an Applications shortcut. A real `--signed-only` run succeeded with the installed Developer ID Application identity and hardened runtime. DMG checksum, mounted contents, custom icon flag, app/volume artwork matching, app signature and disk-image signature were verified. Gatekeeper assessments of both app and DMG explicitly returned `Unnotarized Developer ID`; this output is **not** a Gatekeeper-verified release.
The normal release mode is fail-closed: it requires a valid notarytool Keychain profile, notarizes/staples the app and DMG, and requires Gatekeeper success for both the image and mounted app before publishing verified filenames. Neither `keychain` nor the default `KJMixPro` profile was configured during these checks, so the positive notarization/Gatekeeper path remains unverified pending the user’s one-time credential setup. The missing-profile failure was verified. See RELEASING.md.
+154 -2
View File
@@ -1,6 +1,158 @@
#!/bin/bash
set -euo pipefail
cd "$(dirname "$0")/.."
usage() {
cat <<'EOF'
Usage: ./scripts/release.sh [--signed-only | --unsigned] [--] [xcodebuild arguments]
Default: Developer ID sign, notarize and staple the app and branded DMG, then
require successful Gatekeeper checks before publishing the files.
--signed-only: Build a Developer ID signed DMG without notarization.
--unsigned: Build an unsigned preview DMG and the legacy unsigned ZIP.
KJMIX_SIGNING_IDENTITY: Developer ID Application certificate name or SHA-1.
Auto-selects when all matching certificates have one name.
KJMIX_NOTARY_PROFILE: notarytool Keychain profile (default: KJMixPro).
Save credentials once with ./scripts/setup-notarization.sh in Terminal.
EOF
}
release_mode=notarized
case "${1:-}" in
--help|-h) usage; exit 0 ;;
--signed-only) release_mode=signed;shift ;;
--unsigned) release_mode=unsigned;shift ;;
esac
if [[ "${1:-}" == -- ]]; then shift; fi
release_work="$(mktemp -d "${TMPDIR:-/tmp}/kjmix-release.XXXXXX")"
release_mount="$release_work/mounted"
release_mounted=false
release_completed=false
release_reports=""
cleanup() {
if [[ "$release_completed" != true && -n "$release_reports" && -d "$release_reports" ]]; then printf 'FAILED or interrupted. This attempt is NOT a verified release.\n' > "$release_reports/STATUS.txt";fi
if [[ "$release_mounted" == true ]]; then /usr/bin/hdiutil detach "$release_mount" -quiet || true; fi
/bin/rm -rf "$release_work"
}
trap cleanup EXIT
fail() { printf 'Release failed: %s\n' "$*" >&2;exit 1; }
release_identity="${KJMIX_SIGNING_IDENTITY:-}"
release_profile="${KJMIX_NOTARY_PROFILE:-KJMixPro}"
if [[ "$release_mode" != unsigned ]]; then
release_identities="$(/usr/bin/security find-identity -v -p codesigning)"
if [[ -z "$release_identity" ]]; then
release_names="$(printf '%s\n' "$release_identities" | /usr/bin/sed -n 's/.*"\(Developer ID Application:.*\)"/\1/p' | /usr/bin/sort -u)"
[[ -n "$release_names" ]] || fail 'No Developer ID Application certificate with a private key is available. Use --unsigned only for a local preview.'
[[ "$(printf '%s\n' "$release_names" | /usr/bin/wc -l | /usr/bin/tr -d ' ')" == 1 ]] || fail 'Multiple Developer ID identities found. Set KJMIX_SIGNING_IDENTITY to the intended certificate SHA-1.'
release_identity="$(printf '%s\n' "$release_identities" | /usr/bin/awk '/"Developer ID Application:/ {print $2;exit}')"
fi
printf 'Signing identity: %s\n' "$release_identity"
fi
if [[ "$release_mode" == notarized ]]; then
if ! xcrun notarytool history --keychain-profile "$release_profile" --output-format json > "$release_work/profile-check.json" 2> "$release_work/profile-error.txt"; then
/bin/cat "$release_work/profile-error.txt" >&2
fail "Notarization profile '$release_profile' is unavailable or Apple could not validate it. Run ./scripts/setup-notarization.sh, or explicitly use --signed-only for an unnotarized build."
fi
fi
xcodebuild -project "KJMix Pro.xcodeproj" -scheme "KJMix Pro" -configuration Release -destination 'platform=macOS' -derivedDataPath "$PWD/build-release" CODE_SIGNING_ALLOWED=NO build "$@"
mkdir -p dist
/usr/bin/ditto -c -k --sequesterRsrc --keepParent "build-release/Build/Products/Release/KJMix Pro.app" dist/KJMixPro-unsigned-macOS.zip
release_built="$PWD/build-release/Build/Products/Release/KJMix Pro.app"
[[ -d "$release_built" ]] || fail 'Release app was not produced.'
release_version="$(/usr/libexec/PlistBuddy -c 'Print :CFBundleShortVersionString' "$release_built/Contents/Info.plist")"
[[ "$release_version" =~ ^[A-Za-z0-9._-]+$ ]] || fail 'Invalid release version.'
release_payload="$release_work/payload"
/bin/mkdir -p "$release_payload" dist
release_app="$release_payload/KJMix Pro.app"
/usr/bin/ditto --noextattr --norsrc "$release_built" "$release_app"
release_icon="$release_app/Contents/Resources/AppIcon.icns"
[[ -s "$release_icon" ]] || fail 'AppIcon.icns is missing.'
# The app icon is already in its bundle; use the same approved artwork for the volume.
/bin/cp "$release_icon" "$release_payload/.VolumeIcon.icns"
/bin/ln -s /Applications "$release_payload/Applications"
release_suffix=macOS
if [[ "$release_mode" == unsigned ]]; then release_suffix=unsigned-macOS;fi
if [[ "$release_mode" == signed ]]; then release_suffix=signed-unnotarized-macOS;fi
release_name="KJMixPro-$release_version-$release_suffix"
release_reports="$PWD/dist/$release_name-verification"
/bin/mkdir -p "$release_reports"
printf 'Preparing release. NOT yet verified.\n' > "$release_reports/STATUS.txt"
notarize() {
local artifact="$1" label="$2" report="$release_reports/$2-notary.json" submission status
printf 'Submitting %s to Apple notarization…\n' "$label"
if ! xcrun notarytool submit "$artifact" --keychain-profile "$release_profile" --wait --timeout 30m --output-format json > "$report"; then
printf 'Notarization did not complete. Inspect %s; Apple may still be processing the submission.\n' "$report" >&2
return 1
fi
status="$(/usr/bin/plutil -extract status raw -o - "$report")"
submission="$(/usr/bin/plutil -extract id raw -o - "$report")"
if [[ "$status" != Accepted ]]; then
xcrun notarytool log "$submission" --keychain-profile "$release_profile" "$release_reports/$label-notary-log.json" || true
fail "Apple did not accept $label (status: $status). See $release_reports."
fi
}
if [[ "$release_mode" != unsigned ]]; then
/usr/bin/codesign --force --options runtime --timestamp --sign "$release_identity" "$release_app"
/usr/bin/codesign --verify --deep --strict --verbose=2 "$release_app" 2> "$release_reports/app-signature.txt"
/usr/bin/codesign --display --verbose=4 "$release_app" 2> "$release_reports/app-signing-details.txt"
# A development/ad-hoc certificate must never be presented as a distribution identity.
/usr/bin/awk '/^Authority=Developer ID Application:/ {ok=1} END {exit !ok}' "$release_reports/app-signing-details.txt" || fail 'The app was not signed with Developer ID Application.'
fi
if [[ "$release_mode" == notarized ]]; then
/usr/bin/ditto -c -k --sequesterRsrc --keepParent "$release_app" "$release_work/app-for-notarization.zip"
notarize "$release_work/app-for-notarization.zip" app
xcrun stapler staple "$release_app" > "$release_reports/app-staple.txt" 2>&1
xcrun stapler validate "$release_app" >> "$release_reports/app-staple.txt" 2>&1
/usr/sbin/spctl --assess --type execute --verbose=4 "$release_app" > "$release_reports/app-gatekeeper.txt" 2>&1
if [[ -x /usr/bin/syspolicy_check ]]; then /usr/bin/syspolicy_check distribution "$release_app" > "$release_reports/app-distribution.txt" 2>&1;fi
fi
# Configure the custom volume icon on a writable image, then compress for distribution.
/usr/bin/hdiutil create -quiet -srcfolder "$release_payload" -volname 'KJMix Pro' -fs HFS+ -format UDRW "$release_work/writable.dmg"
/bin/mkdir -p "$release_mount"
/usr/bin/hdiutil attach -quiet -readwrite -nobrowse -noautoopen -mountpoint "$release_mount" "$release_work/writable.dmg"
release_mounted=true
xcrun SetFile -a C "$release_mount"
/usr/bin/hdiutil detach -quiet "$release_mount"
release_mounted=false
release_dmg="$release_work/$release_name.dmg"
/usr/bin/hdiutil convert -quiet "$release_work/writable.dmg" -format UDZO -imagekey zlib-level=9 -o "$release_dmg"
# Set the DMG file's Finder icon before signing the image.
xcrun swift scripts/set-dmg-icon.swift "$release_icon" "$release_dmg"
if [[ "$release_mode" != unsigned ]]; then
/usr/bin/codesign --force --timestamp --sign "$release_identity" "$release_dmg"
/usr/bin/codesign --verify --strict --verbose=2 "$release_dmg" 2> "$release_reports/dmg-signature.txt"
fi
if [[ "$release_mode" == notarized ]]; then
notarize "$release_dmg" dmg
xcrun stapler staple "$release_dmg" > "$release_reports/dmg-staple.txt" 2>&1
xcrun stapler validate "$release_dmg" >> "$release_reports/dmg-staple.txt" 2>&1
/usr/sbin/spctl --assess --type open --context context:primary-signature --verbose=4 "$release_dmg" > "$release_reports/dmg-gatekeeper.txt" 2>&1
fi
/usr/bin/hdiutil verify "$release_dmg" > "$release_reports/dmg-integrity.txt" 2>&1
# Inspect the final compressed image, not only the source staging folder.
/usr/bin/hdiutil attach -quiet -readonly -nobrowse -noautoopen -mountpoint "$release_mount" "$release_dmg"
release_mounted=true
[[ -s "$release_mount/.VolumeIcon.icns" && -L "$release_mount/Applications" && -d "$release_mount/KJMix Pro.app" ]] || fail 'DMG app, Applications shortcut or volume icon is missing.'
[[ "$(/usr/bin/readlink "$release_mount/Applications")" == /Applications ]] || fail 'Applications shortcut target is incorrect.'
if [[ "$release_mode" != unsigned ]]; then /usr/bin/codesign --verify --deep --strict "$release_mount/KJMix Pro.app" 2> "$release_reports/mounted-app-signature.txt";fi
if [[ "$release_mode" == notarized ]]; then
xcrun stapler validate "$release_mount/KJMix Pro.app" > "$release_reports/mounted-app-staple.txt" 2>&1
/usr/sbin/spctl --assess --type execute --verbose=4 "$release_mount/KJMix Pro.app" > "$release_reports/mounted-app-gatekeeper.txt" 2>&1
fi
/usr/bin/hdiutil detach -quiet "$release_mount"
release_mounted=false
/usr/bin/ditto -c -k --sequesterRsrc --keepParent "$release_app" "$release_work/$release_name.zip"
/bin/mv -f "$release_dmg" "dist/$release_name.dmg"
/bin/mv -f "$release_work/$release_name.zip" "dist/$release_name.zip"
if [[ "$release_mode" == unsigned ]]; then /bin/cp "dist/$release_name.zip" dist/KJMixPro-unsigned-macOS.zip;fi
(
cd dist
/usr/bin/shasum -a 256 "$release_name.dmg" > "$release_name.dmg.sha256"
/usr/bin/shasum -a 256 "$release_name.zip" > "$release_name.zip.sha256"
)
if [[ "$release_mode" == notarized ]]; then
printf 'Developer ID signed; Apple notarization accepted; app and DMG tickets stapled; Gatekeeper checks passed.\n' | /usr/bin/tee "$release_reports/STATUS.txt"
else
printf '%s build only. NOT notarized or Gatekeeper verified.\n' "$release_mode" | /usr/bin/tee "$release_reports/STATUS.txt"
fi
release_completed=true
printf 'Created dist/%s.dmg and dist/%s.zip\n' "$release_name" "$release_name"
+8
View File
@@ -0,0 +1,8 @@
import AppKit
// Reuse the approved application icon; no artwork or user files are altered.
guard CommandLine.arguments.count == 3,
let icon = NSImage(contentsOfFile:CommandLine.arguments[1]),
NSWorkspace.shared.setIcon(icon,forFile:CommandLine.arguments[2],options:[]) else {
fputs("Could not set the disk image Finder icon.\n",stderr)
exit(1)
}
+8
View File
@@ -0,0 +1,8 @@
#!/bin/bash
set -euo pipefail
notary_profile="${1:-KJMixPro}"
printf 'Save Apple notarization credentials as Keychain profile "%s".\n' "$notary_profile"
printf 'Apple will prompt for your developer Apple ID, team ID and an app-specific password.\n'
printf 'Enter them in this Terminal session, not in chat or source code.\n'
xcrun notarytool store-credentials "$notary_profile"
printf 'Saved. Run ./scripts/release.sh to build, sign, notarize and verify the DMG.\n'