Copyable Blueprints¶
These are starting files. Copy the block, replace the placeholders, then run the checks from Required Checks.
For complete file sets, use Real Workflow Files. This page stays short on purpose.
Baseline PR Gate¶
Create .github/workflows/pre-commit.yml:
---
# yamllint disable rule:comments
# yamllint disable rule:truthy
name: Pre-commit Code Quality Checks
on:
pull_request:
types:
- opened
- synchronize
- reopened
branches:
- main
push:
branches:
- main
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
jobs:
run-pre-commit:
name: Run pre-commit
runs-on: <runner-labels>
steps:
- name: Checkout repository
uses: actions/checkout@<checkout-ref>
with:
persist-credentials: false
- name: Run pre-commit
shell: bash
run: pre-commit run --show-diff-on-failure --color=always --all-files
Replace:
<runner-labels>withubuntu-latest,k8s, or your self-hosted label list.<checkout-ref>with the pinned checkout SHA used by your org.
IaC Promotion Workflow¶
Create .github/workflows/promotion.yml:
---
# yamllint disable rule:comments
# yamllint disable rule:truthy
name: Terraform Environment Promotion
on:
push:
branches:
- main
paths:
- <release-version-file>
- terraform/**
workflow_dispatch:
permissions:
contents: read
concurrency:
group: <repo>_terraform
cancel-in-progress: false
jobs:
terraform-deployment:
name: Terragrunt Deployment ${{ matrix.env }}
permissions:
contents: read
pull-requests: write
strategy:
fail-fast: false
matrix:
include:
- env: <environment-name-dev>
aws-region: <aws-region>
aws-account-id: '<dev-aws-account-id>'
aws-profile: <dev-aws-profile>
tf-target-env: dev
- env: <environment-name-prod>
aws-region: <aws-region>
aws-account-id: '<prod-aws-account-id>'
aws-profile: <prod-aws-profile>
tf-target-env: prod
uses: ./.github/workflows/rw-terragrunt.yml
with:
aws-region: ${{ matrix.aws-region }}
aws-account-id: ${{ matrix.aws-account-id }}
aws-profile: ${{ matrix.aws-profile }}
role-name: <role-name>
tf-target-env: ${{ matrix.tf-target-env }}
env: ${{ matrix.env }}
run-apply: 'true'
Create .github/workflows/rw-terragrunt.yml:
---
# yamllint disable rule:comments
# yamllint disable rule:truthy
name: Reusable Terragrunt Workflow
on:
workflow_call:
inputs:
aws-region:
type: string
required: true
aws-account-id:
type: string
required: true
aws-profile:
type: string
required: true
role-name:
type: string
required: true
tf-target-env:
type: string
required: true
run-apply:
type: string
required: false
default: 'false'
env:
type: string
required: true
permissions: {}
jobs:
discover:
name: Discover Terragrunt units
runs-on: <runner-labels>
permissions:
contents: read
environment: ${{ inputs.run-apply == 'true' && inputs.env || '' }}
outputs:
units: ${{ steps.set-matrix.outputs.units }}
steps:
- name: Checkout repository
uses: actions/checkout@<checkout-ref>
with:
persist-credentials: false
- id: set-matrix
shell: bash
env:
TF_TARGET_ENV: ${{ inputs.tf-target-env }}
run: |
set -euo pipefail
tf_path="terraform/environments/${TF_TARGET_ENV}"
units_json=$(
find "$tf_path" -name terragrunt.hcl -not -path '*/.terragrunt-cache/*' |
sed 's#/terragrunt.hcl$##' |
jq -R -s -c 'split("\n") | map(select(length > 0)) | map({path: .})'
)
echo "$units_json" | jq .
echo "units=$units_json" >> "$GITHUB_OUTPUT"
terragrunt:
name: Terragrunt - ${{ matrix.unit.path }}
needs:
- discover
runs-on: <runner-labels>
permissions:
contents: read
pull-requests: write
environment: ${{ inputs.run-apply == 'true' && inputs.env || '' }}
strategy:
fail-fast: false
matrix:
unit: ${{ fromJSON(needs.discover.outputs.units) }}
steps:
- name: Checkout repository
uses: actions/checkout@<checkout-ref>
with:
persist-credentials: false
- name: Run Terragrunt
uses: ./.github/actions/terragrunt-deployment-action
with:
aws-region: ${{ inputs.aws-region }}
aws-account-id: ${{ inputs.aws-account-id }}
aws-profile: ${{ inputs.aws-profile }}
role-name: ${{ inputs.role-name }}
tf-target-env: ${{ inputs.tf-target-env }}
tf-path: ${{ matrix.unit.path }}
run-apply: ${{ inputs.run-apply }}
You still need a local .github/actions/terragrunt-deployment-action/action.yml
that handles credentials, terragrunt plan, terragrunt apply, comments, and
lock behavior for your environment.
Weekly Forge Example Test¶
Create .github/workflows/rw-example-folder.yml:
---
name: Reusable Forge Example Folder
on:
workflow_call:
inputs:
example-folder:
type: string
required: true
operation:
type: string
required: true
terragrunt-command:
type: string
required: true
ref:
type: string
required: false
default: main
permissions:
contents: read
jobs:
discover:
name: Discover ${{ inputs.example-folder }} modules
runs-on: <runner-labels>
outputs:
modules: ${{ steps.discover.outputs.terragrunt-modules }}
steps:
- uses: actions/checkout@<checkout-ref>
with:
persist-credentials: false
- name: Discover Terragrunt modules
id: discover
uses: ./.github/actions/terragrunt-deployment-action
with:
example-folder: ${{ inputs.example-folder }}
run-type: discover
terragrunt-command: ${{ inputs.terragrunt-command }}
create-lock: 'false'
ref: ${{ inputs.ref }}
run:
name: ${{ inputs.operation }} ${{ inputs.example-folder }} - ${{ matrix.module.path }}
needs:
- discover
runs-on: <runner-labels>
strategy:
fail-fast: true
max-parallel: 1
matrix:
module: ${{ fromJSON(needs.discover.outputs.modules) }}
steps:
- uses: actions/checkout@<checkout-ref>
with:
persist-credentials: false
- name: Run Terragrunt
uses: ./.github/actions/terragrunt-deployment-action
with:
example-folder: ${{ inputs.example-folder }}
run-type: ${{ inputs.operation }}
terragrunt-unit-path: ${{ matrix.module.path }}
ref: ${{ inputs.ref }}
Create .github/workflows/test-examples.yml:
---
# yamllint disable rule:truthy
name: Test examples deployment
on:
workflow_dispatch:
schedule:
- cron: 0 4 * * 1
permissions:
contents: read
concurrency:
group: <repo>
cancel-in-progress: false
jobs:
apply-helpers:
uses: ./.github/workflows/rw-example-folder.yml
with:
example-folder: helpers
operation: apply
terragrunt-command: apply
ref: ${{ github.head_ref || github.ref_name || 'main' }}
apply-infra:
needs: apply-helpers
uses: ./.github/workflows/rw-example-folder.yml
with:
example-folder: infra
operation: apply
terragrunt-command: apply
ref: ${{ github.head_ref || github.ref_name || 'main' }}
apply-platform:
needs: apply-infra
uses: ./.github/workflows/rw-example-folder.yml
with:
example-folder: platform
operation: apply
terragrunt-command: apply
ref: ${{ github.head_ref || github.ref_name || 'main' }}
apply-integrations:
needs: apply-platform
uses: ./.github/workflows/rw-example-folder.yml
with:
example-folder: integrations
operation: apply
terragrunt-command: apply
ref: ${{ github.head_ref || github.ref_name || 'main' }}
destroy-integrations:
needs: apply-integrations
uses: ./.github/workflows/rw-example-folder.yml
with:
example-folder: integrations
operation: destroy
terragrunt-command: destroy
ref: ${{ github.head_ref || github.ref_name || 'main' }}
destroy-platform:
needs: destroy-integrations
uses: ./.github/workflows/rw-example-folder.yml
with:
example-folder: platform
operation: destroy
terragrunt-command: destroy
ref: ${{ github.head_ref || github.ref_name || 'main' }}
destroy-infra:
needs: destroy-platform
uses: ./.github/workflows/rw-example-folder.yml
with:
example-folder: infra
operation: destroy
terragrunt-command: destroy
ref: ${{ github.head_ref || github.ref_name || 'main' }}
destroy-helpers:
needs: destroy-infra
uses: ./.github/workflows/rw-example-folder.yml
with:
example-folder: helpers
operation: destroy
terragrunt-command: destroy
ref: ${{ github.head_ref || github.ref_name || 'main' }}
This is the core Forge adoption test. A company can remove or empty
integrations if it does not use optional integrations.
Scheduled Cloud Custodian Job¶
Create .github/workflows/cloud-custodian.yml:
---
# yamllint disable rule:comments
# yamllint disable rule:truthy
name: Cloud Custodian
on:
workflow_dispatch:
schedule:
- cron: 0 3 * * *
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: false
jobs:
run-custodian:
name: Run Cloud Custodian
runs-on: <runner-labels>
steps:
- uses: actions/checkout@<checkout-ref>
with:
persist-credentials: false
- name: Acquire lock
uses: <org>/<reusable-actions-repo>/.github/actions/global-lock-action@<reusable-actions-ref>
with:
lock-action: acquire
lock-id: <repo>-cloud-custodian
- name: Configure AWS credentials
uses: <org>/<reusable-actions-repo>/.github/actions/aws-action@<reusable-actions-ref>
with:
role-skip-session-tagging: true
- name: Run Cloud Custodian
shell: bash
run: custodian run -s output/ --region <aws-region> policies/*.yml
- name: Release lock
if: always()
uses: <org>/<reusable-actions-repo>/.github/actions/global-lock-action@<reusable-actions-ref>
with:
lock-action: release
lock-id: <repo>-cloud-custodian
Scheduled Script Job¶
Create .github/workflows/<job-name>.yml:
---
name: <job-name>
on:
workflow_dispatch:
schedule:
- cron: '*/15 * * * *'
permissions:
contents: read
pull-requests: write
concurrency:
group: <repo>-<job-name>
cancel-in-progress: false
jobs:
run:
name: <job-name>
runs-on: <runner-labels>
steps:
- uses: actions/checkout@<checkout-ref>
with:
persist-credentials: false
- name: Get automation token
id: app-token
uses: actions/create-github-app-token@<create-github-app-token-ref>
with:
app-id: ${{ secrets.<APP_ID_SECRET> }}
private-key: ${{ secrets.<APP_PRIVATE_KEY_SECRET> }}
owner: <org>
permission-issues: write
permission-pull-requests: write
- name: Run job
shell: bash
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
run: ./scripts/<job-name>/<job-name>.sh
Renovate Job¶
Create .github/workflows/renovate.yml:
---
name: RenovateBot
on:
schedule:
- cron: 0 */4 * * *
workflow_dispatch:
permissions: {}
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: false
jobs:
renovate:
name: Run Renovate
runs-on: <runner-labels>
permissions:
contents: read
actions: write
steps:
- uses: actions/checkout@<checkout-ref>
with:
persist-credentials: false
- name: Configure credentials
uses: <org>/<reusable-actions-repo>/.github/actions/aws-action@<reusable-actions-ref>
- name: Prepare Renovate log directory
shell: bash
run: install -d -m 755 "${RUNNER_TEMP}/renovate-logs"
- name: Self-hosted Renovate
uses: renovatebot/github-action@<renovate-action-ref>
with:
configurationFile: default.json
token: ${{ secrets.<RENOVATE_TOKEN_SECRET> }}
env:
LOG_LEVEL: warn
LOG_FILE: /tmp/renovate-logs/renovate.log
LOG_FILE_FORMAT: json
LOG_FILE_LEVEL: debug
RENOVATE_ONBOARDING: 'false'
- name: Upload Renovate logs
if: always()
uses: actions/upload-artifact@<upload-artifact-ref>
with:
name: renovate-logs
path: ${{ runner.temp }}/renovate-logs/
if-no-files-found: warn
retention-days: 14
Add private registry, AWS AMI lookup, and enterprise GitHub authentication only when the target repos need them.