Skip to content
Merged
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
73 changes: 65 additions & 8 deletions .github/workflows/lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,19 @@ name: Lint
# See callers/lint.yml in the templates dir for a copy-paste-ready caller.
#
# What it checks:
# - actionlint on every .github/workflows/*.yml (catches GHA-specific bugs +
# embedded shellcheck for `run:` blocks)
# - actionlint on every .github/workflows/*.yml (catches GHA-specific bugs)
# - prettier --check on the markdown glob (default **/*.md)
#
# Why no husky/lint-staged: this is meant to plug into repos with mixed
# tooling (or none). CI-only enforcement keeps the dependency surface flat.
# Defaults chosen for "drop into an existing repo without breaking it":
# - actionlint runs WITHOUT shellcheck. Shellcheck integration in actionlint
# defaults to severity=info, which surfaces stylistic suggestions
# (SC2086 "double quote variables", SC2034 "unused variable", etc.) on
# every existing `run:` block in the target repo. New repos rarely need
# to fix all of those before they can ship a single PR. Set
# `run_shellcheck: true` to opt in once the repo is normalized.
# - prettier auto-installs the target repo's deps if a package.json exists,
# so prettier plugins (prettier-plugin-svelte etc.) resolve correctly.
# Skip with `install_node_deps: false` for plain markdown-only repos.

on:
pull_request:
Expand All @@ -41,14 +48,29 @@ on:
required: false
type: boolean
default: true
run_shellcheck:
description: "Enable actionlint's shellcheck integration. Default false to avoid info-level style noise on first install."
required: false
type: boolean
default: false
install_node_deps:
description: "Run `npm ci` before prettier so plugins (prettier-plugin-svelte etc.) resolve. Default true. Skipped automatically when no package.json present."
required: false
type: boolean
default: true

permissions:
contents: read

jobs:
actionlint:
name: actionlint
if: ${{ github.event_name != 'workflow_call' || inputs.run_actionlint }}
# Skip-logic for the run_actionlint input. github.event_name returns the
# CALLER's event in a reusable workflow (not 'workflow_call'), and the
# `inputs` object is `{}` (not null) on non-workflow_call triggers — so
# the only reliable check is "input is null OR input is truthy". Null on
# self-test (no workflow_call), false only when caller explicitly opts out.
if: ${{ inputs.run_actionlint == null || inputs.run_actionlint }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
Expand All @@ -59,19 +81,54 @@ jobs:
shell: bash

- name: Run actionlint
run: ${{ steps.get_actionlint.outputs.executable }} -color
# `-shellcheck=` (empty) disables the shellcheck integration. We default
# to off because info-level shellcheck warnings (SC2086 etc.) are
# noise on most repos and would block the install PR until every
# existing `run:` block is normalized. inputs.run_shellcheck defaults
# to false, but on self-test the inputs object is `{}` (not null
# via the schema), so we check for `== true` to opt in explicitly.
run: |
if [ "${{ inputs.run_shellcheck == true && 'true' || 'false' }}" = "true" ]; then
${{ steps.get_actionlint.outputs.executable }} -color
else
${{ steps.get_actionlint.outputs.executable }} -color -shellcheck=
fi
shell: bash

prettier:
name: prettier (markdown)
if: ${{ github.event_name != 'workflow_call' || inputs.run_prettier }}
if: ${{ inputs.run_prettier == null || inputs.run_prettier }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- uses: actions/setup-node@v4
with:
node-version: "20"
cache: ${{ hashFiles('**/package-lock.json') != '' && 'npm' || '' }}

# If the target repo has a prettier config that references plugins
# (e.g. prettier-plugin-svelte), prettier needs the project's
# node_modules to resolve them. Run `npm ci` (preferred, exact lockfile)
# or fall back to `npm install` if no lockfile.
- name: Install target repo's deps (for prettier plugins)
if: ${{ (inputs.install_node_deps == null || inputs.install_node_deps) && hashFiles('**/package.json') != '' }}
run: |
if [ -f package-lock.json ]; then
npm ci --ignore-scripts --no-audit --no-fund
else
npm install --ignore-scripts --no-audit --no-fund
fi
shell: bash

- name: prettier --check ${{ inputs.markdown_glob || '**/*.md' }}
run: npx --yes prettier@3 --check "${{ inputs.markdown_glob || '**/*.md' }}"
# Use the project's prettier (with its plugins) when node_modules is
# populated; otherwise fall back to a one-off prettier@3 install.
run: |
GLOB="${{ inputs.markdown_glob || '**/*.md' }}"
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Script injection via inputs.markdown_glob

${{ inputs.markdown_glob }} is interpolated directly into the shell script body. A caller that passes a value containing shell metacharacters — e.g. **/*.md"; malicious_cmd; echo " — would execute arbitrary code in the runner. This is the injection pattern GitHub's security-hardening guide explicitly warns against.

Fix: bind the expression to an environment variable at the step level and reference that inside run:

      - name: prettier --check ${{ inputs.markdown_glob || '**/*.md' }}
        env:
          GLOB: ${{ inputs.markdown_glob || '**/*.md' }}
        run: |
          if [ -x node_modules/.bin/prettier ]; then
            node_modules/.bin/prettier --check "$GLOB"
          else
            npx --yes prettier@3 --check "$GLOB"
          fi
        shell: bash

With the env-var approach, the expression value is never parsed by the shell as code — it's handed in through the environment.

if [ -x node_modules/.bin/prettier ]; then
node_modules/.bin/prettier --check "$GLOB"
else
npx --yes prettier@3 --check "$GLOB"
fi
shell: bash
Loading