Deploy a service (staging & prod)
Deploying a service is a git push. Every repo in the fleet carries two tiny workflow
files; each just picks an environment, names the app, and delegates all the real work to
one shared reusable workflow — soundversegit/service-template. You do not run az
by hand for a normal deploy, and you never touch the deploy engine per-service.
There are exactly two long-lived deploy branches:
| Push to | Runs | Delegates to |
|---|---|---|
staging |
.github/workflows/deploy-aca-staging.yml |
service-template/.github/workflows/deploy-aca-template.yml@staging |
prod |
.github/workflows/deploy-aca-prod.yml |
service-template/.github/workflows/deploy-aca-template.yml@prod |
The two files in every repo
Section titled “The two files in every repo”Directorycore-tool-name/
Directory.github/
Directoryworkflows/
- deploy-aca-staging.yml push to
staging→ staging deploy - deploy-aca-prod.yml push to
prod→ prod deploy - ci.yml lint + typecheck + tests on PR
- deploy-aca-staging.yml push to
- Dockerfile
The per-service file is intentionally dumb — it sets github_environment, service_name,
dockerfile_path, the ingress flags, and forwards all org secrets with secrets: inherit. Because the engine lives in service-template, a fix to the deploy logic
propagates to every service automatically; nothing is vendored or copy-pasted.
Deploy to staging
Section titled “Deploy to staging”-
Get CI green on your PR. Every push and PR runs
.github/workflows/ci.yml(ruff + pyright + pytest for Python services). Merge the PR intostaging, fast-forward the branch, or push directly — any commit that lands onstagingtriggers the deploy. -
The deploy runs on push. Nothing else to do for a normal deploy. For an off-cycle run (redeploy the same SHA, or deploy without a new commit) use
workflow_dispatch:trigger an off-cycle staging deploy gh workflow run deploy-aca-staging.yml --ref staging -
Watch it land. Tail the run to completion:
watch the deploy gh run list --workflow=deploy-aca-staging.yml --limit 3gh run watch # follows the most recent run -
Verify the app took the new image. The engine builds
soundversegeneral.azurecr.io/<service>:<sha>, then creates-or-updates the Azure Container App and (for ingress apps) republishes its FQDN as an org variable. Confirm the running container picked up your image and env with Inspect a running service.
Promote to prod
Section titled “Promote to prod”Prod is the same mechanic against the prod branch — fast-forward or open a PR from
staging into prod, or dispatch deploy-aca-prod.yml --ref prod. Two differences worth
knowing:
- The prod caller pins
@prodand setsgithub_environment: "prod"/service_name: "…-prod", soSTAGING_-prefixed config is swapped forPROD_. - Staging sets
concurrency.cancel-in-progress: true(a newer push cancels an in-flight run); prod sets itfalse— a prod deploy always finishes rather than being cancelled mid-rollout.
Ingress service vs. no-ingress worker
Section titled “Ingress service vs. no-ingress worker”The only meaningful difference between deploying a gateway/data service and deploying a
tool worker is the ingress block. A worker (the tool fleet) sets enable_ingress: false
and declares no ports — it gets no FQDN and accepts no inbound traffic; it reaches Redis,
core-database, and core-storage as a client only.
jobs: deploy-staging: uses: soundversegit/service-template/.github/workflows/deploy-aca-template.yml@staging with: github_environment: "staging" service_name: "core-gateway-consumer-staging" dockerfile_path: "Dockerfile" enable_ingress: true is_external: true target_port: 80 pass_github_token_to_docker: true secrets: inheritjobs: deploy-staging: uses: soundversegit/service-template/.github/workflows/deploy-aca-template.yml@staging with: github_environment: "staging" service_name: "core-tool-agent-staging" dockerfile_path: "Dockerfile" # A worker, not a web service: no inbound traffic. enable_ingress: false # Clone the private soundverse / soundverse-proto git deps in the build. pass_github_token_to_docker: true secrets: inheritis_external: true publishes the app externally over TLS on :443 even though
target_port is 80/8080 — ACA terminates TLS at the ingress. An app can later go
internal-only (is_external: false + allow_insecure: true, reached on :80 in-cluster);
consumers then set the matching CORE_<SERVICE>_USE_TLS=false. When you add a brand-new
repo to this pipeline, follow Add a new service to the deploy pipeline.
What the template does for you
Section titled “What the template does for you”One pass through deploy-aca-template.yml, in order — the full walk-through is in
the CI/CD pipeline page:
- Mints a short-lived GitHub App token (
GO_PUBLISH_APP_ID/GO_PUBLISH_APP_PRIVATE_KEY, falling back toGH_READ_REPO_TOKEN) so the build can clone the privatesoundverse-proto-*SDKs andsoundverse-py.pass_github_token_to_docker: truehands it to BuildKit as a--mount=type=secret, so it is never baked into an image layer. - Builds and pushes
soundversegeneral.azurecr.io/<service>:<sha>(and:latest) to ACR. - Reads the org Variables/Secrets, strips the
STAGING_/PROD_prefix, and rewrites them into Azure env flags (values viasecretref, never inline). - Runs
az containerapp createon first deploy, orsecret set+updateon subsequent ones (--min-replicas 1 --max-replicas 10). - Republishes the app’s FQDN as the org variable
<ENV>_<SERVICE>_GRPCso other services discover it on their next deploy.
If a deploy goes bad
Section titled “If a deploy goes bad”Don’t hotfix the running app — roll back. The fastest recovery is to pin ACA ingress traffic to the previous good revision, or redeploy a known-good commit. Full runbook: Roll back a deployment.
Related
Section titled “Related”- CI/CD deploy pipeline — what the reusable template does, step by step
- Environment & service-discovery convention — how prefix-strip injection and
<ENV>_<SERVICE>_GRPCwire services together - Add a new service to the deploy pipeline — wiring a brand-new repo in
- Inspect a running service — read effective env without leaking values
- Roll back a deployment — the recovery runbook
- Add a new generation tool worker — the no-ingress worker end to end