-
Notifications
You must be signed in to change notification settings - Fork 14
deployment
DroidProxy ships as a single signed, notarized, Sparkle-auto-updating macOS
.app for Apple Silicon. This page documents how a release is produced, how the
bundled cli-proxy-api runtime is kept current, how Sparkle delivers
updates to installed copies, and how to cut a release by hand.
Release automation lives in two GitHub Actions workflows:
-
/Users/nikhilanand/droidproxy/.github/workflows/release.yml— builds, signs, notarizes, and publishes a release. -
/Users/nikhilanand/droidproxy/.github/workflows/update-cliproxyapi.yml— keeps the bundled backend binary current and opens a bump pull request.
The build script /Users/nikhilanand/droidproxy/create-app-bundle.sh assembles
and signs the .app. There is a single arm64 build; there is no Intel /
x86_64 appcast or release path.
The Release workflow runs when either of these happens:
- A push to
maintouches any of:src/**create-app-bundle.shentitlements.plistsparkle-entitlements.plist
- A manual
workflow_dispatch, which accepts an optionalversioninput (without the leadingv). When left empty, the patch number is auto-incremented from the latest git tag.
The job runs on macos-26 with contents: write permission and a
concurrency group of release (with cancel-in-progress: false) so two
releases never run at once.
graph TD
A[Push to main touching src/**, create-app-bundle.sh, entitlements] --> C
B[workflow_dispatch with optional version] --> C
C[Checkout full history] --> D[Import Developer ID certs]
D --> E[Resolve version: input or auto-increment patch from latest tag]
E --> F[Verify bundled cli-proxy-api + detect its version]
F --> G[create-app-bundle.sh: swift build -c release --arch arm64, assemble + sign .app]
G --> H[Verify codesign + Gatekeeper spctl]
H --> I[Notarize: notarytool submit --wait, then staple]
I --> J[Zip DroidProxy-arm64.zip]
J --> K[sha256 checksum]
K --> L[Sparkle sign_update with EdDSA private key]
L --> M[Insert new item into appcast.xml]
M --> N[Commit appcast, tag v-version, push]
N --> O[Create GitHub Release with zip + checksum]
-
Checkout with
fetch-depth: 0so tags and full history are available for version resolution and build numbering. -
Show toolchain logs
sw_vers,xcodebuild, SDK version, andswift --versionfor debugging. -
Import codesigning certificates via
apple-actions/import-codesign-certs, using theAPPLE_DEVELOPER_CERTIFICATE_P12_BASE64andAPPLE_DEVELOPER_CERTIFICATE_PASSWORDsecrets. -
Resolve version. If the
versiondispatch input is set, it is used verbatim. Otherwise the latest tag (git describe --tags --abbrev=0, defaultv0.0.0) is parsed and the patch component is incremented. The build number isgit rev-list --count HEAD. Both are written toGITHUB_OUTPUT. -
Verify bundled CLIProxyAPI. The binary committed at
/Users/nikhilanand/droidproxy/src/Sources/Resources/cli-proxy-apimust exist; it is made executable, run with-h(which prints the version banner on the first line and exits0— running with no args would block trying to start the server), and its version parsed from theCLIProxyAPI Version: <x.y.z>,banner. A missing binary or undetectable version fails the build. Upstream is downloaded only by the dependency-bump workflow, not here. -
Build app bundle.
create-app-bundle.shruns with environmentCODESIGN_IDENTITY(fromAPPLE_DEVELOPER_ID_APPLICATION),APP_VERSION(the resolved version), andTARGET_ARCH=arm64. The script:- runs
swift build -c release --arch arm64fromsrc/, - assembles
DroidProxy.appwithContents/MacOS/CLIProxyMenuBar,Contents/Resources/(includingcli-proxy-apiandAppIcon.icns), andContents/Frameworks/Sparkle.framework, - adds an
@loader_path/../Frameworksrpath for Sparkle, - injects
CFBundleShortVersionStringandCFBundleVersionintoInfo.plistwithPlistBuddy, - signs the bundled binary, Sparkle XPC services /
Autoupdate/Updater.app(withsparkle-entitlements.plist), the framework, the main executable, and finally the whole bundle, all with--options runtime --timestamp.
- runs
-
Verify code signature with
codesign --verify --deep --strictand aspctl -a -vvv -t installGatekeeper check (which is expected to warn before notarization). -
Notarize. The app is zipped with
ditto, submitted withxcrun notarytool submit --wait --output-format json(usingAPPLE_ID,APPLE_TEAM_ID,APPLE_APP_SPECIFIC_PASSWORD). A non-Acceptedstatus pulls the notary log and fails the job. On success the ticket is stapled withxcrun stapler stapleand validated, and the notarization zip is removed. -
Create ZIP. The stapled app is zipped to
DroidProxy-arm64.zipwithditto -c -k --sequesterRsrc --keepParent. -
Calculate checksum.
shasum -a 256writesDroidProxy-arm64.zip.sha256. -
Sparkle sign. The workflow locates
sign_updateundersrc/.build(skippingold_dsa) and signs the zip with the EdDSA private key from theSPARKLE_PRIVATE_KEYsecret (--ed-key-file -). Thesparkle:edSignatureand byte length are captured for the appcast. -
Update appcast. A new
<item>(title,sparkle:version= build number,sparkle:shortVersionString,sparkle:minimumSystemVersion13.0,pubDate, and an<enclosure>pointing at the GitHub release download with the EdDSA signature and length) is inserted into/Users/nikhilanand/droidproxy/appcast.xmlright after the<language>en</language>line, so the newest item sits at the top of the feed. -
Commit appcast and tag. The workflow commits
appcast.xmlasgithub-actions[bot], rebases onorigin/main, pushes, then creates and pushes the annotated tagv<version>. -
Create GitHub Release with
softprops/action-gh-release, attachingDroidProxy-arm64.zipand its.sha256, auto-generating release notes, and using a body that documents the Apple Silicon download and checksum verification.
/Users/nikhilanand/droidproxy/.github/workflows/update-cliproxyapi.yml keeps
the vendored backend binary current. It runs on a 12-hourly cron
(0 */12 * * *) and on workflow_dispatch, on ubuntu-latest, with an
AUTO_UPDATE_TOKEN.
graph TD
A[Cron every 12h or manual dispatch] --> B[Fetch latest router-for-me/CLIProxyAPI release]
B --> C{Open bump PR exists OR version already bundled?}
C -- yes --> D[Skip]
C -- no --> E[Close superseded bump PRs]
E --> F[Download darwin_aarch64 tarball, extract binary]
F --> G[Replace src/Sources/Resources/cli-proxy-api, rm legacy -plus, chmod +x]
G --> H[Size sanity check >= 1 MiB]
H --> I[Open bump PR: bump-cliproxyapi-version]
I --> J[Merging the PR to main triggers the Release workflow]
Step detail:
-
Get latest upstream release reads the GitHub API for
router-for-me/CLIProxyAPIand strips the leadingvfrom the tag. -
Check if already up to date skips when an open PR branch
bump-cliproxyapi-<version>already exists, or when a recent commit message already mentionsCLIProxyAPI to <version>. -
Close superseded bump PRs closes any older open
bump-cliproxyapi-*PRs (with--delete-branch). -
Download CLIProxyAPI fetches
CLIProxyAPI_<version>_darwin_aarch64.tar.gz(with retries; the upstreamversion/tagare passed through env vars rather than direct${{ }}expansion so a crafted release tag can't inject shell), validates and extracts the archive, removes any legacycli-proxy-api-plus, copies the binary oversrc/Sources/Resources/cli-proxy-api, makes it executable, and fails if the result is smaller than 1 MiB. -
Create pull request uses
peter-evans/create-pull-requestto openchore(deps): bump CLIProxyAPI to <version>. Merging that PR tomainchangessrc/**, which triggers theReleaseworkflow above.
Installed copies update themselves through Sparkle, configured by keys in
/Users/nikhilanand/droidproxy/src/Info.plist:
| Key | Value | Role |
|---|---|---|
SUFeedURL |
https://raw.githubusercontent.com/anand-92/droidproxy/main/appcast.xml |
The appcast feed Sparkle polls. |
SUPublicEDKey |
sB98dHKSN9fEe3vmVAufZoI4TbRWE6hHvAGSbzKweYM= |
EdDSA public key used to verify each enclosure's sparkle:edSignature. |
SUScheduledCheckInterval |
86400 |
Automatic check interval in seconds (24 hours). |
SUEnableAutomaticChecks |
true |
Sparkle checks for updates on schedule. |
SUAutomaticallyUpdate |
false |
Updates are not silently installed; the user is prompted. |
The feed lives in the repo and is served raw from main, so the appcast commit
in the release workflow is what makes a new version visible to clients. Each
enclosure carries the EdDSA signature produced by sign_update; the matching
public key is the only one that validates it, so a release zip cannot be swapped
without re-signing with the private key held in CI secrets.
For a hand-built release, replicate the workflow locally. From the commands in
/Users/nikhilanand/droidproxy/AGENTS.md:
Build the signed bundle (picks up CODESIGN_IDENTITY / APP_VERSION /
TARGET_ARCH from the environment when present):
./create-app-bundle.shNotarize and staple:
ditto -c -k --sequesterRsrc --keepParent "DroidProxy.app" "DroidProxy-notarize.zip"
xcrun notarytool submit "DroidProxy-notarize.zip" --keychain-profile "notarytool" --wait
xcrun stapler staple "DroidProxy.app"Sign the distribution zip for Sparkle:
src/.build/artifacts/sparkle/Sparkle/bin/sign_update DroidProxy-arm64.zipThe resulting sparkle:edSignature and byte length go into a new <item> in
appcast.xml, after which you tag v<version> and publish a GitHub release with
the zip and its checksum. Do not use dev-relaunch.sh for releases — that is the
development loop only.
-
/Users/nikhilanand/droidproxy/.github/workflows/release.yml— release pipeline (build, sign, notarize, appcast, tag, GitHub release). -
/Users/nikhilanand/droidproxy/.github/workflows/update-cliproxyapi.yml— backend dependency auto-bump. -
/Users/nikhilanand/droidproxy/create-app-bundle.sh— local and CI build / signing script. -
/Users/nikhilanand/droidproxy/appcast.xml— Sparkle feed. -
/Users/nikhilanand/droidproxy/src/Info.plist— Sparkle and bundle metadata.