Skip to content

ZeroClover/SideloadedIPA

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

66 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

IPA Signing via GitHub Actions

This repository contains a GitHub Actions workflow and helper scripts to:

  • Automatically sync Development provisioning profiles with all registered devices via App Store Connect CLI (asc).
  • Read a TOML config of signing tasks.
  • For each task: download the IPA (from direct URL or GitHub Release), re-sign with zsign (directly from the P12 certificate, no Keychain) using synced profiles, and upload to an Assets server via scp.
  • Generate the itms-services manifest: after each re-sign, read the signed IPA's actual bundle id + version and emit itms.plist next to the IPA.
  • Refresh the download page: update existing app versions / add new app cards in site/apps.js, bump the Cloudflare cache-buster, deploy the page, and commit the change back.
  • Intelligent caching: Only rebuild IPAs when releases are updated or devices change, reducing workflow runtime and costs.

File Structure

  • .github/workflows/sign-and-upload.yml — the workflow (manual, webhook, and scheduled triggers)
  • scripts/sync_profiles_asc.py — syncs provisioning profiles with all devices via App Store Connect CLI
  • scripts/run_signing.py — processes configs/tasks.toml, re-signs, generates itms.plist, uploads, refreshes the download page
  • scripts/site_update.py — merges signing results into the download page data (site/apps.js) and bumps cache-busting versions
  • scripts/check_changes.py — detects changes to determine which tasks need rebuilding
  • configs/tasks.toml — TOML config defining signing tasks (and the optional [site] publishing settings)
  • configs/tasks.toml.example — example configuration file
  • .env.example — example environment variables
  • site/ — the ITMS download page (auto-maintained by the pipeline; see site/README.md)

Required Secrets / Variables

Set these at Repository → Settings → Secrets and variables → Actions:

Apple Developer Credentials

  • APPLE_DEV_CERT_P12_ENCODED — Base64-encoded Apple Developer signing P12 certificate
  • APPLE_DEV_CERT_PASSWORD — Password for the P12 certificate

App Store Connect API (for automatic profile sync)

  • ASC_KEY_ID — App Store Connect API Key ID (e.g., ABC123XYZ)
  • ASC_ISSUER_ID — App Store Connect API Issuer ID (UUID format)
  • ASC_PRIVATE_KEY — Base64-encoded .p8 private key content

Generate API keys at: https://appstoreconnect.apple.com/access/api (requires "Developer" role)

Asset Server

  • ASSETS_SERVER_IP — SSH server IP or hostname
  • ASSETS_SERVER_USER — SSH username
  • ASSETS_SERVER_CREDENTIALS — SSH password

Optional

  • DEBUG_SSH_PUBLIC_KEY — SSH public key for debug mode (only required when debug=true)

Provisioning Profile Management

The workflow automatically creates/updates Development provisioning profiles via App Store Connect CLI (asc), including:

  • All enabled iOS devices (iPhone and iPad classes)
  • All available Development certificates

Provisioning profiles are downloaded to work/profiles/ and used directly for signing. If profile sync fails, the entire workflow will fail.

TOML Config

Edit configs/tasks.toml and add entries like:

Option 1: Direct IPA URL (existing behavior)

[[tasks]]
task_name = "MyApp"
app_name = "My App"
bundle_id = "com.example.myapp"
ipa_url = "https://example.com/path/to/MyApp.ipa"
asset_server_path = "/var/www/assets/ipas/"

Option 2: GitHub Release Tracking (new, with caching)

[[tasks]]
task_name = "AnotherApp"
app_name = "Another App"
bundle_id = "com.example.anotherapp"
repo_url = "https://github.com/owner/repo"
release_glob = "*.ipa"          # Optional, default: "*.ipa"
use_prerelease = false           # Optional, default: false
asset_server_path = "/var/www/assets/"

Required fields:

  • task_name — Identifier for this task (used for profile lookup)
  • app_name — Human-friendly name (used for profile naming: "{app_name} Dev")
  • bundle_id — iOS Bundle Identifier (must exist in Apple Developer Portal)
  • Either ipa_url OR repo_url (mutually exclusive)
    • ipa_url — Direct download URL of the IPA (always rebuilds)
    • repo_url — GitHub repository URL (enables version tracking and caching)
  • asset_server_path — Destination path on asset server (if ends with /, filename is appended)

Optional fields for GitHub Release tracking:

  • release_glob — Pattern to match release assets (default: *.ipa)
  • use_prerelease — Whether to use prerelease versions (default: false)
    • If true, fetches latest prerelease; falls back to latest stable if none exist
    • If false, fetches only latest stable release

See configs/tasks.toml.example for more details.

Triggers

  • Scheduled: Daily at 02:00 UTC (keeps cache fresh and auto-processes new releases)
  • Manual: Workflow Dispatch inputs:
    • debug — Enable Cloudflare Tunnel for SSH debugging (default: false)
    • force_rebuild — Force full rebuild ignoring cache (default: false)
  • Webhook: repository_dispatch with type sign_ipas

Example repository_dispatch payload:

{
  "event_type": "sign_ipas",
  "client_payload": {}
}

