From 0b88d1d3aa0d6b12b3e0fd5461e687711865aec4 Mon Sep 17 00:00:00 2001 From: Yordis Prieto Date: Sat, 10 Oct 2026 17:37:05 -0400 Subject: [PATCH 1/3] feat(actionlint): add actionlint action One repository already lints its workflows this way; shipping the same check as a versioned action lets every other repository inherit it instead of re-implementing it and drifting. Signed-off-by: Yordis Prieto --- .github/release-please-config.json | 4 + .github/release-please-manifest.json | 1 + .github/workflows/ci.yml | 17 ++ actions/actionlint/README.md | 143 ++++++++++++++ actions/actionlint/action.yml | 205 +++++++++++++++++++++ actions/actionlint/actionlint-matcher.json | 17 ++ 6 files changed, 387 insertions(+) create mode 100644 actions/actionlint/README.md create mode 100644 actions/actionlint/action.yml create mode 100644 actions/actionlint/actionlint-matcher.json diff --git a/.github/release-please-config.json b/.github/release-please-config.json index 045a3fc..d7744f3 100644 --- a/.github/release-please-config.json +++ b/.github/release-please-config.json @@ -20,6 +20,10 @@ } ], "packages": { + "actions/actionlint": { + "component": "actionlint", + "initial-version": "0.0.1" + }, "actions/semconv/pull-request": { "component": "semconv-pull-request", "initial-version": "0.0.1" diff --git a/.github/release-please-manifest.json b/.github/release-please-manifest.json index 53d9637..247a128 100644 --- a/.github/release-please-manifest.json +++ b/.github/release-please-manifest.json @@ -1,4 +1,5 @@ { + "actions/actionlint": "0.0.1", "actions/semconv/pull-request": "0.0.1", "actions/release-please": "0.1.0", "actions/stale": "0.0.2", diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 131bc50..712ed69 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -65,6 +65,23 @@ jobs: - run: mise run github:actions:ci:lint + # This repository lints its own workflows with the action it ships, so a + # change to that action is exercised by the pull request that makes the + # change, the same reason the runner job above uses runs-on-selector. + actionlint: + name: Actionlint (action) + needs: runner + runs-on: ${{ needs.runner.outputs.runs-on }} + timeout-minutes: 10 + permissions: + contents: read + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - uses: ./actions/actionlint + test: name: Test needs: runner diff --git a/actions/actionlint/README.md b/actions/actionlint/README.md new file mode 100644 index 0000000..124ca31 --- /dev/null +++ b/actions/actionlint/README.md @@ -0,0 +1,143 @@ +# actionlint + +Lints GitHub Actions workflows with [rhysd/actionlint][actionlint] and annotates +the diff with what it finds. The binary is downloaded and checksum-verified at +run time rather than vendored, so a workflow always lints against the version +this action declares. + +[actionlint]: https://github.com/rhysd/actionlint + +## Usage + +Put the lint job in a workflow of its own: + +```yaml +# .github/workflows/workflow-lint.yml +name: Workflow lint + +on: + pull_request: + push: + branches: [main] + +permissions: {} + +jobs: + actionlint: + runs-on: ubuntu-latest + timeout-minutes: 5 + permissions: + contents: read + steps: + - uses: actions/checkout@v7 + with: + persist-credentials: false + + - uses: TrogonStack/github-actions/actions/actionlint@ # vX.Y.Z +``` + +It has to be its own workflow file, not a job folded into one of the workflows +it checks. A workflow file that fails to parse runs no jobs at all, so a lint +job defined inside the very file it is meant to guard never gets scheduled, +and the broken file reports no checks rather than a failing one. A separate +file keeps parsing and linting independent: `workflow-lint.yml` can fail +`ci.yml` even when `ci.yml` itself cannot be parsed. + +The caller checks out the repository; this action does not, so it can run +against whatever ref the caller already has checked out. Pass +`persist-credentials: false` on that checkout, since nothing after it needs to +push or authenticate as the job. + +## Inputs + +| Input | Default | Description | +| --- | --- | --- | +| `version` | `1.7.12` | actionlint version to install, without the leading `v`. | +| `shellcheck` | `true` | Install shellcheck so actionlint also lints `run:` scripts. See [Shellcheck](#shellcheck). | +| `config` | empty | Path to an actionlint config file, passed as `-config-file`. Empty lets actionlint auto-discover `.github/actionlint.yaml`. | +| `args` | empty | Extra actionlint arguments, one per line. See [Extra arguments](#extra-arguments). | + +## Shellcheck + +actionlint lints the shell inside `run:` steps by shelling out to shellcheck, +and silently skips that check when shellcheck is not on `PATH`. A bare +self-hosted runner image does not ship shellcheck the way `ubuntu-latest` +does, so the same workflow would pass on one runner pool and fail on another +for no reason visible in the diff. This action installs a pinned shellcheck +by default so every runner pool sees the same checks; set `shellcheck: false` +only where that duplicates a shellcheck step the caller already runs. + +The shellcheck version is not an input. It is pinned to the version this +action's checksums were last computed against, so a caller cannot ask for an +unverified build the way the `config` and `args` inputs would otherwise +allow. Picking up a newer shellcheck means picking up a newer release of this +action. + +## Extra arguments + +`args` takes one actionlint argument per line, such as: + +```yaml +with: + args: | + -ignore + "label \".+\" is unknown" +``` + +It is newline-separated rather than space-separated because an argument can +itself contain spaces, and splitting on newlines only needs each line read as +one argument, with no word-splitting and no shell evaluation of the line's +contents. Nothing in `args` is interpolated into the step's script; it is +read from an environment variable, so a value with `$(...)`, backticks, or +quotes in it is passed to actionlint literally rather than executed. + +## Problem matcher + +This action registers actionlint's [problem matcher][problem-matchers] before +running it, so findings on a pull request's changed lines show up as inline +annotations on the diff, not just in the job log. The matcher file is +vendored at [`actionlint-matcher.json`](actionlint-matcher.json) from +[`rhysd/actionlint`'s `.github/actionlint-matcher.json`][matcher-source] at +`v1.7.12`, rather than fetched at run time, so registering it needs no +network call of its own. + +[problem-matchers]: https://github.com/rhysd/actionlint/blob/main/docs/usage.md#problem-matchers +[matcher-source]: https://github.com/rhysd/actionlint/blob/v1.7.12/.github/actionlint-matcher.json + +## Checksums + +Both binaries are downloaded over HTTPS from their project's GitHub releases +and checksummed before anything on `PATH` points at them; a mismatch fails +the step rather than running an unverified binary. + +- For the default `version`, the actionlint checksum is one this action + carries itself, taken from the `actionlint__checksums.txt` file + that release published. No extra network call is needed to verify it. +- For any other `version`, this action downloads that release's own + `actionlint__checksums.txt` and verifies against it instead, + because a version it was not pinned against has no local checksum to check + against. +- shellcheck has no `version` input. Its checksum is pinned the same way the + default actionlint checksum is, and the only way to move it is a new + release of this action. + +## Supported platforms + +Linux on `X64` and `ARM64`, and macOS on `X64` and `ARM64`, read from +`runner.os` and `runner.arch`. Any other combination fails the step with +that pairing named in the error, rather than attempting a download that +cannot exist. + +## Fixed behaviour + +These are not inputs, on purpose. + +- actionlint always runs with no file arguments, from the caller's workspace. + It discovers `.github/workflows` itself, the same way it would from a + contributor's own checkout. +- A finding always fails the step. There is no warn-only mode, because a + lint job that cannot fail is a lint job nobody has to act on. +- This action does not check out the repository and does not depend on mise. + It installs exactly what it needs to run actionlint, so a caller that + already manages its own tool versions is not handed a second opinion about + what Node, Python, or anything else should be. diff --git a/actions/actionlint/action.yml b/actions/actionlint/action.yml new file mode 100644 index 0000000..fd3cb05 --- /dev/null +++ b/actions/actionlint/action.yml @@ -0,0 +1,205 @@ +name: actionlint +description: >- + Lints GitHub Actions workflows with rhysd/actionlint and annotates the diff + with what it finds. +author: TrogonStack + +inputs: + version: + description: actionlint version to install, without the leading `v`. + required: false + default: "1.7.12" + shellcheck: + description: >- + Install koalaman/shellcheck so actionlint also lints `run:` scripts. + actionlint silently skips that check when shellcheck is not on `PATH`, + so leaving this off makes self-hosted runners without shellcheck + preinstalled disagree with ones that have it. + required: false + default: "true" + config: + description: >- + Path to an actionlint config file, passed as `-config-file`. Leave + empty to let actionlint auto-discover `.github/actionlint.yaml`. + required: false + default: "" + args: + description: >- + Extra arguments for actionlint, one per line (for example `-ignore + '...'` on its own line). Newline-separated rather than space-separated + because an argument can itself contain spaces, and each line becomes + exactly one argument with no further word-splitting or shell + evaluation. + required: false + default: "" + +runs: + using: composite + steps: + - name: Resolve platform + id: platform + shell: bash + env: + RUNNER_OS: ${{ runner.os }} + RUNNER_ARCH: ${{ runner.arch }} + run: | + set -euo pipefail + + case "${RUNNER_OS}-${RUNNER_ARCH}" in + Linux-X64) + actionlint_asset=linux_amd64 + shellcheck_asset=linux.x86_64 + ;; + Linux-ARM64) + actionlint_asset=linux_arm64 + shellcheck_asset=linux.aarch64 + ;; + macOS-ARM64) + actionlint_asset=darwin_arm64 + shellcheck_asset=darwin.aarch64 + ;; + macOS-X64) + actionlint_asset=darwin_amd64 + shellcheck_asset=darwin.x86_64 + ;; + *) + echo "::error::Unsupported runner ${RUNNER_OS}/${RUNNER_ARCH}. This action supports Linux X64/ARM64 and macOS X64/ARM64." >&2 + exit 1 + ;; + esac + + echo "actionlint-asset=${actionlint_asset}" >> "$GITHUB_OUTPUT" + echo "shellcheck-asset=${shellcheck_asset}" >> "$GITHUB_OUTPUT" + + - name: Install actionlint + shell: bash + env: + VERSION: ${{ inputs.version }} + ASSET: ${{ steps.platform.outputs.actionlint-asset }} + run: | + set -euo pipefail + + sha256_of() { + if command -v sha256sum >/dev/null 2>&1; then + sha256sum "$1" | cut -d ' ' -f 1 + else + shasum -a 256 "$1" | cut -d ' ' -f 1 + fi + } + + # Checksums rhysd/actionlint published for the version this action + # defaults to. Verified offline, so the default path needs nothing + # from the network beyond the archive itself. + checksums=" + 1.7.12 linux_amd64 8aca8db96f1b94770f1b0d72b6dddcb1ebb8123cb3712530b08cc387b349a3d8 + 1.7.12 linux_arm64 325e971b6ba9bfa504672e29be93c24981eeb1c07576d730e9f7c8805afff0c6 + 1.7.12 darwin_amd64 5b44c3bc2255115c9b69e30efc0fecdf498fdb63c5d58e17084fd5f16324c644 + 1.7.12 darwin_arm64 aba9ced2dee8d27fecca3dc7feb1a7f9a52caefa1eb46f3271ea66b6e0e6953f + " + + archive="actionlint_${VERSION}_${ASSET}.tar.gz" + url="https://github.com/rhysd/actionlint/releases/download/v${VERSION}/${archive}" + dest="${RUNNER_TEMP}/actionlint/${VERSION}" + archive_path="${RUNNER_TEMP}/${archive}" + mkdir -p "$dest" + + curl --fail --silent --show-error --location --output "$archive_path" "$url" + + want=$(echo "$checksums" | awk -v v="$VERSION" -v a="$ASSET" '$1==v && $2==a{print $3}') + if [ -z "$want" ]; then + # A version this action was not pinned against: fall back to the + # checksums file that release itself publishes, rather than + # installing a binary nothing has verified. + sums_url="https://github.com/rhysd/actionlint/releases/download/v${VERSION}/actionlint_${VERSION}_checksums.txt" + sums_path="${RUNNER_TEMP}/actionlint_${VERSION}_checksums.txt" + curl --fail --silent --show-error --location --output "$sums_path" "$sums_url" + want=$(awk -v f="$archive" '$2==f{print $1}' "$sums_path") + if [ -z "$want" ]; then + echo "::error::No checksum for ${archive} in ${sums_url}" >&2 + exit 1 + fi + fi + + got=$(sha256_of "$archive_path") + if [ "$got" != "$want" ]; then + echo "::error::actionlint checksum mismatch for ${archive}: expected ${want}, got ${got}" >&2 + exit 1 + fi + + tar -xzf "$archive_path" -C "$dest" actionlint + echo "$dest" >> "$GITHUB_PATH" + + - name: Install shellcheck + if: inputs.shellcheck == 'true' + shell: bash + env: + ASSET: ${{ steps.platform.outputs.shellcheck-asset }} + run: | + set -euo pipefail + + sha256_of() { + if command -v sha256sum >/dev/null 2>&1; then + sha256sum "$1" | cut -d ' ' -f 1 + else + shasum -a 256 "$1" | cut -d ' ' -f 1 + fi + } + + # Pinned to the one version this action ships. Letting any version + # through unverified via an input would defeat the point of checking + # a checksum at all; moving the pin means a new release of this + # action. + version="0.11.0" + checksums=" + linux.x86_64 b7af85e41cc99489dcc21d66c6d5f3685138f06d34651e6d34b42ec6d54fe6f6 + linux.aarch64 68a8133197a50beb8803f8d42f9908d1af1c5540d4bb05fdfca8c1fa47decefc + darwin.x86_64 c2c15e08df0e8fbc374c335b230a7ee958c313fa5714817a59aa59f1aa594f51 + darwin.aarch64 339b930feb1ea764467013cc1f72d09cd6b869ebf1013296ba9055ab2ffbd26f + " + + archive="shellcheck-v${version}.${ASSET}.tar.gz" + url="https://github.com/koalaman/shellcheck/releases/download/v${version}/${archive}" + dest="${RUNNER_TEMP}/shellcheck/${version}" + archive_path="${RUNNER_TEMP}/${archive}" + mkdir -p "$dest" + + curl --fail --silent --show-error --location --output "$archive_path" "$url" + + want=$(echo "$checksums" | awk -v a="$ASSET" '$1==a{print $2}') + got=$(sha256_of "$archive_path") + if [ "$got" != "$want" ]; then + echo "::error::shellcheck checksum mismatch for ${archive}: expected ${want}, got ${got}" >&2 + exit 1 + fi + + tar -xzf "$archive_path" -C "$dest" --strip-components=1 "shellcheck-v${version}/shellcheck" + chmod +x "${dest}/shellcheck" + echo "$dest" >> "$GITHUB_PATH" + + - name: Register problem matcher + shell: bash + env: + MATCHER_PATH: ${{ github.action_path }}/actionlint-matcher.json + run: echo "::add-matcher::${MATCHER_PATH}" + + - name: Run actionlint + shell: bash + env: + CONFIG: ${{ inputs.config }} + EXTRA_ARGS: ${{ inputs.args }} + run: | + set -euo pipefail + + args=(-color) + if [ -n "$CONFIG" ]; then + args+=(-config-file "$CONFIG") + fi + if [ -n "$EXTRA_ARGS" ]; then + while IFS= read -r line; do + if [ -n "$line" ]; then + args+=("$line") + fi + done <<< "$EXTRA_ARGS" + fi + + actionlint "${args[@]}" diff --git a/actions/actionlint/actionlint-matcher.json b/actions/actionlint/actionlint-matcher.json new file mode 100644 index 0000000..4613e16 --- /dev/null +++ b/actions/actionlint/actionlint-matcher.json @@ -0,0 +1,17 @@ +{ + "problemMatcher": [ + { + "owner": "actionlint", + "pattern": [ + { + "regexp": "^(?:\\x1b\\[\\d+m)?(.+?)(?:\\x1b\\[\\d+m)*:(?:\\x1b\\[\\d+m)*(\\d+)(?:\\x1b\\[\\d+m)*:(?:\\x1b\\[\\d+m)*(\\d+)(?:\\x1b\\[\\d+m)*: (?:\\x1b\\[\\d+m)*(.+?)(?:\\x1b\\[\\d+m)* \\[(.+?)\\]$", + "file": 1, + "line": 2, + "column": 3, + "message": 4, + "code": 5 + } + ] + } + ] +} From 03de4d6c8961c454769c8591afabbedd12631825 Mon Sep 17 00:00:00 2001 From: Yordis Prieto Date: Sat, 10 Oct 2026 17:47:02 -0400 Subject: [PATCH 2/3] ci: lint workflows from a workflow that cannot break with them Signed-off-by: Yordis Prieto --- .github/workflows/ci.yml | 17 --------------- .github/workflows/workflow-lint.yml | 32 +++++++++++++++++++++++++++++ 2 files changed, 32 insertions(+), 17 deletions(-) create mode 100644 .github/workflows/workflow-lint.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 712ed69..131bc50 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -65,23 +65,6 @@ jobs: - run: mise run github:actions:ci:lint - # This repository lints its own workflows with the action it ships, so a - # change to that action is exercised by the pull request that makes the - # change, the same reason the runner job above uses runs-on-selector. - actionlint: - name: Actionlint (action) - needs: runner - runs-on: ${{ needs.runner.outputs.runs-on }} - timeout-minutes: 10 - permissions: - contents: read - steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - persist-credentials: false - - - uses: ./actions/actionlint - test: name: Test needs: runner diff --git a/.github/workflows/workflow-lint.yml b/.github/workflows/workflow-lint.yml new file mode 100644 index 0000000..f10c53f --- /dev/null +++ b/.github/workflows/workflow-lint.yml @@ -0,0 +1,32 @@ +name: Workflow lint + +# Its own workflow rather than a job in ci.yml: a workflow file that does not +# parse runs none of its jobs, so a lint job inside ci.yml could never report +# the one change most likely to break ci.yml. + +on: + push: + branches: [main] + pull_request: + +permissions: {} + +concurrency: + group: workflow-lint-${{ github.event.pull_request.number || github.run_id }} + cancel-in-progress: true + +jobs: + actionlint: + name: Actionlint (action) + runs-on: ubuntu-24.04 + timeout-minutes: 10 + permissions: + contents: read + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + # The repository lints itself with the action it ships, so a change to + # that action is exercised by the pull request that makes the change. + - uses: ./actions/actionlint From bf7eacdf204d1bdd14ee985d083c6f4af3930028 Mon Sep 17 00:00:00 2001 From: Yordis Prieto Date: Sat, 10 Oct 2026 17:47:02 -0400 Subject: [PATCH 3/3] refactor(actionlint): keep step logic in scripts that can be checked and run on their own Signed-off-by: Yordis Prieto --- actions/actionlint/action.yml | 157 +------------------ actions/actionlint/lib/common.sh | 33 ++++ actions/actionlint/lib/install-actionlint.sh | 43 +++++ actions/actionlint/lib/install-shellcheck.sh | 33 ++++ actions/actionlint/lib/run.sh | 16 ++ 5 files changed, 133 insertions(+), 149 deletions(-) create mode 100644 actions/actionlint/lib/common.sh create mode 100755 actions/actionlint/lib/install-actionlint.sh create mode 100755 actions/actionlint/lib/install-shellcheck.sh create mode 100755 actions/actionlint/lib/run.sh diff --git a/actions/actionlint/action.yml b/actions/actionlint/action.yml index fd3cb05..310d96b 100644 --- a/actions/actionlint/action.yml +++ b/actions/actionlint/action.yml @@ -36,170 +36,29 @@ inputs: runs: using: composite steps: - - name: Resolve platform - id: platform - shell: bash - env: - RUNNER_OS: ${{ runner.os }} - RUNNER_ARCH: ${{ runner.arch }} - run: | - set -euo pipefail - - case "${RUNNER_OS}-${RUNNER_ARCH}" in - Linux-X64) - actionlint_asset=linux_amd64 - shellcheck_asset=linux.x86_64 - ;; - Linux-ARM64) - actionlint_asset=linux_arm64 - shellcheck_asset=linux.aarch64 - ;; - macOS-ARM64) - actionlint_asset=darwin_arm64 - shellcheck_asset=darwin.aarch64 - ;; - macOS-X64) - actionlint_asset=darwin_amd64 - shellcheck_asset=darwin.x86_64 - ;; - *) - echo "::error::Unsupported runner ${RUNNER_OS}/${RUNNER_ARCH}. This action supports Linux X64/ARM64 and macOS X64/ARM64." >&2 - exit 1 - ;; - esac - - echo "actionlint-asset=${actionlint_asset}" >> "$GITHUB_OUTPUT" - echo "shellcheck-asset=${shellcheck_asset}" >> "$GITHUB_OUTPUT" - - name: Install actionlint shell: bash env: VERSION: ${{ inputs.version }} - ASSET: ${{ steps.platform.outputs.actionlint-asset }} - run: | - set -euo pipefail - - sha256_of() { - if command -v sha256sum >/dev/null 2>&1; then - sha256sum "$1" | cut -d ' ' -f 1 - else - shasum -a 256 "$1" | cut -d ' ' -f 1 - fi - } - - # Checksums rhysd/actionlint published for the version this action - # defaults to. Verified offline, so the default path needs nothing - # from the network beyond the archive itself. - checksums=" - 1.7.12 linux_amd64 8aca8db96f1b94770f1b0d72b6dddcb1ebb8123cb3712530b08cc387b349a3d8 - 1.7.12 linux_arm64 325e971b6ba9bfa504672e29be93c24981eeb1c07576d730e9f7c8805afff0c6 - 1.7.12 darwin_amd64 5b44c3bc2255115c9b69e30efc0fecdf498fdb63c5d58e17084fd5f16324c644 - 1.7.12 darwin_arm64 aba9ced2dee8d27fecca3dc7feb1a7f9a52caefa1eb46f3271ea66b6e0e6953f - " - - archive="actionlint_${VERSION}_${ASSET}.tar.gz" - url="https://github.com/rhysd/actionlint/releases/download/v${VERSION}/${archive}" - dest="${RUNNER_TEMP}/actionlint/${VERSION}" - archive_path="${RUNNER_TEMP}/${archive}" - mkdir -p "$dest" - - curl --fail --silent --show-error --location --output "$archive_path" "$url" - - want=$(echo "$checksums" | awk -v v="$VERSION" -v a="$ASSET" '$1==v && $2==a{print $3}') - if [ -z "$want" ]; then - # A version this action was not pinned against: fall back to the - # checksums file that release itself publishes, rather than - # installing a binary nothing has verified. - sums_url="https://github.com/rhysd/actionlint/releases/download/v${VERSION}/actionlint_${VERSION}_checksums.txt" - sums_path="${RUNNER_TEMP}/actionlint_${VERSION}_checksums.txt" - curl --fail --silent --show-error --location --output "$sums_path" "$sums_url" - want=$(awk -v f="$archive" '$2==f{print $1}' "$sums_path") - if [ -z "$want" ]; then - echo "::error::No checksum for ${archive} in ${sums_url}" >&2 - exit 1 - fi - fi - - got=$(sha256_of "$archive_path") - if [ "$got" != "$want" ]; then - echo "::error::actionlint checksum mismatch for ${archive}: expected ${want}, got ${got}" >&2 - exit 1 - fi - - tar -xzf "$archive_path" -C "$dest" actionlint - echo "$dest" >> "$GITHUB_PATH" + RUNNER_OS: ${{ runner.os }} + RUNNER_ARCH: ${{ runner.arch }} + run: bash "${GITHUB_ACTION_PATH}/lib/install-actionlint.sh" - name: Install shellcheck if: inputs.shellcheck == 'true' shell: bash env: - ASSET: ${{ steps.platform.outputs.shellcheck-asset }} - run: | - set -euo pipefail - - sha256_of() { - if command -v sha256sum >/dev/null 2>&1; then - sha256sum "$1" | cut -d ' ' -f 1 - else - shasum -a 256 "$1" | cut -d ' ' -f 1 - fi - } - - # Pinned to the one version this action ships. Letting any version - # through unverified via an input would defeat the point of checking - # a checksum at all; moving the pin means a new release of this - # action. - version="0.11.0" - checksums=" - linux.x86_64 b7af85e41cc99489dcc21d66c6d5f3685138f06d34651e6d34b42ec6d54fe6f6 - linux.aarch64 68a8133197a50beb8803f8d42f9908d1af1c5540d4bb05fdfca8c1fa47decefc - darwin.x86_64 c2c15e08df0e8fbc374c335b230a7ee958c313fa5714817a59aa59f1aa594f51 - darwin.aarch64 339b930feb1ea764467013cc1f72d09cd6b869ebf1013296ba9055ab2ffbd26f - " - - archive="shellcheck-v${version}.${ASSET}.tar.gz" - url="https://github.com/koalaman/shellcheck/releases/download/v${version}/${archive}" - dest="${RUNNER_TEMP}/shellcheck/${version}" - archive_path="${RUNNER_TEMP}/${archive}" - mkdir -p "$dest" - - curl --fail --silent --show-error --location --output "$archive_path" "$url" - - want=$(echo "$checksums" | awk -v a="$ASSET" '$1==a{print $2}') - got=$(sha256_of "$archive_path") - if [ "$got" != "$want" ]; then - echo "::error::shellcheck checksum mismatch for ${archive}: expected ${want}, got ${got}" >&2 - exit 1 - fi - - tar -xzf "$archive_path" -C "$dest" --strip-components=1 "shellcheck-v${version}/shellcheck" - chmod +x "${dest}/shellcheck" - echo "$dest" >> "$GITHUB_PATH" + RUNNER_OS: ${{ runner.os }} + RUNNER_ARCH: ${{ runner.arch }} + run: bash "${GITHUB_ACTION_PATH}/lib/install-shellcheck.sh" - name: Register problem matcher shell: bash - env: - MATCHER_PATH: ${{ github.action_path }}/actionlint-matcher.json - run: echo "::add-matcher::${MATCHER_PATH}" + run: echo "::add-matcher::${GITHUB_ACTION_PATH}/actionlint-matcher.json" - name: Run actionlint shell: bash env: CONFIG: ${{ inputs.config }} EXTRA_ARGS: ${{ inputs.args }} - run: | - set -euo pipefail - - args=(-color) - if [ -n "$CONFIG" ]; then - args+=(-config-file "$CONFIG") - fi - if [ -n "$EXTRA_ARGS" ]; then - while IFS= read -r line; do - if [ -n "$line" ]; then - args+=("$line") - fi - done <<< "$EXTRA_ARGS" - fi - - actionlint "${args[@]}" + run: bash "${GITHUB_ACTION_PATH}/lib/run.sh" diff --git a/actions/actionlint/lib/common.sh b/actions/actionlint/lib/common.sh new file mode 100644 index 0000000..9f799db --- /dev/null +++ b/actions/actionlint/lib/common.sh @@ -0,0 +1,33 @@ +# shellcheck shell=bash + +sha256_of() { + if command -v sha256sum >/dev/null 2>&1; then + sha256sum "$1" | cut -d ' ' -f 1 + else + shasum -a 256 "$1" | cut -d ' ' -f 1 + fi +} + +verify_sha256() { + local path="$1" want="$2" name="$3" got + got=$(sha256_of "$path") + if [ "$got" != "$want" ]; then + echo "::error::${name} checksum mismatch: expected ${want}, got ${got}" >&2 + exit 1 + fi +} + +# Prints the release asset suffix the named tool publishes for this runner. +platform_asset() { + local tool="$1" + case "${RUNNER_OS}-${RUNNER_ARCH}" in + Linux-X64) [ "$tool" = actionlint ] && echo linux_amd64 || echo linux.x86_64 ;; + Linux-ARM64) [ "$tool" = actionlint ] && echo linux_arm64 || echo linux.aarch64 ;; + macOS-ARM64) [ "$tool" = actionlint ] && echo darwin_arm64 || echo darwin.aarch64 ;; + macOS-X64) [ "$tool" = actionlint ] && echo darwin_amd64 || echo darwin.x86_64 ;; + *) + echo "::error::Unsupported runner ${RUNNER_OS}/${RUNNER_ARCH}. This action supports Linux X64/ARM64 and macOS X64/ARM64." >&2 + return 1 + ;; + esac +} diff --git a/actions/actionlint/lib/install-actionlint.sh b/actions/actionlint/lib/install-actionlint.sh new file mode 100755 index 0000000..5ae6c8b --- /dev/null +++ b/actions/actionlint/lib/install-actionlint.sh @@ -0,0 +1,43 @@ +#!/usr/bin/env bash +set -euo pipefail + +# shellcheck source=SCRIPTDIR/common.sh +source "$(dirname "${BASH_SOURCE[0]}")/common.sh" + +: "${VERSION:?VERSION is required}" +asset=$(platform_asset actionlint) + +# Checksums rhysd/actionlint published for the version this action defaults +# to, so the default path trusts nothing fetched alongside the archive. +checksums=" +1.7.12 linux_amd64 8aca8db96f1b94770f1b0d72b6dddcb1ebb8123cb3712530b08cc387b349a3d8 +1.7.12 linux_arm64 325e971b6ba9bfa504672e29be93c24981eeb1c07576d730e9f7c8805afff0c6 +1.7.12 darwin_amd64 5b44c3bc2255115c9b69e30efc0fecdf498fdb63c5d58e17084fd5f16324c644 +1.7.12 darwin_arm64 aba9ced2dee8d27fecca3dc7feb1a7f9a52caefa1eb46f3271ea66b6e0e6953f +" + +archive="actionlint_${VERSION}_${asset}.tar.gz" +release="https://github.com/rhysd/actionlint/releases/download/v${VERSION}" +dest="${RUNNER_TEMP}/actionlint/${VERSION}" +archive_path="${RUNNER_TEMP}/${archive}" +mkdir -p "$dest" + +curl --fail --silent --show-error --location --output "$archive_path" "${release}/${archive}" + +want=$(awk -v v="$VERSION" -v a="$asset" '$1==v && $2==a{print $3}' <<<"$checksums") +if [ -z "$want" ]; then + # A version this action was not pinned against: fall back to the checksums + # file that release publishes, rather than installing an unverified binary. + sums="actionlint_${VERSION}_checksums.txt" + curl --fail --silent --show-error --location --output "${RUNNER_TEMP}/${sums}" "${release}/${sums}" + want=$(awk -v f="$archive" '$2==f{print $1}' "${RUNNER_TEMP}/${sums}") + if [ -z "$want" ]; then + echo "::error::No checksum for ${archive} in ${release}/${sums}" >&2 + exit 1 + fi +fi + +verify_sha256 "$archive_path" "$want" "$archive" + +tar -xzf "$archive_path" -C "$dest" actionlint +echo "$dest" >>"$GITHUB_PATH" diff --git a/actions/actionlint/lib/install-shellcheck.sh b/actions/actionlint/lib/install-shellcheck.sh new file mode 100755 index 0000000..3e150c9 --- /dev/null +++ b/actions/actionlint/lib/install-shellcheck.sh @@ -0,0 +1,33 @@ +#!/usr/bin/env bash +set -euo pipefail + +# shellcheck source=SCRIPTDIR/common.sh +source "$(dirname "${BASH_SOURCE[0]}")/common.sh" + +asset=$(platform_asset shellcheck) + +# Pinned rather than an input: accepting any version would mean installing a +# binary with no checksum to hold it to. Moving the pin is a release of this +# action. +version="0.11.0" +checksums=" +linux.x86_64 b7af85e41cc99489dcc21d66c6d5f3685138f06d34651e6d34b42ec6d54fe6f6 +linux.aarch64 68a8133197a50beb8803f8d42f9908d1af1c5540d4bb05fdfca8c1fa47decefc +darwin.x86_64 c2c15e08df0e8fbc374c335b230a7ee958c313fa5714817a59aa59f1aa594f51 +darwin.aarch64 339b930feb1ea764467013cc1f72d09cd6b869ebf1013296ba9055ab2ffbd26f +" + +archive="shellcheck-v${version}.${asset}.tar.gz" +dest="${RUNNER_TEMP}/shellcheck/${version}" +archive_path="${RUNNER_TEMP}/${archive}" +mkdir -p "$dest" + +curl --fail --silent --show-error --location --output "$archive_path" \ + "https://github.com/koalaman/shellcheck/releases/download/v${version}/${archive}" + +want=$(awk -v a="$asset" '$1==a{print $2}' <<<"$checksums") +verify_sha256 "$archive_path" "$want" "$archive" + +tar -xzf "$archive_path" -C "$dest" --strip-components=1 "shellcheck-v${version}/shellcheck" +chmod +x "${dest}/shellcheck" +echo "$dest" >>"$GITHUB_PATH" diff --git a/actions/actionlint/lib/run.sh b/actions/actionlint/lib/run.sh new file mode 100755 index 0000000..34c80a3 --- /dev/null +++ b/actions/actionlint/lib/run.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env bash +set -euo pipefail + +args=(-color) +if [ -n "${CONFIG:-}" ]; then + args+=(-config-file "$CONFIG") +fi +if [ -n "${EXTRA_ARGS:-}" ]; then + while IFS= read -r line; do + if [ -n "$line" ]; then + args+=("$line") + fi + done <<<"$EXTRA_ARGS" +fi + +actionlint "${args[@]}"