chore(lifecycle): purge worker and chunk driver (S20-A plan: 15/21)

What this step delivers

This step creates the internal/lifecycle package. The package holds the tombstone purge job: its arguments, its River worker, and the chunk driver that bounds every transaction. It also enrolls the package in every CI list that decides whether its tests run.

  • PurgeArgs carries a namespace id and a datastore.PurgeScope. UnmarshalJSON rejects a scope outside the defined set, and it rejects a payload with no scope field at all. An absent field decodes to the zero value, so the decode is the boundary where a malformed payload must stop.
  • PurgeWorker.Work runs the discovery scans whose level matches the job's scope. It then reaps the subtree of each row a scan returns. Eligibility is evaluated when the attempt runs, and never carried forward from the enqueue.
  • PurgeWorker.Timeout bounds one attempt at 30 minutes, because datastore.Reaper requires every reap to have a deadline.
  • runPurgeChunks and runPurgeChunk put each chunk in its own transaction. A failed chunk rolls back whole, and the drained verdict is read off the committed chunks only.
  • PurgeMaxAttempts and PurgeEnqueueOptions state the kind's retry policy and its uniqueness option. The section "SPEC-03 — the retry policy, and why this step widened" records why the budget is one.
  • CI enrollment covers five places. Two are in the test:integration job of .gitlab-ci.yml: its go list argument list, and its &integration-test-files changes anchor. The third is the depguard no-reverse-dependency glob list in .golangci.yaml, with the rationale text beside it. The fourth is the test:lifecycle-failpoints job. That job now runs gofail enable over internal/datastore and internal/lifecycle, and it carries internal/lifecycle/**/* in its &lifecycle-failpoint-files anchor. The fifth is that job's local mirror, the test:lifecycle-failpoints task in .mise.toml. Each package declares its own armer: armRepoWalkSeam in internal/datastore/lifecycle_reap_repository_integration_test.go, and armPurgeSeam in internal/lifecycle/purge_integration_test.go. The .gitignore comment that names the trees with *.fail.go bindings now names both.
  • docs/dev/background-jobs.md gains a section on structurally idempotent workers, with this job as the worked example.
  • The lifecycle configuration comments now say which knob has a reader. retention_window has one, and sweep_interval has none.

Nothing registers the worker with River, and nothing enqueues the lifecycle:purge kind. A running server therefore behaves exactly as it does today.

Spec coverage

Spec: docs/specs/S20-a-lifecycle-closed-beta.md

Rows are numbered positionally inside each of the spec's own groups: the spec writes every criterion as 1. and renders the number from its position, so there is no id to copy.

Acceptance criteria

Purger and discovery:

# Criterion Tests
AC-P1 A tombstone older than retention_window is discovered and reaped; a newer one is not Step 5's scan suites own the discovery half. The worker half -- the window evaluated at run time -- is TestPurgeWorker_Work_ALengthenedRetentionWindowLeavesTheTombstoneUnreaped
AC-P2 A row with soft_deleted_at IS NULL is never a purge root, for every table the purger reaps Steps 5-7 (scans) and Step 14 (walk). Worker half: TestPurgeWorker_Work_ReapsATombstonedRepositoryAndLeavesALiveNeighbour, TestPurgeWorker_Work_ArtifactScopeReapsATombstonedVersionUnderALiveRepository
AC-P3 EXPLAIN on the repositories, npm_packages and container_images scans Steps 5 and 7, in internal/datastore/lifecycle_explain_integration_test.go. Not re-asserted here
AC-P4 EXPLAIN on the five version-level scans Step 6, same file. Not re-asserted here
AC-P5 Duplicate fire is a no-op reporting success; a mid-walk abort leaves strictly fewer rows and no marker TestPurgeWorker_Work_DuplicateFireOnAReapedNamespaceReportsSuccess, TestPurgeWorker_Work_AnInterruptedWalkLeavesFewerRowsAndNoMarker
AC-P6 The npm repository walk is state-blind: live and already-marked rows go in one pass Step 14. Carried through the worker by the marked-version fixture in TestPurgeWorker_Work_ReapsATombstonedRepositoryAndLeavesALiveNeighbour
AC-P7 Every purge transaction is bounded, over a repository holding more artifacts than one chunk TestPurgeWorker_Work_BoundsEveryChunkInItsOwnTransaction; TestPurgeWorker_Work_AFailedChunkRollsBackWhole pins the envelope's other half
AC-P8 A frozen namespace refuses retryably before any row is reaped, per lifecycle column Step 15a. Not covered here: the gate is that step's whole subject

