Files
github-action/docs/adr/adr-0010-code-metrics-badges-contract.md
T
Tirsvad 604d4c2d0c
TirSystem/github-action: Sync GitHub mirror metadata / sync-metadata (push) Skipped
Validate workflows / validate (pull_request) Successful in 31s
Resolve pending commit links for MIL-011
2026-10-14 17:13:23 +08:00

12 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 The report reaches the workflow as an artifact of the caller's run (input coverage-artifact), because a called workflow runs in its own job; reviewed (RC-073) 0dc5132
2026-10-14 Proposed Jens Tirsvad Nielsen S01 Amendment: the default tools-ref is the current major tag (MIL-011) eaa0e30

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. A called workflow runs in its own job, so it cannot see the files of the caller's test job: the caller runs its tests in an earlier job, uploads the report as an artifact and gives the artifact's name as the input coverage-artifact. The workflow then downloads that artifact into the directory coverage-artifact of its workspace and coverage-report names the file inside the artifact. Without coverage-artifact, coverage-report is a path in the checkout of the repository (a report the repository commits). coverage-artifact without coverage-report is a [configuration] failure.

  • 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-artifact (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 in an earlier job and upload it as an artifact; other formats need a conversion until a later release adds them. The download needs the artifact service of the instance to serve a called workflow, which task 8 of MIL-010 verifies.
  • 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 (the artifact is a report that is parsed as XML with entity declarations refused), 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.

Amendment (MIL-011)

The default of the input tools-ref in the code metrics contract is the current major tag, defined once in the file MAJOR_TAG of this repository (v2 from release 2.1.1), and no longer v1. A default must name a tag that holds the script the workflow runs; a test checks it. Callers at @v1 are not affected: the v1 tag keeps the v1 workflow files.