Skip to content

Build and release

Your fork already contains the workflows. You do not edit them to change the product. You set repository variables to select the brand and secrets to unlock signing and publishing. Every build job runs .github/actions/select-brand first, so the whole pipeline builds your brand.

Workflow File Trigger What it produces
Release .github/workflows/release.yml Push a v* tag, or run manually with a tag A GitHub release: desktop packages, daemon bundles, Android APK, updater metadata, install scripts, optional Docker image
Build selected .github/workflows/build-selected.yml Manual (target: all, windows, daemon, android); also called by CI on pushes to main Workflow artifacts (not a release): Windows x64 client, Linux x64 daemon, Android arm64 debug-signed APK
CI .github/workflows/ci.yml Pull requests, pushes to main Format, lint, typecheck, unit tests; calls Build selected on main
Branding acceptance .github/workflows/branding.yml PRs touching branding, deploy, desktop or workflows; manual Builds brands/frogg and brands/example on Linux, Windows, macOS, Android, iOS simulator and Nix; checks your selected brand builds
Electron desktop .github/workflows/electron-desktop.yml PRs touching desktop, UI or packages; manual Windows x64 Electron build as an artifact

Release and Build selected honour the brand variables below. Without them they build stock frogg. The checks in ci.yml are brand-independent.

Set repository variables in Settings → Secrets and variables → Actions → Variables, or with the GitHub CLI.

Variable Meaning
FROGG_BRAND_DIR Brand directory committed in the fork, for example brands/acme
FROGG_BRAND_REPOSITORY External branding repository owner/name (used when FROGG_BRAND_DIR is empty)
FROGG_BRAND_REVISION Full 40-character commit SHA of the external repository (required with it)
FROGG_BRAND_SUBDIR Directory inside the external repository (default: its root)
Terminal window
gh variable set FROGG_BRAND_DIR --body brands/acme --repo acme-corp/frogg

Keep artwork and brand.json in their own repository and pin a commit:

Terminal window
gh variable set FROGG_BRAND_REPOSITORY --body acme-corp/studio-brand --repo acme-corp/frogg
gh variable set FROGG_BRAND_REVISION --body 4b1e9c0d2f7a6e5b3c8d9e0f1a2b3c4d5e6f7a8b --repo acme-corp/frogg
gh variable set FROGG_BRAND_SUBDIR --body brand --repo acme-corp/frogg

For a private branding repository, add a fine-grained token with read access to it as the BRANDING_READ_TOKEN secret. The action refuses a branch name or short SHA: bump FROGG_BRAND_REVISION deliberately when the brand changes.

The selection order inside a job is: an explicit FROGG_BRAND_DIR in the job environment (used by the branding matrix), then the external repository, then the FROGG_BRAND_DIR variable, then brands/frogg.

Everything is optional. Missing secrets never fail a release; they turn the feature off and the workflow says so.

Platform Secrets Without them
Windows AZURE_TENANT_ID, AZURE_CLIENT_ID, AZURE_CLIENT_SECRET, TRUSTED_SIGNING_ENDPOINT, TRUSTED_SIGNING_ACCOUNT, TRUSTED_SIGNING_PROFILE (Azure Trusted Signing) Unsigned NSIS installer and zip; SmartScreen warns users
macOS none supported by the workflow Unsigned, not notarized (notarize: false, CSC_IDENTITY_AUTO_DISCOVERY=false); Gatekeeper blocks first launch until the user allows it
Linux none AppImage, deb and tar.gz are unsigned by design

The Azure service principal needs the Trusted Signing Certificate Profile Signer role. The Windows publisher name comes from publisher in your brand.

Secret Required for signing Notes
FROGG_ANDROID_KEYSTORE_BASE64 yes base64 -w0 release.keystore
FROGG_ANDROID_KEYSTORE_PASSWORD yes Keystore password
FROGG_ANDROID_KEY_ALIAS no Defaults to frogg
FROGG_ANDROID_KEY_PASSWORD no Key password, if different

Without the first two, the APK is signed with the public Expo/React Native debug key and published as <prefix>-<version>-android-arm64-v8a-unsigned.apk.

Set the keystore before your first release. Android identifies an app by its signing key. Users who installed a debug-signed APK cannot update to a release-signed one without uninstalling and losing app data. A new product has no such users yet.

Terminal window
keytool -genkeypair -v -keystore acme-release.keystore -alias acme \
-keyalg RSA -keysize 4096 -validity 10000
gh secret set FROGG_ANDROID_KEYSTORE_BASE64 --body "$(base64 -w0 acme-release.keystore)" --repo acme-corp/frogg
gh secret set FROGG_ANDROID_KEYSTORE_PASSWORD --repo acme-corp/frogg
gh secret set FROGG_ANDROID_KEY_ALIAS --body acme --repo acme-corp/frogg
gh secret set FROGG_ANDROID_KEY_PASSWORD --repo acme-corp/frogg

Back up the keystore outside GitHub. Losing it means you can never update the app again.

Secret Plus in brand.json
DOCKERHUB_USERNAME distribution.dockerImage, e.g. acme/studio
DOCKERHUB_TOKEN

With both secrets set, the release builds deploy/docker/base/Dockerfile for linux/amd64 and pushes <image>:<version>. Stable versions also move <major>.<minor>, <major> and latest. An existing version tag is never overwritten. If the secrets are set but dockerImage is not, the Docker job fails with Configure distribution.dockerImage to publish containers.

These run from a workstation or your own automation, not from the shipped workflows:

