Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions .github/actions/github-actions.actionlint/action.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ inputs:
required: false
default: "true"
description: "(Optional) Whether to check `run` scripts with `shellcheck`. Requires the `shellcheck` CLI on the `PATH`. Defaults to `true`."
annotations_enabled:
required: false
default: "true"
description: "(Optional) Whether to annotate the reported lines in the pull request through a problem matcher. Defaults to `true`."

outputs:
skipped:
Expand Down Expand Up @@ -66,6 +70,14 @@ runs:

actionlint -version | head -n 1

- name: Register Problem Matcher
id: matcher
if: steps.target.outputs.skipped != 'true' && inputs.annotations_enabled == 'true'
shell: bash
env:
ACTION_PATH: ${{ github.action_path }}
run: echo "::add-matcher::$ACTION_PATH/problem-matcher.json"

- name: Run actionlint
id: actionlint
if: steps.target.outputs.skipped != 'true'
Expand All @@ -88,6 +100,12 @@ runs:
readarray -t files <<< "$FILES"
actionlint "${args[@]}" "${files[@]}"

- name: Remove Problem Matcher
id: matcher-remove
if: always() && steps.matcher.outcome == 'success'
shell: bash
run: echo "::remove-matcher owner=actionlint::"

- name: Add Failure Details to Job Summary
id: actionlint-summary
if: always() && steps.actionlint.outcome == 'failure'
Expand Down
17 changes: 17 additions & 0 deletions .github/actions/github-actions.actionlint/problem-matcher.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"problemMatcher": [
{
"owner": "actionlint",
"pattern": [
{
"regexp": "^(.+?):(\\d+):(\\d+): (.+?) \\[(.+?)\\]$",
"file": 1,
"line": 2,
"column": 3,
"message": 4,
"code": 5
}
]
}
]
}
18 changes: 18 additions & 0 deletions .github/actions/terraform.tflint/action.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@ inputs:
required: false
default: ${{ github.token }}
description: "(Optional) The GitHub token used by `tflint --init` to download plugins from GitHub without hitting the anonymous rate limit. Defaults to the automatically generated `github.token`."
annotations_enabled:
required: false
default: "true"
description: "(Optional) Whether to annotate the reported lines in the pull request through a problem matcher. Only works with the `compact` format. Defaults to `true`."

outputs:
skipped:
Expand Down Expand Up @@ -122,6 +126,14 @@ runs:
file: ${{ steps.init.outputs.log_file }}
lang: text

- name: Register Problem Matcher
id: matcher
if: steps.target.outputs.skipped != 'true' && inputs.annotations_enabled == 'true' && inputs.format == 'compact'
shell: bash
env:
ACTION_PATH: ${{ github.action_path }}
run: echo "::add-matcher::$ACTION_PATH/problem-matcher.json"

- name: Run TFLint
id: tflint
if: steps.target.outputs.skipped != 'true'
Expand All @@ -146,6 +158,12 @@ runs:
--format "$FORMAT" \
--minimum-failure-severity "$MINIMUM_FAILURE_SEVERITY"

- name: Remove Problem Matcher
id: matcher-remove
if: always() && steps.matcher.outcome == 'success'
shell: bash
run: echo "::remove-matcher owner=tflint::"

- name: Add Failure Details to Job Summary
id: tflint-summary
if: always() && steps.tflint.outcome == 'failure'
Expand Down
18 changes: 18 additions & 0 deletions .github/actions/terraform.tflint/problem-matcher.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"problemMatcher": [
{
"owner": "tflint",
"pattern": [
{
"regexp": "^(.+?):(\\d+):(\\d+): (Error|Warning|Notice) - (.+) \\((.+)\\)$",
"file": 1,
"line": 2,
"column": 3,
"severity": 4,
"message": 5,
"code": 6
}
]
}
]
}
50 changes: 50 additions & 0 deletions .github/actions/terraform.validate/action.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ inputs:
required: false
default: "false"
description: "(Optional) Whether to also validate the query files (`*.tfquery.hcl`) by passing `-query`, which needs Terraform 1.14 or later. Defaults to `false`."
annotations_enabled:
required: false
default: "true"
description: "(Optional) Whether to annotate the failing lines in the pull request when `terraform validate` fails, by re-running it with `-json` and turning each diagnostic into a workflow error or warning. Defaults to `true`."

outputs:
skipped:
Expand Down Expand Up @@ -114,6 +118,52 @@ runs:

