Automate / CI/CD automation
CI/CD Automation — ship OTA releases from GitHub Actions
PatchForge never builds your app — your CI builds the bundle, then hands it to PatchForge via
the /api/v1 automation API (PAT-authenticated), the @zimbstech/patchforge-cli, or the
patchforge/release-action composite Action. This guide covers all three, end to end.
Prerequisites
- A PatchForge organization with an application and channel already created (see your PatchForge dashboard)
- A GitHub repository with permission to add repository secrets
- Your React Native app already builds a JS bundle locally (Metro / RN CLI)
Step 1 — Mint a Personal Access Token (PAT)
In the dashboard: Settings → API Keys → "Create API Key" → copy the plaintext token
immediately (pf_... — it is shown once and never recoverable). Store it as a GitHub Actions
repository secret, e.g. PATCHFORGE_TOKEN.
V1 PATs grant full-organization access (no per-app/per-permission scoping yet — V2).
Step 2 — Building your bundle
Your CI is responsible for producing the artifact — PatchForge only stores, signs, and distributes bytes it's given. Two artifact formats are supported:
- Zip (recommended, SDK ≥ 0.2.7): a
.zipcontainingindex.<platform>.bundleat its root plus the Metro--assets-destoutput. Ships images/fonts OTA and enables server-side differential patches between zip releases (devices download KBs instead of the full bundle). - Raw bundle (legacy): the single
index.android.bundle/main.jsbundlefile, uploaded verbatim. Works on every SDK version; no OTA assets, no patches.
Ship zip releases only to channels whose installed binaries carry SDK ≥ 0.2.7 — an older SDK would activate the zip as raw JS and crash (its own crash-loop recovery then reverts it, but the update never applies).
Build the zip artifact with Metro, e.g. for Android:
mkdir -p build/ota
npx react-native bundle \
--platform android \
--dev false \
--entry-file index.js \
--bundle-output build/ota/index.android.bundle \
--assets-dest build/ota
(cd build/ota && zip -qr ../ota-bundle.zip .)
For iOS, swap --platform ios and --bundle-output build/ota/index.ios.bundle.
Pass the resulting build/ota-bundle.zip (or the raw .bundle file for legacy channels) to
--bundle (CLI) or the bundle input (Action) in the next step.
Step 3 — Workflow YAML
Uses the patchforge/release-action composite Action (see actions/release/README.md).
Branch→channel mapping is just a GitHub Actions if:/github.ref_name check — PatchForge has
no built-in branch mapping, your workflow owns it:
name: OTA Release
on:
push:
branches: [main, staging]
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build bundle
run: |
# your existing `react-native bundle` / metro build step
echo "build the bundle to ./build/index.android.bundle here"
- name: Determine channel
id: channel
run: |
if [ "${{ github.ref_name }}" = "main" ]; then echo "name=Production" >> "$GITHUB_OUTPUT";
else echo "name=Staging" >> "$GITHUB_OUTPUT"; fi
- uses: ZimbsTech/patchforge/actions/release@main
with:
app: my-app
channel: ${{ steps.channel.outputs.name }}
bundle: ./build/index.android.bundle
store-version: 1.4.0
bundle-version: 1.4.0.${{ github.run_number }}
api-token: ${{ secrets.PATCHFORGE_TOKEN }}
server-url: https://patchforge.example.com
Step 4 — Or call the CLI directly (no Action)
npx @zimbstech/patchforge-cli@latest release \
--app my-app --channel Production \
--bundle ./build/index.android.bundle \
--store-version 1.4.0 --bundle-version "1.4.0.$GITHUB_RUN_NUMBER" \
--server https://patchforge.example.com \
--api-token "$PATCHFORGE_TOKEN"
CLI reference
| Flag | Required | Description |
|---|---|---|
--app <slug> | yes | Application slug |
--channel <name> | yes | Channel name |
--bundle <path> | yes | Path to the built bundle |
--store-version <v> | yes | Store/app version |
--bundle-version <v> | yes | Bundle version |
--build-number <n> | no | Build number |
--release-type <type> | no | Standard | Hotfix | Mandatory |
--notes <text> | no | Release notes |
--git-sha <sha> | no | Commit SHA |
--source-ref <ref> | no | Branch/tag ref |
--auto-deploy <bool> | no | Overrides the server default (see below) |
--rollout <percent> | no | Rollout percentage (1–100) |
--wait-timeout <sec> | no | Max seconds to wait for processing (default 600) |
--server <url> / PATCHFORGE_SERVER | yes | Server base URL |
--api-token <token> / PATCHFORGE_TOKEN | yes | Your PAT |
Exit codes: 0 success (including an already-released duplicate), 1 failed release or HTTP
error, 2 bad arguments.
Auto-deploy defaults
The server computes resolvedAutoDeploy on every create response: on by default for any
channel whose environment type is not Production, off (manual, dashboard-only) for
Production — unless you pass --auto-deploy true|false to override. The CLI polls the
release to Ready, then deploys automatically only when resolvedAutoDeploy is true.
Idempotency and re-runs
Releases are unique per (application, channel, bundleVersion). Re-running a workflow with the
same --bundle-version does not fail your build: the server returns 409 with the existing
release, and the CLI treats an existing Ready release as success (exit 0) — it never
re-uploads or re-deploys. Use a version scheme that changes per run (e.g.
1.4.0.${{ github.run_number }} — bundle versions must be x.y.z or x.y.z.w) if you want
every push to ship a new bundle version.
Rate limits
Per PAT: create 10 req/60s, status polling 120 req/60s, deploy 30 req/60s. Exceeding a limit
returns 429; the CLI treats this as a failed HTTP call (exit 1) — retries are your workflow's
responsibility.