From 666939398d2a6a4f812f4866a2d25a9fd0b5a037 Mon Sep 17 00:00:00 2001 From: Byungjin Park Date: Mon, 21 Sep 2026 01:55:45 +0900 Subject: [PATCH] feat(actions): annotate findings from terraform fmt, terraform-docs, and check-jsonschema - Add `annotations_enabled` (default `true`) to the three remaining check actions on the same pattern as validate, tflint, and actionlint - `terraform.fmt` reads the file and hunk line from the diff it captured; `terraform.docs` reads the stale file from its `is out of date` line; both prefix `target_dir` since the tools print module-relative paths - `github-actions.check-jsonschema` re-runs with `--output-format json`; schema validation has no line numbers, so the annotation points at the file and names the failing JSON path --- .../action.yaml | 25 +++++++++++++++++ .github/actions/terraform.docs/action.yaml | 22 +++++++++++++++ .github/actions/terraform.fmt/action.yaml | 27 +++++++++++++++++++ 3 files changed, 74 insertions(+) diff --git a/.github/actions/github-actions.check-jsonschema/action.yaml b/.github/actions/github-actions.check-jsonschema/action.yaml index 3b7dd9b..f40a8fa 100644 --- a/.github/actions/github-actions.check-jsonschema/action.yaml +++ b/.github/actions/github-actions.check-jsonschema/action.yaml @@ -9,6 +9,10 @@ inputs: target_dir: required: false description: "(Optional) The directory to validate. Defaults to `.github/workflows` for `schema_type: workflows` and to `.github/actions` for `schema_type: actions`." + annotations_enabled: + required: false + default: "true" + description: "(Optional) Whether to annotate the failing files in the pull request when the validation fails, by re-running `check-jsonschema` with `--output-format json`. Schema validation reports no line numbers, so the annotation points at the file and names the failing JSON path. Defaults to `true`." outputs: skipped: @@ -94,6 +98,27 @@ runs: readarray -t files <<< "$FILES" check-jsonschema --builtin-schema "$SCHEMA" "${files[@]}" + # GitHub only renders annotations from `::error` lines, so the errors are re-read as JSON. The file names are + # the ones passed in, which are repository-relative already. + - name: Annotate Findings + id: validate-annotate + if: always() && inputs.annotations_enabled == 'true' && steps.validate.outcome == 'failure' + shell: bash + env: + SCHEMA_TYPE: ${{ inputs.schema_type }} + SCHEMA: ${{ steps.target.outputs.schema }} + FILES: ${{ steps.target.outputs.files }} + run: | + # A workflow command takes one line, so newlines and the property separators are percent-encoded the way + # the runner decodes them. + readarray -t files <<< "$FILES" + check-jsonschema --builtin-schema "$SCHEMA" --output-format json "${files[@]}" 2>/dev/null | jq -r --arg type "$SCHEMA_TYPE" ' + def esc: gsub("%"; "%25") | gsub("\r"; "%0D") | gsub("\n"; "%0A"); + def prop: esc | gsub(":"; "%3A") | gsub(","; "%2C"); + (.errors // [])[] + | "::error file=\(.filename),title=\("check-jsonschema · " + $type | prop)::\(.path + ": " + .message | esc)" + ' || true + - name: Add Failure Details to Job Summary id: validate-summary if: always() && steps.validate.outcome == 'failure' diff --git a/.github/actions/terraform.docs/action.yaml b/.github/actions/terraform.docs/action.yaml index e7b9096..914cb25 100644 --- a/.github/actions/terraform.docs/action.yaml +++ b/.github/actions/terraform.docs/action.yaml @@ -40,6 +40,10 @@ inputs: args: required: false description: "(Optional) Additional arguments to pass to `terraform-docs` (e.g. `--sort-by required`)." + annotations_enabled: + required: false + default: "true" + description: "(Optional) Whether to annotate the out-of-date documentation file in the pull request when the check fails. Only applies with `check: true`. Defaults to `true`." outputs: skipped: @@ -136,6 +140,24 @@ runs: terraform-docs "${args[@]}" . + # `terraform-docs` names the stale file (`Error: is out of date`), relative to the module it ran in. + - name: Annotate Findings + id: docs-annotate + if: always() && inputs.annotations_enabled == 'true' && inputs.check == 'true' && steps.docs.outcome == 'failure' + shell: bash + env: + TARGET_DIR: ${{ steps.target.outputs.target_dir }} + LOG_FILE: ${{ steps.docs.outputs.log_file }} + run: | + prefix="" + if [ "$TARGET_DIR" != "." ]; then + prefix="$TARGET_DIR/" + fi + + sed -n 's/^Error: \(.*\) is out of date$/\1/p' "$LOG_FILE" | while IFS= read -r file; do + echo "::error file=$prefix$file,title=terraform-docs · Out of date::Regenerate this file with \`terraform-docs\`." + done + - name: Add Failure Details to Job Summary id: docs-summary if: always() && steps.docs.outcome == 'failure' diff --git a/.github/actions/terraform.fmt/action.yaml b/.github/actions/terraform.fmt/action.yaml index af686a4..b8ab640 100644 --- a/.github/actions/terraform.fmt/action.yaml +++ b/.github/actions/terraform.fmt/action.yaml @@ -11,6 +11,10 @@ inputs: required: false default: "true" description: "(Optional) Whether to also check subdirectories. Defaults to `true`." + annotations_enabled: + required: false + default: "true" + description: "(Optional) Whether to annotate the unformatted lines in the pull request when `terraform fmt` fails, from the hunks of the diff it printed. Defaults to `true`." outputs: skipped: @@ -66,6 +70,29 @@ runs: terraform -chdir="$TARGET_DIR" fmt "${args[@]}" + # The diff already names each file (`+++ new/`) and the first line of every hunk (`@@ -a,b +c,d @@`), so + # the annotations are read from the captured output. Paths are relative to `target_dir`. + - name: Annotate Findings + id: fmt-annotate + if: always() && inputs.annotations_enabled == 'true' && steps.fmt.outcome == 'failure' + shell: bash + env: + TARGET_DIR: ${{ steps.target.outputs.target_dir }} + LOG_FILE: ${{ steps.fmt.outputs.log_file }} + run: | + prefix="" + if [ "$TARGET_DIR" != "." ]; then + prefix="$TARGET_DIR/" + fi + + awk -v prefix="$prefix" ' + /^\+\+\+ new\// { file = substr($0, 9); next } + /^@@ / && file != "" { + line = $3; sub(/^\+/, "", line); sub(/,.*/, "", line) + printf "::error file=%s%s,line=%s,title=terraform fmt · Not formatted::Run `terraform fmt` to format this file.\n", prefix, file, line + } + ' "$LOG_FILE" + - name: Add Failure Details to Job Summary id: fmt-summary if: always() && steps.fmt.outcome == 'failure'