Skip to content

Commit eb87f5d

Browse files
claudiacodacyclaude
andcommitted
chore: drop the metrics-wipe notice now the backend fix has landed
"Fix org-wide metrics wipe on image tag deletion" is done, so deleting SBOM data no longer zero-fills org-wide Container Scanning metrics. The notice printed above every delete confirmation is now false, so it goes, along with the spec section and pending-table row that tracked it. The confirmation prompt itself stays - deleting is still destructive. Deletes also stay sequential, but the comment no longer claims the wipe as the reason. A cleanup run happens before the upload rather than in front of a waiting user, and one request at a time is what makes "deleted 77 of 80, here are the 3 that failed" straightforward to report. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent 9a1d78c commit eb87f5d

5 files changed

Lines changed: 29 additions & 52 deletions

File tree

‎SPECS/README.md‎

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ This is the single source of truth for all project tasks and specs.
99
| Task | Spec | Notes |
1010
|---|---|---|
1111
| Tag count on `ImageSummary` | [images.md](commands/images.md) | **Backend**: add a tag count to `listOrganizationImages`' response so `images` can show a Tags column without one extra request per image. Deriving it client-side was deliberately dropped from OD-710's first PR |
12-
| Confirm the org-wide metrics wipe fix | [images.md](commands/images.md) | **Blocks merging `image --delete --keep-latest`** (built, PR 3). Every tag delete zero-fills org-wide Container Scanning metrics until the next nightly scan; a delete loop fires it once per tag. OD-710 records the fix as "mostly landed" — nobody has confirmed it shipped. Confirm, then merge and drop `METRICS_WIPE_NOTICE` |
1312
| Resolve the per-image tag budget | [images.md](commands/images.md) | The cap is org-wide and counts image × tag rows; `--keep-latest` is per image, so the safe ceiling is `cap ÷ images`. Four options, none chosen: `_main_/projects/container-tagging-guidance/research/per-image-tag-budget.md`. The CLI warns today; it does not solve it |
1413

