Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 10 additions & 17 deletions .github/workflows/apply-state.yml
Original file line number Diff line number Diff line change
Expand Up @@ -45,14 +45,18 @@ on:
permissions: {}

jobs:
# First, before anything is read or changed: who dispatched this (AC18).
access:
name: access
resolve:
name: resolve
runs-on: ubuntu-26.04
timeout-minutes: 5
timeout-minutes: 10
permissions:
contents: read
deployments: read
outputs:
matrix: ${{ toJSON(fromJSON(steps.resolve.outputs.result).matrix) }}
steps:
# First, before any token is minted or anything is read: who dispatched
# this (AC18). A step, not a job, to save a job start.
- name: Check who dispatched this
uses: DEFRA/ai-platform-workflows/.github/actions/gitops@v2
with:
Expand All @@ -68,17 +72,6 @@ jobs:
# reconcile-all keys on it (DD25).
AIP_WORKFLOW_REF: ${{ github.workflow_ref }}

resolve:
name: resolve
needs: access
runs-on: ubuntu-26.04
timeout-minutes: 10
permissions:
contents: read
deployments: read
outputs:
matrix: ${{ toJSON(fromJSON(steps.resolve.outputs.result).matrix) }}
steps:
- id: reader
uses: actions/create-github-app-token@v3
with:
Expand Down Expand Up @@ -183,9 +176,9 @@ jobs:
ARM_USE_OIDC: 'true'
ARM_USE_CLI: 'false'
steps:
# A re-run of failed jobs reuses a successful access job, so the actor
# A re-run of failed jobs reuses a successful resolve job, so the actor
# of this attempt is checked again here, before any token or Terraform.
# AIP_ENVIRONMENT is the input, as in the access job (the job's own is
# AIP_ENVIRONMENT is the input, as in the resolve job (the job's own is
# the matrix leg).
- name: Check who ran this attempt
uses: DEFRA/ai-platform-workflows/.github/actions/gitops@v2
Expand Down
7 changes: 5 additions & 2 deletions scripts/apply.sh
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ params_file="$RUNNER_TEMP/params.tfvars.json"
report_file="run-report-$environment.json"
graph="https://graph.microsoft.com/v1.0"
entra_objects='select(.type == "azuread_application" or .type == "azuread_service_principal")'
# About 9 ARM reads a second at 2 s each, well inside ARM's 25 a second per
# principal for 50 teams; azurerm and azuread serialise writes to one parent.
parallelism=20

checks() {
node "${AIP_CHECKS:?}" "$@"
Expand Down Expand Up @@ -114,7 +117,7 @@ plan() {
-var "environment=$environment" -var "cdp=$cdp"
-var "platform_state=$(jq -c '. + {key: "platform.tfstate"}' <<< "$location")")
fi
terraform -chdir="$root" plan -input=false -lock-timeout=15m -out=tfplan "${vars[@]}"
terraform -chdir="$root" plan -input=false -lock-timeout=15m -parallelism="$parallelism" -out=tfplan "${vars[@]}"
terraform -chdir="$root" show -json tfplan > plan.json
echo "entra_deletes=$(jq "[.resource_changes[]? | $entra_objects | select(.change.actions | index(\"delete\"))] | length" plan.json)" >> "$GITHUB_OUTPUT"

Expand All @@ -136,7 +139,7 @@ plan() {
# the run log shows nothing until the apply is over; a long apply needs to be
# watched as it runs.
apply() {
terraform -chdir="$root" apply -input=false -lock-timeout=15m -json tfplan |
terraform -chdir="$root" apply -input=false -lock-timeout=15m -parallelism="$parallelism" -json tfplan |
tee apply.jsonl | jq --unbuffered -r '."@message"'
}

Expand Down
4 changes: 2 additions & 2 deletions tests/integration/scripts/apply.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ describe('apply.sh plan', () => {
line.includes('/platform plan')
)
).toMatch(
/-var release=v0\.4\.0 -var entra_enabled=true -var-file=.*\/params\.tfvars\.json$/
/plan -input=false -lock-timeout=15m -parallelism=20 -out=tfplan -var release=v0\.4\.0 -var entra_enabled=true -var-file=.*\/params\.tfvars\.json$/
)
expect(command('checks params')).toEqual([
expect.stringMatching(
Expand Down Expand Up @@ -289,7 +289,7 @@ describe('apply.sh apply', () => {
expect(result.stdout).toContain('Apply complete!')
expect(command('terraform -chdir=')).toEqual([
expect.stringMatching(
/\/platform apply -input=false -lock-timeout=15m -json tfplan$/
/\/platform apply -input=false -lock-timeout=15m -parallelism=20 -json tfplan$/
)
])
expect(readFileSync(join(workspace.dir, 'apply.jsonl'), 'utf8')).toContain(
Expand Down
21 changes: 13 additions & 8 deletions tests/unit/workflows.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -194,15 +194,20 @@ describe('apply-state.yml', () => {
expect(text).toMatch(/^ {6}force_unlock:$/m)
})

test('checks who dispatched before it resolves anything, passing the caller workflow for reconcile-all', () => {
const access = job(file, 'access')

expect(access).toContain('AIP_ADMIN_SCOPE: apply')
expect(access).toContain('AIP_WORKFLOW_REF: ${{ github.workflow_ref }}')
expect(access).toContain(
test('checks who dispatched before it mints a token or resolves anything, passing the caller workflow for reconcile-all', () => {
const resolve = job(file, 'resolve')
const check = resolve.indexOf('command: admin-check')

expect(check).toBeGreaterThan(-1)
expect(check).toBeLessThan(resolve.indexOf('create-github-app-token'))
expect(check).toBeLessThan(resolve.indexOf('command: resolve-apply'))
expect(resolve).toContain('AIP_ADMIN_SCOPE: apply')
expect(resolve).toContain('AIP_WORKFLOW_REF: ${{ github.workflow_ref }}')
expect(resolve).toContain(
"AIP_BREAK_GLASS_INPUTS: ${{ inputs.allow_destroy || inputs.force_unlock != '' }}"
)
expect(job(file, 'resolve')).toMatch(/^ {4}needs: access$/m)
expect(resolve).not.toMatch(/^ {4}needs:/m)
expect(read(`.github/workflows/${file}`)).not.toMatch(/^ {2}access:$/m)
})

test('reads each environment pause in its {env}-events Environment before the apply starts', () => {
Expand All @@ -226,7 +231,7 @@ describe('apply-state.yml', () => {
/^ {4}permissions:\n {6}contents: read\n {6}deployments: read\n {6}id-token: write\n(?: {6}#.*\n)* {6}pull-requests: read\n/m
)

for (const other of ['access', 'resolve', 'gate', 'tenants']) {
for (const other of ['resolve', 'gate', 'tenants']) {
expect(job(file, other)).not.toContain('pull-requests')
}

Expand Down
Loading