Per-format reap logic:

# Criterion Tests
AC-F1 A tombstoned npm version, its files and their attachments are reaped under npm's own reference check Step 8 (internal/datastore/lifecycle_reap_npm_integration_test.go)
AC-F2 Unpublish-then-republish leaves no orphan at the same coordinate Step 8
AC-F3 A tombstoned npm package is reaped with its tags, metadata files and their attachments Step 8
AC-F4 DeleteNpmMetadataFiles removes the attachment at the whole-package unpublish call site Step 8
AC-F5 A tombstoned Maven version and package are reaped with their files and attachments Step 9
AC-F6 A container repository is reaped through the existing per-artifact deleters, observed running Step 13
AC-F7 An index and its referenced manifests are reaped parent-first with no MANIFEST_REFERENCED refusal Step 13
AC-F8 A tombstoned container_images row under a live repository is reached by the image scan Step 13
AC-F9 The container repository walk is state-blind Steps 13 and 14
AC-F10 Every remote-cache table carrying the column is reaped, a positive hit per table Steps 10, 11 and 12
AC-F11 container_remote_manifests and container_remote_blobs reaped; tags and relationships cleared Step 12, with the three-table follow-on the plan's Dependencies section records
AC-F12 After a reap every referenced blob has zero attachments Steps 8-13

Repository entry point:

# Criterion Tests
AC-R1 DELETE without destructive, or with a non-boolean value, returns 400 Step 18
AC-R2 destructive=false on an empty repository returns 204 and frees the name Step 18
AC-R3 destructive=false on a non-empty repository returns 409 and changes nothing Step 18
AC-R4 That 409's message names the remedy the caller has, per format and kind Step 18
AC-R5 destructive=true on a non-empty repository returns 202, tombstones and enqueues in-tx Step 18
AC-R6 destructive=true on an empty repository returns 204 with no tombstone and no job Step 18
AC-R7 Every client route naming the repository in its path returns 404 after the 202 Steps 3, 4 and 18
AC-R8 A create at the tombstoned name is 409 while the purge is outstanding and 201 after Step 18
AC-R9 The finalizer cannot succeed early, verified through a gofail seam Step 14 (TestRepositoryReaper_Reap_FinalizerRefusesWhileAnArtifactRemains)
AC-R10 A finalizing purge removes the repositories row, its format child and every collection link Step 14

Sweep:

# Criterion Tests
AC-S1 A tombstone with no live purge job is re-queued within one sweep_interval Step 16
AC-S2 The sweep reads oldest-first, asserted behaviorally and by EXPLAIN Steps 5-7 own the EXPLAIN half; Step 16 the tick half
AC-S3 A tombstoned npm_packages row whose versions are all reaped is enumerated and reaped Step 7 (the scan) and Step 16 (the re-queue)
AC-S4 A repository that already has a live purge job is not re-queued Step 16
AC-S5 A repository whose prior job reached a terminal state is re-queued Step 16
AC-S6 A permanently failing purge stays counted and re-queued at the bounded cadence Step 16 for the cadence, Step 17b for the gauge
AC-S7 The sweep registers a worker for its periodic kind Step 16
AC-S8 A namespace that does not serve writes has nothing enqueued, and recovers on the next tick Step 16, with Step 15a's job-side refusal

Tombstone visibility:

# Criterion Tests
AC-V1 ContainerRepositoryStore.FindByNameInNamespace hides it, for docker and oci Step 3
AC-V2 RepositoryStore.FindByID returns ErrNotFound Step 3
AC-V3 RepositoryStore.Update returns ErrNotFound Step 3
AC-V4 Delete, the three counter movers and Missing still act on a tombstoned row Step 3
AC-V5 parentRepositoryIsActive refuses a parent tombstoned mid-transaction Step 4
AC-V6 Each of the four container_images gates hides a tombstoned image S17 Phase 4 Step 6
AC-V7 Every /v2/ route resolving through FindByName answers 404 NAME_UNKNOWN S17 Phase 4 Step 6
AC-V8 The blob mount route answers 202, not 404, on a tombstoned source S17 Phase 4 Step 6
AC-V9 A tombstoned image named in from= still mounts and answers 201 S17 Phase 4 Step 6
AC-V10 A mount into a tombstoned destination name answers 201 S17 Phase 4 Step 6
AC-V11 UpsertContainerImage does not resurrect a tombstoned image S17 Phase 4 Step 6
AC-V12 ContainerImageStore.Delete still acts on a tombstoned row S17 Phase 4 Step 6

