diff --git a/.github/actions/terragrunt-configure-mise/README.md b/.github/actions/terragrunt-configure-mise/README.md index c451af8..6340d58 100644 --- a/.github/actions/terragrunt-configure-mise/README.md +++ b/.github/actions/terragrunt-configure-mise/README.md @@ -29,7 +29,7 @@ jobs: # Ensure mise.toml contains terraform and terragrunt at our desired versions - name: Configure Mise - uses: launchbynttdata/launch-workflows/.github/actions/terragrunt-configure-mise@0.14.0 # or later + uses: launchbynttdata/launch-workflows/.github/actions/terragrunt-configure-misefeat/unify-provider-auth with: tf_version: '1.5.5' tg_version: '0.54.11' diff --git a/.github/actions/terragrunt-configure-mise/action.yml b/.github/actions/terragrunt-configure-mise/action.yml index e8dc4a9..2c257ad 100644 --- a/.github/actions/terragrunt-configure-mise/action.yml +++ b/.github/actions/terragrunt-configure-mise/action.yml @@ -1,5 +1,6 @@ name: "Configure Mise for Terragrunt" -description: "Configure Mise to install specific versions of Terraform and Terragrunt for use with terragrunt-action" +description: "Configure Mise to install specific versions of Terraform and + Terragrunt for use with terragrunt-action" inputs: tf_version: description: "Version of Terraform to install" @@ -34,8 +35,8 @@ runs: # https://github.com/mrijken/toml-cli # `toml set` will add if necessary or update if the entry already exists - toml set mise.toml tools.terragrunt "${{ inputs.tg_version }}" - toml set mise.toml tools.terraform "${{ inputs.tf_version }}" + toml set --toml-path mise.toml tools.terragrunt "${{ inputs.tg_version }}" + toml set --toml-path mise.toml tools.terraform "${{ inputs.tf_version }}" fi echo "Final mise.toml configuration:" cat mise.toml diff --git a/.github/actions/update-status-check/README.md b/.github/actions/update-status-check/README.md new file mode 100644 index 0000000..da986f9 --- /dev/null +++ b/.github/actions/update-status-check/README.md @@ -0,0 +1,111 @@ +# Update Status Check Action + +GitHub's [Commit Status API](https://docs.github.com/en/rest/commits/statuses) allows workflows to report the state of a check back to a specific commit SHA. This is useful for surfacing the outcome of external processes, gating merges, or providing richer context in pull requests beyond what a workflow run alone provides. + +This action wraps the Commit Status API with a simple interface: provide a check name and a status, and we handle the API call. + +## Behavior + +This action will: +1. Validate that the provided `status` is one of the accepted values (`error`, `failure`, `pending`, `success`) +2. Call the GitHub Commit Status API to create or update the named status check on the specified commit SHA +3. Optionally attach a human-readable description and a target URL to the status + +## Inputs + +| Input | Description | Required | Default | +|-------|-------------|----------|---------| +| `check_name` | The name (context) of the status check to create or update | Yes | — | +| `status` | The state to set. One of: `error`, `failure`, `pending`, `success` | Yes | — | +| `sha` | The commit SHA to attach the status check to | No | `${{ github.sha }}` | +| `description` | A short human-readable description of the status | No | `""` | +| `target_url` | A URL to associate with the status (e.g. a link to a build log) | No | `""` | +| `github_token` | GitHub token for API access | No | `${{ github.token }}` | + +## Usage + +### Basic Usage + +Mark a status check as successful on the current commit (replacing `ref` with a tag or commit SHA from this repository): + +```yaml +jobs: + your-job: + runs-on: ubuntu-latest + permissions: + statuses: write + steps: + - name: Set status check to success + uses: launchbynttdata/launch-workflows/.github/actions/update-status-check@ref + with: + check_name: "my-check" + status: "success" +``` + +### With Description and Target URL + +Provide additional context visible in the GitHub UI: + +```yaml +- name: Set status check to failure + uses: launchbynttdata/launch-workflows/.github/actions/update-status-check@ref + with: + check_name: "security-scan" + status: "failure" + description: "Vulnerabilities were detected." + target_url: "https://example.com/scan-results/123" +``` + +### Marking a Check as Pending Before a Long-Running Step + +Use `pending` to signal that a check is in progress, then update it on completion: + +```yaml +- name: Mark check as pending + uses: launchbynttdata/launch-workflows/.github/actions/update-status-check@ref + with: + check_name: "integration-tests" + status: "pending" + description: "Integration tests are running..." + +- name: Run integration tests + run: make test-integration + +- name: Mark check as success + if: success() + uses: launchbynttdata/launch-workflows/.github/actions/update-status-check@ref + with: + check_name: "integration-tests" + status: "success" + description: "All integration tests passed." + +- name: Mark check as failure + if: failure() + uses: launchbynttdata/launch-workflows/.github/actions/update-status-check@ref + with: + check_name: "integration-tests" + status: "failure" + description: "One or more integration tests failed." +``` + +### Targeting a Specific Commit SHA + +Override the default SHA to set a status on a commit other than the one that triggered the workflow: + +```yaml +- name: Set status on a specific commit + uses: launchbynttdata/launch-workflows/.github/actions/update-status-check@ref + with: + check_name: "my-check" + status: "success" + sha: "abc1234def5678" +``` + +## Required Permissions + +This action requires the following permission on the workflow job: + +```yaml +permissions: + statuses: write +``` diff --git a/.github/actions/update-status-check/action.yml b/.github/actions/update-status-check/action.yml new file mode 100644 index 0000000..f68d48d --- /dev/null +++ b/.github/actions/update-status-check/action.yml @@ -0,0 +1,81 @@ +name: "Update Status Check" +description: "Use the GitHub Commit Status API to create or update a status + check on a commit." +inputs: + check_name: + description: "The name (context) of the status check to create or update." + required: true + status: + description: "The state to set on the status check. Must be one of: error, + failure, pending, success." + required: true + sha: + description: "The commit SHA to attach the status check to. Defaults to the PR + head SHA if available, otherwise the SHA that triggered the workflow." + required: false + default: ${{ github.event.pull_request.head.sha || github.sha }} + description: + description: "A short human-readable description of the status check." + required: false + default: "" + target_url: + description: "A URL to associate with the status check, typically a link to + relevant build or run output." + required: false + default: "" + github_token: + description: "GitHub token for API access. Defaults to the automatic GITHUB_TOKEN." + required: false + default: ${{ github.token }} +runs: + using: "composite" + steps: + - name: Update Status Check + shell: bash + env: + GITHUB_TOKEN: ${{ inputs.github_token }} + CHECK_NAME: ${{ inputs.check_name }} + STATUS: ${{ inputs.status }} + SHA: ${{ inputs.sha }} + DESCRIPTION: ${{ inputs.description }} + TARGET_URL: ${{ inputs.target_url }} + PR_NUMBER: ${{ github.event.pull_request.number }} + run: | + # For pull request events, resolve the actual head SHA via the API. + # github.sha on PR events points to a temporary merge commit that + # doesn't appear on the PR, so status checks set on it are invisible. + if [[ -n "$PR_NUMBER" ]]; then + PR_HEAD_SHA=$(gh api "repos/${{ github.repository }}/pulls/${PR_NUMBER}" --jq '.head.sha' 2>/dev/null || true) + if [[ -n "$PR_HEAD_SHA" ]]; then + echo "Resolved PR #${PR_NUMBER} head SHA: ${PR_HEAD_SHA} (input was: ${SHA})" + SHA="$PR_HEAD_SHA" + fi + fi + + VALID_STATES=("error" "failure" "pending" "success") + VALID=false + for state in "${VALID_STATES[@]}"; do + if [[ "$STATUS" == "$state" ]]; then + VALID=true + break + fi + done + + if [[ "$VALID" != "true" ]]; then + echo "Error: Invalid status '${STATUS}'. Must be one of: error, failure, pending, success." + exit 1 + fi + + echo "Setting status check '${CHECK_NAME}' to '${STATUS}' on commit ${SHA}..." + + PAYLOAD=$(jq -n \ + --arg state "$STATUS" \ + --arg context "$CHECK_NAME" \ + --arg description "$DESCRIPTION" \ + --arg target_url "$TARGET_URL" \ + '{state: $state, context: $context, description: $description, target_url: $target_url}') + + gh api -X POST "repos/${{ github.repository }}/statuses/${SHA}" \ + --input - <<< "$PAYLOAD" + + echo "\nStatus check '${CHECK_NAME}' successfully set to '${STATUS}'." diff --git a/.github/configs/release-drafter-conventional-commits.yml b/.github/configs/release-drafter-conventional-commits.yml index ec7406d..98ba60f 100644 --- a/.github/configs/release-drafter-conventional-commits.yml +++ b/.github/configs/release-drafter-conventional-commits.yml @@ -49,10 +49,6 @@ categories: collapse-after: 3 labels: - "revert" - - title: "🚧 WIP" - collapse-after: 3 - labels: - - "WIP" change-template: "- $TITLE @$AUTHOR (#$NUMBER)" diff --git a/.github/workflows/reusable-pr-automated-approvals.yml b/.github/workflows/reusable-pr-automated-approvals.yml new file mode 100644 index 0000000..7dfbbca --- /dev/null +++ b/.github/workflows/reusable-pr-automated-approvals.yml @@ -0,0 +1,178 @@ +name: Automated Approvals + +on: + workflow_call: + inputs: + approver_alpha_app_id: + description: "The GitHub App ID for the first approver bot. Defaults to the + value of the LAUNCH_APPROVER_ALPHA_ID variable." + required: false + type: string + default: "${{ vars.LAUNCH_APPROVER_ALPHA_ID }}" + approver_bravo_app_id: + description: "The GitHub App ID for the second approver bot. Defaults to the + value of the LAUNCH_APPROVER_BRAVO_ID variable." + required: false + type: string + default: "${{ vars.LAUNCH_APPROVER_BRAVO_ID }}" + secrets: + LAUNCH_APPROVER_ALPHA_KEY: + required: false + description: "The private key for the first approver GitHub App." + LAUNCH_APPROVER_BRAVO_KEY: + required: false + description: "The private key for the second approver GitHub App." + +permissions: + pull-requests: read + contents: read + +jobs: + validate-inputs: + name: "Validate Inputs" + runs-on: ubuntu-latest + if: ${{ github.actor == 'dependabot[bot]' || github.actor == + 'launch-skeleton-auto-updater[bot]' }} + steps: + - name: Validate required secrets and variables + id: validate + shell: bash + env: + APPROVER_ALPHA_APP_ID: ${{ inputs.approver_alpha_app_id }} + APPROVER_BRAVO_APP_ID: ${{ inputs.approver_bravo_app_id }} + LAUNCH_APPROVER_ALPHA_KEY: ${{ secrets.LAUNCH_APPROVER_ALPHA_KEY }} + LAUNCH_APPROVER_BRAVO_KEY: ${{ secrets.LAUNCH_APPROVER_BRAVO_KEY }} + run: | + ERRORS=0 + + if [[ -z "$APPROVER_ALPHA_APP_ID" ]]; then + echo "Error: approver_alpha_app_id input (or LAUNCH_APPROVER_ALPHA_ID variable) is required." + ERRORS=$((ERRORS + 1)) + fi + + if [[ -z "$LAUNCH_APPROVER_ALPHA_KEY" ]]; then + echo "Error: LAUNCH_APPROVER_ALPHA_KEY secret is required." + ERRORS=$((ERRORS + 1)) + fi + + if [[ -z "$APPROVER_BRAVO_APP_ID" ]]; then + echo "Error: approver_bravo_app_id input (or LAUNCH_APPROVER_BRAVO_ID variable) is required." + ERRORS=$((ERRORS + 1)) + fi + + if [[ -z "$LAUNCH_APPROVER_BRAVO_KEY" ]]; then + echo "Error: LAUNCH_APPROVER_BRAVO_KEY secret is required." + ERRORS=$((ERRORS + 1)) + fi + + if [[ $ERRORS -gt 0 ]]; then + echo "Validation failed with $ERRORS error(s)." + exit 1 + fi + + echo "All required inputs and secrets are present." + + approvals: + name: Update Automated Approvals + runs-on: ubuntu-latest + needs: validate-inputs + steps: + - name: Check Pull Request + id: check-pull-request + run: | + ALLOWED_AUTHORS="dependabot[bot] launch-skeleton-auto-updater[bot]" + + # Get all commit authors on this PR + AUTHORS=$(gh api "/repos/${{ github.repository }}/pulls/${{ github.event.pull_request.number }}/commits" \ + --jq '.[].author.login' | sort -u) + + echo "Commit authors on this PR:" + echo "$AUTHORS" + + SHOULD_APPROVE=true + for author in $AUTHORS; do + if [[ ! " $ALLOWED_AUTHORS " =~ " $author " ]]; then + echo "Found non-automated author: $author" + SHOULD_APPROVE=false + break + fi + done + + if [ "$SHOULD_APPROVE" == "true" ] && [ "${{ github.actor }}" == "launch-skeleton-auto-updater[bot]" ]; then + PR_TITLE="${{ github.event.pull_request.title }}" + if [[ "$PR_TITLE" != chore* ]]; then + echo "Skeleton updater PR title does not start with 'chore': $PR_TITLE" + SHOULD_APPROVE=false + fi + fi + + echo "should_approve=$SHOULD_APPROVE" >> "$GITHUB_OUTPUT" + env: + GH_TOKEN: ${{ github.token }} + + - name: Get Approver Alpha Token + id: approver-alpha-token + uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 + with: + app-id: ${{ inputs.approver_alpha_app_id }} + private-key: ${{ secrets.LAUNCH_APPROVER_ALPHA_KEY }} + + - name: Get Approver Bravo Token + id: approver-bravo-token + uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 + with: + app-id: ${{ inputs.approver_bravo_app_id }} + private-key: ${{ secrets.LAUNCH_APPROVER_BRAVO_KEY }} + + - name: Approve PR + if: steps.check-pull-request.outputs.should_approve == 'true' + run: | + echo "All commits are from automated users. Approving PR..." + + GH_TOKEN="${{ steps.approver-alpha-token.outputs.token }}" \ + gh pr review "${{ github.event.pull_request.number }}" \ + --repo "${{ github.repository }}" \ + --approve \ + --body "Automated approval: all commits from trusted automation accounts." + + GH_TOKEN="${{ steps.approver-bravo-token.outputs.token }}" \ + gh pr review "${{ github.event.pull_request.number }}" \ + --repo "${{ github.repository }}" \ + --approve \ + --body "Automated approval: all commits from trusted automation accounts." + + - name: Revoke Approvals + if: steps.check-pull-request.outputs.should_approve == 'false' + run: | + echo "Non-automated commits detected. Revoking any existing automated approvals..." + + # Get approver app slugs + ALPHA_SLUG="${{ steps.approver-alpha-token.outputs.app-slug }}" + BRAVO_SLUG="${{ steps.approver-bravo-token.outputs.app-slug }}" + ALPHA_TOKEN="${{ steps.approver-alpha-token.outputs.token }}" + BRAVO_TOKEN="${{ steps.approver-bravo-token.outputs.token }}" + + # Get all reviews on this PR + REVIEWS=$(gh api "/repos/${{ github.repository }}/pulls/${{ github.event.pull_request.number }}/reviews") + + # Dismiss Alpha's approval if present + ALPHA_REVIEW_ID=$(echo "$REVIEWS" | jq -r ".[] | select(.user.login == \"${ALPHA_SLUG}[bot]\" and .state == \"APPROVED\") | .id" | head -1) + if [ -n "$ALPHA_REVIEW_ID" ] && [ "$ALPHA_REVIEW_ID" != "null" ]; then + echo "Dismissing review $ALPHA_REVIEW_ID from ${ALPHA_SLUG}[bot]" + GH_TOKEN="$ALPHA_TOKEN" gh api \ + --method PUT \ + "/repos/${{ github.repository }}/pulls/${{ github.event.pull_request.number }}/reviews/$ALPHA_REVIEW_ID/dismissals" \ + -f message="Automated approval revoked: non-automated commits detected on this PR." + fi + + # Dismiss Bravo's approval if present + BRAVO_REVIEW_ID=$(echo "$REVIEWS" | jq -r ".[] | select(.user.login == \"${BRAVO_SLUG}[bot]\" and .state == \"APPROVED\") | .id" | head -1) + if [ -n "$BRAVO_REVIEW_ID" ] && [ "$BRAVO_REVIEW_ID" != "null" ]; then + echo "Dismissing review $BRAVO_REVIEW_ID from ${BRAVO_SLUG}[bot]" + GH_TOKEN="$BRAVO_TOKEN" gh api \ + --method PUT \ + "/repos/${{ github.repository }}/pulls/${{ github.event.pull_request.number }}/reviews/$BRAVO_REVIEW_ID/dismissals" \ + -f message="Automated approval revoked: non-automated commits detected on this PR." + fi + env: + GH_TOKEN: ${{ github.token }} diff --git a/.github/workflows/reusable-pr-conventional-commit-title.yml b/.github/workflows/reusable-pr-conventional-commit-title.yml index d8a29c1..1b4091f 100644 --- a/.github/workflows/reusable-pr-conventional-commit-title.yml +++ b/.github/workflows/reusable-pr-conventional-commit-title.yml @@ -3,17 +3,24 @@ on: inputs: task_types: type: string - description: 'A JSON array of task types that are allowed. Default: ["feat","fix","docs","test","ci","refactor","perf","chore","revert"]' + description: "A JSON array of task types that are allowed. Default: + [\"feat\",\"fix\",\"docs\",\"test\",\"ci\",\"refactor\",\"perf\", + \"chore\",\"revert\"]" required: false - default: '["feat","fix","docs","test","ci","refactor","perf","chore","revert"]' + default: "[\"feat\",\"fix\",\"docs\",\"test\",\"ci\",\"refactor\",\"perf\", + \"chore\",\"revert\"]" scope_types: type: string - description: 'A JSON array of valid scope types. If omitted, any scope is allowed. If given, include the empty scope with "". Example: ["login","signup","checkout","payment", ""]' + description: "A JSON array of valid scope types. If omitted, any scope is + allowed. If given, include the empty scope with \"\". Example: + [\"login\",\"signup\",\"checkout\",\"payment\", \"\"]" required: false default: "" title_regex: type: string - description: 'Regular expression for custom title validation; disabled by default. Example: ''PROJECT-\d{2,5}$'' for trailing issue ticket, ''"^[^:]+: [A-Z]"'' for capitalized title' + description: "Regular expression for custom title validation; disabled by + default. Example: 'PROJECT-\\d{2,5}$' for trailing issue ticket, + '\"^[^:]+: [A-Z]\"' for capitalized title" required: false default: "" add_label: @@ -23,28 +30,42 @@ on: default: true add_scope_label: type: boolean - description: 'Whether to add labels to your pull request. Example: the ''login'' scope will be added for this PR title: "fix(login): fix message in login page". Default: true' + description: "Whether to add labels to your pull request. Example: the 'login' + scope will be added for this PR title: \"fix(login): fix message in + login page\". Default: true" required: false default: true custom_labels: type: string - description: 'A JSON object mapping task types to custom label names. Default: {"feat": "feature", "fix": "fix", "docs": "documentation", "test": "test", "ci": "CI/CD", "refactor": "refactor", "perf": "performance", "chore": "chore", "revert": "revert", "wip": "WIP"}' + description: "A JSON object mapping task types to custom label names. Default: + {\"feat\": \"feature\", \"fix\": \"fix\", \"docs\": \"documentation\", + \"test\": \"test\", \"ci\": \"CI/CD\", \"refactor\": \"refactor\", + \"perf\": \"performance\", \"chore\": \"chore\", \"revert\": + \"revert\"}" required: false - default: '{"feat": "feature", "fix": "fix", "docs": "documentation", "test": "test", "ci": "CI/CD", "refactor": "refactor", "perf": "performance", "chore": "chore", "revert": "revert", "wip": "WIP"}' + default: "{\"feat\": \"feature\", \"fix\": \"fix\", \"docs\": \"documentation\", + \"test\": \"test\", \"ci\": \"CI/CD\", \"refactor\": \"refactor\", + \"perf\": \"performance\", \"chore\": \"chore\", \"revert\": + \"revert\"}" comment_on_failure: type: boolean - description: "Whether to comment on the pull request if the title validation fails. Default: true" + description: "Whether to comment on the pull request if the title validation + fails. Default: true" required: false default: true clear_labels: type: boolean - description: "Whether to clear existing labels when the workflow is run. This helps clean up scope labels added by this action when the scope changes. Default: true" + description: "Whether to clear existing labels when the workflow is run. This + helps clean up scope labels added by this action when the scope + changes. Labels will only be cleared for ready_for_review or edited + actions. Default: true" required: false default: true permissions: contents: read pull-requests: write + statuses: write jobs: validate-title: @@ -52,7 +73,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Validate inputs - uses: actions/github-script@v8 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 with: script: | const taskTypes = `${{ inputs.task_types }}`; @@ -101,7 +122,8 @@ jobs: - name: Clear labels id: clear-labels - if: ${{ inputs.clear_labels }} + if: ${{ inputs.clear_labels && (github.event.action == 'ready_for_review' || + (github.event.action == 'edited' && github.event.changes.title)) }} env: GH_TOKEN: ${{ github.token }} run: | @@ -139,15 +161,16 @@ jobs: - name: Find previous failure comment id: find-comment if: ${{ always() && inputs.comment_on_failure }} - uses: peter-evans/find-comment@v4 + uses: peter-evans/find-comment@b30e6a3c0ed37e7c023ccd3f1db5c6c0b0c23aad with: issue-number: ${{ github.event.pull_request.number }} comment-author: github-actions[bot] body-includes: "Pull Request Title Validation Failed" - name: Comment on Failure - if: ${{ failure() && steps.pr-validation.outcome == 'failure' && inputs.comment_on_failure }} - uses: peter-evans/create-or-update-comment@v5 + if: ${{ failure() && steps.pr-validation.outcome == 'failure' && + inputs.comment_on_failure }} + uses: peter-evans/create-or-update-comment@e8674b075228eee787fea43ef493e45ece1004c9 with: issue-number: ${{ github.event.pull_request.number }} comment-id: ${{ steps.find-comment.outputs.comment-id }} @@ -155,10 +178,35 @@ jobs: edit-mode: replace - name: Cleanup Failure Comment on Success - if: ${{ success() && steps.find-comment.outcome == 'success' && inputs.comment_on_failure }} + if: ${{ success() && steps.find-comment.outcome == 'success' && + inputs.comment_on_failure }} env: GH_TOKEN: ${{ github.token }} run: | if [ "${{ steps.find-comment.outputs.comment-id }}" != "" ]; then gh api -X DELETE repos/${{ github.repository }}/issues/comments/${{ steps.find-comment.outputs.comment-id }} --silent fi + + - id: set-status-check + name: Set Status Check + if: ${{ inputs.comment_on_failure }} + uses: launchbynttdata/launch-workflows/.github/actions/update-status-checkfeat/unify-provider-auth + with: + check_name: "Conventional Commit PR Title" + status: ${{ steps.pr-validation.outcome == 'success' && 'success' || + steps.pr-validation.outcome == 'failure' && 'failure' || 'error' }} + description: ${{ steps.pr-validation.outcome == 'success' && 'PR title is + valid.' || steps.pr-validation.outcome == 'failure' && 'PR title is + invalid.' || 'Error validating PR title.' }} + + - id: set-status-check-legacy + name: Set Legacy Check + if: ${{ inputs.comment_on_failure }} + uses: launchbynttdata/launch-workflows/.github/actions/update-status-checkfeat/unify-provider-auth + with: + check_name: "Label Pull Request / Label Pull Request" + status: ${{ steps.pr-validation.outcome == 'success' && 'success' || + steps.pr-validation.outcome == 'failure' && 'failure' || 'error' }} + description: ${{ steps.pr-validation.outcome == 'success' && 'PR title is + valid.' || steps.pr-validation.outcome == 'failure' && 'PR title is + invalid.' || 'Error validating PR title.' }} diff --git a/.github/workflows/reusable-pr-dependabot-automerge.yml b/.github/workflows/reusable-pr-dependabot-automerge.yml new file mode 100644 index 0000000..1d546a1 --- /dev/null +++ b/.github/workflows/reusable-pr-dependabot-automerge.yml @@ -0,0 +1,26 @@ +name: Dependabot Auto-Merge + +on: + workflow_call: + inputs: + # No inputs needed for this workflow, but we need to define at least one to make it callable. + placeholder: + type: string + required: false + default: "" + description: "This input is not used, but is required to make this workflow callable." + +permissions: + contents: write + pull-requests: write + +jobs: + dependabot-automerge: + name: Dependabot Auto-Merge + runs-on: ubuntu-latest + if: ${{ github.actor == 'dependabot[bot]' }} + steps: + - name: Enable auto-merge + run: gh pr merge --auto --squash "${{ github.event.pull_request.html_url }}" + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/reusable-python-uv-pytest.yml b/.github/workflows/reusable-python-uv-pytest.yml index 0bc8652..ec2f674 100644 --- a/.github/workflows/reusable-python-uv-pytest.yml +++ b/.github/workflows/reusable-python-uv-pytest.yml @@ -4,32 +4,34 @@ on: workflow_call: inputs: python-version: - description: 'Python version to test against. Currently, this only supports one version per workflow run.' + description: "Python version to test against. Currently, this only supports one + version per workflow run." required: false type: string - default: '3.11' + default: "3.11" asdf-install: - description: 'Whether to install supplementary tools from .tool-versions' + description: "Whether to install supplementary tools from .tool-versions" required: false type: boolean default: true lcaf-makefile-setup: - description: 'Whether to set the environment up for the LCAF Makefile' + description: "Whether to set the environment up for the LCAF Makefile" required: false type: boolean default: false lcaf-aws-region: - description: 'AWS region to use for LCAF Makefile setup. Ignored if lcaf-makefile-setup is false.' + description: "AWS region to use for LCAF Makefile setup. Ignored if + lcaf-makefile-setup is false." required: false type: string - default: 'us-east-2' + default: "us-east-2" run-ruff: - description: 'Run Ruff for linting and formatting checks' + description: "Run Ruff for linting and formatting checks" required: false type: boolean default: true report-coverage: - description: 'Report on code coverage with a comment on the PR' + description: "Report on code coverage with a comment on the PR" required: false type: boolean default: true @@ -47,62 +49,63 @@ jobs: matrix: python-version: [ "${{ inputs.python-version }}" ] steps: - - uses: actions/checkout@ff7abcd0c3c05ccf6adc123a8cd1fd4fb30fb493 + - uses: actions/checkout@ff7abcd0c3c05ccf6adc123a8cd1fd4fb30fb493 - - name: Restore cached asdf tools - uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 - if: ${{ inputs.asdf-install }} - id: cache - with: - path: ~/.asdf - key: ${{ runner.os }}-tool-versions-${{ hashFiles('.tool-versions') }} + - name: Restore cached asdf tools + uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 + if: ${{ inputs.asdf-install }} + id: cache + with: + path: ~/.asdf + key: ${{ runner.os }}-tool-versions-${{ hashFiles('.tool-versions') }} - - name: asdf install - if: ${{ inputs.asdf-install }} - uses: asdf-vm/actions/install@b7bcd026f18772e44fe1026d729e1611cc435d47 + - name: asdf install + if: ${{ inputs.asdf-install }} + uses: asdf-vm/actions/install@b7bcd026f18772e44fe1026d729e1611cc435d47 - - name: Cache asdf tools - uses: actions/cache/save@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 - id: save-cache - if: ${{ inputs.asdf-install && steps.cache.outputs.cache-hit != 'true' }} - with: - path: ~/.asdf - key: ${{ runner.os }}-tool-versions-${{ hashFiles('.tool-versions') }} + - name: Cache asdf tools + uses: actions/cache/save@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 + id: save-cache + if: ${{ inputs.asdf-install && steps.cache.outputs.cache-hit != 'true' }} + with: + path: ~/.asdf + key: ${{ runner.os }}-tool-versions-${{ hashFiles('.tool-versions') }} - - name: Set up Python ${{ matrix.python-version }} - uses: astral-sh/setup-uv@5a095e7a2014a4212f075830d4f7277575a9d098 - with: - python-version: ${{ matrix.python-version }} + - name: Set up Python ${{ matrix.python-version }} + uses: astral-sh/setup-uv@5a095e7a2014a4212f075830d4f7277575a9d098 + with: + python-version: ${{ matrix.python-version }} - - name: Setup Repository for Makefile - if: ${{ inputs.lcaf-makefile-setup }} - # Ensure the 'repo' tool is installed, set up git to make the Makefile happy - shell: bash - run: | - mkdir -p ~/.local/bin - curl https://storage.googleapis.com/git-repo-downloads/repo > ~/.local/bin/repo - chmod +x ~/.local/bin/repo - echo "$HOME/.local/bin" >> $GITHUB_PATH - set -x - git config --global user.name "GitHub Actions" - git config --global user.email "noreply@launch.nttdata.com" - export AWS_REGION=${{ inputs.lcaf-aws-region }} + - name: Setup Repository for Makefile + if: ${{ inputs.lcaf-makefile-setup }} + # Ensure the 'repo' tool is installed, set up git to make the Makefile happy + shell: bash + env: + AWS_REGION: ${{ inputs.lcaf-aws-region }} + run: | + mkdir -p ~/.local/bin + curl https://storage.googleapis.com/git-repo-downloads/repo > ~/.local/bin/repo + chmod +x ~/.local/bin/repo + echo "$HOME/.local/bin" >> $GITHUB_PATH + set -x + git config --global user.name "GitHub Actions" + git config --global user.email "noreply@launch.nttdata.com" - - name: Ruff check - if: ${{ inputs.run-ruff }} - run: | - uvx ruff check + - name: Ruff check + if: ${{ inputs.run-ruff }} + run: | + uvx ruff check - - name: Test with pytest - run: | - uv run pytest + - name: Test with pytest + run: | + uv run pytest - - name: Report coverage - if: ${{ inputs.report-coverage }} - uses: MishaKav/pytest-coverage-comment@26f986d2599c288bb62f623d29c2da98609e9cd4 - with: - pytest-xml-coverage-path: ./htmlcov/coverage.xml - title: Coverage report for Python ${{ matrix.python-version }} - remove-link-from-badge: true - unique-id-for-comment: ${{ matrix.python-version }} - coverage-path-prefix: src/ + - name: Report coverage + if: ${{ inputs.report-coverage }} + uses: MishaKav/pytest-coverage-comment@26f986d2599c288bb62f623d29c2da98609e9cd4 + with: + pytest-xml-coverage-path: ./htmlcov/coverage.xml + title: Coverage report for Python ${{ matrix.python-version }} + remove-link-from-badge: true + unique-id-for-comment: ${{ matrix.python-version }} + coverage-path-prefix: src/ diff --git a/.github/workflows/reusable-terraform-check-aws.yml b/.github/workflows/reusable-terraform-check-aws.yml index 4197090..01e4227 100644 --- a/.github/workflows/reusable-terraform-check-aws.yml +++ b/.github/workflows/reusable-terraform-check-aws.yml @@ -4,18 +4,21 @@ on: workflow_call: inputs: assume_role_arn: - description: 'ARN of the role to assume prior to Terragrunt invocation. Terragrunt may use this role to assume other roles if configured to do so.' + description: "ARN of the role to assume prior to Terragrunt invocation. + Terragrunt may use this role to assume other roles if configured to do + so." required: true type: string region: - description: 'Region within the environment (e.g. us-east-1) to deploy' - default: 'us-east-2' + description: "Region within the environment (e.g. us-east-1) to deploy" + default: "us-east-2" required: true type: string permissions: id-token: write contents: read + statuses: write jobs: check: @@ -26,7 +29,7 @@ jobs: uses: actions/checkout@ff7abcd0c3c05ccf6adc123a8cd1fd4fb30fb493 - name: Setup asdf - # We use the 'setup' variant of this action, because we have some custom behavior in + # We use the 'setup' variant of this action, because we have some custom behavior in # our .tool-versions file and Makefile to install plugins from outside the default registry. uses: asdf-vm/actions/setup@b7bcd026f18772e44fe1026d729e1611cc435d47 @@ -41,6 +44,8 @@ jobs: - name: Setup Repository for Checks # Ensure the 'repo' tool is installed, set up git to make the Makefile happy, and then configure to clone LCAF. shell: bash + env: + AWS_REGION: ${{ inputs.region }} run: | mkdir -p ~/.local/bin curl https://storage.googleapis.com/git-repo-downloads/repo > ~/.local/bin/repo @@ -49,7 +54,6 @@ jobs: set -x git config user.name "GitHub Actions" git config user.email "noreply@launch.nttdata.com" - export AWS_REGION=${{ inputs.region }} make configure - uses: actions/cache/save@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 @@ -61,11 +65,29 @@ jobs: path: ~/.asdf key: ${{ runner.os }}-tool-versions-${{ hashFiles('.tool-versions') }} - - name: "make lint" + - name: "Set Terraform Lint status to pending" + uses: launchbynttdata/launch-workflows/.github/actions/update-status-checkfeat/unify-provider-auth + with: + check_name: "Terraform Lint" + status: "pending" + description: "Terraform lint check is running..." + + - id: lint + name: "make lint" run: | make lint - - name: Configure AWS credentials + - name: "Update Terraform Lint status" + if: always() && steps.lint.outcome != 'skipped' + uses: launchbynttdata/launch-workflows/.github/actions/update-status-checkfeat/unify-provider-auth + with: + check_name: "Terraform Lint" + status: ${{ steps.lint.outcome == 'success' && 'success' || steps.lint.outcome + == 'failure' && 'failure' || 'error' }} + description: "Terraform lint ${{ steps.lint.outcome }}" + + - id: aws-login + name: Configure AWS credentials uses: aws-actions/configure-aws-credentials@8df5847569e6427dd6c4fb1cf565c83acfa8afa7 with: role-to-assume: ${{ inputs.assume_role_arn }} @@ -80,12 +102,29 @@ jobs: set -x mkdir -p ~/.aws echo "[default]" > ~/.aws/credentials - echo "aws_access_key_id=${{ secrets.AWS_ACCESS_KEY_ID }}" >> ~/.aws/credentials - echo "aws_secret_access_key=${{ secrets.AWS_SECRET_ACCESS_KEY }}" >> ~/.aws/credentials - echo "aws_session_token=${{ secrets.AWS_SESSION_TOKEN }}" >> ~/.aws/credentials + echo "aws_access_key_id=${{ steps.aws-login.outputs.aws-access-key-id }}" >> ~/.aws/credentials + echo "aws_secret_access_key=${{ steps.aws-login.outputs.aws-secret-access-key }}" >> ~/.aws/credentials + echo "aws_session_token=${{ steps.aws-login.outputs.aws-session-token }}" >> ~/.aws/credentials # Fixup examples' provider.tf by dropping references to the profile, so that we can use the default profile. find examples -type f -name "provider.tf" -mindepth 2 -maxdepth 2 -exec bash -c 'grep -vE "profile\s=" "$1" > $1.tmp && mv $1.tmp $1' bash {} \; - - name: "make test" + - name: "Set Terraform Tests status to pending" + uses: launchbynttdata/launch-workflows/.github/actions/update-status-checkfeat/unify-provider-auth + with: + check_name: "Terraform Tests" + status: "pending" + description: "Terraform tests are running..." + + - id: test + name: "make test" run: | make test + + - name: "Update Terraform Tests status" + if: always() && steps.test.outcome != 'skipped' + uses: launchbynttdata/launch-workflows/.github/actions/update-status-checkfeat/unify-provider-auth + with: + check_name: "Terraform Tests" + status: ${{ steps.test.outcome == 'success' && 'success' || steps.test.outcome + == 'failure' && 'failure' || 'error' }} + description: "Terraform tests ${{ steps.test.outcome }}" diff --git a/.github/workflows/reusable-terraform-check-azure.yml b/.github/workflows/reusable-terraform-check-azure.yml index 785dbc7..285dc73 100644 --- a/.github/workflows/reusable-terraform-check-azure.yml +++ b/.github/workflows/reusable-terraform-check-azure.yml @@ -13,6 +13,7 @@ on: permissions: id-token: write contents: read + statuses: write jobs: check: @@ -20,7 +21,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 + uses: actions/checkout@ff7abcd0c3c05ccf6adc123a8cd1fd4fb30fb493 - name: Setup asdf # We use the 'setup' variant of this action, because we have some custom behavior in @@ -38,6 +39,8 @@ jobs: - name: Setup Repository for Checks # Ensure the 'repo' tool is installed, set up git to make the Makefile happy, and then configure to clone LCAF. shell: bash + env: + TERRAFORM_CHECK_AZURE_SUBSCRIPTION_ID: ${{ secrets.TERRAFORM_CHECK_AZURE_SUBSCRIPTION_ID }} run: | mkdir -p ~/.local/bin curl https://storage.googleapis.com/git-repo-downloads/repo > ~/.local/bin/repo @@ -46,7 +49,7 @@ jobs: set -x git config user.name "GitHub Actions" git config user.email "noreply@launch.nttdata.com" - export ARM_SUBSCRIPTION_ID=${{ secrets.TERRAFORM_CHECK_AZURE_SUBSCRIPTION_ID }} + export ARM_SUBSCRIPTION_ID="$TERRAFORM_CHECK_AZURE_SUBSCRIPTION_ID" make configure - uses: actions/cache/save@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 @@ -58,10 +61,27 @@ jobs: path: ~/.asdf key: ${{ runner.os }}-tool-versions-${{ hashFiles('.tool-versions') }} - - name: "make lint" + - name: "Set Terraform Lint status to pending" + uses: launchbynttdata/launch-workflows/.github/actions/update-status-checkfeat/unify-provider-auth + with: + check_name: "Terraform Lint" + status: "pending" + description: "Terraform lint check is running..." + + - id: lint + name: "make lint" run: | make lint + - name: "Update Terraform Lint status" + if: always() && steps.lint.outcome != 'skipped' + uses: launchbynttdata/launch-workflows/.github/actions/update-status-checkfeat/unify-provider-auth + with: + check_name: "Terraform Lint" + status: ${{ steps.lint.outcome == 'success' && 'success' || steps.lint.outcome + == 'failure' && 'failure' || 'error' }} + description: "Terraform lint ${{ steps.lint.outcome }}" + - name: Azure login uses: azure/login@a457da9ea143d694b1b9c7c869ebb04ebe844ef5 with: @@ -69,6 +89,23 @@ jobs: tenant-id: ${{ secrets.TERRAFORM_CHECK_AZURE_TENANT_ID }} subscription-id: ${{ secrets.TERRAFORM_CHECK_AZURE_SUBSCRIPTION_ID }} - - name: "make test" + - name: "Set Terraform Tests status to pending" + uses: launchbynttdata/launch-workflows/.github/actions/update-status-checkfeat/unify-provider-auth + with: + check_name: "Terraform Tests" + status: "pending" + description: "Terraform tests are running..." + + - id: test + name: "make test" run: | make test + + - name: "Update Terraform Tests status" + if: always() && steps.test.outcome != 'skipped' + uses: launchbynttdata/launch-workflows/.github/actions/update-status-checkfeat/unify-provider-auth + with: + check_name: "Terraform Tests" + status: ${{ steps.test.outcome == 'success' && 'success' || steps.test.outcome + == 'failure' && 'failure' || 'error' }} + description: "Terraform tests ${{ steps.test.outcome }}" diff --git a/.github/workflows/reusable-terraform-check.yml b/.github/workflows/reusable-terraform-check.yml new file mode 100644 index 0000000..c586dba --- /dev/null +++ b/.github/workflows/reusable-terraform-check.yml @@ -0,0 +1,373 @@ +name: Check Terraform Code + +on: + workflow_call: + inputs: + auth_method: + description: "Comma-delimited list of authentication methods to use. Valid + values are \"aws\", \"azure\", and \"github\" (e.g. \"aws\", + \"aws,github\", \"aws,azure\"). Pass an empty string or omit to run + Terraform checks without any authentication." + required: false + default: "" + type: string + # --- AWS authentication inputs --- + aws_assume_role_arn: + description: "ARN of the role to assume when running tests and creating test + resources. Required if auth_method includes 'aws', ignored otherwise. + Defaults to the value of the TERRAFORM_CHECK_AWS_ASSUME_ROLE_ARN + variable." + required: false + type: string + default: "${{ vars.TERRAFORM_CHECK_AWS_ASSUME_ROLE_ARN }}" + aws_auth_region: + description: "AWS region to use for authentication. Required if auth_method + includes 'aws', ignored otherwise. Defaults to the value of the + TERRAFORM_CHECK_AWS_REGION variable." + default: "${{ vars.TERRAFORM_CHECK_AWS_REGION }}" + required: false + type: string + # --- GitHub App authentication inputs --- + github_app_id: + description: "The GitHub App ID to use for authentication, defaulting to the + TERRAFORM_CHECK_GITHUB_APP_ID variable. Required if auth_method + includes 'github', ignored otherwise." + required: false + default: "${{ vars.TERRAFORM_CHECK_GITHUB_APP_ID }}" + type: string + secrets: + # --- Azure authentication secrets (required if auth_method includes 'azure') --- + TERRAFORM_CHECK_AZURE_CLIENT_ID: + required: false + TERRAFORM_CHECK_AZURE_TENANT_ID: + required: false + TERRAFORM_CHECK_AZURE_SUBSCRIPTION_ID: + required: false + # --- GitHub App authentication secrets (required if auth_method includes 'github') --- + TERRAFORM_CHECK_GITHUB_APP_SECRET: + required: false + +permissions: + contents: read + pull-requests: read + id-token: write + statuses: write + +jobs: + validate-inputs: + name: "Validate Inputs" + runs-on: ubuntu-latest + outputs: + auth_aws: ${{ steps.validate.outputs.auth_aws }} + auth_azure: ${{ steps.validate.outputs.auth_azure }} + auth_github: ${{ steps.validate.outputs.auth_github }} + steps: + - name: Validate inputs and secrets + id: validate + shell: bash + env: + AUTH_METHOD: ${{ inputs.auth_method }} + AWS_ASSUME_ROLE_ARN: ${{ inputs.aws_assume_role_arn }} + AWS_AUTH_REGION: ${{ inputs.aws_auth_region }} + TERRAFORM_CHECK_AZURE_CLIENT_ID: ${{ secrets.TERRAFORM_CHECK_AZURE_CLIENT_ID }} + TERRAFORM_CHECK_AZURE_TENANT_ID: ${{ secrets.TERRAFORM_CHECK_AZURE_TENANT_ID }} + TERRAFORM_CHECK_AZURE_SUBSCRIPTION_ID: ${{ secrets.TERRAFORM_CHECK_AZURE_SUBSCRIPTION_ID }} + GITHUB_APP_ID: ${{ inputs.github_app_id }} + TERRAFORM_CHECK_GITHUB_APP_SECRET: ${{ secrets.TERRAFORM_CHECK_GITHUB_APP_SECRET }} + run: | + auth_aws=false + auth_azure=false + auth_github=false + + IFS=',' read -ra AUTH_METHODS <<< "$AUTH_METHOD" + for method in "${AUTH_METHODS[@]}"; do + method=$(echo "$method" | xargs) + [[ -z "$method" ]] && continue + case "$method" in + # --- AWS authentication validation --- + aws) + auth_aws=true + if [[ -z "$AWS_ASSUME_ROLE_ARN" ]]; then + echo "Error: aws_assume_role_arn input is required when auth_method includes 'aws'." + exit 1 + fi + if [[ -z "$AWS_AUTH_REGION" ]]; then + echo "Error: aws_auth_region input is required when auth_method includes 'aws'." + exit 1 + fi + ;; + # --- Azure authentication validation --- + azure) + auth_azure=true + if [[ -z "$TERRAFORM_CHECK_AZURE_CLIENT_ID" ]]; then + echo "Error: TERRAFORM_CHECK_AZURE_CLIENT_ID secret is required when auth_method includes 'azure'." + exit 1 + fi + if [[ -z "$TERRAFORM_CHECK_AZURE_TENANT_ID" ]]; then + echo "Error: TERRAFORM_CHECK_AZURE_TENANT_ID secret is required when auth_method includes 'azure'." + exit 1 + fi + if [[ -z "$TERRAFORM_CHECK_AZURE_SUBSCRIPTION_ID" ]]; then + echo "Error: TERRAFORM_CHECK_AZURE_SUBSCRIPTION_ID secret is required when auth_method includes 'azure'." + exit 1 + fi + ;; + # --- GitHub App authentication validation --- + github) + auth_github=true + if [[ -z "$GITHUB_APP_ID" ]]; then + echo "Error: github_app_id input is required when auth_method includes 'github'." + exit 1 + fi + if [[ -z "$TERRAFORM_CHECK_GITHUB_APP_SECRET" ]]; then + echo "Error: TERRAFORM_CHECK_GITHUB_APP_SECRET secret is required when auth_method includes 'github'." + exit 1 + fi + ;; + *) + echo "Error: Invalid auth_method '$method'. Valid values are 'aws', 'azure', and 'github'." + exit 1 + ;; + esac + done + + echo "auth_aws=$auth_aws" >> $GITHUB_OUTPUT + echo "auth_azure=$auth_azure" >> $GITHUB_OUTPUT + echo "auth_github=$auth_github" >> $GITHUB_OUTPUT + lint: + name: "Lint Module" + runs-on: ubuntu-latest + needs: validate-inputs + steps: + - id: checkout + name: Checkout + uses: actions/checkout@ff7abcd0c3c05ccf6adc123a8cd1fd4fb30fb493 + + - id: dependencies + name: Setup asdf + # We use the 'setup' variant of this action, because we have some custom behavior in + # our .tool-versions file and Makefile to install plugins from outside the default registry. + uses: asdf-vm/actions/setup@b7bcd026f18772e44fe1026d729e1611cc435d47 + + - id: cache-restore + uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 + # If we've cached the asdf tools, restore them based on the hash of the .tool-versions file. + name: Restore cached asdf tools + with: + path: ~/.asdf + key: ${{ runner.os }}-tool-versions-${{ hashFiles('.tool-versions') }} + + - id: configure + name: Setup Repository for Checks + # Ensure the 'repo' tool is installed, set up git to make the Makefile happy, and then configure to clone LCAF. + shell: bash + run: | + mkdir -p ~/.local/bin + curl https://storage.googleapis.com/git-repo-downloads/repo > ~/.local/bin/repo + chmod +x ~/.local/bin/repo + echo "$HOME/.local/bin" >> $GITHUB_PATH + set -x + git config user.name "GitHub Actions" + git config user.email "noreply@launch.nttdata.com" + make configure + + - id: save-cache + uses: actions/cache/save@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 + name: Cache asdf tools + # If we didn't restore the asdf tools, save them based on the hash of the .tool-versions file. + if: steps.cache-restore.outputs.cache-hit != 'true' + with: + path: ~/.asdf + key: ${{ runner.os }}-tool-versions-${{ hashFiles('.tool-versions') }} + + - id: set-lint-pending + name: "Set Terraform Lint status to pending" + uses: launchbynttdata/launch-workflows/.github/actions/update-status-checkfeat/unify-provider-auth + with: + check_name: "Terraform Lint" + status: "pending" + description: "Terraform lint check is running..." + target_url: "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ + github.run_id }}" + + - id: lint + name: "make lint" + run: | + make lint + + - id: update-lint-status + name: "Update Terraform Lint status" + if: always() && steps.lint.outcome != 'skipped' + uses: launchbynttdata/launch-workflows/.github/actions/update-status-checkfeat/unify-provider-auth + with: + check_name: "Terraform Lint" + status: ${{ steps.lint.outcome == 'success' && 'success' || steps.lint.outcome + == 'failure' && 'failure' || 'error' }} + description: "Terraform lint ${{ steps.lint.outcome }}" + target_url: "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ + github.run_id }}" + + tests: + name: "Run Tests" + runs-on: ubuntu-latest + needs: validate-inputs + steps: + - id: checkout + name: Checkout + uses: actions/checkout@ff7abcd0c3c05ccf6adc123a8cd1fd4fb30fb493 + + - id: dependencies + name: Setup asdf + # We use the 'setup' variant of this action, because we have some custom behavior in + # our .tool-versions file and Makefile to install plugins from outside the default registry. + uses: asdf-vm/actions/setup@b7bcd026f18772e44fe1026d729e1611cc435d47 + + - id: cache-restore + uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 + # If we've cached the asdf tools, restore them based on the hash of the .tool-versions file. + name: Restore cached asdf tools + with: + path: ~/.asdf + key: ${{ runner.os }}-tool-versions-${{ hashFiles('.tool-versions') }} + + - id: configure + name: Setup Repository for Checks + # Ensure the 'repo' tool is installed, set up git to make the Makefile happy, and then configure to clone LCAF. + shell: bash + env: + AUTH_AWS: ${{ needs.validate-inputs.outputs.auth_aws }} + AUTH_AZURE: ${{ needs.validate-inputs.outputs.auth_azure }} + AWS_AUTH_REGION: ${{ inputs.aws_auth_region }} + TERRAFORM_CHECK_AZURE_SUBSCRIPTION_ID: ${{ secrets.TERRAFORM_CHECK_AZURE_SUBSCRIPTION_ID }} + run: | + mkdir -p ~/.local/bin + curl https://storage.googleapis.com/git-repo-downloads/repo > ~/.local/bin/repo + chmod +x ~/.local/bin/repo + echo "$HOME/.local/bin" >> $GITHUB_PATH + set -x + git config user.name "GitHub Actions" + git config user.email "noreply@launch.nttdata.com" + if [[ "$AUTH_AWS" == "true" ]]; then + export AWS_REGION="$AWS_AUTH_REGION" + elif [[ "$AUTH_AZURE" == "true" ]]; then + export ARM_SUBSCRIPTION_ID="$TERRAFORM_CHECK_AZURE_SUBSCRIPTION_ID" + fi + make configure + + - id: save-cache + uses: actions/cache/save@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 + name: Cache asdf tools + # If we didn't restore the asdf tools, save them based on the hash of the .tool-versions file. + if: steps.cache-restore.outputs.cache-hit != 'true' + with: + path: ~/.asdf + key: ${{ runner.os }}-tool-versions-${{ hashFiles('.tool-versions') }} + + - id: configure-aws-credentials + name: Configure AWS credentials + if: needs.validate-inputs.outputs.auth_aws == 'true' + uses: aws-actions/configure-aws-credentials@8df5847569e6427dd6c4fb1cf565c83acfa8afa7 + with: + role-to-assume: ${{ inputs.aws_assume_role_arn }} + role-session-name: ${{ github.run_id }} + aws-region: ${{ inputs.aws_auth_region }} + + - id: create-aws-profile + name: "Create AWS Profile" + if: needs.validate-inputs.outputs.auth_aws == 'true' + # Works around the interactions of aws-actions/configure-aws-credentials with 5.x.x AWS TF provider. + # In short, we need to create the profile in ~/.aws/credentials for the provider to use, but also need + # to remove line setting the profile in our examples, or we see failures to initialize the provider. + run: | + set -x + mkdir -p ~/.aws + echo "[default]" > ~/.aws/credentials + echo "aws_access_key_id=$AWS_ACCESS_KEY_ID" >> ~/.aws/credentials + echo "aws_secret_access_key=$AWS_SECRET_ACCESS_KEY" >> ~/.aws/credentials + echo "aws_session_token=$AWS_SESSION_TOKEN" >> ~/.aws/credentials + # Fixup examples' provider.tf by dropping references to the profile, so that we can use the default profile. + find examples -type f -name "provider.tf" -mindepth 2 -maxdepth 2 -exec bash -c 'grep -vE "profile\s=" "$1" > $1.tmp && mv $1.tmp $1' bash {} \; + + - id: azure-login + name: Azure login + if: needs.validate-inputs.outputs.auth_azure == 'true' + uses: azure/login@a457da9ea143d694b1b9c7c869ebb04ebe844ef5 + with: + client-id: ${{ secrets.TERRAFORM_CHECK_AZURE_CLIENT_ID }} + tenant-id: ${{ secrets.TERRAFORM_CHECK_AZURE_TENANT_ID }} + subscription-id: ${{ secrets.TERRAFORM_CHECK_AZURE_SUBSCRIPTION_ID }} + + - id: github-app-token + name: Create GitHub App Token + if: needs.validate-inputs.outputs.auth_github == 'true' + uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 + with: + app-id: ${{ inputs.github_app_id }} + private-key: ${{ secrets.TERRAFORM_CHECK_GITHUB_APP_SECRET }} + owner: ${{ github.repository_owner }} + + - id: set-tests-pending + name: "Set Terraform Tests status to pending" + continue-on-error: true + uses: launchbynttdata/launch-workflows/.github/actions/update-status-checkfeat/unify-provider-auth + with: + check_name: "Terraform Tests" + status: "pending" + description: "Terraform tests are running..." + target_url: "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ + github.run_id }}" + + - id: test + name: "make test" + env: + GITHUB_TOKEN: ${{ steps.github-app-token.outputs.token || github.token }} + run: | + # Hack for Azure, reevaluate when we have another look at the Make targets + make tfmodule/create_example_providers + + make test + + - id: update-tests-status + name: "Update Terraform Tests status" + if: always() && steps.test.outcome != 'skipped' + continue-on-error: true + uses: launchbynttdata/launch-workflows/.github/actions/update-status-checkfeat/unify-provider-auth + with: + check_name: "Terraform Tests" + status: ${{ steps.test.outcome == 'success' && 'success' || steps.test.outcome + == 'failure' && 'failure' || 'error' }} + description: "Terraform tests ${{ steps.test.outcome }}" + target_url: "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ + github.run_id }}" + + # These legacy status checks will write the necessary success for the existing launch workflows, so that we can transition + # between models without breaking merge capabilities on existing repositories. Once all repositories have transitioned to + # the new checks, we can remove this entire job. + legacy: + name: "Legacy Status Checks" + runs-on: ubuntu-latest + needs: [ validate-inputs, lint, tests ] + steps: + - id: update-legacy-status-checks-aws + name: "Add AWS Legacy Status Check" + if: startsWith(github.event.repository.name, 'tf-aws-') + uses: launchbynttdata/launch-workflows/.github/actions/update-status-checkfeat/unify-provider-auth + with: + check_name: "Check AWS Terraform Code / Check AWS Terraform Code" + status: "success" + description: "This check is being deprecated. Please refer to the new 'Terraform + Tests' check for test results." + target_url: "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ + github.run_id }}" + + - id: update-legacy-status-checks-azure + name: "Add Azure Legacy Status Check" + if: startsWith(github.event.repository.name, 'tf-az') + uses: launchbynttdata/launch-workflows/.github/actions/update-status-checkfeat/unify-provider-auth + with: + check_name: "Check Azure Terraform Code / Check Azure Terraform Code" + status: "success" + description: "This check is being deprecated. Please refer to the new 'Terraform + Tests' check for test results." + target_url: "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ + github.run_id }}" diff --git a/.github/workflows/reusable-terragrunt-deploy-aws.yml b/.github/workflows/reusable-terragrunt-deploy-aws.yml index b1ec8d3..07210cd 100644 --- a/.github/workflows/reusable-terragrunt-deploy-aws.yml +++ b/.github/workflows/reusable-terragrunt-deploy-aws.yml @@ -18,7 +18,8 @@ on: default: "0.54.11" type: string gh_environment: - description: "GitHub Environment to deploy to (e.g. test, production). If not supplied, will be set from the environment input." + description: "GitHub Environment to deploy to (e.g. test, production). If not + supplied, will be set from the environment input." required: false type: string environments_root: @@ -49,7 +50,8 @@ on: type: string default: "" before_shared_commands: - description: "Commands to run prior to both Terragrunt plan and apply. These are applied after before_plan_commands and before_deploy_commands." + description: "Commands to run prior to both Terragrunt plan and apply. These are + applied after before_plan_commands and before_deploy_commands." required: false type: string default: "" @@ -64,13 +66,15 @@ on: type: string default: "" after_shared_commands: - description: "Commands to run after both Terragrunt plan and apply. These are applied after after_plan_commands and after_deploy_commands." + description: "Commands to run after both Terragrunt plan and apply. These are + applied after after_plan_commands and after_deploy_commands." required: false type: string default: "" outputs: terraform_outputs: - description: "JSON string containing all Terraform outputs from the deployment (base64 encoded)" + description: "JSON string containing all Terraform outputs from the deployment + (base64 encoded)" value: ${{ jobs.deploy.outputs.terraform_outputs }} permissions: @@ -79,7 +83,8 @@ permissions: jobs: deploy: - name: "Plan & Deploy ${{ inputs.environment }}/${{ inputs.region }}/${{ inputs.env_id }}" + name: "Plan & Deploy ${{ inputs.environment }}/${{ inputs.region }}/${{ + inputs.env_id }}" runs-on: ubuntu-latest environment: ${{ inputs.gh_environment || inputs.environment }} outputs: @@ -112,7 +117,7 @@ jobs: - name: Configure Mise id: configure-mise - uses: launchbynttdata/launch-workflows/.github/actions/terragrunt-configure-mise@0.14.2 + uses: launchbynttdata/launch-workflows/.github/actions/terragrunt-configure-misefeat/unify-provider-auth with: tf_version: ${{ inputs.tf_version }} tg_version: ${{ inputs.tg_version }} @@ -137,8 +142,10 @@ jobs: with: tf_path: "terraform" tg_version: ${{ inputs.tg_version }} - tg_dir: "${{ inputs.environments_root }}/${{ inputs.environment }}/${{ inputs.region }}/${{ inputs.env_id }}" - tg_command: "plan -out=${{ inputs.environment }}-${{ inputs.region }}-${{ inputs.env_id }}.tfplan" + tg_dir: "${{ inputs.environments_root }}/${{ inputs.environment }}/${{ + inputs.region }}/${{ inputs.env_id }}" + tg_command: "plan -out=${{ inputs.environment }}-${{ inputs.region }}-${{ + inputs.env_id }}.tfplan" - name: Deploy uses: gruntwork-io/terragrunt-action@5e86476ca61eaf74adb9c0525745f29f921f2199 @@ -159,12 +166,15 @@ jobs: with: tf_path: "terraform" tg_version: ${{ inputs.tg_version }} - tg_dir: "${{ inputs.environments_root }}/${{ inputs.environment }}/${{ inputs.region }}/${{ inputs.env_id }}" - tg_command: "apply ${{ inputs.environment }}-${{ inputs.region }}-${{ inputs.env_id }}.tfplan" + tg_dir: "${{ inputs.environments_root }}/${{ inputs.environment }}/${{ + inputs.region }}/${{ inputs.env_id }}" + tg_command: "apply ${{ inputs.environment }}-${{ inputs.region }}-${{ + inputs.env_id }}.tfplan" - name: Get Terraform Outputs id: set-outputs - working-directory: "${{ inputs.environments_root }}/${{ inputs.environment }}/${{ inputs.region }}/${{ inputs.env_id }}" + working-directory: "${{ inputs.environments_root }}/${{ inputs.environment + }}/${{ inputs.region }}/${{ inputs.env_id }}" env: AWS_REGION: ${{ inputs.region }} run: | diff --git a/.github/workflows/reusable-terragrunt-deploy-azure.yml b/.github/workflows/reusable-terragrunt-deploy-azure.yml index ad1d8e7..109d50c 100644 --- a/.github/workflows/reusable-terragrunt-deploy-azure.yml +++ b/.github/workflows/reusable-terragrunt-deploy-azure.yml @@ -45,7 +45,8 @@ on: type: string default: "" before_shared_commands: - description: "Commands to run prior to both Terragrunt plan and apply. These are applied after before_plan_commands and before_deploy_commands." + description: "Commands to run prior to both Terragrunt plan and apply. These are + applied after before_plan_commands and before_deploy_commands." required: false type: string default: "" @@ -60,13 +61,15 @@ on: type: string default: "" after_shared_commands: - description: "Commands to run after both Terragrunt plan and apply. These are applied after after_plan_commands and after_deploy_commands." + description: "Commands to run after both Terragrunt plan and apply. These are + applied after after_plan_commands and after_deploy_commands." required: false type: string default: "" outputs: terraform_outputs: - description: "JSON string containing all Terraform outputs from the deployment (base64 encoded)" + description: "JSON string containing all Terraform outputs from the deployment + (base64 encoded)" value: ${{ jobs.deploy.outputs.terraform_outputs }} secrets: TERRAGRUNT_DEPLOY_AZURE_CLIENT_ID: @@ -82,7 +85,8 @@ permissions: jobs: deploy: - name: "Plan & Deploy ${{ inputs.environment }}/${{ inputs.region }}/${{ inputs.env_id }}" + name: "Plan & Deploy ${{ inputs.environment }}/${{ inputs.region }}/${{ + inputs.env_id }}" runs-on: ubuntu-latest outputs: terraform_outputs: ${{ steps.set-outputs.outputs.terraform_outputs }} @@ -106,7 +110,7 @@ jobs: - name: Configure Mise id: configure-mise - uses: launchbynttdata/launch-workflows/.github/actions/terragrunt-configure-mise@0.14.2 + uses: launchbynttdata/launch-workflows/.github/actions/terragrunt-configure-misefeat/unify-provider-auth with: tf_version: ${{ inputs.tf_version }} tg_version: ${{ inputs.tg_version }} @@ -129,8 +133,10 @@ jobs: ARM_USE_OIDC: true TF_VAR_system_tags: ${{ steps.set-tags.outputs.SYSTEM_TAGS }} with: - tg_dir: "${{ inputs.environments_root }}/${{ inputs.environment }}/${{ inputs.region }}/${{ inputs.env_id }}" - tg_command: "plan -out=${{ inputs.environment }}-${{ inputs.region }}-${{ inputs.env_id }}.tfplan" + tg_dir: "${{ inputs.environments_root }}/${{ inputs.environment }}/${{ + inputs.region }}/${{ inputs.env_id }}" + tg_command: "plan -out=${{ inputs.environment }}-${{ inputs.region }}-${{ + inputs.env_id }}.tfplan" - name: Deploy uses: gruntwork-io/terragrunt-action@5e86476ca61eaf74adb9c0525745f29f921f2199 @@ -149,12 +155,15 @@ jobs: ARM_USE_OIDC: true TF_VAR_system_tags: ${{ steps.set-tags.outputs.SYSTEM_TAGS }} with: - tg_dir: "${{ inputs.environments_root }}/${{ inputs.environment }}/${{ inputs.region }}/${{ inputs.env_id }}" - tg_command: "apply ${{ inputs.environment }}-${{ inputs.region }}-${{ inputs.env_id }}.tfplan" + tg_dir: "${{ inputs.environments_root }}/${{ inputs.environment }}/${{ + inputs.region }}/${{ inputs.env_id }}" + tg_command: "apply ${{ inputs.environment }}-${{ inputs.region }}-${{ + inputs.env_id }}.tfplan" - name: Get Terraform Outputs id: set-outputs - working-directory: "${{ inputs.environments_root }}/${{ inputs.environment }}/${{ inputs.region }}/${{ inputs.env_id }}" + working-directory: "${{ inputs.environments_root }}/${{ inputs.environment + }}/${{ inputs.region }}/${{ inputs.env_id }}" env: ARM_CLIENT_ID: ${{ secrets.TERRAGRUNT_DEPLOY_AZURE_CLIENT_ID }} ARM_SUBSCRIPTION_ID: ${{ secrets.TERRAGRUNT_DEPLOY_AZURE_SUBSCRIPTION_ID }} diff --git a/.github/workflows/reusable-terragrunt-deploy-ephemeral-aws.yml b/.github/workflows/reusable-terragrunt-deploy-ephemeral-aws.yml index 57e9c21..4f6b153 100644 --- a/.github/workflows/reusable-terragrunt-deploy-ephemeral-aws.yml +++ b/.github/workflows/reusable-terragrunt-deploy-ephemeral-aws.yml @@ -17,7 +17,9 @@ on: default: "0.54.11" type: string assume_role_arn: - description: "ARN of the role to assume prior to Terragrunt invocation. Terragrunt may use this role to assume other roles if configured to do so." + description: "ARN of the role to assume prior to Terragrunt invocation. + Terragrunt may use this role to assume other roles if configured to do + so." required: true type: string region: @@ -44,7 +46,8 @@ on: type: string default: "" before_shared_commands: - description: "Commands to run prior to both Terragrunt plan and apply. These are applied after before_plan_commands and before_deploy_commands." + description: "Commands to run prior to both Terragrunt plan and apply. These are + applied after before_plan_commands and before_deploy_commands." required: false type: string default: "" @@ -59,13 +62,15 @@ on: type: string default: "" after_shared_commands: - description: "Commands to run after both Terragrunt plan and apply. These are applied after after_plan_commands and after_deploy_commands." + description: "Commands to run after both Terragrunt plan and apply. These are + applied after after_plan_commands and after_deploy_commands." required: false type: string default: "" outputs: terraform_outputs_json: - description: "Base64-encoded JSON string containing all non-sensitive Terraform outputs from the apply" + description: "Base64-encoded JSON string containing all non-sensitive Terraform + outputs from the apply" value: ${{ jobs.deploy.outputs.terraform_outputs_json }} permissions: @@ -80,7 +85,7 @@ jobs: terraform_outputs_json: ${{ steps.set-outputs.outputs.terraform_outputs_json }} steps: - name: Checkout - uses: actions/checkout@8edcb1bdb4e267140fa742c62e395cd74f332709 + uses: actions/checkout@ff7abcd0c3c05ccf6adc123a8cd1fd4fb30fb493 - name: Configure AWS credentials uses: aws-actions/configure-aws-credentials@8df5847569e6427dd6c4fb1cf565c83acfa8afa7 @@ -100,7 +105,7 @@ jobs: - name: Configure Mise id: configure-mise - uses: launchbynttdata/launch-workflows/.github/actions/terragrunt-configure-mise@0.14.2 + uses: launchbynttdata/launch-workflows/.github/actions/terragrunt-configure-misefeat/unify-provider-auth with: tf_version: ${{ inputs.tf_version }} tg_version: ${{ inputs.tg_version }} @@ -126,8 +131,10 @@ jobs: with: tf_path: "terraform" tg_version: ${{ inputs.tg_version }} - tg_dir: "${{ inputs.environments_root }}/sandbox/${{ inputs.region }}/${{ inputs.env_id }}" - tg_command: "plan -out=ephemeral-${{ inputs.region }}-${{ inputs.env_id }}.tfplan" + tg_dir: "${{ inputs.environments_root }}/sandbox/${{ inputs.region }}/${{ + inputs.env_id }}" + tg_command: "plan -out=ephemeral-${{ inputs.region }}-${{ inputs.env_id + }}.tfplan" - name: Deploy id: deploy @@ -153,7 +160,8 @@ jobs: with: tf_path: "terraform" tg_version: ${{ inputs.tg_version }} - tg_dir: "${{ inputs.environments_root }}/sandbox/${{ inputs.region }}/${{ inputs.env_id }}" + tg_dir: "${{ inputs.environments_root }}/sandbox/${{ inputs.region }}/${{ + inputs.env_id }}" tg_command: "apply ephemeral-${{ inputs.region }}-${{ inputs.env_id }}.tfplan" - name: Set Terraform Outputs diff --git a/.github/workflows/reusable-terragrunt-deploy.yml b/.github/workflows/reusable-terragrunt-deploy.yml new file mode 100644 index 0000000..d8c02ee --- /dev/null +++ b/.github/workflows/reusable-terragrunt-deploy.yml @@ -0,0 +1,373 @@ +name: Deploy Terragrunt Environment + +on: + workflow_call: + inputs: + auth_method: + description: "Comma-delimited list of authentication methods to use. Valid + values are \"aws\", \"azure\", and \"github\" (e.g. \"aws\", + \"aws,github\", \"aws,azure\"). Pass an empty string or omit to run + Terragrunt without any authentication." + required: false + default: "" + type: string + # --- AWS authentication inputs --- + aws_auth_region: + description: "The AWS region to use for authentication. Required if auth_method + includes 'aws', ignored otherwise." + required: false + type: string + aws_assume_role_arn: + description: "ARN of the role to assume prior to Terragrunt invocation, + defaulting to the DEPLOY_ROLE_ARN variable. Required if auth_method + includes 'aws', ignored otherwise." + required: false + default: "${{ vars.DEPLOY_ROLE_ARN }}" + type: string + # --- GitHub App authentication inputs --- + github_app_id: + description: "The GitHub App ID to use for authentication, defaulting to the + TERRAGRUNT_DEPLOY_GITHUB_APP_ID variable. Required if auth_method + includes 'github', ignored otherwise." + required: false + default: "${{ vars.TERRAGRUNT_DEPLOY_GITHUB_APP_ID }}" + type: string + # --- General workflow configuration --- + git_branch: + description: "Branch triggering this deployment." + required: true + type: string + tf_version: + description: "Version of Terraform to utilize" + required: true + default: "1.5.5" + type: string + tg_version: + description: "Version of Terragrunt to utilize" + required: true + type: string + gh_environment: + description: "GitHub Environment to deploy to (e.g. test, production)." + required: false + type: string + tg_dir: + description: "Folder containing the Terragrunt configuration to deploy (relative + to the repository root, e.g. platform/test/us-east-1/000)" + required: true + type: string + before_plan_commands: + description: "Commands to run prior to executing Terragrunt plan." + required: false + type: string + default: "" + before_deploy_commands: + description: "Commands to run prior to executing Terragrunt apply." + required: false + type: string + default: "" + before_shared_commands: + description: "Commands to run prior to both Terragrunt plan and apply. These are + applied after before_plan_commands and before_deploy_commands." + required: false + type: string + default: "" + after_plan_commands: + description: "Commands to run after executing Terragrunt plan." + required: false + type: string + default: "" + after_deploy_commands: + description: "Commands to run after executing Terragrunt apply." + required: false + type: string + default: "" + after_shared_commands: + description: "Commands to run after both Terragrunt plan and apply. These are + applied after after_plan_commands and after_deploy_commands." + required: false + type: string + default: "" + outputs: + terraform_outputs: + description: "JSON string containing all Terraform outputs from the deployment + (base64 encoded)" + value: ${{ jobs.deploy.outputs.terraform_outputs }} + secrets: + # --- Azure authentication secrets (required if auth_method includes 'azure') --- + TERRAGRUNT_DEPLOY_AZURE_CLIENT_ID: + required: false + TERRAGRUNT_DEPLOY_AZURE_TENANT_ID: + required: false + TERRAGRUNT_DEPLOY_AZURE_SUBSCRIPTION_ID: + required: false + # --- GitHub App authentication secrets (required if auth_method includes 'github') --- + TERRAGRUNT_DEPLOY_GITHUB_APP_SECRET: + required: false + +permissions: + id-token: write + contents: read + statuses: write + +jobs: + validate-inputs: + name: "Validate Inputs" + runs-on: ubuntu-latest + environment: ${{ inputs.gh_environment }} + outputs: + auth_aws: ${{ steps.validate.outputs.auth_aws }} + auth_azure: ${{ steps.validate.outputs.auth_azure }} + auth_github: ${{ steps.validate.outputs.auth_github }} + steps: + - name: Validate inputs and secrets + id: validate + shell: bash + env: + AUTH_METHOD: ${{ inputs.auth_method }} + AWS_AUTH_REGION: ${{ inputs.aws_auth_region }} + AWS_ASSUME_ROLE_ARN: ${{ inputs.aws_assume_role_arn }} + TERRAGRUNT_DEPLOY_AZURE_CLIENT_ID: ${{ secrets.TERRAGRUNT_DEPLOY_AZURE_CLIENT_ID }} + TERRAGRUNT_DEPLOY_AZURE_TENANT_ID: ${{ secrets.TERRAGRUNT_DEPLOY_AZURE_TENANT_ID }} + TERRAGRUNT_DEPLOY_AZURE_SUBSCRIPTION_ID: ${{ secrets.TERRAGRUNT_DEPLOY_AZURE_SUBSCRIPTION_ID }} + GITHUB_APP_ID: ${{ inputs.github_app_id }} + TERRAGRUNT_DEPLOY_GITHUB_APP_SECRET: ${{ secrets.TERRAGRUNT_DEPLOY_GITHUB_APP_SECRET }} + run: | + auth_aws=false + auth_azure=false + auth_github=false + + IFS=',' read -ra AUTH_METHODS <<< "$AUTH_METHOD" + for method in "${AUTH_METHODS[@]}"; do + method=$(echo "$method" | xargs) + [[ -z "$method" ]] && continue + case "$method" in + # --- AWS authentication validation --- + aws) + auth_aws=true + if [[ -z "$AWS_AUTH_REGION" ]]; then + echo "Error: aws_auth_region input is required when auth_method includes 'aws'." + exit 1 + fi + if [[ -z "$AWS_ASSUME_ROLE_ARN" ]]; then + echo "Error: aws_assume_role_arn input is required when auth_method includes 'aws'." + exit 1 + fi + ;; + # --- Azure authentication validation --- + azure) + auth_azure=true + if [[ -z "$TERRAGRUNT_DEPLOY_AZURE_CLIENT_ID" ]]; then + echo "Error: TERRAGRUNT_DEPLOY_AZURE_CLIENT_ID secret is required when auth_method includes 'azure'." + exit 1 + fi + if [[ -z "$TERRAGRUNT_DEPLOY_AZURE_TENANT_ID" ]]; then + echo "Error: TERRAGRUNT_DEPLOY_AZURE_TENANT_ID secret is required when auth_method includes 'azure'." + exit 1 + fi + if [[ -z "$TERRAGRUNT_DEPLOY_AZURE_SUBSCRIPTION_ID" ]]; then + echo "Error: TERRAGRUNT_DEPLOY_AZURE_SUBSCRIPTION_ID secret is required when auth_method includes 'azure'." + exit 1 + fi + ;; + # --- GitHub App authentication validation --- + github) + auth_github=true + if [[ -z "$GITHUB_APP_ID" ]]; then + echo "Error: github_app_id input is required when auth_method includes 'github'." + exit 1 + fi + if [[ -z "$TERRAGRUNT_DEPLOY_GITHUB_APP_SECRET" ]]; then + echo "Error: TERRAGRUNT_DEPLOY_GITHUB_APP_SECRET secret is required when auth_method includes 'github'." + exit 1 + fi + ;; + *) + echo "Error: Invalid auth_method '$method'. Valid values are 'aws', 'azure', and 'github'." + exit 1 + ;; + esac + done + + echo "auth_aws=$auth_aws" >> $GITHUB_OUTPUT + echo "auth_azure=$auth_azure" >> $GITHUB_OUTPUT + echo "auth_github=$auth_github" >> $GITHUB_OUTPUT + + deploy: + name: "Plan & Deploy ${{ inputs.tg_dir }}" + runs-on: ubuntu-latest + needs: validate-inputs + environment: ${{ inputs.gh_environment }} + outputs: + terraform_outputs: ${{ steps.set-outputs.outputs.terraform_outputs }} + steps: + - name: Checkout + uses: actions/checkout@ff7abcd0c3c05ccf6adc123a8cd1fd4fb30fb493 + + - name: Configure AWS credentials + if: needs.validate-inputs.outputs.auth_aws == 'true' + uses: aws-actions/configure-aws-credentials@8df5847569e6427dd6c4fb1cf565c83acfa8afa7 + with: + role-to-assume: ${{ inputs.aws_assume_role_arn }} + role-session-name: ${{ github.run_id }} + aws-region: ${{ inputs.aws_auth_region }} + + - name: Azure Login + if: needs.validate-inputs.outputs.auth_azure == 'true' + uses: azure/login@a457da9ea143d694b1b9c7c869ebb04ebe844ef5 + with: + client-id: ${{ secrets.TERRAGRUNT_DEPLOY_AZURE_CLIENT_ID }} + tenant-id: ${{ secrets.TERRAGRUNT_DEPLOY_AZURE_TENANT_ID }} + subscription-id: ${{ secrets.TERRAGRUNT_DEPLOY_AZURE_SUBSCRIPTION_ID }} + + - name: "Set default Terraform Tags (AWS)" + if: needs.validate-inputs.outputs.auth_aws == 'true' + id: set-tags-aws + run: | + set -x + echo "TF_VAR_organization_tag=${{ github.repository_owner }}" >> "$GITHUB_OUTPUT" + echo "TF_VAR_repository_tag=$(echo "${{ github.repository }}" | cut -d "/" -f 2)" >> "$GITHUB_OUTPUT" + echo "TF_VAR_commit_hash_tag=${{ github.sha }}" >> "$GITHUB_OUTPUT" + echo "TF_VAR_branch_tag=${{ inputs.git_branch }}" >> "$GITHUB_OUTPUT" + + - name: "Set default Terraform Tags (Azure)" + if: needs.validate-inputs.outputs.auth_azure == 'true' + id: set-tags-azure + run: | + set -x + repo=$(echo "${{ github.repository }}" | cut -d "/" -f 2) + echo "SYSTEM_TAGS={\"Organization\":\"${{ github.repository_owner }}\",\"Repository\":\"$repo\",\"Branch\":\"${{ inputs.git_branch }}\",\"CommitHash\":\"${{ github.sha }}\"}" >> "$GITHUB_OUTPUT" + + - name: Create GitHub App Token + id: github-app-token + if: needs.validate-inputs.outputs.auth_github == 'true' + uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 + with: + app-id: ${{ inputs.github_app_id }} + private-key: ${{ secrets.TERRAGRUNT_DEPLOY_GITHUB_APP_SECRET }} + owner: ${{ github.repository_owner }} + + - name: Configure Mise + id: configure-mise + uses: launchbynttdata/launch-workflows/.github/actions/terragrunt-configure-misefeat/unify-provider-auth + with: + tf_version: ${{ inputs.tf_version }} + tg_version: ${{ inputs.tg_version }} + + - name: "Set Terragrunt Plan status to pending" + uses: launchbynttdata/launch-workflows/.github/actions/update-status-checkfeat/unify-provider-auth + with: + check_name: "Terragrunt Plan (${{ inputs.tg_dir }})" + status: "pending" + description: "Terragrunt plan is running..." + target_url: "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ + github.run_id }}" + + - name: Plan + id: plan + uses: gruntwork-io/terragrunt-action@5e86476ca61eaf74adb9c0525745f29f921f2199 + env: + INPUT_PRE_EXEC_0: | + ${{ inputs.before_plan_commands }} + INPUT_PRE_EXEC_1: | + ${{ inputs.before_shared_commands }} + INPUT_POST_EXEC_0: | + ${{ inputs.after_plan_commands }} + INPUT_POST_EXEC_1: | + ${{ inputs.after_shared_commands }} + TF_VAR_organization_tag: ${{ steps.set-tags-aws.outputs.TF_VAR_organization_tag }} + TF_VAR_repository_tag: ${{ steps.set-tags-aws.outputs.TF_VAR_repository_tag }} + TF_VAR_commit_hash_tag: ${{ steps.set-tags-aws.outputs.TF_VAR_commit_hash_tag }} + TF_VAR_branch_tag: ${{ steps.set-tags-aws.outputs.TF_VAR_branch_tag }} + ARM_CLIENT_ID: ${{ secrets.TERRAGRUNT_DEPLOY_AZURE_CLIENT_ID }} + ARM_SUBSCRIPTION_ID: ${{ secrets.TERRAGRUNT_DEPLOY_AZURE_SUBSCRIPTION_ID }} + ARM_TENANT_ID: ${{ secrets.TERRAGRUNT_DEPLOY_AZURE_TENANT_ID }} + ARM_USE_OIDC: ${{ needs.validate-inputs.outputs.auth_azure == 'true' && 'true' + || '' }} + TF_VAR_system_tags: ${{ steps.set-tags-azure.outputs.SYSTEM_TAGS }} + GITHUB_TOKEN: ${{ steps.github-app-token.outputs.token || '' }} + with: + tf_path: "terraform" + tg_version: ${{ inputs.tg_version }} + tg_dir: "${{ inputs.tg_dir }}" + tg_command: "plan -out=${{ github.sha }}.tfplan" + + - name: "Update Terragrunt Plan status" + if: always() && steps.plan.outcome != 'skipped' + uses: launchbynttdata/launch-workflows/.github/actions/update-status-checkfeat/unify-provider-auth + with: + check_name: "Terragrunt Plan (${{ inputs.tg_dir }})" + status: ${{ steps.plan.outcome == 'success' && 'success' || steps.plan.outcome + == 'failure' && 'failure' || 'error' }} + description: "Terragrunt plan ${{ steps.plan.outcome }}" + target_url: "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ + github.run_id }}" + + - name: "Set Terragrunt Deploy status to pending" + uses: launchbynttdata/launch-workflows/.github/actions/update-status-checkfeat/unify-provider-auth + with: + check_name: "Terragrunt Deploy (${{ inputs.tg_dir }})" + status: "pending" + description: "Terragrunt deploy is running..." + target_url: "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ + github.run_id }}" + + - name: Deploy + id: deploy + uses: gruntwork-io/terragrunt-action@5e86476ca61eaf74adb9c0525745f29f921f2199 + env: + INPUT_PRE_EXEC_0: | + ${{ inputs.before_deploy_commands }} + INPUT_PRE_EXEC_1: | + ${{ inputs.before_shared_commands }} + INPUT_POST_EXEC_0: | + ${{ inputs.after_deploy_commands }} + INPUT_POST_EXEC_1: | + ${{ inputs.after_shared_commands }} + TF_VAR_organization_tag: ${{ steps.set-tags-aws.outputs.TF_VAR_organization_tag }} + TF_VAR_repository_tag: ${{ steps.set-tags-aws.outputs.TF_VAR_repository_tag }} + TF_VAR_commit_hash_tag: ${{ steps.set-tags-aws.outputs.TF_VAR_commit_hash_tag }} + TF_VAR_branch_tag: ${{ steps.set-tags-aws.outputs.TF_VAR_branch_tag }} + ARM_CLIENT_ID: ${{ secrets.TERRAGRUNT_DEPLOY_AZURE_CLIENT_ID }} + ARM_SUBSCRIPTION_ID: ${{ secrets.TERRAGRUNT_DEPLOY_AZURE_SUBSCRIPTION_ID }} + ARM_TENANT_ID: ${{ secrets.TERRAGRUNT_DEPLOY_AZURE_TENANT_ID }} + ARM_USE_OIDC: ${{ needs.validate-inputs.outputs.auth_azure == 'true' && 'true' + || '' }} + TF_VAR_system_tags: ${{ steps.set-tags-azure.outputs.SYSTEM_TAGS }} + GITHUB_TOKEN: ${{ steps.github-app-token.outputs.token || '' }} + with: + tf_path: "terraform" + tg_version: ${{ inputs.tg_version }} + tg_dir: "${{ inputs.tg_dir }}" + tg_command: "apply ${{ github.sha }}.tfplan" + + - name: "Update Terragrunt Deploy status" + if: always() && steps.deploy.outcome != 'skipped' + uses: launchbynttdata/launch-workflows/.github/actions/update-status-checkfeat/unify-provider-auth + with: + check_name: "Terragrunt Deploy (${{ inputs.tg_dir }})" + status: ${{ steps.deploy.outcome == 'success' && 'success' || + steps.deploy.outcome == 'failure' && 'failure' || 'error' }} + description: "Terragrunt deploy ${{ steps.deploy.outcome }}" + target_url: "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ + github.run_id }}" + + - name: Get Terraform Outputs + id: set-outputs + working-directory: "${{ inputs.tg_dir }}" + env: + ARM_CLIENT_ID: ${{ secrets.TERRAGRUNT_DEPLOY_AZURE_CLIENT_ID }} + ARM_SUBSCRIPTION_ID: ${{ secrets.TERRAGRUNT_DEPLOY_AZURE_SUBSCRIPTION_ID }} + ARM_TENANT_ID: ${{ secrets.TERRAGRUNT_DEPLOY_AZURE_TENANT_ID }} + ARM_USE_OIDC: ${{ needs.validate-inputs.outputs.auth_azure == 'true' && 'true' + || '' }} + GITHUB_TOKEN: ${{ steps.github-app-token.outputs.token || '' }} + run: | + echo "=== Getting Terraform outputs ===" + TF_OUTPUTS=$(terragrunt output -json 2>/dev/null | jq 'with_entries(select(.value.sensitive != true))' || echo '{}') + echo "Raw outputs:" + echo "$TF_OUTPUTS" | jq '.' + + # Base64 encode for safe transfer + ENCODED_OUTPUTS=$(echo "$TF_OUTPUTS" | base64 -w 0) + echo "terraform_outputs=$ENCODED_OUTPUTS" >> "$GITHUB_OUTPUT" + echo "=== Outputs captured and encoded ===" diff --git a/.github/workflows/reusable-terragrunt-destroy-ephemeral-aws.yml b/.github/workflows/reusable-terragrunt-destroy-ephemeral-aws.yml index 736a1d8..ed6215b 100644 --- a/.github/workflows/reusable-terragrunt-destroy-ephemeral-aws.yml +++ b/.github/workflows/reusable-terragrunt-destroy-ephemeral-aws.yml @@ -17,7 +17,9 @@ on: default: "0.54.11" type: string assume_role_arn: - description: "ARN of the role to assume prior to Terragrunt invocation. Terragrunt may use this role to assume other roles if configured to do so." + description: "ARN of the role to assume prior to Terragrunt invocation. + Terragrunt may use this role to assume other roles if configured to do + so." required: true type: string region: @@ -74,7 +76,7 @@ jobs: - name: Configure Mise id: configure-mise - uses: launchbynttdata/launch-workflows/.github/actions/terragrunt-configure-mise@0.14.2 + uses: launchbynttdata/launch-workflows/.github/actions/terragrunt-configure-misefeat/unify-provider-auth with: tf_version: ${{ inputs.tf_version }} tg_version: ${{ inputs.tg_version }} @@ -95,5 +97,6 @@ jobs: with: tf_path: "terraform" tg_version: ${{ inputs.tg_version }} - tg_dir: "${{ inputs.environments_root }}/sandbox/${{ inputs.region }}/${{ inputs.env_id }}" + tg_dir: "${{ inputs.environments_root }}/sandbox/${{ inputs.region }}/${{ + inputs.env_id }}" tg_command: "destroy" diff --git a/.github/workflows/reusable-terragrunt-plan-only-aws.yml b/.github/workflows/reusable-terragrunt-plan-only-aws.yml index 10bd42c..9973e3c 100644 --- a/.github/workflows/reusable-terragrunt-plan-only-aws.yml +++ b/.github/workflows/reusable-terragrunt-plan-only-aws.yml @@ -18,7 +18,9 @@ on: default: "0.54.11" type: string assume_role_arn: - description: "ARN of the role to assume prior to Terragrunt invocation. Terragrunt may use this role to assume other roles if configured to do so." + description: "ARN of the role to assume prior to Terragrunt invocation. + Terragrunt may use this role to assume other roles if configured to do + so." required: true type: string environments_root: @@ -79,7 +81,7 @@ jobs: - name: Configure Mise id: configure-mise - uses: launchbynttdata/launch-workflows/.github/actions/terragrunt-configure-mise@0.14.2 + uses: launchbynttdata/launch-workflows/.github/actions/terragrunt-configure-misefeat/unify-provider-auth with: tf_version: ${{ inputs.tf_version }} tg_version: ${{ inputs.tg_version }} @@ -100,5 +102,6 @@ jobs: with: tf_path: "terraform" tg_version: ${{ inputs.tg_version }} - tg_dir: "${{ inputs.environments_root }}/${{ inputs.environment }}/${{ inputs.region }}/${{ inputs.env_id }}" + tg_dir: "${{ inputs.environments_root }}/${{ inputs.environment }}/${{ + inputs.region }}/${{ inputs.env_id }}" tg_command: "plan" diff --git a/.github/workflows/reusable-terragrunt-plan-only-azure.yml b/.github/workflows/reusable-terragrunt-plan-only-azure.yml index 4b12a0f..188f51a 100644 --- a/.github/workflows/reusable-terragrunt-plan-only-azure.yml +++ b/.github/workflows/reusable-terragrunt-plan-only-azure.yml @@ -80,7 +80,7 @@ jobs: - name: Configure Mise id: configure-mise - uses: launchbynttdata/launch-workflows/.github/actions/terragrunt-configure-mise@0.14.2 + uses: launchbynttdata/launch-workflows/.github/actions/terragrunt-configure-misefeat/unify-provider-auth with: tf_version: ${{ inputs.tf_version }} tg_version: ${{ inputs.tg_version }} @@ -99,5 +99,6 @@ jobs: ARM_USE_OIDC: true TF_VAR_system_tags: ${{ steps.set-tags.outputs.SYSTEM_TAGS }} with: - tg_dir: "${{inputs.environments_root}}/${{ inputs.environment }}/${{ inputs.region }}/${{ inputs.env_id }}" + tg_dir: "${{inputs.environments_root}}/${{ inputs.environment }}/${{ + inputs.region }}/${{ inputs.env_id }}" tg_command: "plan" diff --git a/.github/workflows/reusable-terragrunt-plan-only.yml b/.github/workflows/reusable-terragrunt-plan-only.yml new file mode 100644 index 0000000..5297f6f --- /dev/null +++ b/.github/workflows/reusable-terragrunt-plan-only.yml @@ -0,0 +1,262 @@ +name: Plan Terragrunt Environment + +on: + workflow_call: + inputs: + auth_method: + description: "Comma-delimited list of authentication methods to use. Valid + values are \"aws\", \"azure\", and \"github\" (e.g. \"aws\", + \"aws,github\", \"aws,azure\"). Pass an empty string or omit to run + Terragrunt without any authentication." + required: false + default: "" + type: string + # --- AWS authentication inputs --- + aws_auth_region: + description: "The AWS region to use for authentication. Required if auth_method + includes 'aws', ignored otherwise." + required: false + type: string + aws_assume_role_arn: + description: "ARN of the role to assume prior to Terragrunt invocation. Required + if auth_method includes 'aws', ignored otherwise." + required: false + type: string + # --- GitHub App authentication inputs --- + github_app_id: + description: "The GitHub App ID to use for authentication, defaulting to the + TERRAGRUNT_DEPLOY_GITHUB_APP_ID variable. Required if auth_method + includes 'github', ignored otherwise." + required: false + default: "${{ vars.TERRAGRUNT_DEPLOY_GITHUB_APP_ID }}" + type: string + # --- General workflow configuration --- + git_branch: + description: "Branch triggering this plan" + required: true + type: string + tf_version: + description: "Version of Terraform to utilize" + required: true + default: "1.5.5" + type: string + tg_version: + description: "Version of Terragrunt to utilize" + required: true + type: string + tg_dir: + description: "Folder containing the Terragrunt configuration to plan (relative + to the repository root, e.g. platform/test/us-east-1/000)" + required: true + type: string + before_plan_commands: + description: "Commands to run prior to executing Terragrunt plan." + required: false + type: string + default: "" + after_plan_commands: + description: "Commands to run after executing Terragrunt plan." + required: false + type: string + default: "" + secrets: + # --- Azure authentication secrets (required if auth_method includes 'azure') --- + TERRAGRUNT_DEPLOY_AZURE_CLIENT_ID: + required: false + TERRAGRUNT_DEPLOY_AZURE_TENANT_ID: + required: false + TERRAGRUNT_DEPLOY_AZURE_SUBSCRIPTION_ID: + required: false + # --- GitHub App authentication secrets (required if auth_method includes 'github') --- + TERRAGRUNT_DEPLOY_GITHUB_APP_SECRET: + required: false + +permissions: + id-token: write + pull-requests: read + contents: read + statuses: write + +jobs: + validate-inputs: + name: "Validate Inputs" + runs-on: ubuntu-latest + outputs: + auth_aws: ${{ steps.validate.outputs.auth_aws }} + auth_azure: ${{ steps.validate.outputs.auth_azure }} + auth_github: ${{ steps.validate.outputs.auth_github }} + steps: + - name: Validate inputs and secrets + id: validate + shell: bash + env: + AUTH_METHOD: ${{ inputs.auth_method }} + AWS_AUTH_REGION: ${{ inputs.aws_auth_region }} + AWS_ASSUME_ROLE_ARN: ${{ inputs.aws_assume_role_arn }} + TERRAGRUNT_DEPLOY_AZURE_CLIENT_ID: ${{ secrets.TERRAGRUNT_DEPLOY_AZURE_CLIENT_ID }} + TERRAGRUNT_DEPLOY_AZURE_TENANT_ID: ${{ secrets.TERRAGRUNT_DEPLOY_AZURE_TENANT_ID }} + TERRAGRUNT_DEPLOY_AZURE_SUBSCRIPTION_ID: ${{ secrets.TERRAGRUNT_DEPLOY_AZURE_SUBSCRIPTION_ID }} + GITHUB_APP_ID: ${{ inputs.github_app_id }} + TERRAGRUNT_DEPLOY_GITHUB_APP_SECRET: ${{ secrets.TERRAGRUNT_DEPLOY_GITHUB_APP_SECRET }} + run: | + auth_aws=false + auth_azure=false + auth_github=false + + IFS=',' read -ra AUTH_METHODS <<< "$AUTH_METHOD" + for method in "${AUTH_METHODS[@]}"; do + method=$(echo "$method" | xargs) + [[ -z "$method" ]] && continue + case "$method" in + # --- AWS authentication validation --- + aws) + auth_aws=true + if [[ -z "$AWS_AUTH_REGION" ]]; then + echo "Error: aws_auth_region input is required when auth_method includes 'aws'." + exit 1 + fi + if [[ -z "$AWS_ASSUME_ROLE_ARN" ]]; then + echo "Error: aws_assume_role_arn input is required when auth_method includes 'aws'." + exit 1 + fi + ;; + # --- Azure authentication validation --- + azure) + auth_azure=true + if [[ -z "$TERRAGRUNT_DEPLOY_AZURE_CLIENT_ID" ]]; then + echo "Error: TERRAGRUNT_DEPLOY_AZURE_CLIENT_ID secret is required when auth_method includes 'azure'." + exit 1 + fi + if [[ -z "$TERRAGRUNT_DEPLOY_AZURE_TENANT_ID" ]]; then + echo "Error: TERRAGRUNT_DEPLOY_AZURE_TENANT_ID secret is required when auth_method includes 'azure'." + exit 1 + fi + if [[ -z "$TERRAGRUNT_DEPLOY_AZURE_SUBSCRIPTION_ID" ]]; then + echo "Error: TERRAGRUNT_DEPLOY_AZURE_SUBSCRIPTION_ID secret is required when auth_method includes 'azure'." + exit 1 + fi + ;; + # --- GitHub App authentication validation --- + github) + auth_github=true + if [[ -z "$GITHUB_APP_ID" ]]; then + echo "Error: github_app_id input is required when auth_method includes 'github'." + exit 1 + fi + if [[ -z "$TERRAGRUNT_DEPLOY_GITHUB_APP_SECRET" ]]; then + echo "Error: TERRAGRUNT_DEPLOY_GITHUB_APP_SECRET secret is required when auth_method includes 'github'." + exit 1 + fi + ;; + *) + echo "Error: Invalid auth_method '$method'. Valid values are 'aws', 'azure', and 'github'." + exit 1 + ;; + esac + done + + echo "auth_aws=$auth_aws" >> $GITHUB_OUTPUT + echo "auth_azure=$auth_azure" >> $GITHUB_OUTPUT + echo "auth_github=$auth_github" >> $GITHUB_OUTPUT + + plan: + name: "Plan ${{ inputs.tg_dir }}" + runs-on: ubuntu-latest + needs: validate-inputs + steps: + - name: Checkout + uses: actions/checkout@ff7abcd0c3c05ccf6adc123a8cd1fd4fb30fb493 + - name: Configure AWS credentials + if: needs.validate-inputs.outputs.auth_aws == 'true' + uses: aws-actions/configure-aws-credentials@8df5847569e6427dd6c4fb1cf565c83acfa8afa7 + with: + role-to-assume: ${{ inputs.aws_assume_role_arn }} + role-session-name: ${{ github.run_id }} + aws-region: ${{ inputs.aws_auth_region }} + + - name: Azure Login + if: needs.validate-inputs.outputs.auth_azure == 'true' + uses: azure/login@a457da9ea143d694b1b9c7c869ebb04ebe844ef5 + with: + client-id: ${{ secrets.TERRAGRUNT_DEPLOY_AZURE_CLIENT_ID }} + tenant-id: ${{ secrets.TERRAGRUNT_DEPLOY_AZURE_TENANT_ID }} + subscription-id: ${{ secrets.TERRAGRUNT_DEPLOY_AZURE_SUBSCRIPTION_ID }} + + - name: "Set default Terraform Tags (AWS)" + if: needs.validate-inputs.outputs.auth_aws == 'true' + id: set-tags-aws + run: | + set -x + echo "TF_VAR_organization_tag=${{ github.repository_owner }}" >> "$GITHUB_OUTPUT" + echo "TF_VAR_repository_tag=$(echo "${{ github.repository }}" | cut -d "/" -f 2)" >> "$GITHUB_OUTPUT" + echo "TF_VAR_commit_hash_tag=${{ github.sha }}" >> "$GITHUB_OUTPUT" + echo "TF_VAR_branch_tag=${{ inputs.git_branch }}" >> "$GITHUB_OUTPUT" + + - name: "Set default Terraform Tags (Azure)" + if: needs.validate-inputs.outputs.auth_azure == 'true' + id: set-tags-azure + run: | + set -x + repo=$(echo "${{ github.repository }}" | cut -d "/" -f 2) + echo "SYSTEM_TAGS={\"Organization\":\"${{ github.repository_owner }}\",\"Repository\":\"$repo\",\"Branch\":\"${{ inputs.git_branch }}\",\"CommitHash\":\"${{ github.sha }}\"}" >> "$GITHUB_OUTPUT" + + - name: Create GitHub App Token + id: github-app-token + if: needs.validate-inputs.outputs.auth_github == 'true' + uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 + with: + app-id: ${{ inputs.github_app_id }} + private-key: ${{ secrets.TERRAGRUNT_DEPLOY_GITHUB_APP_SECRET }} + owner: ${{ github.repository_owner }} + + - name: Configure Mise + id: configure-mise + uses: launchbynttdata/launch-workflows/.github/actions/terragrunt-configure-misefeat/unify-provider-auth + with: + tf_version: ${{ inputs.tf_version }} + tg_version: ${{ inputs.tg_version }} + + - name: "Set Terragrunt Plan status to pending" + uses: launchbynttdata/launch-workflows/.github/actions/update-status-checkfeat/unify-provider-auth + with: + check_name: "Terragrunt Plan (${{ inputs.tg_dir }})" + status: "pending" + description: "Terragrunt plan is running..." + target_url: "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ + github.run_id }}" + + - name: Plan + id: plan + uses: gruntwork-io/terragrunt-action@5e86476ca61eaf74adb9c0525745f29f921f2199 + env: + INPUT_PRE_EXEC_0: | + ${{ inputs.before_plan_commands }} + INPUT_POST_EXEC_0: | + ${{ inputs.after_plan_commands }} + TF_VAR_organization_tag: ${{ steps.set-tags-aws.outputs.TF_VAR_organization_tag }} + TF_VAR_repository_tag: ${{ steps.set-tags-aws.outputs.TF_VAR_repository_tag }} + TF_VAR_commit_hash_tag: ${{ steps.set-tags-aws.outputs.TF_VAR_commit_hash_tag }} + TF_VAR_branch_tag: ${{ steps.set-tags-aws.outputs.TF_VAR_branch_tag }} + ARM_CLIENT_ID: ${{ secrets.TERRAGRUNT_DEPLOY_AZURE_CLIENT_ID }} + ARM_SUBSCRIPTION_ID: ${{ secrets.TERRAGRUNT_DEPLOY_AZURE_SUBSCRIPTION_ID }} + ARM_TENANT_ID: ${{ secrets.TERRAGRUNT_DEPLOY_AZURE_TENANT_ID }} + ARM_USE_OIDC: ${{ needs.validate-inputs.outputs.auth_azure == 'true' && 'true' + || '' }} + TF_VAR_system_tags: ${{ steps.set-tags-azure.outputs.SYSTEM_TAGS }} + GITHUB_TOKEN: ${{ steps.github-app-token.outputs.token || '' }} + with: + tf_path: "terraform" + tg_version: ${{ inputs.tg_version }} + tg_dir: "${{ inputs.tg_dir }}" + tg_command: "plan" + + - name: "Update Terragrunt Plan status" + if: always() && steps.plan.outcome != 'skipped' + uses: launchbynttdata/launch-workflows/.github/actions/update-status-checkfeat/unify-provider-auth + with: + check_name: "Terragrunt Plan (${{ inputs.tg_dir }})" + status: ${{ steps.plan.outcome == 'success' && 'success' || steps.plan.outcome + == 'failure' && 'failure' || 'error' }} + description: "Terragrunt plan ${{ steps.plan.outcome }}" + target_url: "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ + github.run_id }}" diff --git a/.github/workflows/reusable-update-from-skeleton.yml b/.github/workflows/reusable-update-from-skeleton.yml new file mode 100644 index 0000000..dec4c79 --- /dev/null +++ b/.github/workflows/reusable-update-from-skeleton.yml @@ -0,0 +1,227 @@ +name: Update Repository from Skeleton + +on: + workflow_call: + inputs: + recopy: + description: "Perform a full recopy instead of an incremental update (overwrites + all templated files), and re-runs any tasks defined in copier.yml. The + resulting PR will require manual review and merging." + type: boolean + default: false + skeleton_update_app_id: + description: "The GitHub App ID of the app to use for authentication when + pushing updates. The app must be installed on the repository and have + permissions to read and write code, as well as create pull requests." + required: false + type: string + default: ${{ vars.LAUNCH_SKELETON_UPDATE_APP_ID }} + secrets: + LAUNCH_SKELETON_UPDATE_KEY: + description: "The private key for the GitHub App used for authentication when + pushing updates. The app must be installed on the repository and have + permissions to read and write code, as well as create pull requests." + required: true + +permissions: + contents: write + pull-requests: write + +jobs: + update-from-skeleton: + name: Update from Skeleton + runs-on: ubuntu-latest + steps: + - id: get-app-token + uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 + with: + app-id: ${{ inputs.skeleton_update_app_id }} + private-key: ${{ secrets.LAUNCH_SKELETON_UPDATE_KEY }} + + - id: checkout + name: Checkout Repository + uses: actions/checkout@ff7abcd0c3c05ccf6adc123a8cd1fd4fb30fb493 + with: + token: ${{ steps.get-app-token.outputs.token }} + + - id: setup-python + name: Set up Python 3.14 + uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 + with: + python-version: 3.14 + + - id: install-tools + name: Install Tools with uv + run: | + uv tool install copier + uv tool install pre-commit --with pre-commit-uv + + - id: get-repo-custom-properties + name: Get Repository Custom Properties + uses: launchbynttdata/launch-workflows/.github/actions/get-custom-propertiesfeat/unify-provider-auth + + - id: run-updates + name: Run Updates from Skeleton + run: | + PRERELEASE=$(echo '${{ steps.get-repo-custom-properties.outputs.properties }}' | jq -r '.[] | select(.property_name == "prerelease") | .value') + + COPIER_CMD="update" + COPIER_FLAGS="--defaults --trust" + if [ "${{ inputs.recopy }}" == "true" ]; then + COPIER_CMD="recopy" + COPIER_FLAGS="$COPIER_FLAGS --overwrite" + fi + + if [ "$PRERELEASE" == "true" ]; then + echo "Using prerelease versions of skeleton workflows ($COPIER_CMD)" + uv run copier $COPIER_CMD $COPIER_FLAGS --prereleases + else + echo "Using stable versions of skeleton workflows ($COPIER_CMD)" + uv run copier $COPIER_CMD $COPIER_FLAGS + fi + + if [[ -z "$(git status --porcelain)" ]]; then + echo "No changes detected" + echo "has_changes=false" >> "$GITHUB_OUTPUT" + echo "### ✅ No Updates Available" >> "$GITHUB_STEP_SUMMARY" + echo "" >> "$GITHUB_STEP_SUMMARY" + echo "The repository is already up to date with the skeleton template." >> "$GITHUB_STEP_SUMMARY" + else + echo "Changes detected" + echo "has_changes=true" >> "$GITHUB_OUTPUT" + echo "### 🔄 Updates Available" >> "$GITHUB_STEP_SUMMARY" + echo "" >> "$GITHUB_STEP_SUMMARY" + echo "Changes were detected from the skeleton template. A pull request will be created." >> "$GITHUB_STEP_SUMMARY" + if [ ! -f .pre-commit-config.yaml ]; then + echo "No .pre-commit-config.yaml found, skipping pre-commit validation" + echo "pre_commit_passed=false" >> "$GITHUB_OUTPUT" + { + echo "PRE_COMMIT_OUTPUT<<__PRECOMMIT_EOF__" + echo "No .pre-commit-config.yaml found in repository. Pre-commit validation was skipped." + echo "__PRECOMMIT_EOF__" + } >> "$GITHUB_ENV" + else + PRE_COMMIT_LOG=$(mktemp) + set -o pipefail + if ! pre-commit run check-merge-conflict --all-files 2>&1 | tee "$PRE_COMMIT_LOG"; then + PRE_COMMIT_OUTPUT=$(cat "$PRE_COMMIT_LOG") + rm -f "$PRE_COMMIT_LOG" + + echo "Pre-commit checks failed" + echo "pre_commit_passed=false" >> "$GITHUB_OUTPUT" + + CONFLICT_FILES=$(echo "$PRE_COMMIT_OUTPUT" | grep ': Merge conflict' | sed 's/:[0-9]*: Merge conflict.*//' | sort -u) || true + + { + echo "PRE_COMMIT_OUTPUT<<__PRECOMMIT_EOF__" + echo "$PRE_COMMIT_OUTPUT" + echo "__PRECOMMIT_EOF__" + } >> "$GITHUB_ENV" + + if [ -n "$CONFLICT_FILES" ]; then + { + echo "CONFLICT_FILES<<__CONFLICTS_EOF__" + echo "$CONFLICT_FILES" + echo "__CONFLICTS_EOF__" + } >> "$GITHUB_ENV" + fi + else + rm -f "$PRE_COMMIT_LOG" + echo "Pre-commit checks passed" + echo "pre_commit_passed=true" >> "$GITHUB_OUTPUT" + fi + fi + fi + + - name: Commit and Push Changes + id: commit-and-push + if: steps.run-updates.outputs.has_changes == 'true' + run: | + USER_ID=$(gh api "/users/${{ steps.get-app-token.outputs.app-slug }}[bot]" --jq .id) + + git config --global user.name '${{ steps.get-app-token.outputs.app-slug }}[bot]' + git config --global user.email "$USER_ID+${{ steps.get-app-token.outputs.app-slug }}[bot]@users.noreply.github.com" + + BRANCH_NAME="patch/skeleton-update-$(date +%Y%m%d-%H%M%S)" + echo "branch_name=$BRANCH_NAME" >> "$GITHUB_OUTPUT" + git checkout -b "$BRANCH_NAME" + git add . + + if [ "${{ steps.run-updates.outputs.pre_commit_passed }}" == "true" ] && [ "${{ inputs.recopy }}" != "true" ]; then + git commit -m "chore: update from skeleton" + else + git commit -m "fix: update from skeleton" + fi + + git push origin "$BRANCH_NAME" + env: + GH_TOKEN: ${{ steps.get-app-token.outputs.token }} + + - name: Create Pull Request + if: steps.run-updates.outputs.has_changes == 'true' && + steps.run-updates.outputs.pre_commit_passed == 'true' && inputs.recopy + != true + run: | + PR_BODY=$(cat <<-'EOF' + Automated update from skeleton template. + + No conflicting changes were detected from the base repository. + EOF + ) + PR_URL=$(gh pr create \ + --base main \ + --head "${{ steps.commit-and-push.outputs.branch_name }}" \ + --title "chore: update from skeleton" \ + --body "$PR_BODY") + gh pr merge "${{ steps.commit-and-push.outputs.branch_name }}" --auto --squash + echo "### ✅ Pull Request Created" >> "$GITHUB_STEP_SUMMARY" + echo "" >> "$GITHUB_STEP_SUMMARY" + echo "$PR_URL" >> "$GITHUB_STEP_SUMMARY" + echo "" >> "$GITHUB_STEP_SUMMARY" + echo "Auto-merge has been enabled." >> "$GITHUB_STEP_SUMMARY" + env: + GH_TOKEN: ${{ steps.get-app-token.outputs.token }} + + - name: Create Pull Request (requires review) + if: steps.run-updates.outputs.has_changes == 'true' && + (steps.run-updates.outputs.pre_commit_passed == 'false' || + inputs.recopy == true) + run: | + if [ "${{ inputs.recopy }}" == "true" ] && [ "${{ steps.run-updates.outputs.pre_commit_passed }}" == "true" ]; then + PR_BODY=$(cat <<-'EOF' + Automated recopy from skeleton template. + + All templated files have been overwritten. Please review the changes carefully before merging. + EOF + ) + else + PR_BODY=$(cat <<-'EOF' + Automated update from skeleton template. + + ⚠️ Problems occurred during pre-commit validation. Manual review and conflict resolution may be required before merging. + + Pre-commit output: + $(echo "$PRE_COMMIT_OUTPUT" | sed 's/^/> /') + EOF + ) + + if [ -n "$CONFLICT_FILES" ]; then + CONFLICT_LIST=$(echo "$CONFLICT_FILES" | sed 's/^/- `/' | sed 's/$/`/') + PR_BODY="${PR_BODY} + + The following files contain merge conflict markers: + ${CONFLICT_LIST}" + fi + fi + PR_URL=$(gh pr create \ + --base main \ + --head "${{ steps.commit-and-push.outputs.branch_name }}" \ + --title "fix: update from skeleton" \ + --body "$PR_BODY") + echo "### ⚠️ Pull Request Created (requires review)" >> "$GITHUB_STEP_SUMMARY" + echo "" >> "$GITHUB_STEP_SUMMARY" + echo "$PR_URL" >> "$GITHUB_STEP_SUMMARY" + echo "" >> "$GITHUB_STEP_SUMMARY" + echo "This PR requires manual review before merging." >> "$GITHUB_STEP_SUMMARY" + env: + GH_TOKEN: ${{ steps.get-app-token.outputs.token }} diff --git a/docs/reusable-pr-automated-approvals.md b/docs/reusable-pr-automated-approvals.md new file mode 100644 index 0000000..3f915d0 --- /dev/null +++ b/docs/reusable-pr-automated-approvals.md @@ -0,0 +1,53 @@ +# Automated Approvals for Pull Requests + +Automatically approves pull requests authored entirely by trusted automation accounts (`dependabot[bot]` and `launch-skeleton-auto-updater[bot]`). If any non-automated commits are detected on the PR, existing automated approvals are revoked instead. + +This workflow uses two separate GitHub App identities to provide the two approvals typically required by branch protection rules. + +## Behavior + +1. **Approve** — If all commits on the PR are from allowed automation authors, both approver apps submit an approval review. +2. **Revoke** — If any commit is from a non-automated author, any existing approvals from the approver apps are dismissed. This prevents a human from pushing commits onto an automated PR to bypass review requirements. +3. **Skeleton updater title check** — For PRs authored by `launch-skeleton-auto-updater[bot]`, the PR title must start with `chore`. If it does not, approval is withheld. + +## Usage + +Add the following workflow to your repository (suggested name: `.github/workflows/pr-automated-approvals.yml`): + +```yaml +name: Automated Approvals + +on: + pull_request: + types: [opened, reopened, synchronize] + +permissions: + pull-requests: read + contents: read + +jobs: + automated-approvals: + name: Automated Approvals + permissions: + pull-requests: read + contents: read + uses: launchbynttdata/launch-workflows/.github/workflows/reusable-pr-automated-approvals.yml@ref + secrets: inherit +``` + +Be sure you replace `ref` with an appropriate ref to this repository. + +## Inputs + +This workflow has no configurable inputs. + +## Required Secrets and Variables + +| Name | Type | Description | +|------|------|-------------| +| `LAUNCH_APPROVER_ALPHA_ID` | Variable | The GitHub App ID for the first approver app. | +| `LAUNCH_APPROVER_ALPHA_KEY` | Secret | The private key for the first approver app. | +| `LAUNCH_APPROVER_BRAVO_ID` | Variable | The GitHub App ID for the second approver app. | +| `LAUNCH_APPROVER_BRAVO_KEY` | Secret | The private key for the second approver app. | + +Both approver apps must be installed on the repository with permission to submit pull request reviews. diff --git a/docs/reusable-pr-conventional-commit-title.md b/docs/reusable-pr-conventional-commit-title.md index c2eabee..3cdee30 100644 --- a/docs/reusable-pr-conventional-commit-title.md +++ b/docs/reusable-pr-conventional-commit-title.md @@ -42,15 +42,18 @@ jobs: permissions: contents: read pull-requests: write + statuses: write uses: launchbynttdata/launch-workflows/.github/workflows/reusable-pr-conventional-commit-title.yml@ref ``` Be sure you replace `ref` with an appropriate ref to this repository. > [!CAUTION] -> By default, your repository likely does not require this workflow to succeed before a change is merged. By making this workflow required, you ensure that a successful run must be achieved prior to merge, which ensures your commit messages are consistent! +> By default, your repository may not require this workflow to succeed before a change is merged. By making this workflow required, you ensure that a successful run must be achieved prior to merge, which ensures your commit messages are consistent! > -> To make this workflow required, visit your repository's settings and create a new Ruleset with a required status check, as shown below: +> Repositories within `launchbynttdata` are already configured with a required status check, and no further action needs to be taken. + +To make this workflow required, visit your repository's settings and create a new Ruleset with a required status check, as shown below: ![Required status check in the GitHub settings page](images/required-status-check.png) diff --git a/docs/reusable-pr-dependabot-automerge.md b/docs/reusable-pr-dependabot-automerge.md new file mode 100644 index 0000000..5c97129 --- /dev/null +++ b/docs/reusable-pr-dependabot-automerge.md @@ -0,0 +1,43 @@ +# Dependabot Auto-Merge + +Automatically enables GitHub's auto-merge (squash strategy) on pull requests created by Dependabot. Once all required status checks pass and any required approvals are in place, the PR will be merged automatically. + +This workflow only runs when the PR actor is `dependabot[bot]`. For all other actors, the job is skipped. + +## Usage + +Add the following workflow to your repository (suggested name: `.github/workflows/pr-dependabot-automerge.yml`): + +```yaml +name: Dependabot Auto-Merge + +on: + pull_request: + types: [opened, reopened, synchronize] + +permissions: + contents: write + pull-requests: write + +jobs: + dependabot-automerge: + name: Dependabot Auto-Merge + permissions: + contents: write + pull-requests: write + uses: launchbynttdata/launch-workflows/.github/workflows/reusable-pr-dependabot-automerge.yml@ref + secrets: inherit +``` + +Be sure you replace `ref` with an appropriate ref to this repository. + +> [!TIP] +> This workflow pairs well with the [Automated Approvals](reusable-pr-automated-approvals.md) workflow. When both are active, Dependabot PRs receive automated approvals and are then auto-merged once all status checks pass. + +## Inputs + +This workflow has no configurable inputs. + +## Required Permissions + +The calling workflow must grant `contents: write` and `pull-requests: write` permissions so that the `GITHUB_TOKEN` can enable auto-merge on the pull request. diff --git a/docs/reusable-terraform-check-aws.md b/docs/reusable-terraform-check-aws.md index 2986de0..86bdc60 100644 --- a/docs/reusable-terraform-check-aws.md +++ b/docs/reusable-terraform-check-aws.md @@ -1,3 +1,8 @@ +> [!CAUTION] +> Deprecation Notice +> +> This workflow will be deprecated with the 1.0.0 release of launch-workflows. + # Check a Terraform Module in AWS Performs a series of checks of a Terraform module, including deploying the example modules to AWS. diff --git a/docs/reusable-terraform-check-azure.md b/docs/reusable-terraform-check-azure.md index 74c172c..a056a8f 100644 --- a/docs/reusable-terraform-check-azure.md +++ b/docs/reusable-terraform-check-azure.md @@ -1,3 +1,8 @@ +> [!CAUTION] +> Deprecation Notice +> +> This workflow will be deprecated with the 1.0.0 release of launch-workflows. + # Check a Terraform Module in Azure Performs a series of checks of a Terraform module, including deploying the example modules to Azure. diff --git a/docs/reusable-terraform-check.md b/docs/reusable-terraform-check.md new file mode 100644 index 0000000..e2dc7b9 --- /dev/null +++ b/docs/reusable-terraform-check.md @@ -0,0 +1,160 @@ +# Check a Terraform Module + +Performs a series of checks of a Terraform module, including linting the Terraform code via `make lint`, and deploying the example modules to a cloud provider (or no provider at all) and running the golang tests via the `make test` target. This workflow unifies the previously separate `reusable-terraform-check-aws.yml` and `reusable-terraform-check-azure.yml` workflows into a single, provider-agnostic workflow controlled by the `auth_method` input. + +This workflow wraps both the `make lint` and `make test` targets in the Makefile. + +## Usage + +### No cloud authentication (`auth_method: none`) + +Use this when your module does not require cloud credentials to run tests (e.g., pure Terraform logic tests, mocked providers): + +```yaml +name: Check Terraform Code + +on: + pull_request: + types: [ opened, reopened, synchronize, ready_for_review ] + branches: [ main ] + +permissions: + id-token: write + contents: read + +jobs: + check: + name: "Check Terraform Code" + permissions: + contents: read + id-token: write + uses: launchbynttdata/launch-workflows/.github/workflows/reusable-terraform-check.yml@ref + with: + auth_method: "none" +``` + +### AWS authentication (`auth_method: aws`) + +Use this when your module deploys example resources to AWS. Authentication is performed via OIDC using a role you provide: + +```yaml +name: Check AWS Terraform Code + +on: + pull_request: + types: [ opened, reopened, synchronize, ready_for_review ] + branches: [ main ] + +permissions: + id-token: write + contents: read + +jobs: + check: + name: "Check AWS Terraform Code" + permissions: + contents: read + id-token: write + uses: launchbynttdata/launch-workflows/.github/workflows/reusable-terraform-check.yml@ref + with: + auth_method: "aws" + assume_role_arn: "arn:aws:iam::123456789012:role/my-assumed-role" # optional, falls back to TERRAFORM_CHECK_AWS_ASSUME_ROLE_ARN variable + region: "us-east-2" # optional, falls back to TERRAFORM_CHECK_AWS_REGION variable +``` + +Replace `ref` with an appropriate ref to this repository, and replace `assume_role_arn` and `region` with values of your choosing. If either is not provided, they fall back to the `TERRAFORM_CHECK_AWS_ASSUME_ROLE_ARN` and `TERRAFORM_CHECK_AWS_REGION` variables set at the repository, organization, or enterprise level. + +### Azure authentication (`auth_method: azure`) + +Use this when your module deploys example resources to Azure. Authentication is performed via OIDC using Azure credentials passed as secrets: + +```yaml +name: Check Azure Terraform Code + +on: + pull_request: + types: [ opened, reopened, synchronize, ready_for_review ] + branches: [ main ] + +permissions: + id-token: write + contents: read + +jobs: + check: + name: "Check Azure Terraform Code" + permissions: + contents: read + id-token: write + uses: launchbynttdata/launch-workflows/.github/workflows/reusable-terraform-check.yml@ref + with: + auth_method: "azure" + secrets: inherit # pragma: allowlist secret + + # For usage outside the launchbynttdata organization, pass the secrets explicitly: + # secrets: + # TERRAFORM_CHECK_AZURE_CLIENT_ID: ${{ secrets.your_azure_client_id_secret }} + # TERRAFORM_CHECK_AZURE_TENANT_ID: ${{ secrets.your_azure_tenant_id_secret }} + # TERRAFORM_CHECK_AZURE_SUBSCRIPTION_ID: ${{ secrets.your_azure_subscription_id_secret }} +``` + +Replace `ref` with an appropriate ref to this repository. For more information on OIDC setup for Azure, see the [azure/login action documentation](https://github.com/Azure/login?tab=readme-ov-file#login-with-openid-connect-oidc-recommended). + +## Inputs + +| Name | Description | Required | Default | +|------|-------------|----------|---------| +| `auth_method` | Authentication method to use. Valid values are `none`, `aws`, and `azure`. | Yes | — | +| `assume_role_arn` | ARN of the IAM role to assume. Required when `auth_method` is `aws`. | No | `${{ vars.TERRAFORM_CHECK_AWS_ASSUME_ROLE_ARN }}` | +| `region` | AWS region to use when deploying test resources. Only used when `auth_method` is `aws`. | No | `${{ vars.TERRAFORM_CHECK_AWS_REGION }}` | + +## Secrets + +| Name | Description | Required | +|------|-------------|----------| +| `TERRAFORM_CHECK_AZURE_CLIENT_ID` | Azure client ID for OIDC authentication. Required when `auth_method` is `azure`. | No* | +| `TERRAFORM_CHECK_AZURE_TENANT_ID` | Azure tenant ID for OIDC authentication. Required when `auth_method` is `azure`. | No* | +| `TERRAFORM_CHECK_AZURE_SUBSCRIPTION_ID` | Azure subscription ID for OIDC authentication. Required when `auth_method` is `azure`. | No* | + +*These secrets are declared as optional at the workflow level to allow reuse in non-Azure contexts, but the workflow will fail at the `validate-inputs` job if any of them are missing when `auth_method` is `azure`. + +## Migrating from the provider-specific workflows + +If you are currently using `reusable-terraform-check-aws.yml`, replace the `uses` reference and add `auth_method: "aws"`: + +```yaml +# Before +uses: launchbynttdata/launch-workflows/.github/workflows/reusable-terraform-check-aws.yml@ref +with: + assume_role_arn: "arn:aws:iam::123456789012:role/my-assumed-role" + +# After +uses: launchbynttdata/launch-workflows/.github/workflows/reusable-terraform-check.yml@ref +with: + auth_method: "aws" + assume_role_arn: "arn:aws:iam::123456789012:role/my-assumed-role" +``` + +If you are currently using `reusable-terraform-check-azure.yml`, replace the `uses` reference and add `auth_method: "azure"`: + +```yaml +# Before +uses: launchbynttdata/launch-workflows/.github/workflows/reusable-terraform-check-azure.yml@ref +secrets: inherit # pragma: allowlist secret + +# After +uses: launchbynttdata/launch-workflows/.github/workflows/reusable-terraform-check.yml@ref +with: + auth_method: "azure" +secrets: inherit # pragma: allowlist secret +``` + +For our Terraform modules, the `auth_method` can be derived automatically from the repository name: repositories prefixed with `tf-aws` use AWS authentication, those prefixed with `tf-az` use Azure authentication, and all others use no authentication: + +```yaml +uses: launchbynttdata/launch-workflows/.github/workflows/reusable-terraform-check.yml@ref +with: + auth_method: ${{ startsWith(github.event.repository.name, 'tf-aws') && 'aws' || startsWith(github.event.repository.name, 'tf-az') && 'azure' || 'none' }} +secrets: inherit # pragma: allowlist secret +``` + diff --git a/docs/reusable-terragrunt-deploy.md b/docs/reusable-terragrunt-deploy.md new file mode 100644 index 0000000..c5b1497 --- /dev/null +++ b/docs/reusable-terragrunt-deploy.md @@ -0,0 +1,266 @@ +# Terragrunt Environment Deployment + +Plans and deploys a single Terragrunt environment. This workflow unifies the previously separate `reusable-terragrunt-deploy-aws.yml` and `reusable-terragrunt-deploy-azure.yml` workflows into a single, provider-agnostic workflow controlled by the `auth_method` input. + +`auth_method` accepts a comma-delimited list of authentication methods, enabling multiple providers to be authenticated simultaneously (e.g. `"aws,github"` to authenticate with AWS via OIDC while also generating a GitHub App token for private module access). It can also be omitted or set to an empty string to run Terragrunt without any authentication. + +## Usage + +### AWS authentication (`auth_method: aws`) + +Use this when your environment is deployed to AWS. Authentication is performed via OIDC using a role you provide (or set the `DEPLOY_ROLE_ARN` variable on your environment or repository). + +Let's say we have a repository with the structure shown below: + +> ./ +> platform/ +> sandbox/ +> us-east-2/ +> 000/ +> test/ +> us-east-2/ +> 000/ +> production/ +> us-east-2/ +> 000/ + +If we wanted to deploy our production environment when a release was published, we might create a workflow like so: + +```yaml +name: Release to Production + +on: + release: + types: + - published + +jobs: + deploy-production: + permissions: + contents: read + id-token: write + statuses: write + uses: launchbynttdata/launch-workflows/.github/workflows/reusable-terragrunt-deploy.yml@ref + with: + auth_method: "aws" + git_branch: ${{ github.head_ref }} + tf_version: '1.5.5' + tg_version: '0.54.11' + gh_environment: 'production' + tg_dir: 'platform/production/us-east-2/000' + aws_auth_region: 'us-east-2' + aws_assume_role_arn: 'arn:aws:iam::123456789012:role/my-assumed-role' +``` + +If you wanted to deploy all regions and instances within a given environment -- say you had production resources that were in us-east-1 and us-east-2 -- then you can utilize GitHub's matrix functionality and our [workflow to create a matrix for Terragrunt](./reusable-github-matrix-tg.md). + +### Azure authentication (`auth_method: azure`) + +Use this when your environment is deployed to Azure. Authentication is performed via OIDC using Azure credentials passed as secrets. + +Let's say we have a repository with the structure shown below: + +> ./ +> platform/ +> sandbox/ +> eastus2/ +> 000/ +> test/ +> eastus2/ +> 000/ +> production/ +> eastus2/ +> 000/ + +If we wanted to deploy our production environment when a release was published, we might create a workflow like so: + +```yaml +name: Release to Production + +on: + release: + types: + - published + +jobs: + deploy-production: + permissions: + contents: read + id-token: write + statuses: write + uses: launchbynttdata/launch-workflows/.github/workflows/reusable-terragrunt-deploy.yml@ref + with: + auth_method: "azure" + git_branch: ${{ github.head_ref }} + tf_version: '1.5.5' + tg_version: '0.77.22' + gh_environment: 'production' + tg_dir: 'platform/production/eastus2/000' + secrets: inherit # pragma: allowlist secret + + # For usage outside the launchbynttdata organization, pass the secrets explicitly: + # secrets: + # TERRAGRUNT_DEPLOY_AZURE_CLIENT_ID: ${{ secrets.your_azure_client_id_secret }} + # TERRAGRUNT_DEPLOY_AZURE_TENANT_ID: ${{ secrets.your_azure_tenant_id_secret }} + # TERRAGRUNT_DEPLOY_AZURE_SUBSCRIPTION_ID: ${{ secrets.your_azure_subscription_id_secret }} +``` + +If you wanted to deploy all regions and instances within a given environment -- say you had production resources that were in eastus and eastus2 -- then you can utilize GitHub's matrix functionality and our [workflow to create a matrix for Terragrunt](./reusable-github-matrix-tg.md). For more information on OIDC setup for Azure, see the [azure/login action documentation](https://github.com/Azure/login?tab=readme-ov-file#login-with-openid-connect-oidc-recommended). + +### GitHub App authentication (`auth_method: github`) + +Use this when your Terragrunt configuration needs to apply changes via the GitHub Terraform Provider. Authentication is performed using a GitHub App, which generates a short-lived token: + +```yaml +name: Release to Production + +on: + release: + types: + - published + +jobs: + deploy-production: + permissions: + contents: read + id-token: write + statuses: write + uses: launchbynttdata/launch-workflows/.github/workflows/reusable-terragrunt-deploy.yml@ref + with: + auth_method: "github" + git_branch: ${{ github.head_ref }} + tf_version: '1.5.5' + tg_version: '0.54.11' + gh_environment: 'production' + tg_dir: 'platform/production/us-east-2/000' + github_app_id: "123456" + secrets: + TERRAGRUNT_DEPLOY_GITHUB_APP_SECRET: ${{ secrets.MY_GITHUB_APP_PRIVATE_KEY }} +``` + +Replace `ref` with an appropriate ref to this repository, and replace the `TERRAGRUNT_DEPLOY_GITHUB_APP_ID` input and `TERRAGRUNT_DEPLOY_GITHUB_APP_SECRET` secret with values for your GitHub App. + +Note that this does not perform the checkout with the obtained GitHub token, it is only used in the context of applying GitHub resources with Terragrunt! + +### Combined authentication (e.g. `auth_method: "aws,github"`) + +Use this when your infrastructure is deployed across multiple Terraform providers. The example below might deploy a new GitHub repository (authenticating as a GitHub App to use the GitHub API) and then stand up some additional resources for that repository in AWS. Both AWS OIDC and a GitHub App token will be configured: + +```yaml +name: Release to Production + +on: + release: + types: + - published + +jobs: + deploy-production: + permissions: + contents: read + id-token: write + statuses: write + uses: launchbynttdata/launch-workflows/.github/workflows/reusable-terragrunt-deploy.yml@ref + with: + auth_method: "aws,github" + git_branch: ${{ github.head_ref }} + tf_version: '1.5.5' + tg_version: '0.54.11' + gh_environment: 'production' + tg_dir: 'platform/production/us-east-2/000' + aws_auth_region: 'us-east-2' + aws_assume_role_arn: 'arn:aws:iam::123456789012:role/my-assumed-role' + github_app_id: "123456" + secrets: + TERRAGRUNT_DEPLOY_GITHUB_APP_SECRET: ${{ secrets.MY_GITHUB_APP_PRIVATE_KEY }} +``` + +## Inputs + +| Name | Description | Required | Default | +|------|-------------|----------|---------| +| `auth_method` | Comma-delimited list of authentication methods to use. Valid values are `aws`, `azure`, and `github` (e.g. `"aws"`, `"aws,github"`, `"aws,azure"`). Omit or pass an empty string to run without authentication. | No | `""` | +| `git_branch` | Branch triggering this deployment. | Yes | — | +| `tf_version` | Version of Terraform to utilize. | Yes | `1.5.5` | +| `tg_version` | Version of Terragrunt to utilize. | Yes | — | +| `gh_environment` | GitHub Environment to deploy to (e.g. test, production). | No | — | +| `tg_dir` | Folder containing the Terragrunt configuration to deploy (relative to the repository root). | Yes | — | +| `aws_auth_region` | AWS region to use for authentication. Required when `auth_method` includes `aws`. | No | — | +| `aws_assume_role_arn` | ARN of the role to assume prior to Terragrunt invocation. Required when `auth_method` includes `aws`. Defaults to `vars.DEPLOY_ROLE_ARN`. | No | `${{ vars.DEPLOY_ROLE_ARN }}` | +| `github_app_id` | GitHub App ID for authentication. Required when `auth_method` includes `github`. Defaults to `vars.TERRAGRUNT_DEPLOY_GITHUB_APP_ID`. | No | `${{ vars.TERRAGRUNT_DEPLOY_GITHUB_APP_ID }}` | +| `before_plan_commands` | Commands to run prior to executing Terragrunt plan. | No | `""` | +| `before_deploy_commands` | Commands to run prior to executing Terragrunt apply. | No | `""` | +| `before_shared_commands` | Commands to run prior to both Terragrunt plan and apply (after the specific before commands). | No | `""` | +| `after_plan_commands` | Commands to run after executing Terragrunt plan. | No | `""` | +| `after_deploy_commands` | Commands to run after executing Terragrunt apply. | No | `""` | +| `after_shared_commands` | Commands to run after both Terragrunt plan and apply (after the specific after commands). | No | `""` | + +## Outputs + +| Name | Description | +|------|-------------| +| `terraform_outputs` | JSON string containing all Terraform outputs from the deployment (base64 encoded). | + +## Secrets + +| Name | Description | Required | +|------|-------------|----------| +| `TERRAGRUNT_DEPLOY_AZURE_CLIENT_ID` | Azure client ID for OIDC authentication. Required when `auth_method` includes `azure`. | No* | +| `TERRAGRUNT_DEPLOY_AZURE_TENANT_ID` | Azure tenant ID for OIDC authentication. Required when `auth_method` includes `azure`. | No* | +| `TERRAGRUNT_DEPLOY_AZURE_SUBSCRIPTION_ID` | Azure subscription ID for OIDC authentication. Required when `auth_method` includes `azure`. | No* | +| `TERRAGRUNT_DEPLOY_GITHUB_APP_SECRET` | GitHub App private key for authentication. Required when `auth_method` includes `github`. | No* | + +*These secrets are declared as optional at the workflow level to allow reuse across different auth methods, but the workflow will fail at the `validate-inputs` job if the required secrets for the selected `auth_method` are missing. + +## Migrating from the provider-specific workflows + +If you are currently using `reusable-terragrunt-deploy-aws.yml`, replace the `uses` reference and add `auth_method: "aws"`. Several other inputs have been consolidated down to a `tg_dir` input: + +```yaml +# Before +uses: launchbynttdata/launch-workflows/.github/workflows/reusable-terragrunt-deploy-aws.yml@ref +with: + git_branch: ${{ github.head_ref }} + tf_version: '1.5.5' + tg_version: '0.54.11' + environment: 'production' + region: 'us-east-2' + env_id: '000' + +# After +uses: launchbynttdata/launch-workflows/.github/workflows/reusable-terragrunt-deploy.yml@ref +with: + auth_method: "aws" + git_branch: ${{ github.head_ref }} + tf_version: '1.5.5' + tg_version: '0.54.11' + gh_environment: 'production' + tg_dir: 'platform/production/us-east-2/000' + aws_auth_region: 'us-east-2' +``` + +If you are currently using `reusable-terragrunt-deploy-azure.yml`, replace the `uses` reference and add `auth_method: "azure"`. Several other inputs have been consolidated down to a `tg_dir` input: + +```yaml +# Before +uses: launchbynttdata/launch-workflows/.github/workflows/reusable-terragrunt-deploy-azure.yml@ref +with: + git_branch: ${{ github.head_ref }} + tf_version: '1.5.5' + tg_version: '0.77.22' + environment: 'production' + region: 'eastus2' + env_id: '000' +secrets: inherit # pragma: allowlist secret + +# After +uses: launchbynttdata/launch-workflows/.github/workflows/reusable-terragrunt-deploy.yml@ref +with: + auth_method: "azure" + git_branch: ${{ github.head_ref }} + tf_version: '1.5.5' + tg_version: '0.77.22' + gh_environment: 'production' + tg_dir: 'platform/production/eastus2/000' +secrets: inherit # pragma: allowlist secret +``` diff --git a/docs/reusable-terragrunt-plan-only.md b/docs/reusable-terragrunt-plan-only.md new file mode 100644 index 0000000..49ff342 --- /dev/null +++ b/docs/reusable-terragrunt-plan-only.md @@ -0,0 +1,284 @@ +# Plan a Terragrunt Environment + +Plans a single Terragrunt environment. This workflow unifies the previously separate `reusable-terragrunt-plan-only-aws.yml` and `reusable-terragrunt-plan-only-azure.yml` workflows into a single, provider-agnostic workflow controlled by the `auth_method` input. + +`auth_method` accepts a comma-delimited list of authentication methods, enabling multiple providers to be authenticated simultaneously (e.g. `"aws,github"` to authenticate with AWS via OIDC while also generating a GitHub App token for private module access). It can also be omitted or set to an empty string to run Terragrunt without any authentication. + +Rather than utilizing a GitHub Environment (which may require approval for this plan-only scenario), this workflow takes authentication credentials directly. The typical use case for this workflow is to perform a plan against an upper environment, e.g. production, when the code is being PRed. + +## Usage + +### AWS authentication (`auth_method: aws`) + +Use this when your environment is deployed to AWS. Authentication is performed via OIDC using a role you provide: + +```yaml +name: Plan Production Environment + +on: + pull_request: + types: [opened, synchronize, reopened] + branches: [ "**" ] + +jobs: + get-tg-versions: + permissions: + contents: read + uses: launchbynttdata/launch-workflows/.github/workflows/reusable-terragrunt-versions.yml@ref + + build-matrix: + permissions: + contents: read + uses: launchbynttdata/launch-workflows/.github/workflows/reusable-github-matrix-tg.yml@ref + with: + platform_environment: production + + call-terragrunt-plan: + needs: [get-tg-versions, build-matrix] + permissions: + contents: read + id-token: write + statuses: write + strategy: + fail-fast: false + matrix: ${{ fromJson(needs.build-matrix.outputs.matrix) }} + + uses: launchbynttdata/launch-workflows/.github/workflows/reusable-terragrunt-plan-only.yml@ref + with: + auth_method: "aws" + git_branch: ${{ github.head_ref }} + tf_version: ${{ needs.get-tg-versions.outputs.tf_version }} + tg_version: ${{ needs.get-tg-versions.outputs.tg_version }} + tg_dir: "platform/${{ matrix.terragrunt_environment.environment }}/${{ matrix.terragrunt_environment.region }}/${{ matrix.terragrunt_environment.instance }}" + aws_auth_region: "us-east-2" + aws_assume_role_arn: "arn:aws:iam::123456789012:role/my-assumed-role" +``` + +Replace `ref` with an appropriate ref to this repository, and replace the `assume_role_arn` and `auth_region` inputs with values of your choosing. + +### Azure authentication (`auth_method: azure`) + +Use this when your environment is deployed to Azure. Authentication is performed via OIDC using Azure credentials passed as secrets: + +```yaml +name: Plan Production Environment + +on: + pull_request: + types: [opened, synchronize, reopened] + branches: [ "**" ] + +jobs: + get-tg-versions: + permissions: + contents: read + uses: launchbynttdata/launch-workflows/.github/workflows/reusable-terragrunt-versions.yml@ref + + build-matrix: + permissions: + contents: read + uses: launchbynttdata/launch-workflows/.github/workflows/reusable-github-matrix-tg.yml@ref + with: + platform_environment: production + + call-terragrunt-plan: + needs: [get-tg-versions, build-matrix] + permissions: + contents: read + id-token: write + statuses: write + strategy: + fail-fast: false + matrix: ${{ fromJson(needs.build-matrix.outputs.matrix) }} + + uses: launchbynttdata/launch-workflows/.github/workflows/reusable-terragrunt-plan-only.yml@ref + with: + auth_method: "azure" + git_branch: ${{ github.head_ref }} + tf_version: ${{ needs.get-tg-versions.outputs.tf_version }} + tg_version: ${{ needs.get-tg-versions.outputs.tg_version }} + tg_dir: "platform/${{ matrix.terragrunt_environment.environment }}/${{ matrix.terragrunt_environment.region }}/${{ matrix.terragrunt_environment.instance }}" + secrets: inherit # pragma: allowlist secret + + # For usage outside the launchbynttdata organization, pass the secrets explicitly: + # secrets: + # TERRAGRUNT_DEPLOY_AZURE_CLIENT_ID: ${{ secrets.your_azure_client_id_secret }} + # TERRAGRUNT_DEPLOY_AZURE_TENANT_ID: ${{ secrets.your_azure_tenant_id_secret }} + # TERRAGRUNT_DEPLOY_AZURE_SUBSCRIPTION_ID: ${{ secrets.your_azure_subscription_id_secret }} +``` + +Replace `ref` with an appropriate ref to this repository. For more information on OIDC setup for Azure, see the [azure/login action documentation](https://github.com/Azure/login?tab=readme-ov-file#login-with-openid-connect-oidc-recommended). + +### GitHub App authentication (`auth_method: github`) + +Use this when your Terragrunt configuration needs to plan changes via the GitHub Terraform Provider. Authentication is performed using a GitHub App, which generates a short-lived token: + +```yaml +name: Plan Production Environment + +on: + pull_request: + types: [opened, synchronize, reopened] + branches: [ "**" ] + +jobs: + get-tg-versions: + permissions: + contents: read + uses: launchbynttdata/launch-workflows/.github/workflows/reusable-terragrunt-versions.yml@ref + + build-matrix: + permissions: + contents: read + uses: launchbynttdata/launch-workflows/.github/workflows/reusable-github-matrix-tg.yml@ref + with: + platform_environment: production + + call-terragrunt-plan: + needs: [get-tg-versions, build-matrix] + permissions: + contents: read + id-token: write + statuses: write + strategy: + fail-fast: false + matrix: ${{ fromJson(needs.build-matrix.outputs.matrix) }} + + uses: launchbynttdata/launch-workflows/.github/workflows/reusable-terragrunt-plan-only.yml@ref + with: + auth_method: "github" + git_branch: ${{ github.head_ref }} + tf_version: ${{ needs.get-tg-versions.outputs.tf_version }} + tg_version: ${{ needs.get-tg-versions.outputs.tg_version }} + tg_dir: "platform/${{ matrix.terragrunt_environment.environment }}/${{ matrix.terragrunt_environment.region }}/${{ matrix.terragrunt_environment.instance }}" + github_app_id: "123456" + secrets: + TERRAGRUNT_DEPLOY_GITHUB_APP_SECRET: ${{ secrets.MY_GITHUB_APP_PRIVATE_KEY }} +``` + +Replace `ref` with an appropriate ref to this repository, and replace the `github_app_id` input and `TERRAGRUNT_DEPLOY_GITHUB_APP_SECRET` secret with values for your GitHub App. + +Note that this does not perform the checkout with the obtained GitHub token, it is only used in the context of planning GitHub resources managed with Terragrunt! + +### Combined authentication (e.g. `auth_method: "aws,azure"`) + +Use this when your infrastructure is deployed across AWS and Azure within a single Terragrunt environment: + +```yaml +name: Plan Production Environment + +on: + pull_request: + types: [opened, synchronize, reopened] + branches: [ "**" ] + +jobs: + get-tg-versions: + permissions: + contents: read + uses: launchbynttdata/launch-workflows/.github/workflows/reusable-terragrunt-versions.yml@ref + + build-matrix: + permissions: + contents: read + uses: launchbynttdata/launch-workflows/.github/workflows/reusable-github-matrix-tg.yml@ref + with: + platform_environment: production + + call-terragrunt-plan: + needs: [get-tg-versions, build-matrix] + permissions: + contents: read + id-token: write + statuses: write + strategy: + fail-fast: false + matrix: ${{ fromJson(needs.build-matrix.outputs.matrix) }} + + uses: launchbynttdata/launch-workflows/.github/workflows/reusable-terragrunt-plan-only.yml@ref + with: + auth_method: "aws,azure" + git_branch: ${{ github.head_ref }} + tf_version: ${{ needs.get-tg-versions.outputs.tf_version }} + tg_version: ${{ needs.get-tg-versions.outputs.tg_version }} + tg_dir: "platform/${{ matrix.terragrunt_environment.environment }}/${{ matrix.terragrunt_environment.region }}/${{ matrix.terragrunt_environment.instance }}" + aws_auth_region: "us-east-2" + aws_assume_role_arn: "arn:aws:iam::123456789012:role/my-assumed-role" + secrets: + TERRAGRUNT_DEPLOY_AZURE_CLIENT_ID: ${{ secrets.your_azure_client_id_secret }} + TERRAGRUNT_DEPLOY_AZURE_TENANT_ID: ${{ secrets.your_azure_tenant_id_secret }} + TERRAGRUNT_DEPLOY_AZURE_SUBSCRIPTION_ID: ${{ secrets.your_azure_subscription_id_secret }} +``` + +Replace `ref` with an appropriate ref to this repository, and replace the `aws_assume_role_arn`, `aws_auth_region`, and `TERRAGRUNT_DEPLOY_AZURE_*` secrets with your own. + +## Inputs + +| Name | Description | Required | Default | +|------|-------------|----------|---------| +| `auth_method` | Comma-delimited list of authentication methods to use. Valid values are `aws`, `azure`, and `github` (e.g. `"aws"`, `"aws,github"`, `"aws,azure"`). Omit or pass an empty string to run without authentication. | No | `""` | +| `git_branch` | Branch triggering this plan. | Yes | — | +| `tf_version` | Version of Terraform to utilize. | Yes | `1.5.5` | +| `tg_version` | Version of Terragrunt to utilize. | Yes | — | +| `tg_dir` | Folder containing the Terragrunt configuration to plan (relative to the repository root). | Yes | — | +| `aws_auth_region` | AWS region to use for authentication. Required when `auth_method` includes `aws`. | No | — | +| `aws_assume_role_arn` | ARN of the role to assume prior to Terragrunt invocation. Required when `auth_method` includes `aws`. | No | — | +| `github_app_id` | GitHub App ID for authentication. Required when `auth_method` includes `github`. Defaults to `vars.TERRAGRUNT_DEPLOY_GITHUB_APP_ID`. | No | `${{ vars.TERRAGRUNT_DEPLOY_GITHUB_APP_ID }}` | +| `before_plan_commands` | Commands to run prior to executing Terragrunt plan. | No | `""` | +| `after_plan_commands` | Commands to run after executing Terragrunt plan. | No | `""` | + +## Secrets + +| Name | Description | Required | +|------|-------------|----------| +| `TERRAGRUNT_DEPLOY_AZURE_CLIENT_ID` | Azure client ID for OIDC authentication. Required when `auth_method` includes `azure`. | No* | +| `TERRAGRUNT_DEPLOY_AZURE_TENANT_ID` | Azure tenant ID for OIDC authentication. Required when `auth_method` includes `azure`. | No* | +| `TERRAGRUNT_DEPLOY_AZURE_SUBSCRIPTION_ID` | Azure subscription ID for OIDC authentication. Required when `auth_method` includes `azure`. | No* | +| `TERRAGRUNT_DEPLOY_GITHUB_APP_SECRET` | GitHub App private key for authentication. Required when `auth_method` includes `github`. | No* | + +*These secrets are declared as optional at the workflow level to allow reuse across different auth methods, but the workflow will fail at the `validate-inputs` job if the required secrets for the selected `auth_method` are missing. + +## Migrating from the provider-specific workflows + +If you are currently using `reusable-terragrunt-plan-only-aws.yml`, replace the `uses` reference and add `auth_method: "aws"`. Several other inputs have been consolidated down to a `tg_dir` input: + +```yaml +# Before +uses: launchbynttdata/launch-workflows/.github/workflows/reusable-terragrunt-plan-only-aws.yml@ref +with: + git_branch: ${{ github.head_ref }} + assume_role_arn: "arn:aws:iam::123456789012:role/my-assumed-role" + environment: production + region: us-east-2 + env_id: "000" + +# After +uses: launchbynttdata/launch-workflows/.github/workflows/reusable-terragrunt-plan-only.yml@ref +with: + auth_method: "aws" + git_branch: ${{ github.head_ref }} + tg_dir: "platform/production/us-east-2/000" + aws_auth_region: "us-east-2" + aws_assume_role_arn: "arn:aws:iam::123456789012:role/my-assumed-role" +``` + +If you are currently using `reusable-terragrunt-plan-only-azure.yml`, replace the `uses` reference and add `auth_method: "azure"`. Several other inputs have been consolidated down to a `tg_dir` input: + +```yaml +# Before +uses: launchbynttdata/launch-workflows/.github/workflows/reusable-terragrunt-plan-only-azure.yml@ref +with: + git_branch: ${{ github.head_ref }} + environment: production + region: eastus2 + env_id: "000" +secrets: inherit # pragma: allowlist secret + +# After +uses: launchbynttdata/launch-workflows/.github/workflows/reusable-terragrunt-plan-only.yml@ref +with: + auth_method: "azure" + git_branch: ${{ github.head_ref }} + tg_dir: "platform/production/eastus2/000" +secrets: inherit # pragma: allowlist secret +``` diff --git a/docs/reusable-update-from-skeleton.md b/docs/reusable-update-from-skeleton.md new file mode 100644 index 0000000..4b04a49 --- /dev/null +++ b/docs/reusable-update-from-skeleton.md @@ -0,0 +1,73 @@ +# Update Repository from Skeleton + +Keeps a repository in sync with its upstream [Copier](https://copier.readthedocs.io/) skeleton template. When triggered, the workflow runs `copier update` (or `copier recopy`) against the repository, detects changes, validates them with pre-commit, and opens a pull request with the result. + +```mermaid +flowchart TD + A[Workflow Triggered] --> B[Run copier update/recopy] + B --> C{Changes detected?} + C -- No --> D[Done] + C -- Yes --> E{Pre-commit passes?} + E -- Yes --> F[Create PR with auto-merge] + E -- No --> G[Create PR requiring manual review] +``` + +## Behavior + +1. **Copier update** — Runs `copier update --defaults --trust` to pull the latest changes from the skeleton template. If the repository's `prerelease` custom property is `true`, the `--prerelease` flag is added to pick up prerelease skeleton versions. +2. **Recopy mode** — When `recopy` is set to `true`, runs `copier recopy --defaults --trust --overwrite` instead, which overwrites all templated files. Recopy PRs always require manual review. +3. **Pre-commit validation** — If a `.pre-commit-config.yaml` exists, runs `check-merge-conflict` against all files to detect merge conflict markers. +4. **Pull request creation**: + - If pre-commit passes and recopy is not enabled, a PR titled `chore: update from skeleton` is created with auto-merge enabled. + - If pre-commit fails or recopy is enabled, a PR titled `fix: update from skeleton` is created and flagged for manual review. Any files with merge conflict markers are listed in the PR body. + +## Usage + +Add the following workflow to your repository (suggested name: `.github/workflows/update-from-skeleton.yml`): + +```yaml +name: Update from Skeleton + +on: + schedule: + - cron: "0 6 * * 1" # Weekly on Monday at 6:00 UTC + workflow_dispatch: + inputs: + recopy: + description: "Perform a full recopy instead of an incremental update" + type: boolean + default: false + +permissions: + contents: write + pull-requests: write + +jobs: + update-from-skeleton: + name: Update from Skeleton + permissions: + contents: write + pull-requests: write + uses: launchbynttdata/launch-workflows/.github/workflows/reusable-update-from-skeleton.yml@ref + with: + recopy: ${{ github.event.inputs.recopy == 'true' }} + secrets: inherit +``` + +Be sure you replace `ref` with an appropriate ref to this repository. + +## Inputs + +| Input | Description | Required | Default | +|-------|-------------|----------|---------| +| `recopy` | Perform a full recopy instead of an incremental update. Overwrites all templated files and re-runs tasks defined in `copier.yml`. The resulting PR will require manual review. | No | `false` | +| `skeleton_update_app_id` | The GitHub App ID of the app used for authentication when pushing updates. | No | `${{ vars.LAUNCH_SKELETON_UPDATE_APP_ID }}` | + +## Required Secrets and Variables + +| Name | Type | Description | +|------|------|-------------| +| `LAUNCH_SKELETON_UPDATE_APP_ID` | Variable | The GitHub App ID for the skeleton update app. | +| `LAUNCH_SKELETON_UPDATE_KEY` | Secret | The private key for the skeleton update app. | + +The GitHub App must be installed on the repository with permissions to read and write code and create pull requests.