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.
The workflows
Section titled “The workflows”| 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.
Select the brand
Section titled “Select the brand”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) |
Brand in the fork
Section titled “Brand in the fork”gh variable set FROGG_BRAND_DIR --body brands/acme --repo acme-corp/froggExternal brand repository
Section titled “External brand repository”Keep artwork and brand.json in their own repository and pin a commit:
gh variable set FROGG_BRAND_REPOSITORY --body acme-corp/studio-brand --repo acme-corp/frogggh variable set FROGG_BRAND_REVISION --body 4b1e9c0d2f7a6e5b3c8d9e0f1a2b3c4d5e6f7a8b --repo acme-corp/frogggh variable set FROGG_BRAND_SUBDIR --body brand --repo acme-corp/froggFor 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.
Secrets and variables
Section titled “Secrets and variables”Everything is optional. Missing secrets never fail a release; they turn the feature off and the workflow says so.
Desktop
Section titled “Desktop”| 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.
Android
Section titled “Android”| 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.
keytool -genkeypair -v -keystore acme-release.keystore -alias acme \ -keyalg RSA -keysize 4096 -validity 10000gh secret set FROGG_ANDROID_KEYSTORE_BASE64 --body "$(base64 -w0 acme-release.keystore)" --repo acme-corp/frogggh secret set FROGG_ANDROID_KEYSTORE_PASSWORD --repo acme-corp/frogggh secret set FROGG_ANDROID_KEY_ALIAS --body acme --repo acme-corp/frogggh secret set FROGG_ANDROID_KEY_PASSWORD --repo acme-corp/froggBack up the keystore outside GitHub. Losing it means you can never update the app again.
Docker Hub
Section titled “Docker Hub”| 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.
Not in the workflows
Section titled “Not in the workflows”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.
Cut a release
Section titled “Cut a release”-
Merge what you want to ship into
mainand make sure CI is green. -
Add a changelog entry. The release notes are the
## <version>section ofCHANGELOG.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. -
Bump the version and push the tag. The root
package.jsonversion 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 originUse
version:all:minororversion:all:majorfor larger bumps, andversion:all:beta:patch(and friends) for a prerelease such as0.6.20-beta.1.npm run release:patchruns the fullrelease:checkfirst and then both steps. -
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')" -
Check the draft. The release is created as a draft. The
publishjob 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.
Run the release manually
Section titled “Run the release manually”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: defaultgh workflow run release.yml --repo acme-corp/frogg -f tag=v0.6.20 -f platforms=allgh workflow run release.yml --repo acme-corp/frogg -f tag=v0.6.20 -f only=androidgh workflow run build-selected.yml --repo acme-corp/frogg -f target=windowsWhat a release contains
Section titled “What a release contains”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) |
How updates reach your users
Section titled “How updates reach your users”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 inrelease.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. SetFROGG_ELECTRON_UPDATE_URLto test against another HTTPS feed. - Daemon.
install.shfrom your release downloads your daemon bundles. On the host,<cliName> update(Acme:acme update) checkshttps://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.repositorythe update mode isdisabled: 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:
curl -fsSL https://github.com/acme-corp/frogg/releases/latest/download/install.sh | bashinstall.sh is attached to every release. To serve it from your own domain, deploy the
generated install worker (see other distributions).
What has been validated
Section titled “What has been validated”Be clear-eyed about coverage:
- Validated in upstream CI:
brands/examplebuilds 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.ymlrun 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.