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
45 changes: 45 additions & 0 deletions .github/workflows/backend-production.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,53 @@ permissions:
id-token: write

jobs:
infra-apply:
name: Apply production Terraform
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- uses: hashicorp/setup-terraform@v3
with:
terraform_version: "1.15.3"

- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: ${{ secrets.AWS_BACKEND_PRODUCTION_ROLE_ARN }}
aws-region: us-east-1

- name: Bootstrap production Terraform backend
run: bash infra/terraform/bootstrap.sh production

- name: Terraform init
working-directory: infra/terraform/production
run: terraform init -input=false -no-color

- name: Terraform plan
working-directory: infra/terraform/production
run: |
set -euo pipefail
terraform plan -input=false -no-color -out=tfplan 2>&1 | tee tfplan.txt

- name: Add plan to workflow summary
working-directory: infra/terraform/production
run: |
{
echo "## Production Terraform plan"
echo
echo '```terraform'
cat tfplan.txt
echo '```'
} >> "$GITHUB_STEP_SUMMARY"

- name: Terraform apply
working-directory: infra/terraform/production
run: terraform apply -input=false -auto-approve -no-color tfplan

prepare:
name: Resolve backend service manifest
needs: infra-apply
runs-on: ubuntu-latest
outputs:
services: ${{ steps.resolve.outputs.services }}
Expand Down
83 changes: 83 additions & 0 deletions .github/workflows/backend-staging.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ on:
branches: [main]
paths:
- 'backend/**'
- 'infra/terraform/**'
- '.github/workflows/backend-staging.yml'
- 'scripts/ci/backend_staging_smoke.py'
- 'backend/manifest/deployment-services.json'
Expand All @@ -15,12 +16,94 @@ concurrency:
cancel-in-progress: false

permissions:
actions: read
contents: read
id-token: write
pull-requests: read

jobs:
infra-apply:
name: Apply staging Terraform
runs-on: ubuntu-latest
outputs:
used_reviewed_plan: ${{ steps.reviewed-plan.outputs.found }}
steps:
- uses: actions/checkout@v4

- uses: hashicorp/setup-terraform@v3
with:
terraform_version: "1.15.3"

- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: ${{ secrets.AWS_BACKEND_STAGING_ROLE_ARN }}
aws-region: us-east-1

- name: Bootstrap staging Terraform backend
run: bash infra/terraform/bootstrap.sh staging

- name: Terraform init
working-directory: infra/terraform/staging
run: terraform init -input=false -no-color

- name: Download reviewed PR plan when available
id: reviewed-plan
env:
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
echo "found=false" >> "$GITHUB_OUTPUT"

pr_number=$(gh api \
-H "Accept: application/vnd.github+json" \
-H "X-GitHub-Api-Version: 2022-11-28" \
"/repos/${GITHUB_REPOSITORY}/commits/${GITHUB_SHA}/pulls" \
--jq '.[0].number // empty' || true)
if [ -z "$pr_number" ]; then
echo "No merged PR found for ${GITHUB_SHA}; applying a fresh plan."
exit 0
fi

head_sha=$(gh api "/repos/${GITHUB_REPOSITORY}/pulls/${pr_number}" --jq '.head.sha // empty')
if [ -z "$head_sha" ]; then
echo "No head SHA found for PR #${pr_number}; applying a fresh plan."
exit 0
fi

run_id=$(gh api \
"/repos/${GITHUB_REPOSITORY}/actions/workflows/infra-validate.yml/runs?event=pull_request&head_sha=${head_sha}&status=success&per_page=20" \
--jq '.workflow_runs[] | select(.conclusion == "success") | .id' \
| head -n 1)
if [ -z "$run_id" ]; then
echo "No successful infra-validate run found for PR #${pr_number}; applying a fresh plan."
exit 0
fi

artifact_name="epac-tfplan-staging-${head_sha}"
if gh run download "$run_id" --name "$artifact_name" --dir infra/terraform/staging; then
echo "Downloaded reviewed Terraform plan artifact ${artifact_name} from run ${run_id}."
echo "found=true" >> "$GITHUB_OUTPUT"
else
echo "Reviewed Terraform plan artifact ${artifact_name} was not available; applying a fresh plan."
fi

