Add LICENSE file #13
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 | |
| }); |