1514
## Command Inventory
@@ -97,4 +96,4 @@ This is the single source of truth for all project tasks and specs.
9796
| 2026-09-10 | Coverage **status** surfaced in `repositories` and `repository`, from the API's new `Coverage.status` ([`CoverageStatus`](https://api.codacy.com/api/api-docs#tocs_coveragestatus): `None`/`UpToDate`/`Waiting`/`Stopped`), mirroring codacy-spa#3110. **No API bump** — pinned `57.4.17` already ships the field. It rides on `Coverage`, embedded only in `RepositoryWithAnalysis`, so these two commands are the only places it can appear; `ls`/`directories` (flat `coverageWithDecimals`) and `pull-request`/`pull-requests` (`PullRequestCoverage`/`DiffCoverage`) carry no status. The payload shapes differ in more than `status`, which is what drove the rendering: `Waiting` returns a **stale** percentage (from `lastCommitWithCoverage`, `valueUpdatedAt` older than `statusUpdatedAt`), `Stopped` returns **no percentage at all**, `None` returns nothing but the status, and `status` is `undefined` on a large share of repositories. **`repositories`:** a dim `⋯` after a `Waiting` value, a dim `⊘` *instead of* a `Stopped` value, and a legend under the table carrying only the statuses actually present (`coverageStatusLegend`). Glyphs follow the existing vocabulary — `⋯` is already `formatStandards`'s "not final yet" marker, `⊘` shares the Mathematical Operators block with the `⊙` public-repo marker — no emojis. **`repository`:** the Metrics row spells the state out (`Not reported yet for the latest commit — value from 11h ago (5474cbf)` / `Stopped receiving reports 2026-08-26 — last report 8752dbd`, plus `— coverage gate no longer enforced` when `goals.minCoveragePercentage` is set / dim `Not set up` for `None`, which a bare `N/A` could never distinguish from an uncomputed metric), colored on `formatAnalysisStatus`'s existing scale (blueBright = in-flight, yellow = attention, dim = nothing there). **Analysis row rewritten:** `formatAnalysisStatus` gained an authoritative `coverageStatus` that wins over its `expectsCoverage`/`hasCoverageData` heuristic, extracted into `coverageAnalysisSuffix`. The heuristic was *wrong* for `Waiting` — a waiting repo still reports a (stale) percentage, so `hasCoverageData` was true and the row said nothing in exactly the case worth surfacing — and vague for `Stopped` ("Missing coverage reports"). That let `repository` **drop its `listCoverageReports` call entirely** (5 parallel requests → 4), which in turn means repository-token users get the coverage state for the first time (`getRepositoryWithAnalysis` is whitelisted, `listCoverageReports` is not) and `unavailable` is now `["pullRequests"]` alone. `pull-request` keeps the heuristic — its coverage models have no status field — and is untouched. Accepted trade-off: with `status` undefined, `repository`'s Analysis row now shows no coverage hint where the heuristic might have said "Missing coverage reports". Also de-duplicated `repositories.ts`'s local `formatMetric` (a stale copy of the shared `colorMetric` returning a bare `"N/A"`), which would otherwise have let coverage and complexity/duplication drift inside the same table. Five new helpers in `utils/formatting.ts` (`coverageStatusGlyph`/`formatRepoCoverageCell`/`coverageStatusLegend`/`coverageStatusNote`/`formatRepoCoverageDetail`); `formatCoverageCell` deliberately untouched — it renders file/folder coverage, which has no status. JSON gains `coverage.status`/`lastCommitWithCoverage`/`statusUpdatedAt`/`valueUpdatedAt` on both commands (`valueUpdatedAt` is what tells a consumer the `Waiting` value is stale — the job the glyph does in the table); `pickDeep` drops undefined, so a `None` repo emits `{"status":"None"}` and a status-less one gains no keys (39 new tests, 664 total) |
9897
| 2026-09-21 | (OD-710) New `images` (`imgs`) and `image` (`img`) commands — container images with SBOMs uploaded to an organization, and the tags under them. Built entirely on operations already present in the generated client (`SbomService`): **no `npm run update-api`**. `images <provider> <org>` lists images (Image / Latest Tag / Last Upload / Last Generated) in a single request. `image <provider> <org> <image>` lists that image's tags (Tag / Environment / Repository / Generated / Uploaded / Last Analysed), shows one with `-t, --tag <tag>`, and deletes with `-D, --delete`. **`--delete` is the action and `--tag` is the scope**, the same split `issues --ignore` makes with its filters — the flag that narrows what is acted on is the flag that narrows what is shown — so `--delete` alone removes the image and every SBOM under it, `--tag X --delete` removes one tag, and there is no second delete verb needing a mutual-exclusion guard. `--tag` without an action pages the listing and matches exactly (the tags endpoint has no per-tag filter — the same shape `pull-request --issue <id>` uses) and errors naming the tag when absent; with `--delete` it skips the lookup, since the API 404s on a missing tag at lower cost. **No tag count on the `images` table**: it is the number an org at the 1000-tag cap actually wants, but `ImageSummary` doesn't carry one and deriving it costs one request per image, so it is being added server-side instead (pending task above) rather than fanned out from the client. A whole-image `--delete` still reads a `limit: 1` `pagination.total` so its prompt can name how many tags are about to go; a failed lookup drops the count rather than blocking, and `-y` skips it. Both delete scopes go through the shared `confirmAction` (non-TTY without `-y` aborts, as in `issues --ignore`) and print a yellow notice first: a single SBOM delete currently zero-fills Container Scanning metrics for the **whole organization** until the next nightly scan. That defect is also why this is the first of three stacked PRs — bulk cleanup (`--keep-latest`) would fire it once per tag and waits on the fix; `--upload` is PR 2. Both commands are **account-token only** — no image operation is on the 14-operation repository-token whitelist — and are covered by the cross-cutting `repository-token-refusals.test.ts` rather than their own token tests. Image/tag/environment/repository names arrive with the SBOM upload, so all four go through `sanitizeText()` (26 new tests, 699 total) |
9998
| 2026-09-21 | (OD-710, PR 2) `image --upload <file>`: push an SBOM (SPDX or CycloneDX) for one image tag, via the already-generated `uploadImageSbom`. Fits the command's existing action/scope split — `--upload` is a verb, `--tag` scopes it — and **requires `--tag`**, since the API keys the upload on image *and* tag with no untagged fallback; that is refused by name before the file is read. The file is validated locally first (unreadable path, empty file), so the common mistakes fail with something actionable instead of a 400 from the other side of the network. Sent as a `File` rather than a bare `Blob` so the multipart part carries the real filename — a `Blob` goes out as `filename="blob"` — with the media type inferred from the extension (`.json`/`.xml`, else `application/octet-stream`, letting the API decide rather than guessing wrong in the request); the generated `isBlob` accepts both shapes. Optional `-e, --environment` and `-r, --repository` map to the API's `environment`/`repositoryName` and are omitted from the form rather than sent as undefined. `--upload` and `--delete` are refused together: unlike `--delete`'s two scopes these are two different verbs, and asking for both says nothing coherent about what should happen to the SBOM. Still account-token only, which is the awkward part — uploading from a pipeline is exactly where a project token would be natural, so the gap is logged in `SPECS/missing-endpoints.md` (9 new tests, 708 total) |
100-
| 2026-09-21 | (OD-710, PR 3) `image --delete --keep-latest <n>` + `--dry-run`: the release-pipeline cleanup step, keeping the n most recently uploaded tags of an image and deleting every older one. **Shape is a deliberate departure from the design proposal.** `container-tagging-guidance/AGENTS.md` §2 (CLI-1) proposed `image delete-tags <image> --keep-latest N` and marked it "Builder's call"; `--delete` is already the verb here and `--tag` already the scope, so `--keep-latest` is simply one more scope ("all but the newest n") rather than a second command doing almost the same thing, and this CLI avoids its first nested subcommand. The proposal's goal ("verb first, names what it deletes") is met, and the Figma snippet needed changing regardless — `research/setup-copy-and-snippet.md` §7 lists its verbless `codacy image ${IMAGE_NAME} --keep-latest 19` as an outstanding defect. **Cleanup runs before the upload** (forced by `upsertImageTag` raising in-transaction at the cap, so upload-then-delete strands an org there), which sets three behaviours: `n` is **literal** — it counts what exists when it runs, not after the upload, so cleanup-first with 10 leaves 11 and making n secretly mean n-1 would print a number the user didn't type; "nothing to delete" **exits 0**, since an org under n is the common case on every release; and a failed delete **does not stop the loop** — giving up at tag 3 of 80 leaves the org no better off — though the exit code is still 1, because a partial cleanup is a real failure for the step behind it. Ordered by `uploadedAt`, not `generatedAt`: "latest" means when Codacy received the SBOM, which is what accumulates against the cap. Deletes are **sequential**, since each one currently zero-fills org-wide metrics and firing dozens at once is the worst shape for that. **Org-budget warning**: the cap is org-wide and counts image × tag rows while this flag is per image, so `--keep-latest 10` puts a 212-image org at 2,120 against a 1,000 cap while appearing to follow instructions (`research/per-image-tag-budget.md`). That file's rule — never quote a constant n without the image count beside it — is honoured by reading the image count (one request, `limit: 1`) and warning when `n × images` exceeds the cap, naming the count and what the cap allows per image; it **warns rather than refuses** and deliberately does not pick between that file's four unowned options. `DEFAULT_ORG_TAG_CAP` is 1,000 but the cap is configuration (`sbom.image.max-image-tags-per-org`) that no endpoint exposes, so the copy says "default" and admits the CLI cannot read it, with exact figures rather than `formatCount`'s "1k"/"2.1k". `--dry-run` is **long-only**, a documented exception to the short-flag rule: every free letter sits one shift-key from `-D, --delete` and that typo is the destructive one. **Not merged**: blocked on confirming the metrics-wipe fix (pending table above) (13 new tests, 721 total) |
99+
| 2026-09-21 | (OD-710, PR 3) `image --delete --keep-latest <n>` + `--dry-run`: the release-pipeline cleanup step, keeping the n most recently uploaded tags of an image and deleting every older one. **Shape is a deliberate departure from the design proposal.** `container-tagging-guidance/AGENTS.md` §2 (CLI-1) proposed `image delete-tags <image> --keep-latest N` and marked it "Builder's call"; `--delete` is already the verb here and `--tag` already the scope, so `--keep-latest` is simply one more scope ("all but the newest n") rather than a second command doing almost the same thing, and this CLI avoids its first nested subcommand. The proposal's goal ("verb first, names what it deletes") is met, and the Figma snippet needed changing regardless — `research/setup-copy-and-snippet.md` §7 lists its verbless `codacy image ${IMAGE_NAME} --keep-latest 19` as an outstanding defect. **Cleanup runs before the upload** (forced by `upsertImageTag` raising in-transaction at the cap, so upload-then-delete strands an org there), which sets three behaviours: `n` is **literal** — it counts what exists when it runs, not after the upload, so cleanup-first with 10 leaves 11 and making n secretly mean n-1 would print a number the user didn't type; "nothing to delete" **exits 0**, since an org under n is the common case on every release; and a failed delete **does not stop the loop** — giving up at tag 3 of 80 leaves the org no better off — though the exit code is still 1, because a partial cleanup is a real failure for the step behind it. Ordered by `uploadedAt`, not `generatedAt`: "latest" means when Codacy received the SBOM, which is what accumulates against the cap. Deletes are **sequential** — one request at a time is what makes "deleted 77 of 80, here are the 3 that failed" reportable; the org-wide metrics wipe that originally forced it is fixed. **Org-budget warning**: the cap is org-wide and counts image × tag rows while this flag is per image, so `--keep-latest 10` puts a 212-image org at 2,120 against a 1,000 cap while appearing to follow instructions (`research/per-image-tag-budget.md`). That file's rule — never quote a constant n without the image count beside it — is honoured by reading the image count (one request, `limit: 1`) and warning when `n × images` exceeds the cap, naming the count and what the cap allows per image; it **warns rather than refuses** and deliberately does not pick between that file's four unowned options. `DEFAULT_ORG_TAG_CAP` is 1,000 but the cap is configuration (`sbom.image.max-image-tags-per-org`) that no endpoint exposes, so the copy says "default" and admits the CLI cannot read it, with exact figures rather than `formatCount`'s "1k"/"2.1k". `--dry-run` is **long-only**, a documented exception to the short-flag rule: every free letter sits one shift-key from `-D, --delete` and that typo is the destructive one. Shipped once [Fix org-wide metrics wipe on image tag deletion](https://linear.app/codacy/project/fix-org-wide-metrics-wipe-on-image-tag-deletion-197476700869/overview) landed, which is what had held a delete *loop* back while PR 1's single-tag and whole-image deletes went ahead; `METRICS_WIPE_NOTICE` was removed with it. Verified end to end against `gh/claudiacodacy` with five Trivy-generated CycloneDX SBOMs: upload x5, list, dry-run, apply, idempotent re-run, single-tag delete, whole-image delete (13 new tests, 721 total) |