Accounting call sites:

# Criterion Tests
AC-A1 The tombstone write emits no counter delta Step 19
AC-A2 The purger is the emit site, and the delta set depends on the level Step 19
AC-A3 Every emit site commits its source rows before issuing the delta Step 19. The chunk's half of the premise -- a failed chunk commits nothing -- is TestPurgeWorker_Work_AFailedChunkRollsBackWhole
AC-A4 A chunk emits from its own committed affected-row aggregate Step 19. Same premise, same test
AC-A5 A raw foreign-key cascade emits nothing and is not the reaping path Steps 13 and 19

Schema and configuration:

# Criterion Tests
AC-C1 All three discovery-index migrations apply and roll back Step 1, and S17 Phase 4's marker migration
AC-C2 No column added or dropped, and no CHECK pins a soft_deleted_at Step 1
AC-C3 unique_container_images_ns_id_cr_id_name is partial everywhere S17 Phase 4 Step 7
AC-C4 Config load accepts 0s, rejects a negative window and a bad interval Step 2 (internal/config/lifecycle_test.go)

Error cases

# Condition Tests
E-1 DELETE omits destructive: 400, nothing written Step 18
E-2 destructive carries a value other than true or false: 400 Step 18
E-3 destructive=false on a non-empty repository: 409, message per kind Step 18
E-4 An artifact published between the DELETE and its statement: 23503 branch Step 18
E-5 DELETE on a missing or already-tombstoned repository: 404 Steps 3 and 18
E-6 Create at a name a tombstone holds: 409 Step 18
E-7 Any client route naming a tombstoned repository: 404 in its own vocabulary Steps 3, 4, 18 and S17 Phase 4 Step 6
E-8 A blob mount whose from= names a tombstoned repository: 202 S17 Phase 4 Step 6
E-9 Purge job: 23503 on a parent delete fails the job, never ErrRepositoryNotEmpty TestPurgeWorker_Work_APinnedParentFailsTheJobRatherThanReportingNotEmpty
E-10 Purge job: ContainerManifestDeleter refuses a referenced manifest, retried Step 13
E-11 Purge job: the finalizer aborts because an artifact remains, the job fails Step 14 owns the refusal. The worker's propagation of a reap failure is the same path E-9 asserts; no separate case here
E-12 Purge job: repository already hard-deleted when the job runs, reported as success TestPurgeWorker_Work_DuplicateFireOnAReapedNamespaceReportsSuccess
E-13 Purge job: retention window changed between enqueue and run TestPurgeWorker_Work_ALengthenedRetentionWindowLeavesTheTombstoneUnreaped
E-14 Sweep: enumeration query fails, the tick errors and River retries Step 16
E-15 Sweep: the interval elapses before enumeration completes Step 16
E-16 Emit API unavailable at a purge or tombstone site: the delta is dropped and logged Step 19
E-17 Parent gate: parent tombstoned between the gate and the child INSERT Step 4
E-18 A tombstoned coordinate reoccupied by a republished row before the reap Steps 8 and 13

Security considerations

# Concern Tests
S-1 The purger runs with no request context and no caller identity By construction: PurgeWorker.Work takes the job's arguments and nothing else, and PurgeWorkerDeps carries no identity. No test asserts an absence
S-2 destructive is a confirmation, not an authorization control Step 18
S-3 Deletion is irreversible in closed beta and the API says so Step 18, in the OpenAPI contract rather than in a test
S-4 Input validation: no purger input is user-controlled; the job carries UUID identifiers TestPurgeArgs_DecodeRejectsAScopeOutsideTheDefinedSet, TestPurgeArgs_DecodeRejectsAPayloadOutsideTheArgumentShape, TestPurgeArgs_RoundTripsEachDefinedScope. destructive's own validation is Step 18
S-5 No injection surface is added: all purger SQL lives in internal/datastore Owned by internal/datastore under ADR-023. internal/lifecycle adds no statement of its own, and no test in this MR asserts that absence
S-6 Error messages must not become an existence oracle Steps 3, 4 and 18
S-7 A purge that cannot complete must never report success TestPurgeWorker_Work_APinnedParentFailsTheJobRatherThanReportingNotEmpty, TestPurgeWorker_Work_AnInterruptedWalkLeavesFewerRowsAndNoMarker, TestPurgeWorker_Work_AFailedChunkRollsBackWhole, TestPurgeWorker_Work_ACancelledLevelWithNoRowToReportOnStillFailsTheAttempt. The tombstone-count gauge that surfaces a stall is Step 17b

