11 KiB
ADR-0010: Contract of the code metrics and coverage badges workflow
Metadata
| Key | Value |
|---|---|
| ID | ADR-0010 |
| CrossReference | MIL-010, UC-008, US-001, ADR-0005, ADR-0006 |
Version History
| Date | Status | Author | Reviewer | Change | Commit |
|---|---|---|---|---|---|
| 2026-10-14 | Accepted | Jens Tirsvad Nielsen | AI as stand-in for S01 | Initial version; reviewed (RC-072) | pending |
Allowed Status values: Proposed, Accepted, Rejected, Deprecated, Superseded by ADR-NNNN.
Context
MIL-010 adds a reusable workflow that shows the size of a repository's code
and its test coverage as badges (UC-008, US-001.18 to US-001.20). ADR-0005
defines the contract of the quality-check workflow: inputs with working
defaults, a pinned version per tool, a failure that names the cause, and a
script file of this repository, fetched at tools-ref, that the workflow runs.
Decisions the user took in chat on 2026-10-14 bound this one: the counter is a
Python script of this repository with no dependency outside the standard
library, the delivery is a reusable workflow, the badges are committed to a
badges branch, there are three badges (code files, lines of code, coverage),
and the coverage report is read in the Cobertura XML format.
Options for the counter:
- Wrap
cloc,tokeiorscc. Many languages, mature rules, but a tool to install and pin on every run and a download from outside the instance. - A Python script of this repository. No install, fully tested offline, and the rules are ours; fewer languages and simple scanning rules.
Options for the delivery of the badges: a run artifact (disappears and has no stable URL), a file on the default branch (a commit per change on the branch that protects the code), or a dedicated branch (stable raw URL, no effect on the default branch).
Facts: the job of a reusable workflow runs on ubuntu-latest with python3
3.12; a push made with the token of a run does not start another run; a
workflow checks the shared scripts out into the workspace (shared-scripts),
so the counter must not count them.
Decision
A reusable workflow code-metrics.yml runs three script files of this
repository, fetched at its input tools-ref (default v1): tools/count_loc.py
counts the code, tools/badges.py reads the coverage report and writes the
badges, and tools/code-metrics.sh validates the inputs, calls both and
publishes the badge files to the badges branch of the calling repository
with the token of the run (contents: write, no other permission).
The rules that follow from it:
-
Code file. A file under
working-directorywhose extension is in the language table below, that is not excluded, and that contains no NUL byte (it is text; it is read as UTF-8 and invalid bytes are replaced). Its language comes from the extension. A file with a code extension but no line is still a code file. -
Languages and comments (first version).
Language Extensions Line comment Block comment Python .py#none Shell .sh,.bash#at the start of a wordnone PowerShell .ps1,.psm1#<#...#>C .c,.h///*...*/C++ .cc,.cpp,.cxx,.hh,.hpp///*...*/C# .cs///*...*/Java .java///*...*/JavaScript .js,.mjs,.jsx///*...*/TypeScript .ts,.tsx///*...*/Go .go///*...*/Rust .rs///*...*/SQL .sql--/*...*/Markup, data and configuration files (Markdown, YAML, JSON, XML, HTML, CSS) are not code files. A language is added by a minor release.
-
Line classes. Every physical line is exactly one of: blank (only white space, also inside a block comment), comment (not blank, and only comment text and white space), or code (anything else, including a line with code and a trailing comment). A comment marker inside a string literal is not a comment. String literals are the quoted forms of the language that end on the same line, plus the Python triple-quoted string and the backtick string of JavaScript, TypeScript and Go, which may span lines; a Python docstring is a string and so counts as code. Raw-string and verbatim forms that differ from these (Rust
r#"..."#, C#@"..."with doubled quotes, C++ raw strings) are scanned as ordinary strings and can be mis-counted; this is a stated limit. -
Excluded by default. Every directory whose name starts with
., and the directoriesnode_modules,vendor,third_party,dist,build,out,bin,obj,target,venv,__pycache__andshared-scripts(the checkout of the shared scripts); symbolic links are not followed. The inputexcludeis a comma list of patterns that add to this: a pattern without/matches a directory or file name anywhere, a pattern with/matches the path relative toworking-directory(*does not cross/). The counter does not read.gitignoreor generated-file markers; a caller excludes generated code withexclude. -
Totals.
filesis the number of code files andlinesis the sum of their code lines (comment and blank lines are not in it); the result also holdscommentandblanktotals and the same four numbers per language, sorted by language name, incode-metrics.json. -
Coverage report. The input
coverage-reportis the path, relative to the repository root, of a Cobertura XML file. The percentage islines-covered / lines-validof the rootcoverageelement, shown rounded half up to a whole number; when a count is absent,line-rateis used. A path that does not exist, a file that is not well-formed Cobertura XML, and a report with no valid lines are failures with the category[configuration]that names the file; no coverage badge is written. An emptycoverage-reportmeans no coverage badge and a notice. The workflow does not run tests; the caller runs them before the call and the report must be in the workspace (the same job, or an artifact the caller fetched). -
Badges. Three flat SVG files with a label and a value, written to the root of the
badgesbranch with fixed names:code-files.svg(labelcode files),code-lines.svg(labellines of code) andcoverage.svg(labelcoverage); the values are the whole number with,between groups of three digits and, for coverage,%. The value colour of the first two is blue#007ec6; coverage is red#e05d44below the inputcoverage-warn(default60), yellow#dfb317from there to belowcoverage-good(default80) and green#44cc11fromcoverage-good. Each file hasrole="img", a<title>and anaria-labelwith the same text, a width computed from the number of characters (no font is measured) and no timestamp, script or external reference, so the same numbers give the same bytes. -
Publishing. The
badgesbranch holds only the three SVG files andcode-metrics.json; the script creates it as an orphan branch when it does not exist. It writes the files, and when none differs from the branch it commits nothing and says so; otherwise one commit, messageUpdate code metrics badges [skip ci], changes only those files. The commit is made with the token of the run, which does not start another run; the guide tells a caller to start the workflow on pushes to its default branch only. -
Inputs.
working-directory(default.),exclude(default empty),coverage-report(default empty),coverage-warn(default60),coverage-good(default80),badges-branch(defaultbadges) andtools-ref(defaultv1; pin it to the same exact tag as the workflow). There is no tool version input: the tools are files of this repository and Python 3 of the runner. -
Pass, fail, empty. The run passes when the badges were written or were already current. It fails with a
[configuration]message that names the input and the value for an input that is invalid (a threshold that is not a number from 0 to 100,coverage-warnabovecoverage-good, aworking-directorythat is not a directory, a branch name that is not a valid branch name) and for a coverage report as above. A repository with no code file passes with a notice and badges that show0. -
Not in the first version: a badge per language (the JSON holds the numbers), lcov and other report formats, branch or function coverage, and counting only tracked files.
Consequences
Positive:
- No tool is installed and nothing is downloaded: a run needs only the checkout, Python 3 and the instance, so the counter and the renderer are fully testable offline with fixtures.
- The same input gives the same bytes, so an unchanged repository produces no
commit and the
badgesbranch history is the history of the numbers. - The badges have stable URLs on the instance and on the mirror, and nothing on the default branch changes.
- A broken or missing report is a failure that names the file, never a
misleading
0%.
Negative:
- The counter knows only the listed languages and simple scanning rules; some
string forms are mis-counted (stated above), and results will differ a
little from
clocortokei. - The workflow needs
contents: write, which is more than thecontents: readof the quality checks; a caller that grants less cannot use it. The write is limited to thebadgesbranch by the script, not by the host. - A caller must run its tests and produce a Cobertura XML report before the call; other formats need a conversion until a later release adds them.
- A fixed set of thresholds and colours; a caller can move the two thresholds only.
- Security: the script runs with a token that can write to the repository.
The files are ours and no code is downloaded, but a caller that pins
tools-refto a moving ref takes later changes of the script; the guide says to pin the exact tag. The script writes only the four named files, and the branch name input is validated so that it cannot name the default branch (main,master) or a branch of the caller's code.
Affected Artifacts
- MIL-010 — decides its criterion 2 and unlocks tasks 4 to 6.
- UC-008 — the use case this workflow serves.
- US-001 — US-001.18 to US-001.20 state their acceptance criteria.
- ADR-0005 — the contract style that this decision follows, unchanged.
- ADR-0006 — adding a language, or changing a colour or a default, is a release under its rules; raising a default threshold is breaking.