terraform -chdir="$TARGET_DIR" validate "${args[@]}"

# GitHub only renders annotations from `::error` lines, so the diagnostics are re-read as JSON rather than parsed from the text.
- name: Annotate Findings
id: validate-annotate
if: always() && inputs.annotations_enabled == 'true' && steps.validate.outcome == 'failure'
shell: bash
env:
TARGET_DIR: ${{ steps.target.outputs.target_dir }}
WORKSPACE: ${{ inputs.workspace }}
VALIDATE_TESTS: ${{ inputs.validate_tests }}
TEST_DIRECTORY: ${{ inputs.test_directory }}
VALIDATE_QUERY: ${{ inputs.validate_query }}
run: |
if [ -n "$WORKSPACE" ]; then
export TF_WORKSPACE="$WORKSPACE"
fi

args=(-json)
if [ "$VALIDATE_TESTS" != "true" ]; then
args+=(-no-tests)
elif [ -n "$TEST_DIRECTORY" ]; then
args+=(-test-directory="$TEST_DIRECTORY")
fi
if [ "$VALIDATE_QUERY" = "true" ]; then
args+=(-query)
fi

prefix=""
if [ "$TARGET_DIR" != "." ]; then
prefix="$TARGET_DIR/"
fi

# Paths are relative to the module, and a workflow command takes one line, so newlines and the
# property separators are percent-encoded the way the runner decodes them.
terraform -chdir="$TARGET_DIR" validate "${args[@]}" 2>/dev/null | jq -r --arg prefix "$prefix" '
def esc: gsub("%"; "%25") | gsub("\r"; "%0D") | gsub("\n"; "%0A");
def prop: esc | gsub(":"; "%3A") | gsub(","; "%2C");
(.diagnostics // [])[]
| (if .severity == "error" then "error" else "warning" end) as $severity
| (.summary + (if (.detail // "") != "" then "\n\n" + .detail else "" end) | esc) as $message
| if .range then
"::\($severity) file=\($prefix + .range.filename),line=\(.range.start.line),col=\(.range.start.column),endLine=\(.range.end.line),endColumn=\(.range.end.column),title=\(.summary | prop)::\($message)"
else
"::\($severity) title=\(.summary | prop)::\($message)"
end
' || true

- name: Add Failure Details to Job Summary
id: validate-summary
if: always() && steps.validate.outcome == 'failure'
Expand Down
78 changes: 78 additions & 0 deletions .github/workflows/test.check-annotations.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
name: Test - check annotations


on:
pull_request:
branches:
- "**"


jobs:
test:
name: Test
runs-on: ubuntu-latest

steps:
- name: Checkout
id: checkout
uses: actions/checkout@v7

- name: Set up tools
id: setup-tools
uses: tedilabs/github-actions/.github/actions/mise.setup-tools@main
with:
mise_toml: |
[tools]
terraform = "1.16.3"
tflint = "0.64.0"
actionlint = "latest"
shellcheck = "latest"

- name: terraform validate (annotated)
id: validate
continue-on-error: true
uses: tedilabs/github-actions/.github/actions/terraform.validate@feat/check-annotations
with:
target_dir: fixtures/annotations/terraform

- name: terraform validate (annotated, fails at init)
id: validate-init
continue-on-error: true
uses: tedilabs/github-actions/.github/actions/terraform.validate@feat/check-annotations
with:
target_dir: fixtures/annotations/terraform-init

- name: tflint (annotated)
id: tflint
continue-on-error: true
uses: tedilabs/github-actions/.github/actions/terraform.tflint@feat/check-annotations
with:
target_dir: fixtures/annotations/terraform

- name: actionlint (annotated)
id: actionlint
continue-on-error: true
uses: tedilabs/github-actions/.github/actions/github-actions.actionlint@feat/check-annotations
with:
target_dir: fixtures/annotations/workflows

- name: actionlint (annotations disabled, for comparison)
id: actionlint-off
continue-on-error: true
uses: tedilabs/github-actions/.github/actions/github-actions.actionlint@feat/check-annotations
with:
target_dir: fixtures/annotations/workflows
annotations_enabled: "false"

- name: Outcomes
id: outcomes
env:
VALIDATE: ${{ steps.validate.outcome }}/${{ steps.validate.outputs.exitcode }}
VALIDATE_INIT: ${{ steps.validate-init.outcome }}/${{ steps.validate-init.outputs.exitcode }}
TFLINT: ${{ steps.tflint.outcome }}/${{ steps.tflint.outputs.exitcode }}
ACTIONLINT: ${{ steps.actionlint.outcome }}/${{ steps.actionlint.outputs.exitcode }}
ACTIONLINT_OFF: ${{ steps.actionlint-off.outcome }}/${{ steps.actionlint-off.outputs.exitcode }}
run: |
for v in VALIDATE VALIDATE_INIT TFLINT ACTIONLINT ACTIONLINT_OFF; do printf '%s=<%s>\n' "$v" "${!v}"; done
for v in VALIDATE VALIDATE_INIT TFLINT ACTIONLINT ACTIONLINT_OFF; do [ "${!v%%/*}" = "failure" ] || { echo "FAIL $v should fail on the fixtures"; exit 1; }; done
echo "All five checks failed as intended; the annotations are on the run summary and in the Files changed tab."
6 changes: 6 additions & 0 deletions fixtures/annotations/terraform-init/main.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
variable "name" {

Check failure on line 1 in fixtures/annotations/terraform-init/main.tf

View workflow job for this annotation

GitHub Actions / Test

terraform init · Unclosed configuration block

Unclosed configuration block There is no closing brace for this block before the end of the file. This may be caused by incorrect brace nesting elsewhere in this file.
type = string

output "greeting" {

Check failure on line 4 in fixtures/annotations/terraform-init/main.tf

View workflow job for this annotation

GitHub Actions / Test

terraform init · Unsupported block type

Unsupported block type Blocks of type "output" are not expected here.
value = var.name
}
11 changes: 11 additions & 0 deletions fixtures/annotations/terraform/main.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
variable "name" {

Check warning on line 1 in fixtures/annotations/terraform/main.tf

View workflow job for this annotation

GitHub Actions / Test

tflint · terraform_required_version

terraform "required_version" attribute is required

Check warning on line 1 in fixtures/annotations/terraform/main.tf

View workflow job for this annotation

GitHub Actions / Test

tflint · terraform_unused_declarations

variable "name" is declared but not used
type = string
}

locals {
unused = "never read"

Check warning on line 6 in fixtures/annotations/terraform/main.tf

View workflow job for this annotation

GitHub Actions / Test

tflint · terraform_unused_declarations

local.unused is declared but not used
}

output "greeting" {
value = var.nope

Check failure on line 10 in fixtures/annotations/terraform/main.tf

View workflow job for this annotation

GitHub Actions / Test

terraform validate · Reference to undeclared input variable

Reference to undeclared input variable An input variable with the name "nope" has not been declared. Did you mean "name"?
}
6 changes: 6 additions & 0 deletions fixtures/annotations/workflows/bad.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
on: push
jobs:
x:
runs-on: my-custom-runner

Check failure on line 4 in fixtures/annotations/workflows/bad.yaml

View workflow job for this annotation

GitHub Actions / Test

actionlint · runner-label

label "my-custom-runner" is unknown. available labels are "windows-latest", "windows-latest-8-cores", "windows-2025", "windows-2025-vs2026", "windows-2022", "windows-11-arm", "ubuntu-slim", "ubuntu-latest", "ubuntu-latest-4-cores", "ubuntu-latest-8-cores", "ubuntu-latest-16-cores", "ubuntu-24.04", "ubuntu-24.04-arm", "ubuntu-22.04", "ubuntu-22.04-arm", "macos-latest", "macos-latest-xlarge", "macos-latest-large", "macos-26-intel", "macos-26-xlarge", "macos-26-large", "macos-26", "macos-15-intel", "macos-15-xlarge", "macos-15-large", "macos-15", "macos-14-xlarge", "macos-14-large", "macos-14", "self-hosted", "x64", "arm", "arm64", "linux", "macos", "windows". if it is a custom label for self-hosted runner, set list of labels in actionlint.yaml config file
steps:
- run: echo ${{ github.event.issue.title }}

Check failure on line 6 in fixtures/annotations/workflows/bad.yaml

View workflow job for this annotation

GitHub Actions / Test

actionlint · expression

"github.event.issue.title" is potentially untrusted. avoid using it directly in inline scripts. instead, pass it through an environment variable. see https://docs.github.com/en/actions/reference/security/secure-use#good-practices-for-mitigating-script-injection-attacks for more details
Loading