Sign inSign up

zaventh/lintorama

By zaventh

•Updated 15 days ago

One Docker image, many linters, a single command.

Image
0

1.1K

zaventh/lintorama repository overview

lintorama — one Docker image, many linters, a single command

License: MIT Source Docker Hub Base image Linters

⁠lintorama

lintorama is a single Docker image that bundles nine widely-used linters and validators — yamllint⁠, ShellCheck⁠, hadolint⁠, markdownlint (mdl)⁠, luacheck⁠, Ruff⁠, actionlint⁠, check-jsonschema⁠, and editorconfig-checker⁠ — behind one entrypoint, lint-extras. Point it at a Git repository and it runs the right check over every tracked YAML, shell, Lua, Dockerfile, Markdown, and GitHub Actions workflow file, syntax-checks JSON, JSONC, and TOML, lints the Python in supporting-script directories (scripts/, build/, .claude/hooks/), schema-validates common config files (CI pipelines, Docker Compose, GitHub issue forms and actions, Dependabot, Renovate, and more), and — when an .editorconfig is present — verifies every tracked file against it, then exits non-zero if any check fails. Dependency directories such as node_modules/ and vendor/ are never linted. It is built for CI pipelines: no per-project linter installs, no juggling tool versions, no bespoke setup — just docker run.

In short: one container image for polyglot static analysis and code-quality checks in continuous integration.

⁠Contents

⁠Highlights

  • Nine linters, one image. YAML, JSON, TOML, shell, Dockerfiles, Markdown, Lua, Python scripts, GitHub Actions workflows, schema-backed config files, and .editorconfig conformance are all covered by a single pull.
  • Zero setup to start. Sensible defaults work out of the box; every linter still honors its own config file when you want to tune it.
  • CI-native. A single lint-extras command lints an entire repository and returns a meaningful exit code for your pipeline.
  • Reproducible. Tool versions are pinned in the image, so every run uses the exact same linters everywhere.
  • Small footprint. Built on python:3-alpine3.24.
  • Native on amd64 and arm64. Every tag is a multi-arch image, so it runs without emulation on x86-64 CI runners and on Apple Silicon Macs alike.

⁠Bundled linters

