From 444c844eb9ceaad9975550a9dc7f06f0a143d88c Mon Sep 17 00:00:00 2001 From: Byungjin Park Date: Thu, 24 Sep 2026 00:23:55 +0900 Subject: [PATCH] feat(terraform.tflint): lint workspace projects as their workspace MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `tflint` evaluates the configuration and its input variables, so a project that derives a path from `terraform.workspace` — the `/config.yaml` layout the workspace workflow is built around — is evaluated as the `default` workspace unless it is told otherwise. The lookup then resolves to a path that does not exist and the rule fails to check: Failed to check ruleset; failed to check "aws_lb_invalid_ip_address_type" rule: terraform.tf:58: no file exists at "./default/config.yaml" `terraform.validate` already takes a `workspace` input for this reason; the tflint action had no equivalent. Add a `mode` input (`module` or `workspace`) and a `workspace` input. Only `workspace` mode exports `TF_WORKSPACE`, so module linting keeps evaluating the configuration as written. An empty workspace falls back to `default`, which the input default alone does not cover — a matrix entry for a single-workspace project passes an empty string rather than omitting it. The workspace workflow now passes the matrix workspace through, and the modules workflow states `mode: module` explicitly. Only the calls that evaluate the configuration export the variable. `tflint --init` reads the tflint config to install plugins and never touches the Terraform configuration. --- .github/actions/terraform.tflint/action.yaml | 39 +++++++++++++++++++ .../terraform.modules.integration.yaml | 1 + .../terraform.workspaces.integration.yaml | 2 + 3 files changed, 42 insertions(+) diff --git a/.github/actions/terraform.tflint/action.yaml b/.github/actions/terraform.tflint/action.yaml index 4a2e10f..9a295ba 100644 --- a/.github/actions/terraform.tflint/action.yaml +++ b/.github/actions/terraform.tflint/action.yaml @@ -7,6 +7,14 @@ inputs: required: false default: ./ description: "(Optional) The directory to lint. Defaults to `./`." + mode: + required: false + default: module + description: "(Optional) What kind of Terraform directory is being linted. `module` lints the configuration as written. `workspace` lints it as a named Terraform workspace, which is needed when the configuration references `terraform.workspace` (e.g. to locate `/config.yaml`) — `tflint` evaluates that expression as `default` unless it is told otherwise. Valid values are `module` or `workspace`. Defaults to `module`." + workspace: + required: false + default: default + description: "(Optional) The Terraform workspace name to lint as, exported as `TF_WORKSPACE`. Only used when `mode` is `workspace`, and an empty value falls back to the default. Defaults to Terraform's `default` workspace." config_file: required: false default: .tflint.hcl @@ -61,9 +69,30 @@ runs: shell: bash env: TARGET_DIR: ${{ inputs.target_dir }} + MODE: ${{ inputs.mode }} + WORKSPACE: ${{ inputs.workspace }} CONFIG_FILE: ${{ inputs.config_file }} RECURSIVE: ${{ inputs.recursive }} run: | + # `tflint` reads the configuration and its input variables, but never the state or the backend. The one + # thing it cannot infer is the workspace: `terraform.workspace` evaluates to `default` unless + # `TF_WORKSPACE` is set. Resolving it here keeps every `tflint` call below on the same workspace. + case "$MODE" in + module) + workspace="" + ;; + workspace) + # The caller may pass an empty workspace (e.g. a matrix entry for a single-workspace project), + # which the action input default does not cover. + workspace="${WORKSPACE:-default}" + ;; + *) + echo "::error::Invalid mode: $MODE. Valid values are module or workspace." + exit 1 + ;; + esac + echo "workspace=$workspace" >> "$GITHUB_OUTPUT" + # A trailing slash (e.g. `./`) makes `--recursive` silently skip every subdirectory, so strip it. target_dir="${TARGET_DIR%/}" target_dir="${target_dir:-.}" @@ -132,12 +161,17 @@ runs: uses: tedilabs/github-actions/.github/actions/shell.run@main env: TARGET_DIR: ${{ steps.target.outputs.target_dir }} + WORKSPACE: ${{ steps.target.outputs.workspace }} CONFIG: ${{ steps.target.outputs.config }} RECURSIVE: ${{ inputs.recursive }} MINIMUM_FAILURE_SEVERITY: ${{ inputs.minimum_failure_severity }} FORMAT: ${{ inputs.format }} with: run: | + if [ -n "$WORKSPACE" ]; then + export TF_WORKSPACE="$WORKSPACE" + fi + args=(--chdir "$TARGET_DIR") if [ -n "$CONFIG" ]; then args+=(--config "$CONFIG") @@ -158,9 +192,14 @@ runs: shell: bash env: TARGET_DIR: ${{ steps.target.outputs.target_dir }} + WORKSPACE: ${{ steps.target.outputs.workspace }} CONFIG: ${{ steps.target.outputs.config }} RECURSIVE: ${{ inputs.recursive }} run: | + if [ -n "$WORKSPACE" ]; then + export TF_WORKSPACE="$WORKSPACE" + fi + args=(--chdir "$TARGET_DIR") if [ -n "$CONFIG" ]; then args+=(--config "$CONFIG") diff --git a/.github/workflows/terraform.modules.integration.yaml b/.github/workflows/terraform.modules.integration.yaml index 5027b59..f4e78aa 100644 --- a/.github/workflows/terraform.modules.integration.yaml +++ b/.github/workflows/terraform.modules.integration.yaml @@ -188,6 +188,7 @@ jobs: uses: tedilabs/github-actions/.github/actions/terraform.tflint@main with: target_dir: ${{ matrix.path }} + mode: module config_file: ${{ inputs.tflint_config_file }} minimum_failure_severity: ${{ inputs.tflint_minimum_failure_severity }} recursive: ${{ inputs.tflint_recursive }} diff --git a/.github/workflows/terraform.workspaces.integration.yaml b/.github/workflows/terraform.workspaces.integration.yaml index bb103e3..e36da31 100644 --- a/.github/workflows/terraform.workspaces.integration.yaml +++ b/.github/workflows/terraform.workspaces.integration.yaml @@ -197,6 +197,8 @@ jobs: uses: tedilabs/github-actions/.github/actions/terraform.tflint@main with: target_dir: ${{ matrix.project }} + mode: workspace + workspace: ${{ matrix.workspace }} config_file: ${{ inputs.tflint_config_file }} minimum_failure_severity: ${{ inputs.tflint_minimum_failure_severity }} recursive: ${{ inputs.tflint_recursive }}