Skip to content
Merged
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
54 changes: 30 additions & 24 deletions .github/actions/github-actions.actionlint/action.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
name: GitHub Actions - actionlint
description: Lint GitHub Actions workflow files with `actionlint`. On failure, the findings are added to the job summary. Requires the `actionlint` CLI on the `PATH`. When `shellcheck` is also on the `PATH`, `run` scripts are checked with it.
description: Lint GitHub Actions workflow files with `jactionlint`, an actively maintained fork of `actionlint`. On failure, the findings are added to the job summary. Requires the `jactionlint` CLI on the `PATH`. When `shellcheck` is also on the `PATH`, `run` scripts are checked with it.


inputs:
Expand All @@ -9,29 +9,33 @@ inputs:
description: "(Optional) The directory holding the workflow files to lint. Only the `*.yml` and `*.yaml` files directly in it are linted, since GitHub only reads top-level workflow files. Defaults to `.github/workflows`."
config_file:
required: false
description: "(Optional) The path to the `actionlint` config file, relative to the repository root. When omitted, `actionlint` discovers `.github/actionlint.yaml` in the Git repository that holds the workflow files as usual; files outside a Git repository are linted without a config."
description: "(Optional) The path to the `jactionlint` config file, relative to the repository root. When omitted, `jactionlint` discovers `.github/jactionlint.yaml`, or the `.github/actionlint.yaml` of the original `actionlint`, in the Git repository that holds the workflow files; files outside a Git repository are linted without a config."
profile:
required: false
default: correctness
description: "(Optional) The `jactionlint` rule profile: `correctness` (the checks of the original `actionlint`), `default` or `pedantic`. Overrides the profile of the config file. Defaults to `correctness`."
shellcheck_enabled:
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 when `actionlint` fails, by re-running it with a JSON format and turning each finding into a workflow error. Defaults to `true`."
description: "(Optional) Whether to annotate the reported lines in the pull request when `jactionlint` fails, by re-running it with the JSON format and turning each finding into a workflow command of its severity. Defaults to `true`."

outputs:
skipped:
value: ${{ steps.target.outputs.skipped }}
description: "`true` when `target_dir` holds no workflow files and `actionlint` was not run. Empty otherwise."
description: "`true` when `target_dir` holds no workflow files and `jactionlint` was not run. Empty otherwise."
stdout:
value: ${{ steps.actionlint.outputs.stdout }}
description: "The STDOUT stream of the call to `actionlint`. Empty when skipped."
description: "The STDOUT stream of the call to `jactionlint`. Empty when skipped."
stderr:
value: ${{ steps.actionlint.outputs.stderr }}
description: "The STDERR stream of the call to `actionlint`. Empty when skipped."
description: "The STDERR stream of the call to `jactionlint`. Empty when skipped."
exitcode:
value: ${{ steps.actionlint.outputs.exitcode }}
description: "The exit code of the call to `actionlint`. Empty when skipped. The action still fails on a non-zero exit code, so use `continue-on-error: true` to inspect it."
description: "The exit code of the call to `jactionlint`. Empty when skipped. The action still fails on a non-zero exit code, so use `continue-on-error: true` to inspect it."


runs:
Expand All @@ -49,8 +53,7 @@ runs:
target_dir="${target_dir:-.}"
echo "target_dir=$target_dir" >> "$GITHUB_OUTPUT"

# `actionlint` exits 3 when it finds no workflows directory, so an empty or missing target is skipped instead
# of failing; `find` would fail on a missing directory too.
# An empty or missing target is skipped instead of failing; `find` would fail on a missing directory too.
files=""
if [ -d "$target_dir" ]; then
files=$(find "$target_dir" -maxdepth 1 -type f \( -name '*.yml' -o -name '*.yaml' \) | sort)
Expand All @@ -68,64 +71,67 @@ runs:
echo "config=$(realpath "$CONFIG_FILE")" >> "$GITHUB_OUTPUT"
fi

actionlint -version | head -n 1
jactionlint --version | head -n 1

- name: Run actionlint
- name: Run jactionlint
id: actionlint
if: steps.target.outputs.skipped != 'true'
uses: tedilabs/github-actions/.github/actions/shell.run@main
env:
FILES: ${{ steps.target.outputs.files }}
CONFIG: ${{ steps.target.outputs.config }}
PROFILE: ${{ inputs.profile }}
SHELLCHECK_ENABLED: ${{ inputs.shellcheck_enabled }}
with:
run: |
args=(-no-color)
args=(--no-color "--profile=$PROFILE")
if [ -n "$CONFIG" ]; then
args+=(-config-file "$CONFIG")
args+=("--config-file=$CONFIG")
fi
if [ "$SHELLCHECK_ENABLED" != "true" ]; then
args+=(-shellcheck=)
args+=(--shellcheck=)
fi

# The files are passed explicitly; `actionlint` still discovers the config from the Git repository that holds them.
# The files are passed explicitly; `jactionlint` still discovers the config from the Git repository that holds them.
readarray -t files <<< "$FILES"
actionlint "${args[@]}" "${files[@]}"
jactionlint "${args[@]}" "${files[@]}"

# GitHub only renders annotations from `::error` lines, so the findings are re-read as JSON. The files are passed
# repository-relative, and `actionlint` reports them as given.
# GitHub only renders annotations from workflow commands, so the findings are re-read as JSON. The files are passed
# repository-relative, and `jactionlint` reports them as given.
- name: Annotate Findings
id: actionlint-annotate
if: always() && inputs.annotations_enabled == 'true' && steps.actionlint.outcome == 'failure'
shell: bash
env:
FILES: ${{ steps.target.outputs.files }}
CONFIG: ${{ steps.target.outputs.config }}
PROFILE: ${{ inputs.profile }}
SHELLCHECK_ENABLED: ${{ inputs.shellcheck_enabled }}
run: |
args=(-no-color -format '{{json .}}')
args=(--no-color --format=json "--profile=$PROFILE")
if [ -n "$CONFIG" ]; then
args+=(-config-file "$CONFIG")
args+=("--config-file=$CONFIG")
fi
if [ "$SHELLCHECK_ENABLED" != "true" ]; then
args+=(-shellcheck=)
args+=(--shellcheck=)
fi