ToolVersionChecks
yamllint⁠1.38.0YAML (*.yml, *.yaml)
ShellCheck⁠0.11.0Shell scripts (*.sh, *.bash, and extensionless scripts with a shell shebang)
hadolint⁠2.15.1Dockerfile, Containerfile, and variants such as Dockerfile.dev or app.Dockerfile, at any depth
markdownlint (mdl)⁠0.18.1Markdown (*.md, *.markdown)
luacheck⁠1.2.0Lua (*.lua)
Ruff⁠0.16.10Python in scripts/, build/, .claude/hooks/ (*.py and Python-shebang scripts)
actionlint⁠1.7.12GitHub Actions workflows (.github/workflows/*.yml)
check-jsonschema⁠0.38.0Schema validation: CI pipelines, Docker Compose, GitHub issue forms and actions, Dependabot, Renovate, and more⁠
editorconfig-checker⁠4.0.1.editorconfig conformance (all tracked files)
Built-in syntax check, using tomli⁠ for TOML2.3.1JSON, JSONC, and TOML⁠ (*.json, *.jsonc, *.toml, Pipfile)

Built on python:3-alpine3.24.

⁠Quick start

The target must be a local Git checkout, and its Git directory has to be available inside the container — the linters enumerate files with git ls-files:

gitdir=$(git rev-parse --path-format=absolute --git-common-dir)
docker run --rm \
  -v "$PWD":/code \
  -v "$gitdir":"$gitdir" \
  zaventh/lintorama:9

The image works out of /code (its WORKDIR), which is already registered as a Git safe.directory. That is the only requirement — run the command from the root of any Git repository, or any of its worktrees, and it lints every supported file type.

⁠Git worktrees

In a regular checkout, .git is a directory and the first mount already brings it into /code. In a linked worktree (git worktree add), .git is a file pointing into the main repository's Git directory, for example gitdir: /home/me/project/.git/worktrees/feature. The second mount puts that directory at the same path inside the container, so the pointer resolves. In a regular checkout it's redundant but harmless, so the same command works in both cases.

Worktrees created with git worktree add --relative-paths (or worktree.useRelativePaths) store a relative pointer instead, which only resolves if the worktree keeps its host path in the container. Mount it there and mark that path as a Git safe.directory:

gitdir=$(git rev-parse --path-format=absolute --git-common-dir)
docker run --rm \
  -v "$PWD":"$PWD" -w "$PWD" \
  -v "$gitdir":"$gitdir" \
  -e GIT_CONFIG_COUNT=1 \
  -e GIT_CONFIG_KEY_0=safe.directory \
  -e GIT_CONFIG_VALUE_0="$PWD" \
  zaventh/lintorama:9

⁠Continuous integration

lint-extras is the image's entrypoint, so most CI systems need only a few lines. The examples below all run the full linter suite over the checked-out repository and fail the job on any lint error.

⁠GitLab CI
lint:
  image: zaventh/lintorama:9
  script:
    - lint-extras
⁠GitHub Actions
jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Run lintorama
        run: |
          gitdir=$(git rev-parse --path-format=absolute --git-common-dir)
          docker run --rm \
            -v "$PWD":/code \
            -v "$gitdir":"$gitdir" \
            zaventh/lintorama:9
⁠Any other CI (generic Docker)
gitdir=$(git rev-parse --path-format=absolute --git-common-dir)
docker run --rm \
  -v "$PWD":/code \
  -v "$gitdir":"$gitdir" \
  zaventh/lintorama:9

⁠What it checks

The lint-extras entrypoint operates on the Git-tracked files in the working directory (it uses git ls-files), so the target must be a Git repository. In order, it:

  1. Runs yamllint -s (strict) over all tracked *.yml / *.yaml files, except generated lockfiles (pnpm-lock.yaml, conda-lock.yml).
  2. Schema-validates well-known config files against their published schemas with check-jsonschema, when present. See Schema-validated files⁠.
  3. Syntax-checks all tracked JSON, JSONC, and TOML files, except generated lockfiles (package-lock.json, npm-shrinkwrap.json). See JSON and TOML⁠.
  4. Runs actionlint over all tracked .github/workflows/*.yml / *.yaml files (delegating embedded run: scripts to the bundled ShellCheck).
  5. If a package.json exists, requires a lockfile to accompany it: package-lock.json, npm-shrinkwrap.json, yarn.lock, pnpm-lock.yaml, bun.lock, or bun.lockb.
  6. Runs shellcheck over all tracked *.sh / *.bash files, plus extensionless files whose shebang names sh, bash, dash, or ksh (directly or through env), such as Git hooks. The generated gradlew and mvnw wrappers are skipped.
  7. Runs luacheck over all tracked *.lua files.
  8. Runs ruff check over the Python in directories named scripts, build, or .claude/hooks at any depth: *.py files plus extensionless files with a python (or uv run) shebang. Change the directories with --python-dirs⁠.
  9. Runs hadolint over every Dockerfile and Containerfile at any depth, including variants such as Dockerfile.dev and app.Dockerfile. Per-Dockerfile ignore files (Dockerfile.dockerignore) and templates (Dockerfile.j2, Dockerfile.in, Dockerfile.tmpl, …) are skipped.
  10. Requires a README.md (case-sensitive) to exist.
  11. Runs mdl over all tracked *.md / *.markdown files.
  12. If an .editorconfig exists, runs editorconfig-checker to verify every tracked file conforms to it.

The exit code is the sum of the individual linter results — any failure fails the run.

⁠Dependency directories

Files under package-manager install directories are never linted, at any depth, even when they are committed or missing from .gitignore: node_modules, bower_components, jspm_packages, .yarn, .pnpm-store, vendor, .venv, venv, site-packages, __pypackages__, .tox, .nox, Pods, Carthage, .build, .gradle, .dart_tool, elm-stuff, and .terraform. Git submodules are skipped too, since git ls-files doesn't descend into them.

⁠Schema-validated files

check-jsonschema validates each of these files against the schema bundled with it, so validation runs offline. File names follow check-jsonschema's own pre-commit hooks.

SchemaFiles
GitLab CI.gitlab-ci.yml
GitHub Actions (composite and other actions)action.yml at the root, .github/actions/**/action.yml
GitHub issue forms.github/ISSUE_TEMPLATE/*.yml, except config.yml
GitHub issue template chooser.github/ISSUE_TEMPLATE/config.yml
GitHub discussion forms.github/DISCUSSION_TEMPLATE/*.yml
Dependabot.github/dependabot.yml
Renovaterenovate.json, renovate.json5, .renovaterc, .renovaterc.json, .renovaterc.json5, and renovate.json / renovate.json5 under .github/ or .gitlab/
Docker Composecompose.yaml, docker-compose.yml, and variants such as compose.prod.yaml, at any depth. Files that use Compose's !reset or !override merge tags are skipped, since check-jsonschema can't load them.
Azure Pipelinesazure-pipelines.yml, .azure-pipelines.yml
Bamboo Specsbamboo-specs/**/*.yml
Bitbucket Pipelinesbitbucket-pipelines.yml, at any depth
Buildkitebuildkite.yml, buildkite.*.yml, .buildkite/pipeline.yml, .buildkite/pipeline.*.yml (also .json)
CircleCI.circleci/config.yml
Google Cloud Buildcloudbuild.yaml (also .yml, .json)
Travis CI.travis.yml
Woodpecker CI.woodpecker.yml, .woodpecker/**/*.yml
Changie.changie.yaml
Citation File FormatCITATION.cff
Codecovcodecov.yml or .codecov.yml at the root, in .github/, or in dev/
Meltanomeltano.yml at any depth, meltano-manifest.json, meltano-manifest.*.json
Mergify.mergify.yml, .mergify/config.yml, .github/mergify.yml
Read the Docs.readthedocs.yaml
Snapcraftsnapcraft.yaml, at any depth
TaskfileTaskfile.yml, taskfile.yml, Taskfile.dist.yml, taskfile.dist.yml

Each .yml or .yaml name in the table matches both spellings, except Meltano and Mergify (.yml only) and Snapcraft (.yaml only).

⁠JSON and TOML

JSON files (*.json) must be strict RFC 8259⁠ JSON: no comments, trailing commas, NaN or Infinity, duplicate keys, or byte order mark. JSONC files may also use // and /* */ comments, trailing commas, and a byte order mark, as TypeScript and VS Code allow. These files count as JSONC, at any depth, because the tools that read them accept comments:

  • *.jsonc, *.code-workspace, *.code-snippets, and .vscode/*.json
  • devcontainer.json and .devcontainer.json
  • tsconfig*.json and jsconfig*.json, such as tsconfig.build.json
  • deno.json, biome.json, turbo.json, wrangler.json, and typedoc.json
  • .eslintrc.json, .oxlintrc.json, .babelrc.json, babel.config.json, .markdownlint.json, cspell.json, and .cspell.json
  • appsettings*.json (.NET)

A strict JSON file that would parse as JSONC gets a hint saying so.

TOML files (*.toml and Pipenv's Pipfile) must be valid TOML 1.0⁠, checked with tomli⁠, which also rejects duplicate keys and tables. TOML 1.1 additions, such as trailing commas and line breaks in inline tables, are rejected: Python's tomllib (which reads pyproject.toml) and Gradle (which reads libs.versions.toml) only understand 1.0, and every 1.1 reader accepts 1.0.

Each problem is reported as path:line:column: message.

⁠Options

lint-extras takes these command-line options:

OptionDefaultMeaning
--python-dirs DIRSscripts,build,.claude/hooksComma-separated directory names or repository-relative paths whose Python Ruff lints, matched at any depth. Pass '' to skip Python.
-h, --helpPrint usage and exit.

Python is limited to these directories on purpose: lintorama covers a project's supporting files, and its main Python code is better served by the project's own tooling. To lint scripts kept somewhere else, list those directories instead.

In GitLab CI, pass options to lint-extras in the job's script:

lint:
  image: zaventh/lintorama:9
  script:
    - lint-extras --python-dirs scripts,tools

With docker run, put them after the image name:

gitdir=$(git rev-parse --path-format=absolute --git-common-dir)
docker run --rm \
  -v "$PWD":/code \
  -v "$gitdir":"$gitdir" \
  zaventh/lintorama:9 --python-dirs scripts,tools

⁠Configuration

Each linter honors its standard configuration file when present in the repository root, so consumers can tune the rules without changing this image:

FileLinter
.yamllintyamllint
.hadolint.yamlhadolint
.mdlrcmarkdownlint
ruff.toml, .ruff.toml, or pyproject.toml with [tool.ruff]Ruff
.github/actionlint.yamlactionlint
.editorconfigeditorconfig-checker (also the file it enforces)

The .yamllint, .hadolint.yaml, .mdlrc, and .editorconfig files in this repository are the ones lintorama applies to itself. check-jsonschema needs no configuration — it picks each file's schema by name — and editorconfig-checker only runs when the repository provides an .editorconfig.

When the repository has no Ruff configuration, Ruff uses lintorama's bundled defaults⁠: Ruff's own default rule set, tuned lightly for readability. Line length, complexity, naming, and docstring rules stay off, and formatting isn't enforced. A Ruff config in the repository replaces those defaults entirely, since Ruff doesn't merge configs, so copy the bundled file as a starting point. Ruff's exclude and extend-exclude settings are honored for the files lintorama passes it.

⁠Image tags

Published to Docker Hub as zaventh/lintorama⁠:

TagMeaning
9.0.0Exact, immutable version
9Rolling major tag (recommended for most pipelines)
latestThe most recent build

Each tag is a multi-arch image for linux/amd64 and linux/arm64; Docker picks the right one for the host automatically.

⁠FAQ

What is lintorama? lintorama is a Docker image that bundles yamllint, ShellCheck, hadolint, markdownlint (mdl), luacheck, Ruff, actionlint, check-jsonschema, and editorconfig-checker behind a single command, lint-extras, for linting a Git repository in CI pipelines. It also syntax-checks JSON, JSONC, and TOML.

Which linters does lintorama include? Nine: yamllint (YAML), ShellCheck (shell scripts), hadolint (Dockerfiles), markdownlint / mdl (Markdown), luacheck (Lua), Ruff (Python scripts), actionlint (GitHub Actions workflows), check-jsonschema (schema validation for CI and tooling config), and editorconfig-checker (.editorconfig conformance). A built-in check covers JSON, JSONC, and TOML syntax. See Bundled linters⁠ for exact versions.

How do I run lintorama locally? Run the Quick start⁠ command from the root of any Git repository or worktree: it mounts the checkout at /code and the repository's Git directory at its host path.

How do I use lintorama in CI? In GitLab CI, use it as the job image and call lint-extras. In GitHub Actions or any other system, run the image with docker run. See Continuous integration⁠.

Does lintorama require configuration? No. It works with sensible defaults, but each linter honors its standard config file (.yamllint, .hadolint.yaml, .mdlrc, ruff.toml) when present. See Configuration⁠.

Why does lintorama only lint Python in scripts/ and build/? lintorama checks a project's supporting files, not its main code. Python in scripts/, build/, and .claude/hooks/ is usually helper tooling with no linter of its own, while a Python codebase has its own lint setup. Use --python-dirs⁠ to choose different directories.

Why does lintorama need the .git directory? lint-extras discovers files with git ls-files, so the target must be a Git repository with its Git directory available inside the container. For a worktree, that means the main repository's Git directory too. See Git worktrees⁠.

How does lintorama report failures? The exit code is the sum of the individual linters' results, so a non-zero exit means at least one check failed — exactly what a CI pipeline needs.

Does lintorama run on Apple Silicon or other arm64 hosts? Yes. Every tag includes a native linux/arm64 image next to linux/amd64, so Docker on an Apple Silicon Mac runs it without Rosetta or QEMU emulation.

Where is the image published? On Docker Hub as zaventh/lintorama⁠. The source lives on GitHub⁠.

⁠Building and releasing

The image is built and pushed by .gitlab-ci.yml on every push to the default branch. Each architecture is built natively on its own runner — linux/amd64 on a Linux runner, linux/arm64 on an Apple Silicon Mac runner (tagged macos) — and linted with that same image before anything is published. To cut a new release, bump the BUILD_VER variable in that file (semver, e.g. 7.1.0); the pipeline then combines both builds into one multi-arch image, publishes it as the full version, the major tag, and latest, stamps the version, build date, and commit SHA into the image's OCI labels, and syncs this README to the Docker Hub repository description.

⁠License

Released under the MIT License⁠. Copyright (c) 2016-2026 Jeff Mixon.

Tag summary

Content type

Image

Digest

sha256:efefc0599…

Size

168.5 MB

Last updated

15 days ago

docker pull zaventh/lintorama