Skip to content

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
  • 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
    • 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.

  1. Get CI green on your PR. Every push and PR runs .github/workflows/ci.yml (ruff + pyright + pytest for Python services). Merge the PR into staging, fast-forward the branch, or push directly — any commit that lands on staging triggers the deploy.

  2. 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
  3. Watch it land. Tail the run to completion:

    watch the deploy
    gh run list --workflow=deploy-aca-staging.yml --limit 3
    gh run watch # follows the most recent run
  4. 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.

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 @prod and sets github_environment: "prod" / service_name: "…-prod", so STAGING_-prefixed config is swapped for PROD_.
  • Staging sets concurrency.cancel-in-progress: true (a newer push cancels an in-flight run); prod sets it false — a prod deploy always finishes rather than being cancelled mid-rollout.

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.

core-gateway-consumer/.github/workflows/deploy-aca-staging.yml
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: inherit

is_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.

One pass through deploy-aca-template.yml, in order — the full walk-through is in the CI/CD pipeline page:

  1. Mints a short-lived GitHub App token (GO_PUBLISH_APP_ID / GO_PUBLISH_APP_PRIVATE_KEY, falling back to GH_READ_REPO_TOKEN) so the build can clone the private soundverse-proto-* SDKs and soundverse-py. pass_github_token_to_docker: true hands it to BuildKit as a --mount=type=secret, so it is never baked into an image layer.
  2. Builds and pushes soundversegeneral.azurecr.io/<service>:<sha> (and :latest) to ACR.
  3. Reads the org Variables/Secrets, strips the STAGING_/PROD_ prefix, and rewrites them into Azure env flags (values via secretref, never inline).
  4. Runs az containerapp create on first deploy, or secret set + update on subsequent ones (--min-replicas 1 --max-replicas 10).
  5. Republishes the app’s FQDN as the org variable <ENV>_<SERVICE>_GRPC so other services discover it on their next deploy.

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.