Notes on the spec-coverage table

The table above is pasted unedited. Four things about it need a note.

  • The branch carries tests that no criterion names. They are TestPurgeArgs_KindIsTheDurableRoutingKey, TestNewPurgeWorker_ChunkSizeTakesTheDefaultBelowOneAndClampsAboveTheMaximum, TestNewPurgeWorker_ANilLevelListTakesTheDispatchAndAnEmptyOneStaysEmpty, TestPurgeWorker_Timeout_EndsAnAttemptBeforeRiversStuckJobRescueHorizon, TestPurgeWorker_Work_RefusesAnAttemptWithNoDatabase, TestPurgeWorker_Work_RefusesAScopeTheDecodeNeverSaw, TestPurgeWorker_Work_AReapWithNoVerdictStopsAtTheChunkBudget, TestDefaultPurgeLevels_CoverEveryScanLevelOfBothScopes, TestPurgeWorker_Work_APageContinuesPastATombstoneItCannotReap, TestPurgeWorker_Work_ADiscoveryScanFailureLogsTheNamespaceAndTheLevel, and TestPurgeWorker_Work_GivesEveryReapADeadlineOnADeadlineFreeContext.
  • AC-S4 and AC-S5 were reworded by the spec amendment that the section "SPEC-01 — the purge job's addressing unit" describes. Their subject is now a namespace and a scope rather than a repository. Both rows still point at Step 16.
  • AC-S6's "never retried hot" half is served by PurgeMaxAttempts in this MR. The cadence half stays with Step 16, and the gauge stays with Step 17b.
  • AC-P8's "refuses retryably" wording is superseded by the retry policy that the section "SPEC-03 — the retry policy, and why this step widened" describes. PurgeMaxAttempts is 1, so the job backend discards a refused attempt and runs no second one. The row still points at Step 15a.

e2e scenario catalogs

docs/testing/ gains nothing in this change set. git diff --name-only origin/prozlach/s20a-lifecycle-closed-beta-step-14 -- docs/testing returns no path.

No scenario is affected, and the reason is the same one that empties the runtime-behavior list. Nothing registers the worker, and nothing enqueues the kind, so there is no behavior an end-to-end scenario can drive. A purge scenario becomes writable at Step 16, which registers the worker and the sweep.

Diff size, and why a split does not help

The diff is past the 500 reviewable-LOC ceiling that docs/dev/development-model.md names, so it owes a justification with the split. These numbers were re-measured at 63ccb79f4, with one command:

git diff --numstat origin/prozlach/s20a-lifecycle-closed-beta-step-14
Group Files Added Removed
Test Go 5 2125 5
Production Go 9 875 44
Documentation, CI and configuration 9 157 88
Generated: gen/artifactregistry/config/v1/config.pb.go 1 8 6
Total 24 3165 143
Reviewable, total minus generated 23 3157 137

Tests are 2125 of the 3165 added lines, which is 67 percent of them. internal/lifecycle holds 2120 of the added test lines and 825 of the added production lines. The other 50 added and 44 removed production lines are the comment-only corrections in internal/datastore and internal/config, which the section "Disclosures" lists.

A split does not help here, for three reasons. The tests, the worker and the chunk driver are one deliverable, and the CI enrollment cannot be reviewed apart from them. An enrollment MR that adds this package to two job lists before the package has a test is green either way, and it proves nothing. The spec amendment is the operator's answer to a finding raised against this diff, so it belongs with the diff that raised it. The comment-only files correct text that this MR's own code makes false.

Merge order

This MR targets prozlach/s20a-lifecycle-closed-beta-step-14, not main. That branch is !1812 (merged).

  • !1812 (merged) must merge first.
  • After !1812 (merged) merges, this MR is retargeted to main.
  • The base branch was force-pushed three times on 2026-08-21. No base commit id is quoted here for that reason. Re-read the base before you compare anything against it.

!1811 (merged) rewrites the same text this MR rewrites. Both edit the doc comment on the Reaper interface in internal/datastore/lifecycle_scan.go. !1811 (merged) is open, it is unapproved, and it has unresolved discussions. It targets main, where this MR targets step 14's branch. Whichever lands second rebases over the other. Both MRs also edit internal/datastore/lifecycle_reap_container_remote.go, in hunks that do not overlap.

