@@ -4,18 +4,22 @@ name: Build and pin
44# and pushes the dcp image, then opens a PR in the infra repo that moves the
55# Terraform image pin. dcp's Cloud Run service lives inside a pinned upstream
66# Terraform module, so its image is a Terraform-managed attribute. Unlike
7- # dfo- ai and dfo- apis, this repo can't deploy with `gcloud run services
7+ # ai and apis, this repo can't deploy with `gcloud run services
88# update`, since the next `terraform apply` would revert it. The infra repo's
99# own apply-on-merge does the deploy once the pin PR lands.
1010#
11+ # The pin PR goes to DevelopmentFinanceObservatory/infra, into the branch the
12+ # caller names in `infra-base`: dev pins land on infra `dev`, prod pins on
13+ # infra `main`.
14+ #
1115# The job declares the caller's environment, which supplies every GCP name
1216# (vars.REGION, vars.PROJECT_ID, vars.AR_REPO, vars.AR_CACHE_REPO,
13- # vars.DFO_APIS_BASE_URL ), the WIF_PROVIDER/WIF_SA secrets, and the pair for the
17+ # vars.DFO_SITE_ORIGIN ), the WIF_PROVIDER/WIF_SA secrets, and the pair for the
1418# GitHub App that opens the pin PR. A caller job that uses a reusable workflow
1519# cannot declare an environment, so it cannot hand environment-level values
1620# down. The caller names its App in the `app` input and the job reads only that
1721# App's pair:
18- # dfo-bot : secrets.DEPLOY_APP_ID , secrets.DEPLOY_APP_PRIVATE_KEY (dev)
22+ # dfo-pin : vars.PIN_APP_CLIENT_ID , secrets.PIN_APP_PRIVATE_KEY (dev)
1923# dfo-release: vars.RELEASE_APP_CLIENT_ID, secrets.RELEASE_APP_PRIVATE_KEY (production)
2024# A called workflow sees no secrets, its environment's included, unless the
2125# caller passes them, so each caller uses `secrets: inherit`.
3539 description : The only branch this workflow pins from.
3640 required : true
3741 type : string
38- infra-owner :
39- description : Owner of the infra repo that receives the pin PR.
40- required : true
41- type : string
42- infra-repo :
43- description : Name of the infra repo that receives the pin PR.
42+ infra-base :
43+ description : The infra branch the pin PR targets (dev, main).
4444 required : true
4545 type : string
4646 auto-merge :
4747 description : Arm auto-merge on the pin PR.
4848 required : true
4949 type : boolean
5050 app :
51- description : The GitHub App that opens the pin PR (dfo-bot , dfo-release).
51+ description : The GitHub App that opens the pin PR (dfo-pin , dfo-release).
5252 required : true
5353 type : string
5454
7070 # is the only writer on AR_CACHE_REPO.
7171 CACHE_PATH : ${{ vars.REGION }}-docker.pkg.dev/${{ vars.PROJECT_ID }}/${{ vars.AR_CACHE_REPO }}/dfo-services-image
7272 TIER : ${{ inputs.tier }}
73- INFRA_REPO : ${{ inputs.infra-owner }}/${{ inputs.infra-repo }}
73+ INFRA_OWNER : DevelopmentFinanceObservatory
74+ INFRA_NAME : infra
75+ INFRA_REPO : DevelopmentFinanceObservatory/infra
76+ INFRA_BASE : ${{ inputs.infra-base }}
7477 steps :
7578 # Runs before authentication, so a missing variable or secret fails here
7679 # with its name, not as an empty input inside an action or a failed Next
@@ -83,19 +86,19 @@ jobs:
8386 PROJECT_ID : ${{ vars.PROJECT_ID }}
8487 AR_REPO : ${{ vars.AR_REPO }}
8588 AR_CACHE_REPO : ${{ vars.AR_CACHE_REPO }}
86- DFO_APIS_BASE_URL : ${{ vars.DFO_APIS_BASE_URL }}
89+ DFO_SITE_ORIGIN : ${{ vars.DFO_SITE_ORIGIN }}
8790 WIF_PROVIDER : ${{ secrets.WIF_PROVIDER }}
8891 WIF_SA : ${{ secrets.WIF_SA }}
89- DEPLOY_APP_ID : ${{ secrets.DEPLOY_APP_ID }}
90- DEPLOY_APP_PRIVATE_KEY : ${{ secrets.DEPLOY_APP_PRIVATE_KEY }}
92+ PIN_APP_CLIENT_ID : ${{ vars.PIN_APP_CLIENT_ID }}
93+ PIN_APP_PRIVATE_KEY : ${{ secrets.PIN_APP_PRIVATE_KEY }}
9194 RELEASE_APP_CLIENT_ID : ${{ vars.RELEASE_APP_CLIENT_ID }}
9295 RELEASE_APP_PRIVATE_KEY : ${{ secrets.RELEASE_APP_PRIVATE_KEY }}
9396 run : |
94- required="REGION PROJECT_ID AR_REPO AR_CACHE_REPO DFO_APIS_BASE_URL WIF_PROVIDER WIF_SA"
97+ required="REGION PROJECT_ID AR_REPO AR_CACHE_REPO DFO_SITE_ORIGIN WIF_PROVIDER WIF_SA"
9598 case "$APP" in
96- dfo-bot ) required="$required DEPLOY_APP_ID DEPLOY_APP_PRIVATE_KEY " ;;
99+ dfo-pin ) required="$required PIN_APP_CLIENT_ID PIN_APP_PRIVATE_KEY " ;;
97100 dfo-release) required="$required RELEASE_APP_CLIENT_ID RELEASE_APP_PRIVATE_KEY" ;;
98- *) echo "::error::Unknown app '$APP'; expected dfo-bot or dfo-release."; exit 1 ;;
101+ *) echo "::error::Unknown app '$APP'; expected dfo-pin or dfo-release."; exit 1 ;;
99102 esac
100103 missing=""
101104 for v in $required; do
@@ -180,12 +183,12 @@ jobs:
180183 # on the rare commits that move the submodule, and the website SHA is
181184 # already recoverable from the image tag.
182185 #
183- # DFO_APIS_BASE_URL is the dfo-apis origin next.config.mjs bakes into
186+ # DFO_SITE_ORIGIN is the website origin next.config.mjs bakes into
184187 # the /api/search and /api/datasets rewrites. The Next build stage
185188 # fails when it is empty.
186189 build-args : |
187190 MIXER_HASH=${{ steps.target.outputs.mixer_hash }}
188- DFO_APIS_BASE_URL =${{ vars.DFO_APIS_BASE_URL }}
191+ DFO_SITE_ORIGIN =${{ vars.DFO_SITE_ORIGIN }}
189192 # Registry-backed cache, not GHA's own cache backend, which is
190193 # capped and evicted too aggressively for this build. mode=max
191194 # caches every intermediate layer, so the mixer and Next.js build
@@ -211,25 +214,17 @@ jobs:
211214 # condition -- both restrict this job to running for deploy-branch at
212215 # all. This `if` is belt-and-braces on top of that, guarding the mint,
213216 # checkout, and pin steps that touch the App token and the infra repo.
214- - name : Mint infra repo app token (dfo-bot)
215- id : app-token-bot
216- if : inputs.app == 'dfo-bot' && github.ref == format('refs/heads/{0}', inputs.deploy-branch)
217- uses : actions/create-github-app-token@fee1f7d63c2ff003460e3d139729b119787bc349 # v2
218- with :
219- app-id : ${{ secrets.DEPLOY_APP_ID }}
220- private-key : ${{ secrets.DEPLOY_APP_PRIVATE_KEY }}
221- owner : ${{ inputs.infra-owner }}
222- repositories : ${{ inputs.infra-repo }}
223-
224- - name : Mint infra repo app token (dfo-release)
225- id : app-token-release
226- if : inputs.app == 'dfo-release' && github.ref == format('refs/heads/{0}', inputs.deploy-branch)
217+ # The check step above has already failed the job if the named App's
218+ # pair is unset, so each expression resolves to that App's value.
219+ - name : Mint the infra repo App token
220+ id : app-token
221+ if : github.ref == format('refs/heads/{0}', inputs.deploy-branch)
227222 uses : actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
228223 with :
229- client-id : ${{ vars.RELEASE_APP_CLIENT_ID }}
230- private-key : ${{ secrets.RELEASE_APP_PRIVATE_KEY }}
231- owner : ${{ inputs.infra-owner }}
232- repositories : ${{ inputs.infra-repo }}
224+ client-id : ${{ inputs.app == 'dfo-pin' && vars.PIN_APP_CLIENT_ID || vars.RELEASE_APP_CLIENT_ID }}
225+ private-key : ${{ inputs.app == 'dfo-pin' && secrets.PIN_APP_PRIVATE_KEY || secrets.RELEASE_APP_PRIVATE_KEY }}
226+ owner : ${{ env.INFRA_OWNER }}
227+ repositories : ${{ env.INFRA_NAME }}
233228 permission-contents : write
234229 permission-pull-requests : write
235230
@@ -238,7 +233,8 @@ jobs:
238233 uses : actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
239234 with :
240235 repository : ${{ env.INFRA_REPO }}
241- token : ${{ steps.app-token-bot.outputs.token || steps.app-token-release.outputs.token }}
236+ ref : ${{ inputs.infra-base }}
237+ token : ${{ steps.app-token.outputs.token }}
242238 path : infra-repo
243239
244240 - name : Rewrite the pin line
@@ -267,11 +263,11 @@ jobs:
267263 exit 1
268264 fi
269265
270- # main can already carry this sha's pin (a redeploy, or a re-run
271- # after the previous run's PR merged), in which case sed matched
272- # nothing to change and there's no diff to commit.
266+ # The base branch can already carry this sha's pin (a redeploy, or
267+ # a re-run after the previous run's PR merged), in which case sed
268+ # matched nothing to change and there's no diff to commit.
273269 if git diff --quiet -- "$TFVARS"; then
274- echo "::notice::main already pinned to this digest, nothing to commit"
270+ echo "::notice::${INFRA_BASE} already pinned to this digest, nothing to commit"
275271 changed=false
276272 else
277273 changed=true
@@ -283,45 +279,15 @@ jobs:
283279 echo "title=chore(dcp): pin ${TIER} image to ${SHORT_SHA}"
284280 } >> "$GITHUB_OUTPUT"
285281
286- # dfo-bot pushes a local commit, which dfo-infra's approve-image-pin.yml
287- # accepts as it is.
288- - name : Commit and push the pin (dfo-bot)
289- if : inputs.app == 'dfo-bot' && steps.rewrite.outputs.changed == 'true'
290- working-directory : infra-repo
291- env :
292- TFVARS : ${{ steps.rewrite.outputs.tfvars }}
293- BRANCH : ${{ steps.rewrite.outputs.branch }}
294- TITLE : ${{ steps.rewrite.outputs.title }}
295- run : |
296- set -euo pipefail
297-
298- git config user.name "dfo-dcp-deployer[bot]"
299- git config user.email "dfo-dcp-deployer[bot]@users.noreply.github.com"
300-
301- git checkout -B "$BRANCH"
302- git add "$TFVARS"
303- git commit -m "$TITLE"
304-
305- # Force-pushed to a single reused branch, so that if two runs'
306- # git operations ever interleave, the last writer wins instead of
307- # leaving push order to chance and risking a stale pin.
308- git push --force origin "HEAD:refs/heads/${BRANCH}"
309-
310- # The new org's sync-guard accepts a pin commit only when its author and
311- # committer are dfo-release[bot], its signature verifies, and its one
312- # parent is the PR's base. GitHub signs a commit a GitHub App creates
313- # through the API when the request carries no author, committer, or
314- # signature ("Signature verification for bots", docs.github.com
315- # /en/authentication/managing-commit-signature-verification
316- # /about-commit-signature-verification). So this step builds the tree
317- # and the commit through the REST git database API ("Create a tree" and
318- # "Create a commit", docs.github.com/en/rest/git), naming main's head as
319- # the only parent, and force-moves the pin branch to it.
320- - name : Commit the pin through the API (dfo-release)
321- if : inputs.app == 'dfo-release' && steps.rewrite.outputs.changed == 'true'
282+ # The pin commit is built through the REST git database API ("Create a
283+ # tree" and "Create a commit", docs.github.com/en/rest/git), so it is
284+ # authored by the App and GitHub signs it. Its one parent is the base
285+ # branch's head, and the pin branch is force-moved to it.
286+ - name : Commit the pin through the API
287+ if : steps.rewrite.outputs.changed == 'true'
322288 working-directory : infra-repo
323289 env :
324- GH_TOKEN : ${{ steps.app-token-release .outputs.token }}
290+ GH_TOKEN : ${{ steps.app-token.outputs.token }}
325291 TFVARS : ${{ steps.rewrite.outputs.tfvars }}
326292 BRANCH : ${{ steps.rewrite.outputs.branch }}
327293 TITLE : ${{ steps.rewrite.outputs.title }}
@@ -345,17 +311,12 @@ jobs:
345311 exit 1
346312 fi
347313
348- commit ="$(jq -n --arg message "$TITLE" --arg tree "$tree" --arg parent "$parent" \
314+ sha ="$(jq -n --arg message "$TITLE" --arg tree "$tree" --arg parent "$parent" \
349315 '{message: $message, tree: $tree, parents: [$parent]}' |
350- gh api "repos/${INFRA_REPO}/git/commits" --input - \
351- --jq '[.sha, (.verification.verified | tostring), .verification.reason] | @tsv')"
352- IFS=$'\t' read -r sha verified reason <<<"$commit"
353- if [ "$verified" != "true" ]; then
354- echo "::error::GitHub did not verify pin commit $sha (reason: $reason), and sync-guard would reject it."
355- exit 1
356- fi
316+ gh api "repos/${INFRA_REPO}/git/commits" --input - --jq .sha)"
357317
358- # Force-moved, like the dfo-bot push, so the last run wins.
318+ # Force-moved to a single reused branch, so that if two runs ever
319+ # interleave, the last writer wins and no stale pin survives.
359320 if gh api "repos/${INFRA_REPO}/git/ref/heads/${BRANCH}" --silent 2>/dev/null; then
360321 gh api -X PATCH "repos/${INFRA_REPO}/git/refs/heads/${BRANCH}" \
361322 -f sha="$sha" -F force=true --silent
@@ -370,14 +331,13 @@ jobs:
370331 if : github.ref == format('refs/heads/{0}', inputs.deploy-branch)
371332 working-directory : infra-repo
372333 env :
373- GH_TOKEN : ${{ steps.app-token-bot.outputs.token || steps.app-token-release .outputs.token }}
334+ GH_TOKEN : ${{ steps.app-token.outputs.token }}
374335 CHANGED : ${{ steps.rewrite.outputs.changed }}
375336 BRANCH : ${{ steps.rewrite.outputs.branch }}
376337 TITLE : ${{ steps.rewrite.outputs.title }}
377338 IMAGE_REF : ${{ steps.digest.outputs.image }}
378339 TAG_REF : ${{ steps.target.outputs.image }}
379340 AUTO_MERGE : ${{ inputs.auto-merge }}
380- INFRA_NAME : ${{ inputs.infra-repo }}
381341 run : |
382342 set -euo pipefail
383343
@@ -386,16 +346,15 @@ jobs:
386346 exit 0
387347 fi
388348
389- # Opened with the App token minted above, never GITHUB_TOKEN.
390- # GitHub suppresses other workflow runs triggered by
349+ # Opened with the App token minted above. GitHub suppresses other workflow runs triggered by
391350 # GITHUB_TOKEN-authored pushes and PRs, so the infra repo's required
392351 # status checks would never fire and the PR would sit unmergeable.
393352 BODY="Automated image pin from ${GITHUB_REPOSITORY}@${GITHUB_SHA}. Merging runs ${INFRA_NAME}'s apply-on-merge, which deploys this image to dfo-${TIER}."$'\n\n'"Digest (pinned): ${IMAGE_REF}"$'\n'"Tag (browsable): ${TAG_REF}"
394353
395354 pr_url=$(gh pr list --repo "$INFRA_REPO" --head "$BRANCH" --state open --json url --jq '.[0].url // empty')
396355 if [ -z "$pr_url" ]; then
397356 pr_url=$(gh pr create --repo "$INFRA_REPO" \
398- --base main \
357+ --base "$INFRA_BASE" \
399358 --head "$BRANCH" \
400359 --title "$TITLE" \
401360 --body "$BODY")
@@ -410,9 +369,9 @@ jobs:
410369
411370 # Auto-merge is armed here, with the App token, because a merge made
412371 # with GITHUB_TOKEN creates no workflow runs, so the infra repo's
413- # apply.yml would never fire on the merged pin. Arming it at
414- # PR-creation time is safe: auto-merge still waits on branch
415- # protection, which requires an approving review .
372+ # apply.yml would never fire on the merged pin. Auto-merge waits on
373+ # the base branch's ruleset: on infra `dev`, its required checks
374+ # (`Changed stacks planned` among them), with no approval .
416375 if [ "$AUTO_MERGE" = "true" ]; then
417376 gh pr merge "$pr_url" --auto --squash
418377 fi
0 commit comments