From 008e9bc175ac99a6096bb45e26969b3d82f294b9 Mon Sep 17 00:00:00 2001 From: Mark Gregory Date: Tue, 3 Nov 2026 15:08:34 +0000 Subject: [PATCH] updated release script --- README.md | 2 +- docs/RELEASING.md | 57 +++++++++++++ docs/VERIFICATION.md | 6 ++ scripts/release.sh | 156 +++++++++++++++++++++++++++++++++- scripts/set-dmg-icon.swift | 8 ++ scripts/setup-notarization.sh | 8 ++ 6 files changed, 234 insertions(+), 3 deletions(-) create mode 100644 docs/RELEASING.md create mode 100644 scripts/set-dmg-icon.swift create mode 100755 scripts/setup-notarization.sh diff --git a/README.md b/README.md index a856a7f..2baeec0 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/docs/RELEASING.md b/docs/RELEASING.md new file mode 100644 index 0000000..1c8a9d5 --- /dev/null +++ b/docs/RELEASING.md @@ -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). diff --git a/docs/VERIFICATION.md b/docs/VERIFICATION.md index e71ffd7..f620942 100644 --- a/docs/VERIFICATION.md +++ b/docs/VERIFICATION.md @@ -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. diff --git a/scripts/release.sh b/scripts/release.sh index 8bf9179..bb1db4e 100755 --- a/scripts/release.sh +++ b/scripts/release.sh @@ -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" diff --git a/scripts/set-dmg-icon.swift b/scripts/set-dmg-icon.swift new file mode 100644 index 0000000..7e3e53e --- /dev/null +++ b/scripts/set-dmg-icon.swift @@ -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) +} diff --git a/scripts/setup-notarization.sh b/scripts/setup-notarization.sh new file mode 100755 index 0000000..68aa1c4 --- /dev/null +++ b/scripts/setup-notarization.sh @@ -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'