This step widens no predicate ahead of the code that serves it. PurgeEnqueueOptions has no caller and no enqueue site exists, so nothing reaches the worker until Step 16 registers it.

The plan file is not edited here

AGENTS.md on main states that a step MR does not edit the plan file, the Status table included. Row 15 of the S20-A Status table is therefore empty in this change set. An empty cell means "not yet recorded", not "not started". !1840 (merged), a separate docs(plans) merge request, fills row 15, states the merge order, and records a plan correction for the addressing-unit change that the section "SPEC-01 — the purge job's addressing unit" describes.

Operator decisions recorded on this branch

SPEC-01 — the purge job's addressing unit

The spec and the branch disagreed. The spec made one purge job address one tombstone. The branch makes one purge job address one namespace at one scope.

The operator kept the branch's shape and amended the spec to match. The amendment is +11 −6 in docs/specs/S20-a-lifecycle-closed-beta.md, in commit 233a1ae94. The whole-branch diff of that file is larger, because commit d5de89305 corrected six more claims in it. The section "Spec-coherence items answered on this branch" covers those. It covers the five places that named the old unit:

  • the job-kind row
  • the sweep enumeration sentence
  • the frozen-namespace row
  • the sequence diagram
  • Sweep criteria 4 and 5

It also adds one sentence to the uniqueness paragraph, because the uniqueness subject follows from the arguments.

The same decision changed purgeLevel in this MR. A row whose reap fails no longer ends the page. The failure is collected, and the rows behind it are reaped. The collected failures return together, so the attempt still fails over any one of them. Without that change one permanently failing tombstone stands at the head of every later scan of its level in its namespace. The scans read oldest first, so that one row holds back every row behind it, with no bound. TestPurgeWorker_Work_APageContinuesPastATombstoneItCannotReap covers the new behavior.

SPEC-03 — the retry policy, and why this step widened

Spec line 928 requires a permanently failing purge to be re-queued at the bounded once-per-interval cadence, never retried hot and never silently abandoned. No step claimed that criterion. The operator widened step 15 to carry the retry policy.

The policy has three parts.

  • PurgeMaxAttempts is 1. River discards a failed attempt instead of retrying it, and the re-queue comes from the periodic sweep at the sweep_interval cadence. River's own default is 25 attempts on a fourth-power backoff. That default lands four attempts inside the five-minute default interval, and then waits days between the last ones.
  • The worker adds no backoff of its own. The budget is spent on the first failure, so River discards the job before it consults any per-worker schedule.
  • uniqueStatesWhileLive() in internal/jobsriver is not edited. At a budget of one the job never reaches the retryable state, so it cannot deduplicate a sweep enqueue behind it.

Disclosures

internal/datastore/lifecycle_scan.go is in the diff and in no step-15 Files bullet. The change is comment-only, +35 −32. The comment guardrail compels it: those doc comments described the purge worker as work that had not landed, and this MR lands it. !1811 (merged) also edits that file, and it edits the same doc comment. The section "Merge order" records what that means.

Six more files are in the diff and in no step-15 Files bullet. Every one of them is comment-only, and each corrects text that this MR's own code makes false.

  • internal/datastore/lifecycle_reap_maven.go, internal/datastore/lifecycle_reap_maven_remote.go, internal/datastore/lifecycle_reap_npm_remote.go and internal/datastore/lifecycle_reap_container_remote.go. Their comments said the chunk driver had not landed, or that Reaper had no caller outside internal/datastore. This MR lands that caller.
  • internal/datastore/lifecycle_scan_test.go, which carried the same claim about Reaper.
  • .gitignore, whose comment lists the trees that test:lifecycle-failpoints rewrites. That job now rewrites two.

The planned file count was 18. These six take it to 24, which is the count the section "Diff size, and why a split does not help" reports.

purgeLevel logs the cause and returns the error. docs/dev/go-style.md normally forbids that shape. docs/dev/database-query-patterns.md prescribes it for a call site outside the management-API writers, and this is such a call site. The store errors carry no identifiers by that same rule, and the worker must still return the error, because River records the attempt from it. internal/managementapi/bulk_container_worker.go follows the same shape.

