5.9 KiB
ADR-0005: Contract of the shared quality-check workflow
Metadata
| Key | Value |
|---|---|
| ID | ADR-0005 |
| CrossReference | MIL-003, UC-005, US-001, ADR-0004 |
Version History
| Date | Status | Author | Reviewer | Change | Commit |
|---|---|---|---|---|---|
| 2026-10-10 | Deprecated | Jens Tirsvad Nielsen | S01 | Reviewed (RC-038): the security impact of installing the tools from PyPI named | 385d2fb |
| 2026-10-11 | Accepted | Jens Tirsvad Nielsen | S01 | Added the input tools-ref: the shell steps moved to a script file fetched at that ref (RC-051) | pending |
Allowed Status values: Proposed, Accepted, Rejected, Deprecated, Superseded by ADR-NNNN.
Context
MIL-003 offers one workflow that any repository calls to run lint, format and type checks. Before it is built (task 3) its inputs and the meaning of a pass and a fail must be fixed, because consumers in four organizations will depend on them and MIL-004 will version them. Facts found on 2026-10-10:
- The runner image (Ubuntu 24.04, Python 3.12,
git, Node.js,jq,make, run asroot) has none of ruff, mypy, pytest, shellcheck or shfmt. - PyPI is reachable, and a virtual environment installs all four tools with
pinned versions:
ruff0.6.9,mypy1.11.2,shellcheck-py0.10.0.1 (ShellCheck 0.10.0) andshfmt-py3.11.0.2 (shfmt 3.11.0). - Each tool exits non-zero on a finding and prints
path:lineand a message.shellcheckalso exits 1 on an empty file, so a language with no files must be skipped before the tool runs. - Reusable workflows work from the same repository and from other organizations (issue #15). Secrets passing is untested, and the checks need none.
- The conventions name the tools: ruff and
mypy --strictfor Python (QC-PY-001) and shellcheck and shfmt for shell (QC-SH-001).
Options for the shape of the workflow:
- One reusable workflow with a
languagesinput that selects the tool sets, defaults that work with no configuration, and one version input per tool. - One reusable workflow per language (
python-checks.yml,shell-checks.yml). Simple, but a mixed repository calls two workflows and the contract exists twice. - Detect the languages from the files and take no inputs. Zero configuration, but a repository cannot opt out of a tool, and versions change only with the workflow.
- The consumer passes its own commands. Most flexible, but it is not a shared check any more and defeats the point of a contract.
Decision
The shared workflow quality-checks.yml is one reusable workflow with the
inputs languages (a comma list of python and shell, default both),
working-directory (default .), one version input per tool whose
defaults are the pinned versions above, and tools-ref (default v1, the
tag or branch from which the workflow fetches its script
tools/quality-checks.sh), and it installs the tools from PyPI into a virtual
environment. A call at an exact tag is pinned completely only when tools-ref
names the same tag; with the default the script follows the major tag. A run passes when every selected tool exits 0 and
fails when any exits non-zero, and a language with no files is skipped with a
notice and counts as a pass.
The rules that follow from it:
- Python:
ruff check,ruff format --checkandmypy --strict, run on the files underworking-directory. - Shell: the tracked
*.shfiles are checked withshellcheckandshfmt -d. - Configuration: projects configure the tools in the standard way
(
pyproject.toml,.shellcheckrc,.editorconfig); the workflow adds none and changes no file. - Reporting: each tool runs in its own log group named after the tool, and
its own output carries
path:lineand the message; the job status is the pull request status. - Out of scope for the first version: running the project's tests
(
pytestneeds the project's dependencies), auto-fixing, and other languages.
Consequences
Positive:
- A repository gets the checks with one
uses:and no configuration, and a mixed repository gets both language sets from one call. - Tool versions are explicit and reviewed, so a new tool release cannot turn every consumer red overnight.
- A failure is read from the tool's own message, so the workflow needs no parser of its own.
Negative:
- Every run installs the tools from PyPI (about half a minute) and depends on PyPI being reachable; caching is not part of the first version.
- Projects that need their tests or their own dependencies in the type check
(
mypywith third-party stubs) must configure that themselves or wait for a later version. mypy --stricton an existing codebase is likely to fail at first; a project that cannot meet it has to configure the tool, which weakens the shared meaning of "pass".- Pinned versions need a release (MIL-004) to move; until then they are
updated by changing the workflow on
main. - Security: the tools are downloaded from PyPI on every run and execute
with the consumer's repository content on a runner that runs as
root. Versions are pinned but the packages are not hash-pinned, so a compromised release of one of the four packages would reach every consumer; hash pinning is a later hardening step.
Affected Artifacts
- MIL-003 — tasks 3 to 5 (issues #17 to #19) implement, adopt and document this contract.
- UC-005 — the use case this contract serves.
- US-001 — stories US-001.09 and US-001.10 state its acceptance criteria.
- ADR-0004 — the same pattern of a shared scoped or reusable workflow with an explicit gate; this ADR does not change it.