diff --git a/.github/actions/github-actions.actionlint/action.yaml b/.github/actions/github-actions.actionlint/action.yaml index 6b29361..3fce6d6 100644 --- a/.github/actions/github-actions.actionlint/action.yaml +++ b/.github/actions/github-actions.actionlint/action.yaml @@ -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: @@ -9,7 +9,11 @@ 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" @@ -17,21 +21,21 @@ inputs: 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: @@ -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) @@ -68,32 +71,33 @@ 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' @@ -101,24 +105,26 @@ runs: 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 @@ -126,6 +132,6 @@ runs: 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 diff --git a/.github/workflows/github-actions.integration.yaml b/.github/workflows/github-actions.integration.yaml index 2de6c3f..aed8d29 100644 --- a/.github/workflows/github-actions.integration.yaml +++ b/.github/workflows/github-actions.integration.yaml @@ -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 @@ -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 @@ -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) @@ -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 }}" }