From 5db092eda4155a9d40ca12389c3a0959dc3ff045 Mon Sep 17 00:00:00 2001 From: Byungjin Park Date: Mon, 14 Sep 2026 21:48:01 +0900 Subject: [PATCH 1/8] feat(actions): add packer.fmt composite action - Check formatting with `packer fmt -check -diff` - Support `recursive` input to also check subdirectories (default `true`) - Add the diff to the job summary on failure via github.step-summary --- .github/actions/packer.fmt/action.yaml | 41 ++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 .github/actions/packer.fmt/action.yaml diff --git a/.github/actions/packer.fmt/action.yaml b/.github/actions/packer.fmt/action.yaml new file mode 100644 index 0000000..225218f --- /dev/null +++ b/.github/actions/packer.fmt/action.yaml @@ -0,0 +1,41 @@ +name: Packer - Format +description: Check that Packer HCL templates are formatted with `packer fmt`. On failure, the diff is added to the job summary. Requires the `packer` CLI on the `PATH`. + + +inputs: + target_dir: + required: false + default: ./ + description: "(Optional) The directory to check formatting in. Defaults to `./`." + recursive: + required: false + default: "true" + description: "(Optional) Whether to also check subdirectories. Defaults to `true`." + + +runs: + using: composite + + steps: + - name: Check Packer Format + id: fmt + shell: bash + env: + TARGET_DIR: ${{ inputs.target_dir }} + RECURSIVE: ${{ inputs.recursive }} + run: | + args=(-check -diff) + if [ "$RECURSIVE" = "true" ]; then + args+=(-recursive) + fi + + packer fmt "${args[@]}" "$TARGET_DIR" 2>&1 | tee "$RUNNER_TEMP/packer-fmt.log" + + - name: Add Failure Details to Job Summary + id: fmt-summary + if: steps.fmt.outcome == 'failure' + uses: tedilabs/github-actions/.github/actions/github.step-summary@main + with: + title: "❌ packer fmt · ${{ inputs.target_dir }}" + file: ${{ runner.temp }}/packer-fmt.log + lang: diff From e101682e0433a8ae51eed1e709a1859ca15c214b Mon Sep 17 00:00:00 2001 From: Byungjin Park Date: Mon, 14 Sep 2026 21:48:01 +0900 Subject: [PATCH 2/8] feat(actions): add packer.validate composite action - Install required plugins with `packer init` and run `packer validate` - Export `github_token` as `PACKER_GITHUB_API_TOKEN` to avoid anonymous GitHub API rate limits when downloading plugins - Add init/validate output to the job summary on failure via github.step-summary --- .github/actions/packer.validate/action.yaml | 54 +++++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 .github/actions/packer.validate/action.yaml diff --git a/.github/actions/packer.validate/action.yaml b/.github/actions/packer.validate/action.yaml new file mode 100644 index 0000000..e37083a --- /dev/null +++ b/.github/actions/packer.validate/action.yaml @@ -0,0 +1,54 @@ +name: Packer - Validate +description: Install the required plugins of a Packer template directory with `packer init` and validate it with `packer validate`. On failure, the output is added to the job summary. Requires the `packer` CLI on the `PATH`. + + +inputs: + target_dir: + required: false + default: ./ + description: "(Optional) The Packer template directory to initialize and validate. Defaults to `./`." + github_token: + required: false + default: ${{ github.token }} + description: "(Optional) The GitHub token used by `packer init` to download plugins from GitHub without hitting the anonymous rate limit, exported as `PACKER_GITHUB_API_TOKEN`. Defaults to the automatically generated `github.token`." + + +runs: + using: composite + + steps: + - name: Packer Init + id: init + shell: bash + env: + PACKER_GITHUB_API_TOKEN: ${{ inputs.github_token }} + TARGET_DIR: ${{ inputs.target_dir }} + run: | + packer version + packer init "$TARGET_DIR" 2>&1 | tee "$RUNNER_TEMP/packer-validate-init.log" + + - name: Add Failure Details to Job Summary + id: init-summary + if: steps.init.outcome == 'failure' + uses: tedilabs/github-actions/.github/actions/github.step-summary@main + with: + title: "❌ packer init · ${{ inputs.target_dir }}" + file: ${{ runner.temp }}/packer-validate-init.log + lang: text + + - name: Packer Validate + id: validate + shell: bash + env: + TARGET_DIR: ${{ inputs.target_dir }} + run: | + packer validate "$TARGET_DIR" 2>&1 | tee "$RUNNER_TEMP/packer-validate.log" + + - name: Add Failure Details to Job Summary + id: validate-summary + if: steps.validate.outcome == 'failure' + uses: tedilabs/github-actions/.github/actions/github.step-summary@main + with: + title: "❌ packer validate · ${{ inputs.target_dir }}" + file: ${{ runner.temp }}/packer-validate.log + lang: text From e7cb51524816e55ef61fed101171304b453cec8c Mon Sep 17 00:00:00 2001 From: Byungjin Park Date: Tue, 15 Sep 2026 16:27:55 +0900 Subject: [PATCH 3/8] feat(workflows): add packer.templates.integration reusable workflow - Detect changed Packer template directories under builds/** - Run packer fmt and packer init/validate per directory with continue-on-error so all checks run - Collect per-directory outcomes via github.matrix-report and publish an aggregated report to the job summary and a sticky PR comment - Add pr_comment_enabled input (default true) to toggle the PR comment - Write default tool versions to a throwaway global mise config pointed at by `MISE_GLOBAL_CONFIG_FILE`, so repository pins in `mise.toml` / `.tool-versions` win and a self-hosted runner's config is left alone - Export `AWS_DEFAULT_REGION` so Amazon builders validate without AWS config --- .../packer.templates.integration.yaml | 172 ++++++++++++++++++ 1 file changed, 172 insertions(+) create mode 100644 .github/workflows/packer.templates.integration.yaml diff --git a/.github/workflows/packer.templates.integration.yaml b/.github/workflows/packer.templates.integration.yaml new file mode 100644 index 0000000..04a3b0c --- /dev/null +++ b/.github/workflows/packer.templates.integration.yaml @@ -0,0 +1,172 @@ +name: Packer Templates - Integration + + +on: + workflow_call: + inputs: + runs_on: + description: > + JSON-encoded runs-on value. + Examples: + - '"ubuntu-latest"' + - '["self-hosted","linux","x64"]' + required: false + type: string + default: '"ubuntu-latest"' + + paths: + type: string + required: false + default: | + builds/** + description: "(Optional) File and directory patterns used to detect changed Packer template directories, one per line. Defaults to `builds/**`." + paths_max_depth: + type: string + required: false + default: "2" + description: "(Optional) The maximum depth of the changed directories to check. For example, `builds/foo/source.pkr.hcl` with a max depth of `2` is checked as `builds/foo`. Defaults to `2`." + + packer_version: + type: string + required: false + default: latest + description: "(Optional) The version of `packer` to install when the repository does not pin one in `mise.toml` or `.tool-versions`. Defaults to `latest`." + aws_region: + type: string + required: false + default: us-east-1 + description: "(Optional) The region exported as `AWS_DEFAULT_REGION` so that Amazon builders without an explicit `region` pass `packer validate` on a runner with no AWS configuration. No credentials are needed. Defaults to `us-east-1`." + + fmt_enabled: + type: boolean + required: false + default: true + description: "(Optional) Whether to check formatting with `packer fmt`. Defaults to `true`." + validate_enabled: + type: boolean + required: false + default: true + description: "(Optional) Whether to run `packer init` and `packer validate`. Defaults to `true`." + + pr_comment_enabled: + type: boolean + required: false + default: true + description: "(Optional) Whether to post the integration report as a single sticky comment on pull requests. Requires the `pull-requests: write` permission on the caller. The report is always written to the job summary. Defaults to `true`." + + +jobs: + changed: + name: Detect Changed Directories + runs-on: ${{ fromJson(inputs.runs_on) }} + + outputs: + has_directories: ${{ steps.changed-dirs.outputs.has_directories }} + directories: ${{ steps.changed-dirs.outputs.directories }} + + steps: + - name: Checkout + id: checkout + uses: actions/checkout@v7 + with: + fetch-depth: 0 + + - name: Get Changed Directories + id: changed-dirs + uses: tedilabs/github-actions/.github/actions/git.changed-dirs@main + with: + paths: ${{ inputs.paths }} + max_depth: ${{ inputs.paths_max_depth }} + + + lint: + name: Lint (${{ matrix.path }}) + needs: + - changed + if: needs.changed.outputs.has_directories == 'true' + runs-on: ${{ fromJson(inputs.runs_on) }} + + strategy: + fail-fast: false + matrix: + path: ${{ fromJson(needs.changed.outputs.directories) }} + + env: + AWS_DEFAULT_REGION: ${{ inputs.aws_region }} + + steps: + - name: Checkout + id: checkout + uses: actions/checkout@v7 + + # Written to a throwaway global mise config, so the repository's own `mise.toml` / `.tool-versions` + # still take precedence while a self-hosted runner's real global config is left untouched. + - name: Set Default Tool Versions + id: default-tools + env: + PACKER_VERSION: ${{ inputs.packer_version }} + run: | + config_file="$RUNNER_TEMP/mise-defaults.toml" + cat > "$config_file" <> "$GITHUB_ENV" + + - name: Set up tools + id: setup-tools + uses: tedilabs/github-actions/.github/actions/mise.setup-tools@main + + - name: Check Format + id: fmt + if: inputs.fmt_enabled + continue-on-error: true + uses: tedilabs/github-actions/.github/actions/packer.fmt@main + with: + target_dir: ${{ matrix.path }} + + - name: Validate + id: validate + if: inputs.validate_enabled + continue-on-error: true + uses: tedilabs/github-actions/.github/actions/packer.validate@main + with: + target_dir: ${{ matrix.path }} + + - name: Collect Results + id: results + if: always() + uses: tedilabs/github-actions/.github/actions/github.matrix-report@main + with: + mode: collect + id: ${{ matrix.path }} + id_label: Directory + artifact_prefix: packer-integration + job_status: ${{ job.status }} + results: | + { + "fmt": "${{ steps.fmt.outcome }}", + "validate": "${{ steps.validate.outcome }}" + } + + + report: + name: Report + needs: + - changed + - lint + if: always() && needs.changed.outputs.has_directories == 'true' + runs-on: ${{ fromJson(inputs.runs_on) }} + + steps: + - name: Publish Report + id: report + uses: tedilabs/github-actions/.github/actions/github.matrix-report@main + with: + mode: publish + id_label: Directory + artifact_prefix: packer-integration + title: Packer Integration + pr_comment_enabled: ${{ inputs.pr_comment_enabled }} + pr_comment_marker: packer-integration From 62478ff4e15e22f837e29676fe705a8f86ccaf2a Mon Sep 17 00:00:00 2001 From: Byungjin Park Date: Sun, 20 Sep 2026 00:58:10 +0900 Subject: [PATCH 4/8] refactor(actions): align the Packer checks with the check-action interface - Run `packer fmt`, `packer init`, and `packer validate` through `shell.run`, and expose `skipped`, `stdout`, `stderr`, and `exitcode` like the Terraform and GitHub Actions checks - Resolve the target in a dedicated step: strip the trailing slash, skip with a notice when the directory holds no Packer HCL or template files, and print the tool version there so it stays out of `stdout` - Guard the failure summary steps with `always()`, since the implicit `success()` otherwise skips them after the check fails - Report `skipped` and drop disabled checks in the Packer integration report, as the Terraform integration workflows do --- .github/actions/packer.fmt/action.yaml | 53 ++++++++++++--- .github/actions/packer.validate/action.yaml | 65 +++++++++++++++---- .../packer.templates.integration.yaml | 6 +- 3 files changed, 99 insertions(+), 25 deletions(-) diff --git a/.github/actions/packer.fmt/action.yaml b/.github/actions/packer.fmt/action.yaml index 225218f..8f12fa7 100644 --- a/.github/actions/packer.fmt/action.yaml +++ b/.github/actions/packer.fmt/action.yaml @@ -12,30 +12,65 @@ inputs: default: "true" description: "(Optional) Whether to also check subdirectories. Defaults to `true`." +outputs: + skipped: + value: ${{ steps.target.outputs.skipped }} + description: "`true` when `target_dir` holds no Packer HCL files and `packer fmt` was not run. Empty otherwise." + stdout: + value: ${{ steps.fmt.outputs.stdout }} + description: "The STDOUT stream of the call to `packer fmt`. Empty when skipped." + stderr: + value: ${{ steps.fmt.outputs.stderr }} + description: "The STDERR stream of the call to `packer fmt`. Empty when skipped." + exitcode: + value: ${{ steps.fmt.outputs.exitcode }} + description: "The exit code of the call to `packer fmt`. Empty when skipped. The action still fails on a non-zero exit code, so use `continue-on-error: true` to inspect it." + runs: using: composite steps: - - name: Check Packer Format - id: fmt + - name: Resolve Target + id: target shell: bash env: TARGET_DIR: ${{ inputs.target_dir }} RECURSIVE: ${{ inputs.recursive }} run: | - args=(-check -diff) - if [ "$RECURSIVE" = "true" ]; then - args+=(-recursive) + target_dir="${TARGET_DIR%/}" + target_dir="${target_dir:-.}" + echo "target_dir=$target_dir" >> "$GITHUB_OUTPUT" + + # `packer fmt` exits 0 on a directory without Packer HCL files, which would report a vacuous pass. + depth_args=() + [ "$RECURSIVE" = "true" ] || depth_args=(-maxdepth 1) + if [ -z "$(find "$target_dir" "${depth_args[@]}" -type f \( -name '*.pkr.hcl' -o -name '*.pkrvars.hcl' \) -print -quit)" ]; then + echo "::notice::Skipping packer fmt: no Packer HCL files in $target_dir." + echo "skipped=true" >> "$GITHUB_OUTPUT" fi - packer fmt "${args[@]}" "$TARGET_DIR" 2>&1 | tee "$RUNNER_TEMP/packer-fmt.log" + - name: Check Packer Format + id: fmt + if: steps.target.outputs.skipped != 'true' + uses: tedilabs/github-actions/.github/actions/shell.run@main + env: + TARGET_DIR: ${{ steps.target.outputs.target_dir }} + RECURSIVE: ${{ inputs.recursive }} + with: + run: | + args=(-check -diff) + if [ "$RECURSIVE" = "true" ]; then + args+=(-recursive) + fi + + packer fmt "${args[@]}" "$TARGET_DIR" - name: Add Failure Details to Job Summary id: fmt-summary - if: steps.fmt.outcome == 'failure' + if: always() && steps.fmt.outcome == 'failure' uses: tedilabs/github-actions/.github/actions/github.step-summary@main with: - title: "❌ packer fmt · ${{ inputs.target_dir }}" - file: ${{ runner.temp }}/packer-fmt.log + title: "❌ packer fmt · ${{ steps.target.outputs.target_dir }}" + file: ${{ steps.fmt.outputs.log_file }} lang: diff diff --git a/.github/actions/packer.validate/action.yaml b/.github/actions/packer.validate/action.yaml index e37083a..d8aced3 100644 --- a/.github/actions/packer.validate/action.yaml +++ b/.github/actions/packer.validate/action.yaml @@ -12,43 +12,80 @@ inputs: default: ${{ github.token }} description: "(Optional) The GitHub token used by `packer init` to download plugins from GitHub without hitting the anonymous rate limit, exported as `PACKER_GITHUB_API_TOKEN`. Defaults to the automatically generated `github.token`." +outputs: + skipped: + value: ${{ steps.target.outputs.skipped }} + description: "`true` when `target_dir` holds no Packer template files and neither `packer init` nor `packer validate` was run. Empty otherwise." + # When `packer init` fails, `packer validate` does not run, so the outputs come from the command that failed. + stdout: + value: ${{ steps.init.outcome == 'failure' && steps.init.outputs.stdout || steps.validate.outputs.stdout }} + description: "The STDOUT stream of the call to `packer validate`, or to `packer init` when it failed. Empty when skipped." + stderr: + value: ${{ steps.init.outcome == 'failure' && steps.init.outputs.stderr || steps.validate.outputs.stderr }} + description: "The STDERR stream of the call to `packer validate`, or to `packer init` when it failed. Empty when skipped." + exitcode: + value: ${{ steps.init.outcome == 'failure' && steps.init.outputs.exitcode || steps.validate.outputs.exitcode }} + description: "The exit code of the call to `packer validate`, or to `packer init` when it failed. Empty when skipped. The action still fails on a non-zero exit code, so use `continue-on-error: true` to inspect it." + runs: using: composite steps: - - name: Packer Init - id: init + - name: Resolve Target + id: target shell: bash env: - PACKER_GITHUB_API_TOKEN: ${{ inputs.github_token }} TARGET_DIR: ${{ inputs.target_dir }} run: | + target_dir="${TARGET_DIR%/}" + target_dir="${target_dir:-.}" + echo "target_dir=$target_dir" >> "$GITHUB_OUTPUT" + + # `packer validate` fails on a directory without templates, which would report a failure for an empty target. + if [ -z "$(find "$target_dir" -maxdepth 1 -type f \( -name '*.pkr.hcl' -o -name '*.pkr.json' \) -print -quit)" ]; then + echo "::notice::Skipping packer validate: no Packer template files in $target_dir." + echo "skipped=true" >> "$GITHUB_OUTPUT" + exit 0 + fi + packer version - packer init "$TARGET_DIR" 2>&1 | tee "$RUNNER_TEMP/packer-validate-init.log" + + - name: Packer Init + id: init + if: steps.target.outputs.skipped != 'true' + uses: tedilabs/github-actions/.github/actions/shell.run@main + env: + PACKER_GITHUB_API_TOKEN: ${{ inputs.github_token }} + TARGET_DIR: ${{ steps.target.outputs.target_dir }} + with: + run: | + packer init "$TARGET_DIR" - name: Add Failure Details to Job Summary id: init-summary - if: steps.init.outcome == 'failure' + if: always() && steps.init.outcome == 'failure' uses: tedilabs/github-actions/.github/actions/github.step-summary@main with: - title: "❌ packer init · ${{ inputs.target_dir }}" - file: ${{ runner.temp }}/packer-validate-init.log + title: "❌ packer init · ${{ steps.target.outputs.target_dir }}" + file: ${{ steps.init.outputs.log_file }} lang: text - name: Packer Validate id: validate - shell: bash + if: steps.target.outputs.skipped != 'true' + uses: tedilabs/github-actions/.github/actions/shell.run@main env: - TARGET_DIR: ${{ inputs.target_dir }} - run: | - packer validate "$TARGET_DIR" 2>&1 | tee "$RUNNER_TEMP/packer-validate.log" + TARGET_DIR: ${{ steps.target.outputs.target_dir }} + with: + run: | + packer validate "$TARGET_DIR" - name: Add Failure Details to Job Summary id: validate-summary - if: steps.validate.outcome == 'failure' + if: always() && steps.validate.outcome == 'failure' uses: tedilabs/github-actions/.github/actions/github.step-summary@main with: - title: "❌ packer validate · ${{ inputs.target_dir }}" - file: ${{ runner.temp }}/packer-validate.log + title: "❌ packer validate · ${{ steps.target.outputs.target_dir }}" + file: ${{ steps.validate.outputs.log_file }} lang: text diff --git a/.github/workflows/packer.templates.integration.yaml b/.github/workflows/packer.templates.integration.yaml index 04a3b0c..d54ec54 100644 --- a/.github/workflows/packer.templates.integration.yaml +++ b/.github/workflows/packer.templates.integration.yaml @@ -144,10 +144,12 @@ jobs: id_label: Directory artifact_prefix: packer-integration job_status: ${{ job.status }} + # An empty value keeps a check that is turned off out of the report; a check that ran but had + # nothing to do reports `skipped`. results: | { - "fmt": "${{ steps.fmt.outcome }}", - "validate": "${{ steps.validate.outcome }}" + "fmt": "${{ inputs.fmt_enabled && (steps.fmt.outputs.skipped == 'true' && 'skipped' || steps.fmt.outcome) || '' }}", + "validate": "${{ inputs.validate_enabled && (steps.validate.outputs.skipped == 'true' && 'skipped' || steps.validate.outcome) || '' }}" } From e0c800d0e8d0a75aef53e8d980bddd39ea6ec2fa Mon Sep 17 00:00:00 2001 From: Byungjin Park Date: Mon, 21 Sep 2026 01:57:10 +0900 Subject: [PATCH 5/8] feat(actions): annotate findings from packer fmt and packer validate - Add `annotations_enabled` (default `true`) on the pattern of the Terraform and GitHub Actions checks - `packer.fmt` reads file and hunk line from the captured diff; `packer.validate` pairs each `Error:` line with its `on line ` line; both print repository-relative paths already --- .github/actions/packer.fmt/action.yaml | 22 ++++++++++++++++++++ .github/actions/packer.validate/action.yaml | 23 +++++++++++++++++++++ 2 files changed, 45 insertions(+) diff --git a/.github/actions/packer.fmt/action.yaml b/.github/actions/packer.fmt/action.yaml index 8f12fa7..fe0597e 100644 --- a/.github/actions/packer.fmt/action.yaml +++ b/.github/actions/packer.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 `packer fmt` fails, from the hunks of the diff it printed. Defaults to `true`." outputs: skipped: @@ -66,6 +70,24 @@ runs: packer fmt "${args[@]}" "$TARGET_DIR" + # 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. `packer fmt` prints the paths as given, relative to the + # repository. + - name: Annotate Findings + id: fmt-annotate + if: always() && inputs.annotations_enabled == 'true' && steps.fmt.outcome == 'failure' + shell: bash + env: + LOG_FILE: ${{ steps.fmt.outputs.log_file }} + run: | + awk ' + /^\+\+\+ new\// { file = substr($0, 9); next } + /^@@ / && file != "" { + line = $3; sub(/^\+/, "", line); sub(/,.*/, "", line) + printf "::error file=%s,line=%s,title=packer fmt · Not formatted::Run `packer fmt` to format this file.\n", file, line + } + ' "$LOG_FILE" + - name: Add Failure Details to Job Summary id: fmt-summary if: always() && steps.fmt.outcome == 'failure' diff --git a/.github/actions/packer.validate/action.yaml b/.github/actions/packer.validate/action.yaml index d8aced3..4e834fc 100644 --- a/.github/actions/packer.validate/action.yaml +++ b/.github/actions/packer.validate/action.yaml @@ -11,6 +11,10 @@ inputs: required: false default: ${{ github.token }} description: "(Optional) The GitHub token used by `packer init` to download plugins from GitHub without hitting the anonymous rate limit, exported as `PACKER_GITHUB_API_TOKEN`. Defaults to the automatically generated `github.token`." + annotations_enabled: + required: false + default: "true" + description: "(Optional) Whether to annotate the failing lines in the pull request when `packer validate` fails, from the `Error:` and `on line ` lines it printed. Defaults to `true`." outputs: skipped: @@ -81,6 +85,25 @@ runs: run: | packer validate "$TARGET_DIR" + # `packer validate` has no machine-readable output, but each diagnostic prints `Error: ` followed by + # `on line :` with the path as given, relative to the repository. + - name: Annotate Findings + id: validate-annotate + if: always() && inputs.annotations_enabled == 'true' && steps.validate.outcome == 'failure' + shell: bash + env: + LOG_FILE: ${{ steps.validate.outputs.log_file }} + run: | + awk ' + /^(Error|Warning): / { severity = tolower($1); sub(/:$/, "", severity); summary = substr($0, index($0, ": ") + 2); next } + /^[[:space:]]+on .+ line [0-9]+/ && summary != "" { + file = $2; line = $4; sub(/[:,].*$/, "", line) + gsub(/%/, "%25", summary) + printf "::%s file=%s,line=%s,title=packer validate · %s::%s\n", severity, file, line, summary, summary + summary = "" + } + ' "$LOG_FILE" + - name: Add Failure Details to Job Summary id: validate-summary if: always() && steps.validate.outcome == 'failure' From ff9c7169b2fd5ace70a25357003a28d11409d69c Mon Sep 17 00:00:00 2001 From: Byungjin Park Date: Mon, 21 Sep 2026 15:44:45 +0900 Subject: [PATCH 6/8] feat(actions): add syntax_only to packer.validate and annotate location-less diagnostics - Add `syntax_only` (default `false`), which passes `-syntax-only` and skips `packer init`, since a syntax-only check resolves no plugins and `packer init` would otherwise fail on a runner that cannot reach the plugin registry - Annotate a diagnostic that names no file, such as `Unset variable`, without a location instead of dropping it: the summary was only ever emitted once an `on line ` line followed it - Correct the comment on the annotation step. `packer validate` does take `-machine-readable`, but it is the legacy stream format, which packs every diagnostic into one `ui,error` record as the same text with `\n` escaped and commas replaced by `%!(PACKER_COMMA)`, and carries no file or line field --- .github/actions/packer.validate/action.yaml | 41 +++++++++++++++++---- 1 file changed, 33 insertions(+), 8 deletions(-) diff --git a/.github/actions/packer.validate/action.yaml b/.github/actions/packer.validate/action.yaml index 4e834fc..a09180c 100644 --- a/.github/actions/packer.validate/action.yaml +++ b/.github/actions/packer.validate/action.yaml @@ -11,10 +11,14 @@ inputs: required: false default: ${{ github.token }} description: "(Optional) The GitHub token used by `packer init` to download plugins from GitHub without hitting the anonymous rate limit, exported as `PACKER_GITHUB_API_TOKEN`. Defaults to the automatically generated `github.token`." + syntax_only: + required: false + default: "false" + description: "(Optional) Whether to check only the syntax of the templates with `packer validate -syntax-only`, leaving their configuration unverified. `packer init` is skipped as well, since a syntax-only check resolves no plugins, which is what makes this usable on a runner that cannot reach the plugin registry. Defaults to `false`." annotations_enabled: required: false default: "true" - description: "(Optional) Whether to annotate the failing lines in the pull request when `packer validate` fails, from the `Error:` and `on line ` lines it printed. Defaults to `true`." + description: "(Optional) Whether to annotate the failing lines in the pull request when `packer validate` fails, from the `Error:` and `on line ` lines it printed. A diagnostic that names no file is annotated without a location rather than dropped. Defaults to `true`." outputs: skipped: @@ -55,9 +59,11 @@ runs: packer version + # `-syntax-only` resolves no plugins, so `packer init` is pointless there and would fail on a runner that + # cannot reach the plugin registry, which is one of the reasons to ask for a syntax-only check. - name: Packer Init id: init - if: steps.target.outputs.skipped != 'true' + if: steps.target.outputs.skipped != 'true' && inputs.syntax_only != 'true' uses: tedilabs/github-actions/.github/actions/shell.run@main env: PACKER_GITHUB_API_TOKEN: ${{ inputs.github_token }} @@ -81,12 +87,21 @@ runs: uses: tedilabs/github-actions/.github/actions/shell.run@main env: TARGET_DIR: ${{ steps.target.outputs.target_dir }} + SYNTAX_ONLY: ${{ inputs.syntax_only }} with: run: | - packer validate "$TARGET_DIR" + args=() + if [ "$SYNTAX_ONLY" = "true" ]; then + args+=(-syntax-only) + fi + + packer validate "${args[@]}" "$TARGET_DIR" - # `packer validate` has no machine-readable output, but each diagnostic prints `Error: ` followed by - # `on line :` with the path as given, relative to the repository. + # `-machine-readable` is not a better source here: it is the legacy stream format, which packs every diagnostic + # of the run into a single `ui,error` record as the same human-readable text with `\n` escaped and each comma + # replaced by `%!(PACKER_COMMA)`. It carries no file or line field, so the text output is parsed instead. Each + # diagnostic prints `Error: `, usually followed by `on line :` with the path as given, + # relative to the repository. Some, such as an unset variable, name no file and are annotated without one. - name: Annotate Findings id: validate-annotate if: always() && inputs.annotations_enabled == 'true' && steps.validate.outcome == 'failure' @@ -95,13 +110,23 @@ runs: LOG_FILE: ${{ steps.validate.outputs.log_file }} run: | awk ' - /^(Error|Warning): / { severity = tolower($1); sub(/:$/, "", severity); summary = substr($0, index($0, ": ") + 2); next } + function escape(text) { gsub(/%/, "%25", text); return text } + # A diagnostic that reached the next one, or the end of the log, without naming a file still gets an + # annotation, just without a location. + function flush( message) { + if (summary == "") { return } + message = escape(summary) + printf "::%s title=packer validate · %s::%s\n", severity, message, message + summary = "" + } + /^(Error|Warning): / { flush(); severity = tolower($1); sub(/:$/, "", severity); summary = substr($0, index($0, ": ") + 2); next } /^[[:space:]]+on .+ line [0-9]+/ && summary != "" { file = $2; line = $4; sub(/[:,].*$/, "", line) - gsub(/%/, "%25", summary) - printf "::%s file=%s,line=%s,title=packer validate · %s::%s\n", severity, file, line, summary, summary + message = escape(summary) + printf "::%s file=%s,line=%s,title=packer validate · %s::%s\n", severity, file, line, message, message summary = "" } + END { flush() } ' "$LOG_FILE" - name: Add Failure Details to Job Summary From 1ee644e7adeabbf5583aa0f8d735de500383e13e Mon Sep 17 00:00:00 2001 From: Byungjin Park Date: Mon, 21 Sep 2026 16:00:11 +0900 Subject: [PATCH 7/8] fix(workflows): install only packer in the Packer integration workflow Without `install_args`, `mise install` resolves every tool in the calling repository's mise config. `packer-templates` pins `terraform` alongside `packer` in `.tool-versions`, so each leg of the matrix was installing Terraform it never runs, as the Terraform integration workflows already avoid by naming the tools they need. --- .github/workflows/packer.templates.integration.yaml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/packer.templates.integration.yaml b/.github/workflows/packer.templates.integration.yaml index d54ec54..6f3136d 100644 --- a/.github/workflows/packer.templates.integration.yaml +++ b/.github/workflows/packer.templates.integration.yaml @@ -114,9 +114,13 @@ jobs: echo "MISE_GLOBAL_CONFIG_FILE=$config_file" >> "$GITHUB_ENV" + # Only `packer` is installed, so a repository that pins other tools in its mise config does not pay to + # install them on every leg of the matrix. Both checks need it, so it is not tied to either toggle. - name: Set up tools id: setup-tools uses: tedilabs/github-actions/.github/actions/mise.setup-tools@main + with: + install_args: packer - name: Check Format id: fmt From 97c91d399139d0e01a4d0022f610ac7625db91da Mon Sep 17 00:00:00 2001 From: Byungjin Park Date: Mon, 21 Sep 2026 16:05:39 +0900 Subject: [PATCH 8/8] refactor(workflows): take the packer version from the repository's mise config Drop the `packer_version` input and the throwaway global mise config it was written to, matching the Terraform integration workflows since #18: the tools and their versions come from the calling repository's own `mise.toml` or `.tool-versions`, with no workflow-side default. Pass the build directory as `working_directory` as well, so a `mise.toml` placed there overrides the repository-wide one. --- .../packer.templates.integration.yaml | 27 ++++--------------- 1 file changed, 5 insertions(+), 22 deletions(-) diff --git a/.github/workflows/packer.templates.integration.yaml b/.github/workflows/packer.templates.integration.yaml index 6f3136d..5e1ab61 100644 --- a/.github/workflows/packer.templates.integration.yaml +++ b/.github/workflows/packer.templates.integration.yaml @@ -26,11 +26,6 @@ on: default: "2" description: "(Optional) The maximum depth of the changed directories to check. For example, `builds/foo/source.pkr.hcl` with a max depth of `2` is checked as `builds/foo`. Defaults to `2`." - packer_version: - type: string - required: false - default: latest - description: "(Optional) The version of `packer` to install when the repository does not pin one in `mise.toml` or `.tool-versions`. Defaults to `latest`." aws_region: type: string required: false @@ -99,27 +94,15 @@ jobs: id: checkout uses: actions/checkout@v7 - # Written to a throwaway global mise config, so the repository's own `mise.toml` / `.tool-versions` - # still take precedence while a self-hosted runner's real global config is left untouched. - - name: Set Default Tool Versions - id: default-tools - env: - PACKER_VERSION: ${{ inputs.packer_version }} - run: | - config_file="$RUNNER_TEMP/mise-defaults.toml" - cat > "$config_file" <> "$GITHUB_ENV" - - # Only `packer` is installed, so a repository that pins other tools in its mise config does not pay to - # install them on every leg of the matrix. Both checks need it, so it is not tied to either toggle. + # The tools and their versions come from the repository's mise config (`mise.toml` or `.tool-versions`). + # The build directory is passed so a `mise.toml` placed there overrides the repository-wide one, + # and only `packer` is installed, so a repository that pins other tools does not pay to install them on + # every leg of the matrix. Both checks need it, so it is not tied to either toggle. - name: Set up tools id: setup-tools uses: tedilabs/github-actions/.github/actions/mise.setup-tools@main with: + working_directory: ${{ matrix.path }} install_args: packer - name: Check Format