- name: Terraform apply
working-directory: infra/terraform/staging
run: |
set -euo pipefail
if [ "${{ steps.reviewed-plan.outputs.found }}" = "true" ] && [ -f tfplan ]; then
echo "Applying reviewed Terraform plan artifact."
terraform apply -input=false -auto-approve -no-color tfplan
else
echo "Generating and applying a fresh Terraform plan."
terraform plan -input=false -no-color -out=tfplan 2>&1 | tee tfplan.txt
terraform apply -input=false -auto-approve -no-color tfplan
fi

prepare:
name: Resolve backend service manifest
needs: infra-apply
runs-on: ubuntu-latest
outputs:
services: ${{ steps.resolve.outputs.services }}
Expand Down
113 changes: 113 additions & 0 deletions .github/workflows/infra-validate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,16 @@ name: Infrastructure Validation
on:
pull_request:
paths:
- 'backend/**'
- 'infra/terraform/**'
- '.github/workflows/infra-validate.yml'

permissions:
contents: read
id-token: write
issues: write
pull-requests: read

jobs:
validate:
name: Terraform Validate
Expand Down Expand Up @@ -34,3 +41,109 @@ jobs:
run: |
terraform init -backend=false
terraform validate

infra-plan:
name: infra-plan
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- uses: hashicorp/setup-terraform@v3
with:
terraform_version: "1.15.3"

- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: ${{ secrets.AWS_BACKEND_STAGING_ROLE_ARN }}
aws-region: us-east-1

- name: Bootstrap staging Terraform backend
run: bash infra/terraform/bootstrap.sh staging

- name: Terraform init
working-directory: infra/terraform/staging
run: terraform init -input=false -no-color

- name: Terraform plan
id: plan
working-directory: infra/terraform/staging
run: |
set +e
terraform plan -input=false -no-color -out=tfplan > tfplan.txt 2>&1
status=$?
set -e
cat tfplan.txt
echo "exitcode=$status" >> "$GITHUB_OUTPUT"

- name: Post sticky plan comment
uses: actions/github-script@v7
env:
PLAN_EXIT_CODE: ${{ steps.plan.outputs.exitcode }}
PLAN_PATH: infra/terraform/staging/tfplan.txt
PLAN_MARKER: '<!-- epac-tfplan-staging -->'
with:
script: |
const fs = require('fs');

const marker = process.env.PLAN_MARKER;
const exitCode = process.env.PLAN_EXIT_CODE || '1';
const planPath = process.env.PLAN_PATH;
const statusLabel = exitCode === '0' ? 'succeeded' : 'failed';
let plan = fs.existsSync(planPath) ? fs.readFileSync(planPath, 'utf8') : 'Terraform did not produce a plan output file.';
const maxPlanLength = 60000;
if (plan.length > maxPlanLength) {
plan = `${plan.slice(0, maxPlanLength)}\n\n[Plan output truncated to fit GitHub comment limits.]`;
}

const body = [
marker,
`### Staging Terraform plan ${statusLabel}`,
'',
`Commit: \`${context.payload.pull_request.head.sha}\``,
`Workflow run: ${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`,
'',
'<details><summary>terraform plan output</summary>',
'',
'```terraform',
plan,
'```',
'',
'</details>'
].join('\n');

const comments = await github.paginate(github.rest.issues.listComments, {
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
per_page: 100
});
const existing = comments.find(comment => comment.user.type === 'Bot' && comment.body.includes(marker));
if (existing) {
await github.rest.issues.updateComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: existing.id,
body
});
} else {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body
});
}

- name: Upload staging Terraform plan
if: ${{ steps.plan.outputs.exitcode == '0' }}
uses: actions/upload-artifact@v4
with:
name: epac-tfplan-staging-${{ github.event.pull_request.head.sha }}
path: infra/terraform/staging/tfplan
if-no-files-found: error
retention-days: 14

- name: Fail if plan failed
if: ${{ steps.plan.outputs.exitcode != '0' }}
run: exit 1
Loading
Loading