Skip to content

Add LICENSE file

Add LICENSE file #13

name: Delivery GitOps Pipeline
on:
push:
branches: [ main, master, develop ]
pull_request:
branches: [ main, master, develop ]
workflow_dispatch:
permissions:
contents: read
pull-requests: write
env:
TERRAFORM_VERSION: 1.6.6
jobs:
preflight:
name: Preflight Checks
runs-on: ubuntu-latest
outputs:
ok: ${{ steps.check.outputs.ok }}
steps:
- name: Verify required secrets/vars
id: check
run: |
missing=()
[ -z "$AWS_ROLE_TO_ASSUME" ] && missing+=("AWS_ROLE_TO_ASSUME")
[ -z "$AWS_REGION" ] && missing+=("AWS_REGION")
# TF_BACKEND_CONFIG is optional; APP_URL_FALLBACK optional
if [ ${#missing[@]} -gt 0 ]; then
echo "Required secrets/vars missing: ${missing[*]}" >&2
echo "ok=false" >> $GITHUB_OUTPUT
exit 1
fi
echo "ok=true" >> $GITHUB_OUTPUT
env:
AWS_ROLE_TO_ASSUME: ${{ secrets.AWS_ROLE_TO_ASSUME }}
AWS_REGION: ${{ secrets.AWS_REGION }}
lint-test-build:
name: Lint, Test, Build
runs-on: ubuntu-latest
needs: preflight
outputs:
duration_seconds: ${{ steps.duration.outputs.seconds }}
steps:
- name: Mark start
run: echo "JOB_START=$(date +%s)" >> $GITHUB_ENV
- name: Checkout
uses: actions/checkout@v4
- name: Use Node 18
uses: actions/setup-node@v4
with:
node-version: 18
cache: npm
- name: Install dependencies
run: npm ci
- name: Lint
run: npm run lint
- name: Test
run: npm test -- --runInBand
- name: Build
run: npm run build
- name: Duration
id: duration
if: always()
run: |
end=$(date +%s)
echo "seconds=$((end - JOB_START))" >> $GITHUB_OUTPUT
docs:
name: Docs Build
runs-on: ubuntu-latest
needs: lint-test-build
outputs:
duration_seconds: ${{ steps.duration.outputs.seconds }}
steps:
- name: Mark start
run: echo "JOB_START=$(date +%s)" >> $GITHUB_ENV
- name: Checkout
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Install doc toolchain
run: |
python -m pip install --upgrade pip
python -m pip install -r docs/requirements.txt
- name: Build docs
run: mkdocs build --strict
- name: Duration
id: duration
if: always()
run: |
end=$(date +%s)
echo "seconds=$((end - JOB_START))" >> $GITHUB_OUTPUT
terraform-validate:
name: Terraform Validate
runs-on: ubuntu-latest
needs: lint-test-build
outputs:
duration_seconds: ${{ steps.duration.outputs.seconds }}
steps:
- name: Mark start
run: echo "JOB_START=$(date +%s)" >> $GITHUB_ENV
- name: Checkout
uses: actions/checkout@v4
- name: Setup Terraform
uses: hashicorp/setup-terraform@v3
with:
terraform_version: ${{ env.TERRAFORM_VERSION }}
- name: Terraform fmt (check)
run: terraform fmt -check -recursive
working-directory: terraform
- name: Terraform init (no backend)
run: terraform init -backend=false
working-directory: terraform
- name: Terraform validate
run: terraform validate -no-color
working-directory: terraform
- name: Duration
id: duration
if: always()
run: |
end=$(date +%s)
echo "seconds=$((end - JOB_START))" >> $GITHUB_OUTPUT
deploy:
name: Auto Deploy (staging)
runs-on: ubuntu-latest
needs: [lint-test-build, docs, terraform-validate]
if: needs.lint-test-build.result == 'success' && needs.docs.result == 'success' && needs.terraform-validate.result == 'success' && github.ref == 'refs/heads/main' && github.event_name == 'push'
environment:
name: staging
url: ${{ steps.outputs_step.outputs.app_url }}
outputs:
duration_seconds: ${{ steps.duration.outputs.seconds }}
app_url: ${{ steps.outputs_step.outputs.app_url }}
env:
AWS_REGION: ${{ secrets.AWS_REGION }}
AWS_ROLE_TO_ASSUME: ${{ secrets.AWS_ROLE_TO_ASSUME }}
TF_BACKEND_CONFIG: ${{ secrets.TF_BACKEND_CONFIG }}
APP_URL_FALLBACK: ${{ secrets.APP_URL_FALLBACK }}
TERRAFORM_VERSION: 1.6.6
steps:
- name: Mark start
run: echo "JOB_START=$(date +%s)" >> $GITHUB_ENV
- name: Checkout
uses: actions/checkout@v4
- name: Configure AWS credentials (OIDC)
if: env.AWS_ROLE_TO_ASSUME != ''
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: ${{ env.AWS_ROLE_TO_ASSUME }}
aws-region: ${{ env.AWS_REGION }}
- name: Setup Terraform
uses: hashicorp/setup-terraform@v3
with:
terraform_version: ${{ env.TERRAFORM_VERSION }}
- name: Write backend config (optional)
if: env.TF_BACKEND_CONFIG != ''
run: |
cat <<'EOF' > terraform/backend.hcl
${{ env.TF_BACKEND_CONFIG }}
EOF
- name: Terraform init (staging backend)
run: |
if [ -f backend.hcl ]; then
terraform init -backend-config=backend.hcl
else
terraform init
fi
working-directory: terraform
- name: Select workspace (staging)
run: terraform workspace select staging || terraform workspace new staging
working-directory: terraform
- name: Terraform apply (staging)
run: terraform apply -auto-approve -var="deployment_environment=staging"
working-directory: terraform
- name: Capture outputs
id: outputs_step
run: |
APP_URL=$(terraform output -raw app_url 2>/dev/null || true)
if [ -z "$APP_URL" ] && [ -n "$APP_URL_FALLBACK" ]; then
APP_URL="$APP_URL_FALLBACK"
fi
if [ -z "$APP_URL" ]; then
APP_URL="unavailable"
fi
echo "app_url=$APP_URL" >> $GITHUB_OUTPUT
working-directory: terraform
- name: Duration
id: duration
if: always()
run: |
end=$(date +%s)
echo "seconds=$((end - JOB_START))" >> $GITHUB_OUTPUT
deploy-preview:
name: Deploy Preview (PR)
runs-on: ubuntu-latest
needs: [lint-test-build, docs, terraform-validate]
if: github.event_name == 'pull_request'
environment:
name: preview-${{ github.event.pull_request.number }}
url: ${{ steps.outputs_step.outputs.app_url }}
outputs:
duration_seconds: ${{ steps.duration.outputs.seconds }}
app_url: ${{ steps.outputs_step.outputs.app_url }}
env:
AWS_REGION: ${{ secrets.AWS_REGION }}
AWS_ROLE_TO_ASSUME: ${{ secrets.AWS_ROLE_TO_ASSUME }}
TF_BACKEND_CONFIG: ${{ secrets.TF_BACKEND_CONFIG }}
APP_URL_FALLBACK: ${{ secrets.APP_URL_FALLBACK }}
TERRAFORM_VERSION: 1.6.6
steps:
- name: Mark start
run: echo "JOB_START=$(date +%s)" >> $GITHUB_ENV
- name: Checkout
uses: actions/checkout@v4
- name: Configure AWS credentials (OIDC)
if: env.AWS_ROLE_TO_ASSUME != ''
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: ${{ env.AWS_ROLE_TO_ASSUME }}
aws-region: ${{ env.AWS_REGION }}
- name: Setup Terraform
uses: hashicorp/setup-terraform@v3
with:
terraform_version: ${{ env.TERRAFORM_VERSION }}
- name: Write backend config (optional)
if: env.TF_BACKEND_CONFIG != ''
run: |
cat <<'EOF' > terraform/backend.hcl
${{ env.TF_BACKEND_CONFIG }}
EOF
- name: Terraform init (preview backend)
run: |
if [ -f backend.hcl ]; then
terraform init -backend-config=backend.hcl -reconfigure
else
terraform init -reconfigure
fi
working-directory: terraform
- name: Select workspace (preview)
run: |
WS="pr-${{ github.event.pull_request.number }}"
terraform workspace select "$WS" || terraform workspace new "$WS"
working-directory: terraform
- name: Terraform apply (preview)
run: terraform apply -auto-approve -var="deployment_environment=preview"
working-directory: terraform
- name: Capture outputs
id: outputs_step
run: |
APP_URL=$(terraform output -raw app_url 2>/dev/null || true)
if [ -z "$APP_URL" ] && [ -n "$APP_URL_FALLBACK" ]; then
APP_URL="$APP_URL_FALLBACK"
fi
if [ -z "$APP_URL" ]; then
APP_URL="unavailable"
fi
echo "app_url=$APP_URL" >> $GITHUB_OUTPUT
working-directory: terraform
- name: Duration
id: duration
if: always()
run: |
end=$(date +%s)
echo "seconds=$((end - JOB_START))" >> $GITHUB_OUTPUT
deploy-prod:
name: Promote to Prod
runs-on: ubuntu-latest
needs: [deploy, prod-gate]
if: needs.deploy.result == 'success' && needs.prod-gate.result == 'success' && github.ref == 'refs/heads/main' && github.event_name == 'push'
environment:
name: production
url: ${{ steps.outputs_step.outputs.app_url }}
outputs:
duration_seconds: ${{ steps.duration.outputs.seconds }}
app_url: ${{ steps.outputs_step.outputs.app_url }}
env:
AWS_REGION: ${{ secrets.AWS_REGION }}
AWS_ROLE_TO_ASSUME: ${{ secrets.AWS_ROLE_TO_ASSUME }}
TF_BACKEND_CONFIG: ${{ secrets.TF_BACKEND_CONFIG }}
APP_URL_FALLBACK: ${{ secrets.APP_URL_FALLBACK }}
TERRAFORM_VERSION: 1.6.6
steps:
- name: Mark start
run: echo "JOB_START=$(date +%s)" >> $GITHUB_ENV
- name: Checkout
uses: actions/checkout@v4
- name: Configure AWS credentials (OIDC)
if: env.AWS_ROLE_TO_ASSUME != ''
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: ${{ env.AWS_ROLE_TO_ASSUME }}
aws-region: ${{ env.AWS_REGION }}
- name: Setup Terraform
uses: hashicorp/setup-terraform@v3
with:
terraform_version: ${{ env.TERRAFORM_VERSION }}
- name: Write backend config (optional)
if: env.TF_BACKEND_CONFIG != ''
run: |
cat <<'EOF' > terraform/backend.hcl
${{ env.TF_BACKEND_CONFIG }}
EOF
- name: Terraform init (prod backend)
run: |
if [ -f backend.hcl ]; then
terraform init -backend-config=backend.hcl -reconfigure
else
terraform init -reconfigure
fi
working-directory: terraform
- name: Select workspace (prod)
run: terraform workspace select prod || terraform workspace new prod
working-directory: terraform
- name: Terraform apply (prod)
run: terraform apply -auto-approve -var="deployment_environment=prod"
working-directory: terraform
- name: Capture outputs
id: outputs_step
run: |
APP_URL=$(terraform output -raw app_url 2>/dev/null || true)
if [ -z "$APP_URL" ] && [ -n "$APP_URL_FALLBACK" ]; then
APP_URL="$APP_URL_FALLBACK"
fi
if [ -z "$APP_URL" ]; then
APP_URL="unavailable"
fi
echo "app_url=$APP_URL" >> $GITHUB_OUTPUT
working-directory: terraform
- name: Duration
id: duration
if: always()
run: |
end=$(date +%s)
echo "seconds=$((end - JOB_START))" >> $GITHUB_OUTPUT
prod-gate:
name: Production Gate
runs-on: ubuntu-latest
needs: deploy
if: needs.deploy.result == 'success' && github.ref == 'refs/heads/main' && github.event_name == 'push'
outputs:
approved: ${{ steps.gate.outputs.approved }}
steps:
- name: Require prod approval toggle
id: gate
run: |
if [ "${{ vars.PROD_PROMOTE_ENABLED }}" = "true" ]; then
echo "approved=true" >> $GITHUB_OUTPUT
else
echo "Production promotion is disabled. Set repository variable PROD_PROMOTE_ENABLED=true to allow." >&2
echo "approved=false" >> $GITHUB_OUTPUT
exit 1
fi
summary:
name: Delivery Summary
runs-on: ubuntu-latest
needs: [preflight, lint-test-build, docs, terraform-validate, deploy, deploy-preview, prod-gate, deploy-prod]
if: always()
steps:
- name: Write summary
run: |
echo "# Delivery Pipeline Summary" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "## Status" >> $GITHUB_STEP_SUMMARY
echo "- Preflight: ${{ needs.preflight.result }}" >> $GITHUB_STEP_SUMMARY
echo "- Lint/Test/Build: ${{ needs.lint-test-build.result }}" >> $GITHUB_STEP_SUMMARY
echo "- Docs: ${{ needs.docs.result }}" >> $GITHUB_STEP_SUMMARY
echo "- Terraform Validate: ${{ needs.terraform-validate.result }}" >> $GITHUB_STEP_SUMMARY
echo "- Deploy (staging): ${{ needs.deploy.result }}" >> $GITHUB_STEP_SUMMARY
echo "- Deploy Preview (PR): ${{ needs.deploy-preview.result }}" >> $GITHUB_STEP_SUMMARY
echo "- Prod Gate: ${{ needs.prod-gate.result }}" >> $GITHUB_STEP_SUMMARY
echo "- Deploy Prod: ${{ needs.deploy-prod.result }}" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "## Durations (s)" >> $GITHUB_STEP_SUMMARY
echo "- Lint/Test/Build: ${{ needs.lint-test-build.outputs.duration_seconds }}" >> $GITHUB_STEP_SUMMARY
echo "- Docs: ${{ needs.docs.outputs.duration_seconds }}" >> $GITHUB_STEP_SUMMARY
echo "- Terraform Validate: ${{ needs.terraform-validate.outputs.duration_seconds }}" >> $GITHUB_STEP_SUMMARY
echo "- Deploy (staging): ${{ needs.deploy.outputs.duration_seconds }}" >> $GITHUB_STEP_SUMMARY
echo "- Deploy Preview: ${{ needs.deploy-preview.outputs.duration_seconds }}" >> $GITHUB_STEP_SUMMARY
echo "- Prod Gate: n/a" >> $GITHUB_STEP_SUMMARY
echo "- Deploy Prod: ${{ needs.deploy-prod.outputs.duration_seconds }}" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "## Deployment" >> $GITHUB_STEP_SUMMARY
echo "- Staging App URL: ${{ needs.deploy.outputs.app_url || 'n/a' }}" >> $GITHUB_STEP_SUMMARY
echo "- Preview App URL: ${{ needs.deploy-preview.outputs.app_url || 'n/a' }}" >> $GITHUB_STEP_SUMMARY
echo "- Prod App URL: ${{ needs.deploy-prod.outputs.app_url || 'n/a' }}" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "## What it covers" >> $GITHUB_STEP_SUMMARY
echo "- Code quality (eslint)" >> $GITHUB_STEP_SUMMARY
echo "- Unit tests (jest)" >> $GITHUB_STEP_SUMMARY
echo "- Build artifacts (tsc)" >> $GITHUB_STEP_SUMMARY
echo "- Docs as code (MkDocs + mike)" >> $GITHUB_STEP_SUMMARY
echo "- IaC sanity (terraform fmt/validate)" >> $GITHUB_STEP_SUMMARY
echo "- Auto-deploy on main (terraform apply to staging)" >> $GITHUB_STEP_SUMMARY
echo "- Preview environments for PRs" >> $GITHUB_STEP_SUMMARY
echo "- Production promotion after staging (guarded by PROD_PROMOTE_ENABLED)" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "Pipeline aligns with GitOps: everything (code, docs, IaC) validated per change, ready for promotion." >> $GITHUB_STEP_SUMMARY
post-report:
name: Post Metrics to PR
runs-on: ubuntu-latest
needs: summary
if: github.event_name == 'pull_request'
permissions:
pull-requests: write
steps:
- name: Comment with metrics
uses: actions/github-script@v7
env:
LINT_RESULT: ${{ needs.lint-test-build.result }}
LINT_DURATION: ${{ needs.lint-test-build.outputs.duration_seconds }}
DOCS_RESULT: ${{ needs.docs.result }}
DOCS_DURATION: ${{ needs.docs.outputs.duration_seconds }}
TF_RESULT: ${{ needs.terraform-validate.result }}
TF_DURATION: ${{ needs.terraform-validate.outputs.duration_seconds }}
DEPLOY_RESULT: ${{ needs.deploy-preview.result }}
DEPLOY_DURATION: ${{ needs.deploy-preview.outputs.duration_seconds }}
APP_URL: ${{ needs.deploy-preview.outputs.app_url }}
with:
script: |
const {context, github} = require('@actions/github');
const body = [
'### Delivery Pipeline Report',
`- Lint/Test/Build: **${process.env.LINT_RESULT}** (${process.env.LINT_DURATION || 'n/a'}s)`,
`- Docs: **${process.env.DOCS_RESULT}** (${process.env.DOCS_DURATION || 'n/a'}s)`,
`- Terraform Validate: **${process.env.TF_RESULT}** (${process.env.TF_DURATION || 'n/a'}s)`,
`- Deploy Preview: **${process.env.DEPLOY_RESULT}** (${process.env.DEPLOY_DURATION || 'n/a'}s)`,
`- Preview App URL: ${process.env.APP_URL || 'n/a'}`,
].join('\n');
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.payload.pull_request.number,
body
});