Skip to content

Commit cc43f25

Browse files
authored
Merge pull request datacommonsorg#8 from DevelopmentFinanceObservatory/dev
release: dcp dev to main
2 parents 8d71da7 + 4bdd806 commit cc43f25

16 files changed

Lines changed: 219 additions & 314 deletions

File tree

‎.github/workflows/build-and-pin.yml‎

Lines changed: 54 additions & 95 deletions
Original file line numberDiff line numberDiff line change
@@ -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`.
@@ -35,20 +39,16 @@ on:
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

@@ -70,7 +70,10 @@ jobs:
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

‎.github/workflows/checks.yml‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
name: DFO checks
22

33
# ONE/DFO: the reusable body behind ci.yml and the deploy workflows' gate.
4-
# Checks the DFO-owned code paths only, not the upstream Data Commons test
5-
# suite; upstream code outside custom_dc/ is tested upstream. Two lanes:
6-
# - custom_dc/dfo/prototypes: the Next.js frontend (TypeScript, vitest).
7-
# - custom_dc/api_server: the Search API engine (ruff, ty, pytest).
4+
# Checks the DFO-owned frontend only, and upstream code outside custom_dc/ is
5+
# tested upstream. The single `prototypes` job covers custom_dc/dfo/prototypes,
6+
# the Next.js frontend (TypeScript, vitest). The `dev` ruleset requires it as
7+
# `checks / prototypes`.
88
#
99
# `on: workflow_call` rather than plain jobs in ci.yml so deploy-dev.yml and
1010
# deploy-prod.yml can `needs:` it directly, including on their

‎.github/workflows/ci.yml‎

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
name: CI
22

33
# ONE/DFO: runs checks.yml (the DFO-owned lint/typecheck/test jobs) on pull
4-
# requests into dev and main. main is the prod branch in the
5-
# DevelopmentFinanceObservatory org, where sync PRs need this check.
4+
# requests into dev and main.
65
#
76
# A draft pull request runs nothing; marking it ready starts the run. A push
87
# to dev has no run here, because deploy-dev.yml runs checks.yml on the merged

‎.github/workflows/deploy-dev.yml‎

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
name: Deploy dev
22

33
# ONE/DFO: builds the dcp image into dev's Artifact Registry and opens the
4-
# image-pin PR on ONEcampaign/dfo-infra. build-and-pin.yml holds the steps,
5-
# shared with deploy-prod.yml.
4+
# image-pin PR into DevelopmentFinanceObservatory/infra's `dev` branch.
5+
# build-and-pin.yml holds the steps, shared with deploy-prod.yml.
66

77
on:
88
push:
@@ -24,22 +24,19 @@ jobs:
2424
# lands with red tests (workflow_dispatch, an admin override) can't still
2525
# deploy.
2626
checks:
27-
if: github.repository_owner == 'ONEcampaign'
2827
uses: ./.github/workflows/checks.yml
2928

3029
build-and-deploy:
3130
needs: checks
32-
if: github.repository_owner == 'ONEcampaign'
3331
uses: ./.github/workflows/build-and-pin.yml
3432
# A called workflow sees no secrets, its environment's included, without this.
3533
secrets: inherit
3634
with:
3735
environment: dev
3836
tier: dev
3937
deploy-branch: dev
40-
infra-owner: ONEcampaign
41-
infra-repo: dfo-infra
42-
# dfo-infra's approve-image-pin.yml approves a pin PR once its checks
43-
# pass, and the armed auto-merge then lands it.
38+
infra-base: dev
39+
# Infra `dev` needs no approval, so the armed auto-merge lands the pin
40+
# once that branch's required checks pass.
4441
auto-merge: true
45-
app: dfo-bot
42+
app: dfo-pin

‎.github/workflows/deploy-prod.yml‎

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,8 @@
11
name: Deploy prod
22

33
# ONE/DFO: builds the dcp image into prod's Artifact Registry and opens the
4-
# image-pin PR on DevelopmentFinanceObservatory/infra. build-and-pin.yml
5-
# holds the steps, shared with deploy-dev.yml. Runs only in the
6-
# DevelopmentFinanceObservatory org, where main is a tree copy of a
7-
# ONEcampaign/dfo-dcp commit.
4+
# image-pin PR into DevelopmentFinanceObservatory/infra's `main` branch.
5+
# build-and-pin.yml holds the steps, shared with deploy-dev.yml.
86

97
on:
108
push:
@@ -23,21 +21,18 @@ permissions:
2321

2422
jobs:
2523
checks:
26-
if: github.repository_owner == 'DevelopmentFinanceObservatory'
2724
uses: ./.github/workflows/checks.yml
2825

2926
build-and-deploy:
3027
needs: checks
31-
if: github.repository_owner == 'DevelopmentFinanceObservatory'
3228
uses: ./.github/workflows/build-and-pin.yml
3329
# A called workflow sees no secrets, its environment's included, without this.
3430
secrets: inherit
3531
with:
3632
environment: production
3733
tier: prod
3834
deploy-branch: main
39-
infra-owner: DevelopmentFinanceObservatory
40-
infra-repo: infra
35+
infra-base: main
4136
# A release team member approves and merges the pin PR.
4237
auto-merge: false
4338
app: dfo-release

0 commit comments

Comments
 (0)