Skip to content

Promote (test / prod) #11

Promote (test / prod)

Promote (test / prod) #11

Workflow file for this run

name: Promote (test / prod)
# Promotion across the security boundary:
# - Manual workflow_dispatch -> TEST: build the canonical image from the chosen ref into TEST's
# own Artifact Registry and deploy it to test. (test doubles as UAT.) This is the "one
# rebuild into test" — dev's image is never promoted, since dev is outside the boundary.
# - Push a v* tag -> PROD: deploy the exact image TEST already validated, by digest,
# referencing TEST's registry directly (no rebuild, no copy). Gated by the `prod`
# Environment's required-reviewer rule. prod's deployer SA + Cloud Run service agent must
# have read on TEST's Artifact Registry. The tag must equal "v" + pyproject.toml's version
# (checked below): no rebuild happens here, so the version is whatever test baked in.
#
# For the prod path, test's registry coordinates come from repo-level Variables (test's project
# isn't in prod's Environment): TEST_PROJECT_ID (required), TEST_REGION / TEST_AR_REPO (optional).
# The image for the tagged commit must already exist in test (i.e. it was promoted to test first).
# See dev/deployment.md.
on:
workflow_dispatch:
inputs:
ref:
description: "Git ref (branch / tag / SHA) to build into test"
type: string
required: true
default: main
target:
description: "Which service(s) to deploy"
type: choice
options: [rest, mcp, both]
default: both
push:
tags: ["v*"]
permissions:
contents: read
jobs:
# ---------- TEST path (manual dispatch): build canonical image into test, then deploy ----------
build-test:
if: ${{ github.event_name == 'workflow_dispatch' }}
runs-on: ubuntu-latest
environment: test
outputs:
build_label: ${{ steps.vars.outputs.sha }}
steps:
- uses: actions/checkout@v7
with:
ref: ${{ inputs.ref }}
- name: Resolve short SHA
id: vars
run: echo "sha=$(git rev-parse --short=7 HEAD)" >> "$GITHUB_OUTPUT"
- name: Authenticate to Google Cloud
uses: google-github-actions/auth@v3
with:
credentials_json: ${{ secrets.GCP_SA_KEY }}
- name: Set up gcloud
uses: google-github-actions/setup-gcloud@v3
with:
project_id: ${{ secrets.GCP_PROJECT_ID }}
- name: Build & push canonical image (Cloud Build)
env:
PROJECT: ${{ secrets.GCP_PROJECT_ID }}
REGION: ${{ vars.REGION || 'us-central1' }}
AR_REPO: ${{ vars.AR_REPO || 'idc' }}
SHA: ${{ steps.vars.outputs.sha }}
run: |
set -euo pipefail
IMAGE="${REGION}-docker.pkg.dev/${PROJECT}/${AR_REPO}/idc-api-v3:${SHA}"
gcloud builds submit --config dev/cloudbuild.yaml --substitutions _IMAGE="$IMAGE"
deploy-test:
if: ${{ github.event_name == 'workflow_dispatch' }}
needs: build-test
uses: ./.github/workflows/deploy.yml
with:
environment: test
build_label: ${{ needs.build-test.outputs.build_label }}
target: ${{ inputs.target }}
secrets: inherit
# ---------- PROD path (v* tag): deploy test's digest to prod, no rebuild ----------
resolve-prod:
if: ${{ github.event_name == 'push' }}
runs-on: ubuntu-latest
outputs:
build_label: ${{ steps.r.outputs.sha }}
source_image: ${{ steps.r.outputs.image }}
steps:
- uses: actions/checkout@v7
# The tag names the release, but the version the running server reports comes from
# pyproject.toml, baked into the image at build time. Prod redeploys test's existing digest
# and never rebuilds, so a tag that disagrees with the packaged version would deploy fine
# and quietly serve the wrong version at /v3/version. Fail here, before the reviewer gate.
- name: Verify the tag matches the packaged version
env:
TAG: ${{ github.ref_name }}
run: |
set -euo pipefail
VERSION=$(python3 -c 'import tomllib; print(tomllib.load(open("pyproject.toml","rb"))["project"]["version"])')
if [ "${TAG#v}" != "$VERSION" ]; then
echo "::error::Tag '${TAG}' does not match pyproject.toml version '${VERSION}'. The version is baked in when test builds the image, so bump pyproject.toml, promote that commit to test, then tag it. See CONTRIBUTING.md -> Releasing."
exit 1
fi
echo "Tag ${TAG} matches packaged version ${VERSION}."
- id: r
env:
TEST_PROJECT: ${{ vars.TEST_PROJECT_ID }}
TEST_REGION: ${{ vars.TEST_REGION || vars.REGION || 'us-central1' }}
TEST_AR_REPO: ${{ vars.TEST_AR_REPO || vars.AR_REPO || 'idc' }}
run: |
set -euo pipefail
if [ -z "${TEST_PROJECT:-}" ]; then
echo "::error::Repo variable TEST_PROJECT_ID is not set — prod deploys test's image and needs to know test's project."
exit 1
fi
SHA=${GITHUB_SHA:0:7}
{
echo "sha=$SHA"
echo "image=${TEST_REGION}-docker.pkg.dev/${TEST_PROJECT}/${TEST_AR_REPO}/idc-api-v3:${SHA}"
} >> "$GITHUB_OUTPUT"
deploy-prod:
if: ${{ github.event_name == 'push' }}
needs: resolve-prod
uses: ./.github/workflows/deploy.yml
with:
environment: prod
build_label: ${{ needs.resolve-prod.outputs.build_label }}
source_image: ${{ needs.resolve-prod.outputs.source_image }}
target: both
secrets: inherit