Skip to content

Coverage baseline auto commit-back — CI persists/ratchets the baseline (opt-in, on top of #282) #372

Description

@rucka

Story Statement

As a maintainer running the coverage guardrail
I want CI to persist a raised coverage baseline back to tech/coverage-baseline.md — opt-in, ratcheting only upward
So that improvements are locked in automatically instead of relying on someone remembering to bump the file by hand, while the human-committed default stays available for projects that want the baseline under review

Where: .github/workflows/ci.yml's Coverage guardrail step + the shipped coverage-gate.sh, driven by a flag in way-of-working.md › Quality Gates.

Epic Context

Parent Epic: nessuna — follow-up di #282 (coverage baseline + CI guardrail). Apparteneva a #210, chiusa il 26/07: serve una decisione di riallocazione, non un'assegnazione automatica.
Status: Refined
Priority: P2 (Could-Have) — era P3/deferred nel piano; resta la meno urgente delle tre del batch.

Status Workflow

  • Refined: Story is detailed, estimated, and ready for development
  • In Progress: Story is actively being developed
  • Done: Story delivered and accepted

Classification

risk:yellow · cost:green · coupling: not assessed — no domain artifacts

Matrix — per dimension
Dimension Tier Source Note
Service/domain criticality yellow KB default no Criticality Table declared ⇒ Medium (D21)
Change/diff risk yellow story scope touches CI; a wrong loop guard costs every subsequent run, and a write-back needs a token
Business impact green subdomain class Integration & Process Standardization (Supporting); no product surface
Security relevance yellow path heuristic introduces a write credential into CI and a commit from an automated actor — authorization surface, even though it grants nothing to a PR author
Coupling balance not assessed absent no new integration point

Acceptance Criteria

Functional Requirements

Given-When-Then Format:

  1. Given Coverage baseline commit-back: disabled (the default, and the framework default)
    When CI runs with coverage above the baseline
    Then nothing is written and behaviour is byte-identical to today — the baseline stays human-committed (option A)

  2. Given the flag is enabled and a run measures line coverage above a baseline.<type> value
    When the guardrail step completes
    Then the new value is written to .pair/adoption/tech/coverage-baseline.md and committed back, and the commit message marks it as automated

  3. Given the flag is enabled and coverage is equal to or below the baseline
    When the guardrail step completes
    Then nothing is committed — the ratchet only ever moves up, and a drop still fails the gate exactly as now

  4. Given a commit-back has just landed
    When CI reacts to it
    Then it does not start another coverage run that commits again — the loop terminates, proven by an explicit skip condition, not by luck

  5. Given the run is a pull request rather than a push to the base branch
    When coverage is above the baseline
    Then the behaviour is the one this story explicitly decides and documents (write to the PR branch, or defer to the post-merge run) — never left implicit, because a PR from a fork has no write access

  6. Given the write-back is refused (missing token, protected branch, insufficient scope)
    When the step runs
    Then the run degrades to a warning naming the reason and the gate's verdict is unchanged — a persistence failure must never turn a passing gate red, nor a failing one green

Business Rules

  • Ratchet is monotonic: the file is only ever raised. Lowering a baseline stays a deliberate human commit.
  • Default off: the framework default ships disabled; pair's own opt-in is a project-level line in way-of-working.md, exactly as Coverage guardrail itself is.
  • The gate's verdict and the persistence are independent: coverage-gate.sh decides pass/fail; the write-back is a side effect that can fail alone.
  • Least privilege: the write credential is scoped to what the commit needs and is documented; no broader token is introduced for convenience.
  • Config format unchanged: ^baseline.<type>=<int> lines (today baseline.shared=84, baseline.frontend=19) — the writer edits values in place, never rewrites the surrounding markdown.

Edge Cases and Error Handling

  • Invalid Input: a malformed/absent coverage-summary.json keeps the conservative fail-safe (TIER=red ⇒ fail) and commits nothing.
  • System Errors: a push rejected by branch protection → warning + unchanged verdict (AC6).
  • Boundary Conditions: coverage exactly equal to the baseline ⇒ no write (AC3). A type present in the report but absent from the config ⇒ no write, reported.
  • Exceptional Scenarios: two concurrent runs both raising the same baseline — the second must not clobber a higher value written by the first (re-read before write, or push with lease semantics).

Definition of Done Checklist

  • All acceptance criteria implemented and verified
  • The loop-termination condition (AC4) is demonstrated, not asserted — a real run, or a smoke test over the skip predicate
  • AC5's PR-vs-push decision is recorded in an ADL, not only in the workflow YAML
  • Token/permission requirement documented where a consumer will read it (KB guideline + the adoption line)
  • coverage-gate.sh logic covered by unit tests in its owning package; the workflow step stays a thin entrypoint (ADL 2026-07-13)
  • pnpm quality-gate green
  • PR follows pr-template.md

Dependencies

Notes

Origin: filed during #281's review round 2 as the deferred half of the coverage work. The plan classified it P3, deliberately deferred ("da riprendere solo dopo il lavoro ad alto valore") — it enters this batch because it is one of the few residual stories that is file-disjoint from the six parked PRs, not because its priority rose.

Task Breakdown

  • T-1: Decide + record the PR-vs-push behaviour and the credential model (ADL)
  • T-2: Ratchet writer in the owning package (monotonic, in-place value edit)
  • T-3: Wire the opt-in flag and the CI step; keep the step a thin entrypoint
  • T-4: Loop-termination skip condition + its demonstration
  • T-5: Degradation path when the write is refused
  • T-6: Document the flag, the token requirement and the default in KB + adoption

Dependency Graph

T-1 ──┬── T-2 ── T-3 ──┬── T-4
      │                └── T-5
      └── T-6

AC Coverage

AC Tasks
AC-1 (default off, unchanged) T-3, T-6
AC-2 (raise ⇒ commit back) T-2, T-3
AC-3 (never lowers) T-2
AC-4 (no CI loop) T-4
AC-5 (PR vs push decided) T-1, T-3
AC-6 (refusal degrades) T-5

T-1: Decide and record PR-vs-push behaviour + credential model

Priority: P0 | Estimated Hours: 2h | Bounded Context: Integration & Process Standardization

Summary: The two decisions that shape everything else: does a PR run write back, and with what credential.

Description: A fork PR has no write access, and after #234 the base branch is protected. Decide: write only on the post-merge push to the base branch (simplest, protection-compatible), or on the PR branch (immediate, but fails on forks). Record it as an ADL with the rejected option and why — this is the decision a later reader will question.

T-2: Monotonic ratchet writer

Priority: P0 | Estimated Hours: 3h | Bounded Context: Integration & Process Standardization

Summary: Given a measured coverage map and the config, produce the updated config — raising only.

Description: Lives in the owning package as a tested module (ADL 2026-07-13), not in the shell script. Edits ^baseline.<type>= values in place, leaving the markdown around them untouched. Re-reads before writing so a concurrent higher value is not clobbered. Unit-tested for: raise, equal, drop, unknown type, malformed config.

T-3: Flag + CI wiring

Priority: P0 | Estimated Hours: 2h | Bounded Context: Integration & Process Standardization

Summary: Coverage baseline commit-back flag read from the adoption; the CI step delegates to the module.

T-4: Loop termination

Priority: P0 | Estimated Hours: 2h | Bounded Context: Integration & Process Standardization

Summary: The automated commit must not trigger another coverage run that commits again.

Description: Explicit skip predicate (commit-message marker or actor check) — not [skip ci] alone if the coverage job is what must be skipped. Demonstrate it: a real sequence, or a smoke assertion over the predicate. An infinite CI loop is the failure mode that makes this story net-negative.

T-5: Degradation on refused write

Priority: P1 | Estimated Hours: 1h | Bounded Context: Integration & Process Standardization

Summary: Missing token / protected branch / insufficient scope ⇒ warning, verdict untouched.

T-6: Documentation

Priority: P1 | Estimated Hours: 1h | Bounded Context: Integration & Process Standardization

Summary: The flag, its default (disabled), the token requirement and the PR-vs-push decision, in the KB guideline and the adoption line — where a consumer reads them, not only in the workflow YAML.

Metadata

Metadata

Assignees

Labels

risk:yellowClassification: medium risk tieruser storyWork item representing a user story

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions