MIL-010 task 2: ADR-0010, the contract of the code metrics badges (RC-072)
Refs #156
This commit is contained in:
@@ -0,0 +1,203 @@
|
||||
# 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:
|
||||
|
||||
1. **Wrap `cloc`, `tokei` or `scc`.** Many languages, mature rules, but a tool
|
||||
to install and pin on every run and a download from outside the instance.
|
||||
2. **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-directory` whose 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 word | none |
|
||||
| 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
|
||||
directories `node_modules`, `vendor`, `third_party`, `dist`, `build`, `out`,
|
||||
`bin`, `obj`, `target`, `venv`, `__pycache__` and `shared-scripts`
|
||||
(the checkout of the shared scripts); symbolic links are not followed. The
|
||||
input `exclude` is 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 to `working-directory` (`*` does not cross `/`).
|
||||
The counter does not read `.gitignore` or generated-file markers; a caller
|
||||
excludes generated code with `exclude`.
|
||||
- **Totals.** `files` is the number of code files and `lines` is the sum of
|
||||
their code lines (comment and blank lines are not in it); the result also
|
||||
holds `comment` and `blank` totals and the same four numbers per language,
|
||||
sorted by language name, in `code-metrics.json`.
|
||||
- **Coverage report.** The input `coverage-report` is the path, relative to
|
||||
the repository root, of a Cobertura XML file. The percentage is
|
||||
`lines-covered / lines-valid` of the root `coverage` element, shown rounded
|
||||
half up to a whole number; when a count is absent, `line-rate` is 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
|
||||
empty `coverage-report` means 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 `badges` branch with fixed names: `code-files.svg` (label
|
||||
`code files`), `code-lines.svg` (label `lines of code`) and `coverage.svg`
|
||||
(label `coverage`); 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 `#e05d44` below the input `coverage-warn`
|
||||
(default `60`), yellow `#dfb317` from there to below `coverage-good`
|
||||
(default `80`) and green `#44cc11` from `coverage-good`. Each file has
|
||||
`role="img"`, a `<title>` and an `aria-label` with 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 `badges` branch holds only the three SVG files and
|
||||
`code-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, message
|
||||
`Update 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` (default `60`),
|
||||
`coverage-good` (default `80`), `badges-branch` (default `badges`) and
|
||||
`tools-ref` (default `v1`; 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-warn` above `coverage-good`, a
|
||||
`working-directory` that 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 show `0`.
|
||||
- **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 `badges` branch 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 `cloc` or `tokei`.
|
||||
- The workflow needs `contents: write`, which is more than the
|
||||
`contents: read` of the quality checks; a caller that grants less cannot use
|
||||
it. The write is limited to the `badges` branch 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-ref` to 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.
|
||||
|
||||
---
|
||||
|
||||
[MIL-010]: ../milestones/mil-010-code-metrics-badges.md
|
||||
[UC-008]: ../uc-008/uc.md
|
||||
[US-001]: ../user-stories.md
|
||||
[ADR-0005]: ./adr-0005-quality-check-contract.md
|
||||
[ADR-0006]: ./adr-0006-release-process-and-version-pinning.md
|
||||
Reference in New Issue
Block a user