Skip to content

deployment

Nik edited this page May 30, 2026 · 2 revisions

Deployment

Purpose

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.

Release triggers

The Release workflow runs when either of these happens:

  • A push to main touches any of:
    • src/**
    • create-app-bundle.sh
    • entitlements.plist
    • sparkle-entitlements.plist
  • A manual workflow_dispatch, which accepts an optional version input (without the leading v). 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.

The release pipeline

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]
Loading

Step detail

  1. Checkout with fetch-depth: 0 so tags and full history are available for version resolution and build numbering.
  2. Show toolchain logs sw_vers, xcodebuild, SDK version, and swift --version for debugging.
  3. Import codesigning certificates via apple-actions/import-codesign-certs, using the APPLE_DEVELOPER_CERTIFICATE_P12_BASE64 and APPLE_DEVELOPER_CERTIFICATE_PASSWORD secrets.
  4. Resolve version. If the version dispatch input is set, it is used verbatim. Otherwise the latest tag (git describe --tags --abbrev=0, default v0.0.0) is parsed and the patch component is incremented. The build number is git rev-list --count HEAD. Both are written to GITHUB_OUTPUT.
  5. Verify bundled CLIProxyAPI. The binary committed at /Users/nikhilanand/droidproxy/src/Sources/Resources/cli-proxy-api must exist; it is made executable, run with -h (which prints the version banner on the first line and exits 0 — running with no args would block trying to start the server), and its version parsed from the CLIProxyAPI 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.
  6. Build app bundle. create-app-bundle.sh runs with environment CODESIGN_IDENTITY (from APPLE_DEVELOPER_ID_APPLICATION), APP_VERSION (the resolved version), and TARGET_ARCH=arm64. The script:
    • runs swift build -c release --arch arm64 from src/,
    • assembles DroidProxy.app with Contents/MacOS/CLIProxyMenuBar, Contents/Resources/ (including cli-proxy-api and AppIcon.icns), and Contents/Frameworks/Sparkle.framework,
    • adds an @loader_path/../Frameworks rpath for Sparkle,
    • injects CFBundleShortVersionString and CFBundleVersion into Info.plist with PlistBuddy,
    • signs the bundled binary, Sparkle XPC services / Autoupdate / Updater.app (with sparkle-entitlements.plist), the framework, the main executable, and finally the whole bundle, all with --options runtime --timestamp.
  7. Verify code signature with codesign --verify --deep --strict and a spctl -a -vvv -t install Gatekeeper check (which is expected to warn before notarization).
  8. Notarize. The app is zipped with ditto, submitted with xcrun notarytool submit --wait --output-format json (using APPLE_ID, APPLE_TEAM_ID, APPLE_APP_SPECIFIC_PASSWORD). A non-Accepted status pulls the notary log and fails the job. On success the ticket is stapled with xcrun stapler staple and validated, and the notarization zip is removed.
  9. Create ZIP. The stapled app is zipped to DroidProxy-arm64.zip with ditto -c -k --sequesterRsrc --keepParent.
  10. Calculate checksum. shasum -a 256 writes DroidProxy-arm64.zip.sha256.
  11. Sparkle sign. The workflow locates sign_update under src/.build (skipping old_dsa) and signs the zip with the EdDSA private key from the SPARKLE_PRIVATE_KEY secret (--ed-key-file -). The sparkle:edSignature and byte length are captured for the appcast.
  12. Update appcast. A new <item> (title, sparkle:version = build number, sparkle:shortVersionString, sparkle:minimumSystemVersion 13.0, pubDate, and an <enclosure> pointing at the GitHub release download with the EdDSA signature and length) is inserted into /Users/nikhilanand/droidproxy/appcast.xml right after the <language>en</language> line, so the newest item sits at the top of the feed.
  13. Commit appcast and tag. The workflow commits appcast.xml as github-actions[bot], rebases on origin/main, pushes, then creates and pushes the annotated tag v<version>.
  14. Create GitHub Release with softprops/action-gh-release, attaching DroidProxy-arm64.zip and its .sha256, auto-generating release notes, and using a body that documents the Apple Silicon download and checksum verification.

Dependency auto-bump workflow

/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]
Loading

Step detail:

  • Get latest upstream release reads the GitHub API for router-for-me/CLIProxyAPI and strips the leading v from 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 mentions CLIProxyAPI 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 upstream version/tag are passed through env vars rather than direct ${{ }} expansion so a crafted release tag can't inject shell), validates and extracts the archive, removes any legacy cli-proxy-api-plus, copies the binary over src/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-request to open chore(deps): bump CLIProxyAPI to <version>. Merging that PR to main changes src/**, which triggers the Release workflow above.

Sparkle auto-update mechanics

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.

Local / manual release steps

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.sh

Notarize 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.zip

The 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.

Entry points

  • /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.

Related pages

Clone this wiki locally