Releasing DeviceTerm
August 10, 2026 · View on GitHub
The release workflow runs locally. make release builds, signs, notarizes, and
staples the artifacts on your Mac. make publish creates the GitHub release
and updates the Homebrew tap.
See BUILDING.md for the signing and bundle pipeline.
One-Time Setup
-
Confirm that the DeviceTerm repository is public. GitHub Releases, Sparkle, Homebrew, and the published-source obligations in
THIRD_PARTY_NOTICES.mddepend on public access. -
Copy
.env.release.exampleto.env.releaseand configure a Developer ID Application identity:cp .env.release.example .env.release security find-identity -v -p codesigning -
Configure notarization in
.env.release. PreferNOTARY_PROFILEfor anotarytoolKeychain profile. Otherwise provideAPPLE_ID,APPLE_TEAM_ID, andAPPLE_APP_PASSWORD. -
Clone the Homebrew tap and point the publisher at it:
git clone git@github.com:sethdeckard/homebrew-tap.git ../homebrew-tapSet
DEVICETERM_TAP_DIRin.env.releasewhen the checkout is not at the default../homebrew-tappath. -
Authenticate the GitHub CLI:
gh auth status -
Install the Sparkle release tools. Ensure
generate_keysandgenerate_appcastare available. SetSPARKLE_BIN_DIRwhengenerate_appcastis not onPATH. -
Generate the Sparkle EdDSA key with
generate_keys. Keep the private key in the login Keychain. Replace theSUPublicEDKeyplaceholder inSources/App/Resources/Info.plistwith the printed public key.
Back up the Sparkle private key before the first release. Existing installations trust this key for future updates. Losing it requires those users to reinstall manually before they can trust a replacement key.
Export the private key with generate_keys -x, store it offline or in a
password manager, and never commit it.
Developer ID certificates and notarization credentials can be regenerated
through the Apple Developer account. You may also export the Developer ID
certificate as a .p12 when preparing another release machine.
Scrub Public Assets
Before publishing a new image or binary asset, remove EXIF, C2PA, XMP, local paths, and macOS extended attributes. Preserve the original image encoding when removing a metadata chunk is sufficient; otherwise re-encode it.
Use ExifTool or another format-aware tool that supports the asset's container. Inspect embedded metadata before editing it, including the C2PA/JUMBF group:
exiftool -a -G1 -s <file>
exiftool -jumbf:all -G3 -b -j -u -struct <file>
Remove the EXIF, XMP, C2PA, and path-bearing fields reported by the inspector. For a writable format, this ExifTool command removes the three standard metadata groups without deleting an ICC color profile:
exiftool -EXIF:all= -XMP:all= -JUMBF:all= -overwrite_original <file>
If the tool cannot delete embedded metadata from that format, re-encode the asset and inspect the result. Check that required color and orientation information still renders correctly. Repeat the format-aware inspection, then run these secondary checks:
strings <file> | grep -iE 'c2pa|gpt-image|xmp|exif|/Users/|file://' # heuristic; expect empty
xattr -c <file>
xattr <file> # expect empty
Prepare the Release
Set the Version and Notes
-
Choose the public version according to SemVer. During
0.x, a minor release may break the public CLI or JSON contract. Patch releases remain compatible. Starting with1.0, breaking public changes require a new major version. -
Set the same release version in:
Sources/App/Resources/Info.plistscripts/make-app-bundle.shSources/DaemonProtocol/DeviceTermVersion.swift
-
Review
DaemonProtocolInfo.wireVersionseparately. It identifies the internal app, daemon, CLI, and shim RPC contract during an update. Change it only with an incompatible wire change, not for an ordinary product release.DaemonInfo.versionmust continue to mirror it. -
Write the GitHub release notes at
release/release-notes-<version>.md. If this file is absent,make publishasks GitHub to generate the notes. -
Write the in-app update notes at
release/release-notes-<version>.html.make publishembeds this file in the Sparkle appcast. If it is absent, the update popover reports that no release notes are available. -
Scrub any new public assets as described above.
Run the Release Gates
-
Run the full repository gate:
make verify -
Build the release tree with Xcode 26.4 and Xcode 26.6. These are the currently covered build toolchains. Xcode 27 beta remains unsupported until its build failure is resolved.
-
Run
make probein every Simulator-services environment claimed by the release. Record each result inSources/CoreSimulatorBridge/as-tested.md.
The Simulator live track shuts down every running Simulator. Save any work in them before continuing.
-
Run the default Simulator live track for every claimed iOS runtime:
make test-live -
Complete the watchOS manual checklist for every claimed watchOS runtime. Its setup runs the automated watch live track before the required visual checks.
-
Run the physical-device track for every physical-device and OS combination claimed by the release:
make test-device-liveThis track does not reboot or shut down the physical device. Physical-device stream compatibility is independent of Simulator services and compiler compatibility.
Commit and Build
-
Commit the complete release-ready tree.
-
Push the release commit to GitHub before publishing. When the version tag does not already exist,
gh release createcreates it from the latest state of the default branch. The remote default branch must therefore contain the release commit. -
Confirm that the tracked tree is clean:
git status --shortmake releaserefuses tracked changes because the About panel records the source commit used for the build. -
Build the release:
make releaseThis produces the signed, notarized, and stapled
release/deviceterm-<version>.dmg, along with the intermediate release artifacts.
Publish the Release
Publishing changes GitHub and the Homebrew tap. First inspect the planned version, checksum, appcast URL, and cask:
scripts/publish-release.sh --dry-run
When the dry run is correct:
make publish
The publisher performs these operations in order:
- Calculate the DMG checksum.
- Generate the EdDSA-signed Sparkle appcast.
- Create the GitHub release and upload the DMG and
appcast.xml. - Render, commit, and push the Homebrew cask.
The cask is pushed last so it cannot point at a release artifact that failed to upload.
Verify the Published Release
-
Confirm that GitHub release
v<version>points at the release commit. Verify that the commit shown in About DeviceTerm is reachable in the public repository. -
Confirm that the release contains
deviceterm-<version>.dmgandappcast.xml, and that theSUFeedURLresolves to the published appcast. -
Download the DMG from GitHub Releases, mount it, drag DeviceTerm to Applications, and launch it. Verify Gatekeeper acceptance:
spctl --assess --type exec /Applications/DeviceTerm.app -
Install through Homebrew and launch the app:
brew install --cask sethdeckard/tap/deviceterm -
Run
deviceterm versioninside a DeviceTerm tab. Confirm the public release version and expected internal RPC wire version. -
Choose DeviceTerm ▸ Check for Updates…. Confirm that it reports no newer update and that the update indicator dismisses normally.
-
Confirm that
LICENSEexists at the mounted DMG root and at/Applications/DeviceTerm.app/Contents/Resources/LICENSE. -
Confirm that
Contents/Resources/containsTHIRD_PARTY_NOTICES.mdand thelicenses/directory. -
Confirm that the Kitty-derived shell integration carries
GPL-3.0.txtat both packaged locations:Contents/Resources/ghostty/ghostty/shell-integration/GPL-3.0.txt Contents/Resources/Libghostty_GhosttyKitResources.bundle/Resources/ghostty/ghostty/shell-integration/GPL-3.0.txt -
Open DeviceTerm ▸ About DeviceTerm. Verify the copyright, GPL notice, Read the license link, and Third-Party Notices link.
Maintenance Notes
Remove the Login Item During Uninstall
The embedded daemon registers through SMAppService. Homebrew's zap removes
DeviceTerm application data but does not unregister the login item.
Quit DeviceTerm, open System Settings ▸ General ▸ Login Items & Extensions, and remove or disable the DeviceTerm background item when fully uninstalling the app. Daemon idle exit stops the process but does not unregister the service.
Change the Apple Team
Changing from one Apple Team to another is a credential change, not a code
change. PeerIdentity.swift reads the daemon's Team ID from its signature and
derives the host bundle identifier from the daemon bundle identifier.
- Create a Developer ID Application certificate for the new Team.
- Update
CODESIGN_IDENTITYand the notarization credentials. - Keep the
com.devicetermbundle identifier. - Keep the existing Sparkle EdDSA key so installed copies continue to trust updates.
- Expect macOS to request permissions again when it associates them with the new signing identity.