diff --git a/.serena/memories/core.md b/.serena/memories/core.md index d21ad1f2e..9489672f6 100644 --- a/.serena/memories/core.md +++ b/.serena/memories/core.md @@ -17,9 +17,9 @@ Read on demand, never all of them. session against this repo. - `mem:workflow/landing-loop` — landing a branch; before "repairing" `land`, the lease, the CI wait or their suites. -- `mem:workflow/sonar-gate-race` — Sonar refuses your branch; before treating a - `final` failure on `sonar-gate` as yours, or reading a check-run's annotations - as the whole finding list. +- `mem:workflow/sonar-scope` — Sonar refuses your branch; reading or changing + `sonar-gate`; a Sonar verdict looks wrong on a SHA; before treating a `final` + failure as trunk's, or a check-run's annotations as the whole finding list. - `mem:session-transcript-access` — asked to read chat history or another session; before probing a session API or credential. - `mem:github-access` — any GitHub op; before claiming the toolchain or CI diff --git a/.serena/memories/workflow/sonar-gate-race.md b/.serena/memories/workflow/sonar-gate-race.md deleted file mode 100644 index 412d1b828..000000000 --- a/.serena/memories/workflow/sonar-gate-race.md +++ /dev/null @@ -1,84 +0,0 @@ -# Sonar has been red for everyone, and `final` normally never finds out - -Measured 2026-08-22 on PR #638, with `mise exec -- gh`. - -## The finding - -`sonar-gate` runs inside the `final` job and reads the `SonarCloud Code Analysis` -check-run **by name**. Absent is a pass, deliberately (`mise-tasks/sonar-gate.sh` -states the reason: an analyzer that declines to grade a PR produces no run at -all, and failing on that would wedge every PR it has no opinion about). - -**`final` normally completes before Sonar has even started.** Three consecutive -merged PRs, from the check-runs API: - -| PR | `final` completed | Sonar STARTED | `final` | -| ---- | ----------------- | ---------------- | ------- | -| #648 | 04:56:37Z | 04:57:06Z (+29s) | success | -| #647 | 04:46:24Z | 04:46:52Z (+28s) | success | -| #646 | 04:14:51Z | 04:15:16Z (+25s) | success | - -Every one of those landed with **Sonar `failure`** on its head and `final` -green, because the gate read _absent_. The verdict a PR gets from this gate is -therefore a function of how long its own CI takes, not of what the analyzer -found. That is not a gate; it is a coin toss with a bias. - -## What it is hiding - -`main` itself fails Sonar — **C Security Rating on New Code** — on roughly every -other trunk commit. Nothing surfaces it: AGENTS.md forbids push-to-`main` -workflows, so no run on `main` reads that verdict, and on PRs the race above -swallows it. - -PR #638 was the first branch slow enough (a 300+ file diff plus the `perf` job) -for Sonar to answer before `final` ran. It reported **D Security Rating on New -Code** and blocked the landing. Nothing about that PR made Sonar red; it made -Sonar _audible_. - -## Before you "fix" a Sonar failure on your branch - -1. Check `main` first: - `gh api repos///commits//check-runs --jq '.check_runs[] | select(.name=="SonarCloud Code Analysis") | .conclusion'` - If trunk is red, the failure is not yours — AGENTS.md's "red on the base - branch too" exemption applies, and the remedy is an issue, not a patch. -2. Compare `final`'s `completed_at` against Sonar's `started_at` on a recently - merged PR. If `final` finished first, that PR did not pass the gate — it - outran it. - -## Reading the findings at all - -The dashboard is auth-walled (a private project answers `{"errors":[{"msg": -"Project doesn't exist"}]}` to an unauthenticated caller — a denial wearing a -404, so an empty `api/issues/search` result there is NOT a zero). No -`SONAR_TOKEN` exists in the container or in CI, since the SonarCloud GitHub App -does automatic analysis and needs none. - -What DOES work is the check-run's **annotations** sub-resource, which the GitHub -MCP `get_check_run` tool does not expose: - - mise exec -- gh api repos///check-runs//annotations --paginate - -**Capped at 50 by GitHub, and Sonar truncates to it silently.** Prove truncation -by arithmetic rather than trusting the list: on #638 one run showed 50 -annotations of which 42 were one rule; fixing that rule left **50 again**, not 8. -So ≥92 issues existed and the list is a window, not an inventory. A -security-rated issue can sit entirely outside it — every one of the 50 visible on -#638 was a maintainability rule while the failing condition was Security. - -Unioning annotations across every SHA on the PR does not fix this: only pushed -heads are analysed, and each posts the same truncated window. - -## The two issues this deserves - -Neither is a branch's job to fix in passing: - -- **The race.** A gate whose verdict depends on which job finishes first is not - a verdict. Either `final` waits for the analyzer (bounded, with absent still a - pass after the wait) or the analyzer stops being read there at all. Today the - bounded retry in `ci.yml` only re-runs `sonar-gate` when it answers exit 3 - (pending) — it never fires, because absent (pass) is returned first. -- **The standing red.** Trunk sits at C Security Rating with nobody watching. - Whatever that is, it predates any branch reading this memory. - -Related: `mem:workflow/landing-loop` for the rest of the landing sequence, and -`mem:github-access` before concluding an API is unreachable. diff --git a/.serena/memories/workflow/sonar-scope.md b/.serena/memories/workflow/sonar-scope.md new file mode 100644 index 000000000..16ef45714 --- /dev/null +++ b/.serena/memories/workflow/sonar-scope.md @@ -0,0 +1,92 @@ +# SonarCloud: two analyses, one check-run name + +Read when: reading, trusting, or changing `sonar-gate`; a Sonar verdict looks +wrong on a SHA; or someone claims the analyzer's findings cannot be read. +Measured 2026-08-22 (CLOUD-897, CLOUD-528). + +## The one fact everything else follows from + +SonarCloud posts **two different analyses under the identical check-run name** +`SonarCloud Code Analysis`. Only `details_url` tells them apart: + +| scope | `details_url` ends | when it posts | +| ---------------- | ------------------ | -------------------------------------------- | +| the pull request | `…&pullRequest=N` | seconds after a push to the PR branch | +| the branch | `…&branch=main` | seconds after the merge, on `main`'s new tip | + +**Fast-forward puts both on one SHA.** `main`'s tip is byte-identical to the PR +head it landed, so a landed commit accumulates the PR verdict and then main's. +`sonar-gate` matches by NAME only and takes the latest run, so on any landed +SHA it reports **main's** verdict as that commit's: + +``` +$ SHA= REPO=button-inc/batten mise run sonar-gate +failure SonarCloud Code Analysis → exit 1 +``` + +That is trunk's standing `C` (CLOUD-528), not the commit's. Any `verify` over a +HEAD that has already landed hits it. + +## There is no race with `final` + +A PR-scoped analysis starts within ~0–20s of the push and finishes inside 30s, +including on a 338-file diff. `final` runs after the whole matrix, minutes +later. CLOUD-897 was filed on a table of "analyzer started 25s AFTER `final` +finished" — every one of those rows was the **branch** analysis posting after +the merge. Do not re-derive the race; the timestamps look exactly like one. + +## The analyzer does not grade the head that lands + +Across the last 14 merged PRs (#631–#650), **every merged head carried exactly +one sonar run and it was `branch=main`** — not one carried a PR-scoped analysis. +Intermediate SHAs on the same branches were graded in under 30s each. #638 is +the clean demonstration: `c3da83c` and `c757a33` each graded within 20s, and its +merged head `5c510fa` — head for 18 minutes, `final` green — was never graded. + +So absent-is-a-pass (CLOUD-441's deliberate choice, and correct) fires on the +one SHA that matters. Scoping the gate is necessary and not sufficient. + +## Trunk being red does NOT make your failure trunk's + +The memory this one supersedes (`workflow/sonar-gate-race`, deleted) advised +checking `main` first and treating a Sonar refusal as not-yours if trunk is red. That is wrong here and +would have waved through the one real finding this repo has seen: #638's `D` was +`pullRequest=638`, computed on its own new code, while trunk's `C` is +`branch=main` and cannot enter a PR verdict. Read the scope, not `main`. + +## Reading the findings — what works and what lies + +- The dashboard API answers an unauthenticated caller + `{"errors":[{"msg":"Project doesn't exist"}]}` — **a denial wearing a 404**. + An empty `api/issues/search` from there is not a zero. +- Check-run **annotations** are reachable and the GitHub MCP `get_check_run` + tool does not expose them: + `gh api repos/button-inc/batten/check-runs//annotations --paginate` +- Annotations are **capped at 50 and truncated silently**. Provable by + arithmetic: one run showed 50 of which 42 were a single rule; clearing that + rule left **50 again**, not 8 — so ≥92 existed. A security-rated issue can sit + wholly outside the window, which is what happened. +- Unioning annotations across every SHA on a PR does not defeat the cap: each + analysed head posts the same truncated window. +- A **branch** analysis carries `annotations_count: 0`, so that route cannot + reach `main`'s verdict at all. It is PR-only, and truncated even there. + +There is no `SONAR_TOKEN` and no Sonar step in any workflow — analysis arrives +through the SonarCloud GitHub App's automatic analysis, which is why none of it +is under this repository's control or observation. + +## Nothing in the landing path reads any of this today + +`5c510fa` removed `sonar-gate` from `final` and from `verify:gated` — on the +race diagnosis corrected above. The task still exists and still runs by hand. +Restoring it is CLOUD-897's, and it must not be restored unfixed. + +## The probe + +``` +gh api "repos/button-inc/batten/commits/$SHA/check-runs?per_page=100" \ + --jq '.check_runs[]|select(.name|test("Sonar"))|"\(.started_at) \(.conclusion) \(.details_url)"' +``` + +Always read `details_url`. A conclusion without it is not a verdict about +anything in particular.