Sweep references are future-tense in the new files, and the merged precedent is left alone. grep -in sweep over internal/lifecycle/chunk.go and internal/lifecycle/purge.go matches ten lines. Six of them state the relationship to the sweep, which lands with Step 16. All six are in the future tense, as the forward-reference guardrail requires: chunk.go:37, chunk.go:49, chunk.go:97, purge.go:149, purge.go:348 and purge.go:562. The other four state no relationship. purge.go:150 names the SweepInterval field, inside the sentence that starts at purge.go:149. purge.go:339 and purge.go:343 name the configuration knob. purge.go:181 states a conditional consequence inside the doc comment on PurgeEnqueueOptions. Every line number here is read at 63ccb79f4. internal/datastore/lifecycle_reap_repository.go:698 and internal/datastore/lifecycle_reap_npm.go:682 carry the same claim in the present tense, and this MR does not touch them. Those files are merged and outside this step's scope, so the difference is deliberate rather than an oversight.

One acceptance criterion is discharged after this MR opens, not before. The step's acceptance text asks that the test:integration job's package list resolve internal/lifecycle. The assertion is a run of that job on this MR, read from its per-test output rather than from the diff. A green branch pipeline is interim evidence and does not close it.

Spec-coherence items answered on this branch

Two items were open when this MR opened, and commit d5de89305 answers both. That commit corrects four more claims in the same spec, found by the same review pass. Its diff is +9 −7 in docs/specs/S20-a-lifecycle-closed-beta.md.

The two that were open:

  1. The freeze table said "A retry completes the work when the condition lifts". PurgeMaxAttempts is 1, so River discards a refused attempt rather than retrying it. The cell now says the sweep's next tick completes the work after the condition lifts.
  2. "Never re-swept" overclaimed, in the uniqueness paragraph and in Sweep criterion 5. River retains a terminal job row for a bounded window, so the deduplication lifts when that row is cleaned. Both sites now name River's retention of the terminal row as the bound, in place of "never".

The four found with them:

  1. The freeze table's stated reason for a refusal over a skip did not hold. It said a skip discards a job the sweep has no reason to enqueue again. A skipped job completes, UniqueByArgsWhileLive counts no terminal state as live, and the next tick therefore re-enqueues either way. The cell now states what a refusal does buy: a failed attempt an operator can read.
  2. The uniqueness sentence still reasoned about a repository. The SPEC-01 amendment moved the uniqueness subject to the namespace and scope pair, and the sentence three lines under it did not move.
  3. Sweep criterion 4 said a second eligible tombstone "is served by the job already live". A purge attempt takes one page per level and carries no cursor, so a tombstone beyond that page is not served by the live job. The load-bearing half — no second job at that scope — is unchanged.
  4. "Doubles that probe count" failed at the weak end of its own range. An attempt re-runs only its own scope's scans, so a namespace holding only repository-scoped tombstones pays nine probes and not sixteen. The claim is now "at most doubles", with both ends stated.

Recorded, not acted on

ADR-025's attachment lock is not taken on the reap path. ADR-025 requires an attachment lock taken with FOR UPDATE, and the reap path does not take it. Nothing in this change set can act on that. The reapers that delete blob_storage_attachments rows landed in earlier steps, and this MR does not touch them. runPurgeChunk is the seam a later step locks. This is recorded here so the gap is read as known rather than as a defect of this MR.

!1811 (merged)'s row-budget obligation is already met, so this MR owes no code change for it. That obligation is that a driver either sizes a row budget against the arm it dispatches, or treats Rows as an after-the-fact count. runPurgeChunks takes the second option. It passes deps.ChunkSize unchanged as every reap's limit, and it reads Rows only to accumulate a total and to test chunk.Rows == 0. No budget is derived from limit. The real in-tree contradiction was elsewhere. DefaultPurgeChunkSize's doc claimed the budget bounds the rows one reap writes. The Reaper contract bounds what a reap selects, and it says a call can delete past the limit. Commit ff0fb9977 corrects that doc.

Review notes declined without a round

A review round read the confidential AppSec review thread on this MR in full, and the operator declined it. No reply goes in that thread, and this MR gets no calibration label. This entry names the thread only, because the thread is confidential.

The GitLabDuo review note on this MR reports no findings. It carries nothing to act on, so it gets no reply and no resolve.

The step card gives this step the type chore. The issue-link guardrail exempts chore work from the Related to <issue URL> line, so this description carries none. The exemption is stated rather than left silent.

This is a bot message 🤖 — /smurfit

Edited by Pawel Rozlach

Merge request reports

Loading
Loading