‎SPECS/commands/images.md‎

Lines changed: 15 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -18,21 +18,16 @@ Split into stacked PRs, because one piece is blocked on a backend fix:
1818
|---|---|---|
1919
| 1 | `images` (list) and `image` (list tags, show a tag, `--delete` scoped by `--tag`) | done |
2020
| 2 | `--upload` (`uploadImageSbom`) | done |
21-
| 3 | bulk cleanup — `--delete --keep-latest <n>` | this one — **see the blocker below** |
22-
23-
**The bulk-cleanup blocker, unresolved as of 2026-09-21.** Every single tag
24-
delete currently zero-fills Container Scanning metrics for the *whole
25-
organization*, across every repository, healing only on the next nightly scan.
26-
`--keep-latest` deletes in a loop, so it fires that once per tag — 80 times for
27-
the org this exists for. OD-710 records the fix (Container Scanning Findings
28-
Integrity, milestone "Fix org-wide metrics wipe on image tag deletion") as
29-
"mostly landed"; nobody has confirmed it shipped. **PR 3 must not merge until
30-
someone does.** Single-tag and whole-image delete were safe to ship ahead of it
31-
and did, in PR 1.
32-
33-
Every delete path prints a yellow notice above the confirmation saying the
34-
metrics will be zeroed and restored by the next nightly scan. Remove
35-
`METRICS_WIPE_NOTICE` and this section together when the fix is confirmed.
21+
| 3 | bulk cleanup — `--delete --keep-latest <n>` | this one |
22+
23+
**The bulk-cleanup blocker is resolved (2026-09-21).** Every tag delete used to
24+
zero-fill Container Scanning metrics for the *whole organization* until the next
25+
nightly scan, which is why `--keep-latest` — a delete loop — was held back while
26+
single-tag and whole-image delete shipped in PR 1. The backend fix
27+
([Fix org-wide metrics wipe on image tag deletion](https://linear.app/codacy/project/fix-org-wide-metrics-wipe-on-image-tag-deletion-197476700869/overview))
28+
is done, so the loop is safe and the warning notice that used to sit above every
29+
delete confirmation is gone. Deletes are still sequential, for the reasons in
30+
`deleteTagsInSequence`, but no longer because of this.
3631

3732
## API
3833

@@ -113,7 +108,7 @@ errors naming the tag when there is no match. JSON emits one object, not an
113108
array. `--tag --delete` skips the lookup entirely and deletes straight away; the
114109
API 404s on a tag that isn't there, which is the same answer at a lower cost.
115110

116-
**Confirmation.** Both delete scopes prompt via the shared `confirmAction`
111+
**Confirmation.** Every delete scope prompts via the shared `confirmAction`
117112
(`utils/prompt.ts`) and proceed only on an explicit `y`; `-y` bypasses it for CI.
118113
`confirmAction` returns `false` on a non-TTY, so a non-interactive run without
119114
`-y` aborts rather than deleting by accident — same rule as `issues --ignore`.
@@ -191,8 +186,10 @@ upload-then-delete *fails at the cap* and can strand an org there
191186
uploads per release means when Codacy received the SBOM; `generatedAt` is when
192187
it was built, which can differ and is not what accumulates against the cap.
193188

194-
**Deletes run sequentially.** Each one currently zero-fills organization-wide
195-
metrics, so firing dozens in parallel is the worst possible shape for it.
189+
**Deletes run sequentially.** Not for latency — a cleanup run happens before the
190+
upload, not in front of a waiting user — but because one request at a time is
191+
what makes "deleted 77 of 80, here are the 3 that failed" straightforward to
192+
report. (The original reason, the org-wide metrics wipe, is fixed.)
196193

197194
**`--dry-run` is long-only** — a deliberate exception to the "every option gets
198195
a short flag" rule. Every free letter sits one shift-key from `-D, --delete`,

‎src/commands/AGENTS.md‎

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -508,14 +508,12 @@ the parts that constrain future edits:
508508
resolve the open design question — see `SPECS/commands/images.md`.
509509
- **`--dry-run` is long-only on purpose.** Every free short letter sits one
510510
shift-key from `-D, --delete`, and that typo is the destructive one.
511-
- **The metrics-wipe notice is temporary.** Deleting any SBOM currently
512-
zero-fills Container Scanning metrics for the whole organization until the
513-
next nightly scan, so both delete scopes print `METRICS_WIPE_NOTICE` above the
514-
confirmation. Delete the constant (and its test) once the backend fix ships.
515-
It is also why bulk tag cleanup (`--keep-latest`) is not here yet: a delete
516-
loop fires the wipe once per tag.
517511
- **`describeTagCount` must never block a delete.** It exists only to make the
518512
whole-image prompt concrete ("all 85 of its tags"); a failed or `total`-less
519513
lookup falls back to vaguer wording rather than throwing, and `-y` skips it.
514+
- **Deletes are sequential, and that is not about the metrics wipe any more.**
515+
That defect is fixed; the shape stays because a cleanup run is not
516+
latency-sensitive and one request at a time is what makes a partial-failure
517+
report ("deleted 77 of 80") straightforward.
520518
- **Sanitize everything that came in with the upload** — image name, tag,
521519
environment, repository name are all user-controlled.

0 commit comments

Comments
 (0)