How It Works

  1. Restore Cache: Restores cached device lists and release versions from previous runs
  2. Install zsign: Downloads zsign's official prebuilt Linux binary (pinned via ZSIGN_VERSION, checksum-verified). The static musl build has no runtime dependencies, and zsign signs straight from the P12 — no Keychain involved
  3. Check Entitlements Profile: Python script (sync_profiles_asc.py check) via App Store Connect CLI:
    • Fetches all enabled iOS devices
    • Saves device list snapshot to cache for change detection
    • Compares with cached device list to detect changes
    • Verifies tasks.toml apps have corresponding provisioning profiles
  4. Check App Version: Python script (check_changes.py):
    • Uses device-change status + force_rebuild to decide whether to rebuild all
    • Checks GitHub release versions vs cache to decide which tasks need rebuilding
  5. Sync Entitlements Profile: Python script (sync_profiles_asc.py) via App Store Connect CLI:
    • If device list changed → regenerates all provisioning profiles and downloads them
    • If device list unchanged → downloads existing profiles and creates missing ones if needed
  6. Sign IPAs: Python script (run_signing.py):
    • For ipa_url tasks: Always downloads and rebuilds
    • For repo_url tasks:
      • Fetches latest release via authenticated GitHub API
      • Compares version with cache
      • Only rebuilds if version or publish timestamp changed
    • Re-signs with zsign using the P12 certificate and synced profile
    • Uploads the signed IPA to the asset server via scp
    • Generates itms.plist from the signed IPA's actual bundle id + version and uploads it next to the IPA
    • Refreshes the download page (site/apps.js + index.html cache-buster) and deploys site/ to the docroot
    • Updates release cache with new versions
  7. Commit Download Page: Commits the refreshed site/apps.js / index.html back to the repo (keeps it the single source of truth)
  8. Save Cache: Saves updated cache state for next run

Caching Behavior

The workflow uses GitHub Actions cache to minimize unnecessary work:

  • Cache Storage: work/cache/ directory containing:

    • device-list.json — Snapshot of registered devices with checksum
    • release-versions.json — Tracked release versions and timestamps
  • Cache Lifetime: 7 days of inactivity (refreshed by daily scheduled runs)

  • Change Detection Logic:

    • Device list changes → Full rebuild (all profiles regenerated, all IPAs re-signed)
    • Release version changes → Rebuild only affected IPA
    • Direct URL (ipa_url) tasks → Always rebuild (no version tracking)
    • New tasks or first run → Always rebuild
  • Force Rebuild: Use the force_rebuild input to bypass cache and rebuild everything

Requirements and Notes

  • Runner: ubuntu-latestzsign signs via OpenSSL (not Apple's codesign/Security.framework), so the whole pipeline runs on Linux (≈10× cheaper than a macOS runner)
  • Tools installed: zsign (prebuilt static Linux binary), asc (App Store Connect CLI, prebuilt Linux binary), sshpass (via apt) — all checksum-verified where downloaded
  • Signing: Uses zsign with the P12 certificate and synced profile (no Keychain / codesign identity required)
  • Upload: Password-based scp to asset server
  • Bundle IDs: Must be pre-registered in Apple Developer Portal
  • GitHub Token: Workflow automatically uses GITHUB_TOKEN for authenticated API access
    • Provides 1,000 requests/hour per repository (vs 60/hour unauthenticated)
    • Avoids shared runner IP rate limiting
    • No additional configuration required (default contents: read permission)

Debug Mode (Cloudflare Tunnel)

If debug is enabled for a manual run (workflow_dispatch), the workflow will:

  • Write the provided DEBUG_SSH_PUBLIC_KEY to ~runner/.ssh/authorized_keys.
  • Start a throwaway dropbear SSH server on 127.0.0.1:2222 — public-key auth only (password auth disabled), with a per-run host key.
  • Download cloudflared and run cloudflared --no-autoupdate --url ssh://localhost:2222 in the foreground, which prints a trycloudflare.com hostname.

Connect with the private key matching DEBUG_SSH_PUBLIC_KEY, tunnelling raw TCP through Cloudflare (end-to-end encrypted, no third-party SSH relay):

ssh -o ProxyCommand='cloudflared access tcp --hostname <printed-host>.trycloudflare.com' runner@localhost

The tunnel runs in the foreground and keeps the job alive until you exit or cancel the run.

Local Development Setup

This project uses uv for Python dependency management.

Prerequisites

  • Python 3.11+
  • uv installed

Setup

  1. Install uv (if not already installed):

    curl -LsSf https://astral.sh/uv/install.sh | sh
  2. Sync dependencies:

    uv sync

    This will:

    • Create a virtual environment at .venv/
    • Install all project dependencies
    • Install development dependencies (pytest, mypy, black, isort)
  3. Run scripts:

    # Run check_changes.py
    uv run python scripts/check_changes.py
    
    # Run run_signing.py
    uv run python scripts/run_signing.py
  4. Run tests (when available):

    uv run pytest
  5. Format code:

    # Format with black
    uv run black scripts/
    
    # Sort imports with isort
    uv run isort scripts/
    
    # Type check with mypy
    uv run mypy scripts/

Why uv?

  • Fast: 10-100x faster than pip
  • Reliable: Lockfile ensures reproducible installs
  • Simple: Single tool for virtual environments and dependencies
  • Compatible: Works with standard pyproject.toml

Latest Actions Versions

  • actions/checkout@v6
  • astral-sh/setup-uv@v8
  • actions/cache@v5

These are selected based on current docs and should be kept up to date.

About

No description, website, or topics provided.

Resources

License

Stars

1 star

Watchers

0 watching

Forks

Releases

No releases published

Sponsor this project

 

Packages

 
 
 

Contributors