Release Guide
This document covers the full release lifecycle: branching, PRs, versioning, CI secrets, and deploying staging and production builds to the App Store and Play Store.
Branch Strategy
main ← production-ready code; triggers releases
next ← pre-release / RC staging area
feature/* ← new features, branched from main
fix/* ← bug fixes, branched from main
chore/* ← non-functional changes (deps, CI, docs)
Rules
mainandnextare protected. Direct pushes are blocked.- All changes enter via a Pull Request targeting
main(ornextfor pre-releases). - PRs require the
Quality GateCI check to pass before merging. - Commit messages must follow Conventional Commits — semantic-release reads these to determine the next version.
Commit message format
<type>(<scope>): <short description>
feat(app): add lightning invoice QR scanner
fix(sdk): handle relay disconnect gracefully
chore(ci): update Flutter version to 3.41.1
docs: add release guide
Pull Request Workflow
- Branch from
main:git checkout -b feat/my-feature - Push and open a PR against
main. - CI runs automatically:
- Change detection determines which test suites are relevant.
- Only affected suites run (
hostr_sdk,app). - The
Quality Gatejob is the required status check.
- Merge via Squash and Merge to keep
mainhistory linear.
Versioning
Version is managed by semantic-release based on commit history since the last tag. The version in app/pubspec.yaml is updated automatically as part of the release step (via .releaserc.yml prepareCmd).
Build numbers
Both Android (AAB) and iOS (IPA) use github.run_number as the build number. This is:
- Monotonically increasing across all workflow runs in the repo.
- Identical for Android and iOS builds triggered in the same workflow run, ensuring consistent build numbers across both stores.
- Never requires an external API call to calculate.
The semantic version string (x.y.z) is kept in sync via pubspec.yaml. The build number and version string together uniquely identify every build.
Environments
There are two deployed environments, each with their own backend, app build, and CI secrets.
When each build is triggered
A production build is always preceded by a passing staging build (same release version).
CI Pipeline Overview
PR / push
│
▼
[changes] ← dorny/paths-filter: detect app / sdk / escrow changes
│
├── [test_sdk] (only if hostr_sdk/** or models/** changed)
└── [test_app] (only if app/** or hostr_sdk/** or models/** changed)
│
▼
[quality_gate] ← skipped jobs count as passing; failed/cancelled = block
│
▼ (main branch, push only)
[release] ← semantic-release: bump version, create GitHub Release
│
▼ (GitHub Release published event)
┌──────────┴──────────┐
[build_android_staging] [build_ios_staging]
[build_android_prod] [build_ios_prod]
Required CI Secrets
Secrets are scoped to GitHub Environments (staging and production). Go to:
Settings → Environments → <environment> → Environment secrets
Shared (both environments)
These are identical values in both environments unless you use separate developer accounts per environment.
Android (per environment)
⚠️ Use separate upload keystores for staging and production. If a staging keystore is compromised, it cannot be used to push to the production track.
iOS (per environment)
Staging and production share the same bundle ID (
com.sudonym.hostr), the same App Store Distribution certificate, and the same provisioning profile. They differ only in the Dart entrypoint (main_staging.dartvsmain_production.dart), which sets the backend URL. Use separate TestFlight groups (e.g. "Internal — Staging" vs "Internal — Production") to control who receives which build. The CI workflow overrides signing settings inRelease.xcconfigat build time — locally, the project defaults to Automatic signing for development.
Cloud Deploy (per environment)
Repository/Environment variable required for Terraform remote state:
Deployment is executed by the Infrastructure Deploy workflow (.github/workflows/infra_deploy.yaml), which applies Terraform and then resets the compose VM to trigger the startup deploy script.
Preparing a Release Manually
In normal operation, semantic-release handles this automatically. If you need to cut a release manually:
# Ensure you are on main with a clean working tree
git checkout main && git pull
# Dry-run to preview what semantic-release would do
npx semantic-release --dry-run
# Trigger CI to do the real release by pushing a conventional commit
git commit --allow-empty -m "chore(release): trigger release"
git push
Promoting a Staging Build to Production
After validating the staging build on internal TestFlight / Play internal track:
- Go to Actions → Build & Deploy → Run workflow.
- Select
track: production. - The workflow builds with
lib/main_production.dart(pointing tohostr.network) and uploads directly to the production track.
Alternatively, on Android you can promote within the Play Console without a rebuild (the same AAB from the internal track is promoted). On iOS, promote the same TestFlight build to App Store review.
🔒 The
productionGitHub Environment requires manual approval before the job runs. Configure this underSettings → Environments → production → Required reviewers.
Adding Secrets to the VM (Cloud Services)
Runtime secrets are stored in Google Secret Manager and fetched onto the VM at deploy time. Required keys are:
ESCROW_PRIVATE_KEYBLOSSOM_DASHBOARD_PASSWORD
Non-sensitive runtime values (DOMAIN, LETSENCRYPT_EMAIL, RPC_URL,
ESCROW_CONTRACT_ADDRESS) are sourced from .env.staging / .env.prod.
See .doc/infrastructure/README.md for the
hostr-fetch-secrets flow and example commands.