diff --git a/.github/scripts/qc-actionlint-setup.sh b/.github/scripts/qc-actionlint-setup.sh new file mode 100755 index 0000000..f42d39b --- /dev/null +++ b/.github/scripts/qc-actionlint-setup.sh @@ -0,0 +1,28 @@ +#!/usr/bin/env bash +# Install a pinned actionlint binary via the upstream download script. +# Sets ACTIONLINT_BIN and prepends the install dir to PATH (GitHub Actions). +set -euo pipefail + +ACTIONLINT_VERSION="${ACTIONLINT_VERSION:-1.7.12}" +install_dir="${ACTIONLINT_INSTALL_DIR:-${RUNNER_TEMP:-/tmp}/actionlint}" + +mkdir -p "${install_dir}" +script_url="https://raw.githubusercontent.com/rhysd/actionlint/v${ACTIONLINT_VERSION}/scripts/download-actionlint.bash" +bash <(curl -fsSL "${script_url}") "${ACTIONLINT_VERSION}" "${install_dir}" + +ACTIONLINT_BIN="${install_dir}/actionlint" +export ACTIONLINT_BIN + +if [[ -n "${GITHUB_PATH:-}" ]]; then + echo "${install_dir}" >>"${GITHUB_PATH}" +fi + +if [[ -n "${GITHUB_ENV:-}" ]]; then + echo "ACTIONLINT_BIN=${ACTIONLINT_BIN}" >>"${GITHUB_ENV}" +fi + +if [[ -n "${GITHUB_OUTPUT:-}" ]]; then + echo "executable=${ACTIONLINT_BIN}" >>"${GITHUB_OUTPUT}" +fi + +"${ACTIONLINT_BIN}" -version diff --git a/.github/scripts/qc-filter-changed-paths.sh b/.github/scripts/qc-filter-changed-paths.sh new file mode 100755 index 0000000..5bea073 --- /dev/null +++ b/.github/scripts/qc-filter-changed-paths.sh @@ -0,0 +1,22 @@ +#!/usr/bin/env bash +# Drop dependency and build-output paths from PR-scoped changed-file lists. +# Reads paths from stdin; prints kept paths to stdout. +set -euo pipefail + +qc_should_skip_changed_path() { + local path="$1" + case "${path}" in + node_modules/* | */node_modules/*) return 0 ;; + vendor/* | */vendor/*) return 0 ;; + dist/* | */dist/*) return 0 ;; + build/* | */build/*) return 0 ;; + coverage/* | */coverage/*) return 0 ;; + esac + return 1 +} + +while IFS= read -r path || [[ -n "${path}" ]]; do + [[ -z "${path}" ]] && continue + qc_should_skip_changed_path "${path}" && continue + printf '%s\n' "${path}" +done diff --git a/.github/workflows/accessibility-checks.yaml b/.github/workflows/accessibility-checks.yaml index 7edf591..58299fd 100644 --- a/.github/workflows/accessibility-checks.yaml +++ b/.github/workflows/accessibility-checks.yaml @@ -22,7 +22,7 @@ on: node_version: description: Node.js version for axe-core and Lighthouse CI tooling. required: false - default: '20' + default: '24' type: string axe_run: description: > diff --git a/.github/workflows/quality-checks.yaml b/.github/workflows/quality-checks.yaml index 30d2dbe..003a362 100644 --- a/.github/workflows/quality-checks.yaml +++ b/.github/workflows/quality-checks.yaml @@ -24,7 +24,7 @@ on: Node.js version for ESLint, markdownlint-cli2, Prettier, and accessibility-related installs in sibling workflows. required: false - default: '20' + default: '24' type: string go_version: description: Go toolchain version for golangci-lint. @@ -169,17 +169,14 @@ jobs: fetch-depth: 0 - name: 'Install actionlint' + id: actionlint-install env: # Pin release; bump intentionally when upgrading. - ACTIONLINT_VERSION: v1.7.7 + ACTIONLINT_VERSION: '1.7.12' run: | set -euo pipefail - tag="${ACTIONLINT_VERSION#v}" - url="https://github.com/rhysd/actionlint/releases/download/${ACTIONLINT_VERSION}/actionlint_${tag}_linux_amd64.tar.gz" - curl -fsSL "$url" -o actionlint.tar.gz - tar -xzf actionlint.tar.gz actionlint - sudo mv actionlint /usr/local/bin/ - rm actionlint.tar.gz + # shellcheck source=.github/scripts/qc-actionlint-setup.sh + source .github/scripts/qc-actionlint-setup.sh - name: 'Get changed GitHub Actions files' id: changed-files @@ -235,7 +232,7 @@ jobs: echo 'No existing paths to lint' exit 0 fi - actionlint "${paths[@]}" + "${ACTIONLINT_BIN:-actionlint}" "${paths[@]}" eslint: if: ${{ inputs.eslint_run == true }} @@ -494,7 +491,8 @@ jobs: fi mapfile -t CHANGED_FILES < <( git diff --name-only --diff-filter=ACMR "${BASE_REF}"...HEAD | - grep -E '\.(md|markdown)$' || true + grep -E '\.(md|markdown)$' | + .github/scripts/qc-filter-changed-paths.sh || true ) if [[ ${#CHANGED_FILES[@]} -gt 0 ]] && \ [[ -n "${CHANGED_FILES[0]}" ]]; then @@ -571,7 +569,8 @@ jobs: mapfile -t CHANGED_FILES < <( git diff --name-only --diff-filter=ACMR "${BASE_REF}"...HEAD | grep -E \ - '\.(js|jsx|ts|tsx|json|css|scss|md|markdown)$' || true + '\.(js|jsx|ts|tsx|json|css|scss|md|markdown)$' | + .github/scripts/qc-filter-changed-paths.sh || true ) if [[ ${#CHANGED_FILES[@]} -gt 0 ]] && \ [[ -n "${CHANGED_FILES[0]}" ]]; then @@ -747,7 +746,7 @@ jobs: echo 'No existing paths to lint' exit 0 fi - shellcheck "${paths[@]}" + shellcheck -x "${paths[@]}" stylua: if: ${{ inputs.stylua_run == true }} diff --git a/.github/workflows/test-workflow.yaml b/.github/workflows/test-workflow.yaml index 2a60bd1..2e523bd 100644 --- a/.github/workflows/test-workflow.yaml +++ b/.github/workflows/test-workflow.yaml @@ -8,7 +8,7 @@ on: jobs: linters: - name: 'Suite 1 — Linters' + name: 'Linters' uses: ./.github/workflows/quality-checks.yaml with: diff_base: ${{ github.event.pull_request.base.sha || '' }} @@ -29,7 +29,7 @@ jobs: secrets: inherit accessibility: - name: 'Suite 2 — Accessibility' + name: 'Accessibility Audits' uses: ./.github/workflows/accessibility-checks.yaml with: diff_base: ${{ github.event.pull_request.base.sha || '' }} diff --git a/AGENTS.md b/AGENTS.md index ba53dbb..fe894c5 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,20 +1,20 @@ # Agent guide — quality-checks -Reusable GitHub Actions for **Suite 1 (linters)** and **Suite 2 (accessibility)** on -pull-request diffs. Keep workflows **opt-in**, **PR-scoped**, and **pinned to commit -SHAs** for third-party actions. +Reusable GitHub Actions for **Linters** and **Accessibility Audits** on pull-request +diffs. Keep workflows **opt-in**, **PR-scoped**, and **pinned to commit SHAs** for +third-party actions. ## Repository layout -| Path | Purpose | -| --------------------------------------------- | -------------------------------------------- | -| `.github/workflows/quality-checks.yaml` | Reusable linter workflow (Suite 1) | -| `.github/workflows/accessibility-checks.yaml` | Reusable axe + Lighthouse workflow (Suite 2) | -| `.github/workflows/test-workflow.yaml` | Self-test on PRs (both suites) | -| `.github/scripts/qc-resolve-base.sh` | Shared diff-base resolution for jobs | -| `scripts/lint-changed.sh` | Run local linters matching changed files | -| `docs/assets/` | Branding (logo SVG) | -| `docs/fixtures/a11y-sample.html` | axe self-test fixture | +| Path | Purpose | +| --------------------------------------------- | ---------------------------------------- | +| `.github/workflows/quality-checks.yaml` | Reusable linter workflow | +| `.github/workflows/accessibility-checks.yaml` | Reusable axe + Lighthouse workflow | +| `.github/workflows/test-workflow.yaml` | Self-test on PRs (both workflows) | +| `.github/scripts/qc-resolve-base.sh` | Shared diff-base resolution for jobs | +| `scripts/lint-changed.sh` | Run local linters matching changed files | +| `docs/assets/` | Branding (logo SVG) | +| `docs/fixtures/a11y-sample.html` | axe self-test fixture | ## Workflow conventions @@ -73,7 +73,7 @@ npm run lint:docs ./scripts/lint-changed.sh ``` -Or the full Suite 1 pass: +Or the full Linters pass: ```bash npm run lint @@ -89,26 +89,26 @@ LINT_BASE=origin/main ./scripts/lint-changed.sh ### If you edited … also run … -| Paths you changed | Additional checks | -| ---------------------------------------------------------------------------------- | ---------------------------------------------------------- | -| `.github/workflows/*.yaml`, `test-workflow.yaml` | `npm run lint:yaml`, `npm run lint:workflows` | -| `.github/ISSUE_TEMPLATE/**`, `dependabot.yaml`, `.yamllint`, `.markdownlint*.yaml` | `npm run lint:yaml` | -| `.github/scripts/*.sh`, `scripts/*.sh` | `npm run lint:shell`, `shellcheck ` | -| `*.html` | axe via `lint-changed.sh` (optional locally) or CI Suite 2 | -| Workflow inputs / README tables | Keep **README.md** and workflow `inputs` in sync | +| Paths you changed | Additional checks | +| ---------------------------------------------------------------------------------- | ----------------------------------------------------------------------- | +| `.github/workflows/*.yaml`, `test-workflow.yaml` | `npm run lint:yaml`, `npm run lint:workflows` | +| `.github/ISSUE_TEMPLATE/**`, `dependabot.yaml`, `.yamllint`, `.markdownlint*.yaml` | `npm run lint:yaml` | +| `.github/scripts/*.sh`, `scripts/*.sh` | `npm run lint:shell` (`shfmt` + `shellcheck -x` on all scripts) | +| `*.html` | axe via `lint-changed.sh` (optional locally) or CI Accessibility Audits | +| Workflow inputs / README tables | Keep **README.md** and workflow `inputs` in sync | -### Suite mapping (CI vs local) +### Workflow mapping (CI vs local) -| Suite | CI workflow | Local equivalent | -| --------------------- | --------------------------------------------- | --------------------------------------------------------------------------------------------------------------------- | -| **1 — Linters** | `test-workflow` → `quality-checks.yaml` | `npm run lint` (includes `lint:docs`) | -| **2 — Accessibility** | `test-workflow` → `accessibility-checks.yaml` | axe on changed `.html` via `lint-changed.sh`; Lighthouse needs deployed `urls` (CI only unless you pass URLs locally) | +| Workflow | CI workflow | Local equivalent | +| ------------------------ | --------------------------------------------- | --------------------------------------------------------------------------------------------------------------------- | +| **Linters** | `test-workflow` → `quality-checks.yaml` | `npm run lint` (includes `lint:docs`) | +| **Accessibility Audits** | `test-workflow` → `accessibility-checks.yaml` | axe on changed `.html` via `lint-changed.sh`; Lighthouse needs deployed `urls` (CI only unless you pass URLs locally) | ### Tool install (if a script reports “skipped”) | Tool | Install | | ------------------------------- | ----------------------------------------------------------------------------------------------------------- | -| **actionlint** | `brew install actionlint` | +| **actionlint** | `brew install actionlint` (CI uses `.github/scripts/qc-actionlint-setup.sh`, pinned `1.7.12`) | | **yamllint** | `pip install yamllint` | | **shfmt** | `brew install shfmt` | | **shellcheck** | `brew install shellcheck` | @@ -119,6 +119,8 @@ LINT_BASE=origin/main ./scripts/lint-changed.sh ### Checklist before handoff - [ ] **`npm run fix:docs`** exits 0 (or `npm run lint:docs` if you did not need to write) +- [ ] **`npm run lint:shell`** exits 0 when you changed `.github/scripts/*.sh` or `scripts/*.sh` (use + `shellcheck -x` so sourced helpers are analyzed) - [ ] **`npm run lint`** or **`./scripts/lint-changed.sh`** exits 0 when you changed workflows, shell, or config - [ ] If workflows or inputs changed, README tables updated - [ ] New third-party actions use full commit SHAs diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 6680bf6..62390aa 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -26,18 +26,19 @@ npm run fix:docs npm run lint:changed ``` -`fix:docs` formats and lints **all** Markdown (required on every PR). For a full Suite 1 pass: +`fix:docs` formats and lints **all** Markdown (required on every PR). For a full Linters pass: ```bash npm run lint ``` Install optional tools listed in **AGENTS.md** when `lint-changed` reports a skip. -**`npm run lint`** mirrors CI **Suite 1 — Linters**; Suite 2 (accessibility) runs in -CI via `test-workflow.yaml`. +**`npm run lint`** mirrors CI **Linters**; **Accessibility Audits** run in CI via +`test-workflow.yaml`. Shell script changes require **shfmt** and **shellcheck** (`brew install shfmt shellcheck`, -or `npm run lint:shell` after installing shfmt). +or `npm run lint:shell` after installing both). Use **`shellcheck -x`** so sourced files under +`.github/scripts/` are checked. Documentation-only changes still need **Prettier** and **Markdownlint** on touched Markdown files. diff --git a/README.md b/README.md index efc94d2..cea2d27 100644 --- a/README.md +++ b/README.md @@ -14,9 +14,9 @@

