From 9ca9673b00351cf913966f347c4da09890ed3f1f Mon Sep 17 00:00:00 2001 From: Jens Tirsvad Nielsen Date: Wed, 14 Oct 2026 13:41:14 +0800 Subject: [PATCH 01/10] MIL-010 task 1: use case UC-008, stories US-001.18 to US-001.20, RC-069 to RC-071 Refs #155 --- docs/artifact-registry.md | 4 +- docs/sqa/reviews/rc-069-uc-008.md | 55 ++++++++++++++++++++++ docs/sqa/reviews/rc-070-us-001-metrics.md | 50 ++++++++++++++++++++ docs/sqa/reviews/rc-071-ucd-001-metrics.md | 51 ++++++++++++++++++++ docs/sqa/traceability-matrix.md | 19 +++++--- docs/uc-008/uc.md | 43 +++++++++++++++++ docs/use-case-diagram.md | 13 +++-- docs/user-stories.md | 55 +++++++++++++++++++--- 8 files changed, 270 insertions(+), 20 deletions(-) create mode 100644 docs/sqa/reviews/rc-069-uc-008.md create mode 100644 docs/sqa/reviews/rc-070-us-001-metrics.md create mode 100644 docs/sqa/reviews/rc-071-ucd-001-metrics.md create mode 100644 docs/uc-008/uc.md diff --git a/docs/artifact-registry.md b/docs/artifact-registry.md index 50bd9a8..1d51d75 100644 --- a/docs/artifact-registry.md +++ b/docs/artifact-registry.md @@ -11,12 +11,12 @@ each document lives in *this* project and the next version to use. | SA | Stakeholder Analysis | docs/stakeholder-analysis.md | 002 | | PP | Project Plan | docs/project-plan.md | 002 | | MIL | Milestone / Gateway | docs/milestones/*.md | 011 | -| RC | SQA Review Record | docs/sqa/reviews/rc-*.md | 069 | +| RC | SQA Review Record | docs/sqa/reviews/rc-*.md | 072 | | TM | Traceability Matrix | docs/sqa/traceability-matrix.md | 002 | | ADR | Architecture Decision Record | docs/adr/adr-NNNN-*.md | 0010 | | UCD | Use Case Diagram | docs/use-case-diagram.md | 002 | | US | User Story | docs/user-stories.md | 002 | -| UC | Use Case | docs/uc-NNN/uc.md | 008 | +| UC | Use Case | docs/uc-NNN/uc.md | 009 | ## Languages diff --git a/docs/sqa/reviews/rc-069-uc-008.md b/docs/sqa/reviews/rc-069-uc-008.md new file mode 100644 index 0000000..6798c0c --- /dev/null +++ b/docs/sqa/reviews/rc-069-uc-008.md @@ -0,0 +1,55 @@ +# Review Record: UC-008 (Show Code Metrics and Coverage Badges) + +## Metadata +| Key | Value | +| --- | --- | +| ID | RC-069 | +| CrossReference | [UC-008], [QC-UC-001] | + +## Version History +| Date | Status | Author | Reviewer | Change | Commit | +| --- | --- | --- | --- | --- | --- | +| 2026-10-14 | Accepted | Jens Tirsvad Nielsen | AI as stand-in for S01 | Initial version; accepted | pending | + +--- + +## Artifact Under Review + +- Instance reviewed: [UC-008] +- Checklist used: [QC-UC-001] +- Scope: full review (Brief format; MIL-010 task 1) +- Language and domain: n/a (technical type) +- Language reviewer: none +- Mode: autonomous, not independent (the reviewer is `AI as stand-in for S01`; the user accepted the plan in chat on 2026-10-14) + +## Checklist Results + +| # | Criterion | Status | Evidence/Notes | +| --- | --- | --- | --- | +| 1 | Consists of a single, concise paragraph summarizing only the primary success scenario | Pass | One paragraph: push, count, read the report, publish three badges, README shows them. | +| 2 | Written as an informal multi-paragraph narrative; may mention some alternate flows without formal structure | N-A | Format is Brief. | +| 3 | All standard sections are present: actors, preconditions, postconditions, main success scenario, alternative/exception flows | N-A | Format is Brief; actor, stakeholders, pre- and postconditions and the paragraph are present. | +| 4 | Preconditions and postconditions are explicitly defined | Pass | Both are stated; the postcondition says that only the badges change. | +| 5 | Primary actor is explicitly stated | Pass | Software Engineer. | +| 6 | Stakeholders and their interests are stated | Pass | S02 and S05 with an interest each. | +| 7 | Main success scenario is written as clear, numbered steps | N-A | Format is Brief. | +| 8 | Alternative/exception flows correctly reference `<>`/`<>` use cases where relevant, per UML 2.5.1 | N-A | None in a Brief. | +| 9 | Explicit business rules are captured per step where applicable, rather than embedded loosely in narrative text | N-A | No steps in a Brief. | +| 10 | Naming of actors and use case title is consistent with the corresponding Use Case Diagram and User Stories | Pass | Title and actor equal UCD-001 (RC-071); the Stories line lists US-001.18 to US-001.20, which exist (RC-070). | +| 11 | Scope/level (e.g. summary, user-goal, subfunction) is explicitly stated | Pass | Scope Reusable Workflow Collection, user-goal level. | +| 12 | Use case is written from the actor's goal perspective, free of UI or implementation detail | Pass | Goal wording; no tool, file name or format of a report is named. | + +## Overall Verdict + +Go - all applicable criteria pass. The AI records the decision as stand-in for S01; there is no independent reader and the decision stays open to challenge in the run summary. + +## Action Items + +| Action | Owner | Due | +| --- | --- | --- | +| None | - | - | + +--- + +[UC-008]: ../../uc-008/uc.md +[QC-UC-001]: ../../../framework/qc/artifacts/qc-use-case.md diff --git a/docs/sqa/reviews/rc-070-us-001-metrics.md b/docs/sqa/reviews/rc-070-us-001-metrics.md new file mode 100644 index 0000000..25ae7ee --- /dev/null +++ b/docs/sqa/reviews/rc-070-us-001-metrics.md @@ -0,0 +1,50 @@ +# Review Record: US-001 (stories US-001.18 to US-001.20) + +## Metadata +| Key | Value | +| --- | --- | +| ID | RC-070 | +| CrossReference | [US-001], [QC-US-001] | + +## Version History +| Date | Status | Author | Reviewer | Change | Commit | +| --- | --- | --- | --- | --- | --- | +| 2026-10-14 | Accepted | Jens Tirsvad Nielsen | AI as stand-in for S01 | Initial version; accepted | pending | + +--- + +## Artifact Under Review + +- Instance reviewed: [US-001] +- Checklist used: [QC-US-001] +- Scope: delta re-review of the added stories US-001.18 to US-001.20 and the Purpose and Scope and INVEST sections; earlier record RC-057 (US-001.01 to US-001.17 unchanged) +- Language and domain: n/a (technical type) +- Language reviewer: none +- Mode: autonomous, not independent (the reviewer is `AI as stand-in for S01`; the user accepted the plan in chat on 2026-10-14) + +## Checklist Results + +| # | Criterion | Status | Evidence/Notes | +| --- | --- | --- | --- | +| 1 | Follows INVEST criteria (Independent, Negotiable, Valuable, Estimable, Small, Testable) | Pass | US-001.18 to US-001.20 are independent of each other, each states one value and has checkable criteria; the INVEST section of US-001 now counts twenty stories. | +| 2 | Written in "As a / I want / So that" form | Pass | All three stories. | +| 3 | Clear, testable acceptance criteria are included | Pass | Each has Given/When/Then criteria with a pass and a fail case (a missing report, no files, unchanged numbers). | +| 4 | Traceable to a use case or epic | Pass | Each traces to [UC-008] and [MIL-010]. | +| 5 | Story is sized to fit within a single iteration | Pass | Each says: fits one iteration. | +| 6 | Story statement avoids technical implementation detail | Pass | The statements name badges, files, lines and coverage; format and branch names are left to the contract. | +| 7 | Role named in the story matches an actor defined in the Use Case Diagram | Pass | Software Engineer, an actor of UCD-001. | + +## Overall Verdict + +Go - all criteria pass. The AI records the decision as stand-in for S01; there is no independent reader and the decision stays open to challenge in the run summary. + +## Action Items + +| Action | Owner | Due | +| --- | --- | --- | +| None | - | - | + +--- + +[US-001]: ../../user-stories.md +[QC-US-001]: ../../../framework/qc/artifacts/qc-user-story.md diff --git a/docs/sqa/reviews/rc-071-ucd-001-metrics.md b/docs/sqa/reviews/rc-071-ucd-001-metrics.md new file mode 100644 index 0000000..dc50bdf --- /dev/null +++ b/docs/sqa/reviews/rc-071-ucd-001-metrics.md @@ -0,0 +1,51 @@ +# Review Record: UCD-001 (Show Code Metrics and Coverage Badges) + +## Metadata +| Key | Value | +| --- | --- | +| ID | RC-071 | +| CrossReference | [UCD-001], [QC-UCD-001] | + +## Version History +| Date | Status | Author | Reviewer | Change | Commit | +| --- | --- | --- | --- | --- | --- | +| 2026-10-14 | Accepted | Jens Tirsvad Nielsen | AI as stand-in for S01 | Initial version; accepted | pending | + +--- + +## Artifact Under Review + +- Instance reviewed: [UCD-001] +- Checklist used: [QC-UCD-001] +- Scope: delta re-review of the added use case UC8, the actor and use case tables; earlier record RC-045 +- Language and domain: n/a (technical type) +- Language reviewer: none +- Mode: autonomous, not independent (the reviewer is `AI as stand-in for S01`; the user accepted the plan in chat on 2026-10-14) + +## Checklist Results + +| # | Criterion | Status | Evidence/Notes | +| --- | --- | --- | --- | +| 1 | Actors are defined with correct UML stereotypes (e.g. `<>`, `<>`) | Pass | Unchanged; no new actor. | +| 2 | System boundary is clearly drawn and labeled | Pass | The new use case is inside the Reusable Workflow Collection rectangle. | +| 3 | Include/extend relationships are used correctly per UML 2.5.1, not as generic "uses" arrows | Pass | The new use case has no include or extend; the existing two are unchanged. | +| 4 | Every actor participates in at least one use case (no orphan actors) | Pass | Software Engineer is linked to UC5, UC7 and UC8. | +| 5 | Diagram is traceable to a documented stakeholder need | Pass | S02 and S05 interests are in UC-008; the actor table lists S02. | +| 6 | Use case names are verb phrases describing actor goals, not internal system operations | Pass | Show Code Metrics and Coverage Badges. | +| 7 | Diagram is free of implementation detail (e.g. UI widgets, database tables) | Pass | None added. | +| 8 | Actor and use case naming is consistent with corresponding Use Case and User Story documents | Pass | Equals the title of UC-008 and the role of US-001.18 to US-001.20. | + +## Overall Verdict + +Go - all criteria pass. The AI records the decision as stand-in for S01; there is no independent reader and the decision stays open to challenge in the run summary. + +## Action Items + +| Action | Owner | Due | +| --- | --- | --- | +| None | - | - | + +--- + +[UCD-001]: ../../use-case-diagram.md +[QC-UCD-001]: ../../../framework/qc/artifacts/qc-use-case-diagram.md diff --git a/docs/sqa/traceability-matrix.md b/docs/sqa/traceability-matrix.md index 4bfdcdb..20cbbbf 100644 --- a/docs/sqa/traceability-matrix.md +++ b/docs/sqa/traceability-matrix.md @@ -4,13 +4,13 @@ | Key | Value | | --- | --- | | ID | TM-001 | -| CrossReference | [BC-001], [SA-001], [PP-001], [MIL-001], [MIL-002], [MIL-003], [MIL-004], [MIL-005], [MIL-006], [MIL-007], [MIL-008], [MIL-009], [MIL-010], [RC-001], [RC-002], [RC-003], [RC-004], [RC-005], [RC-006], [RC-007], [RC-008], [RC-009], [RC-010], [RC-011], [RC-012], [RC-013], [RC-014], [RC-015], [RC-016], [RC-017], [RC-018], [RC-019], [RC-020], [RC-021], [RC-022], [RC-023], [RC-024], [RC-025], [RC-026], [RC-027], [RC-028], [RC-029], [RC-030], [RC-031], [RC-032], [RC-033], [RC-034], [RC-035], [RC-036], [RC-037], [RC-038], [RC-039], [RC-040], [RC-041], [RC-042], [RC-043], [RC-044], [RC-045], [RC-046], [RC-047], [RC-048], [RC-049], [RC-050], [RC-051], [RC-052], [RC-053], [RC-054], [RC-055], [RC-056], [RC-057], [RC-058], [RC-059], [RC-060], [RC-061], [RC-062], [RC-063], [RC-064], [RC-065], [RC-066], [RC-067], [RC-068], [ADR-0007], [ADR-0008], [ADR-0009], [UC-007], [UC-006], [ADR-0006], [UC-005], [ADR-0005], [UCD-001], [US-001], [UC-001], [UC-002], [UC-003], [UC-004], [ADR-0001], [ADR-0002], [ADR-0003], [ADR-0004] | +| CrossReference | [BC-001], [SA-001], [PP-001], [MIL-001], [MIL-002], [MIL-003], [MIL-004], [MIL-005], [MIL-006], [MIL-007], [MIL-008], [MIL-009], [MIL-010], [UC-008], [RC-001], [RC-002], [RC-003], [RC-004], [RC-005], [RC-006], [RC-007], [RC-008], [RC-009], [RC-010], [RC-011], [RC-012], [RC-013], [RC-014], [RC-015], [RC-016], [RC-017], [RC-018], [RC-019], [RC-020], [RC-021], [RC-022], [RC-023], [RC-024], [RC-025], [RC-026], [RC-027], [RC-028], [RC-029], [RC-030], [RC-031], [RC-032], [RC-033], [RC-034], [RC-035], [RC-036], [RC-037], [RC-038], [RC-039], [RC-040], [RC-041], [RC-042], [RC-043], [RC-044], [RC-045], [RC-046], [RC-047], [RC-048], [RC-049], [RC-050], [RC-051], [RC-052], [RC-053], [RC-054], [RC-055], [RC-056], [RC-057], [RC-058], [RC-059], [RC-060], [RC-061], [RC-062], [RC-063], [RC-064], [RC-065], [RC-066], [RC-067], [RC-068], [RC-069], [RC-070], [RC-071], [ADR-0007], [ADR-0008], [ADR-0009], [UC-007], [UC-006], [ADR-0006], [UC-005], [ADR-0005], [UCD-001], [US-001], [UC-001], [UC-002], [UC-003], [UC-004], [ADR-0001], [ADR-0002], [ADR-0003], [ADR-0004] | ## Version History | Date | Status | Author | Reviewer | Change | Commit | | --- | --- | --- | --- | --- | --- | -| 2026-10-14 | Proposed | Jens Tirsvad Nielsen | S01 | Added RC-066 (MIL-009 outcome) | [95abc18] | | 2026-10-14 | Proposed | Jens Tirsvad Nielsen | S01 | Added MIL-010, RC-067 and RC-068 (code metrics and coverage badges) | [5002450] | +| 2026-10-14 | Proposed | Jens Tirsvad Nielsen | S01 | Added UC-008, RC-069 to RC-071 (code metrics badges, MIL-010 task 1) | pending | --- @@ -59,8 +59,8 @@ updated whenever an artifact instance is created or reviewed. | [RC-016] | RC | [MIL-006] | - | - | | [RC-017] | RC | [PP-001] | - | - | | [RC-018] | RC | [ADR-0004] | - | - | -| [UCD-001] | UCD | [SA-001], [BC-001] | [US-001], [UC-001], [UC-002], [UC-003], [UC-004], [UC-005], [UC-006], [UC-007] | [RC-019], [RC-035], [RC-040], [RC-045] | -| [US-001] | US | [UCD-001], [BC-001], [MIL-001], [MIL-002], [MIL-003], [MIL-004], [MIL-007] | [UC-001], [UC-002], [UC-003], [UC-004], [UC-005], [UC-006], [UC-007] | [RC-020], [RC-036], [RC-041], [RC-046], [RC-057] | +| [UCD-001] | UCD | [SA-001], [BC-001] | [US-001], [UC-001], [UC-002], [UC-003], [UC-004], [UC-005], [UC-006], [UC-007], [UC-008] | [RC-019], [RC-035], [RC-040], [RC-045], [RC-071] | +| [US-001] | US | [UCD-001], [BC-001], [MIL-001], [MIL-002], [MIL-003], [MIL-004], [MIL-007] | [UC-001], [UC-002], [UC-003], [UC-004], [UC-005], [UC-006], [UC-007], [UC-008] | [RC-020], [RC-036], [RC-041], [RC-046], [RC-057], [RC-070] | | [UC-001] | UC | [UCD-001], [US-001], [SA-001] | - | [RC-021] | | [UC-002] | UC | [UCD-001], [US-001], [SA-001] | - | [RC-022] | | [UC-003] | UC | [UCD-001], [US-001], [SA-001] | - | [RC-023] | @@ -73,6 +73,7 @@ updated whenever an artifact instance is created or reviewed. | [ADR-0008] | ADR | [MIL-007], [UC-005], [US-001], [ADR-0005], [ADR-0006] | - | [RC-054] | | [ADR-0009] | ADR | [MIL-008], [ADR-0003], [ADR-0004], [ADR-0005], [ADR-0006] | - | [RC-063] | | [UC-007] | UC | [UCD-001], [US-001], [SA-001] | - | [RC-044] | +| [UC-008] | UC | [UCD-001], [US-001], [SA-001], [MIL-010] | - | [RC-069] | | [RC-019] | RC | [UCD-001] | - | - | | [RC-020] | RC | [US-001] | - | - | | [RC-021] | RC | [UC-001] | - | - | @@ -123,6 +124,9 @@ updated whenever an artifact instance is created or reviewed. | [RC-066] | RC | [MIL-009] | - | - | | [RC-067] | RC | [BC-001] | - | - | | [RC-068] | RC | [MIL-010], [PP-001] | - | - | +| [RC-069] | RC | [UC-008] | - | - | +| [RC-070] | RC | [US-001] | - | - | +| [RC-071] | RC | [UCD-001] | - | - | ## Coverage Notes @@ -224,15 +228,16 @@ updated whenever an artifact instance is created or reviewed. [RC-066]: ./reviews/rc-066-mil-009-outcome.md [RC-067]: ./reviews/rc-067-bc-001-o5.md [RC-068]: ./reviews/rc-068-mil-010-plan.md +[RC-069]: ./reviews/rc-069-uc-008.md +[RC-070]: ./reviews/rc-070-us-001-metrics.md +[RC-071]: ./reviews/rc-071-ucd-001-metrics.md [ADR-0008]: ../adr/adr-0008-cpp-csharp-quality-check-contract.md [ADR-0009]: ../adr/adr-0009-wiki-publishing-reusable-workflow.md [ADR-0007]: ../adr/adr-0007-release-publish-destination.md [UC-007]: ../uc-007/uc.md +[UC-008]: ../uc-008/uc.md [UC-006]: ../uc-006/uc.md [ADR-0006]: ../adr/adr-0006-release-process-and-version-pinning.md [UC-005]: ../uc-005/uc.md [ADR-0005]: ../adr/adr-0005-quality-check-contract.md -[faf13b2]: https://git.tirsystem.com/TirSystem/github-action/commit/faf13b2c2a4901c0171977de1ffc2841023f537a -[1bc56f8]: https://git.tirsystem.com/TirSystem/github-action/commit/1bc56f8b771531f221f218fc00b147ecc73c051e -[95abc18]: https://git.tirsystem.com/TirSystem/github-action/commit/95abc18a95d70c065b979123b8f8de22173af90c [5002450]: https://git.tirsystem.com/TirSystem/github-action/commit/50024505fe788b1c5e3021c0d1fc3d079590070e diff --git a/docs/uc-008/uc.md b/docs/uc-008/uc.md new file mode 100644 index 0000000..442d4a1 --- /dev/null +++ b/docs/uc-008/uc.md @@ -0,0 +1,43 @@ +# UC-008: Show Code Metrics and Coverage Badges + +## Metadata +| Key | Value | +| --- | --- | +| ID | UC-008 | +| CrossReference | [UCD-001], [US-001], [SA-001], [MIL-010] | + +## 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-069) | pending | + +--- + +**Format:** Brief + +**Title:** Show Code Metrics and Coverage Badges +**Scope:** Reusable Workflow Collection +**Level:** user-goal +**Primary Actor:** Software Engineer +**Stakeholders:** S02, S05 (interests below) +**Stories:** US-001.18, US-001.19, US-001.20 + +## Brief + +A Software Engineer pushes a change to a repository that calls the shared code-metrics workflow. The workflow counts the code files and the lines of code (without comment and blank lines), reads the coverage report the repository supplies, and publishes three badges: the number of code files, the number of lines of code and the coverage. The repository's README shows the badges, and they show the numbers of the latest push. + +**Stakeholder interests** + +- S02 — the size and the coverage of the code are visible at a glance, without a service outside the instance, and a wrong or missing report is reported instead of shown as a number +- S05 — the counting rules and the badge contract are defined once and change through a reviewed release + +**Preconditions:** The repository is onboarded and the shared workflow is available through reusable workflow delivery; a coverage badge needs a coverage report that the repository's own tests have produced. + +**Postconditions (success guarantee):** The three badges show the numbers of the pushed revision, nothing but the badges is changed in the repository, and a run whose numbers are unchanged changes nothing. + +--- + +[UCD-001]: ../use-case-diagram.md +[US-001]: ../user-stories.md +[SA-001]: ../stakeholder-analysis.md +[MIL-010]: ../milestones/mil-010-code-metrics-badges.md diff --git a/docs/use-case-diagram.md b/docs/use-case-diagram.md index c63d73d..7a7ae5f 100644 --- a/docs/use-case-diagram.md +++ b/docs/use-case-diagram.md @@ -4,13 +4,13 @@ | Key | Value | | --- | --- | | ID | UCD-001 | -| CrossReference | [SA-001], [BC-001], [US-001], [UC-001], [UC-002], [UC-003], [UC-004], [UC-005], [UC-006], [UC-007] | +| CrossReference | [SA-001], [BC-001], [US-001], [UC-001], [UC-002], [UC-003], [UC-004], [UC-005], [UC-006], [UC-007], [UC-008] | ## Version History | Date | Status | Author | Reviewer | Change | Commit | | --- | --- | --- | --- | --- | --- | -| 2026-10-10 | Deprecated | Jens Tirsvad Nielsen | S01 | Added the use case Release a Reusable Workflow (UC-006) | [044d8ba] | -| 2026-10-10 | Accepted | Jens Tirsvad Nielsen | S01 | Added the use case Consume a Workflow Update (UC-007) (RC-045) | [4cf52af] | +| 2026-10-10 | Deprecated | Jens Tirsvad Nielsen | S01 | Added the use case Consume a Workflow Update (UC-007) (RC-045) | [4cf52af] | +| 2026-10-14 | Accepted | Jens Tirsvad Nielsen | AI as stand-in for S01 | Added the use case Show Code Metrics and Coverage Badges (UC-008); reviewed (RC-071) | pending | --- @@ -46,6 +46,7 @@ rectangle "Reusable Workflow Collection" <> { usecase "Run Project Quality Checks" as UC5 usecase "Release a Reusable Workflow" as UC6 usecase "Consume a Workflow Update" as UC7 + usecase "Show Code Metrics and Coverage Badges" as UC8 } DevOps --> UC1 @@ -57,6 +58,7 @@ Operator --> UC2 Operator --> UC4 Engineer --> UC5 Engineer --> UC7 +Engineer --> UC8 UC1 --> Gitea UC1 --> GitHub UC3 ..> UC2 : <> @@ -70,7 +72,7 @@ UC4 ..> UC1 : <> | --- | --- | --- | --- | | DevOps Professional | `<>` | S05 | Synchronize Repository Metadata, Configure Workflow Credentials, Onboard a Repository, Diagnose a Failed Workflow, Release a Reusable Workflow | | Platform Operator | `<>` | S03 | Configure Workflow Credentials, Diagnose a Failed Workflow | -| Software Engineer | `<>` | S02 | Run Project Quality Checks, Consume a Workflow Update | +| Software Engineer | `<>` | S02 | Run Project Quality Checks, Consume a Workflow Update, Show Code Metrics and Coverage Badges | | Gitea Instance | `<>` | - | Synchronize Repository Metadata (source of description and topics) | | GitHub | `<>` | S04 (audience of the mirror) | Synchronize Repository Metadata (target) | @@ -85,6 +87,7 @@ UC4 ..> UC1 : <> | Run Project Quality Checks | Software Engineer | Lint, format and type checks run on every change and report a pass or a fail | | Release a Reusable Workflow | DevOps Professional | A validated, versioned update that consumers can pin | | Consume a Workflow Update | Software Engineer | A new version is adopted safely, one repository first | +| Show Code Metrics and Coverage Badges | Software Engineer | The size and the coverage of the code are visible as badges that follow every push | ## Relationships @@ -104,6 +107,6 @@ UC4 ..> UC1 : <> [UC-004]: ./uc-004/uc.md [UC-005]: ./uc-005/uc.md [UC-006]: ./uc-006/uc.md -[044d8ba]: https://git.tirsystem.com/TirSystem/github-action/commit/044d8ba21f96f4aceb807d62addc35cdcbb56051 [UC-007]: ./uc-007/uc.md +[UC-008]: ./uc-008/uc.md [4cf52af]: https://git.tirsystem.com/TirSystem/github-action/commit/4cf52afe00f546c6b20424fb8dfc12796ffd9296 diff --git a/docs/user-stories.md b/docs/user-stories.md index 810f17f..9f09556 100644 --- a/docs/user-stories.md +++ b/docs/user-stories.md @@ -4,13 +4,13 @@ | Key | Value | | --- | --- | | ID | US-001 | -| CrossReference | [UCD-001], [BC-001], [MIL-001], [MIL-002], [MIL-003], [MIL-004], [UC-001], [UC-002], [UC-003], [UC-004], [UC-005], [UC-006], [UC-007], [MIL-007] | +| CrossReference | [UCD-001], [BC-001], [MIL-001], [MIL-002], [MIL-003], [MIL-004], [UC-001], [UC-002], [UC-003], [UC-004], [UC-005], [UC-006], [UC-007], [MIL-007], [UC-008], [MIL-010] | ## Version History | Date | Status | Author | Reviewer | Change | Commit | | --- | --- | --- | --- | --- | --- | -| 2026-10-11 | Deprecated | Jens Tirsvad Nielsen | S01 | Added US-001.16 and US-001.17 (C++ and C# checks, MIL-007 task 1) | [32b5ab6] | -| 2026-10-11 | Accepted | Jens Tirsvad Nielsen | S01 | US-001.16 and US-001.17 reviewed (RC-057) and accepted; the Purpose and Scope names the later stories | [1bc56f8] | +| 2026-10-11 | Deprecated | Jens Tirsvad Nielsen | S01 | US-001.16 and US-001.17 reviewed (RC-057) and accepted; the Purpose and Scope names the later stories | [1bc56f8] | +| 2026-10-14 | Accepted | Jens Tirsvad Nielsen | AI as stand-in for S01 | Added US-001.18 to US-001.20 (code metrics and coverage badges, MIL-010 task 1); reviewed (RC-070) | pending | --- @@ -21,7 +21,8 @@ These stories cover the P1 use cases of the Project Plan that MIL-002 delivers credentials, onboarding a repository and diagnosing a failed run. Roles are the actors of [UCD-001]. Later stories cover quality checks (US-001.09, US-001.10 and, for C++ and C#, US-001.16 and US-001.17), releases and updates (US-001.11 to -US-001.15). Stories for the wiki (MIL-005, MIL-006) are not written yet. +US-001.15) and the code metrics and coverage badges (US-001.18 to +US-001.20). Stories for the wiki (MIL-005, MIL-006) are not written yet. ## Story List @@ -248,9 +249,50 @@ US-001.15). Stories for the wiki (MIL-005, MIL-006) are not written yet. | --- | --- | --- | | [UC-005], [MIL-007] | fits one iteration | none | +### US-001.18 — The size of the code is visible as badges + +**As a** Software Engineer, **I want** badges that show the number of code files and the number of lines of code without comment and blank lines, **so that** the size of the project is visible in its README without a service outside the instance. + +**Acceptance Criteria** + +- Given a repository that calls the shared code-metrics workflow, when a change is pushed, then a badge for the code files and a badge for the lines of code show the numbers of that revision. +- Given a file with a comment line, a blank line and a code line, when the lines are counted, then only the code line is counted as a line of code. +- Given a repository without a file of a supported language, when the workflow runs, then it passes with a notice and the badges show zero. + +| Traces to | Size | INVEST exceptions | +| --- | --- | --- | +| [UC-008], [MIL-010] | fits one iteration | none | + +### US-001.19 — The coverage is visible as a badge + +**As a** Software Engineer, **I want** a badge that shows how much of the code my tests cover, **so that** a drop in coverage is seen as soon as it is pushed. + +**Acceptance Criteria** + +- Given a repository that supplies a coverage report, when a change is pushed, then a coverage badge shows the percentage of the report, and its colour follows the thresholds of the contract. +- Given a coverage report that is missing or that cannot be read, when the workflow runs, then it fails with a `[configuration]` message that names the report file and no coverage badge is changed. +- Given a repository that supplies no coverage report, when the workflow runs, then it passes with a notice and publishes no coverage badge. + +| Traces to | Size | INVEST exceptions | +| --- | --- | --- | +| [UC-008], [MIL-010] | fits one iteration | none | + +### US-001.20 — A run with unchanged numbers changes nothing + +**As a** Software Engineer, **I want** the workflow to commit nothing when the numbers did not change, **so that** the history of the repository is not filled with badge commits. + +**Acceptance Criteria** + +- Given a push after which none of the three badges would differ, when the workflow runs, then no commit is made. +- Given a push after which a number changed, when the workflow runs, then one commit changes only the badge files, and the workflow does not start again from that commit. + +| Traces to | Size | INVEST exceptions | +| --- | --- | --- | +| [UC-008], [MIL-010] | fits one iteration | none | + ## INVEST Check -All seventeen stories are independent of each other except that US-001.05 and +All twenty stories are independent of each other except that US-001.05 and US-001.06 build on US-001.04 (the secrets must exist before a repository can run the workflow). Each states one goal and can be negotiated, delivers value to the named role, is small enough for one iteration, and has acceptance @@ -274,5 +316,6 @@ behaviour is tested rather than built. [UC-005]: ./uc-005/uc.md [UC-006]: ./uc-006/uc.md [UC-007]: ./uc-007/uc.md -[32b5ab6]: https://git.tirsystem.com/TirSystem/github-action/commit/32b5ab62ab5a82638b71b8fcfb43a819e92ab346 +[UC-008]: ./uc-008/uc.md +[MIL-010]: ./milestones/mil-010-code-metrics-badges.md [1bc56f8]: https://git.tirsystem.com/TirSystem/github-action/commit/1bc56f8b771531f221f218fc00b147ecc73c051e -- 2.54.0 From aa4ca0f897b7ea20d90745aa4b7d7a215df123a8 Mon Sep 17 00:00:00 2001 From: Jens Tirsvad Nielsen Date: Wed, 14 Oct 2026 13:43:15 +0800 Subject: [PATCH 02/10] MIL-010 task 2: ADR-0010, the contract of the code metrics badges (RC-072) Refs #156 --- .../adr-0010-code-metrics-badges-contract.md | 203 ++++++++++++++++++ docs/artifact-registry.md | 4 +- docs/sqa/reviews/rc-072-adr-0010.md | 54 +++++ docs/sqa/traceability-matrix.md | 9 +- 4 files changed, 265 insertions(+), 5 deletions(-) create mode 100644 docs/adr/adr-0010-code-metrics-badges-contract.md create mode 100644 docs/sqa/reviews/rc-072-adr-0010.md diff --git a/docs/adr/adr-0010-code-metrics-badges-contract.md b/docs/adr/adr-0010-code-metrics-badges-contract.md new file mode 100644 index 0000000..59a0c86 --- /dev/null +++ b/docs/adr/adr-0010-code-metrics-badges-contract.md @@ -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 `` 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 diff --git a/docs/artifact-registry.md b/docs/artifact-registry.md index 1d51d75..1239ca2 100644 --- a/docs/artifact-registry.md +++ b/docs/artifact-registry.md @@ -11,9 +11,9 @@ each document lives in *this* project and the next version to use. | SA | Stakeholder Analysis | docs/stakeholder-analysis.md | 002 | | PP | Project Plan | docs/project-plan.md | 002 | | MIL | Milestone / Gateway | docs/milestones/*.md | 011 | -| RC | SQA Review Record | docs/sqa/reviews/rc-*.md | 072 | +| RC | SQA Review Record | docs/sqa/reviews/rc-*.md | 073 | | TM | Traceability Matrix | docs/sqa/traceability-matrix.md | 002 | -| ADR | Architecture Decision Record | docs/adr/adr-NNNN-*.md | 0010 | +| ADR | Architecture Decision Record | docs/adr/adr-NNNN-*.md | 0011 | | UCD | Use Case Diagram | docs/use-case-diagram.md | 002 | | US | User Story | docs/user-stories.md | 002 | | UC | Use Case | docs/uc-NNN/uc.md | 009 | diff --git a/docs/sqa/reviews/rc-072-adr-0010.md b/docs/sqa/reviews/rc-072-adr-0010.md new file mode 100644 index 0000000..70c7f1f --- /dev/null +++ b/docs/sqa/reviews/rc-072-adr-0010.md @@ -0,0 +1,54 @@ +# Review Record: ADR-0010 (contract of the code metrics and coverage badges workflow) + +## Metadata +| Key | Value | +| --- | --- | +| ID | RC-072 | +| CrossReference | [ADR-0010], [QC-ADR-001] | + +## Version History +| Date | Status | Author | Reviewer | Change | Commit | +| --- | --- | --- | --- | --- | --- | +| 2026-10-14 | Accepted | Jens Tirsvad Nielsen | AI as stand-in for S01 | Initial version; accepted | pending | + +--- + +## Artifact Under Review + +- Instance reviewed: [ADR-0010] +- Checklist used: [QC-ADR-001] +- Scope: full review (MIL-010 task 2) +- Language and domain: n/a (technical type) +- Language reviewer: none +- Mode: autonomous, not independent (the reviewer is `AI as stand-in for S01`; the user accepted the plan in chat on 2026-10-14) + +## Checklist Results + +| # | Criterion | Status | Evidence/Notes | +| --- | --- | --- | --- | +| 1 | The ID (`ADR-NNNN`, 4-digit) matches the filename number and is unique and sequential | Pass | ADR-0010 in `adr-0010-code-metrics-badges-contract.md`; the previous ADR is ADR-0009. | +| 2 | Context states the problem, the forces (cost, risk, constraints) and the options evaluated | Pass | Problem, the decisions of the user, facts about the runner and the checkout directory, two counter options and three delivery options. | +| 3 | At least two options were considered, or the absence of alternatives is justified | Pass | Wrap an existing counter or write one; run artifact, default branch or a branch. | +| 4 | Decision is stated in one or two unhedged sentences and matches one of the evaluated options | Pass | A reusable workflow with three script files and a `badges` branch; the rules follow as a list. | +| 5 | Consequences list both Positive and Negative outcomes | Pass | Four positive, five negative. | +| 6 | Quality-attribute impacts (performance, security, portability, etc.) are named where the decision affects them | Pass | Security (a write token, a moving `tools-ref`, branch name validation), compatibility (report formats), maintainability (language table). | +| 7 | Affected Artifacts are listed as links, and each is also in CrossReference | Pass | MIL-010, UC-008, US-001, ADR-0005, ADR-0006 in both. | +| 8 | Status changes are recorded as new Version History rows with a Change summary and Commit link; retained rows are never rewritten | Pass | One row, Accepted, commit `pending` until the commit exists. | +| 9 | A superseded or deprecated ADR names its successor, and the successor links back | N-A | Neither superseded nor deprecated. | +| 10 | Written in business or technical language of the domain, with no unexplained abbreviations | Pass | SVG, XML and NUL are standard; Cobertura is named as a format. | +| 11 | Status is one of `Proposed`, `Accepted`, `Rejected`, `Deprecated` or `Superseded by ADR-NNNN`, and every Version History row uses only these | Pass | Accepted. | + +## Overall Verdict + +Go - all applicable criteria pass; the decisions of the user of 2026-10-14 are recorded and not reopened. The AI records the decision as stand-in for S01; there is no independent reader and the decision stays open to challenge in the run summary. + +## Action Items + +| Action | Owner | Due | +| --- | --- | --- | +| None | - | - | + +--- + +[ADR-0010]: ../../adr/adr-0010-code-metrics-badges-contract.md +[QC-ADR-001]: ../../../framework/qc/artifacts/qc-adr.md diff --git a/docs/sqa/traceability-matrix.md b/docs/sqa/traceability-matrix.md index 20cbbbf..b6f58e1 100644 --- a/docs/sqa/traceability-matrix.md +++ b/docs/sqa/traceability-matrix.md @@ -4,13 +4,13 @@ | Key | Value | | --- | --- | | ID | TM-001 | -| CrossReference | [BC-001], [SA-001], [PP-001], [MIL-001], [MIL-002], [MIL-003], [MIL-004], [MIL-005], [MIL-006], [MIL-007], [MIL-008], [MIL-009], [MIL-010], [UC-008], [RC-001], [RC-002], [RC-003], [RC-004], [RC-005], [RC-006], [RC-007], [RC-008], [RC-009], [RC-010], [RC-011], [RC-012], [RC-013], [RC-014], [RC-015], [RC-016], [RC-017], [RC-018], [RC-019], [RC-020], [RC-021], [RC-022], [RC-023], [RC-024], [RC-025], [RC-026], [RC-027], [RC-028], [RC-029], [RC-030], [RC-031], [RC-032], [RC-033], [RC-034], [RC-035], [RC-036], [RC-037], [RC-038], [RC-039], [RC-040], [RC-041], [RC-042], [RC-043], [RC-044], [RC-045], [RC-046], [RC-047], [RC-048], [RC-049], [RC-050], [RC-051], [RC-052], [RC-053], [RC-054], [RC-055], [RC-056], [RC-057], [RC-058], [RC-059], [RC-060], [RC-061], [RC-062], [RC-063], [RC-064], [RC-065], [RC-066], [RC-067], [RC-068], [RC-069], [RC-070], [RC-071], [ADR-0007], [ADR-0008], [ADR-0009], [UC-007], [UC-006], [ADR-0006], [UC-005], [ADR-0005], [UCD-001], [US-001], [UC-001], [UC-002], [UC-003], [UC-004], [ADR-0001], [ADR-0002], [ADR-0003], [ADR-0004] | +| CrossReference | [BC-001], [SA-001], [PP-001], [MIL-001], [MIL-002], [MIL-003], [MIL-004], [MIL-005], [MIL-006], [MIL-007], [MIL-008], [MIL-009], [MIL-010], [UC-008], [RC-001], [RC-002], [RC-003], [RC-004], [RC-005], [RC-006], [RC-007], [RC-008], [RC-009], [RC-010], [RC-011], [RC-012], [RC-013], [RC-014], [RC-015], [RC-016], [RC-017], [RC-018], [RC-019], [RC-020], [RC-021], [RC-022], [RC-023], [RC-024], [RC-025], [RC-026], [RC-027], [RC-028], [RC-029], [RC-030], [RC-031], [RC-032], [RC-033], [RC-034], [RC-035], [RC-036], [RC-037], [RC-038], [RC-039], [RC-040], [RC-041], [RC-042], [RC-043], [RC-044], [RC-045], [RC-046], [RC-047], [RC-048], [RC-049], [RC-050], [RC-051], [RC-052], [RC-053], [RC-054], [RC-055], [RC-056], [RC-057], [RC-058], [RC-059], [RC-060], [RC-061], [RC-062], [RC-063], [RC-064], [RC-065], [RC-066], [RC-067], [RC-068], [RC-069], [RC-070], [RC-071], [RC-072], [ADR-0007], [ADR-0008], [ADR-0009], [ADR-0010], [UC-007], [UC-006], [ADR-0006], [UC-005], [ADR-0005], [UCD-001], [US-001], [UC-001], [UC-002], [UC-003], [UC-004], [ADR-0001], [ADR-0002], [ADR-0003], [ADR-0004] | ## Version History | Date | Status | Author | Reviewer | Change | Commit | | --- | --- | --- | --- | --- | --- | -| 2026-10-14 | Proposed | Jens Tirsvad Nielsen | S01 | Added MIL-010, RC-067 and RC-068 (code metrics and coverage badges) | [5002450] | | 2026-10-14 | Proposed | Jens Tirsvad Nielsen | S01 | Added UC-008, RC-069 to RC-071 (code metrics badges, MIL-010 task 1) | pending | +| 2026-10-14 | Proposed | Jens Tirsvad Nielsen | S01 | Added ADR-0010, RC-072 (contract of the code metrics badges, MIL-010 task 2) | pending | --- @@ -31,6 +31,7 @@ updated whenever an artifact instance is created or reviewed. | [MIL-002] | MIL | [BC-001], [PP-001], [MIL-001] | [MIL-003], [MIL-005] | [RC-005], [RC-027] | | [MIL-003] | MIL | [BC-001], [PP-001], [MIL-002] | [MIL-004] | [RC-006], [RC-028], [RC-033], [RC-039] | | [MIL-004] | MIL | [BC-001], [PP-001], [MIL-003] | - | [RC-007], [RC-029], [RC-034], [RC-048] | +| [ADR-0010] | ADR | [MIL-010], [UC-008], [US-001], [ADR-0005], [ADR-0006] | - | [RC-072] | | [RC-001] | RC | [BC-001] | - | - | | [RC-002] | RC | [PP-001] | - | - | | [RC-003] | RC | [MIL-001] | - | - | @@ -127,6 +128,7 @@ updated whenever an artifact instance is created or reviewed. | [RC-069] | RC | [UC-008] | - | - | | [RC-070] | RC | [US-001] | - | - | | [RC-071] | RC | [UCD-001] | - | - | +| [RC-072] | RC | [ADR-0010] | - | - | ## Coverage Notes @@ -150,6 +152,7 @@ updated whenever an artifact instance is created or reviewed. [MIL-008]: ../milestones/mil-008-wiki-reusable-workflow.md [MIL-010]: ../milestones/mil-010-code-metrics-badges.md [MIL-009]: ../milestones/mil-009-migrate-wiki-callers-and-remove-scoped-workflow.md +[ADR-0010]: ../adr/adr-0010-code-metrics-badges-contract.md [RC-001]: ./reviews/rc-001-business-case.md [RC-002]: ./reviews/rc-002-project-plan.md [RC-003]: ./reviews/rc-003-mil-001.md @@ -231,6 +234,7 @@ updated whenever an artifact instance is created or reviewed. [RC-069]: ./reviews/rc-069-uc-008.md [RC-070]: ./reviews/rc-070-us-001-metrics.md [RC-071]: ./reviews/rc-071-ucd-001-metrics.md +[RC-072]: ./reviews/rc-072-adr-0010.md [ADR-0008]: ../adr/adr-0008-cpp-csharp-quality-check-contract.md [ADR-0009]: ../adr/adr-0009-wiki-publishing-reusable-workflow.md [ADR-0007]: ../adr/adr-0007-release-publish-destination.md @@ -240,4 +244,3 @@ updated whenever an artifact instance is created or reviewed. [ADR-0006]: ../adr/adr-0006-release-process-and-version-pinning.md [UC-005]: ../uc-005/uc.md [ADR-0005]: ../adr/adr-0005-quality-check-contract.md -[5002450]: https://git.tirsystem.com/TirSystem/github-action/commit/50024505fe788b1c5e3021c0d1fc3d079590070e -- 2.54.0 From 6c3544837ca029b6e6e39a031786a229de2e68bc Mon Sep 17 00:00:00 2001 From: Jens Tirsvad Nielsen <jenstirsvad@gmail.com> Date: Wed, 14 Oct 2026 13:44:34 +0800 Subject: [PATCH 03/10] MIL-010 task 3: results of the delivery check Refs #157 --- .../milestones/mil-010-code-metrics-badges.md | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/docs/milestones/mil-010-code-metrics-badges.md b/docs/milestones/mil-010-code-metrics-badges.md index 15d67ce..5f4e4d5 100644 --- a/docs/milestones/mil-010-code-metrics-badges.md +++ b/docs/milestones/mil-010-code-metrics-badges.md @@ -103,6 +103,33 @@ Case sets no deadline. | 8 | Adopt the workflow in one other repository | Onboard a real repository, or a throwaway one when none fits, show the badges in its README and verify a seeded change that moves the coverage badge. Fix the gaps in the guide that appear. | No | | | 9 | Release | Add the changelog entry (a new workflow is a minor change, ADR-0006) and run the Release workflow; record the results in this milestone and review it. | No | | +## Verification results + +Task 3 was run on 2026-10-14 with a throwaway branch, `verify-badges-delivery`, +in `TirSystem/github-action` (run 1418, success). One job on `ubuntu-latest` +with `permissions: contents: write` checked the repository out, created an +orphan branch `badges-verify` with one SVG file and a commit message ending in +`[skip ci]`, and pushed it with the token of the run (the credential that +`actions/checkout` stores); it then requested the raw URL. + +| Fact | Result | +| --- | --- | +| Push with the token of the run | Accepted: `git push origin HEAD:refs/heads/badges-verify` created the branch; no secret and no personal access token was needed | +| Raw URL on the instance | `https://git.tirsystem.com/TirSystem/github-action/raw/branch/badges-verify/code-files.svg` answers `200`, `Content-Type: image/svg+xml`, `Content-Security-Policy: default-src 'none'; style-src 'unsafe-inline'; sandbox`, `Cache-Control: public, max-age=21600` (six hours) | +| The branch on the GitHub mirror | The push mirror (`sync_on_commit`, every 8 h otherwise) copied the branch within seconds; `https://raw.githubusercontent.com/TirSystem/github-action/badges-verify/code-files.svg` answers `200`, `Content-Type: image/svg+xml`, `Cache-Control: max-age=300` | +| Repository visibility | Public; a README image on GitHub reads the raw URL, which needs no token | + +The SVG is served as an image on both hosts, so it renders in a README on the +instance and on the mirror; the content security policy rules out script and +external references inside the file, as ADR-0010 requires. The six-hour +cache on the instance means a changed badge can show the old number for up to +six hours there; the guide says so. Not covered by this task: a private +repository (the raw URL then needs a login, so a badge does not render for an +anonymous reader), and the rendering inside a Gitea README, which is the same +`<img>` request. The throwaway branches `verify-badges-delivery` and +`badges-verify` are left in place because deleting a branch is an action the +run stops on; they are listed in the summary for the user to delete. + --- [BC-001]: ../business-case.md -- 2.54.0 From 5bc14d789d7397442aee09b6c39cd28bd2efce86 Mon Sep 17 00:00:00 2001 From: Jens Tirsvad Nielsen <jenstirsvad@gmail.com> Date: Wed, 14 Oct 2026 13:48:04 +0800 Subject: [PATCH 04/10] MIL-010 tasks 4 and 5: counter, coverage reader and badge renderer Task: MIL-010#4 Task: MIL-010#5 Refs #158 Refs #159 --- tests/test_badges.py | 282 ++++++++++++++++++++++++++++++ tests/test_count_loc.py | 317 +++++++++++++++++++++++++++++++++ tools/badges.py | 226 ++++++++++++++++++++++++ tools/count_loc.py | 375 ++++++++++++++++++++++++++++++++++++++++ 4 files changed, 1200 insertions(+) create mode 100644 tests/test_badges.py create mode 100644 tests/test_count_loc.py create mode 100644 tools/badges.py create mode 100644 tools/count_loc.py diff --git a/tests/test_badges.py b/tests/test_badges.py new file mode 100644 index 0000000..e980169 --- /dev/null +++ b/tests/test_badges.py @@ -0,0 +1,282 @@ +"""Offline tests for tools/badges.py (ADR-0010): the coverage reader and the SVG renderer.""" + +import importlib.util +import json +import xml.etree.ElementTree as ElementTree +from pathlib import Path + +import pytest + +REPO_ROOT = Path(__file__).resolve().parent.parent +_SPEC = importlib.util.spec_from_file_location( + "badges", REPO_ROOT / "tools" / "badges.py" +) +assert _SPEC is not None and _SPEC.loader is not None +badges = importlib.util.module_from_spec(_SPEC) +_SPEC.loader.exec_module(badges) + +DTD = ( + '<!DOCTYPE coverage SYSTEM "http://cobertura.sourceforge.net/xml/coverage-04.dtd">' +) + + +def report( + tmp_path: Path, attributes: str, root: str = "coverage", prolog: str = "" +) -> Path: + path = tmp_path / "coverage.xml" + path.write_text( + f'<?xml version="1.0" ?>\n{prolog}<{root} {attributes}><packages/></{root}>\n' + ) + return path + + +class TestReadCoverage: + @pytest.mark.parametrize( + ("covered", "valid", "percent"), + [ + (0, 10, 0), + (1, 3, 33), + (2, 3, 67), + (5, 8, 63), + (87, 100, 87), + (1, 200, 1), + (10, 10, 100), + ], + ) + def test_percent_is_covered_over_valid_rounded_half_up( + self, tmp_path, covered, valid, percent + ): + path = report( + tmp_path, f'lines-covered="{covered}" lines-valid="{valid}" line-rate="0"' + ) + assert badges.read_coverage(path) == percent + + def test_the_half_rounds_up(self, tmp_path): + assert ( + badges.read_coverage(report(tmp_path, 'lines-covered="1" lines-valid="8"')) + == 13 + ) + + def test_line_rate_is_used_when_the_counts_are_absent(self, tmp_path): + assert badges.read_coverage(report(tmp_path, 'line-rate="0.875"')) == 88 + + def test_a_document_type_declaration_is_accepted(self, tmp_path): + path = report(tmp_path, 'lines-covered="9" lines-valid="10"', prolog=DTD + "\n") + assert badges.read_coverage(path) == 90 + + def test_missing_file_names_the_file(self, tmp_path): + with pytest.raises(badges.ConfigurationError, match="nope.xml' does not exist"): + badges.read_coverage(tmp_path / "nope.xml") + + def test_malformed_xml_names_the_file(self, tmp_path): + path = tmp_path / "bad.xml" + path.write_text("<coverage lines-valid=") + with pytest.raises( + badges.ConfigurationError, match="bad.xml' is not well-formed" + ): + badges.read_coverage(path) + + def test_other_xml_is_not_a_cobertura_report(self, tmp_path): + with pytest.raises(badges.ConfigurationError, match="not a Cobertura report"): + badges.read_coverage(report(tmp_path, 'lines-valid="1"', root="testsuite")) + + def test_no_valid_lines_is_a_failure_and_not_zero_percent(self, tmp_path): + with pytest.raises(badges.ConfigurationError, match="has no valid lines"): + badges.read_coverage(report(tmp_path, 'lines-covered="0" lines-valid="0"')) + + def test_covered_above_valid_is_refused(self, tmp_path): + with pytest.raises(badges.ConfigurationError, match="outside 0..lines-valid"): + badges.read_coverage(report(tmp_path, 'lines-covered="5" lines-valid="4"')) + + def test_report_without_any_rate_is_refused(self, tmp_path): + with pytest.raises(badges.ConfigurationError, match="neither lines-valid"): + badges.read_coverage(report(tmp_path, 'branch-rate="1"')) + + def test_line_rate_outside_zero_to_one_is_refused(self, tmp_path): + with pytest.raises(badges.ConfigurationError, match="outside 0..1"): + badges.read_coverage(report(tmp_path, 'line-rate="1.5"')) + + def test_values_that_are_not_numbers_are_refused(self, tmp_path): + with pytest.raises(badges.ConfigurationError, match="not a number"): + badges.read_coverage(report(tmp_path, 'lines-covered="a" lines-valid="b"')) + with pytest.raises(badges.ConfigurationError, match="not a number"): + badges.read_coverage(report(tmp_path, 'line-rate="high"')) + + def test_entity_declarations_are_refused(self, tmp_path): + prolog = '<!DOCTYPE coverage [<!ENTITY x "y">]>\n' + with pytest.raises(badges.ConfigurationError, match="declares entities"): + badges.read_coverage( + report(tmp_path, 'lines-covered="1" lines-valid="2"', prolog=prolog) + ) + + +class TestColor: + @pytest.mark.parametrize( + ("percent", "color"), + [ + (0, badges.RED), + (59, badges.RED), + (60, badges.YELLOW), + (79, badges.YELLOW), + (80, badges.GREEN), + (100, badges.GREEN), + ], + ) + def test_thresholds_are_warn_and_good(self, percent, color): + assert badges.coverage_color(percent, 60, 80) == color + + def test_thresholds_can_be_moved(self): + assert badges.coverage_color(50, 40, 50) == badges.GREEN + assert badges.coverage_color(39, 40, 50) == badges.RED + + +class TestRender: + def test_is_valid_xml_with_title_and_text_alternative(self): + svg = badges.render("code files", "1,234", badges.BLUE) + root = ElementTree.fromstring(svg) + ns = "{http://www.w3.org/2000/svg}" + assert root.tag == ns + "svg" + assert root.get("role") == "img" + assert root.get("aria-label") == "code files: 1,234" + assert root.find(ns + "title").text == "code files: 1,234" + + def test_same_input_gives_the_same_bytes_and_has_no_script_or_link(self): + first = badges.render("coverage", "87%", badges.GREEN) + assert first == badges.render("coverage", "87%", badges.GREEN) + for forbidden in ( + "<script", + "href", + "<image", + "<style", + "http://www.w3.org/1999/xlink", + ): + assert forbidden not in first + + def test_width_follows_the_number_of_characters(self): + narrow = ElementTree.fromstring(badges.render("a", "1", badges.BLUE)).get( + "width" + ) + wide = ElementTree.fromstring(badges.render("a", "1,000", badges.BLUE)).get( + "width" + ) + assert int(wide) - int(narrow) == 4 * badges.CHAR_WIDTH + + def test_text_is_escaped(self): + svg = badges.render("a<b", 'c&"d', badges.BLUE) + assert "a<b" in svg and "c&"d" in svg + ElementTree.fromstring(svg) + + def test_text_colour_has_the_better_contrast(self): + assert badges._text_color("#555555") == "#ffffff" + assert badges._text_color(badges.GREEN) == "#000000" + + +def metrics_file(tmp_path: Path, files: int = 3, lines: int = 1200) -> Path: + path = tmp_path / "metrics.json" + path.write_text( + json.dumps( + {"files": files, "lines": lines, "comment": 5, "blank": 6, "languages": {}} + ) + ) + return path + + +class TestMain: + def test_writes_three_badges_and_the_metrics(self, tmp_path, capsys): + out = tmp_path / "out" + cov = report(tmp_path, 'lines-covered="87" lines-valid="100"') + code = badges.main( + [ + "--metrics", + str(metrics_file(tmp_path)), + "--output-dir", + str(out), + "--coverage-report", + str(cov), + ] + ) + assert code == 0 + assert sorted(p.name for p in out.iterdir()) == [ + "code-files.svg", + "code-lines.svg", + "code-metrics.json", + "coverage.svg", + ] + assert ( + 'aria-label="lines of code: 1,200"' in (out / "code-lines.svg").read_text() + ) + assert 'aria-label="coverage: 87%"' in (out / "coverage.svg").read_text() + assert f'fill="{badges.GREEN}"' in (out / "coverage.svg").read_text() + assert b"\r" not in (out / "code-files.svg").read_bytes() + assert "wrote" in capsys.readouterr().out + + def test_output_is_byte_identical_on_a_second_run(self, tmp_path): + args = [ + "--metrics", + str(metrics_file(tmp_path)), + "--output-dir", + str(tmp_path / "o"), + ] + badges.main(args) + first = {p.name: p.read_bytes() for p in (tmp_path / "o").iterdir()} + badges.main(args) + assert first == {p.name: p.read_bytes() for p in (tmp_path / "o").iterdir()} + + def test_without_a_report_no_coverage_badge_and_a_notice(self, tmp_path, capsys): + out = tmp_path / "out" + assert ( + badges.main( + ["--metrics", str(metrics_file(tmp_path)), "--output-dir", str(out)] + ) + == 0 + ) + assert not (out / "coverage.svg").exists() + assert "::notice::No coverage-report given" in capsys.readouterr().out + + def test_a_bad_report_fails_and_writes_nothing(self, tmp_path, capsys): + out = tmp_path / "out" + code = badges.main( + [ + "--metrics", + str(metrics_file(tmp_path)), + "--output-dir", + str(out), + "--coverage-report", + str(tmp_path / "missing.xml"), + ] + ) + assert code == 1 + assert "::error::[configuration] coverage-report" in capsys.readouterr().out + assert not out.exists() + + @pytest.mark.parametrize( + ("warn", "good", "message"), + [ + ("x", "80", "coverage-warn 'x'"), + ("60", "101", "coverage-good '101'"), + ("-1", "80", "coverage-warn '-1'"), + ("90", "80", "above coverage-good"), + ], + ) + def test_invalid_thresholds_fail(self, tmp_path, capsys, warn, good, message): + code = badges.main( + [ + "--metrics", + str(metrics_file(tmp_path)), + "--output-dir", + str(tmp_path / "o"), + "--coverage-warn", + warn, + "--coverage-good", + good, + ] + ) + assert code == 1 + assert message in capsys.readouterr().out + + def test_unreadable_metrics_fail(self, tmp_path, capsys): + bad = tmp_path / "m.json" + bad.write_text('{"files": 1}') + code = badges.main(["--metrics", str(bad), "--output-dir", str(tmp_path / "o")]) + assert code == 1 + assert "cannot be read" in capsys.readouterr().out diff --git a/tests/test_count_loc.py b/tests/test_count_loc.py new file mode 100644 index 0000000..cc7ab6e --- /dev/null +++ b/tests/test_count_loc.py @@ -0,0 +1,317 @@ +"""Offline tests for tools/count_loc.py (ADR-0010): hand-counted fixtures per language.""" + +import importlib.util +import json +from pathlib import Path + +import pytest + +REPO_ROOT = Path(__file__).resolve().parent.parent +_SPEC = importlib.util.spec_from_file_location( + "count_loc", REPO_ROOT / "tools" / "count_loc.py" +) +assert _SPEC is not None and _SPEC.loader is not None +count_loc = importlib.util.module_from_spec(_SPEC) +_SPEC.loader.exec_module(count_loc) + +LANGUAGE = {language.name: language for language in count_loc.LANGUAGES} + +# name -> (source, code, comment, blank), counted by hand. +FIXTURES = { + "Python": ( + "# a comment\n" + "x = 1 # trailing comment\n" + "\n" + 's = "a # not a comment"\n' + "t = 'it''s # still a string'\n" + '"""docstring\n' + "# inside a string\n" + "\n" + '"""\n' + "y = 2", # no final newline + 7, + 1, + 2, + ), + "Shell": ( + "#!/usr/bin/env bash\n" + "# comment\n" + 'echo "$#" ${#args[@]} a#b\n' + "\n" + " # indented comment\n" + "echo 'it # stays' # but this is a comment\n" + "ls | wc -l;# after a semicolon\n", + 3, + 3, + 1, + ), + "PowerShell": ( + '<# block\ncomment #>\nWrite-Host "a # b"\n# line\n$x = 1 # trailing\n', + 2, + 3, + 0, + ), + "C": ( + "/* header\n" + " * text\n" + " */\n" + "#include <stdio.h>\n" + "\n" + "int main(void) { /* inline */ return 0; }\n" + "char quote = '\"'; // a double quote in a char\n" + 'const char *url = "http://example.com";\n' + "// done\n", + 4, + 4, + 1, + ), + "C++": ( + 'int a = 0; // one\n/* block */\nauto s = "/* not a comment */";\n', + 2, + 1, + 0, + ), + "C#": ( + "// line\n" + "using System;\n" + "/// <summary>doc</summary>\n" + 'class A { string s = "// no"; }\n', + 2, + 2, + 0, + ), + "Java": ( + "/** doc */\nclass A {\n // c\n int x; /* t */\n}\n", + 3, + 2, + 0, + ), + "JavaScript": ( + "const a = `line one\n" + "// in a template\n" + "line three`;\n" + "// real comment\n" + "const b = 'x // y';\n" + "/* a */ /* b */\n", + 4, + 2, + 0, + ), + "TypeScript": ( + "let n: number = 1; // c\n\n/*\n * block\n */\n", + 1, + 3, + 1, + ), + "Go": ( + "package main\n// c\nvar r = '/'\nvar raw = `// not a comment\n`\n", + 4, + 1, + 0, + ), + "Rust": ( + "/* outer /* nested */ still a comment */\n" + "fn f<'a>(x: &'a str) -> &'a str { x } // lifetimes\n" + "let c = '\\'';\n" + "// end\n", + 2, + 2, + 0, + ), + "SQL": ( + "-- query\n" + "SELECT '-- not a comment' FROM t; -- but this is\n" + "/* a\n" + " block */\n" + "\n", + 1, + 3, + 1, + ), +} + + +@pytest.mark.parametrize("name", sorted(FIXTURES)) +def test_fixture_counts_equal_the_hand_count(name): + source, code, comment, blank = FIXTURES[name] + assert count_loc.classify_text(source, LANGUAGE[name]) == (code, comment, blank) + + +def test_every_language_has_a_fixture(): + assert set(FIXTURES) == set(LANGUAGE) + + +class TestClassify: + def test_empty_text_has_no_lines(self): + assert count_loc.classify_text("", LANGUAGE["Python"]) == (0, 0, 0) + + def test_windows_line_endings_are_one_line_each(self): + assert count_loc.classify_text("a = 1\r\n# c\r\n\r\n", LANGUAGE["Python"]) == ( + 1, + 1, + 1, + ) + + def test_blank_line_inside_a_block_comment_is_blank(self): + assert count_loc.classify_text("/*\n\n*/\n", LANGUAGE["C"]) == (0, 2, 1) + + def test_code_after_a_closing_block_comment_is_code(self): + assert count_loc.classify_text("/* a\n b */ x();\n", LANGUAGE["C"]) == (1, 1, 0) + + def test_unterminated_string_ends_at_end_of_line(self): + assert count_loc.classify_text('s = "open // x\n// c\n', LANGUAGE["C"]) == ( + 1, + 1, + 0, + ) + + def test_unterminated_backtick_string_continues(self): + assert count_loc.classify_text("a = `x\n// y\n", LANGUAGE["JavaScript"]) == ( + 2, + 0, + 0, + ) + + def test_escaped_quote_does_not_end_the_string(self): + assert count_loc.classify_text('s = "a \\" // b";\n', LANGUAGE["C"]) == ( + 1, + 0, + 0, + ) + + +class TestPatterns: + def test_star_stops_at_a_slash_and_double_star_does_not(self): + excludes = count_loc.Excludes(["src/*.py", "docs/**"]) + assert excludes.matches("a.py", "src/a.py") + assert not excludes.matches("a.py", "src/sub/a.py") + assert excludes.matches("x.py", "docs/a/b/x.py") + + def test_name_pattern_matches_anywhere(self): + excludes = count_loc.Excludes(["gen_*", "?.py"]) + assert excludes.matches("gen_a.py", "deep/er/gen_a.py") + assert excludes.matches("a.py", "x/a.py") + assert not excludes.matches("ab.py", "x/ab.py") + + def test_empty_patterns_are_ignored(self): + assert not count_loc.Excludes(["", " / "]).matches("a.py", "a.py") + + +def write(root: Path, name: str, content: str | bytes) -> None: + path = root / name + path.parent.mkdir(parents=True, exist_ok=True) + if isinstance(content, bytes): + path.write_bytes(content) + else: + path.write_text(content, encoding="utf-8") + + +class TestCount: + def test_counts_files_and_lines_per_language(self, tmp_path): + write(tmp_path, "a.py", "x = 1\n# c\n\n") + write(tmp_path, "pkg/b.py", "y = 2\n") + write(tmp_path, "run.sh", "echo hi\n") + write(tmp_path, "README.md", "# not code\n") + result = count_loc.count(tmp_path) + assert result == { + "files": 3, + "lines": 3, + "comment": 1, + "blank": 1, + "languages": { + "Python": {"files": 2, "lines": 2, "comment": 1, "blank": 1}, + "Shell": {"files": 1, "lines": 1, "comment": 0, "blank": 0}, + }, + } + + def test_default_excludes_hidden_vendored_and_shared_script_directories( + self, tmp_path + ): + write(tmp_path, "keep.py", "a = 1\n") + for directory in ( + ".git", + ".venv", + "node_modules", + "vendor", + "shared-scripts", + "build", + ): + write(tmp_path, f"{directory}/skip.py", "b = 2\n") + assert count_loc.count(tmp_path)["files"] == 1 + + def test_exclude_patterns_add_to_the_defaults(self, tmp_path): + write(tmp_path, "keep.py", "a = 1\n") + write(tmp_path, "gen/out.py", "b = 2\n") + write(tmp_path, "src/gen_x.py", "c = 3\n") + write(tmp_path, "src/keep2.py", "d = 4\n") + result = count_loc.count(tmp_path, ["gen", "gen_*"]) + assert result["files"] == 2 + + def test_binary_file_with_a_code_extension_is_not_counted(self, tmp_path): + write(tmp_path, "blob.py", b"a = 1\n\x00\x01") + assert count_loc.count(tmp_path)["files"] == 0 + + def test_invalid_utf8_and_a_byte_order_mark_are_read(self, tmp_path): + write(tmp_path, "a.py", b"\xef\xbb\xbfx = 1\n\xff\xfe = 2\n") + assert count_loc.count(tmp_path)["lines"] == 2 + + def test_empty_code_file_is_a_file_with_no_lines(self, tmp_path): + write(tmp_path, "empty.py", "") + result = count_loc.count(tmp_path) + assert (result["files"], result["lines"]) == (1, 0) + + def test_directory_without_code_files_counts_zero(self, tmp_path): + write(tmp_path, "notes.txt", "hello\n") + result = count_loc.count(tmp_path) + assert result == { + "files": 0, + "lines": 0, + "comment": 0, + "blank": 0, + "languages": {}, + } + + def test_symbolic_link_is_not_followed(self, tmp_path): + real = tmp_path / "real" + write(real, "a.py", "x = 1\n") + link = tmp_path / "link.py" + try: + link.symlink_to(real / "a.py") + except (OSError, NotImplementedError): + pytest.skip("symbolic links are not available here") + assert count_loc.count(real)["files"] == 1 + assert count_loc.count(tmp_path)["files"] == 1 # real/a.py only, not link.py + + +class TestMain: + def test_prints_json_to_standard_output(self, tmp_path, capsys): + write(tmp_path, "a.py", "x = 1\n") + assert count_loc.main(["--directory", str(tmp_path)]) == 0 + out = json.loads(capsys.readouterr().out) + assert out["files"] == 1 and out["lines"] == 1 + + def test_writes_the_output_file_with_lf_endings(self, tmp_path): + write(tmp_path, "src/a.py", "x = 1\n") + target = tmp_path / "result.json" + assert ( + count_loc.main( + ["--directory", str(tmp_path / "src"), "--output", str(target)] + ) + == 0 + ) + data = target.read_bytes() + assert b"\r" not in data and json.loads(data)["files"] == 1 + + def test_exclude_option_is_a_comma_list(self, tmp_path, capsys): + write(tmp_path, "a.py", "x = 1\n") + write(tmp_path, "b.py", "y = 1\n") + write(tmp_path, "c.py", "z = 1\n") + assert ( + count_loc.main(["--directory", str(tmp_path), "--exclude", "a.py, b.py"]) + == 0 + ) + assert json.loads(capsys.readouterr().out)["files"] == 1 + + def test_missing_directory_is_a_configuration_error(self, tmp_path, capsys): + assert count_loc.main(["--directory", str(tmp_path / "nope")]) == 1 + assert "::error::[configuration]" in capsys.readouterr().out diff --git a/tools/badges.py b/tools/badges.py new file mode 100644 index 0000000..1c6ebf1 --- /dev/null +++ b/tools/badges.py @@ -0,0 +1,226 @@ +"""Read a Cobertura coverage report and write the code metrics badges (ADR-0010). + +Usage: + python tools/badges.py --metrics code-metrics.json --output-dir DIR + [--coverage-report coverage.xml] [--coverage-warn 60] [--coverage-good 80] + +Writes `code-files.svg`, `code-lines.svg`, `code-metrics.json` and, when a report +is given, `coverage.svg` into DIR. The same input gives the same bytes. + +Exit codes: 0 written, 1 a configuration problem (printed as +`::error::[configuration] ...`, nothing is written). +""" + +import argparse +import json +import sys +import xml.etree.ElementTree as ElementTree +from decimal import ROUND_HALF_UP, Decimal, InvalidOperation +from pathlib import Path + +BLUE = "#007ec6" +RED = "#e05d44" +YELLOW = "#dfb317" +GREEN = "#44cc11" +LABEL_COLOR = "#555555" +CHAR_WIDTH = 6 +PADDING = 10 +HEIGHT = 20 + + +class ConfigurationError(Exception): + """An input is invalid; the message is printed as a `[configuration]` failure.""" + + +def read_coverage(path: Path) -> int: + """Return the line coverage of a Cobertura XML report as a whole percent, half up.""" + if not path.is_file(): + raise ConfigurationError(f"coverage-report '{path}' does not exist.") + data = path.read_bytes() + if b"<!ENTITY" in data: + raise ConfigurationError( + f"coverage-report '{path}' declares entities, which are refused." + ) + try: + root = ElementTree.fromstring(data) + except ElementTree.ParseError as error: + raise ConfigurationError( + f"coverage-report '{path}' is not well-formed XML: {error}." + ) from error + if root.tag != "coverage": + raise ConfigurationError( + f"coverage-report '{path}' is not a Cobertura report (root element '{root.tag}')." + ) + covered, valid = root.get("lines-covered"), root.get("lines-valid") + try: + if covered is not None and valid is not None: + covered_lines, valid_lines = int(covered), int(valid) + if valid_lines <= 0: + raise ConfigurationError( + f"coverage-report '{path}' has no valid lines." + ) + if not 0 <= covered_lines <= valid_lines: + raise ConfigurationError( + f"coverage-report '{path}' has lines-covered outside 0..lines-valid." + ) + fraction = Decimal(covered_lines) / Decimal(valid_lines) + else: + rate = root.get("line-rate") + if rate is None: + raise ConfigurationError( + f"coverage-report '{path}' has neither lines-valid and lines-covered nor line-rate." + ) + fraction = Decimal(rate) + if not Decimal(0) <= fraction <= Decimal(1): + raise ConfigurationError( + f"coverage-report '{path}' has a line-rate outside 0..1." + ) + except (ValueError, InvalidOperation) as error: + raise ConfigurationError( + f"coverage-report '{path}' has a value that is not a number." + ) from error + return int((fraction * 100).quantize(Decimal(1), rounding=ROUND_HALF_UP)) + + +def coverage_color(percent: int, warn: float, good: float) -> str: + """Return red below warn, yellow from warn to below good, green from good.""" + if percent < warn: + return RED + if percent < good: + return YELLOW + return GREEN + + +def _luminance(color: str) -> float: + channels = [int(color[i : i + 2], 16) / 255 for i in (1, 3, 5)] + linear = [ + c / 12.92 if c <= 0.04045 else ((c + 0.055) / 1.055) ** 2.4 for c in channels + ] + return 0.2126 * linear[0] + 0.7152 * linear[1] + 0.0722 * linear[2] + + +def _text_color(background: str) -> str: + """Black or white, whichever has the higher contrast on the background.""" + lum = _luminance(background) + return "#ffffff" if (1.05 / (lum + 0.05)) >= ((lum + 0.05) / 0.05) else "#000000" + + +def _escape(text: str) -> str: + return ( + text.replace("&", "&") + .replace("<", "<") + .replace(">", ">") + .replace('"', """) + ) + + +def render(label: str, value: str, color: str) -> str: + """Return a flat two-part SVG badge; the width follows the number of characters.""" + label_width = CHAR_WIDTH * len(label) + PADDING + value_width = CHAR_WIDTH * len(value) + PADDING + total = label_width + value_width + text = _escape(f"{label}: {value}") + label_x = label_width / 2 + value_x = label_width + value_width / 2 + return ( + f'<svg xmlns="http://www.w3.org/2000/svg" width="{total}" height="{HEIGHT}" ' + f'role="img" aria-label="{text}">\n' + f"<title>{text}\n" + f'\n' + f'\n' + '\n' + f'{_escape(label)}\n' + f'{_escape(value)}\n' + "\n" + "\n" + ) + + +def _threshold(name: str, text: str) -> float: + try: + value = float(text) + except ValueError as error: + raise ConfigurationError( + f"{name} '{text}' is not a number from 0 to 100." + ) from error + if not 0 <= value <= 100: + raise ConfigurationError(f"{name} '{text}' is not a number from 0 to 100.") + return value + + +def write_badges( + file_count: int, + line_count: int, + metrics_json: str, + output_dir: Path, + coverage: int | None, + warn: float, + good: float, +) -> list[Path]: + """Write the badge files and the metrics JSON; return the paths in a fixed order.""" + output_dir.mkdir(parents=True, exist_ok=True) + files = { + "code-files.svg": render("code files", f"{file_count:,}", BLUE), + "code-lines.svg": render("lines of code", f"{line_count:,}", BLUE), + } + if coverage is not None: + files["coverage.svg"] = render( + "coverage", f"{coverage}%", coverage_color(coverage, warn, good) + ) + files["code-metrics.json"] = metrics_json + paths = [] + for name, content in files.items(): + path = output_dir / name + path.write_text(content, encoding="utf-8", newline="\n") + paths.append(path) + return paths + + +def main(argv: list[str] | None = None) -> int: + parser = argparse.ArgumentParser(description=__doc__.splitlines()[0]) + parser.add_argument("--metrics", required=True, help="JSON written by count_loc.py") + parser.add_argument("--output-dir", required=True) + parser.add_argument("--coverage-report", default="") + parser.add_argument("--coverage-warn", default="60") + parser.add_argument("--coverage-good", default="80") + args = parser.parse_args(argv) + try: + warn = _threshold("coverage-warn", args.coverage_warn) + good = _threshold("coverage-good", args.coverage_good) + if warn > good: + raise ConfigurationError( + f"coverage-warn {args.coverage_warn} is above coverage-good {args.coverage_good}." + ) + coverage = ( + read_coverage(Path(args.coverage_report)) if args.coverage_report else None + ) + metrics = json.loads(Path(args.metrics).read_text(encoding="utf-8")) + file_count, line_count = int(metrics["files"]), int(metrics["lines"]) + except ConfigurationError as error: + print(f"::error::[configuration] {error}") + return 1 + except (OSError, ValueError, KeyError, TypeError) as error: + print( + f"::error::[configuration] metrics file '{args.metrics}' cannot be read: {error}" + ) + return 1 + if coverage is None: + print("::notice::No coverage-report given; no coverage badge is written.") + metrics_json = json.dumps(metrics, indent=2, sort_keys=True) + "\n" + written = write_badges( + file_count, + line_count, + metrics_json, + Path(args.output_dir), + coverage, + warn, + good, + ) + for path in written: + print(f"wrote {path}") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tools/count_loc.py b/tools/count_loc.py new file mode 100644 index 0000000..4472900 --- /dev/null +++ b/tools/count_loc.py @@ -0,0 +1,375 @@ +"""Count the code files and the lines of code of a directory (ADR-0010). + +A code file is a text file whose extension is in LANGUAGES. Every physical line +of it is exactly one of blank, comment or code; the lines of code are the code +lines only. The result is JSON: + + {"files": 3, "lines": 120, "comment": 14, "blank": 22, + "languages": {"Python": {"files": 2, "lines": 100, "comment": 10, "blank": 20}}} + +Usage: + python tools/count_loc.py [--directory DIR] [--exclude PATTERNS] [--output FILE] + +Exit codes: 0 counted, 1 the directory is not a directory (printed as +`::error::[configuration] ...`). +""" + +import argparse +import json +import os +import re +import sys +from dataclasses import dataclass +from pathlib import Path + +DEFAULT_EXCLUDED_DIRS = frozenset( + { + "node_modules", + "vendor", + "third_party", + "dist", + "build", + "out", + "bin", + "obj", + "target", + "venv", + "__pycache__", + "shared-scripts", + } +) + +_C_STYLE_CHAR = re.compile(r"'(?:\\.[^']*|[^'\\])'") + + +@dataclass(frozen=True) +class Language: + """The comment and string rules of one language.""" + + name: str + extensions: tuple[str, ...] + line_comment: str | None = None + block: tuple[str, str] | None = None + nested_block: bool = False + hash_at_word_start: bool = False + triple_quotes: bool = False + backtick_strings: bool = False + single_quote: str = ( + "string" # "string", "char" (a literal of one character) or "none" + ) + backslash_escapes: bool = True + escapes_in_single_quotes: bool = True + + +def _c_family(name: str, *extensions: str, single_quote: str = "char") -> Language: + return Language( + name, + extensions, + line_comment="//", + block=("/*", "*/"), + single_quote=single_quote, + ) + + +LANGUAGES: tuple[Language, ...] = ( + Language("Python", (".py",), line_comment="#", triple_quotes=True), + Language( + "Shell", + (".sh", ".bash"), + line_comment="#", + hash_at_word_start=True, + escapes_in_single_quotes=False, + ), + Language( + "PowerShell", + (".ps1", ".psm1"), + line_comment="#", + block=("<#", "#>"), + hash_at_word_start=True, + backslash_escapes=False, + ), + _c_family("C", ".c", ".h"), + _c_family("C++", ".cc", ".cpp", ".cxx", ".hh", ".hpp"), + _c_family("C#", ".cs"), + _c_family("Java", ".java"), + Language( + "JavaScript", + (".js", ".mjs", ".jsx"), + line_comment="//", + block=("/*", "*/"), + backtick_strings=True, + ), + Language( + "TypeScript", + (".ts", ".tsx"), + line_comment="//", + block=("/*", "*/"), + backtick_strings=True, + ), + Language( + "Go", + (".go",), + line_comment="//", + block=("/*", "*/"), + backtick_strings=True, + single_quote="char", + ), + Language( + "Rust", + (".rs",), + line_comment="//", + block=("/*", "*/"), + nested_block=True, + single_quote="char", + ), + Language( + "SQL", + (".sql",), + line_comment="--", + block=("/*", "*/"), + backslash_escapes=False, + ), +) + +_BY_EXTENSION = {ext: language for language in LANGUAGES for ext in language.extensions} +_WORD_START = " \t;|&(" + + +def language_of(path: str) -> Language | None: + """Return the language of a file name by its extension, or None.""" + return _BY_EXTENSION.get(os.path.splitext(path)[1].lower()) + + +@dataclass +class _State: + block_depth: int = 0 + triple: str | None = None + backtick: bool = False + + +def _skip_string(line: str, start: int, quote: str, escapes: bool) -> int: + """Return the index after the string that opens at start; the end of line if unclosed.""" + i = start + 1 + while i < len(line): + char = line[i] + if escapes and char == "\\": + i += 2 + continue + if char == quote: + return i + 1 + i += 1 + return len(line) + + +def _scan_line(line: str, language: Language, state: _State) -> tuple[bool, bool]: + """Scan one line; return (has_code, has_comment) and update the state.""" + has_code = False + has_comment = False + i = 0 + size = len(line) + block = language.block + while i < size: + if state.block_depth: + assert block is not None + has_comment = True + end = line.find(block[1], i) + nested = line.find(block[0], i) if language.nested_block else -1 + if nested != -1 and (end == -1 or nested < end): + state.block_depth += 1 + i = nested + len(block[0]) + elif end == -1: + return has_code, has_comment + else: + state.block_depth -= 1 + i = end + len(block[1]) + continue + if state.triple is not None: + has_code = True + end = line.find(state.triple, i) + if end == -1: + return has_code, has_comment + i = end + len(state.triple) + state.triple = None + continue + if state.backtick: + has_code = True + end = line.find("`", i) + if end == -1: + return has_code, has_comment + i = end + 1 + state.backtick = False + continue + char = line[i] + if char in " \t\r\f\v": + i += 1 + continue + if block is not None and line.startswith(block[0], i): + state.block_depth = 1 + has_comment = True + i += len(block[0]) + continue + comment = language.line_comment + if comment is not None and line.startswith(comment, i): + word_start = i == 0 or line[i - 1] in _WORD_START + if word_start or not language.hash_at_word_start: + return has_code, True + if language.triple_quotes and ( + line.startswith('"""', i) or line.startswith("'''", i) + ): + state.triple = line[i : i + 3] + has_code = True + i += 3 + continue + if char == '"': + has_code = True + i = _skip_string(line, i, '"', language.backslash_escapes) + continue + if char == "'" and language.single_quote == "string": + has_code = True + escapes = language.backslash_escapes and language.escapes_in_single_quotes + i = _skip_string(line, i, "'", escapes) + continue + if char == "'" and language.single_quote == "char": + has_code = True + literal = _C_STYLE_CHAR.match(line, i) + i = literal.end() if literal else i + 1 + continue + if char == "`" and language.backtick_strings: + has_code = True + end = line.find("`", i + 1) + if end == -1: + state.backtick = True + return has_code, has_comment + i = end + 1 + continue + has_code = True + i += 1 + return has_code, has_comment + + +def classify_text(text: str, language: Language) -> tuple[int, int, int]: + """Return (code, comment, blank) line counts of the text of a file.""" + lines = text.replace("\r\n", "\n").replace("\r", "\n").split("\n") + if lines and lines[-1] == "": + lines.pop() + state = _State() + code = comment = blank = 0 + for line in lines: + has_code, _ = _scan_line(line, language, state) + if line.strip() == "": + blank += 1 + elif has_code: + code += 1 + else: + comment += 1 + return code, comment, blank + + +def _pattern_regex(pattern: str) -> re.Pattern[str]: + """Translate an exclude pattern: `*` stops at `/`, `**` crosses it, `?` is one character.""" + out = [] + i = 0 + while i < len(pattern): + char = pattern[i] + if pattern.startswith("**", i): + out.append(".*") + i += 2 + continue + out.append( + "[^/]*" if char == "*" else "[^/]" if char == "?" else re.escape(char) + ) + i += 1 + return re.compile("".join(out) + r"\Z") + + +class Excludes: + """The exclude patterns of the caller; a pattern without `/` matches a name anywhere.""" + + def __init__(self, patterns: list[str]) -> None: + self._names: list[re.Pattern[str]] = [] + self._paths: list[re.Pattern[str]] = [] + for pattern in patterns: + pattern = pattern.strip().strip("/") + if not pattern: + continue + target = self._paths if "/" in pattern else self._names + target.append(_pattern_regex(pattern)) + + def matches(self, name: str, relative: str) -> bool: + return any(rx.match(name) for rx in self._names) or any( + rx.match(relative) for rx in self._paths + ) + + +def _is_text(data: bytes) -> bool: + return b"\x00" not in data + + +def count(directory: Path, patterns: list[str] | None = None) -> dict[str, object]: + """Count the code files under directory; see the module docstring for the result.""" + excludes = Excludes(patterns or []) + totals = {"files": 0, "lines": 0, "comment": 0, "blank": 0} + per_language: dict[str, dict[str, int]] = {} + root = str(directory) + for current, dirs, files in os.walk(root, followlinks=False): + relative_dir = os.path.relpath(current, root).replace(os.sep, "/") + prefix = "" if relative_dir == "." else relative_dir + "/" + dirs[:] = sorted( + name + for name in dirs + if not name.startswith(".") + and name not in DEFAULT_EXCLUDED_DIRS + and not excludes.matches(name, prefix + name) + ) + for name in sorted(files): + full = os.path.join(current, name) + language = language_of(name) + if ( + language is None + or os.path.islink(full) + or excludes.matches(name, prefix + name) + ): + continue + data = Path(full).read_bytes() + if not _is_text(data): + continue + text = data.decode("utf-8", errors="replace").lstrip("") + code, comment, blank = classify_text(text, language) + entry = per_language.setdefault( + language.name, {"files": 0, "lines": 0, "comment": 0, "blank": 0} + ) + for target in (entry, totals): + target["files"] += 1 + target["lines"] += code + target["comment"] += comment + target["blank"] += blank + return {**totals, "languages": dict(sorted(per_language.items()))} + + +def main(argv: list[str] | None = None) -> int: + parser = argparse.ArgumentParser(description=__doc__.splitlines()[0]) + parser.add_argument( + "--directory", default=".", help="directory to count (default .)" + ) + parser.add_argument("--exclude", default="", help="comma list of exclude patterns") + parser.add_argument( + "--output", help="write the JSON here instead of standard output" + ) + args = parser.parse_args(argv) + directory = Path(args.directory) + if not directory.is_dir(): + print( + f"::error::[configuration] working-directory '{args.directory}' does not exist." + ) + return 1 + result = count(directory, [p for p in args.exclude.split(",") if p.strip()]) + text = json.dumps(result, indent=2, sort_keys=True) + "\n" + if args.output: + Path(args.output).write_text(text, encoding="utf-8", newline="\n") + else: + sys.stdout.write(text) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) -- 2.54.0 From 4fc0a8cab38664e0b58e3929dbfdede49483b910 Mon Sep 17 00:00:00 2001 From: Jens Tirsvad Nielsen Date: Wed, 14 Oct 2026 13:50:34 +0800 Subject: [PATCH 05/10] MIL-010 task 6: reusable workflow code-metrics.yml and tools/code-metrics.sh Task: MIL-010#6 Refs #160 --- .gitea/workflows/code-metrics.yml | 81 ++++++++ tests/test_badges.py | 7 + tests/test_code_metrics.py | 306 ++++++++++++++++++++++++++++++ tools/badges.py | 2 + tools/code-metrics.sh | 130 +++++++++++++ 5 files changed, 526 insertions(+) create mode 100644 .gitea/workflows/code-metrics.yml create mode 100644 tests/test_code_metrics.py create mode 100644 tools/code-metrics.sh diff --git a/.gitea/workflows/code-metrics.yml b/.gitea/workflows/code-metrics.yml new file mode 100644 index 0000000..abbc514 --- /dev/null +++ b/.gitea/workflows/code-metrics.yml @@ -0,0 +1,81 @@ +name: Code metrics + +# Reusable workflow (ADR-0010). A repository calls it with +# +# on: +# push: +# branches: [main] +# jobs: +# metrics: +# uses: TirSystem/github-action/.gitea/workflows/code-metrics.yml@v1 +# permissions: +# contents: write +# +# It counts the code files and the lines of code of the repository (comment +# and blank lines are not counted), reads an optional Cobertura XML coverage +# report and commits three SVG badges to a branch of the calling repository. +# It runs no test, needs no secret and writes only the badges branch. +on: + workflow_call: + inputs: + working-directory: + description: "Directory, relative to the repository root, whose code is counted" + type: string + default: "." + exclude: + description: "Comma list of patterns of files and directories that are not counted, in addition to the defaults of ADR-0010" + type: string + default: "" + coverage-report: + description: "Path, relative to the repository root, of a Cobertura XML coverage report; empty writes no coverage badge" + type: string + default: "" + coverage-warn: + description: "Coverage in percent below which the coverage badge is red" + type: string + default: "60" + coverage-good: + description: "Coverage in percent from which the coverage badge is green" + type: string + default: "80" + badges-branch: + description: "Branch that holds the badge files; it is created when it does not exist" + type: string + default: "badges" + tools-ref: + description: "Tag or branch of TirSystem/github-action whose tools/ files are run; pin it to the exact tag together with the workflow" + type: string + default: "v1" + +jobs: + code-metrics: + permissions: + contents: write + runs-on: ubuntu-latest + timeout-minutes: 10 + env: + WORKING_DIRECTORY: ${{ inputs.working-directory }} + EXCLUDE: ${{ inputs.exclude }} + COVERAGE_REPORT: ${{ inputs.coverage-report }} + COVERAGE_WARN: ${{ inputs.coverage-warn }} + COVERAGE_GOOD: ${{ inputs.coverage-good }} + BADGES_BRANCH: ${{ inputs.badges-branch }} + steps: + - name: Check out the repository + uses: actions/checkout@v4.2.2 + + - name: Fetch the shared scripts + uses: actions/checkout@v4.2.2 + with: + repository: TirSystem/github-action + ref: ${{ inputs.tools-ref }} + path: shared-scripts + + - name: Validate the inputs + run: bash shared-scripts/tools/code-metrics.sh validate + + - name: Count the code and write the badges + run: bash shared-scripts/tools/code-metrics.sh measure + + - name: Publish the badges + run: bash shared-scripts/tools/code-metrics.sh publish diff --git a/tests/test_badges.py b/tests/test_badges.py index e980169..628a522 100644 --- a/tests/test_badges.py +++ b/tests/test_badges.py @@ -280,3 +280,10 @@ class TestMain: code = badges.main(["--metrics", str(bad), "--output-dir", str(tmp_path / "o")]) assert code == 1 assert "cannot be read" in capsys.readouterr().out + + def test_no_code_files_is_a_notice_and_badges_that_show_zero(self, tmp_path, capsys): + out = tmp_path / "out" + metrics = metrics_file(tmp_path, files=0, lines=0) + assert badges.main(["--metrics", str(metrics), "--output-dir", str(out)]) == 0 + assert "::notice::No code files were found" in capsys.readouterr().out + assert 'aria-label="code files: 0"' in (out / "code-files.svg").read_text() diff --git a/tests/test_code_metrics.py b/tests/test_code_metrics.py new file mode 100644 index 0000000..e9d8631 --- /dev/null +++ b/tests/test_code_metrics.py @@ -0,0 +1,306 @@ +"""Offline tests for the reusable code-metrics workflow and tools/code-metrics.sh (ADR-0010). + +The script steps run in a throwaway git repository whose `origin` is a bare +repository on disk, so no network and no instance is needed. +""" + +import os +import re +import shutil +import subprocess +import sys +from pathlib import Path + +import pytest +import yaml + +from workflow_source import REPO_ROOT + +WORKFLOW = REPO_ROOT / ".gitea" / "workflows" / "code-metrics.yml" +SCRIPT = REPO_ROOT / "tools" / "code-metrics.sh" +BASH = shutil.which("bash") +GIT = shutil.which("git") + +pytestmark = pytest.mark.skipif( + BASH is None or GIT is None, reason="bash and git are needed to run the workflow steps" +) + +COVERAGE = '\n\n' + + +@pytest.fixture(scope="module") +def document() -> dict: + return yaml.safe_load(WORKFLOW.read_text(encoding="utf-8")) + + +class TestDefinition: + def test_is_a_reusable_workflow_with_the_contract_inputs(self, document): + inputs = document[True]["workflow_call"]["inputs"] # PyYAML reads `on` as True + assert {name: item["default"] for name, item in inputs.items()} == { + "working-directory": ".", + "exclude": "", + "coverage-report": "", + "coverage-warn": "60", + "coverage-good": "80", + "badges-branch": "badges", + "tools-ref": "v1", + } + + def test_asks_for_contents_write_and_nothing_else_and_needs_no_secret(self, document): + job = document["jobs"]["code-metrics"] + assert job["permissions"] == {"contents": "write"} + assert "secrets" not in WORKFLOW.read_text(encoding="utf-8").replace("needs no secret", "") + + def test_inputs_reach_the_scripts_only_through_the_environment(self, document): + job = document["jobs"]["code-metrics"] + for step in job["steps"]: + assert "${{" not in step.get("run", ""), f"{step.get('name')} interpolates an expression" + assert set(job["env"]) == { + "WORKING_DIRECTORY", + "EXCLUDE", + "COVERAGE_REPORT", + "COVERAGE_WARN", + "COVERAGE_GOOD", + "BADGES_BRANCH", + } + + def test_the_workflow_embeds_no_script_and_runs_the_file(self, document): + steps = document["jobs"]["code-metrics"]["steps"] + assert [step["run"] for step in steps if "run" in step] == [ + "bash shared-scripts/tools/code-metrics.sh validate", + "bash shared-scripts/tools/code-metrics.sh measure", + "bash shared-scripts/tools/code-metrics.sh publish", + ] + + def test_the_scripts_are_fetched_at_the_tools_ref_input(self, document): + steps = document["jobs"]["code-metrics"]["steps"] + fetch = next(step for step in steps if step.get("name") == "Fetch the shared scripts") + assert fetch["with"] == { + "repository": "TirSystem/github-action", + "ref": "${{ inputs.tools-ref }}", + "path": "shared-scripts", + } + + def test_the_script_parses(self): + result = subprocess.run([BASH, "-n", str(SCRIPT)], text=True, capture_output=True) + assert result.returncode == 0, result.stderr + + def test_an_unknown_command_is_refused(self): + result = subprocess.run([BASH, str(SCRIPT), "bogus"], text=True, capture_output=True) + assert result.returncode == 2 + + +def git(*args: str, cwd: Path) -> str: + result = subprocess.run( + [GIT, "-c", "user.name=t", "-c", "user.email=t@t", *args], + cwd=cwd, + text=True, + capture_output=True, + check=True, + ) + return result.stdout.strip() + + +@pytest.fixture() +def python3_bin(tmp_path) -> Path: + """A directory with a `python3` that runs the interpreter of the tests.""" + directory = tmp_path / "pybin" + directory.mkdir() + shim = directory / "python3" + shim.write_text(f'#!/bin/sh\nexec "{Path(sys.executable).as_posix()}" "$@"\n', newline="\n") + shim.chmod(0o755) + return directory + + +@pytest.fixture() +def remote(tmp_path) -> Path: + path = tmp_path / "remote.git" + subprocess.run([GIT, "init", "-q", "--bare", str(path)], check=True) + return path + + +@pytest.fixture() +def repo(tmp_path, remote) -> Path: + root = tmp_path / "repo" + root.mkdir() + git("init", "-q", "-b", "main", cwd=root) + git("remote", "add", "origin", str(remote), cwd=root) + (root / "a.py").write_text("x = 1\n# c\n\n", newline="\n") + (root / "run.sh").write_text("echo hi\n", newline="\n") + git("add", "-A", cwd=root) + git("commit", "-q", "-m", "init", cwd=root) + return root + + +def run(command: str, repo: Path, tmp_path: Path, python3_bin: Path, **env: str): + environment = dict(os.environ) + environment.update( + { + "WORKING_DIRECTORY": ".", + "EXCLUDE": "", + "COVERAGE_REPORT": "", + "COVERAGE_WARN": "60", + "COVERAGE_GOOD": "80", + "BADGES_BRANCH": "badges", + "BADGES_OUT": str(tmp_path / "out"), + "PATH": f"{python3_bin.as_posix()}{os.pathsep}{environment['PATH']}", + } + ) + environment.update(env) + return subprocess.run( + [BASH, str(SCRIPT), command], cwd=repo, env=environment, text=True, capture_output=True + ) + + +def branch_files(remote: Path, branch: str = "badges") -> list[str]: + out = subprocess.run( + [GIT, "--git-dir", str(remote), "ls-tree", "-r", "--name-only", branch], + text=True, + capture_output=True, + ) + return out.stdout.split() + + +def commit_count(remote: Path, branch: str = "badges") -> int: + out = subprocess.run( + [GIT, "--git-dir", str(remote), "rev-list", "--count", branch], text=True, capture_output=True + ) + return int(out.stdout.strip() or 0) + + +def measure_and_publish(repo, tmp_path, python3_bin, **env): + measured = run("measure", repo, tmp_path, python3_bin, **env) + assert measured.returncode == 0, measured.stdout + measured.stderr + return run("publish", repo, tmp_path, python3_bin, **env) + + +class TestValidate: + def test_accepts_the_defaults(self, repo, tmp_path, python3_bin): + result = run("validate", repo, tmp_path, python3_bin) + assert result.returncode == 0, result.stdout + assert "badges go to the branch badges" in result.stdout + + @pytest.mark.parametrize( + ("name", "value", "message"), + [ + ("WORKING_DIRECTORY", "/etc", "working-directory must be a relative path"), + ("WORKING_DIRECTORY", "../x", "working-directory must be a relative path"), + ("WORKING_DIRECTORY", "nope", "working-directory 'nope' does not exist"), + ("COVERAGE_REPORT", "/tmp/c.xml", "coverage-report must be a relative path"), + ("BADGES_BRANCH", "main", "must not be a default branch"), + ("BADGES_BRANCH", "master", "must not be a default branch"), + ("BADGES_BRANCH", "", "must not be a default branch"), + ("BADGES_BRANCH", "a b", "is not a valid branch name"), + ("BADGES_BRANCH", "x..y", "is not a valid branch name"), + ], + ) + def test_refuses_an_invalid_input_and_names_it( + self, repo, tmp_path, python3_bin, name, value, message + ): + result = run("validate", repo, tmp_path, python3_bin, **{name: value}) + assert result.returncode == 1 + assert f"::error::[configuration] " in result.stdout + assert message in result.stdout + + +class TestPublish: + def test_creates_the_branch_with_the_badges_and_the_numbers(self, repo, remote, tmp_path, python3_bin): + result = measure_and_publish(repo, tmp_path, python3_bin) + assert result.returncode == 0, result.stdout + result.stderr + assert sorted(branch_files(remote)) == ["code-files.svg", "code-lines.svg", "code-metrics.json"] + shown = subprocess.run( + [GIT, "--git-dir", str(remote), "show", "badges:code-lines.svg"], text=True, capture_output=True + ).stdout + assert 'aria-label="lines of code: 2"' in shown # a.py has one code line, run.sh one + message = subprocess.run( + [GIT, "--git-dir", str(remote), "log", "-1", "--format=%s", "badges"], + text=True, + capture_output=True, + ).stdout.strip() + assert message == "Update code metrics badges [skip ci]" + + def test_the_branch_of_the_caller_is_not_changed(self, repo, remote, tmp_path, python3_bin): + before = git("status", "--porcelain", cwd=repo) + measure_and_publish(repo, tmp_path, python3_bin) + assert git("status", "--porcelain", cwd=repo) == before + assert git("branch", "--show-current", cwd=repo) == "main" + + def test_an_unchanged_run_commits_nothing(self, repo, remote, tmp_path, python3_bin): + measure_and_publish(repo, tmp_path, python3_bin) + second = measure_and_publish(repo, tmp_path, python3_bin) + assert second.returncode == 0 + assert "Badges are current; nothing to commit." in second.stdout + assert commit_count(remote) == 1 + + def test_a_changed_number_is_one_commit_that_changes_only_badge_files( + self, repo, remote, tmp_path, python3_bin + ): + measure_and_publish(repo, tmp_path, python3_bin) + (repo / "b.py").write_text("y = 2\nz = 3\n", newline="\n") + git("add", "-A", cwd=repo) + git("commit", "-q", "-m", "more", cwd=repo) + result = measure_and_publish(repo, tmp_path, python3_bin) + assert result.returncode == 0, result.stdout + assert commit_count(remote) == 2 + changed = subprocess.run( + [GIT, "--git-dir", str(remote), "show", "--name-only", "--format=", "badges"], + text=True, + capture_output=True, + ).stdout.split() + assert sorted(changed) == ["code-files.svg", "code-lines.svg", "code-metrics.json"] + + def test_a_coverage_report_adds_the_coverage_badge(self, repo, remote, tmp_path, python3_bin): + (repo / "cov.xml").write_text(COVERAGE, newline="\n") + result = measure_and_publish(repo, tmp_path, python3_bin, COVERAGE_REPORT="cov.xml") + assert result.returncode == 0, result.stdout + assert "coverage.svg" in branch_files(remote) + + def test_a_missing_report_fails_names_the_file_and_changes_nothing( + self, repo, remote, tmp_path, python3_bin + ): + result = run("measure", repo, tmp_path, python3_bin, COVERAGE_REPORT="nope.xml") + assert result.returncode == 1 + assert "::error::[configuration] coverage-report 'nope.xml' does not exist." in result.stdout + assert branch_files(remote) == [] + + def test_the_shared_scripts_directory_is_not_counted(self, repo, remote, tmp_path, python3_bin): + shared = repo / "shared-scripts" / "tools" + shared.mkdir(parents=True) + (shared / "big.py").write_text("a = 1\n" * 50, newline="\n") + measure_and_publish(repo, tmp_path, python3_bin) + shown = subprocess.run( + [GIT, "--git-dir", str(remote), "show", "badges:code-files.svg"], text=True, capture_output=True + ).stdout + assert 'aria-label="code files: 2"' in shown + + def test_a_branch_that_holds_other_files_is_refused(self, repo, remote, tmp_path, python3_bin): + git("push", "-q", "origin", "main:docs", cwd=repo) + result = measure_and_publish(repo, tmp_path, python3_bin, BADGES_BRANCH="docs") + assert result.returncode == 1 + assert "it is not a badges branch" in result.stdout + + def test_publish_without_measure_is_refused(self, repo, tmp_path, python3_bin): + result = run("publish", repo, tmp_path, python3_bin) + assert result.returncode == 1 + assert "run measure first" in result.stdout + + def test_a_rejected_push_is_retried_once_after_a_fetch(self, repo, remote, tmp_path, python3_bin): + marker = tmp_path / "rejected-once" + hook = remote / "hooks" / "pre-receive" + hook.write_text( + f'#!/bin/sh\nif [ ! -e "{marker.as_posix()}" ]; then touch "{marker.as_posix()}"; ' + "echo rejected once >&2; exit 1; fi\nexit 0\n", + newline="\n", + ) + hook.chmod(0o755) + result = measure_and_publish(repo, tmp_path, python3_bin) + assert result.returncode == 0, result.stdout + result.stderr + assert marker.exists() + + def test_two_rejected_pushes_fail(self, repo, remote, tmp_path, python3_bin): + hook = remote / "hooks" / "pre-receive" + hook.write_text("#!/bin/sh\necho rejected >&2\nexit 1\n", newline="\n") + hook.chmod(0o755) + result = measure_and_publish(repo, tmp_path, python3_bin) + assert result.returncode == 1 + assert re.search(r"push to 'badges' failed twice", result.stdout) diff --git a/tools/badges.py b/tools/badges.py index 1c6ebf1..047b028 100644 --- a/tools/badges.py +++ b/tools/badges.py @@ -205,6 +205,8 @@ def main(argv: list[str] | None = None) -> int: f"::error::[configuration] metrics file '{args.metrics}' cannot be read: {error}" ) return 1 + if file_count == 0: + print("::notice::No code files were found; the badges show 0.") if coverage is None: print("::notice::No coverage-report given; no coverage badge is written.") metrics_json = json.dumps(metrics, indent=2, sort_keys=True) + "\n" diff --git a/tools/code-metrics.sh b/tools/code-metrics.sh new file mode 100644 index 0000000..44c3d04 --- /dev/null +++ b/tools/code-metrics.sh @@ -0,0 +1,130 @@ +#!/usr/bin/env bash +# Steps of the reusable workflow .gitea/workflows/code-metrics.yml (ADR-0010). +# The workflow checks out the calling repository, fetches this file into +# shared-scripts/ and runs one command per step: +# +# code-metrics.sh validate check the inputs, before anything is counted +# code-metrics.sh measure count the code, read the coverage report and +# write the badge files (count_loc.py, badges.py) +# code-metrics.sh publish commit the badge files to the badges branch, +# and nothing when none of them changed +# +# Settings come from the environment, never from interpolated expressions: +# WORKING_DIRECTORY, EXCLUDE, COVERAGE_REPORT, COVERAGE_WARN, COVERAGE_GOOD, +# BADGES_BRANCH, BADGES_OUT (the directory of the badge files; a temporary +# directory under RUNNER_TEMP by default). +# Every failure is printed as ::error::[category] message. + +TOOLS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +OUT_DIR="${BADGES_OUT:-${RUNNER_TEMP:-/tmp}/code-metrics}" +ALLOWED_FILES='^(code-files\.svg|code-lines\.svg|coverage\.svg|code-metrics\.json)$' +COMMIT_MESSAGE="Update code metrics badges [skip ci]" + +fail() { + echo "::error::[configuration] $1" + exit 1 +} + +relative_path() { + case "$1" in + "" | /* | .. | ../* | */.. | */../*) return 1 ;; + esac +} + +validate_inputs() { + set -u + relative_path "$WORKING_DIRECTORY" || + fail "working-directory must be a relative path inside the repository." + [ -d "$WORKING_DIRECTORY" ] || + fail "working-directory '$WORKING_DIRECTORY' does not exist." + if [ -n "$COVERAGE_REPORT" ]; then + relative_path "$COVERAGE_REPORT" || + fail "coverage-report must be a relative path inside the repository." + fi + case "$BADGES_BRANCH" in + main | master | HEAD | "") fail "badges-branch '$BADGES_BRANCH' must not be a default branch." ;; + esac + git check-ref-format --branch "$BADGES_BRANCH" > /dev/null 2>&1 || + fail "badges-branch '$BADGES_BRANCH' is not a valid branch name." + echo "Counting ${WORKING_DIRECTORY}; badges go to the branch ${BADGES_BRANCH}" +} + +measure() { + set -eu + metrics="$(mktemp)" + python3 "$TOOLS_DIR/count_loc.py" \ + --directory "$WORKING_DIRECTORY" --exclude "$EXCLUDE" --output "$metrics" + rm -rf "$OUT_DIR" + python3 "$TOOLS_DIR/badges.py" \ + --metrics "$metrics" --output-dir "$OUT_DIR" \ + --coverage-report "$COVERAGE_REPORT" \ + --coverage-warn "$COVERAGE_WARN" --coverage-good "$COVERAGE_GOOD" + rm -f "$metrics" +} + +# Copy the badge files into the worktree and stage them; return 0 when +# something differs from the branch. +stage_files() { + local worktree="$1" file + for file in "$OUT_DIR"/*; do + cp "$file" "$worktree/$(basename "$file")" + done + git -C "$worktree" add -- . + ! git -C "$worktree" diff --cached --quiet +} + +commit_files() { + git -C "$1" -c user.name="code-metrics" -c user.email="code-metrics@invalid" \ + commit --quiet -m "$COMMIT_MESSAGE" +} + +publish() { + set -eu + [ -d "$OUT_DIR" ] || fail "no badge files in '$OUT_DIR'; run measure first." + local worktree exists=0 committed=0 attempt + worktree="$(mktemp -d)" + if git fetch --quiet origin "+refs/heads/$BADGES_BRANCH:refs/remotes/origin/$BADGES_BRANCH" 2> /dev/null; then + exists=1 + if git ls-tree -r --name-only "origin/$BADGES_BRANCH" | grep -qvE "$ALLOWED_FILES"; then + fail "branch '$BADGES_BRANCH' holds files that are not badge files; it is not a badges branch." + fi + git worktree add --quiet --detach "$worktree" "origin/$BADGES_BRANCH" + else + git worktree add --quiet --detach "$worktree" HEAD + git -C "$worktree" checkout --quiet --orphan "$BADGES_BRANCH" + git -C "$worktree" rm -rfq --ignore-unmatch . + fi + + for attempt in 1 2; do + if [ "$committed" -eq 0 ]; then + if stage_files "$worktree"; then + commit_files "$worktree" + committed=1 + elif [ "$exists" -eq 1 ]; then + echo "Badges are current; nothing to commit." + break + fi + fi + if git -C "$worktree" push --quiet origin "HEAD:refs/heads/$BADGES_BRANCH"; then + echo "Published the badges to the branch ${BADGES_BRANCH}." + break + fi + [ "$attempt" -lt 2 ] || fail "the push to '$BADGES_BRANCH' failed twice." + if git fetch --quiet origin "+refs/heads/$BADGES_BRANCH:refs/remotes/origin/$BADGES_BRANCH" 2> /dev/null; then + git -C "$worktree" reset --quiet --hard "origin/$BADGES_BRANCH" + exists=1 + committed=0 + fi + done + git worktree remove --force "$worktree" +} + +case "${1:-}" in + validate) validate_inputs ;; + measure) measure ;; + publish) publish ;; + *) + echo "::error::[configuration] usage: code-metrics.sh validate|measure|publish" >&2 + exit 2 + ;; +esac -- 2.54.0 From 0dc5132a2ea419ba05035ee3594cec63335c240b Mon Sep 17 00:00:00 2001 From: Jens Tirsvad Nielsen Date: Wed, 14 Oct 2026 13:54:01 +0800 Subject: [PATCH 06/10] MIL-010 task 6: the coverage report reaches the workflow as an artifact (ADR-0010, RC-073) Task: MIL-010#6 Refs #160 --- .gitea/workflows/code-metrics.yml | 16 +++++- .../adr-0010-code-metrics-badges-contract.md | 27 +++++++--- docs/artifact-registry.md | 2 +- docs/sqa/reviews/rc-073-adr-0010-artifact.md | 54 +++++++++++++++++++ docs/sqa/traceability-matrix.md | 8 +-- tests/test_code_metrics.py | 28 ++++++++++ tools/badges.py | 18 +++---- tools/code-metrics.sh | 13 +++-- 8 files changed, 142 insertions(+), 24 deletions(-) create mode 100644 docs/sqa/reviews/rc-073-adr-0010-artifact.md diff --git a/.gitea/workflows/code-metrics.yml b/.gitea/workflows/code-metrics.yml index abbc514..b7f83b8 100644 --- a/.gitea/workflows/code-metrics.yml +++ b/.gitea/workflows/code-metrics.yml @@ -13,7 +13,9 @@ name: Code metrics # # It counts the code files and the lines of code of the repository (comment # and blank lines are not counted), reads an optional Cobertura XML coverage -# report and commits three SVG badges to a branch of the calling repository. +# report (an artifact of an earlier job of the caller, or a file of the +# repository) and commits three SVG badges to a branch of the calling +# repository. # It runs no test, needs no secret and writes only the badges branch. on: workflow_call: @@ -30,6 +32,10 @@ on: description: "Path, relative to the repository root, of a Cobertura XML coverage report; empty writes no coverage badge" type: string default: "" + coverage-artifact: + description: "Name of an artifact, uploaded by an earlier job of the same run, that holds the coverage report; empty reads coverage-report from the checkout" + type: string + default: "" coverage-warn: description: "Coverage in percent below which the coverage badge is red" type: string @@ -57,6 +63,7 @@ jobs: WORKING_DIRECTORY: ${{ inputs.working-directory }} EXCLUDE: ${{ inputs.exclude }} COVERAGE_REPORT: ${{ inputs.coverage-report }} + COVERAGE_ARTIFACT: ${{ inputs.coverage-artifact }} COVERAGE_WARN: ${{ inputs.coverage-warn }} COVERAGE_GOOD: ${{ inputs.coverage-good }} BADGES_BRANCH: ${{ inputs.badges-branch }} @@ -74,6 +81,13 @@ jobs: - name: Validate the inputs run: bash shared-scripts/tools/code-metrics.sh validate + - name: Download the coverage report + if: ${{ inputs.coverage-artifact != '' }} + uses: actions/download-artifact@v4.1.8 + with: + name: ${{ inputs.coverage-artifact }} + path: coverage-artifact + - name: Count the code and write the badges run: bash shared-scripts/tools/code-metrics.sh measure diff --git a/docs/adr/adr-0010-code-metrics-badges-contract.md b/docs/adr/adr-0010-code-metrics-badges-contract.md index 59a0c86..35fd2a2 100644 --- a/docs/adr/adr-0010-code-metrics-badges-contract.md +++ b/docs/adr/adr-0010-code-metrics-badges-contract.md @@ -9,7 +9,8 @@ ## 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 | +| 2026-10-14 | Deprecated | Jens Tirsvad Nielsen | AI as stand-in for S01 | Initial version; reviewed (RC-072) | pending | +| 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) | pending | Allowed Status values: `Proposed`, `Accepted`, `Rejected`, `Deprecated`, `Superseded by ADR-NNNN`. @@ -112,8 +113,15 @@ The rules that follow from it: 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). + 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` @@ -135,7 +143,8 @@ The rules that follow from it: 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-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 @@ -174,12 +183,16 @@ The rules that follow from it: `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 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, but a caller that pins + 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 diff --git a/docs/artifact-registry.md b/docs/artifact-registry.md index 1239ca2..2b0f32e 100644 --- a/docs/artifact-registry.md +++ b/docs/artifact-registry.md @@ -11,7 +11,7 @@ each document lives in *this* project and the next version to use. | SA | Stakeholder Analysis | docs/stakeholder-analysis.md | 002 | | PP | Project Plan | docs/project-plan.md | 002 | | MIL | Milestone / Gateway | docs/milestones/*.md | 011 | -| RC | SQA Review Record | docs/sqa/reviews/rc-*.md | 073 | +| RC | SQA Review Record | docs/sqa/reviews/rc-*.md | 074 | | TM | Traceability Matrix | docs/sqa/traceability-matrix.md | 002 | | ADR | Architecture Decision Record | docs/adr/adr-NNNN-*.md | 0011 | | UCD | Use Case Diagram | docs/use-case-diagram.md | 002 | diff --git a/docs/sqa/reviews/rc-073-adr-0010-artifact.md b/docs/sqa/reviews/rc-073-adr-0010-artifact.md new file mode 100644 index 0000000..af269f6 --- /dev/null +++ b/docs/sqa/reviews/rc-073-adr-0010-artifact.md @@ -0,0 +1,54 @@ +# Review Record: ADR-0010 (the coverage report as an artifact of the caller) + +## Metadata +| Key | Value | +| --- | --- | +| ID | RC-073 | +| CrossReference | [ADR-0010], [QC-ADR-001] | + +## Version History +| Date | Status | Author | Reviewer | Change | Commit | +| --- | --- | --- | --- | --- | --- | +| 2026-10-14 | Accepted | Jens Tirsvad Nielsen | AI as stand-in for S01 | Initial version; accepted | pending | + +--- + +## Artifact Under Review + +- Instance reviewed: [ADR-0010] +- Checklist used: [QC-ADR-001] +- Scope: delta re-review of the Coverage report and Inputs rules, the Consequences and the Version History after the implementation of MIL-010 task 6 showed that a called workflow cannot see the files of the caller's test job; earlier record RC-072 +- Language and domain: n/a (technical type) +- Language reviewer: none +- Mode: autonomous, not independent (the reviewer is `AI as stand-in for S01`; the user accepted the plan in chat on 2026-10-14) + +## Checklist Results + +| # | Criterion | Status | Evidence/Notes | +| --- | --- | --- | --- | +| 1 | The ID (`ADR-NNNN`, 4-digit) matches the filename number and is unique and sequential | Pass | Unchanged. | +| 2 | Context states the problem, the forces (cost, risk, constraints) and the options evaluated | Pass | Unchanged; the new force (a called workflow runs in its own job) is stated in the Decision rule that it explains. | +| 3 | At least two options were considered, or the absence of alternatives is justified | Pass | Unchanged. For the report: an artifact of the caller's run, or a file committed to the repository; both are supported. | +| 4 | Decision is stated in one or two unhedged sentences and matches one of the evaluated options | Pass | The Decision sentence is unchanged; the coverage rule is a refinement of it. | +| 5 | Consequences list both Positive and Negative outcomes | Pass | The negative consequence about the report now names the earlier job, the upload and the dependency on the artifact service, which task 8 verifies. | +| 6 | Quality-attribute impacts (performance, security, portability, etc.) are named where the decision affects them | Pass | The security bullet says the artifact is parsed as XML with entity declarations refused. | +| 7 | Affected Artifacts are listed as links, and each is also in CrossReference | Pass | Unchanged. | +| 8 | Status changes are recorded as new Version History rows with a Change summary and Commit link; retained rows are never rewritten | Pass | A new row; the earlier Accepted row became Deprecated and was not rewritten otherwise. | +| 9 | A superseded or deprecated ADR names its successor, and the successor links back | N-A | The ADR is amended, not superseded. | +| 10 | Written in business or technical language of the domain, with no unexplained abbreviations | Pass | Artifact is the term of the Actions runtime. | +| 11 | Status is one of `Proposed`, `Accepted`, `Rejected`, `Deprecated` or `Superseded by ADR-NNNN`, and every Version History row uses only these | Pass | Deprecated and Accepted. | + +## Overall Verdict + +Go - all applicable criteria pass; the amendment keeps the decisions of the user of 2026-10-14 and corrects a mechanism that cannot work as first written. The AI records the decision as stand-in for S01; there is no independent reader and the decision stays open to challenge in the run summary. + +## Action Items + +| Action | Owner | Due | +| --- | --- | --- | +| None | - | - | + +--- + +[ADR-0010]: ../../adr/adr-0010-code-metrics-badges-contract.md +[QC-ADR-001]: ../../../framework/qc/artifacts/qc-adr.md diff --git a/docs/sqa/traceability-matrix.md b/docs/sqa/traceability-matrix.md index b6f58e1..2a5a1a6 100644 --- a/docs/sqa/traceability-matrix.md +++ b/docs/sqa/traceability-matrix.md @@ -4,13 +4,13 @@ | Key | Value | | --- | --- | | ID | TM-001 | -| CrossReference | [BC-001], [SA-001], [PP-001], [MIL-001], [MIL-002], [MIL-003], [MIL-004], [MIL-005], [MIL-006], [MIL-007], [MIL-008], [MIL-009], [MIL-010], [UC-008], [RC-001], [RC-002], [RC-003], [RC-004], [RC-005], [RC-006], [RC-007], [RC-008], [RC-009], [RC-010], [RC-011], [RC-012], [RC-013], [RC-014], [RC-015], [RC-016], [RC-017], [RC-018], [RC-019], [RC-020], [RC-021], [RC-022], [RC-023], [RC-024], [RC-025], [RC-026], [RC-027], [RC-028], [RC-029], [RC-030], [RC-031], [RC-032], [RC-033], [RC-034], [RC-035], [RC-036], [RC-037], [RC-038], [RC-039], [RC-040], [RC-041], [RC-042], [RC-043], [RC-044], [RC-045], [RC-046], [RC-047], [RC-048], [RC-049], [RC-050], [RC-051], [RC-052], [RC-053], [RC-054], [RC-055], [RC-056], [RC-057], [RC-058], [RC-059], [RC-060], [RC-061], [RC-062], [RC-063], [RC-064], [RC-065], [RC-066], [RC-067], [RC-068], [RC-069], [RC-070], [RC-071], [RC-072], [ADR-0007], [ADR-0008], [ADR-0009], [ADR-0010], [UC-007], [UC-006], [ADR-0006], [UC-005], [ADR-0005], [UCD-001], [US-001], [UC-001], [UC-002], [UC-003], [UC-004], [ADR-0001], [ADR-0002], [ADR-0003], [ADR-0004] | +| CrossReference | [BC-001], [SA-001], [PP-001], [MIL-001], [MIL-002], [MIL-003], [MIL-004], [MIL-005], [MIL-006], [MIL-007], [MIL-008], [MIL-009], [MIL-010], [UC-008], [RC-001], [RC-002], [RC-003], [RC-004], [RC-005], [RC-006], [RC-007], [RC-008], [RC-009], [RC-010], [RC-011], [RC-012], [RC-013], [RC-014], [RC-015], [RC-016], [RC-017], [RC-018], [RC-019], [RC-020], [RC-021], [RC-022], [RC-023], [RC-024], [RC-025], [RC-026], [RC-027], [RC-028], [RC-029], [RC-030], [RC-031], [RC-032], [RC-033], [RC-034], [RC-035], [RC-036], [RC-037], [RC-038], [RC-039], [RC-040], [RC-041], [RC-042], [RC-043], [RC-044], [RC-045], [RC-046], [RC-047], [RC-048], [RC-049], [RC-050], [RC-051], [RC-052], [RC-053], [RC-054], [RC-055], [RC-056], [RC-057], [RC-058], [RC-059], [RC-060], [RC-061], [RC-062], [RC-063], [RC-064], [RC-065], [RC-066], [RC-067], [RC-068], [RC-069], [RC-070], [RC-071], [RC-072], [RC-073], [ADR-0007], [ADR-0008], [ADR-0009], [ADR-0010], [UC-007], [UC-006], [ADR-0006], [UC-005], [ADR-0005], [UCD-001], [US-001], [UC-001], [UC-002], [UC-003], [UC-004], [ADR-0001], [ADR-0002], [ADR-0003], [ADR-0004] | ## Version History | Date | Status | Author | Reviewer | Change | Commit | | --- | --- | --- | --- | --- | --- | -| 2026-10-14 | Proposed | Jens Tirsvad Nielsen | S01 | Added UC-008, RC-069 to RC-071 (code metrics badges, MIL-010 task 1) | pending | | 2026-10-14 | Proposed | Jens Tirsvad Nielsen | S01 | Added ADR-0010, RC-072 (contract of the code metrics badges, MIL-010 task 2) | pending | +| 2026-10-14 | Proposed | Jens Tirsvad Nielsen | S01 | Added RC-073 (ADR-0010: the coverage report as an artifact, MIL-010 task 6) | pending | --- @@ -31,7 +31,7 @@ updated whenever an artifact instance is created or reviewed. | [MIL-002] | MIL | [BC-001], [PP-001], [MIL-001] | [MIL-003], [MIL-005] | [RC-005], [RC-027] | | [MIL-003] | MIL | [BC-001], [PP-001], [MIL-002] | [MIL-004] | [RC-006], [RC-028], [RC-033], [RC-039] | | [MIL-004] | MIL | [BC-001], [PP-001], [MIL-003] | - | [RC-007], [RC-029], [RC-034], [RC-048] | -| [ADR-0010] | ADR | [MIL-010], [UC-008], [US-001], [ADR-0005], [ADR-0006] | - | [RC-072] | +| [ADR-0010] | ADR | [MIL-010], [UC-008], [US-001], [ADR-0005], [ADR-0006] | - | [RC-072], [RC-073] | | [RC-001] | RC | [BC-001] | - | - | | [RC-002] | RC | [PP-001] | - | - | | [RC-003] | RC | [MIL-001] | - | - | @@ -129,6 +129,7 @@ updated whenever an artifact instance is created or reviewed. | [RC-070] | RC | [US-001] | - | - | | [RC-071] | RC | [UCD-001] | - | - | | [RC-072] | RC | [ADR-0010] | - | - | +| [RC-073] | RC | [ADR-0010] | - | - | ## Coverage Notes @@ -235,6 +236,7 @@ updated whenever an artifact instance is created or reviewed. [RC-070]: ./reviews/rc-070-us-001-metrics.md [RC-071]: ./reviews/rc-071-ucd-001-metrics.md [RC-072]: ./reviews/rc-072-adr-0010.md +[RC-073]: ./reviews/rc-073-adr-0010-artifact.md [ADR-0008]: ../adr/adr-0008-cpp-csharp-quality-check-contract.md [ADR-0009]: ../adr/adr-0009-wiki-publishing-reusable-workflow.md [ADR-0007]: ../adr/adr-0007-release-publish-destination.md diff --git a/tests/test_code_metrics.py b/tests/test_code_metrics.py index e9d8631..10ed637 100644 --- a/tests/test_code_metrics.py +++ b/tests/test_code_metrics.py @@ -40,6 +40,7 @@ class TestDefinition: "working-directory": ".", "exclude": "", "coverage-report": "", + "coverage-artifact": "", "coverage-warn": "60", "coverage-good": "80", "badges-branch": "badges", @@ -59,6 +60,7 @@ class TestDefinition: "WORKING_DIRECTORY", "EXCLUDE", "COVERAGE_REPORT", + "COVERAGE_ARTIFACT", "COVERAGE_WARN", "COVERAGE_GOOD", "BADGES_BRANCH", @@ -139,6 +141,7 @@ def run(command: str, repo: Path, tmp_path: Path, python3_bin: Path, **env: str) "WORKING_DIRECTORY": ".", "EXCLUDE": "", "COVERAGE_REPORT": "", + "COVERAGE_ARTIFACT": "", "COVERAGE_WARN": "60", "COVERAGE_GOOD": "80", "BADGES_BRANCH": "badges", @@ -187,6 +190,7 @@ class TestValidate: ("WORKING_DIRECTORY", "../x", "working-directory must be a relative path"), ("WORKING_DIRECTORY", "nope", "working-directory 'nope' does not exist"), ("COVERAGE_REPORT", "/tmp/c.xml", "coverage-report must be a relative path"), + ("COVERAGE_ARTIFACT", "cov", "coverage-artifact 'cov' needs coverage-report"), ("BADGES_BRANCH", "main", "must not be a default branch"), ("BADGES_BRANCH", "master", "must not be a default branch"), ("BADGES_BRANCH", "", "must not be a default branch"), @@ -255,6 +259,30 @@ class TestPublish: assert result.returncode == 0, result.stdout assert "coverage.svg" in branch_files(remote) + def test_a_report_from_an_artifact_is_read_below_the_artifact_directory( + self, repo, remote, tmp_path, python3_bin + ): + artifact = repo / "coverage-artifact" + artifact.mkdir() + (artifact / "cov.xml").write_text(COVERAGE, newline="\n") + result = measure_and_publish( + repo, tmp_path, python3_bin, COVERAGE_ARTIFACT="coverage", COVERAGE_REPORT="cov.xml" + ) + assert result.returncode == 0, result.stdout + assert "coverage.svg" in branch_files(remote) + + def test_a_report_missing_from_the_artifact_names_the_path(self, repo, tmp_path, python3_bin): + result = run( + "measure", + repo, + tmp_path, + python3_bin, + COVERAGE_ARTIFACT="coverage", + COVERAGE_REPORT="cov.xml", + ) + assert result.returncode == 1 + assert "coverage-report 'coverage-artifact/cov.xml' does not exist" in result.stdout + def test_a_missing_report_fails_names_the_file_and_changes_nothing( self, repo, remote, tmp_path, python3_bin ): diff --git a/tools/badges.py b/tools/badges.py index 047b028..c85d97a 100644 --- a/tools/badges.py +++ b/tools/badges.py @@ -35,21 +35,21 @@ class ConfigurationError(Exception): def read_coverage(path: Path) -> int: """Return the line coverage of a Cobertura XML report as a whole percent, half up.""" if not path.is_file(): - raise ConfigurationError(f"coverage-report '{path}' does not exist.") + raise ConfigurationError(f"coverage-report '{path.as_posix()}' does not exist.") data = path.read_bytes() if b" int: covered_lines, valid_lines = int(covered), int(valid) if valid_lines <= 0: raise ConfigurationError( - f"coverage-report '{path}' has no valid lines." + f"coverage-report '{path.as_posix()}' has no valid lines." ) if not 0 <= covered_lines <= valid_lines: raise ConfigurationError( - f"coverage-report '{path}' has lines-covered outside 0..lines-valid." + f"coverage-report '{path.as_posix()}' has lines-covered outside 0..lines-valid." ) fraction = Decimal(covered_lines) / Decimal(valid_lines) else: rate = root.get("line-rate") if rate is None: raise ConfigurationError( - f"coverage-report '{path}' has neither lines-valid and lines-covered nor line-rate." + f"coverage-report '{path.as_posix()}' has neither lines-valid and lines-covered nor line-rate." ) fraction = Decimal(rate) if not Decimal(0) <= fraction <= Decimal(1): raise ConfigurationError( - f"coverage-report '{path}' has a line-rate outside 0..1." + f"coverage-report '{path.as_posix()}' has a line-rate outside 0..1." ) except (ValueError, InvalidOperation) as error: raise ConfigurationError( - f"coverage-report '{path}' has a value that is not a number." + f"coverage-report '{path.as_posix()}' has a value that is not a number." ) from error return int((fraction * 100).quantize(Decimal(1), rounding=ROUND_HALF_UP)) diff --git a/tools/code-metrics.sh b/tools/code-metrics.sh index 44c3d04..27dd832 100644 --- a/tools/code-metrics.sh +++ b/tools/code-metrics.sh @@ -10,8 +10,8 @@ # and nothing when none of them changed # # Settings come from the environment, never from interpolated expressions: -# WORKING_DIRECTORY, EXCLUDE, COVERAGE_REPORT, COVERAGE_WARN, COVERAGE_GOOD, -# BADGES_BRANCH, BADGES_OUT (the directory of the badge files; a temporary +# WORKING_DIRECTORY, EXCLUDE, COVERAGE_REPORT, COVERAGE_ARTIFACT, +# COVERAGE_WARN, COVERAGE_GOOD, BADGES_BRANCH, BADGES_OUT (the directory of the badge files; a temporary # directory under RUNNER_TEMP by default). # Every failure is printed as ::error::[category] message. @@ -41,6 +41,9 @@ validate_inputs() { relative_path "$COVERAGE_REPORT" || fail "coverage-report must be a relative path inside the repository." fi + if [ -n "$COVERAGE_ARTIFACT" ] && [ -z "$COVERAGE_REPORT" ]; then + fail "coverage-artifact '$COVERAGE_ARTIFACT' needs coverage-report, the file inside the artifact." + fi case "$BADGES_BRANCH" in main | master | HEAD | "") fail "badges-branch '$BADGES_BRANCH' must not be a default branch." ;; esac @@ -55,9 +58,13 @@ measure() { python3 "$TOOLS_DIR/count_loc.py" \ --directory "$WORKING_DIRECTORY" --exclude "$EXCLUDE" --output "$metrics" rm -rf "$OUT_DIR" + report="$COVERAGE_REPORT" + if [ -n "$COVERAGE_ARTIFACT" ]; then + report="coverage-artifact/$COVERAGE_REPORT" + fi python3 "$TOOLS_DIR/badges.py" \ --metrics "$metrics" --output-dir "$OUT_DIR" \ - --coverage-report "$COVERAGE_REPORT" \ + --coverage-report "$report" \ --coverage-warn "$COVERAGE_WARN" --coverage-good "$COVERAGE_GOOD" rm -f "$metrics" } -- 2.54.0 From 95f9577a14fc44a2c22f76cc49d93e69cfcb60eb Mon Sep 17 00:00:00 2001 From: Jens Tirsvad Nielsen Date: Wed, 14 Oct 2026 13:56:49 +0800 Subject: [PATCH 07/10] MIL-010 task 7: guide, README, wiki pages in three languages Task: MIL-010#7 Refs #161 --- README.md | 50 +++++++++++++- docs/wiki/Capabilities.da.md | 3 +- docs/wiki/Capabilities.md | 1 + docs/wiki/Capabilities.ms.md | 3 +- docs/wiki/CodeMetrics.da.md | 92 ++++++++++++++++++++++++++ docs/wiki/CodeMetrics.md | 123 +++++++++++++++++++++++++++++++++++ docs/wiki/CodeMetrics.ms.md | 92 ++++++++++++++++++++++++++ docs/wiki/Home.da.md | 51 ++++++++++++++- docs/wiki/Home.md | 49 +++++++++++++- docs/wiki/Home.ms.md | 51 ++++++++++++++- docs/wiki/Upgrading.da.md | 4 +- docs/wiki/Upgrading.md | 4 +- docs/wiki/Upgrading.ms.md | 4 +- docs/wiki/_Sidebar.md | 3 + guides/code-metrics.md | 117 +++++++++++++++++++++++++++++++++ guides/upgrading.md | 4 +- tests/test_build_wiki.py | 1 + tools/build_wiki.py | 8 ++- 18 files changed, 637 insertions(+), 23 deletions(-) create mode 100644 docs/wiki/CodeMetrics.da.md create mode 100644 docs/wiki/CodeMetrics.md create mode 100644 docs/wiki/CodeMetrics.ms.md create mode 100644 guides/code-metrics.md diff --git a/README.md b/README.md index 6c5cf0a..2c12849 100644 --- a/README.md +++ b/README.md @@ -17,11 +17,11 @@ A collection of reusable Gitea Actions workflows for DevOps professionals and so This repository is the central collection of reusable automation for TirSystem repositories. The primary repository lives at `git.tirsystem.com` (Gitea) and is push-mirrored to GitHub. -It contains nine workflows: metadata sync to the GitHub mirror, wiki publishing (called by each repository), shared quality checks for Python and shell, for C and C++ and for C#, a release workflow, the publishing of accepted milestones and the repository's own validation. They are listed with their purpose and a diagram under [Workflows](#-workflows). +It contains ten workflows: metadata sync to the GitHub mirror, wiki publishing (called by each repository), shared quality checks for Python and shell, for C and C++ and for C#, code metrics and coverage badges, a release workflow, the publishing of accepted milestones and the repository's own validation. They are listed with their purpose and a diagram under [Workflows](#-workflows). ## 🔧 Workflows -Nine workflows live in this repository. A **scoped** workflow is delivered by the instance to every repository of the TirSystem organizations and needs nothing in the repository; a **reusable** workflow is called by a repository with `uses:`; the others run only here. The diagrams are PlantUML source. +Ten workflows live in this repository. A **scoped** workflow is delivered by the instance to every repository of the TirSystem organizations and needs nothing in the repository; a **reusable** workflow is called by a repository with `uses:`; the others run only here. The diagrams are PlantUML source. | Workflow | File | Kind | Starts on | Purpose | | --- | --- | --- | --- | --- | @@ -30,6 +30,7 @@ Nine workflows live in this repository. A **scoped** workflow is delivered by th | Quality checks | [`quality-checks.yml`](.gitea/workflows/quality-checks.yml) | Reusable (`workflow_call`) | a call from another repository | Runs ruff, mypy, shellcheck and shfmt on the tracked files and changes nothing | | Quality checks C++ | [`quality-checks-cpp.yml`](.gitea/workflows/quality-checks-cpp.yml) | Reusable (`workflow_call`) | a call from another repository | Runs clang-format and clang-tidy at exact versions on the tracked C and C++ files and changes nothing | | Quality checks C# | [`quality-checks-csharp.yml`](.gitea/workflows/quality-checks-csharp.yml) | Reusable (`workflow_call`) | a call from another repository | Runs `dotnet format` and `dotnet build` on an exact .NET SDK 10 or later and changes nothing | +| Code metrics | [`code-metrics.yml`](.gitea/workflows/code-metrics.yml) | Reusable (`workflow_call`) | a call from another repository | Counts the code files and the lines of code (without comment and blank lines), reads a Cobertura coverage report and commits three SVG badges to a `badges` branch | | Publish own wiki | [`publish-own-wiki.yml`](.gitea/workflows/publish-own-wiki.yml) | This repository only | push to `main` that changes `docs/wiki/**`, manual | Calls the reusable Publish wiki pages workflow for the wiki of this repository | | Release | [`release.yml`](.gitea/workflows/release.yml) | This repository only | manual | Creates the version tag, the release page and the major tag after the checks | | Publish accepted milestones | [`sync-milestones.yml`](.gitea/workflows/sync-milestones.yml) | This repository only | push to `main` that changes `docs/milestones/**` or `docs/sqa/reviews/**`, manual (dry run by default) | Creates or updates the Gitea milestone and issues of every milestone that is Accepted with a Go review | @@ -237,6 +238,50 @@ deactivate Workflow @enduml ``` +### 🏅 Code metrics + +A reusable workflow that counts the code files and the lines of code (without comment and blank lines) of a repository and publishes them, with the test coverage, as three SVG badges on a `badges` branch, called with `uses: TirSystem/github-action/.gitea/workflows/code-metrics.yml@v1` (ADR-0010). It needs `contents: write` and runs no test: an earlier job of the caller produces a Cobertura XML coverage report and uploads it as an artifact ([guide](guides/code-metrics.md)). + +#### 🔀 Workflow + +```plantuml +@startuml +title Code metrics and coverage badges (reusable workflow) + +autonumber + +actor "Calling repository\nuses: ...code-metrics.yml@v1" as Caller +participant "Gitea Actions\ncode-metrics" as Workflow +participant "Branch badges\nof the caller" as Branch + +Caller -> Workflow: workflow_call with working-directory, exclude,\ncoverage-report and thresholds +activate Workflow + +Workflow -> Workflow: Check out the caller and the shared scripts +Workflow -> Workflow: Validate the inputs +alt an input is invalid + Workflow --> Caller: Fail with [configuration] +end + +Workflow -> Workflow: Download the coverage artifact (when given) +Workflow -> Workflow: Count code files and lines of code +alt a coverage report is given but missing or malformed + Workflow --> Caller: Fail with [configuration] that names the file +else + Workflow -> Workflow: Write code-files.svg, code-lines.svg\nand coverage.svg +end + +Workflow -> Branch: Compare with the files of the branch +alt no badge changed + Workflow --> Caller: Notice: nothing to commit, pass +else + Workflow -> Branch: One commit with [skip ci] + Workflow --> Caller: Badges published +end +deactivate Workflow +@enduml +``` + ### 🏷️ Release Started by hand by a maintainer; with `dry_run` `true` (the default) it only checks (see the [release process](guides/release-process.md)). @@ -500,6 +545,7 @@ Rows that say Implemented or Guides written exist today. The rest is planned and | Publish a repository wiki from `docs/wiki/` through a called workflow | Implemented as the reusable workflow `.gitea/workflows/publish-wiki-pages.yml`, with offline tests; called by every repository that publishes a wiki, and this repository's own wiki is in `docs/wiki/` (MIL-008, MIL-009) | | Shared quality checks (Python and shell) | Implemented as the reusable workflow `.gitea/workflows/quality-checks.yml`, with offline tests and a trial in a second organization; adopted in RepoFoundry ([guide](guides/quality-checks.md)) | | Shared quality checks (C and C++, C#) | Implemented as the reusable workflows `.gitea/workflows/quality-checks-cpp.yml` and `.gitea/workflows/quality-checks-csharp.yml` with exact tool versions and offline tests; not yet used in another repository (MIL-007) ([C and C++ guide](guides/quality-checks-cpp.md), [C# guide](guides/quality-checks-csharp.md)) | +| Code metrics and coverage badges | Implemented as the reusable workflow `.gitea/workflows/code-metrics.yml` with a counter, a Cobertura reader, an SVG renderer and offline tests; not yet used in another repository (MIL-010) ([guide](guides/code-metrics.md)) | | Versioned release and publishing | Release process, changelog and upgrade guide defined; the release workflow exists and `v1.0.0` is released (MIL-004) ([guide](guides/upgrading.md)) | ## 📄 License diff --git a/docs/wiki/Capabilities.da.md b/docs/wiki/Capabilities.da.md index affa1e6..0e2f374 100644 --- a/docs/wiki/Capabilities.da.md +++ b/docs/wiki/Capabilities.da.md @@ -1,4 +1,4 @@ - + > Dette er en oversættelse af [`Capabilities`](Capabilities). Den engelske side er autoritativ, og ændringer her bliver overskrevet, næste gang wikien publiceres. @@ -16,4 +16,5 @@ Rækker, der siger Implementeret eller Vejledninger er skrevet, findes i dag. Re | Publicering af et repositorys wiki fra `docs/wiki/` gennem et kaldt workflow | Implementeret som det genbrugelige workflow `.gitea/workflows/publish-wiki-pages.yml`, med offline-tests; kaldt af hvert repository, der publicerer en wiki, og dette repositorys egen wiki er i `docs/wiki/` (MIL-008, MIL-009) | | Fælles kvalitetskontrol (Python og shell) | Implementeret som det genbrugelige workflow `.gitea/workflows/quality-checks.yml`, med offline-tests og et forsøg i en anden organisation; taget i brug i RepoFoundry ([vejledning](QualityChecksPythonShell.da)) | | Fælles kvalitetskontrol (C og C++, C#) | Implementeret som de genbrugelige workflows `.gitea/workflows/quality-checks-cpp.yml` og `.gitea/workflows/quality-checks-csharp.yml` med præcise værktøjsversioner og offline-tests; endnu ikke brugt i et andet repository (MIL-007) ([vejledning til C og C++](QualityChecksCpp.da), [vejledning til C#](QualityChecksCSharp.da)) | +| Kodemålinger og dækningsbadges | Implementeret som det genbrugelige workflow `.gitea/workflows/code-metrics.yml` med en tæller, en Cobertura-læser, en SVG-renderer og offline-tests; endnu ikke brugt i et andet repository (MIL-010) ([vejledning](CodeMetrics.da)) | | Versioneret udgivelse og publicering | Udgivelsesproces, changelog og opgraderingsvejledning er defineret; udgivelsesworkflowet findes, og `v1.0.0` er udgivet (MIL-004) ([vejledning](Upgrading.da)) | diff --git a/docs/wiki/Capabilities.md b/docs/wiki/Capabilities.md index 78262e6..604823b 100644 --- a/docs/wiki/Capabilities.md +++ b/docs/wiki/Capabilities.md @@ -16,4 +16,5 @@ Rows that say Implemented or Guides written exist today. The rest is planned and | Publish a repository wiki from `docs/wiki/` through a called workflow | Implemented as the reusable workflow `.gitea/workflows/publish-wiki-pages.yml`, with offline tests; called by every repository that publishes a wiki, and this repository's own wiki is in `docs/wiki/` (MIL-008, MIL-009) | | Shared quality checks (Python and shell) | Implemented as the reusable workflow `.gitea/workflows/quality-checks.yml`, with offline tests and a trial in a second organization; adopted in RepoFoundry ([guide](QualityChecksPythonShell)) | | Shared quality checks (C and C++, C#) | Implemented as the reusable workflows `.gitea/workflows/quality-checks-cpp.yml` and `.gitea/workflows/quality-checks-csharp.yml` with exact tool versions and offline tests; not yet used in another repository (MIL-007) ([C and C++ guide](QualityChecksCpp), [C# guide](QualityChecksCSharp)) | +| Code metrics and coverage badges | Implemented as the reusable workflow `.gitea/workflows/code-metrics.yml` with a counter, a Cobertura reader, an SVG renderer and offline tests; not yet used in another repository (MIL-010) ([guide](CodeMetrics)) | | Versioned release and publishing | Release process, changelog and upgrade guide defined; the release workflow exists and `v1.0.0` is released (MIL-004) ([guide](Upgrading)) | diff --git a/docs/wiki/Capabilities.ms.md b/docs/wiki/Capabilities.ms.md index 57e84f3..1eefede 100644 --- a/docs/wiki/Capabilities.ms.md +++ b/docs/wiki/Capabilities.ms.md @@ -1,4 +1,4 @@ - + > Ini ialah terjemahan [`Capabilities`](Capabilities). Halaman bahasa Inggeris ialah teks yang sah, dan perubahan di sini akan ditimpa pada penerbitan wiki yang seterusnya. @@ -16,4 +16,5 @@ Baris yang menyatakan Dilaksanakan atau Panduan telah ditulis wujud hari ini. Se | Menerbitkan wiki repositori daripada `docs/wiki/` melalui aliran kerja yang dipanggil | Dilaksanakan sebagai aliran kerja boleh guna semula `.gitea/workflows/publish-wiki-pages.yml`, dengan ujian luar talian; dipanggil oleh setiap repositori yang menerbitkan wiki, dan wiki repositori ini sendiri ada dalam `docs/wiki/` (MIL-008, MIL-009) | | Semakan kualiti bersama (Python dan shell) | Dilaksanakan sebagai aliran kerja boleh guna semula `.gitea/workflows/quality-checks.yml`, dengan ujian luar talian dan percubaan dalam organisasi kedua; digunakan dalam RepoFoundry ([panduan](QualityChecksPythonShell.ms)) | | Semakan kualiti bersama (C dan C++, C#) | Dilaksanakan sebagai aliran kerja boleh guna semula `.gitea/workflows/quality-checks-cpp.yml` dan `.gitea/workflows/quality-checks-csharp.yml` dengan versi alat yang tepat dan ujian luar talian; belum digunakan dalam repositori lain (MIL-007) ([panduan C dan C++](QualityChecksCpp.ms), [panduan C#](QualityChecksCSharp.ms)) | +| Metrik kod dan lencana liputan | Dilaksanakan sebagai aliran kerja boleh guna semula `.gitea/workflows/code-metrics.yml` dengan pengira, pembaca Cobertura, pemaparan SVG dan ujian luar talian; belum digunakan dalam repositori lain (MIL-010) ([panduan](CodeMetrics.ms)) | | Keluaran berversi dan penerbitan | Proses keluaran, changelog dan panduan naik taraf telah ditakrifkan; aliran kerja keluaran sudah ada dan `v1.0.0` telah dikeluarkan (MIL-004) ([panduan](Upgrading.ms)) | diff --git a/docs/wiki/CodeMetrics.da.md b/docs/wiki/CodeMetrics.da.md new file mode 100644 index 0000000..28f54b4 --- /dev/null +++ b/docs/wiki/CodeMetrics.da.md @@ -0,0 +1,92 @@ + + +> Dette er en oversættelse af [`CodeMetrics`](CodeMetrics). Den engelske side er autoritativ, og ændringer her bliver overskrevet, næste gang wikien publiceres. + +[English](CodeMetrics) | Dansk | [Bahasa Melayu](CodeMetrics.ms) + +# Visning af kodemålinger og dækningsbadges + +Sådan kalder en softwareudvikler det fælles kodemålings-workflow og viser dets tre badges (kodefiler, kodelinjer og dækning) i en README. Workflowet tæller repositoryets kode, læser en dækningsrapport, som kalderen leverer, og committer tre SVG-filer til en `badges`-gren. Det kører ingen test og kræver ingen secret. Dets kontrakt er ADR-0010 i dette repository. + +## Kald af workflowet + +Workflowet kan ikke se filerne fra dit testjob, så testene kører i et job, du selv ejer, uploader Cobertura XML-rapporten som artefakt, og målingsjobbet kører bagefter: + +```yaml +name: Code metrics + +on: + push: + branches: [ main ] + +jobs: + tests: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4.2.2 + - run: python3 -m pip install pytest pytest-cov && python3 -m pytest --cov --cov-report=xml + - uses: actions/upload-artifact@v4.6.2 + with: + name: coverage + path: coverage.xml + + metrics: + needs: tests + permissions: + contents: write + uses: TirSystem/github-action/.gitea/workflows/code-metrics.yml@v1 + with: + coverage-artifact: coverage + coverage-report: coverage.xml +``` + +| Input | Standard | Betydning | +| --- | --- | --- | +| `working-directory` | `.` | Mappe, relativt til repositoryets rod, hvis kode tælles | +| `exclude` | tom | Kommasepareret liste af mønstre for filer og mapper, der ikke tælles, ud over standardudeladelserne; et mønster uden `/` matcher et navn overalt, et mønster med `/` matcher stien under `working-directory` | +| `coverage-report` | tom | Cobertura XML-rapport: filen i artefaktet, når `coverage-artifact` er sat, ellers en sti i repositoryet; tom giver intet dækningsbadge | +| `coverage-artifact` | tom | Navn på et artefakt, uploadet af et tidligere job i samme kørsel, der indeholder rapporten | +| `coverage-warn` | `60` | Dækning i procent, under hvilken badget er rødt | +| `coverage-good` | `80` | Dækning i procent, fra hvilken badget er grønt; derimellem er det gult | +| `badges-branch` | `badges` | Gren, der indeholder badge-filerne; den oprettes, hvis den ikke findes | +| `tools-ref` | `v1` | Tag eller gren i `TirSystem/github-action`, hvorfra scripts i `tools/` hentes | + +Reference workflowet ved major-tagget (`@v1`) eller ved et præcist tag, aldrig ved `main`. Et kald ved et præcist tag er kun fuldt fastlåst, når `tools-ref` navngiver samme tag. Se opgraderingsvejledningen. Start workflowet kun ved push til din standardgren: badge-committet har `[skip ci]` og laves med kørslens token, som ikke starter en ny kørsel, men en trigger på alle grene ville skrive ét commit pr. gren. + +## Hvad der tælles + +- En kodefil er en tekstfil med filendelsen for et understøttet sprog: Python, shell, PowerShell, C, C++, C#, Java, JavaScript, TypeScript, Go, Rust og SQL. Markdown, YAML, JSON, XML, HTML og CSS er ikke kodefiler. +- Hver linje er præcis én af: tom (kun blanktegn), kommentar (kun kommentartekst) eller kode (alt andet, også en linje med kode og en afsluttende kommentar). Badget viser kun kodelinjerne. +- En kommentarmarkør i en streng er ikke en kommentar, og en Python-docstring er en streng og tæller derfor som kode. +- Mapper, der starter med punktum, og `node_modules`, `vendor`, `third_party`, `dist`, `build`, `out`, `bin`, `obj`, `target`, `venv`, `__pycache__` og `shared-scripts` tælles ikke. Genereret kode udelades med `exclude`. +- Rå og ordrette strenge, der afviger fra en almindelig streng (Rust `r#"..."#`, C# `@"..."`, C++-rå strenge), kan tælles forkert. Tallene afviger lidt fra andre værktøjer; reglerne er de samme ved hver kørsel. + +## Dækningsbadget + +Rapporten er en Cobertura XML-fil, som `coverage.py` (`coverage xml`), `gcovr` (`--cobertura`) og `coverlet` (formatet `cobertura`) skriver. Badget viser `lines-covered` over `lines-valid`, afrundet til hele procent, og farven følger de to grænser. En rapport, der ikke findes, ikke er velformet Cobertura XML eller ikke har gyldige linjer, får kørslen til at fejle med filen nævnt; dækningsbadget skrives aldrig som `0%` for en dårlig rapport. Uden `coverage-report` består kørslen med en note og uden dækningsbadge. + +## Visning af badges + +Tilføj billederne til README med grenens rå URL'er: + +```markdown +![code files](https://git.tirsystem.com/OWNER/REPO/raw/branch/badges/code-files.svg) +![lines of code](https://git.tirsystem.com/OWNER/REPO/raw/branch/badges/code-lines.svg) +![coverage](https://git.tirsystem.com/OWNER/REPO/raw/branch/badges/coverage.svg) +``` + +På GitHub-spejlet bruges `https://raw.githubusercontent.com/OWNER/REPO/badges/code-files.svg`; push-spejlet kopierer grenen. `badges`-grenen indeholder også `code-metrics.json` med tallene pr. sprog. Instansen cacher en rå fil i op til seks timer, så et ændret badge kan vise det gamle tal et stykke tid dér; spejlet cacher i fem minutter. Et privat repository viser ikke badges for en læser, der ikke er logget ind. + +## Når workflowet fejler + +| Meddelelse | Årsag | Løsning | +| --- | --- | --- | +| `[configuration] working-directory must be a relative path inside the repository.` | En absolut sti eller `..` | Brug en sti inde i repositoryet | +| `[configuration] coverage-report '' does not exist.` | Rapporten blev ikke frembragt, eller artefaktnavnet eller stien er forkert | Tjek testjobbet og artefaktnavnet; stien er inde i artefaktet | +| `[configuration] coverage-report '' is not well-formed XML` eller `is not a Cobertura report` | Filen har et andet format | Skriv Cobertura XML (`coverage xml`, `gcovr --cobertura`) | +| `[configuration] coverage-report '' has no valid lines.` | Testene kørte ingen målt kode | Tjek dækningskonfigurationen | +| `[configuration] coverage-artifact '' needs coverage-report` | Artefaktet er navngivet, men ikke filen i det | Sæt `coverage-report` | +| `[configuration] coverage-warn ... is above coverage-good` eller `is not a number from 0 to 100` | En grænse er ugyldig | Ret inputtet | +| `[configuration] badges-branch '' must not be a default branch` eller `holds files that are not badge files` | Grenen hedder `main`, `master` eller har andet indhold | Brug en gren, der kun indeholder badges | +| `the push to '' failed twice` | En anden kørsel skrev grenen samtidig, eller tokenet kan ikke skrive | Kør igen; tjek at jobbet har `contents: write` | +| Download-trinnet fejler | Artefaktet findes ikke i denne kørsel, eller jobbet kører ikke efter testjobbet | Tilføj `needs:` og tjek artefaktnavnet | diff --git a/docs/wiki/CodeMetrics.md b/docs/wiki/CodeMetrics.md new file mode 100644 index 0000000..9821298 --- /dev/null +++ b/docs/wiki/CodeMetrics.md @@ -0,0 +1,123 @@ + + +> This page is generated from [`guides/code-metrics.md`](https://git.tirsystem.com/TirSystem/github-action/src/branch/main/guides/code-metrics.md). Changes made here are overwritten the next time the wiki is published. + +English | [Dansk](CodeMetrics.da) | [Bahasa Melayu](CodeMetrics.ms) + +# Showing code metrics and coverage badges + +How a software engineer calls the shared code-metrics workflow and shows its +three badges (code files, lines of code and coverage) in a README. The workflow +counts the code of the repository, reads a coverage report that the caller +supplies and commits three SVG files to a `badges` branch. It runs no test and +needs no secret. Its contract is ADR-0010 in this repository. + +## Calling the workflow + +The workflow cannot see the files of your test job, so the tests run in a job of +your own, upload the Cobertura XML report as an artifact, and the metrics job +runs after it: + +```yaml +name: Code metrics + +on: + push: + branches: [ main ] + +jobs: + tests: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4.2.2 + - run: python3 -m pip install pytest pytest-cov && python3 -m pytest --cov --cov-report=xml + - uses: actions/upload-artifact@v4.6.2 + with: + name: coverage + path: coverage.xml + + metrics: + needs: tests + permissions: + contents: write + uses: TirSystem/github-action/.gitea/workflows/code-metrics.yml@v1 + with: + coverage-artifact: coverage + coverage-report: coverage.xml +``` + +| Input | Default | Meaning | +| --- | --- | --- | +| `working-directory` | `.` | Directory, relative to the repository root, whose code is counted | +| `exclude` | empty | Comma list of patterns of files and directories that are not counted, in addition to the defaults; a pattern without `/` matches a name anywhere, a pattern with `/` matches the path below `working-directory` | +| `coverage-report` | empty | Cobertura XML report: the file inside the artifact when `coverage-artifact` is set, otherwise a path in the repository; empty writes no coverage badge | +| `coverage-artifact` | empty | Name of an artifact, uploaded by an earlier job of the same run, that holds the report | +| `coverage-warn` | `60` | Coverage in percent below which the badge is red | +| `coverage-good` | `80` | Coverage in percent from which the badge is green; in between it is yellow | +| `badges-branch` | `badges` | Branch that holds the badge files; it is created when it does not exist | +| `tools-ref` | `v1` | Tag or branch of `TirSystem/github-action` from which the scripts in `tools/` are fetched | + +Reference the workflow at the major tag (`@v1`) or at an exact tag, never at +`main`. A call at an exact tag is pinned completely only when `tools-ref` names +the same tag. See the upgrade guide. Start the workflow on pushes to your +default branch only: the badge commit carries `[skip ci]` and is made with the +token of the run, which does not start another run, but a trigger on every +branch would write one commit per branch. + +## What is counted + +- A code file is a text file with the extension of a supported language: + Python, shell, PowerShell, C, C++, C#, Java, JavaScript, TypeScript, Go, Rust + and SQL. Markdown, YAML, JSON, XML, HTML and CSS are not code files. +- Every line is exactly one of blank (only white space), comment (only comment + text) or code (everything else, including a line with code and a trailing + comment). The badge shows the lines of code only. +- A comment marker inside a string is not a comment, and a Python docstring is a + string, so it counts as code. +- Directories that start with a dot and `node_modules`, `vendor`, `third_party`, + `dist`, `build`, `out`, `bin`, `obj`, `target`, `venv`, `__pycache__` and + `shared-scripts` are not counted. Generated code is excluded with `exclude`. +- Raw and verbatim strings that differ from an ordinary string (Rust `r#"..."#`, + C# `@"..."`, C++ raw strings) can be counted wrongly. The counts will differ + a little from other tools; the rules are the same on every run. + +## The coverage badge + +The report is a Cobertura XML file, which `coverage.py` (`coverage xml`), +`gcovr` (`--cobertura`) and `coverlet` (`cobertura` format) write. The badge +shows `lines-covered` over `lines-valid`, rounded to a whole percent, and its +colour follows the two thresholds. A report that does not exist, is not +well-formed Cobertura XML or has no valid lines fails the run with the file +named; the coverage badge is never written as `0%` for a bad report. Without +`coverage-report` the run passes with a notice and no coverage badge. + +## Showing the badges + +Add the images to the README with the raw URLs of the branch: + +```markdown +![code files](https://git.tirsystem.com/OWNER/REPO/raw/branch/badges/code-files.svg) +![lines of code](https://git.tirsystem.com/OWNER/REPO/raw/branch/badges/code-lines.svg) +![coverage](https://git.tirsystem.com/OWNER/REPO/raw/branch/badges/coverage.svg) +``` + +On the GitHub mirror use `https://raw.githubusercontent.com/OWNER/REPO/badges/code-files.svg`; +the push mirror copies the branch. The `badges` branch also holds +`code-metrics.json` with the numbers per language. The instance caches a raw +file for up to six hours, so a changed badge can show the old number for a +while there; the mirror caches for five minutes. A private repository does not +show the badges to a reader who is not signed in. + +## When the workflow fails + +| Message | Cause | Fix | +| --- | --- | --- | +| `[configuration] working-directory must be a relative path inside the repository.` | An absolute path or `..` | Use a path inside the repository | +| `[configuration] coverage-report '' does not exist.` | The report was not produced, or the artifact name or path is wrong | Check the test job and the artifact name; the path is inside the artifact | +| `[configuration] coverage-report '' is not well-formed XML` or `is not a Cobertura report` | The file is another format | Write Cobertura XML (`coverage xml`, `gcovr --cobertura`) | +| `[configuration] coverage-report '' has no valid lines.` | The tests ran no measured code | Check the coverage configuration | +| `[configuration] coverage-artifact '' needs coverage-report` | The artifact is named but not the file in it | Set `coverage-report` | +| `[configuration] coverage-warn ... is above coverage-good` or `is not a number from 0 to 100` | A threshold is invalid | Correct the input | +| `[configuration] badges-branch '' must not be a default branch` or `holds files that are not badge files` | The branch name is `main`, `master` or a branch with other content | Use a branch that holds only the badges | +| `the push to '' failed twice` | Another run wrote the branch at the same time, or the token cannot write | Re-run; check that the job has `contents: write` | +| The download step fails | The artifact does not exist in this run, or the job does not run after the test job | Add `needs:` and check the artifact name | diff --git a/docs/wiki/CodeMetrics.ms.md b/docs/wiki/CodeMetrics.ms.md new file mode 100644 index 0000000..0fa5e1b --- /dev/null +++ b/docs/wiki/CodeMetrics.ms.md @@ -0,0 +1,92 @@ + + +> Ini ialah terjemahan [`CodeMetrics`](CodeMetrics). Halaman bahasa Inggeris ialah teks yang sah, dan perubahan di sini akan ditimpa pada penerbitan wiki yang seterusnya. + +[English](CodeMetrics) | [Dansk](CodeMetrics.da) | Bahasa Melayu + +# Menunjukkan metrik kod dan lencana liputan + +Cara seorang jurutera perisian memanggil aliran kerja metrik kod bersama dan menunjukkan tiga lencananya (fail kod, baris kod dan liputan) dalam README. Aliran kerja ini mengira kod repositori, membaca laporan liputan yang dibekalkan oleh pemanggil dan melakukan commit tiga fail SVG ke cabang `badges`. Ia tidak menjalankan sebarang ujian dan tidak memerlukan secret. Kontraknya ialah ADR-0010 dalam repositori ini. + +## Memanggil aliran kerja + +Aliran kerja tidak dapat melihat fail daripada kerja ujian anda, jadi ujian dijalankan dalam kerja anda sendiri, memuat naik laporan Cobertura XML sebagai artifak, dan kerja metrik dijalankan selepasnya: + +```yaml +name: Code metrics + +on: + push: + branches: [ main ] + +jobs: + tests: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4.2.2 + - run: python3 -m pip install pytest pytest-cov && python3 -m pytest --cov --cov-report=xml + - uses: actions/upload-artifact@v4.6.2 + with: + name: coverage + path: coverage.xml + + metrics: + needs: tests + permissions: + contents: write + uses: TirSystem/github-action/.gitea/workflows/code-metrics.yml@v1 + with: + coverage-artifact: coverage + coverage-report: coverage.xml +``` + +| Input | Lalai | Maksud | +| --- | --- | --- | +| `working-directory` | `.` | Direktori, relatif kepada akar repositori, yang kodnya dikira | +| `exclude` | kosong | Senarai corak fail dan direktori yang tidak dikira, dipisahkan koma, sebagai tambahan kepada pengecualian lalai; corak tanpa `/` sepadan dengan nama di mana-mana, corak dengan `/` sepadan dengan laluan di bawah `working-directory` | +| `coverage-report` | kosong | Laporan Cobertura XML: fail dalam artifak apabila `coverage-artifact` ditetapkan, jika tidak laluan dalam repositori; kosong bermakna tiada lencana liputan | +| `coverage-artifact` | kosong | Nama artifak, dimuat naik oleh kerja yang lebih awal dalam larian yang sama, yang memegang laporan | +| `coverage-warn` | `60` | Liputan dalam peratus di bawahnya lencana berwarna merah | +| `coverage-good` | `80` | Liputan dalam peratus yang bermulanya lencana berwarna hijau; di antaranya kuning | +| `badges-branch` | `badges` | Cabang yang memegang fail lencana; ia dicipta jika tidak wujud | +| `tools-ref` | `v1` | Tag atau cabang `TirSystem/github-action` tempat skrip dalam `tools/` diambil | + +Rujuk aliran kerja pada tag major (`@v1`) atau pada tag tepat, jangan sekali-kali pada `main`. Panggilan pada tag tepat hanya tersemat sepenuhnya apabila `tools-ref` menamakan tag yang sama. Lihat panduan menaik taraf. Mulakan aliran kerja pada push ke cabang lalai anda sahaja: commit lencana membawa `[skip ci]` dan dibuat dengan token larian, yang tidak memulakan larian lain, tetapi pencetus pada setiap cabang akan menulis satu commit bagi setiap cabang. + +## Apa yang dikira + +- Fail kod ialah fail teks dengan sambungan bagi bahasa yang disokong: Python, shell, PowerShell, C, C++, C#, Java, JavaScript, TypeScript, Go, Rust dan SQL. Markdown, YAML, JSON, XML, HTML dan CSS bukan fail kod. +- Setiap baris tepat satu daripada: kosong (hanya ruang putih), komen (hanya teks komen) atau kod (selebihnya, termasuk baris dengan kod dan komen di hujung). Lencana hanya menunjukkan baris kod. +- Penanda komen dalam rentetan bukan komen, dan docstring Python ialah rentetan jadi dikira sebagai kod. +- Direktori yang bermula dengan titik, serta `node_modules`, `vendor`, `third_party`, `dist`, `build`, `out`, `bin`, `obj`, `target`, `venv`, `__pycache__` dan `shared-scripts` tidak dikira. Kod yang dijana dikecualikan dengan `exclude`. +- Rentetan mentah dan verbatim yang berbeza daripada rentetan biasa (Rust `r#"..."#`, C# `@"..."`, rentetan mentah C++) boleh dikira dengan salah. Angka sedikit berbeza daripada alat lain; peraturannya sama pada setiap larian. + +## Lencana liputan + +Laporan ialah fail Cobertura XML, yang ditulis oleh `coverage.py` (`coverage xml`), `gcovr` (`--cobertura`) dan `coverlet` (format `cobertura`). Lencana menunjukkan `lines-covered` berbanding `lines-valid`, dibundarkan kepada peratus penuh, dan warnanya mengikut dua ambang. Laporan yang tidak wujud, bukan Cobertura XML yang terbentuk dengan baik atau tidak mempunyai baris sah menyebabkan larian gagal dengan nama fail disebut; lencana liputan tidak pernah ditulis sebagai `0%` bagi laporan yang rosak. Tanpa `coverage-report` larian lulus dengan notis dan tanpa lencana liputan. + +## Menunjukkan lencana + +Tambahkan imej ke README dengan URL mentah cabang: + +```markdown +![code files](https://git.tirsystem.com/OWNER/REPO/raw/branch/badges/code-files.svg) +![lines of code](https://git.tirsystem.com/OWNER/REPO/raw/branch/badges/code-lines.svg) +![coverage](https://git.tirsystem.com/OWNER/REPO/raw/branch/badges/coverage.svg) +``` + +Pada cermin GitHub gunakan `https://raw.githubusercontent.com/OWNER/REPO/badges/code-files.svg`; cermin push menyalin cabang itu. Cabang `badges` juga memegang `code-metrics.json` dengan angka bagi setiap bahasa. Instans menyimpan fail mentah dalam cache sehingga enam jam, jadi lencana yang berubah mungkin menunjukkan angka lama seketika di sana; cermin menyimpannya lima minit. Repositori persendirian tidak menunjukkan lencana kepada pembaca yang belum log masuk. + +## Apabila aliran kerja gagal + +| Mesej | Punca | Pembetulan | +| --- | --- | --- | +| `[configuration] working-directory must be a relative path inside the repository.` | Laluan mutlak atau `..` | Gunakan laluan di dalam repositori | +| `[configuration] coverage-report '' does not exist.` | Laporan tidak dihasilkan, atau nama artifak atau laluan salah | Semak kerja ujian dan nama artifak; laluan berada di dalam artifak | +| `[configuration] coverage-report '' is not well-formed XML` atau `is not a Cobertura report` | Fail itu berformat lain | Tulis Cobertura XML (`coverage xml`, `gcovr --cobertura`) | +| `[configuration] coverage-report '' has no valid lines.` | Ujian tidak menjalankan kod yang diukur | Semak konfigurasi liputan | +| `[configuration] coverage-artifact '' needs coverage-report` | Artifak dinamakan tetapi bukan fail di dalamnya | Tetapkan `coverage-report` | +| `[configuration] coverage-warn ... is above coverage-good` atau `is not a number from 0 to 100` | Ambang tidak sah | Betulkan input | +| `[configuration] badges-branch '' must not be a default branch` atau `holds files that are not badge files` | Nama cabang ialah `main`, `master` atau cabang dengan kandungan lain | Gunakan cabang yang hanya memegang lencana | +| `the push to '' failed twice` | Larian lain menulis cabang pada masa yang sama, atau token tidak boleh menulis | Jalankan semula; semak bahawa kerja mempunyai `contents: write` | +| Langkah muat turun gagal | Artifak tidak wujud dalam larian ini, atau kerja tidak berjalan selepas kerja ujian | Tambah `needs:` dan semak nama artifak | diff --git a/docs/wiki/Home.da.md b/docs/wiki/Home.da.md index 5b4b84b..30dc278 100644 --- a/docs/wiki/Home.da.md +++ b/docs/wiki/Home.da.md @@ -1,4 +1,4 @@ - + > Dette er en oversættelse af [`Home`](Home). Den engelske side er autoritativ, og ændringer her bliver overskrevet, næste gang wikien publiceres. @@ -13,12 +13,12 @@ En samling af genbrugelige Gitea Actions-workflows til DevOps-professionelle og Dette repository er den centrale samling af genbrugelig automatisering til TirSystems repositories. Det primære repository ligger på `git.tirsystem.com` (Gitea) og spejles med push til GitHub. -Det indeholder ni workflows: synkronisering af metadata til GitHub-spejlet, publicering af wiki (kaldt af hvert repository), fælles kvalitetskontrol for Python og shell, for C og C++ og for C#, et udgivelsesworkflow, publicering af accepterede milepæle og repositoryets egen validering. De er listet med formål og diagram under [Workflows](#workflows). +Det indeholder ti workflows: synkronisering af metadata til GitHub-spejlet, publicering af wiki (kaldt af hvert repository), fælles kvalitetskontrol for Python og shell, for C og C++ og for C#, kodemålinger og dækningsbadges, et udgivelsesworkflow, publicering af accepterede milepæle og repositoryets egen validering. De er listet med formål og diagram under [Workflows](#workflows). ## Workflows -Der er ni workflows i dette repository. Et **scoped** workflow leveres af instansen til alle repositories i TirSystem-organisationerne og kræver intet i repositoryet; et **genbrugeligt** workflow kaldes fra et repository med `uses:`; de øvrige kører kun her. Diagrammerne er PlantUML-kildekode og er på engelsk. +Der er ti workflows i dette repository. Et **scoped** workflow leveres af instansen til alle repositories i TirSystem-organisationerne og kræver intet i repositoryet; et **genbrugeligt** workflow kaldes fra et repository med `uses:`; de øvrige kører kun her. Diagrammerne er PlantUML-kildekode og er på engelsk. | Workflow | Fil | Type | Starter ved | Formål | | --- | --- | --- | --- | --- | @@ -27,6 +27,7 @@ Der er ni workflows i dette repository. Et **scoped** workflow leveres af instan | Quality checks | [`quality-checks.yml`](https://git.tirsystem.com/TirSystem/github-action/src/branch/main/.gitea/workflows/quality-checks.yml) | Genbrugeligt (`workflow_call`) | et kald fra et andet repository | Kører ruff, mypy, shellcheck og shfmt på de sporede filer og ændrer intet | | Quality checks C++ | [`quality-checks-cpp.yml`](https://git.tirsystem.com/TirSystem/github-action/src/branch/main/.gitea/workflows/quality-checks-cpp.yml) | Genbrugeligt (`workflow_call`) | et kald fra et andet repository | Kører clang-format og clang-tidy i præcise versioner på de sporede C- og C++-filer og ændrer intet | | Quality checks C# | [`quality-checks-csharp.yml`](https://git.tirsystem.com/TirSystem/github-action/src/branch/main/.gitea/workflows/quality-checks-csharp.yml) | Genbrugeligt (`workflow_call`) | et kald fra et andet repository | Kører `dotnet format` og `dotnet build` på et præcist .NET SDK 10 eller nyere og ændrer intet | +| Code metrics | [`code-metrics.yml`](https://git.tirsystem.com/TirSystem/github-action/src/branch/main/.gitea/workflows/code-metrics.yml) | Genbrugeligt (`workflow_call`) | et kald fra et andet repository | Tæller kodefilerne og kodelinjerne (uden kommentar- og tomme linjer), læser en Cobertura-dækningsrapport og committer tre SVG-badges til en `badges`-gren | | Publish own wiki | [`publish-own-wiki.yml`](https://git.tirsystem.com/TirSystem/github-action/src/branch/main/.gitea/workflows/publish-own-wiki.yml) | Kun dette repository | push til `main`, der ændrer `docs/wiki/**`, manuelt | Kalder det genbrugelige workflow Publish wiki pages for dette repositorys wiki | | Release | [`release.yml`](https://git.tirsystem.com/TirSystem/github-action/src/branch/main/.gitea/workflows/release.yml) | Kun dette repository | manuelt | Opretter versionstagget, udgivelsessiden og major-tagget efter kontrollerne | | Publicering af accepterede milepæle | [`sync-milestones.yml`](https://git.tirsystem.com/TirSystem/github-action/src/branch/main/.gitea/workflows/sync-milestones.yml) | Kun dette repository | push til `main`, der ændrer `docs/milestones/**` eller `docs/sqa/reviews/**`, manuelt (tørkørsel som standard) | Opretter eller opdaterer Gitea-milepælen og issues for hver milepæl, der er Accepted med en Go-gennemgang | @@ -234,6 +235,50 @@ deactivate Workflow @enduml ``` +### Kodemålinger og dækningsbadges + +Et genbrugeligt workflow, der tæller kodefilerne og kodelinjerne (uden kommentar- og tomme linjer) i et repository og udgiver dem sammen med testdækningen som tre SVG-badges på en `badges`-gren, kaldt med `uses: TirSystem/github-action/.gitea/workflows/code-metrics.yml@v1` (ADR-0010). Det kræver `contents: write` og kører ingen test: et tidligere job hos kalderen frembringer en dækningsrapport i Cobertura XML-format og uploader den som artefakt ([vejledning](CodeMetrics.da)). + +#### Workflow + +```plantuml +@startuml +title Code metrics and coverage badges (reusable workflow) + +autonumber + +actor "Calling repository\nuses: ...code-metrics.yml@v1" as Caller +participant "Gitea Actions\ncode-metrics" as Workflow +participant "Branch badges\nof the caller" as Branch + +Caller -> Workflow: workflow_call with working-directory, exclude,\ncoverage-report and thresholds +activate Workflow + +Workflow -> Workflow: Check out the caller and the shared scripts +Workflow -> Workflow: Validate the inputs +alt an input is invalid + Workflow --> Caller: Fail with [configuration] +end + +Workflow -> Workflow: Download the coverage artifact (when given) +Workflow -> Workflow: Count code files and lines of code +alt a coverage report is given but missing or malformed + Workflow --> Caller: Fail with [configuration] that names the file +else + Workflow -> Workflow: Write code-files.svg, code-lines.svg\nand coverage.svg +end + +Workflow -> Branch: Compare with the files of the branch +alt no badge changed + Workflow --> Caller: Notice: nothing to commit, pass +else + Workflow -> Branch: One commit with [skip ci] + Workflow --> Caller: Badges published +end +deactivate Workflow +@enduml +``` + ### Udgivelse Startes manuelt af en vedligeholder; med `dry_run` `true` (standard) kontrollerer det kun (se [udgivelsesprocessen](https://git.tirsystem.com/TirSystem/github-action/src/branch/main/guides/release-process.md)). diff --git a/docs/wiki/Home.md b/docs/wiki/Home.md index 1bf3b92..52243f6 100644 --- a/docs/wiki/Home.md +++ b/docs/wiki/Home.md @@ -13,12 +13,12 @@ A collection of reusable Gitea Actions workflows for DevOps professionals and so This repository is the central collection of reusable automation for TirSystem repositories. The primary repository lives at `git.tirsystem.com` (Gitea) and is push-mirrored to GitHub. -It contains nine workflows: metadata sync to the GitHub mirror, wiki publishing (called by each repository), shared quality checks for Python and shell, for C and C++ and for C#, a release workflow, the publishing of accepted milestones and the repository's own validation. They are listed with their purpose and a diagram under [Workflows](#workflows). +It contains ten workflows: metadata sync to the GitHub mirror, wiki publishing (called by each repository), shared quality checks for Python and shell, for C and C++ and for C#, code metrics and coverage badges, a release workflow, the publishing of accepted milestones and the repository's own validation. They are listed with their purpose and a diagram under [Workflows](#workflows). ## Workflows -Nine workflows live in this repository. A **scoped** workflow is delivered by the instance to every repository of the TirSystem organizations and needs nothing in the repository; a **reusable** workflow is called by a repository with `uses:`; the others run only here. The diagrams are PlantUML source. +Ten workflows live in this repository. A **scoped** workflow is delivered by the instance to every repository of the TirSystem organizations and needs nothing in the repository; a **reusable** workflow is called by a repository with `uses:`; the others run only here. The diagrams are PlantUML source. | Workflow | File | Kind | Starts on | Purpose | | --- | --- | --- | --- | --- | @@ -27,6 +27,7 @@ Nine workflows live in this repository. A **scoped** workflow is delivered by th | Quality checks | [`quality-checks.yml`](https://git.tirsystem.com/TirSystem/github-action/src/branch/main/.gitea/workflows/quality-checks.yml) | Reusable (`workflow_call`) | a call from another repository | Runs ruff, mypy, shellcheck and shfmt on the tracked files and changes nothing | | Quality checks C++ | [`quality-checks-cpp.yml`](https://git.tirsystem.com/TirSystem/github-action/src/branch/main/.gitea/workflows/quality-checks-cpp.yml) | Reusable (`workflow_call`) | a call from another repository | Runs clang-format and clang-tidy at exact versions on the tracked C and C++ files and changes nothing | | Quality checks C# | [`quality-checks-csharp.yml`](https://git.tirsystem.com/TirSystem/github-action/src/branch/main/.gitea/workflows/quality-checks-csharp.yml) | Reusable (`workflow_call`) | a call from another repository | Runs `dotnet format` and `dotnet build` on an exact .NET SDK 10 or later and changes nothing | +| Code metrics | [`code-metrics.yml`](https://git.tirsystem.com/TirSystem/github-action/src/branch/main/.gitea/workflows/code-metrics.yml) | Reusable (`workflow_call`) | a call from another repository | Counts the code files and the lines of code (without comment and blank lines), reads a Cobertura coverage report and commits three SVG badges to a `badges` branch | | Publish own wiki | [`publish-own-wiki.yml`](https://git.tirsystem.com/TirSystem/github-action/src/branch/main/.gitea/workflows/publish-own-wiki.yml) | This repository only | push to `main` that changes `docs/wiki/**`, manual | Calls the reusable Publish wiki pages workflow for the wiki of this repository | | Release | [`release.yml`](https://git.tirsystem.com/TirSystem/github-action/src/branch/main/.gitea/workflows/release.yml) | This repository only | manual | Creates the version tag, the release page and the major tag after the checks | | Publish accepted milestones | [`sync-milestones.yml`](https://git.tirsystem.com/TirSystem/github-action/src/branch/main/.gitea/workflows/sync-milestones.yml) | This repository only | push to `main` that changes `docs/milestones/**` or `docs/sqa/reviews/**`, manual (dry run by default) | Creates or updates the Gitea milestone and issues of every milestone that is Accepted with a Go review | @@ -234,6 +235,50 @@ deactivate Workflow @enduml ``` +### Code metrics + +A reusable workflow that counts the code files and the lines of code (without comment and blank lines) of a repository and publishes them, with the test coverage, as three SVG badges on a `badges` branch, called with `uses: TirSystem/github-action/.gitea/workflows/code-metrics.yml@v1` (ADR-0010). It needs `contents: write` and runs no test: an earlier job of the caller produces a Cobertura XML coverage report and uploads it as an artifact ([guide](CodeMetrics)). + +#### Workflow + +```plantuml +@startuml +title Code metrics and coverage badges (reusable workflow) + +autonumber + +actor "Calling repository\nuses: ...code-metrics.yml@v1" as Caller +participant "Gitea Actions\ncode-metrics" as Workflow +participant "Branch badges\nof the caller" as Branch + +Caller -> Workflow: workflow_call with working-directory, exclude,\ncoverage-report and thresholds +activate Workflow + +Workflow -> Workflow: Check out the caller and the shared scripts +Workflow -> Workflow: Validate the inputs +alt an input is invalid + Workflow --> Caller: Fail with [configuration] +end + +Workflow -> Workflow: Download the coverage artifact (when given) +Workflow -> Workflow: Count code files and lines of code +alt a coverage report is given but missing or malformed + Workflow --> Caller: Fail with [configuration] that names the file +else + Workflow -> Workflow: Write code-files.svg, code-lines.svg\nand coverage.svg +end + +Workflow -> Branch: Compare with the files of the branch +alt no badge changed + Workflow --> Caller: Notice: nothing to commit, pass +else + Workflow -> Branch: One commit with [skip ci] + Workflow --> Caller: Badges published +end +deactivate Workflow +@enduml +``` + ### Release Started by hand by a maintainer; with `dry_run` `true` (the default) it only checks (see the [release process](https://git.tirsystem.com/TirSystem/github-action/src/branch/main/guides/release-process.md)). diff --git a/docs/wiki/Home.ms.md b/docs/wiki/Home.ms.md index 6b14d66..6743bdd 100644 --- a/docs/wiki/Home.ms.md +++ b/docs/wiki/Home.ms.md @@ -1,4 +1,4 @@ - + > Ini ialah terjemahan [`Home`](Home). Halaman bahasa Inggeris ialah teks yang sah, dan perubahan di sini akan ditimpa pada penerbitan wiki yang seterusnya. @@ -13,12 +13,12 @@ Koleksi aliran kerja (workflow) Gitea Actions yang boleh digunakan semula untuk Repositori ini ialah koleksi pusat bagi automasi yang boleh digunakan semula untuk repositori TirSystem. Repositori utama berada di `git.tirsystem.com` (Gitea) dan dicerminkan dengan push ke GitHub. -Ia mengandungi sembilan aliran kerja: penyegerakan metadata ke cermin GitHub, penerbitan wiki (dipanggil oleh setiap repositori), semakan kualiti bersama untuk Python dan shell, untuk C dan C++ serta untuk C#, aliran kerja keluaran, penerbitan pencapaian (milestone) yang diterima dan pengesahan repositori sendiri. Ia disenaraikan dengan tujuan dan rajah di bawah [Aliran kerja](#aliran-kerja). +Ia mengandungi sepuluh aliran kerja: penyegerakan metadata ke cermin GitHub, penerbitan wiki (dipanggil oleh setiap repositori), semakan kualiti bersama untuk Python dan shell, untuk C dan C++ serta untuk C#, metrik kod dan lencana liputan, aliran kerja keluaran, penerbitan pencapaian (milestone) yang diterima dan pengesahan repositori sendiri. Ia disenaraikan dengan tujuan dan rajah di bawah [Aliran kerja](#aliran-kerja). ## Aliran kerja -Terdapat sembilan aliran kerja dalam repositori ini. Aliran kerja **scoped** dihantar oleh instans ke setiap repositori organisasi TirSystem dan tidak memerlukan apa-apa dalam repositori; aliran kerja **boleh guna semula** dipanggil oleh sebuah repositori dengan `uses:`; yang lain hanya berjalan di sini. Rajah ialah kod sumber PlantUML dan dalam bahasa Inggeris. +Terdapat sepuluh aliran kerja dalam repositori ini. Aliran kerja **scoped** dihantar oleh instans ke setiap repositori organisasi TirSystem dan tidak memerlukan apa-apa dalam repositori; aliran kerja **boleh guna semula** dipanggil oleh sebuah repositori dengan `uses:`; yang lain hanya berjalan di sini. Rajah ialah kod sumber PlantUML dan dalam bahasa Inggeris. | Aliran kerja | Fail | Jenis | Bermula apabila | Tujuan | | --- | --- | --- | --- | --- | @@ -27,6 +27,7 @@ Terdapat sembilan aliran kerja dalam repositori ini. Aliran kerja **scoped** dih | Quality checks | [`quality-checks.yml`](https://git.tirsystem.com/TirSystem/github-action/src/branch/main/.gitea/workflows/quality-checks.yml) | Boleh guna semula (`workflow_call`) | panggilan daripada repositori lain | Menjalankan ruff, mypy, shellcheck dan shfmt pada fail yang dijejaki dan tidak mengubah apa-apa | | Quality checks C++ | [`quality-checks-cpp.yml`](https://git.tirsystem.com/TirSystem/github-action/src/branch/main/.gitea/workflows/quality-checks-cpp.yml) | Boleh guna semula (`workflow_call`) | panggilan daripada repositori lain | Menjalankan clang-format dan clang-tidy pada versi tepat pada fail C dan C++ yang dijejaki dan tidak mengubah apa-apa | | Quality checks C# | [`quality-checks-csharp.yml`](https://git.tirsystem.com/TirSystem/github-action/src/branch/main/.gitea/workflows/quality-checks-csharp.yml) | Boleh guna semula (`workflow_call`) | panggilan daripada repositori lain | Menjalankan `dotnet format` dan `dotnet build` pada .NET SDK 10 atau lebih baharu yang tepat dan tidak mengubah apa-apa | +| Code metrics | [`code-metrics.yml`](https://git.tirsystem.com/TirSystem/github-action/src/branch/main/.gitea/workflows/code-metrics.yml) | Boleh guna semula (`workflow_call`) | panggilan daripada repositori lain | Mengira fail kod dan baris kod (tanpa baris komen dan baris kosong), membaca laporan liputan Cobertura dan melakukan commit tiga lencana SVG ke cabang `badges` | | Publish own wiki | [`publish-own-wiki.yml`](https://git.tirsystem.com/TirSystem/github-action/src/branch/main/.gitea/workflows/publish-own-wiki.yml) | Repositori ini sahaja | push ke `main` yang mengubah `docs/wiki/**`, manual | Memanggil aliran kerja boleh guna semula Publish wiki pages untuk wiki repositori ini | | Release | [`release.yml`](https://git.tirsystem.com/TirSystem/github-action/src/branch/main/.gitea/workflows/release.yml) | Repositori ini sahaja | manual | Mencipta tag versi, halaman keluaran dan tag major selepas semakan | | Menerbitkan milestone yang diterima | [`sync-milestones.yml`](https://git.tirsystem.com/TirSystem/github-action/src/branch/main/.gitea/workflows/sync-milestones.yml) | Repositori ini sahaja | push ke `main` yang mengubah `docs/milestones/**` atau `docs/sqa/reviews/**`, manual (larian percubaan secara lalai) | Mencipta atau mengemas kini milestone Gitea dan isu bagi setiap milestone yang Accepted dengan semakan Go | @@ -234,6 +235,50 @@ deactivate Workflow @enduml ``` +### Metrik kod dan lencana liputan + +Aliran kerja boleh guna semula yang mengira fail kod dan baris kod (tanpa baris komen dan baris kosong) sesebuah repositori dan menerbitkannya, bersama liputan ujian, sebagai tiga lencana SVG pada cabang `badges`, dipanggil dengan `uses: TirSystem/github-action/.gitea/workflows/code-metrics.yml@v1` (ADR-0010). Ia memerlukan `contents: write` dan tidak menjalankan sebarang ujian: kerja yang lebih awal oleh pemanggil menghasilkan laporan liputan format Cobertura XML dan memuat naiknya sebagai artifak ([panduan](CodeMetrics.ms)). + +#### Aliran kerja + +```plantuml +@startuml +title Code metrics and coverage badges (reusable workflow) + +autonumber + +actor "Calling repository\nuses: ...code-metrics.yml@v1" as Caller +participant "Gitea Actions\ncode-metrics" as Workflow +participant "Branch badges\nof the caller" as Branch + +Caller -> Workflow: workflow_call with working-directory, exclude,\ncoverage-report and thresholds +activate Workflow + +Workflow -> Workflow: Check out the caller and the shared scripts +Workflow -> Workflow: Validate the inputs +alt an input is invalid + Workflow --> Caller: Fail with [configuration] +end + +Workflow -> Workflow: Download the coverage artifact (when given) +Workflow -> Workflow: Count code files and lines of code +alt a coverage report is given but missing or malformed + Workflow --> Caller: Fail with [configuration] that names the file +else + Workflow -> Workflow: Write code-files.svg, code-lines.svg\nand coverage.svg +end + +Workflow -> Branch: Compare with the files of the branch +alt no badge changed + Workflow --> Caller: Notice: nothing to commit, pass +else + Workflow -> Branch: One commit with [skip ci] + Workflow --> Caller: Badges published +end +deactivate Workflow +@enduml +``` + ### Keluaran Dimulakan secara manual oleh penyelenggara; dengan `dry_run` `true` (lalai) ia hanya menyemak (lihat [proses keluaran](https://git.tirsystem.com/TirSystem/github-action/src/branch/main/guides/release-process.md)). diff --git a/docs/wiki/Upgrading.da.md b/docs/wiki/Upgrading.da.md index 733172a..9fa17b6 100644 --- a/docs/wiki/Upgrading.da.md +++ b/docs/wiki/Upgrading.da.md @@ -1,4 +1,4 @@ - + > Dette er en oversættelse af [`Upgrading`](Upgrading). Den engelske side er autoritativ, og ændringer her bliver overskrevet, næste gang wikien publiceres. @@ -18,7 +18,7 @@ Sådan tager en softwareudvikler en ny udgivelse af de fælles workflows i brug En udgivelse, der bryder kompatibiliteten, får en ny major-version (`v2`), så en pipeline på `@v1` aldrig modtager den, før du selv ændrer linjen. -Kvalitetskontrol-workflowene (`quality-checks.yml`, `quality-checks-cpp.yml` og `quality-checks-csharp.yml`) henter deres script ved deres input `tools-ref`, som som standard er `v1`. For at fastlåse et helt skal du kalde det ved et præcist tag og sætte `tools-ref` til samme tag; ellers følger scriptet major-tagget. +Kvalitetskontrol-workflowene (`quality-checks.yml`, `quality-checks-cpp.yml` og `quality-checks-csharp.yml`) og kodemålings-workflowet (`code-metrics.yml`) henter deres scripts ved deres input `tools-ref`, som som standard er `v1`. For at fastlåse et helt skal du kalde det ved et præcist tag og sætte `tools-ref` til samme tag; ellers følger scriptet major-tagget. Det scoped workflow **Sync GitHub mirror metadata** leveres af instansen og står ikke i dine `uses:`-linjer; du kan ikke fastlåse det, men det henter sit script ved major-tagget. Publicering af wiki er et genbrugeligt workflow, du kalder (`publish-wiki-pages.yml`); det scoped workflow **Publish wiki** og dets markør `wiki/.publish` blev fjernet i 2.0.0, så et repository, der stadig har markøren, publicerer ikke mere, før det kalder det genbrugelige workflow (se introduktionsguiden). diff --git a/docs/wiki/Upgrading.md b/docs/wiki/Upgrading.md index 2899d75..81642fc 100644 --- a/docs/wiki/Upgrading.md +++ b/docs/wiki/Upgrading.md @@ -23,8 +23,8 @@ A breaking release gets a new major version (`v2`), so a pipeline on `@v1` never receives it until you change the line yourself. The quality-checks workflows (`quality-checks.yml`, `quality-checks-cpp.yml` and -`quality-checks-csharp.yml`) fetch their script at their input `tools-ref`, which -defaults to `v1`. To pin one completely, call it at an exact tag and set +`quality-checks-csharp.yml`) and the code-metrics workflow (`code-metrics.yml`) +fetch their scripts at their input `tools-ref`, which defaults to `v1`. To pin one completely, call it at an exact tag and set `tools-ref` to the same tag; otherwise the script follows the major tag. The scoped workflow **Sync GitHub mirror metadata** is delivered by the diff --git a/docs/wiki/Upgrading.ms.md b/docs/wiki/Upgrading.ms.md index 52c2237..cdffe6a 100644 --- a/docs/wiki/Upgrading.ms.md +++ b/docs/wiki/Upgrading.ms.md @@ -1,4 +1,4 @@ - + > Ini ialah terjemahan [`Upgrading`](Upgrading). Halaman bahasa Inggeris ialah teks yang sah, dan perubahan di sini akan ditimpa pada penerbitan wiki yang seterusnya. @@ -18,7 +18,7 @@ Cara seorang jurutera perisian menerima keluaran baharu aliran kerja bersama den Keluaran yang memecahkan keserasian mendapat versi major baharu (`v2`), jadi saluran paip pada `@v1` tidak menerimanya sehingga anda sendiri menukar baris itu. -Aliran kerja semakan kualiti (`quality-checks.yml`, `quality-checks-cpp.yml` dan `quality-checks-csharp.yml`) mengambil skripnya pada inputnya `tools-ref`, yang lalai ialah `v1`. Untuk menyematkan satu sepenuhnya, panggil pada tag tepat dan tetapkan `tools-ref` kepada tag yang sama; jika tidak, skrip mengikuti tag major. +Aliran kerja semakan kualiti (`quality-checks.yml`, `quality-checks-cpp.yml` dan `quality-checks-csharp.yml`) dan aliran kerja metrik kod (`code-metrics.yml`) mengambil skripnya pada inputnya `tools-ref`, yang lalai ialah `v1`. Untuk menyematkan satu sepenuhnya, panggil pada tag tepat dan tetapkan `tools-ref` kepada tag yang sama; jika tidak, skrip mengikuti tag major. Aliran kerja scoped **Sync GitHub mirror metadata** dihantar oleh instans dan tidak terdapat dalam baris `uses:` anda; anda tidak boleh menyematkannya, tetapi ia mengambil skripnya pada tag major. Penerbitan wiki ialah aliran kerja boleh guna semula yang anda panggil (`publish-wiki-pages.yml`); aliran kerja scoped **Publish wiki** dan penanda `wiki/.publish` telah dibuang dalam 2.0.0, jadi repositori yang masih mempunyai penanda itu tidak lagi menerbitkan sehingga ia memanggil aliran kerja boleh guna semula (lihat panduan permulaan). diff --git a/docs/wiki/_Sidebar.md b/docs/wiki/_Sidebar.md index 593b9aa..5ffc1c9 100644 --- a/docs/wiki/_Sidebar.md +++ b/docs/wiki/_Sidebar.md @@ -11,6 +11,7 @@ - [Quality checks (Python, shell)](QualityChecksPythonShell) - [Quality checks (C and C++)](QualityChecksCpp) - [Quality checks (C#)](QualityChecksCSharp) +- [Code metrics](CodeMetrics) - [Upgrading](Upgrading) - [Capabilities](Capabilities) @@ -23,6 +24,7 @@ - [Kvalitetskontrol (Python, shell)](QualityChecksPythonShell.da) - [Kvalitetskontrol (C og C++)](QualityChecksCpp.da) - [Kvalitetskontrol (C#)](QualityChecksCSharp.da) +- [Kodemålinger](CodeMetrics.da) - [Opgradering](Upgrading.da) - [Kapabiliteter](Capabilities.da) @@ -35,5 +37,6 @@ - [Semakan kualiti (Python, shell)](QualityChecksPythonShell.ms) - [Semakan kualiti (C dan C++)](QualityChecksCpp.ms) - [Semakan kualiti (C#)](QualityChecksCSharp.ms) +- [Metrik kod](CodeMetrics.ms) - [Menaik taraf](Upgrading.ms) - [Keupayaan](Capabilities.ms) diff --git a/guides/code-metrics.md b/guides/code-metrics.md new file mode 100644 index 0000000..9373026 --- /dev/null +++ b/guides/code-metrics.md @@ -0,0 +1,117 @@ +# Showing code metrics and coverage badges + +How a software engineer calls the shared code-metrics workflow and shows its +three badges (code files, lines of code and coverage) in a README. The workflow +counts the code of the repository, reads a coverage report that the caller +supplies and commits three SVG files to a `badges` branch. It runs no test and +needs no secret. Its contract is ADR-0010 in this repository. + +## Calling the workflow + +The workflow cannot see the files of your test job, so the tests run in a job of +your own, upload the Cobertura XML report as an artifact, and the metrics job +runs after it: + +```yaml +name: Code metrics + +on: + push: + branches: [ main ] + +jobs: + tests: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4.2.2 + - run: python3 -m pip install pytest pytest-cov && python3 -m pytest --cov --cov-report=xml + - uses: actions/upload-artifact@v4.6.2 + with: + name: coverage + path: coverage.xml + + metrics: + needs: tests + permissions: + contents: write + uses: TirSystem/github-action/.gitea/workflows/code-metrics.yml@v1 + with: + coverage-artifact: coverage + coverage-report: coverage.xml +``` + +| Input | Default | Meaning | +| --- | --- | --- | +| `working-directory` | `.` | Directory, relative to the repository root, whose code is counted | +| `exclude` | empty | Comma list of patterns of files and directories that are not counted, in addition to the defaults; a pattern without `/` matches a name anywhere, a pattern with `/` matches the path below `working-directory` | +| `coverage-report` | empty | Cobertura XML report: the file inside the artifact when `coverage-artifact` is set, otherwise a path in the repository; empty writes no coverage badge | +| `coverage-artifact` | empty | Name of an artifact, uploaded by an earlier job of the same run, that holds the report | +| `coverage-warn` | `60` | Coverage in percent below which the badge is red | +| `coverage-good` | `80` | Coverage in percent from which the badge is green; in between it is yellow | +| `badges-branch` | `badges` | Branch that holds the badge files; it is created when it does not exist | +| `tools-ref` | `v1` | Tag or branch of `TirSystem/github-action` from which the scripts in `tools/` are fetched | + +Reference the workflow at the major tag (`@v1`) or at an exact tag, never at +`main`. A call at an exact tag is pinned completely only when `tools-ref` names +the same tag. See the upgrade guide. Start the workflow on pushes to your +default branch only: the badge commit carries `[skip ci]` and is made with the +token of the run, which does not start another run, but a trigger on every +branch would write one commit per branch. + +## What is counted + +- A code file is a text file with the extension of a supported language: + Python, shell, PowerShell, C, C++, C#, Java, JavaScript, TypeScript, Go, Rust + and SQL. Markdown, YAML, JSON, XML, HTML and CSS are not code files. +- Every line is exactly one of blank (only white space), comment (only comment + text) or code (everything else, including a line with code and a trailing + comment). The badge shows the lines of code only. +- A comment marker inside a string is not a comment, and a Python docstring is a + string, so it counts as code. +- Directories that start with a dot and `node_modules`, `vendor`, `third_party`, + `dist`, `build`, `out`, `bin`, `obj`, `target`, `venv`, `__pycache__` and + `shared-scripts` are not counted. Generated code is excluded with `exclude`. +- Raw and verbatim strings that differ from an ordinary string (Rust `r#"..."#`, + C# `@"..."`, C++ raw strings) can be counted wrongly. The counts will differ + a little from other tools; the rules are the same on every run. + +## The coverage badge + +The report is a Cobertura XML file, which `coverage.py` (`coverage xml`), +`gcovr` (`--cobertura`) and `coverlet` (`cobertura` format) write. The badge +shows `lines-covered` over `lines-valid`, rounded to a whole percent, and its +colour follows the two thresholds. A report that does not exist, is not +well-formed Cobertura XML or has no valid lines fails the run with the file +named; the coverage badge is never written as `0%` for a bad report. Without +`coverage-report` the run passes with a notice and no coverage badge. + +## Showing the badges + +Add the images to the README with the raw URLs of the branch: + +```markdown +![code files](https://git.tirsystem.com/OWNER/REPO/raw/branch/badges/code-files.svg) +![lines of code](https://git.tirsystem.com/OWNER/REPO/raw/branch/badges/code-lines.svg) +![coverage](https://git.tirsystem.com/OWNER/REPO/raw/branch/badges/coverage.svg) +``` + +On the GitHub mirror use `https://raw.githubusercontent.com/OWNER/REPO/badges/code-files.svg`; +the push mirror copies the branch. The `badges` branch also holds +`code-metrics.json` with the numbers per language. The instance caches a raw +file for up to six hours, so a changed badge can show the old number for a +while there; the mirror caches for five minutes. A private repository does not +show the badges to a reader who is not signed in. + +## When the workflow fails + +| Message | Cause | Fix | +| --- | --- | --- | +| `[configuration] working-directory must be a relative path inside the repository.` | An absolute path or `..` | Use a path inside the repository | +| `[configuration] coverage-report '' does not exist.` | The report was not produced, or the artifact name or path is wrong | Check the test job and the artifact name; the path is inside the artifact | +| `[configuration] coverage-report '' is not well-formed XML` or `is not a Cobertura report` | The file is another format | Write Cobertura XML (`coverage xml`, `gcovr --cobertura`) | +| `[configuration] coverage-report '' has no valid lines.` | The tests ran no measured code | Check the coverage configuration | +| `[configuration] coverage-artifact '' needs coverage-report` | The artifact is named but not the file in it | Set `coverage-report` | +| `[configuration] coverage-warn ... is above coverage-good` or `is not a number from 0 to 100` | A threshold is invalid | Correct the input | +| `[configuration] badges-branch '' must not be a default branch` or `holds files that are not badge files` | The branch name is `main`, `master` or a branch with other content | Use a branch that holds only the badges | +| `the push to '' failed twice` | Another run wrote the branch at the same time, or the token cannot write | Re-run; check that the job has `contents: write` | +| The download step fails | The artifact does not exist in this run, or the job does not run after the test job | Add `needs:` and check the artifact name | diff --git a/guides/upgrading.md b/guides/upgrading.md index db5a920..a6f10df 100644 --- a/guides/upgrading.md +++ b/guides/upgrading.md @@ -17,8 +17,8 @@ A breaking release gets a new major version (`v2`), so a pipeline on `@v1` never receives it until you change the line yourself. The quality-checks workflows (`quality-checks.yml`, `quality-checks-cpp.yml` and -`quality-checks-csharp.yml`) fetch their script at their input `tools-ref`, which -defaults to `v1`. To pin one completely, call it at an exact tag and set +`quality-checks-csharp.yml`) and the code-metrics workflow (`code-metrics.yml`) +fetch their scripts at their input `tools-ref`, which defaults to `v1`. To pin one completely, call it at an exact tag and set `tools-ref` to the same tag; otherwise the script follows the major tag. The scoped workflow **Sync GitHub mirror metadata** is delivered by the diff --git a/tests/test_build_wiki.py b/tests/test_build_wiki.py index 7cf5958..569584b 100644 --- a/tests/test_build_wiki.py +++ b/tests/test_build_wiki.py @@ -105,6 +105,7 @@ def _write_sources(root: Path) -> None: "quality-checks", "quality-checks-cpp", "quality-checks-csharp", + "code-metrics", "upgrading", ) for name in guide_names: diff --git a/tools/build_wiki.py b/tools/build_wiki.py index 66e6440..a9ad642 100644 --- a/tools/build_wiki.py +++ b/tools/build_wiki.py @@ -46,6 +46,7 @@ PAGES: tuple[Page, ...] = ( Page("QualityChecksPythonShell", "guides/quality-checks.md"), Page("QualityChecksCpp", "guides/quality-checks-cpp.md"), Page("QualityChecksCSharp", "guides/quality-checks-csharp.md"), + Page("CodeMetrics", "guides/code-metrics.md"), Page("Upgrading", "guides/upgrading.md"), Page("Capabilities", "README.md", ("Capabilities",)), ) @@ -66,6 +67,7 @@ _PAGE_FOR_SOURCE = { "guides/quality-checks.md": "QualityChecksPythonShell", "guides/quality-checks-cpp.md": "QualityChecksCpp", "guides/quality-checks-csharp.md": "QualityChecksCSharp", + "guides/code-metrics.md": "CodeMetrics", "guides/upgrading.md": "Upgrading", } @@ -191,9 +193,9 @@ def render_page(page: Page, repo_root: Path = REPO_ROOT) -> str: # Link text of the sidebar: English, then one list per translation code. SIDEBAR_TITLE = "TirSystem GitHub Actions" SIDEBAR_LABELS: dict[str, tuple[str, ...]] = { - "en": ("Home", "Onboarding", "Credentials", "Troubleshooting", "Quality checks (Python, shell)", "Quality checks (C and C++)", "Quality checks (C#)", "Upgrading", "Capabilities"), - "da": ("Hjem", "Tilslutning", "Credentials", "Fejlfinding", "Kvalitetskontrol (Python, shell)", "Kvalitetskontrol (C og C++)", "Kvalitetskontrol (C#)", "Opgradering", "Kapabiliteter"), - "ms": ("Utama", "Menyertakan", "Kelayakan", "Diagnosis", "Semakan kualiti (Python, shell)", "Semakan kualiti (C dan C++)", "Semakan kualiti (C#)", "Menaik taraf", "Keupayaan"), + "en": ("Home", "Onboarding", "Credentials", "Troubleshooting", "Quality checks (Python, shell)", "Quality checks (C and C++)", "Quality checks (C#)", "Code metrics", "Upgrading", "Capabilities"), + "da": ("Hjem", "Tilslutning", "Credentials", "Fejlfinding", "Kvalitetskontrol (Python, shell)", "Kvalitetskontrol (C og C++)", "Kvalitetskontrol (C#)", "Kodemålinger", "Opgradering", "Kapabiliteter"), + "ms": ("Utama", "Menyertakan", "Kelayakan", "Diagnosis", "Semakan kualiti (Python, shell)", "Semakan kualiti (C dan C++)", "Semakan kualiti (C#)", "Metrik kod", "Menaik taraf", "Keupayaan"), } SIDEBAR_NAME = "_Sidebar.md" -- 2.54.0 From 5c5a1d19d96a6979b4f6141e736957174ba686a9 Mon Sep 17 00:00:00 2001 From: Jens Tirsvad Nielsen Date: Wed, 14 Oct 2026 14:05:20 +0800 Subject: [PATCH 08/10] MIL-010 task 8: adoption in a throwaway repository, results and RC-074; release entry 2.1.0 Task: MIL-010#8 Refs #162 --- CHANGELOG.md | 13 +++++ README.md | 2 +- docs/artifact-registry.md | 2 +- .../milestones/mil-010-code-metrics-badges.md | 37 +++++++++++++- docs/sqa/reviews/rc-074-mil-010-results.md | 50 +++++++++++++++++++ docs/sqa/traceability-matrix.md | 8 +-- docs/wiki/Capabilities.da.md | 4 +- docs/wiki/Capabilities.md | 2 +- docs/wiki/Capabilities.ms.md | 4 +- 9 files changed, 111 insertions(+), 11 deletions(-) create mode 100644 docs/sqa/reviews/rc-074-mil-010-results.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 6480479..2437cae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,19 @@ Each release lists its changes in these groups, in this order: ## [Unreleased] +## [2.1.0] - 2026-10-14 + +### Added + +- The reusable workflow `code-metrics.yml` (ADR-0010, MIL-010). It counts the + code files and the lines of code (without comment and blank lines) of a + repository, reads a Cobertura XML coverage report that the caller uploads as an + artifact, and commits three SVG badges (code files, lines of code, coverage) to + a `badges` branch. It needs `contents: write` and no secret; the inputs are + `working-directory`, `exclude`, `coverage-report`, `coverage-artifact`, + `coverage-warn`, `coverage-good`, `badges-branch` and `tools-ref`. See + `guides/code-metrics.md`. + ## [2.0.0] - 2026-10-14 ### Breaking diff --git a/README.md b/README.md index 2c12849..e4a4a7f 100644 --- a/README.md +++ b/README.md @@ -545,7 +545,7 @@ Rows that say Implemented or Guides written exist today. The rest is planned and | Publish a repository wiki from `docs/wiki/` through a called workflow | Implemented as the reusable workflow `.gitea/workflows/publish-wiki-pages.yml`, with offline tests; called by every repository that publishes a wiki, and this repository's own wiki is in `docs/wiki/` (MIL-008, MIL-009) | | Shared quality checks (Python and shell) | Implemented as the reusable workflow `.gitea/workflows/quality-checks.yml`, with offline tests and a trial in a second organization; adopted in RepoFoundry ([guide](guides/quality-checks.md)) | | Shared quality checks (C and C++, C#) | Implemented as the reusable workflows `.gitea/workflows/quality-checks-cpp.yml` and `.gitea/workflows/quality-checks-csharp.yml` with exact tool versions and offline tests; not yet used in another repository (MIL-007) ([C and C++ guide](guides/quality-checks-cpp.md), [C# guide](guides/quality-checks-csharp.md)) | -| Code metrics and coverage badges | Implemented as the reusable workflow `.gitea/workflows/code-metrics.yml` with a counter, a Cobertura reader, an SVG renderer and offline tests; not yet used in another repository (MIL-010) ([guide](guides/code-metrics.md)) | +| Code metrics and coverage badges | Implemented as the reusable workflow `.gitea/workflows/code-metrics.yml` with a counter, a Cobertura reader, an SVG renderer and offline tests; tried in a throwaway repository, not yet used in a project repository (MIL-010) ([guide](guides/code-metrics.md)) | | Versioned release and publishing | Release process, changelog and upgrade guide defined; the release workflow exists and `v1.0.0` is released (MIL-004) ([guide](guides/upgrading.md)) | ## 📄 License diff --git a/docs/artifact-registry.md b/docs/artifact-registry.md index 2b0f32e..502616b 100644 --- a/docs/artifact-registry.md +++ b/docs/artifact-registry.md @@ -11,7 +11,7 @@ each document lives in *this* project and the next version to use. | SA | Stakeholder Analysis | docs/stakeholder-analysis.md | 002 | | PP | Project Plan | docs/project-plan.md | 002 | | MIL | Milestone / Gateway | docs/milestones/*.md | 011 | -| RC | SQA Review Record | docs/sqa/reviews/rc-*.md | 074 | +| RC | SQA Review Record | docs/sqa/reviews/rc-*.md | 075 | | TM | Traceability Matrix | docs/sqa/traceability-matrix.md | 002 | | ADR | Architecture Decision Record | docs/adr/adr-NNNN-*.md | 0011 | | UCD | Use Case Diagram | docs/use-case-diagram.md | 002 | diff --git a/docs/milestones/mil-010-code-metrics-badges.md b/docs/milestones/mil-010-code-metrics-badges.md index 5f4e4d5..6055f10 100644 --- a/docs/milestones/mil-010-code-metrics-badges.md +++ b/docs/milestones/mil-010-code-metrics-badges.md @@ -9,7 +9,8 @@ ## 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-068) | [5002450] | +| 2026-10-14 | Deprecated | Jens Tirsvad Nielsen | AI as stand-in for S01 | Initial version; reviewed (RC-068) | [5002450] | +| 2026-10-14 | Accepted | Jens Tirsvad Nielsen | AI as stand-in for S01 | Results of tasks 3, 6 and 8 added: the report as an artifact of the caller (ADR-0010), adoption in a throwaway repository with seeded change and failure; reviewed (RC-074) | pending | --- @@ -130,6 +131,40 @@ anonymous reader), and the rendering inside a Gitea README, which is the same `badges-verify` are left in place because deleting a branch is an action the run stops on; they are listed in the summary for the user to delete. +Task 6 changed the contract once while it was implemented. A called workflow +runs in its own job, so it cannot read a report that the caller's test job +wrote in its workspace; a report the repository commits is the only file it can +see. ADR-0010 was amended (RC-073): the caller uploads the report as an +artifact and names it in the new input `coverage-artifact`; the workflow +downloads it with `actions/download-artifact` into the directory +`coverage-artifact` and `coverage-report` names the file inside it. The scope +and the three badges did not change. + +Task 8 was run on 2026-10-14 in a throwaway private repository of the +organisation, `TirSystem/code-metrics-adoption` (no repository of a project +fits, and the user's plan allows a throwaway). It holds a two-file Python +module with tests and a workflow of two jobs: the tests with `pytest-cov` +upload `coverage.xml` as the artifact `coverage`, and the second job calls +`code-metrics.yml` and `tools-ref` of the branch `mil-010-implementation` of +this repository, before its merge. + +| Step | Result | +| --- | --- | +| Baseline (run 1423) | All steps pass, including `Download the coverage report` with `actions/download-artifact@v4.1.8` on Gitea 1.27.3; the `badges` branch is created with one commit by `code-metrics` and the message `Update code metrics badges [skip ci]`; `code-files.svg` shows 2, `code-lines.svg` 28 (the hand count of the two files), `coverage.svg` 100% in green `#44cc11` | +| The push of the badges | No run was started by it: the runs of the repository are the two of the push to `main` (the metrics workflow and the scoped sync) | +| Seeded change (run 1425): two tests removed | The coverage badge reads 60% in yellow `#dfb317`; one new commit on `badges` that changes the badge files; the files and the lines badges changed with the tests | +| Seeded failure (run 1429): `coverage-report` names a file that does not exist | The job `code-metrics` fails: `::error::[configuration] coverage-report 'coverage-artifact/nope.xml' does not exist.`; `badges` is unchanged (two commits) | +| README | Its three Markdown images render as `` of the raw URLs of the `badges` branch (checked with the markdown API of the instance); the raw URLs answer `image/svg+xml` as in task 3 | + +Not covered: a run that changes nothing (the unchanged case is covered by the +offline tests, not by a run), two runs that write the branch at the same time +(covered by an offline test with a rejecting remote, not on the instance), the +rendering for a reader who is not signed in to a private repository, and a +repository of a project with a real coverage report. The throwaway repository +`TirSystem/code-metrics-adoption` is left in place because deleting a +repository is an action the run stops on; it is listed in the summary for the +user to delete. + --- [BC-001]: ../business-case.md diff --git a/docs/sqa/reviews/rc-074-mil-010-results.md b/docs/sqa/reviews/rc-074-mil-010-results.md new file mode 100644 index 0000000..6c759fb --- /dev/null +++ b/docs/sqa/reviews/rc-074-mil-010-results.md @@ -0,0 +1,50 @@ +# Review Record: MIL-010 (results of tasks 3, 6 and 8) + +## Metadata +| Key | Value | +| --- | --- | +| ID | RC-074 | +| CrossReference | [MIL-010], [QC-MIL-001] | + +## Version History +| Date | Status | Author | Reviewer | Change | Commit | +| --- | --- | --- | --- | --- | --- | +| 2026-10-14 | Accepted | Jens Tirsvad Nielsen | AI as stand-in for S01 | Initial version; accepted | pending | + +--- + +## Artifact Under Review + +- Instance reviewed: [MIL-010] +- Checklist used: [QC-MIL-001] +- Scope: delta re-review of the Verification results section (tasks 3, 6 and 8) and the Version History; the Go/No-Go criteria, deliverable and tasks are unchanged since RC-068 +- Language and domain: n/a (technical type) +- Language reviewer: none +- Mode: autonomous, not independent (the reviewer is `AI as stand-in for S01`; the user accepted the plan in chat on 2026-10-14) + +## Checklist Results + +| # | Criterion | Status | Evidence/Notes | +| --- | --- | --- | --- | +| 1 | A concrete deliverable is defined for every gate | Pass | Unchanged; the results show the workflow, the three badges and a use in another repository. | +| 2 | Explicit Go/No-Go criteria are stated for each gate | Pass | Unchanged. Evidence so far: criterion 1 (task 3: push with the token of the run, SVG served as image on the instance and the mirror), 2 (ADR-0010, RC-072, RC-073), 3 and 4 (offline fixtures and reports), 5 (valid, stable SVG, offline tests), 6 (no run from the badge push, no commit when unchanged in offline tests, `contents: write` only), 7 (throwaway repository: badges, colour change, named failure), 8 (the workflow runs three script files, no embedded script). Criterion 9 (release) follows in task 9. | +| 3 | Dependencies on other milestones are explicitly mapped | Pass | Unchanged: MIL-003 and MIL-004. | +| 4 | Each milestone is traceable to a Business Case objective or KPI | Pass | O5 and success criterion 5; criterion 5 is met in the throwaway repository and still wants a project repository. | +| 5 | Milestone owner and approving reviewer are identified | Pass | Unchanged: S05 and S01. | +| 6 | Milestone has a defined target date consistent with project constraints | Pass | Unchanged: 2027-01-30. | + +## Overall Verdict + +Go - the recorded results support the criteria 1 to 8; criterion 9 is decided after the release (task 9). The AI records the decision as stand-in for S01; there is no independent reader and the decision stays open to challenge in the run summary. + +## Action Items + +| Action | Owner | Due | +| --- | --- | --- | +| Delete the throwaway repository `TirSystem/code-metrics-adoption` and the branches `verify-badges-delivery` and `badges-verify` of `TirSystem/github-action` (and their copies on the GitHub mirror); the run does not delete repositories or branches | S01 | after the review of the summary | +| Adopt the workflow in a project repository so that success criterion 5 of the Business Case is met by a real project | S01 | next project that has tests | + +--- + +[MIL-010]: ../../milestones/mil-010-code-metrics-badges.md +[QC-MIL-001]: ../../../framework/qc/artifacts/qc-milestones-gateways.md diff --git a/docs/sqa/traceability-matrix.md b/docs/sqa/traceability-matrix.md index 2a5a1a6..cda5ef3 100644 --- a/docs/sqa/traceability-matrix.md +++ b/docs/sqa/traceability-matrix.md @@ -4,13 +4,13 @@ | Key | Value | | --- | --- | | ID | TM-001 | -| CrossReference | [BC-001], [SA-001], [PP-001], [MIL-001], [MIL-002], [MIL-003], [MIL-004], [MIL-005], [MIL-006], [MIL-007], [MIL-008], [MIL-009], [MIL-010], [UC-008], [RC-001], [RC-002], [RC-003], [RC-004], [RC-005], [RC-006], [RC-007], [RC-008], [RC-009], [RC-010], [RC-011], [RC-012], [RC-013], [RC-014], [RC-015], [RC-016], [RC-017], [RC-018], [RC-019], [RC-020], [RC-021], [RC-022], [RC-023], [RC-024], [RC-025], [RC-026], [RC-027], [RC-028], [RC-029], [RC-030], [RC-031], [RC-032], [RC-033], [RC-034], [RC-035], [RC-036], [RC-037], [RC-038], [RC-039], [RC-040], [RC-041], [RC-042], [RC-043], [RC-044], [RC-045], [RC-046], [RC-047], [RC-048], [RC-049], [RC-050], [RC-051], [RC-052], [RC-053], [RC-054], [RC-055], [RC-056], [RC-057], [RC-058], [RC-059], [RC-060], [RC-061], [RC-062], [RC-063], [RC-064], [RC-065], [RC-066], [RC-067], [RC-068], [RC-069], [RC-070], [RC-071], [RC-072], [RC-073], [ADR-0007], [ADR-0008], [ADR-0009], [ADR-0010], [UC-007], [UC-006], [ADR-0006], [UC-005], [ADR-0005], [UCD-001], [US-001], [UC-001], [UC-002], [UC-003], [UC-004], [ADR-0001], [ADR-0002], [ADR-0003], [ADR-0004] | +| CrossReference | [BC-001], [SA-001], [PP-001], [MIL-001], [MIL-002], [MIL-003], [MIL-004], [MIL-005], [MIL-006], [MIL-007], [MIL-008], [MIL-009], [MIL-010], [UC-008], [RC-001], [RC-002], [RC-003], [RC-004], [RC-005], [RC-006], [RC-007], [RC-008], [RC-009], [RC-010], [RC-011], [RC-012], [RC-013], [RC-014], [RC-015], [RC-016], [RC-017], [RC-018], [RC-019], [RC-020], [RC-021], [RC-022], [RC-023], [RC-024], [RC-025], [RC-026], [RC-027], [RC-028], [RC-029], [RC-030], [RC-031], [RC-032], [RC-033], [RC-034], [RC-035], [RC-036], [RC-037], [RC-038], [RC-039], [RC-040], [RC-041], [RC-042], [RC-043], [RC-044], [RC-045], [RC-046], [RC-047], [RC-048], [RC-049], [RC-050], [RC-051], [RC-052], [RC-053], [RC-054], [RC-055], [RC-056], [RC-057], [RC-058], [RC-059], [RC-060], [RC-061], [RC-062], [RC-063], [RC-064], [RC-065], [RC-066], [RC-067], [RC-068], [RC-069], [RC-070], [RC-071], [RC-072], [RC-073], [RC-074], [ADR-0007], [ADR-0008], [ADR-0009], [ADR-0010], [UC-007], [UC-006], [ADR-0006], [UC-005], [ADR-0005], [UCD-001], [US-001], [UC-001], [UC-002], [UC-003], [UC-004], [ADR-0001], [ADR-0002], [ADR-0003], [ADR-0004] | ## Version History | Date | Status | Author | Reviewer | Change | Commit | | --- | --- | --- | --- | --- | --- | -| 2026-10-14 | Proposed | Jens Tirsvad Nielsen | S01 | Added ADR-0010, RC-072 (contract of the code metrics badges, MIL-010 task 2) | pending | | 2026-10-14 | Proposed | Jens Tirsvad Nielsen | S01 | Added RC-073 (ADR-0010: the coverage report as an artifact, MIL-010 task 6) | pending | +| 2026-10-14 | Proposed | Jens Tirsvad Nielsen | S01 | Added RC-074 (MIL-010 results of tasks 3, 6 and 8) | pending | --- @@ -43,7 +43,7 @@ updated whenever an artifact instance is created or reviewed. | [MIL-007] | MIL | [BC-001], [PP-001], [MIL-003], [MIL-004], [ADR-0008] | - | [RC-052], [RC-055], [RC-059] | | [MIL-008] | MIL | [BC-001], [PP-001], [MIL-006], [ADR-0004] | [MIL-009] | [RC-060], [RC-061] | | [MIL-009] | MIL | [BC-001], [PP-001], [MIL-008], [ADR-0004], [ADR-0006], [ADR-0009] | - | [RC-064], [RC-066] | -| [MIL-010] | MIL | [BC-001], [PP-001], [MIL-003], [MIL-004] | - | [RC-068] | +| [MIL-010] | MIL | [BC-001], [PP-001], [MIL-003], [MIL-004] | - | [RC-068], [RC-074] | | [RC-007] | RC | [MIL-004] | - | - | | [RC-008] | RC | [BC-001] | - | - | | [RC-009] | RC | [PP-001] | - | - | @@ -130,6 +130,7 @@ updated whenever an artifact instance is created or reviewed. | [RC-071] | RC | [UCD-001] | - | - | | [RC-072] | RC | [ADR-0010] | - | - | | [RC-073] | RC | [ADR-0010] | - | - | +| [RC-074] | RC | [MIL-010] | - | - | ## Coverage Notes @@ -237,6 +238,7 @@ updated whenever an artifact instance is created or reviewed. [RC-071]: ./reviews/rc-071-ucd-001-metrics.md [RC-072]: ./reviews/rc-072-adr-0010.md [RC-073]: ./reviews/rc-073-adr-0010-artifact.md +[RC-074]: ./reviews/rc-074-mil-010-results.md [ADR-0008]: ../adr/adr-0008-cpp-csharp-quality-check-contract.md [ADR-0009]: ../adr/adr-0009-wiki-publishing-reusable-workflow.md [ADR-0007]: ../adr/adr-0007-release-publish-destination.md diff --git a/docs/wiki/Capabilities.da.md b/docs/wiki/Capabilities.da.md index 0e2f374..90ed2b7 100644 --- a/docs/wiki/Capabilities.da.md +++ b/docs/wiki/Capabilities.da.md @@ -1,4 +1,4 @@ - + > Dette er en oversættelse af [`Capabilities`](Capabilities). Den engelske side er autoritativ, og ændringer her bliver overskrevet, næste gang wikien publiceres. @@ -16,5 +16,5 @@ Rækker, der siger Implementeret eller Vejledninger er skrevet, findes i dag. Re | Publicering af et repositorys wiki fra `docs/wiki/` gennem et kaldt workflow | Implementeret som det genbrugelige workflow `.gitea/workflows/publish-wiki-pages.yml`, med offline-tests; kaldt af hvert repository, der publicerer en wiki, og dette repositorys egen wiki er i `docs/wiki/` (MIL-008, MIL-009) | | Fælles kvalitetskontrol (Python og shell) | Implementeret som det genbrugelige workflow `.gitea/workflows/quality-checks.yml`, med offline-tests og et forsøg i en anden organisation; taget i brug i RepoFoundry ([vejledning](QualityChecksPythonShell.da)) | | Fælles kvalitetskontrol (C og C++, C#) | Implementeret som de genbrugelige workflows `.gitea/workflows/quality-checks-cpp.yml` og `.gitea/workflows/quality-checks-csharp.yml` med præcise værktøjsversioner og offline-tests; endnu ikke brugt i et andet repository (MIL-007) ([vejledning til C og C++](QualityChecksCpp.da), [vejledning til C#](QualityChecksCSharp.da)) | -| Kodemålinger og dækningsbadges | Implementeret som det genbrugelige workflow `.gitea/workflows/code-metrics.yml` med en tæller, en Cobertura-læser, en SVG-renderer og offline-tests; endnu ikke brugt i et andet repository (MIL-010) ([vejledning](CodeMetrics.da)) | +| Kodemålinger og dækningsbadges | Implementeret som det genbrugelige workflow `.gitea/workflows/code-metrics.yml` med en tæller, en Cobertura-læser, en SVG-renderer og offline-tests; afprøvet i et kasserepository, endnu ikke brugt i et projektrepository (MIL-010) ([vejledning](CodeMetrics.da)) | | Versioneret udgivelse og publicering | Udgivelsesproces, changelog og opgraderingsvejledning er defineret; udgivelsesworkflowet findes, og `v1.0.0` er udgivet (MIL-004) ([vejledning](Upgrading.da)) | diff --git a/docs/wiki/Capabilities.md b/docs/wiki/Capabilities.md index 604823b..9096db2 100644 --- a/docs/wiki/Capabilities.md +++ b/docs/wiki/Capabilities.md @@ -16,5 +16,5 @@ Rows that say Implemented or Guides written exist today. The rest is planned and | Publish a repository wiki from `docs/wiki/` through a called workflow | Implemented as the reusable workflow `.gitea/workflows/publish-wiki-pages.yml`, with offline tests; called by every repository that publishes a wiki, and this repository's own wiki is in `docs/wiki/` (MIL-008, MIL-009) | | Shared quality checks (Python and shell) | Implemented as the reusable workflow `.gitea/workflows/quality-checks.yml`, with offline tests and a trial in a second organization; adopted in RepoFoundry ([guide](QualityChecksPythonShell)) | | Shared quality checks (C and C++, C#) | Implemented as the reusable workflows `.gitea/workflows/quality-checks-cpp.yml` and `.gitea/workflows/quality-checks-csharp.yml` with exact tool versions and offline tests; not yet used in another repository (MIL-007) ([C and C++ guide](QualityChecksCpp), [C# guide](QualityChecksCSharp)) | -| Code metrics and coverage badges | Implemented as the reusable workflow `.gitea/workflows/code-metrics.yml` with a counter, a Cobertura reader, an SVG renderer and offline tests; not yet used in another repository (MIL-010) ([guide](CodeMetrics)) | +| Code metrics and coverage badges | Implemented as the reusable workflow `.gitea/workflows/code-metrics.yml` with a counter, a Cobertura reader, an SVG renderer and offline tests; tried in a throwaway repository, not yet used in a project repository (MIL-010) ([guide](CodeMetrics)) | | Versioned release and publishing | Release process, changelog and upgrade guide defined; the release workflow exists and `v1.0.0` is released (MIL-004) ([guide](Upgrading)) | diff --git a/docs/wiki/Capabilities.ms.md b/docs/wiki/Capabilities.ms.md index 1eefede..5c0f154 100644 --- a/docs/wiki/Capabilities.ms.md +++ b/docs/wiki/Capabilities.ms.md @@ -1,4 +1,4 @@ - + > Ini ialah terjemahan [`Capabilities`](Capabilities). Halaman bahasa Inggeris ialah teks yang sah, dan perubahan di sini akan ditimpa pada penerbitan wiki yang seterusnya. @@ -16,5 +16,5 @@ Baris yang menyatakan Dilaksanakan atau Panduan telah ditulis wujud hari ini. Se | Menerbitkan wiki repositori daripada `docs/wiki/` melalui aliran kerja yang dipanggil | Dilaksanakan sebagai aliran kerja boleh guna semula `.gitea/workflows/publish-wiki-pages.yml`, dengan ujian luar talian; dipanggil oleh setiap repositori yang menerbitkan wiki, dan wiki repositori ini sendiri ada dalam `docs/wiki/` (MIL-008, MIL-009) | | Semakan kualiti bersama (Python dan shell) | Dilaksanakan sebagai aliran kerja boleh guna semula `.gitea/workflows/quality-checks.yml`, dengan ujian luar talian dan percubaan dalam organisasi kedua; digunakan dalam RepoFoundry ([panduan](QualityChecksPythonShell.ms)) | | Semakan kualiti bersama (C dan C++, C#) | Dilaksanakan sebagai aliran kerja boleh guna semula `.gitea/workflows/quality-checks-cpp.yml` dan `.gitea/workflows/quality-checks-csharp.yml` dengan versi alat yang tepat dan ujian luar talian; belum digunakan dalam repositori lain (MIL-007) ([panduan C dan C++](QualityChecksCpp.ms), [panduan C#](QualityChecksCSharp.ms)) | -| Metrik kod dan lencana liputan | Dilaksanakan sebagai aliran kerja boleh guna semula `.gitea/workflows/code-metrics.yml` dengan pengira, pembaca Cobertura, pemaparan SVG dan ujian luar talian; belum digunakan dalam repositori lain (MIL-010) ([panduan](CodeMetrics.ms)) | +| Metrik kod dan lencana liputan | Dilaksanakan sebagai aliran kerja boleh guna semula `.gitea/workflows/code-metrics.yml` dengan pengira, pembaca Cobertura, pemaparan SVG dan ujian luar talian; dicuba dalam repositori sekali pakai, belum digunakan dalam repositori projek (MIL-010) ([panduan](CodeMetrics.ms)) | | Keluaran berversi dan penerbitan | Proses keluaran, changelog dan panduan naik taraf telah ditakrifkan; aliran kerja keluaran sudah ada dan `v1.0.0` telah dikeluarkan (MIL-004) ([panduan](Upgrading.ms)) | -- 2.54.0 From 292886531e740e0ba090a16417d07941d8dc8bbc Mon Sep 17 00:00:00 2001 From: Jens Tirsvad Nielsen Date: Wed, 14 Oct 2026 14:05:29 +0800 Subject: [PATCH 09/10] Resolve pending commit links --- docs/adr/adr-0010-code-metrics-badges-contract.md | 5 +++-- docs/milestones/mil-010-code-metrics-badges.md | 3 ++- docs/sqa/reviews/rc-069-uc-008.md | 3 ++- docs/sqa/reviews/rc-070-us-001-metrics.md | 3 ++- docs/sqa/reviews/rc-071-ucd-001-metrics.md | 3 ++- docs/sqa/reviews/rc-072-adr-0010.md | 3 ++- docs/sqa/reviews/rc-073-adr-0010-artifact.md | 3 ++- docs/sqa/reviews/rc-074-mil-010-results.md | 3 ++- docs/sqa/traceability-matrix.md | 5 +++-- docs/uc-008/uc.md | 3 ++- docs/use-case-diagram.md | 3 ++- docs/user-stories.md | 3 ++- 12 files changed, 26 insertions(+), 14 deletions(-) diff --git a/docs/adr/adr-0010-code-metrics-badges-contract.md b/docs/adr/adr-0010-code-metrics-badges-contract.md index 35fd2a2..b8a93b9 100644 --- a/docs/adr/adr-0010-code-metrics-badges-contract.md +++ b/docs/adr/adr-0010-code-metrics-badges-contract.md @@ -9,8 +9,8 @@ ## Version History | Date | Status | Author | Reviewer | Change | Commit | | --- | --- | --- | --- | --- | --- | -| 2026-10-14 | Deprecated | Jens Tirsvad Nielsen | AI as stand-in for S01 | Initial version; reviewed (RC-072) | pending | -| 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) | pending | +| 2026-10-14 | Deprecated | Jens Tirsvad Nielsen | AI as stand-in for S01 | Initial version; reviewed (RC-072) | [0dc5132] | +| 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] | Allowed Status values: `Proposed`, `Accepted`, `Rejected`, `Deprecated`, `Superseded by ADR-NNNN`. @@ -214,3 +214,4 @@ The rules that follow from it: [US-001]: ../user-stories.md [ADR-0005]: ./adr-0005-quality-check-contract.md [ADR-0006]: ./adr-0006-release-process-and-version-pinning.md +[0dc5132]: https://git.tirsystem.com/TirSystem/github-action/commit/0dc5132a2ea419ba05035ee3594cec63335c240b diff --git a/docs/milestones/mil-010-code-metrics-badges.md b/docs/milestones/mil-010-code-metrics-badges.md index 6055f10..85c9a8c 100644 --- a/docs/milestones/mil-010-code-metrics-badges.md +++ b/docs/milestones/mil-010-code-metrics-badges.md @@ -10,7 +10,7 @@ | Date | Status | Author | Reviewer | Change | Commit | | --- | --- | --- | --- | --- | --- | | 2026-10-14 | Deprecated | Jens Tirsvad Nielsen | AI as stand-in for S01 | Initial version; reviewed (RC-068) | [5002450] | -| 2026-10-14 | Accepted | Jens Tirsvad Nielsen | AI as stand-in for S01 | Results of tasks 3, 6 and 8 added: the report as an artifact of the caller (ADR-0010), adoption in a throwaway repository with seeded change and failure; reviewed (RC-074) | pending | +| 2026-10-14 | Accepted | Jens Tirsvad Nielsen | AI as stand-in for S01 | Results of tasks 3, 6 and 8 added: the report as an artifact of the caller (ADR-0010), adoption in a throwaway repository with seeded change and failure; reviewed (RC-074) | [5c5a1d1] | --- @@ -174,3 +174,4 @@ user to delete. [ADR-0005]: ../adr/adr-0005-quality-check-contract.md [ADR-0006]: ../adr/adr-0006-release-process-and-version-pinning.md [5002450]: https://git.tirsystem.com/TirSystem/github-action/commit/50024505fe788b1c5e3021c0d1fc3d079590070e +[5c5a1d1]: https://git.tirsystem.com/TirSystem/github-action/commit/5c5a1d19d96a6979b4f6141e736957174ba686a9 diff --git a/docs/sqa/reviews/rc-069-uc-008.md b/docs/sqa/reviews/rc-069-uc-008.md index 6798c0c..1fd5696 100644 --- a/docs/sqa/reviews/rc-069-uc-008.md +++ b/docs/sqa/reviews/rc-069-uc-008.md @@ -9,7 +9,7 @@ ## Version History | Date | Status | Author | Reviewer | Change | Commit | | --- | --- | --- | --- | --- | --- | -| 2026-10-14 | Accepted | Jens Tirsvad Nielsen | AI as stand-in for S01 | Initial version; accepted | pending | +| 2026-10-14 | Accepted | Jens Tirsvad Nielsen | AI as stand-in for S01 | Initial version; accepted | [9ca9673] | --- @@ -53,3 +53,4 @@ Go - all applicable criteria pass. The AI records the decision as stand-in for S [UC-008]: ../../uc-008/uc.md [QC-UC-001]: ../../../framework/qc/artifacts/qc-use-case.md +[9ca9673]: https://git.tirsystem.com/TirSystem/github-action/commit/9ca9673b00351cf913966f347c4da09890ed3f1f diff --git a/docs/sqa/reviews/rc-070-us-001-metrics.md b/docs/sqa/reviews/rc-070-us-001-metrics.md index 25ae7ee..25a95b8 100644 --- a/docs/sqa/reviews/rc-070-us-001-metrics.md +++ b/docs/sqa/reviews/rc-070-us-001-metrics.md @@ -9,7 +9,7 @@ ## Version History | Date | Status | Author | Reviewer | Change | Commit | | --- | --- | --- | --- | --- | --- | -| 2026-10-14 | Accepted | Jens Tirsvad Nielsen | AI as stand-in for S01 | Initial version; accepted | pending | +| 2026-10-14 | Accepted | Jens Tirsvad Nielsen | AI as stand-in for S01 | Initial version; accepted | [9ca9673] | --- @@ -48,3 +48,4 @@ Go - all criteria pass. The AI records the decision as stand-in for S01; there i [US-001]: ../../user-stories.md [QC-US-001]: ../../../framework/qc/artifacts/qc-user-story.md +[9ca9673]: https://git.tirsystem.com/TirSystem/github-action/commit/9ca9673b00351cf913966f347c4da09890ed3f1f diff --git a/docs/sqa/reviews/rc-071-ucd-001-metrics.md b/docs/sqa/reviews/rc-071-ucd-001-metrics.md index dc50bdf..db7a2c4 100644 --- a/docs/sqa/reviews/rc-071-ucd-001-metrics.md +++ b/docs/sqa/reviews/rc-071-ucd-001-metrics.md @@ -9,7 +9,7 @@ ## Version History | Date | Status | Author | Reviewer | Change | Commit | | --- | --- | --- | --- | --- | --- | -| 2026-10-14 | Accepted | Jens Tirsvad Nielsen | AI as stand-in for S01 | Initial version; accepted | pending | +| 2026-10-14 | Accepted | Jens Tirsvad Nielsen | AI as stand-in for S01 | Initial version; accepted | [9ca9673] | --- @@ -49,3 +49,4 @@ Go - all criteria pass. The AI records the decision as stand-in for S01; there i [UCD-001]: ../../use-case-diagram.md [QC-UCD-001]: ../../../framework/qc/artifacts/qc-use-case-diagram.md +[9ca9673]: https://git.tirsystem.com/TirSystem/github-action/commit/9ca9673b00351cf913966f347c4da09890ed3f1f diff --git a/docs/sqa/reviews/rc-072-adr-0010.md b/docs/sqa/reviews/rc-072-adr-0010.md index 70c7f1f..0611fc9 100644 --- a/docs/sqa/reviews/rc-072-adr-0010.md +++ b/docs/sqa/reviews/rc-072-adr-0010.md @@ -9,7 +9,7 @@ ## Version History | Date | Status | Author | Reviewer | Change | Commit | | --- | --- | --- | --- | --- | --- | -| 2026-10-14 | Accepted | Jens Tirsvad Nielsen | AI as stand-in for S01 | Initial version; accepted | pending | +| 2026-10-14 | Accepted | Jens Tirsvad Nielsen | AI as stand-in for S01 | Initial version; accepted | [aa4ca0f] | --- @@ -52,3 +52,4 @@ Go - all applicable criteria pass; the decisions of the user of 2026-10-14 are r [ADR-0010]: ../../adr/adr-0010-code-metrics-badges-contract.md [QC-ADR-001]: ../../../framework/qc/artifacts/qc-adr.md +[aa4ca0f]: https://git.tirsystem.com/TirSystem/github-action/commit/aa4ca0f897b7ea20d90745aa4b7d7a215df123a8 diff --git a/docs/sqa/reviews/rc-073-adr-0010-artifact.md b/docs/sqa/reviews/rc-073-adr-0010-artifact.md index af269f6..aca9472 100644 --- a/docs/sqa/reviews/rc-073-adr-0010-artifact.md +++ b/docs/sqa/reviews/rc-073-adr-0010-artifact.md @@ -9,7 +9,7 @@ ## Version History | Date | Status | Author | Reviewer | Change | Commit | | --- | --- | --- | --- | --- | --- | -| 2026-10-14 | Accepted | Jens Tirsvad Nielsen | AI as stand-in for S01 | Initial version; accepted | pending | +| 2026-10-14 | Accepted | Jens Tirsvad Nielsen | AI as stand-in for S01 | Initial version; accepted | [0dc5132] | --- @@ -52,3 +52,4 @@ Go - all applicable criteria pass; the amendment keeps the decisions of the user [ADR-0010]: ../../adr/adr-0010-code-metrics-badges-contract.md [QC-ADR-001]: ../../../framework/qc/artifacts/qc-adr.md +[0dc5132]: https://git.tirsystem.com/TirSystem/github-action/commit/0dc5132a2ea419ba05035ee3594cec63335c240b diff --git a/docs/sqa/reviews/rc-074-mil-010-results.md b/docs/sqa/reviews/rc-074-mil-010-results.md index 6c759fb..53978b4 100644 --- a/docs/sqa/reviews/rc-074-mil-010-results.md +++ b/docs/sqa/reviews/rc-074-mil-010-results.md @@ -9,7 +9,7 @@ ## Version History | Date | Status | Author | Reviewer | Change | Commit | | --- | --- | --- | --- | --- | --- | -| 2026-10-14 | Accepted | Jens Tirsvad Nielsen | AI as stand-in for S01 | Initial version; accepted | pending | +| 2026-10-14 | Accepted | Jens Tirsvad Nielsen | AI as stand-in for S01 | Initial version; accepted | [5c5a1d1] | --- @@ -48,3 +48,4 @@ Go - the recorded results support the criteria 1 to 8; criterion 9 is decided af [MIL-010]: ../../milestones/mil-010-code-metrics-badges.md [QC-MIL-001]: ../../../framework/qc/artifacts/qc-milestones-gateways.md +[5c5a1d1]: https://git.tirsystem.com/TirSystem/github-action/commit/5c5a1d19d96a6979b4f6141e736957174ba686a9 diff --git a/docs/sqa/traceability-matrix.md b/docs/sqa/traceability-matrix.md index cda5ef3..2da5dd3 100644 --- a/docs/sqa/traceability-matrix.md +++ b/docs/sqa/traceability-matrix.md @@ -9,8 +9,8 @@ ## Version History | Date | Status | Author | Reviewer | Change | Commit | | --- | --- | --- | --- | --- | --- | -| 2026-10-14 | Proposed | Jens Tirsvad Nielsen | S01 | Added RC-073 (ADR-0010: the coverage report as an artifact, MIL-010 task 6) | pending | -| 2026-10-14 | Proposed | Jens Tirsvad Nielsen | S01 | Added RC-074 (MIL-010 results of tasks 3, 6 and 8) | pending | +| 2026-10-14 | Proposed | Jens Tirsvad Nielsen | S01 | Added RC-073 (ADR-0010: the coverage report as an artifact, MIL-010 task 6) | [5c5a1d1] | +| 2026-10-14 | Proposed | Jens Tirsvad Nielsen | S01 | Added RC-074 (MIL-010 results of tasks 3, 6 and 8) | [5c5a1d1] | --- @@ -248,3 +248,4 @@ updated whenever an artifact instance is created or reviewed. [ADR-0006]: ../adr/adr-0006-release-process-and-version-pinning.md [UC-005]: ../uc-005/uc.md [ADR-0005]: ../adr/adr-0005-quality-check-contract.md +[5c5a1d1]: https://git.tirsystem.com/TirSystem/github-action/commit/5c5a1d19d96a6979b4f6141e736957174ba686a9 diff --git a/docs/uc-008/uc.md b/docs/uc-008/uc.md index 442d4a1..fb075d0 100644 --- a/docs/uc-008/uc.md +++ b/docs/uc-008/uc.md @@ -9,7 +9,7 @@ ## 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-069) | pending | +| 2026-10-14 | Accepted | Jens Tirsvad Nielsen | AI as stand-in for S01 | Initial version; reviewed (RC-069) | [9ca9673] | --- @@ -41,3 +41,4 @@ A Software Engineer pushes a change to a repository that calls the shared code-m [US-001]: ../user-stories.md [SA-001]: ../stakeholder-analysis.md [MIL-010]: ../milestones/mil-010-code-metrics-badges.md +[9ca9673]: https://git.tirsystem.com/TirSystem/github-action/commit/9ca9673b00351cf913966f347c4da09890ed3f1f diff --git a/docs/use-case-diagram.md b/docs/use-case-diagram.md index 7a7ae5f..91d7edf 100644 --- a/docs/use-case-diagram.md +++ b/docs/use-case-diagram.md @@ -10,7 +10,7 @@ | Date | Status | Author | Reviewer | Change | Commit | | --- | --- | --- | --- | --- | --- | | 2026-10-10 | Deprecated | Jens Tirsvad Nielsen | S01 | Added the use case Consume a Workflow Update (UC-007) (RC-045) | [4cf52af] | -| 2026-10-14 | Accepted | Jens Tirsvad Nielsen | AI as stand-in for S01 | Added the use case Show Code Metrics and Coverage Badges (UC-008); reviewed (RC-071) | pending | +| 2026-10-14 | Accepted | Jens Tirsvad Nielsen | AI as stand-in for S01 | Added the use case Show Code Metrics and Coverage Badges (UC-008); reviewed (RC-071) | [9ca9673] | --- @@ -110,3 +110,4 @@ UC4 ..> UC1 : <> [UC-007]: ./uc-007/uc.md [UC-008]: ./uc-008/uc.md [4cf52af]: https://git.tirsystem.com/TirSystem/github-action/commit/4cf52afe00f546c6b20424fb8dfc12796ffd9296 +[9ca9673]: https://git.tirsystem.com/TirSystem/github-action/commit/9ca9673b00351cf913966f347c4da09890ed3f1f diff --git a/docs/user-stories.md b/docs/user-stories.md index 9f09556..a9f0992 100644 --- a/docs/user-stories.md +++ b/docs/user-stories.md @@ -10,7 +10,7 @@ | Date | Status | Author | Reviewer | Change | Commit | | --- | --- | --- | --- | --- | --- | | 2026-10-11 | Deprecated | Jens Tirsvad Nielsen | S01 | US-001.16 and US-001.17 reviewed (RC-057) and accepted; the Purpose and Scope names the later stories | [1bc56f8] | -| 2026-10-14 | Accepted | Jens Tirsvad Nielsen | AI as stand-in for S01 | Added US-001.18 to US-001.20 (code metrics and coverage badges, MIL-010 task 1); reviewed (RC-070) | pending | +| 2026-10-14 | Accepted | Jens Tirsvad Nielsen | AI as stand-in for S01 | Added US-001.18 to US-001.20 (code metrics and coverage badges, MIL-010 task 1); reviewed (RC-070) | [9ca9673] | --- @@ -319,3 +319,4 @@ behaviour is tested rather than built. [UC-008]: ./uc-008/uc.md [MIL-010]: ./milestones/mil-010-code-metrics-badges.md [1bc56f8]: https://git.tirsystem.com/TirSystem/github-action/commit/1bc56f8b771531f221f218fc00b147ecc73c051e +[9ca9673]: https://git.tirsystem.com/TirSystem/github-action/commit/9ca9673b00351cf913966f347c4da09890ed3f1f -- 2.54.0 From a795f3b03ee5bd309de8331f5fefd0e7c678e4b6 Mon Sep 17 00:00:00 2001 From: Jens Tirsvad Nielsen Date: Wed, 14 Oct 2026 14:10:45 +0800 Subject: [PATCH 10/10] Re-run the checks: the status of the previous head stayed pending -- 2.54.0