#
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_ADDR) are sourced from .env.staging / .env.prod.
See .doc/infrastructure/README.md for the
hostr-fetch-secrets flow and example commands.