- Suite 1 runs opt-in linters only on changed files. Suite 2 runs axe-core and - Lighthouse CI against deployed URLs and/or local HTML — tuned for fast, - actionable feedback in pull requests. + Linters run opt-in checks only on changed files. Accessibility Audits run + axe-core and Lighthouse CI against deployed URLs and/or local HTML — tuned + for fast, actionable feedback in pull requests.

@@ -30,12 +30,12 @@ alt="PR-scoped checks" /> Suite 1: Linters Suite 2: Accessibility

@@ -68,17 +68,17 @@ workflows](https://docs.github.com/en/actions/using-workflows/reusing-workflows) for pull requests: -| Suite | Workflow | Purpose | -| --------------------- | ---------------------------------------------------------------------------- | -------------------------------------------- | -| **1 — Linters** | [`quality-checks.yaml`](./.github/workflows/quality-checks.yaml) | Language and config linters on the PR diff | -| **2 — Accessibility** | [`accessibility-checks.yaml`](./.github/workflows/accessibility-checks.yaml) | axe-core and Lighthouse accessibility audits | +| Workflow | Reusable workflow | Purpose | +| ------------------------ | ---------------------------------------------------------------------------- | -------------------------------------------- | +| **Linters** | [`quality-checks.yaml`](./.github/workflows/quality-checks.yaml) | Language and config linters on the PR diff | +| **Accessibility Audits** | [`accessibility-checks.yaml`](./.github/workflows/accessibility-checks.yaml) | axe-core and Lighthouse accessibility audits | Each tool is **opt-in** (`*_run` defaults to `false`). Jobs no-op when nothing relevant changed, keeping runs fast. ## Quick start -### Suite 1 — Linters +### Linters ```yaml name: 'Quality Checks' @@ -87,7 +87,7 @@ on: pull_request jobs: linters: - name: 'Suite 1 — Linters' + name: 'Linters' uses: garretpatten/quality-checks/.github/workflows/quality-checks.yaml@master with: diff_base: ${{ github.event.pull_request.base.sha }} @@ -107,7 +107,7 @@ jobs: secrets: inherit ``` -### Suite 2 — Accessibility +### Accessibility Audits ```yaml name: 'Accessibility Checks' @@ -116,7 +116,7 @@ on: pull_request jobs: accessibility: - name: 'Suite 2 — Accessibility' + name: 'Accessibility Audits' uses: garretpatten/quality-checks/.github/workflows/accessibility-checks.yaml@master with: axe_run: true @@ -145,12 +145,12 @@ with: diff_base: ${{ github.event.pull_request.base.sha }} ``` -## Suite 1 — Linter inputs +## Linter inputs | Parameter | Type | Default | Description | | ------------------- | ------- | ----------- | --------------------------------------------- | | `diff_base` | string | _(empty)_ | Git ref to diff against | -| `node_version` | string | `20` | Node.js for ESLint, markdownlint, Prettier | +| `node_version` | string | `24` | Node.js for ESLint, markdownlint, Prettier | | `go_version` | string | `stable` | Go toolchain for golangci-lint | | `stylua_args` | string | `--check .` | Arguments for StyLua | | `stylua_version` | string | `v2.4.1` | StyLua release tag | @@ -171,12 +171,12 @@ with: | `typos_config` | string | _(empty)_ | Path to `typos.toml` (auto-detected if empty) | | `yamllint_run` | boolean | `false` | Yamllint | -## Suite 2 — Accessibility inputs +## Accessibility inputs | Parameter | Type | Default | Description | | ------------------------------ | ------- | --------- | ----------------------------------------------------- | | `diff_base` | string | _(empty)_ | Git ref for changed `.html` detection | -| `node_version` | string | `20` | Node.js for axe-core CLI | +| `node_version` | string | `24` | Node.js for axe-core CLI | | `axe_run` | boolean | `false` | Run axe-core audits | | `lighthouse_run` | boolean | `false` | Run Lighthouse CI (accessibility category) | | `urls` | string | _(empty)_ | URLs to audit (one per line; required for Lighthouse) | @@ -187,7 +187,7 @@ with: If the consumer repo already has `lighthouserc.json` or `lighthouserc.js`, that config is used instead of the generated accessibility-only defaults. -## Supported linters (Suite 1) +## Supported linters | Tool | Files | Notes | | ----------------- | -------------------------------------- | ------------------------------------------------- | @@ -239,7 +239,7 @@ Biome-as-full-replacement) or need heavy language SDKs (RuboCop, Clippy, `terraf are out of scope so the workflow stays maintainable. Open an issue if you need a narrow, high-signal addition. -## Accessibility (Suite 2) +## Accessibility | Job | Tool | When to use | | -------------- | -------------------------------------------------------------- | --------------------------------------------------------------- | diff --git a/package.json b/package.json index e235b5e..e769c2c 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,7 @@ "lint:md": "markdownlint-cli2", "lint:yaml": "yamllint .github .yamllint .markdownlint.yaml", "lint:workflows": "actionlint", - "lint:shell": "shfmt -d .github/scripts/*.sh scripts/*.sh" + "lint:shell": "shfmt -d .github/scripts/*.sh scripts/*.sh && shellcheck -x .github/scripts/*.sh scripts/*.sh" }, "devDependencies": { "markdownlint-cli2": "^0.18.1", diff --git a/scripts/lint-changed.sh b/scripts/lint-changed.sh index 2339371..34ed462 100755 --- a/scripts/lint-changed.sh +++ b/scripts/lint-changed.sh @@ -27,7 +27,7 @@ mapfile -t changed < <( git diff --name-only --diff-filter=ACMR "${BASE_REF}"...HEAD 2>/dev/null || true git diff --name-only git diff --cached --name-only - } | sed '/^$/d' | sort -u + } | sed '/^$/d' | sort -u | .github/scripts/qc-filter-changed-paths.sh ) if [[ ${#changed[@]} -eq 0 ]]; then @@ -103,7 +103,7 @@ run_shellcheck() { return 0 fi echo '→ shellcheck' - shellcheck "${files[@]}" + shellcheck -x "${files[@]}" } run_typos() {