Task Command Credentials
EAS mobile builds npm run brand:eas -- build --platform android --profile production-apk EXPO_TOKEN or eas login; distribution.expoProjectId
iOS npm run brand:eas -- build --platform ios Apple developer account via EAS; distribution.iosStoreId for submit
Web app on Cloudflare Pages FROGG_WEB_DEPLOY_PROJECT=acme-studio npm run deploy:web --workspace=@frogg/app Wrangler login or CLOUDFLARE_API_TOKEN + CLOUDFLARE_ACCOUNT_ID
Pairing page image scripts/release/build-pair-page-docker.sh --push docker login; distribution.pairingImage

See mobile, Docker, Nix and hosted services.

  1. Merge what you want to ship into main and make sure CI is green.

  2. Add a changelog entry. The release notes are the ## <version> section of CHANGELOG.md. Without one, the release says only that no entry exists.

    CHANGELOG.md
    ## 0.6.20 — 2026-09-20
    - First Acme Studio release, based on frogg 0.6.19.
  3. Bump the version and push the tag. The root package.json version is the product version; the tag must match it exactly.

    Terminal window
    npm run version:all:patch # npm version across workspaces, commits "chore(release): cut 0.6.20"
    npm run release:push # creates annotated tag v0.6.20 on HEAD, pushes branch and tag to origin

    Use version:all:minor or version:all:major for larger bumps, and version:all:beta:patch (and friends) for a prerelease such as 0.6.20-beta.1. npm run release:patch runs the full release:check first and then both steps.

  4. Watch the run.

    Terminal window
    gh run watch --repo acme-corp/frogg "$(gh run list --repo acme-corp/frogg --workflow release.yml --limit 1 --json databaseId --jq '.[0].databaseId')"
  5. Check the draft. The release is created as a draft. The publish job clears the draft flag only when desktop, daemon bundles, Android and updater metadata all succeed (Docker may be skipped). A failed platform leaves a draft you can fix and re-run.

A tag push always builds everything. A manual run builds an existing tag, and defaults to a smaller set: Windows x64 desktop, Linux x64 and Windows x64 daemon bundles, and the Android APK. Pick platforms=all for the full matrix.

There is no screenshot of GitHub’s dispatch form here; these are the same inputs:

Run workflow: Release
tag (required) Existing tag to build and publish (for example v0.1.5)
only all | android default: all
platforms default | all default: default
Terminal window
gh workflow run release.yml --repo acme-corp/frogg -f tag=v0.6.20 -f platforms=all
gh workflow run release.yml --repo acme-corp/frogg -f tag=v0.6.20 -f only=android
gh workflow run build-selected.yml --repo acme-corp/frogg -f target=windows

For Acme, artifactPrefix is acme and the brand id is acme:

Asset Built by
acme-0.6.20-win-x64.exe, .exe.blockmap, acme-0.6.20-win-x64.zip, acme-0.6.20-win-x64-installer.zip desktop (Windows)
acme-0.6.20-linux-x86_64.AppImage, acme-0.6.20-linux-amd64.deb, acme-0.6.20-linux-x64.tar.gz desktop (Linux)
acme-0.6.20-mac-arm64.dmg, acme-0.6.20-mac-x64.dmg, .zip and .blockmap files desktop (macOS)
acme-daemon-0.6.20-<linux|darwin|win>-<x64|arm64>.tar.gz or .zip, each with .sha256 daemon bundles (6 targets)
acme-0.6.20-android-arm64-v8a.apk (or -unsigned.apk) and .sha256 android
electron-latest.yml, electron-latest-linux.yml, electron-latest-mac.yml (electron-beta* for prereleases), release.json, SHA256SUMS-desktop updater-manifest
install.sh, uninstall.sh, install-docker.sh, uninstall-docker.sh meta (generated for your brand)
acme/studio:0.6.20 on Docker Hub docker (optional)

Everything keys off distribution.repository in your brand.

  • Desktop app. electron-updater reads https://github.com/<repository>/releases/latest/download/ on the stable channel, then pins to the version named in release.json. The beta channel picks the newest non-draft release. Users choose the channel in the app. Updates download in the background and install when the app quits; AppImage builds do not install on quit. Set FROGG_ELECTRON_UPDATE_URL to test against another HTTPS feed.
  • Daemon. install.sh from your release downloads your daemon bundles. On the host, <cliName> update (Acme: acme update) checks https://api.github.com/repos/<repository>/releases, verifies the .sha256, swaps versions and rolls back if the new daemon fails its health check.
  • No repository, no updates. Without distribution.repository the update mode is disabled: the desktop reports updates as disabled and the daemon refuses to self-update. A custom build never falls back to frogg’s releases.

Only a semver prerelease suffix (0.6.20-beta.1) marks a GitHub prerelease. Stable clients ignore prereleases.

Users install the daemon from your release:

Terminal window
curl -fsSL https://github.com/acme-corp/frogg/releases/latest/download/install.sh | bash

install.sh is attached to every release. To serve it from your own domain, deploy the generated install worker (see other distributions).

Be clear-eyed about coverage:

  • Validated in upstream CI: brands/example builds on Linux, Windows and macOS desktop runners, a debug Android APK, an unsigned iOS simulator build, the daemon bundle, installer tests, runtime isolation between frogg and Acme daemons, and Nix.
  • Not validated upstream: a full tagged release.yml run and a real installed update under a custom brand. Upstream publishes only the frogg brand. Run your first release as a beta tag, install it, then ship a second beta and confirm the update path before you announce a stable release.