diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000..6415599 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,35 @@ +// For format details, see https://aka.ms/devcontainer.json. For config options, see the +// README at: https://github.com/devcontainers/templates/tree/main/src/ubuntu +{ + "name": "Ubuntu", + // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile + "image": "mcr.microsoft.com/devcontainers/base:jammy", + "features": { + "ghcr.io/devcontainers/features/azure-cli:1": {}, + "ghcr.io/devcontainers/features/terraform:1": {}, + "ghcr.io/devcontainers-contrib/features/actionlint:1": {}, + "ghcr.io/devcontainers-contrib/features/terramate:1": { + "version": "v0.4.2" + }, + "ghcr.io/dhoeric/features/terraform-docs:1": {}, + "ghcr.io/guiyomh/features/vim:0": {} + }, + "customizations": { + "vscode": { + "extensions": [ + "github.vscode-github-actions", + "mineiros.terramate" + ] + } + } + // Features to add to the dev container. More info: https://containers.dev/features. + // "features": {}, + // Use 'forwardPorts' to make a list of ports inside the container available locally. + // "forwardPorts": [], + // Use 'postCreateCommand' to run commands after the container is created. + // "postCreateCommand": "uname -a", + // Configure tool-specific properties. + // "customizations": {}, + // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. + // "remoteUser": "root" +} \ No newline at end of file diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..de60e09 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,11 @@ +# Set update schedule for GitHub Actions + +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + # Check for updates to GitHub Actions every week + interval: "weekly" + reviewers: + - m4s-b3n diff --git a/.github/workflows/test-and-release.yml b/.github/workflows/test-and-release.yml new file mode 100644 index 0000000..b05e550 --- /dev/null +++ b/.github/workflows/test-and-release.yml @@ -0,0 +1,143 @@ +name: "Test & Release" + +on: + push: + branches: ["main"] + pull_request: + branches: ["main"] + +permissions: + id-token: write + contents: write + pull-requests: write + +jobs: + test-setup: + name: Test Setup + runs-on: ubuntu-latest + strategy: + matrix: + terramate-version: ["0.4.2", "latest"] + terraform-version: ["1.5.7", "latest"] + plan-file: ["out.tfplan", "plan.out"] + tmp-file: ["tmp.txt", "file.tmp"] + comment-pull-request: ["true", "false"] + cleanup: ["true", "false"] + fail-fast: true + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + ref: ${{ github.head_ref }} + fetch-depth: 0 + - name: Test + uses: "./" + with: + terraform-version: ${{ matrix.terraform-version }} + plan-file: ${{ matrix.plan-file }} + github-token: ${{ secrets.GITHUB_TOKEN }} + + test-args: + name: Test Arguments + runs-on: ubuntu-latest + strategy: + matrix: + terramate-args: ["--changed", ""] + terraform-common-args: ["-no-color", ""] + terraform-init-args: ["-upgrade", ""] + terraform-validate-args: ["-json", ""] + terraform-plan-args: ["-parallelism=3", ""] + terraform-apply-args: ["-parallelism=2", ""] + terraform-destroy-args: ["-compact-warnings", ""] + fail-fast: true + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + ref: ${{ github.head_ref }} + fetch-depth: 0 + - name: Test + uses: "./" + with: + terramate-args: ${{ matrix.terramate-args }} + terraform-common-args: ${{ matrix.terraform-common-args }} + terraform-init-args: ${{ matrix.terraform-init-args }} + terraform-validate-args: ${{ matrix.terraform-validate-args }} + terraform-plan-args: ${{ matrix.terraform-plan-args }} + terraform-apply-args: ${{ matrix.terraform-apply-args }} + terraform-destroy-args: ${{ matrix.terraform-destroy-args }} + apply: true + destroy: true + github-token: ${{ secrets.GITHUB_TOKEN }} + + test-jobs: + name: Test Jobs + runs-on: ubuntu-latest + strategy: + matrix: + lint: ["true", "false"] + generate: ["true", "false"] + validate: ["true", "false"] + plan: ["true", "false"] + apply: ["true", "false"] + destroy: ["true", "false"] + fail-fast: true + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + ref: ${{ github.head_ref }} + fetch-depth: 0 + - name: Test With Setup And Init + uses: "./" + with: + lint: ${{ matrix.lint }} + validate: ${{ matrix.validate }} + plan: ${{ matrix.plan }} + apply: ${{ matrix.apply }} + destroy: ${{ matrix.destroy }} + github-token: ${{ secrets.GITHUB_TOKEN }} + - name: Test With Init Only + uses: "./" + with: + setup: false + init: true + lint: ${{ matrix.lint }} + validate: ${{ matrix.validate }} + plan: ${{ matrix.plan }} + apply: ${{ matrix.apply }} + destroy: ${{ matrix.destroy }} + github-token: ${{ secrets.GITHUB_TOKEN }} + - name: Test Without Setup And Init + uses: "./" + with: + setup: false + init: false + lint: ${{ matrix.lint }} + validate: ${{ matrix.validate }} + plan: ${{ matrix.plan }} + apply: ${{ matrix.apply }} + destroy: ${{ matrix.destroy }} + github-token: ${{ secrets.GITHUB_TOKEN }} + + release: + name: Release + needs: + - test-setup + - test-args + - test-jobs + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Install Semantic Release Plugins + run: | + npm install semantic-release-replace-plugin -D + npm install --save-dev semantic-release-major-tag + - name: Semantic Release + uses: cycjimmy/semantic-release-action@v4 + with: + dry_run: ${{ github.event_name == 'pull_request' }} + ci: true + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.gitignore b/.gitignore index 9b8a46e..59919bc 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,10 @@ +# ci +out.tfplan +pr-comment.txt +_terramate_generated_* +.terraform.lock.hcl +.tools-version + # Local .terraform directories **/.terraform/* diff --git a/.releaserc.json b/.releaserc.json new file mode 100644 index 0000000..d4d5842 --- /dev/null +++ b/.releaserc.json @@ -0,0 +1,51 @@ +{ + "branches": [ + "main" + ], + "tagFormat": "v${version}", + "plugins": [ + "@semantic-release/commit-analyzer", + "semantic-release-major-tag", + "@semantic-release/release-notes-generator", + [ + "@semantic-release/changelog", + { + "changelogFile": "CHANGELOG.md" + } + ], + [ + "semantic-release-replace-plugin", + { + "replacements": [ + { + "files": [ + "README.md" + ], + "from": "\"m4s-b3n/terramate-all-in-one@v.*\"", + "to": "\"m4s-b3n/terramate-all-in-one@v${nextRelease.version}\"", + "results": [ + { + "file": "README.md", + "hasChanged": true, + "numMatches": 2, + "numReplacements": 2 + } + ], + "countMatches": true + } + ] + } + ], + [ + "@semantic-release/git", + { + "message": "chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}", + "assets": [ + "CHANGELOG.md", + "README.md" + ] + } + ], + "@semantic-release/github" + ] +} \ No newline at end of file diff --git a/README.md b/README.md index 0d3e1cb..9dc724d 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,85 @@ # terramate-all-in-one + Run common terramate commands in a single GitHub action + +## Usage + +```yaml +name: Terramate Flow + +on: + push: + branches: ["main"] + pull_request: + branches: ["main"] + +permissions: + id-token: write + contents: write + pull-requests: write + +jobs: + plan: + name: Plan + runs-on: ubuntu-latest + if: ${{ github.event_name == 'pull_request' }} + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + ref: ${{ github.head_ref }} + fetch-depth: 0 + - name: Terramate + uses: m4s-b3n/terramate@v0.0.0 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + + apply: + name: Apply + runs-on: ubuntu-latest + if: ${{ github.event_name != 'pull_request' }} + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + ref: ${{ github.head_ref }} + fetch-depth: 0 + - name: Terramate + uses: m4s-b3n/terramate@v0.0.0 + with: + apply: true +``` + +## Inputs + +| Input | Description | Required | Default | +| --- | --- | --- | --- | +| directory | The directory where the Terraform configuration files are located | false | . | +| terramate-version | The version of Terramate to use | false | latest | +| terraform-version | The version of Terraform to use | false | latest | +| terramate-args | Arguments to pass to Terramate | false | --changed | +| terraform-common-args | Arguments to pass to Terraform | false | | +| terraform-init-args | Arguments to pass to Terraform init | false | | +| terraform-validate-args | Arguments to pass to Terraform validate | false | | +| terraform-plan-args | Arguments to pass to Terraform plan | false | | +| terraform-apply-args | Arguments to pass to Terraform apply | false | | +| terraform-destroy-args | Arguments to pass to Terraform destroy | false | | +| terraform-plan-file | Path to Terraform plan file | false | out.tfplan | +| temp-file | Path to preview comment file | false | tmp.txt | +| setup | Setup Terramate and Terraform | false | true | +| init | Initialize Terraform | false | true | +| lint | Run linting | false | true | +| generate | Generate code | false | true | +| validate | Validate Terraform | false | true | +| plan | Plan Terraform | false | true | +| apply | Apply Terraform | false | false | +| destroy | Destroy Terraform | false | false | +| comment-pull-request | Comment on pull requests | false | true | +| github-token | GitHub token required for commenting on pull requests | false | `` | +| cleanup | Cleanup temporary files | false | true | + +>Note: The `github-token` is not marked as required but it is required when the workflow was triggered by a pull request and commenting on pull requests is enabled. + +## Changelog + +See the [Changelog](./CHANGELOG.md) file for details diff --git a/action.yml b/action.yml new file mode 100644 index 0000000..3fc4882 --- /dev/null +++ b/action.yml @@ -0,0 +1,229 @@ +name: "Terramate All-In-One" + +description: "Run common terramate commands in a single GitHub action" + +inputs: + terramate-version: + description: "The version of Terramate to use" + required: false + default: "latest" + terraform-version: + description: "The version of Terraform to use" + required: false + default: "latest" + terramate-args: + description: "Arguments to pass to Terramate" + required: false + default: "--changed" + terraform-common-args: + description: "Arguments to pass to Terraform" + required: false + default: "" + terraform-init-args: + description: "Arguments to pass to Terraform init" + required: false + default: "" + terraform-validate-args: + description: "Arguments to pass to Terraform validate" + required: false + default: "" + terraform-plan-args: + description: "Arguments to pass to Terraform plan" + required: false + default: "" + terraform-apply-args: + description: "Arguments to pass to Terraform apply" + required: false + default: "" + terraform-destroy-args: + description: "Arguments to pass to Terraform destroy" + required: false + default: "" + terraform-plan-file: + description: "Path to Terraform plan file" + required: false + default: "out.tfplan" + temp-file: + description: "Path to preview comment file" + required: false + default: "tmp.txt" + setup: + description: "Setup Terramate and Terraform" + required: false + default: "true" + init: + description: "Initialize Terraform" + required: false + default: "true" + lint: + description: "Run linting" + required: false + default: "true" + generate: + description: "Generate code" + required: false + default: "true" + validate: + description: "Validate Terraform" + required: false + default: "true" + plan: + description: "Plan Terraform" + required: false + default: "true" + apply: + description: "Apply Terraform" + required: false + default: "false" + destroy: + description: "Destroy Terraform" + required: false + default: "false" + comment-pull-request: + description: "Comment on pull requests" + required: false + default: "true" + github-token: + description: "GitHub oken required for commenting on pull requests" + required: false + default: "" + cleanup: + description: "Cleanup temporary files" + required: false + default: "true" + +runs: + using: "composite" + steps: + - name: Setup Terramate and Terraform using asfd + if: ${{ inputs.setup == 'true' }} + uses: asdf-vm/actions/install@v3.0.2 + with: + tool_versions: | + terramate ${{ inputs.terramate-version }} + terraform ${{ inputs.terraform-version }} + - name: Lint + if: ${{ inputs.lint == 'true' }} + run: | + terramate fmt --check + shell: bash + - name: List Affected Stacks + id: list + run: | + echo "stdout<>$GITHUB_OUTPUT + terramate list ${{ inputs.terramate-args }} >>$GITHUB_OUTPUT + EXIT_CODE=$? + echo "STDOUT" >>$GITHUB_OUTPUT + exit ${EXIT_CODE} + shell: bash + - name: Generate Code + if: ${{ inputs.generate == 'true' && steps.list.outputs.stdout != '' }} + run: | + terramate generate ${{ inputs.terramate-args }} + shell: bash + - name: Initialize + if: ${{ inputs.init == 'true' && steps.list.outputs.stdout != '' }} + run: | + terramate run ${{ inputs.terramate-args }} \ + -- terraform init \ + ${{ inputs.terraform-common-args }} \ + ${{ inputs.terraform-init-args }} + shell: bash + - name: Validate + if: ${{ inputs.validate == 'true' && steps.list.outputs.stdout != '' }} + run: | + terramate run ${{ inputs.terramate-args }} \ + -- terraform validate \ + ${{ inputs.terraform-common-args }} \ + ${{ inputs.terraform-validate-args }} + shell: bash + - name: Plan + if: >- + ${{ + ( inputs.plan == 'true' || + inputs.comment-pull-request == 'true' + ) && + steps.list.outputs.stdout != '' }} + run: | + terramate run ${{ inputs.terramate-args }} \ + -- terraform plan \ + ${{ inputs.terraform-common-args }} \ + ${{ inputs.terraform-plan-args }} + -out=${{ inputs.terraform-plan-file }} + shell: bash + - name: Generate Preview Comment + if: >- + ${{ + inputs.comment-pull-request == 'true' && + github.event_name == 'pull_request' + }} + run: | + if [[ -z ${{ steps.list.outputs.stdout }} ]]; then + echo >>${{ inputs.preview-comment-file }} '### No changed stacks.' + cat ${{ inputs.preview-comment-file }} >>$GITHUB_STEP_SUMMARY + else + echo >>${{ inputs.preview-comment-file }} "### List of Changed Stacks" + echo >>${{ inputs.preview-comment-file }} + echo >>${{ inputs.preview-comment-file }} '```bash' + echo >>${{ inputs.preview-comment-file }} "${{ steps.list.outputs.stdout }}" + echo >>${{ inputs.preview-comment-file }} '```' + echo >>${{ inputs.preview-comment-file }} + echo >>${{ inputs.preview-comment-file }} "#### Terraform Plan" + echo >>${{ inputs.preview-comment-file }} + echo >>${{ inputs.preview-comment-file }} '```terraform' + terramate run --changed -- terraform show -no-color ${{ inputs.terraform-plan-file }} 2>&1 | dd bs=1024 count=248 >>${{ inputs.preview-comment-file }} + echo >>${{ inputs.preview-comment-file }} '```' + cat ${{ inputs.preview-comment-file }} >>$GITHUB_STEP_SUMMARY + fi + shell: bash + - name: Publish Plans + if: >- + ${{ + inputs.comment-pull-request == 'true' && + github.event_name == 'pull_request' + }} + uses: marocchino/sticky-pull-request-comment@v2 + with: + GITHUB_TOKEN: ${{ inputs.github-token }} + header: terraform-plan + path: ${{ inputs.preview-comment-file }} + - name: Apply + if: >- + ${{ + inputs.apply == 'true' && + steps.list.outputs.stdout != '' + }} + run: | + if [ -f "${{ inputs.terraform-plan-file }}" ]; then + terramate run ${{ inputs.terramate-args }} \ + -- terraform apply \ + -auto-approve \ + -input=false \ + ${{ inputs.terraform-common-args }} \ + ${{ inputs.terraform-apply-args }} \ + ${{ inputs.terraform-plan-file }} + else + terramate run ${{ inputs.terramate-args }} \ + -- terraform apply \ + -auto-approve \ + -input=false \ + ${{ inputs.terraform-common-args }} \ + ${{ inputs.terraform-apply-args }} + fi + shell: bash + - name: Destroy + if: ${{ inputs.destroy == 'true' && steps.list.outputs.stdout != '' }} + run: | + terramate run ${{ inputs.terramate-args }} \ + -- terraform destroy \ + -auto-approve \ + -input=false \ + ${{ inputs.terraform-common-args }} \ + ${{ inputs.terraform-destroy-args }} + shell: bash + - name: Cleanup + if: ${{ inputs.cleanup == 'true' }} + run: | + find . -name out.tfplan -exec rm -rf {} \; + rm -f ${{ inputs.preview-comment-file }} + shell: bash diff --git a/terramate.tm.hcl b/terramate.tm.hcl new file mode 100644 index 0000000..2f6c211 --- /dev/null +++ b/terramate.tm.hcl @@ -0,0 +1,11 @@ +terramate { + required_version = "~> 0.4.0" + + config { + git { + check_untracked = false + check_uncommitted = false + check_remote = false + } + } +} \ No newline at end of file diff --git a/test/terramate/stacks/config.tm.hcl b/test/terramate/stacks/config.tm.hcl new file mode 100644 index 0000000..95c0656 --- /dev/null +++ b/test/terramate/stacks/config.tm.hcl @@ -0,0 +1,4 @@ +globals { + terraform_version = "~> 1.0" + terraform_null_provider_version = "3.2.1" +} \ No newline at end of file diff --git a/test/terramate/stacks/stack-A/main.tm.hcl b/test/terramate/stacks/stack-A/main.tm.hcl new file mode 100644 index 0000000..107d621 --- /dev/null +++ b/test/terramate/stacks/stack-A/main.tm.hcl @@ -0,0 +1,6 @@ +generate_hcl "_terramate_generated_main.tf" { + content { + resource "null_resource" "this" { + } + } +} \ No newline at end of file diff --git a/test/terramate/stacks/stack-A/stack.tm.hcl b/test/terramate/stacks/stack-A/stack.tm.hcl new file mode 100644 index 0000000..5d6e5ff --- /dev/null +++ b/test/terramate/stacks/stack-A/stack.tm.hcl @@ -0,0 +1,5 @@ +stack { + name = "stack-A" + description = "stack-A" + id = "18c36773-55cb-468e-8e5f-9a3b72166017" +} diff --git a/test/terramate/stacks/stack-B/main.tm.hcl b/test/terramate/stacks/stack-B/main.tm.hcl new file mode 100644 index 0000000..107d621 --- /dev/null +++ b/test/terramate/stacks/stack-B/main.tm.hcl @@ -0,0 +1,6 @@ +generate_hcl "_terramate_generated_main.tf" { + content { + resource "null_resource" "this" { + } + } +} \ No newline at end of file diff --git a/test/terramate/stacks/stack-B/stack.tm.hcl b/test/terramate/stacks/stack-B/stack.tm.hcl new file mode 100644 index 0000000..fb5b578 --- /dev/null +++ b/test/terramate/stacks/stack-B/stack.tm.hcl @@ -0,0 +1,5 @@ +stack { + name = "stack-B" + description = "stack-B" + id = "e76f2fc4-323a-4e43-9a26-1e119869d1c4" +} diff --git a/test/terramate/stacks/versions.tm.hcl b/test/terramate/stacks/versions.tm.hcl new file mode 100644 index 0000000..ac133aa --- /dev/null +++ b/test/terramate/stacks/versions.tm.hcl @@ -0,0 +1,13 @@ +generate_hcl "_terramate_generated_versions.tf" { + content { + terraform { + required_version = global.terraform_version + required_providers { + null = { + source = "hashicorp/null" + version = global.terraform_null_provider_version + } + } + } + } +} \ No newline at end of file