DockDash - App Release and Updates#
This is the single end-to-end procedure for preparing, notarizing, publishing, and testing a DockDash release. It follows the tested SoundDash release method, adapted for DockDash’s bundle identifier, signing keys, support repository, and Dock-specific checks. Follow the execution checklist from top to bottom. The reference sections afterward explain setup and why the order matters.
The production update feed is https://marcoue.github.io/dockdash-support/appcast.xml. Any Sparkle-enabled copy of DockDash can see a release as soon as this feed advertises it. A GitHub release marked Pre-release is still offered by Sparkle if it is in this feed.
Execute this release in order#
1. Set the release version and build in Xcode#
In Xcode, select the DockDash target, then General → Identity:
- Set Version to the new user-facing version.
- Increase Build above every build already in the public appcast.
- Save the project changes.
- Commit the release source and version changes before archiving. Record that commit alongside the support-repository release tag.
The commands below use 1.0 build 1 as a first-release example; replace both values together for later releases. Sparkle compares the build number (CFBundleVersion). The GitHub tag format used here is version_build, for example 1.2_6.
2. Archive and export the notarized app#
- Select the DockDash scheme and Any Mac (arm64, x86_64) archive destination.
- Choose Product → Archive.
- In Organizer → Archives, select the new archive and choose Distribute App → Direct Distribution to submit it to Apple for notarization.
- When Organizer shows Ready to distribute, choose Export Notarized App and save it in Downloads as
DockDash.app. If the archive already shows Ready to distribute, choose Export Notarized App directly; do not start the distribution flow again. - Confirm the exported bundle has the expected identifier, version, build, minimum macOS, feed, and public key. When opening it for About/functional checks, coordinate the existing protection engine first.
3. Create, sign, and notarize the final DMG#
Before running this block, complete One-time setup → Apple notarization credential profile and confirm the
DockDash-NotaryKeychain profile was created successfully. Do not start the DMG block until that profile exists; the block creates and signs the DMG before it submits it to Apple.
Update the version and build to match Xcode. The signing command uses the confirmed DockDash Developer ID identity; change it only if signing with a different identity. Update the exported-app path if Xcode saved the app elsewhere. Run this entire block in Terminal. It creates one visible output folder named DockDash 1.0 Release (version only, no build number); the temporary DMG source folder is created outside Downloads and automatically removed:
(
# Keep failures from closing the Warp shell.
set -e
# CHANGE for every release: use Xcode Target > General > Identity > Version.
APP_VERSION="1.0.3"
# CHANGE for every release: use the matching Xcode Build value.
APP_BUILD="4"
# CHANGE only if the exported app is in a different location.
EXPORTED_APP="/Users/marc/Downloads/DockDash.app"
RELEASE_DIR="/Users/marc/Downloads/DockDash $APP_VERSION Release"
DMG_PATH="$RELEASE_DIR/DockDash.dmg"
# This is the only output folder. It must not already exist.
mkdir "$RELEASE_DIR"
# Temporary DMG source; automatically removed when this block finishes.
PACKAGE_DIR="$(mktemp -d "${TMPDIR:-/tmp}/DockDash-package.XXXXXX")"
trap 'rm -rf "$PACKAGE_DIR"' EXIT
APP_PLIST="$EXPORTED_APP/Contents/Info.plist"
ACTUAL_VERSION="$(/usr/libexec/PlistBuddy -c 'Print :CFBundleShortVersionString' "$APP_PLIST")"
ACTUAL_BUILD="$(/usr/libexec/PlistBuddy -c 'Print :CFBundleVersion' "$APP_PLIST")"
ACTUAL_ID="$(/usr/libexec/PlistBuddy -c 'Print :CFBundleIdentifier' "$APP_PLIST")"
if [ "$ACTUAL_VERSION" != "$APP_VERSION" ] || [ "$ACTUAL_BUILD" != "$APP_BUILD" ] || [ "$ACTUAL_ID" != "com.marcoue.DockDash" ]; then
echo "Exported app identity/version/build does not match this release."
exit 1
fi
codesign --verify --deep --strict --verbose=2 "$EXPORTED_APP"
xcrun stapler validate "$EXPORTED_APP"
ditto "$EXPORTED_APP" "$PACKAGE_DIR/DockDash.app"
ln -s /Applications "$PACKAGE_DIR/Applications"
hdiutil create \
-volname "DockDash" \
-srcfolder "$PACKAGE_DIR" \
-format UDZO \
"$DMG_PATH"
codesign --sign "Developer ID Application: Marc Ouellet (M99GK4VVC8)" \
--timestamp \
--identifier "com.marcoue.DockDash.dmg" \
"$DMG_PATH"
codesign --verify --verbose=2 "$DMG_PATH"
xcrun notarytool submit "$DMG_PATH" \
--keychain-profile "DockDash-Notary" \
--wait
xcrun stapler staple "$DMG_PATH"
xcrun stapler validate "$DMG_PATH"
)Continue only after Apple’s submission is Accepted and stapler validate succeeds. The file in the release folder is now the final DMG. Keep its filename exactly DockDash.dmg.
Resume if the notarization profile was missing#
If the block created and signed the DMG but notarytool submit reports No Keychain password item found for profile: DockDash-Notary, do not rerun the full block: its mkdir command will stop because the release folder already exists. Complete One-time setup → Apple notarization credential profile first, then resume against the existing signed DMG. For the example version/build above, run:
RELEASE_DIR="/Users/marc/Downloads/DockDash 1.0.3 Release"
DMG_PATH="$RELEASE_DIR/DockDash.dmg"
xcrun notarytool submit "$DMG_PATH" \
--keychain-profile "DockDash-Notary" \
--wait
# Continue only after Apple's result is Accepted.
xcrun stapler staple "$DMG_PATH"
xcrun stapler validate "$DMG_PATH"For a different release version, change RELEASE_DIR to that release’s existing output folder. After stapling succeeds, treat the DMG as final. Any change requires signing and notarizing the replacement DMG, then regenerating its Sparkle appcast signature.
Before publishing it, open this DMG in Finder. Confirm it contains DockDash.app and the Applications shortcut, then test dragging the app into Applications and launching the copied app. Check Accessibility/event-access onboarding and the bottom-Dock protection flow, using one agreed protection engine. Confirm repeated Settings open/close/reopen and saved display preferences. When possible, test on another Mac with Gatekeeper quarantine intact. This block creates the DMG directly in the release folder, so there is no separate cp command to stage a second copy.
4. Publish the GitHub release and its DMG#
Open New release:
- Create the tag using the exact pattern
version_build. For version1.2, build6, the tag is1.2_6. - Set the release title to
version (build), such as1.2 (6). - Enter the release notes.
- Attach the exact final
DockDash.dmgfrom the release folder created in step 3. Its uploaded asset name must remainDockDash.dmg. - For a production release, choose the normal None release label, not Pre-release. GitHub may not show a separate Latest release choice; it automatically labels the newest eligible published release as latest.
- Publish the release and confirm its tag page shows the notes and the
DockDash.dmgasset.
If this version/build already has a published GitHub release, open it and verify its existing tag, notes, and asset instead of creating a duplicate. The appcast must use the tag that actually exists.
Keep rehearsal releases on the separate test feed. Publish an entry to the production feed only when you intend every app checking it to be offered that build.
5. Generate and sign the appcast#
Update the version and build to match step 3. The tag is derived from them. Replace the sample Markdown between the marked lines with this release’s “What’s New” text, leaving both marker lines in place; the command removes those markers before embedding the notes. This command downloads the current public feed, keeps its earlier entries, signs the final DMG with Sparkle, and embeds the notes in the feed instead of linking to GitHub’s release page. It uses the single release folder created in step 3; the temporary DMG source folder has already been removed.
Run the entire block in Terminal. When macOS asks to unlock the login Keychain for the Sparkle signing key, enter the login Keychain password and choose Allow.
(
set -e
#### CHANGE for every release: use the same Xcode Version value as step 3.
APP_VERSION="1.0.3"
#### CHANGE for every release: use the same Xcode Build value as step 3.
APP_BUILD="4"
#### CHANGE the Markdown text between the marker lines for every release.
RELEASE_NOTES="$(cat <<'NOTES'
# MODIFY FROM HERE IN MARKDOWN--->
## What’s New in DockDash 1.0.3
- Fixed OS permission handling and added an informational popup explaining why these permissions are necessary.
- Fix Dock protection permission flow
# <--- TO HERE
NOTES
)"
RELEASE_NOTES="$(printf '%s\n' "$RELEASE_NOTES" | sed '1d;$d')"
RELEASE_TAG="${APP_VERSION}_${APP_BUILD}"
STAGING_DIR="/Users/marc/Downloads/DockDash $APP_VERSION Release"
DMG_PATH="$STAGING_DIR/DockDash.dmg"
APPCAST_URL="https://marcoue.github.io/dockdash-support/appcast.xml"
SUPPORT_REPO="https://github.com/marcoue/dockdash-support"
#### If Xcode uses a custom Derived Data location, change this to the path in Xcode > Settings > Locations.
DERIVED_DATA="$HOME/Library/Developer/Xcode/DerivedData"
if [ ! -f "$DMG_PATH" ]; then
echo "Final DMG not found: $DMG_PATH"
exit 1
fi
# One-time setup must have published the initial empty feed already.
# Stop on HTTP/network failure; never replace a failed download with an empty feed.
curl -fsSL "$APPCAST_URL" -o "$STAGING_DIR/appcast.xml"
APPCAST_TOOL="$(find "$DERIVED_DATA" \
-path '*/DockDash-*/SourcePackages/artifacts/sparkle/Sparkle/bin/generate_appcast' \
-type f -print -quit)"
if [ -z "$APPCAST_TOOL" ]; then
echo "Could not find Sparkle's generate_appcast tool under $DERIVED_DATA"
echo "Open the DockDash project in Xcode and let Swift packages resolve."
exit 1
fi
"$APPCAST_TOOL" \
--account DockDash \
--maximum-deltas 0 \
--maximum-versions 0 \
--download-url-prefix "$SUPPORT_REPO/releases/download/$RELEASE_TAG/" \
--link "$SUPPORT_REPO" \
"$STAGING_DIR"
python3 - "$STAGING_DIR/appcast.xml" "$APP_VERSION" "$APP_BUILD" "$RELEASE_TAG" "$SUPPORT_REPO" "$RELEASE_NOTES" <<'PY'
import sys
import xml.etree.ElementTree as ET
(
appcast_path,
app_version,
app_build,
release_tag,
support_repo,
release_notes_text,
) = sys.argv[1:]
sparkle_ns = "http://www.andymatuschak.org/xml-namespaces/sparkle"
ET.register_namespace("sparkle", sparkle_ns)
ET.register_namespace("dc", "http://purl.org/dc/elements/1.1/")
tree = ET.parse(appcast_path)
channel = tree.getroot().find("channel")
if channel is None:
raise SystemExit("Could not find the appcast channel")
matches = []
for item in channel.findall("item"):
build = item.find(f"{{{sparkle_ns}}}version")
if build is not None and (build.text or "").strip() == app_build:
matches.append(item)
if len(matches) != 1:
raise SystemExit(f"Expected one appcast item for build {app_build}; found {len(matches)}")
item = matches[0]
short_version = item.find(f"{{{sparkle_ns}}}shortVersionString")
if short_version is None or (short_version.text or "").strip() != app_version:
raise SystemExit(f"Appcast item for build {app_build} does not show version {app_version}")
enclosure = item.find("enclosure")
if enclosure is None:
raise SystemExit(f"Appcast item for build {app_build} has no enclosure")
expected_download = f"{support_repo}/releases/download/{release_tag}/DockDash.dmg"
if enclosure.get("url") != expected_download:
raise SystemExit(
"Wrong DMG URL in appcast. Expected "
f"{expected_download}, found {enclosure.get('url')}"
)
if not enclosure.get(f"{{{sparkle_ns}}}edSignature") or not enclosure.get("length"):
raise SystemExit("Appcast enclosure is missing its Sparkle signature or file length")
# Remove any external release-notes link for this update.
for release_notes_link in item.findall(f"{{{sparkle_ns}}}releaseNotesLink"):
item.remove(release_notes_link)
# Embed the release notes as Markdown in this appcast item.
description = item.find("description")
if description is None:
description = ET.Element("description")
item.insert(list(item).index(enclosure), description)
description.set(f"{{{sparkle_ns}}}format", "markdown")
description.text = release_notes_text.strip()
if hasattr(ET, "indent"):
ET.indent(tree, space=" ")
tree.write(appcast_path, encoding="utf-8", xml_declaration=True)
print(f"Verified version {app_version}, build {app_build}")
print(f"DMG: {expected_download}")
print("Embedded the What's New text as Markdown.")
print(f"Updated {appcast_path}")
PY
)This procedure signs the DMG with EdDSA; it does not opt into Sparkle signed-feed verification (SURequireSignedFeed). If signed feeds are adopted later, the XML post-processing in the block above must happen before final feed signing. --maximum-deltas 0 keeps this procedure limited to the complete DMG asset.
Do not publish if this block reports an error. Inspect the generated file at:
/Users/marc/Downloads/DockDash 1.0 Release/appcast.xml
Confirm it contains the new version and build, the correct GitHub download URL, embedded Markdown release notes, the DMG signature, and the file length. Do not edit the signature or file length by hand.
6. Publish the appcast#
- Open the
dockdash-supportrepository on GitHub and selectappcast.xmlat the root of themainbranch. - Click Edit and replace the entire file with the generated
appcast.xmlfrom the release folder. - Commit the change to
main. - Wait for GitHub Pages to publish the commit, then reload https://marcoue.github.io/dockdash-support/appcast.xml.
- Confirm the new entry is visible, the DMG URL uses the exact release tag from step 4, and the What’s New text appears inside its
<description>element. Confirm the DMG link reaches the release asset.
The GitHub release and DMG must be public before the appcast points to them. Keep older appcast entries and release assets so previously installed versions can still update.
7. Test the update#
- Use an installed Sparkle-enabled DockDash build with a lower build number than the release. If no older Sparkle-enabled build has been published yet, install this first updater-enabled release manually; verify updating with the next release or use a separate test feed for rehearsal.
- Choose Check for Updates… from the DockDash menu.
- Confirm Sparkle shows the new version and the release notes load.
- Choose Install Update and allow Sparkle to finish its quit/relaunch flow.
- Confirm About DockDash shows the new version and build. Check the saved display choice and names, startup-protection preference, Accessibility/event access, bottom-Dock behavior, and repeated Settings reopening. Confirm the protection engine actually operates after relaunch; a status indicator alone is insufficient evidence.
If the installed app already has the newest build advertised by the feed, You’re up to date is expected.
One-time setup#
These steps are done once per development Mac or when the relevant credential or distribution configuration changes.
Developer ID and Hardened Runtime#
- In Xcode → Settings → Accounts → Manage Certificates, confirm a valid Developer ID Application identity for team
M99GK4VVC8. The release identity currently used by the DMG command isDeveloper ID Application: Marc Ouellet (M99GK4VVC8). - Keep Hardened Runtime enabled for Debug and Release.
- The Debug configuration uses
DockDash/DockDash-Debug.entitlementsto disable library validation so a local Debug launch can load Sparkle’s ad-hoc-signed framework. Keep that exception Debug-only. Release must not use the library-validation exception; Xcode’s Direct Distribution export signs the embedded Sparkle code for distribution. - Keep App Sandbox disabled for DockDash’s current Accessibility and event-tap behavior. Do not copy SoundDash’s sandbox setting or entitlements.
- Test the notarized export’s permission flow and Dock protection behavior. Current source checks Accessibility trust and listen/post event access; a passing archive alone does not prove the live Dock behavior.
Apple notarization credential profile#
If the DockDash-Notary profile is not already saved in Keychain, create an app-specific password at account.apple.com under Sign-In and Security → App-Specific Passwords, then run:
xcrun notarytool store-credentials "DockDash-Notary" \
--apple-id "marc.ouellet@gmail.com" \
--team-id "M99GK4VVC8"Enter the app-specific password only when Terminal prompts. Do not put it in this note, a command, or source control. Do not use the normal Apple Account password. The profile is reused for each DMG submission.
Sparkle keys and app settings#
- The DockDash target must link Sparkle and retain one
SPUStandardUpdaterControllerin the existing application delegate. The menu’s Check for Updates… action must use that controller. SUFeedURLmust remainhttps://marcoue.github.io/dockdash-support/appcast.xml;SUPublicEDKeymust contain DockDash’s public key. Do not copy SoundDash’s key or feed.- The EdDSA private key stays in the trusted release Mac’s login Keychain under account
DockDash. Never place it in this note, the app bundle, a release asset, or GitHub. Apple’s notarization profile and Sparkle’s key are separate credentials. - The public support repository is https://github.com/marcoue/dockdash-support. Keep
appcast.xmlat the root ofmain, with GitHub Pages deploying frommainand/(root). Verify the public feed URL returns XML before publishing a release. Preserve existing appcast entries on later updates. - Use a separate test feed for updater rehearsal. Do not advertise a test build on the production appcast unless every production app should receive it.
Why this order matters#
- Export Notarized App exports the notarized app from Organizer; use that exact exported
.appas the input when creating the DMG. The DMG is a separate distribution artifact and must also be signed, notarized, and stapled. - The final DMG is then built, Developer ID signed, submitted to Apple, and stapled. The file customers download is therefore the notarized file.
generate_appcastsigns the exact final DMG with Sparkle’s EdDSA key and records its byte length. Changing or rebuilding the DMG afterward makes the appcast signature or length wrong, so regenerate the appcast from the final DMG.- Publish the GitHub release first so the download and release-notes URLs work before the appcast advertises them.
- The appcast is the update source. Sparkle does not decide update availability from GitHub’s Latest or Pre-release label.
Troubleshooting#
Sparkle offers the update, but release notes fail to load#
This procedure embeds the release notes in <description sparkle:format="markdown">; it does not require a GitHub release-page URL for the notes. Inspect the public feed and confirm the new item contains the expected text and format, without a stale <sparkle:releaseNotesLink> overriding it. Embedded Markdown requires Sparkle 2.9 or later, covered by the proposed 2.10.0 baseline. See Sparkle’s release-notes format.
Separately, verify the enclosure URL reaches the exact GitHub asset. For version 1.2, build 6, that URL ends with releases/download/1.2_6/DockDash.dmg. If the tag or asset name differs, fix the inputs and regenerate from the same final DMG, then publish the corrected appcast. The script validates this download URL; it does not validate a release-page URL or cryptographically verify the signature it just generated.
No update is offered#
- Confirm the public appcast contains a build greater than the installed app’s build.
- Confirm the app’s
SUFeedURLis the public feed above. - Confirm the installed copy includes Sparkle. A DockDash build created before Sparkle integration must be replaced manually before it can update itself.
- Allow GitHub Pages time to publish after committing the appcast.
Sparkle prints an hdiutil deprecation warning#
Sparkle may print a warning that its internal hdiutil attach invocation is deprecated. If the command continues and reports that it wrote or updated the appcast, the warning is informational; verify the generated appcast and DMG links as described above.
Apple rejects the DMG notarization#
Do not publish a rejected DMG. Use the submission ID reported by notarytool to retrieve Apple’s diagnostic log:
xcrun notarytool log "<submission-id>" \
--keychain-profile "DockDash-Notary" \
"notary-log.json"Resolve the reported issue, rebuild or re-sign the DMG as needed, submit the final DMG again, staple and validate it, then regenerate the Sparkle appcast from that final file.
References#
- SoundDash - App Release and Updates — local method adapted for this note.
- Sparkle: Programmatic setup
- Sparkle 2.10.0: generate_keys options
- Sparkle 2.10.0: generate_appcast options
- GitHub Pages: Configure a publishing source
- Sparkle: Publishing an update
- Sparkle documentation
- Apple: Packaging Mac software for distribution
- Apple: Notarizing macOS software before distribution
- Apple: Customizing the notarization workflow
- Apple Support: App-specific passwords