# 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"
actionlint "${args[@]}" "${files[@]}" 2>/dev/null | jq -r '
jactionlint "${args[@]}" "${files[@]}" 2>/dev/null | jq -r '
def esc: gsub("%"; "%25") | gsub("\r"; "%0D") | gsub("\n"; "%0A");
def prop: esc | gsub(":"; "%3A") | gsub(","; "%2C");
.[]
| "::error file=\(.filepath),line=\(.line),col=\(.column)\(if (.end_column // 0) > 0 then ",endColumn=\(.end_column)" else "" end),title=\("actionlint Β· " + .kind | prop)::\(.message | esc)"
| ({"error": "error", "warn": "warning"}[.severity] // "notice") as $command
| "::\($command) file=\(.filepath),line=\(.line),col=\(.column)\(if (.end_column // 0) > 0 then ",endColumn=\(.end_column)" else "" end),title=\("jactionlint Β· " + (.id // .kind) | prop)::\(.message | esc)"
' || true

- name: Add Failure Details to Job Summary
id: actionlint-summary
if: always() && steps.actionlint.outcome == 'failure'
uses: tedilabs/github-actions/.github/actions/github.step-summary@main
with:
title: "❌ actionlint · ${{ steps.target.outputs.target_dir }}"
title: "❌ jactionlint · ${{ steps.target.outputs.target_dir }}"
file: ${{ steps.actionlint.outputs.log_file }}
lang: text
24 changes: 15 additions & 9 deletions .github/workflows/github-actions.integration.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,13 @@ on:
actionlint_version:
type: string
required: false
default: latest
description: "(Optional) The version of `actionlint` to install with mise. Defaults to `latest`."
default: "2.1.0"
description: "(Optional) The version of `jactionlint`, an actively maintained fork of `actionlint`, to install with mise. Pinned by default because its releases add checks. Defaults to `2.1.0`."
shellcheck_version:
type: string
required: false
default: latest
description: "(Optional) The version of `shellcheck` to install with mise. Used by `actionlint` to check `run` scripts. Defaults to `latest`."
description: "(Optional) The version of `shellcheck` to install with mise. Used by `jactionlint` to check `run` scripts. Defaults to `latest`."
check_jsonschema_version:
type: string
required: false
Expand All @@ -34,16 +34,21 @@ on:
type: boolean
required: false
default: true
description: "(Optional) Whether to lint workflow files with `actionlint`. Defaults to `true`."
description: "(Optional) Whether to lint workflow files with `jactionlint`. Defaults to `true`."
actionlint_config_file:
type: string
required: false
description: "(Optional) The path to the `actionlint` config file, relative to the repository root. When omitted, `actionlint` discovers `.github/actionlint.yaml` as usual."
description: "(Optional) The path to the `jactionlint` config file, relative to the repository root. When omitted, `jactionlint` discovers `.github/jactionlint.yaml` or `.github/actionlint.yaml`."
actionlint_profile:
type: string
required: false
default: correctness
description: "(Optional) The `jactionlint` rule profile: `correctness` (the checks of the original `actionlint`), `default` or `pedantic`. Defaults to `correctness`."
actionlint_shellcheck_enabled:
type: boolean
required: false
default: true
description: "(Optional) Whether `actionlint` also checks `run` scripts with `shellcheck`. Defaults to `true`."
description: "(Optional) Whether `jactionlint` also checks `run` scripts with `shellcheck`. Defaults to `true`."
check_jsonschema_enabled:
type: boolean
required: false
Expand Down Expand Up @@ -73,17 +78,18 @@ jobs:
with:
mise_toml: |
[tools]
actionlint = "${{ inputs.actionlint_version }}"
jactionlint = "${{ inputs.actionlint_version }}"
shellcheck = "${{ inputs.shellcheck_version }}"
"pipx:check-jsonschema" = "${{ inputs.check_jsonschema_version }}"

- name: Lint (actionlint)
- name: Lint (jactionlint)
id: actionlint
if: inputs.actionlint_enabled
continue-on-error: true
uses: tedilabs/github-actions/.github/actions/github-actions.actionlint@main
with:
config_file: ${{ inputs.actionlint_config_file }}
profile: ${{ inputs.actionlint_profile }}
shellcheck_enabled: ${{ inputs.actionlint_shellcheck_enabled }}

- name: Validate Workflows (check-jsonschema)
Expand Down Expand Up @@ -115,7 +121,7 @@ jobs:
# A check that ran but found no files reports `skipped`.
results: |
{
"actionlint": "${{ steps.actionlint.outputs.skipped == 'true' && 'skipped' || steps.actionlint.outcome }}",
"jactionlint": "${{ steps.actionlint.outputs.skipped == 'true' && 'skipped' || steps.actionlint.outcome }}",
"check-jsonschema (workflows)": "${{ steps.check-jsonschema-workflows.outputs.skipped == 'true' && 'skipped' || steps.check-jsonschema-workflows.outcome }}",
"check-jsonschema (actions)": "${{ steps.check-jsonschema-actions.outputs.skipped == 'true' && 'skipped' || steps.check-jsonschema-actions.outcome }}"
}
Expand Down
Loading