This repository contains a reusable GitHub Action that checks out a repository, sets up the required toolchains, discovers runnable projects, and executes standard checks for each one.
Current support:
- Node projects detected by
package.json - Python projects detected by
pyproject.toml
Behavior:
- Single-project repo: runs checks for the one discovered project
- Multi-project repo: runs checks for every discovered project root
- Release Please pull requests are skipped when the PR only changes
.release-please-manifest.json,CHANGELOG.md,package.json, andpackage-lock.json, and both the manifest and a changelog are present in the diff - Optional changed-only mode: limits execution to project roots with changed files
- on pull requests, changed files are calculated from the git merge-base to avoid selecting projects changed only on the base branch
- on pushes, changed files are calculated from the previous pushed commit to
HEAD - changed-only requires both comparison refs to exist in the local checkout, so keep
fetch-depth: 0 - for
pull_request_target,HEADis usually the base branch unless you explicitly check out the pull request head commit
Node checks:
npm run formatnpm run lintnpm run testnpm run build
format and lint are required scripts for Node projects.
Format-script enforcement:
formatmust be a standalone Prettier command (no shell operators such as&&,||,;,|)- when format enforcement is needed, the command must invoke
prettierdirectly (wrapper commands likenpx prettier ...orcross-env ... prettier ...are rejected) - if
formatis not already in check mode, the action rewrites it to check mode by removing--writevariants and enforcing--check - rewritten format commands are executed with
npm exec -- prettier ...so local tool resolution still happens through npm
test and build remain optional. Missing optional scripts are logged and do not fail the action.
Python checks:
uv run ruff format --check .uv run ruff check .
Python checks only run when the action detects Ruff usage in pyproject.toml. Otherwise the action emits a warning and continues.
Minimal usage:
name: CI
on:
pull_request:
push:
branches: [main]
jobs:
checks:
runs-on: ubuntu-latest
steps:
- uses: elementx-ai/code-quality-check@main
with:
changed-only: true
base-ref: ${{ github.event.pull_request.base.sha || github.event.before }}If you use pull_request_target, do your own checkout first so HEAD points at the PR head commit:
- uses: actions/checkout@v6
with:
ref: ${{ github.event.pull_request.head.sha }}
fetch-depth: 0
- uses: elementx-ai/code-quality-check@main
with:
checkout: false
changed-only: true
base-ref: ${{ github.event.pull_request.base.sha }}Depth control:
project-depth: 0means only the working directory itself is consideredproject-depth: 1means the working directory plus direct child foldersproject-depth: -1means unlimited depth and preserves the current broad discovery behavior
What the action handles internally:
actions/checkout@v6actions/setup-node@v6when apackage.jsonis detectedactions/setup-python@v6andastral-sh/setup-uv@v7when apyproject.tomlis detected- automatic Node dependency installation with
npm ciwhen a lockfile can be resolved
Node install behavior:
- if the repo root is an npm workspace with a root lockfile, it runs one root
npm ci - otherwise, it runs
npm ciinside each selected Node project that has its own lockfile - if no
package-lock.jsonornpm-shrinkwrap.jsonis available for a selected Node project, the action warns and continues
Important constraint:
- Python projects still need a usable
uvproject configuration
If you want, the next iteration can add an optional install phase with repo-specific heuristics.
Useful inputs:
checkout: defaulttruefetch-depth: default0auto-setup: defaulttrueauto-install: defaulttrueproject-depth: default-1node-version: default24node-install-command: defaultnpm cipython-version: default3.12uv-version: optionalchanged-only: defaulttruebase-ref: optionalhead-ref: defaultHEAD
repo_modeproject_countselected_project_countproject_pathsselected_project_pathsdetected_ecosystemspassed_project_pathsfailed_project_pathsexecution_results
passed_project_paths and failed_project_paths are JSON arrays, so downstream workflows can query them with fromJSON(...).
Example:
- id: quality
uses: elementx-ai/code-quality-check@main
with:
changed-only: true
base-ref: ${{ github.event.pull_request.base.sha || github.event.before }}
- name: React to evaluator failure
if: ${{ contains(fromJSON(steps.quality.outputs.failed_project_paths), 'evaluator') }}
run: echo "evaluator failed quality checks"npm install
npm test
npm run buildThis repo includes Release Please. On pushes to main, it opens or updates a release PR. When that PR is merged, Release Please creates the Git tag and GitHub release automatically.