Mobile release pipeline + CI secrets via 1Password
How a Flutter app builds, signs, and uploads itself to the Play Console
internal track and TestFlight from one GitHub Actions pipeline — with
every signing/store secret pulled at run time from a single 1Password vault,
nothing sensitive in the repo. This is the mobile counterpart to
uniform-ci.md: same "one shared pattern, thin per-repo
wiring" philosophy.
Reference implementation: leaders-app
is the first adopter. Per-repo specifics (the workflow, the /release skill,
docs/deployment.md) live in that repo; this runbook is the shared pattern
other mobile apps (next: consumer-mobile) reuse. The platform-wide extraction
(a workflow_call reusable in bewith-dev/.github) is the planned end state —
see Sharing across apps.
Secrets via 1Password (the reusable half)
The decision that makes this scale: one place to manage every key. Instead of pasting N secrets into each repo's GitHub settings, all secrets live in a 1Password vault and the workflow reads them at run time.
- One GitHub secret org-wide:
OP_SERVICE_ACCOUNT_TOKEN— a 1Password Service Account token scoped to read the secrets vault. - Everything else (signing keystore, Play service account, App Store Connect API
key, Slack bot token) lives in the
MCP-Exposed1Password vault and is fetched per run withop read(files via--out-file). - Rotating a key is a one-place edit in 1Password; no repo changes, no N GitHub secrets to chase.
leaders-app vault contract (MCP-Exposed):
| 1Password item / field | Use |
|---|---|
Android Signing/keystore + store_password / key_password / key_alias | sign the AAB (android/key.properties assembled in CI; storeFile constant) |
Play Store Service Account/json | fastlane supply auth to Play |
App Store Connect API/key_id + issuer_id + AuthKey_*.p8 | TestFlight upload and iOS signing |
Slack bot token/credential | release notifications |
A small composite action (.github/actions/op-secrets in the app repo)
installs the op CLI, masks the string secrets, materializes the file secrets
into $RUNNER_TEMP, assembles android/key.properties, and exports the env the
fastlane lanes read. A matching cleanup step wipes everything if: always().
Security note (read before adding more apps)
1Password service-account access is per-vault, not per-item: any token that
can read MCP-Exposed can read everything in it, including the non-rotatable
Android upload keystore. This is acceptable while only the CI service account
reads the vault. Before broadening read access (more MCP connectors) or piling
in unrelated keys, isolate the signing material into a dedicated CI-only vault
(e.g. release-signing) read solely by the release pipeline. Isolation is done by
vaults, and you need one service account per trust boundary, not per app.
The pipeline (the per-repo half)
GitHub Actions, manual workflow_dispatch, jobs prepare → android + ios → notify:
- Android (ubuntu):
flutter build appbundle→fastlane supplyto the Play internal track (release_status: completed). - iOS (macos):
flutter build ios --config-only→pod install→fastlanebuild_app+upload_to_testflight(internal). - iOS signing is cloud-managed:
xcodebuild -allowProvisioningUpdateswith the App Store Connect API key creates the distribution cert + provisioning profile on demand. No.p12/ profile in the repo or CI. (Xcode Cloud is retired — GitHub Actions is the single iOS CI.) - Versioning needs no input:
versionNamefrompubspec.yaml; the build number is resolved automatically asmax(highest Play versionCode, latest TestFlight build) + 1, shared by both platforms — never rejected for a stale code. - Notifications: state is posted to a releases Slack channel at each stage (started → Play internal → TestFlight → ✅/❌), best-effort.
Production promotion, AI-generated release notes, and a beta flavor are later, gated phases — not part of this baseline.
The /release skill (first repo-specific artifact)
leaders-app ships a repo-local Claude Code skill at .claude/skills/release/
that is the single entry point: it picks the next version (semver bump from
commits since the last tag), bumps pubspec.yaml, and triggers the workflow via
gh workflow run — so no one has to remember to bump a version. The build number
stays automatic.
This is the first artifact that is specific to one repo rather than shared
platform content, so it deliberately lives in the app repo, not in
bewith-docs .claude/skills/ (which are platform-wide). The shared knowledge is
this runbook; the executable skill is repo-local until the reusable workflow lands
and a templated skill can be promoted.
Sharing across apps
Following the uniform-ci model, the end state is a
workflow_call reusable workflow in bewith-dev/.github that each mobile app
calls with a thin caller (secrets: inherit for OP_SERVICE_ACCOUNT_TOKEN).
leaders-app proves the pattern first (POC); consumer-mobile is the next adopter.
Pin the reusable workflow by @v1 and move the tag on release — never @main.
Adoption checklist for a new mobile app
- Add the app's signing keystore + store credentials to the secrets vault as the items above (or app-scoped equivalents).
- Grant the Play service account release permission on the app; register the bundle id in App Store Connect.
- Copy
fastlane/,ios/ExportOptions.plist, and the.github/actions/op-secretsslack-notifycomposites; add the caller workflow.
- Confirm
OP_SERVICE_ACCOUNT_TOKEN(org secret) can read the vault. - First run validates iOS cloud signing on a real macOS runner.
See also
leaders-app/docs/deployment.md— the per-repo deployment doc (reference implementation).- Versioning & release-notes standard — the proposal this pipeline implements the build-numbering half of.
uniform-ci.md— the TS/JS shared-CI sibling; same hosting model (bewith-dev/.github).secret-rotation(planned) — coordinated rotation; relevant when the signing vault is isolated.