Add a new service to the deploy pipeline
Deploying to the fleet is deploy-by-convention Deployed. You do
not write a pipeline. Every deployable repo carries two tiny caller workflows — one for
staging, one for prod — and each just calls the one shared reusable engine that lives
in a single repo. Adding a service means dropping those two files in, setting a handful of
inputs, and confirming the org-level secrets exist. That’s it.
What you add to the repo
Section titled “What you add to the repo”Directorycore-your-service/
- Dockerfile the image the engine builds and pushes
Directory.github/workflows/
- deploy-aca-staging.yml
push: staging→ engine@staging - deploy-aca-prod.yml
push: prod→ engine@prod - ci.yml lint / typecheck / test gate on PRs + branch pushes
- deploy-aca-staging.yml
The two deploy-aca-*.yml files are intentionally dumb: they pick the environment, name the
app, declare ingress/ports, and forward all org secrets with secrets: inherit. All the real
work (mint a build token, build + push to ACR, inject env, az containerapp create/update,
publish the FQDN) happens inside
service-template/.github/workflows/deploy-aca-template.yml.
Runbook
Section titled “Runbook”-
Decide the shape. Two shapes deploy through the same engine:
- Ingress service (a Go data plane, a Python gateway, the frontend): accepts inbound
traffic, gets a public FQDN, participates in service discovery. Set
enable_ingress: trueand atarget_port. - No-ingress worker (any tool-fleet queue worker): no HTTP, no FQDN — it reaches Redis,
core-database, core-storage, etc. as a client. Set
enable_ingress: falseand omit ports entirely.
See Backend at a glance for the three-shapes lens.
- Ingress service (a Go data plane, a Python gateway, the frontend): accepts inbound
traffic, gets a public FQDN, participates in service discovery. Set
-
Add
deploy-aca-staging.yml. Pick the matching variant. Notecancel-in-progress: trueon staging so a newer push supersedes an in-flight one..github/workflows/deploy-aca-staging.yml name: Deploy Stagingon:push:branches: [ "staging" ]workflow_dispatch:concurrency:group: ${{ github.workflow }}-${{ github.ref }}cancel-in-progress: truejobs:deploy-staging:uses: soundversegit/service-template/.github/workflows/deploy-aca-template.yml@stagingwith:github_environment: "staging"service_name: "core-your-service-staging" # rename per servicedockerfile_path: "Dockerfile"enable_ingress: false # worker: no inbound trafficpass_github_token_to_docker: true # clone private soundverse* deps at buildsecrets: inherit.github/workflows/deploy-aca-staging.yml name: Deploy Stagingon:push:branches: [ "staging" ]workflow_dispatch:concurrency:group: ${{ github.workflow }}-${{ github.ref }}cancel-in-progress: truejobs:deploy-staging:uses: soundversegit/service-template/.github/workflows/deploy-aca-template.yml@stagingwith:github_environment: "staging"service_name: "core-your-service-staging"dockerfile_path: "Dockerfile"enable_ingress: trueis_external: true # public ingress on :443 (ACA terminates TLS)target_port: 80 # the port your container listens onpass_github_token_to_docker: truesecrets: inherit -
Add
deploy-aca-prod.yml. Byte-for-byte the same shape as staging, with three changes: pin the engine at@prod, setgithub_environment: "prod", and use a-prodservice-name suffix. Prod setscancel-in-progress: false(never abandon a prod rollout mid-flight):.github/workflows/deploy-aca-prod.yml on:push:branches: [ "prod" ]workflow_dispatch:concurrency:group: ${{ github.workflow }}-${{ github.ref }}cancel-in-progress: falsejobs:deploy-prod:uses: soundversegit/service-template/.github/workflows/deploy-aca-template.yml@prodwith:github_environment: "prod"service_name: "core-your-service-prod"dockerfile_path: "Dockerfile"enable_ingress: false # match your staging shapepass_github_token_to_docker: truesecrets: inherit -
Name the app correctly — the suffix is load-bearing. The
-staging/-prodsuffix onservice_nameis stripped by the engine when it publishes the FQDN back as an org variable. Deployingcore-your-service-stagingpublishesSTAGING_CORE_YOUR_SERVICE_GRPC, which is then injected asCORE_YOUR_SERVICE_GRPCinto every other service on its next deploy. Workers get no FQDN (nothing is published). See Environment & service discovery. -
Confirm the org secrets exist. Config is not per-repo — it lives as org-level Variables and Secrets on the
soundversegitorg, andsecrets: inheritforwards all of them. A brand-new service usually needs to add nothing, because the engine’s own secrets and the shared fleet config already exist org-wide (names only — never values):Kind Name(s) Purpose Secret AZURE_CREDENTIALSthe deploy identity ( azure/login)Secret GO_PUBLISH_APP_ID,GO_PUBLISH_APP_PRIVATE_KEYGitHub App token to clone private soundverse*deps at buildSecret GH_READ_REPO_TOKENfallback private-repo read token Secret STAGING_INTERNAL_RPC_SECRET/PROD_…internal Bearer auth between services Secret STAGING_REDIS_PASSWORD/PROD_…Redis auth Variable STAGING_REDIS_ADDR,STAGING_CORE_*_GRPCconnection wiring (the _GRPCvars are auto-published on deploy)You only add a new org secret if your service needs a new provider key. Add it under both prefixes (
STAGING_<NAME>andPROD_<NAME>); the engine strips the prefix and injects it as<NAME>(a plain env var for a Variable, asecretreffor a Secret). Full catalog: Environment variable catalog. -
Deploy. Push (or fast-forward merge) to
staging; the workflow runs on push. For an off-cycle run useworkflow_dispatchand watch it:Terminal window gh workflow run deploy-aca-staging.yml --ref staginggh run watchThen promote to
prodthe same way once staging is verified. See Deploy a service for the full deploy/promote loop and Roll back a deployment if it goes wrong.
Template inputs (reference)
Section titled “Template inputs (reference)”The complete input surface of the reusable engine. Only the three required inputs and your ingress shape usually matter; the rest have sane defaults.
| Input | Type | Default | Purpose |
|---|---|---|---|
github_environment |
string | required | Picks the STAGING_ / PROD_ prefix for env injection |
service_name |
string | required | ACA app name; the -staging / -prod suffix is stripped when publishing the FQDN var |
dockerfile_path |
string | required | Path to the Dockerfile the engine builds |
enable_ingress |
boolean | true |
false for background workers — no FQDN, no inbound |
is_external |
boolean | false |
true = public ingress on :443 (TLS at ACA); false = in-env only |
target_port |
number | 80 |
The container port your app listens on |
ingress_transport |
string | http2 |
auto / http / http2 / tcp; gRPC uses http2, a REST gateway overrides to http |
health_check_path |
string | "" |
If set, injects an HTTP readiness probe (only core-identity uses it, /healthz) |
pass_github_token_to_docker |
boolean | false |
Mounts a short-lived token into BuildKit to clone private soundverse* deps |
requires_proto_go_repo |
boolean | false |
Clones soundverse-proto-go before the build (Go data services) |
proto_go_ref |
string | staging |
Branch/tag of proto-go to check out |
resource_group |
string | rg-core |
Azure resource group |
aca_environment |
string | env-soundverse-core |
ACA environment |
acr_name |
string | soundversegeneral |
Azure Container Registry |
Secrets are all passed via secrets: inherit: AZURE_CREDENTIALS (required) plus the optional
GO_PUBLISH_APP_ID / GO_PUBLISH_APP_PRIVATE_KEY / GH_READ_REPO_TOKEN.
Related
Section titled “Related”- CI/CD deploy pipeline — the full pipeline diagram and what the engine does
- Environment & service discovery — prefix-strip injection + the auto-published
*_GRPCFQDNs - Deploy a service — the day-to-day deploy/promote loop
- Roll back a deployment — ACA revision pinning
- Add a tool worker — scaffolding a new worker repo from
template-core-tool - Environment variable catalog — every fleet env-var name