Check deployed versions#
How to find out which application version and which infrastructure commit
are live in dev, staging, and prd — including when the app itself is down
and the health endpoint is unreachable.
For how versions get there in the first place, see Release flow. For the runtime picture, see Deployment: runtime overview.
Quick answer: the helper script#
scripts/show_deployed_versions.sh
prints the deployed app version and infra commit for every environment using
only the GitHub paper trail (needs gh and jq, no Azure access):
$ scripts/show_deployed_versions.sh
== dev ==
app: v2.0.0 @ sha256:8a994c… [Release] 2026-06-11T15:18:38Z ref=main sha=23247174df94
infra: 2026-06-11T17:22:46Z ref=main sha=8a994cd2e6b7 (Terraform apply)
== staging ==
app: v2.0.0 @ sha256:8a994c… [Auto-deploy published release to staging] ...
...
The sections below explain where those numbers come from and how to read them by hand.
Live app version: the health endpoint#
When the app is up, every environment exposes its application version at
GET /v1/health:
curl https://<app-host>/v1/health # → {"status":"ok","version":"2.0.0"}
The version is qfa.__version__, read from the installed package. It is the
ground truth for what is actually serving — but it only reports the app
version, and it is unavailable when the App Service is down.
The GitHub paper trail#
Every deploy and every Terraform run declares a GitHub environment, so GitHub
records a deployment object for each one. These are queryable per
environment without touching Azure:
gh api "repos/{owner}/{repo}/deployments?environment=prd&per_page=5" \
--jq '.[] | {id, ref, sha: .sha[0:12], created_at}'
The records are accurate, but three properties make the raw list misleading on its own — read them before trusting a single field:
Important
Infra and app deploys are interleaved. Both the app deploy (the reusable
_deploy-releaseworkflow) and theTerraformworkflow declare an environment, so both producetask: deployrecords. The deployment record alone does not say which kind it was — you must follow it to its originating workflow (see below).Terraform
planruns also appear. TheTerraformworkflow declares its environment for every run, including theplanit runs on pull requests. A deployment record can therefore correspond to a plan that changed nothing. Real applies come fromworkflow_dispatchruns; plans come frompull_request/push.The deployment
refis not the version.Promote to prdruns frommain, so its deploymentrefismain, not the released tag. The version is instead recorded in the deployment status description (v2.0.0 @ sha256:…), written by the deploy pipeline — read that, notref. (Promote to stagingand the publish-triggered auto-deploy happen to run from the tag, so forstagingtherefis the version too.)
Note
The status description is populated by the annotate job in
_deploy-release.yaml, which runs after the deploy so its status is the latest
one — that is why the release tag, not the bare commit, shows on the repo’s
Environments page. Deploys from before this was added have an empty description;
fall back to the run logs (below) for those.
The reliable disambiguator is the originating workflow name. Each deployment
has a status whose target_url points at the workflow run that created it (the
annotate job’s version status sets it too, via log_url; read the first
status that has a target_url, since some may not):
Originating workflow |
What the record means |
|---|---|
|
An infrastructure plan or apply |
|
App auto-deployed to |
|
App deployed to |
|
App promoted to that environment |
Application version per environment#
The deployed app version is the release tag (and its immutable image digest). The most direct read, for any environment, is the latest app deployment’s status description:
ENV=prd
ID=$(gh api "repos/{owner}/{repo}/deployments?environment=$ENV&per_page=1" --jq '.[0].id')
gh api "repos/{owner}/{repo}/deployments/$ID/statuses" \
--jq 'first(.[] | select(.description != "") | .description)'
# → v2.0.0 @ sha256:…
(scripts/show_deployed_versions.sh does exactly this, for all environments,
and skips Terraform records.) Two notes per environment:
dev—Build from commitpublishes anephemeral-<branch>-<sha>image that is not a release, so adevdeploy is not always a released version. The App Service tags andGET /v1/healthreflect whichever image is live.older deploys — deployments cut before the status description was added have an empty description. Fall back to the run logs: the
verifyjob echoes the tag and the deploy job logs the digest.RUN=$(gh run list --workflow=promote-to-prd.yaml --status success -L 1 \ --json databaseId --jq '.[0].databaseId') gh run view "$RUN" --log | grep -E 'TAG:|Deploying v|sha256:'
Infrastructure version per environment#
Infrastructure has no version tag — the deployed “version” is the git commit
whose infra/ Terraform state was last applied. Find the last apply for an
environment by filtering Terraform runs to manual dispatches:
gh run list --workflow=terraform.yaml --event workflow_dispatch --status success \
-L 5 --json databaseId,headBranch,headSha,createdAt \
--jq '.[] | {id:.databaseId, ref:.headBranch, sha:.headSha[0:12], createdAt}'
The run metadata does not expose the environment / command inputs, so confirm
which environment a run targeted by cross-referencing the Terraform deployment
records for that environment, or by opening the run. pull_request / push
Terraform runs are always plan and never change infrastructure.
Note
Terraform state in the Azure backend is the ultimate source of truth. From a
machine with backend access you can read it directly — select the workspace
(terraform workspace select <env>) and run terraform show. The GitHub trail
above is the answer when you only have repository access.
Check everything at once#
The packaged way is scripts/show_deployed_versions.sh
(see Quick answer above). The loop it wraps,
for reference — it labels every recent deployment per environment with the
workflow that produced it, separating infra applies from app deploys and
exposing the ref/sha and timestamp in one pass:
for env in dev staging prd; do
echo "===== $env ====="
for id in $(gh api "repos/{owner}/{repo}/deployments?environment=$env&per_page=5" --jq '.[].id'); do
url=$(gh api "repos/{owner}/{repo}/deployments/$id/statuses" \
--jq 'first(.[] | select(.target_url != "") | .target_url) // ""')
run=$(echo "$url" | grep -oE 'runs/[0-9]+' | grep -oE '[0-9]+')
wf=$(gh api "repos/{owner}/{repo}/actions/runs/$run" --jq '.name')
meta=$(gh api "repos/{owner}/{repo}/deployments/$id" --jq '{ref, sha: .sha[0:12], created_at}')
echo " [$wf] $meta"
done
done
Read the result with the rules above: Terraform rows are infrastructure (only
the workflow_dispatch-originated ones are applies); every other workflow is an
app deploy, and for prd the version tag must still be read from that run’s logs.