docs(plans): split the S04-A plaintext drops into un-bind and drop

What this changes

Step 5 of the S04-A per-format credential-columns plan becomes two merge requests: stop binding Maven's interim plaintext pair, then drop the columns a release later. Steps 8 and 11 record the same split, for Container and npm.

Why

Step 5b's Scope assumed the deployed release had already stopped naming tmp_plaintext_username and tmp_plaintext_password, because Step 4's scope line said nothing touches tmp_plaintext_*.

The code that merged for Step 4 does name them. Both of its update arms bind the pair to NULL, deliberately, so a URL change cannot leave the old host's credential readable to a pod on the previous image. The insert arm, insertMavenRemoteRepositoryStmt, names neither column and needs to name neither, because a new row holds no earlier value to erase. So updateMavenRemoteCredentialsStmt and mavenRemoteCredentialUnitAssignments still bind both columns in the deployed release.

That makes the two-release rule in database-migrations.md impossible to satisfy with the file list Step 5 carried. A drop riding the same release as the un-bind answers an in-flight Maven credential set, replace, clear or URL change with 42703 for the length of a rolling deploy. A one-release image rollback makes it permanent until someone runs the Down by hand.

Step 4's scope clause is corrected here too, in Step 4's own entry. It is the site that produced this defect, and Step 4 has merged, so that entry is now a record of shipped code rather than a forecast.

Step 5 keeps one Status-table row, so this amendment adds no row and moves no denominator: the table is still eleven rows. The halves are named 5a and 5b in the plan only; both MRs carry the plain 5/11 marker and separate themselves as part 1/2 and part 2/2. Step 10 takes that form: !2625 (merged) to !2627 (merged), titled part 1/3 to part 3/3 over a plain 10/11. Step 4 is the precedent for the plain marker alone, not for the part count: !2553 (merged) and !2554 (merged) both carry 4/11 and separate themselves by describing their own half.

Container and npm owe the same split

Container's and npm's update arms name their interim columns today, and Step 7 and Step 10 keep naming them by nulling them the way Step 4 does. So the release before Step 8's and Step 11's drops still names those columns, and each drop carries the defect Step 5 carried. Both entries now hold the split, with the plain 8/11 and 11/11 markers and the same release gate.

Step 8a un-binds two statements, and that count is taken at Step 7's head rather than today. Three statements write the pair today: container_remote_repositories_update.go, updateContainerRemoteCredentialsStmt in container_remote_credentials.go, and insertContainerRemoteRepositoryStmt in container_remote_repositories_create.go. Five name it; the other two are reads 8a does not touch, findContainerRemoteCredentialsStmt and containerRemoteHasCredentialsProjection, both already in Step 7's Files and both the read half 8a inherits as the declared read deviation. Step 7's Scope now commits to taking the pair out of that insert, the way Step 4 did for Maven's, which is what makes two the right number at the head 8a opens on. Step 8a names both update arms, because un-binding only the first would leave the second binding a dropped column.

Seven bare "as Step 5" references defined Steps 8 and 11 by a step that now has two Scopes and two Files lists. Re-derived at this head: seven on main, zero left here, each resolved to Step 5a or Step 5b.

What the split opens, and who owns it

Between 5a's release and 5b's, nothing nulls the pair. A clear or a rotation no longer erases a plaintext value a row already holds, and re-entering the credential through the API stops being the operator's remedy for it. One remedy survives until 5b drops the columns: operator SQL over the ids the warn line samples, which column-encryption.md already prescribes for rows the API cannot reach. key-rotation.md is where an incident responder finds that, so Step 5a moves the passage.

So the gate reading moves earlier. The engineer shipping 5a reads the leftover-plaintext line and clears whatever it names before 5a merges, which for staging is the same event as before it deploys, and records it in 5a's own description, !2629 (merged). The count cannot grow inside that window from the deployed release, because by then no path writes a plaintext value at all; a rollback to a pre-Step-4 image is the one thing that falsifies the unconditional form, and it has the same owner.

5a also keeps one read of the pair, mavenRemoteLeftoverPlaintextPredicate, so the two-release rule is met at 5a for writes and not for reads. The reason is not 5b's main gate reading, which is taken off Step 4's release before 5a deploys and which 5a cannot affect. It is the residual that reading leaves: an environment migrating later than it, a restored database or a long-lived review app among them, boots on 5a's image, and the report is the only thing that names its rows.

The release gate names the role that owns it

A merge to main reaches staging without anyone acting. The production manifest upload waits for a manual play, so merge order alone does not put 5a on the production pods ahead of 5b's migration. Step 5b's Depends on names the manual play, the per-environment kube_pod_container_info check, and the role that owns it, stated as an absence check ("no series carries a version below 5a's") because the query returns one series per ready container.

Review rounds

Round nine answered three blocking findings from a ninth branch review, all on Step 5a and all in this one plan file.

  • The enumeration of surfaces that lie stopped short of the audit trail. The passage concluded that the boot report was the only surface disagreeing with a cleared plaintext pair. The remote repository credential write wide event disagrees too, and it is the durable one: the API response is not retained and the boot line is one record per process, so the audit trail is where a revocation is reconstructed after the fact, and after 5a it asserts one that did not happen. It is named now beside credentials_cleared, with its emitter, the call site that books audit_action=clear and the observability.md row that owns the field. 5a's Files bullet for maven_remote_repositories_update.go named one comment where three invert: :133 and :173-175 sit in the doc comment of a file the step already opens, and :173-175 already reasons about the audit record being untrue on a rollback, which 5a widens to the ordinary pre-move row.
  • The test instructions misstated what two existing assertions do. The commissioned leftover-count subtest was written as if the report suite took a baseline per subtest. One baseline is captured at :477, :505 and :519 assert absolute values against it, the suite is //nolint:paralleltest so order is load-bearing, and the count query pins no namespace_id, so seeding a leftover row before :509 would have reddened :519. The subtest goes last now and takes its own baseline after the seed, which also settles what "unchanged" is measured against. Separately, the arg loop at maven_remote_repositories_writes_test.go:159-162 was called vacuous after the un-bind. It is not: it still asserts that the supplied plaintext never rides as a bound argument, and re-binding pg.String(username) in the builder fires it. Only its message at :161 goes stale, and the entry says so rather than leaving the message rewrites it commissions elsewhere reading as permission to delete the one assertion in that file making the point. The rationale comment at :133 joins the comments that invert.
  • Four smaller items, batched. The four .gitlab-ci.yml citations in the merge-train bullet were four to five lines high. Re-derived: target_latest is :526-529, the added set :536-538, the loop :544-549, and lint:comment-caps's "a train car's diff from the tip" :866-867, the same four lines at the amendment basis, at this head and on main. The "at this plan's basis" phrase in Testing Strategy sat outside the Step entries the basis paragraph scopes, so the sha is written at that one site rather than widening the enumeration onto every other citation in that section. Step 8's preamble now says its single field set is deliberate, with Step 11 pointing at it, so a reader scanning for the un-bind and drop boundary stops looking for sub-entries Step 5's shape carries and these two do not. And 5a's Scope names the one metric interaction the amendment had left out: a budget expiry on the read 5a keeps books cancellation_reason=deadline, which observability.md:27 designates as the series to alert on, so both query names the deviation keeps alive are named for whoever wires that alert.

Two half-sentences narrating drafts that never reached main go with them, at the credential_table.md and maven_remote_credentials_test.go citations.

Round eight answered one blocking finding, on Step 8's bookkeeping: the Container half of the narrowing this amendment documents for Maven was left unenforced.

  • A narrow-twice document sat in no Shared seams field. migrations/npm_remote_encrypted_credentials.md :110-112 compares Maven's and Container's interim CHECK, so 5b falsifies its Maven half and 8b its Container half, and Approach and Step 5b's Files both said so. Neither Shared seams field carried it and Step 8's Files named only container_remote_credential_columns.md :23, so the 5b-then-8b order reached no Depends on reader and an 8b implementer working from Step 8's entry never opened the file. It sits in Step 5b's Shared seams now, with what the second lander owes, in Step 8's Files as 8b's second sidecar, and on 8b's Depends on with the order.
  • The spec-amendment gate bound only 5b. The Dependencies bullet was titled "merged before Step 5b" while its body assigned S16-container-remote.md and S13-virtual-remote-foundation.md's Container half to 8b, and Step 8's Depends on named no amendment. The title gates both drops now, and 8b's Depends on carries the amendment line naming S16 and S13's :93, :94 and :98, so the premise that an irreversible drop cannot ship against a merged spec contradicting it is enforced for Container the way it already was for Maven.

Round seven answered the blocking finding round six left open, on Step 5b's migration bounds.

  • The bounds ruling settled the directives but left the session bound, the values and the Down's directives unstated. The Up is now ruled as one ALTER TABLE maven_remote_repositories carrying both DROP COLUMN clauses, bracketed by SET statement_timeout = '120s' and SET lock_timeout = '10s'. The plan says the single statement is what bounds the Up, so the ceiling is 120s rather than 65 times lock_timeout, and two statements would each take a fresh statement_timeout and double it to 240s; 120s leaves 180s of the 5 minute upTimeout (internal/datastore/migrations/runner.go:28) for the rest of the pending set and the goose advisory-lock retry. lock_timeout is 10s rather than the tree's 5s, with the reason beside the value: 5s is exactly leftoverPlaintextReadTimeout (cmd/artifact-registry/wire_remote.go:576), so at 5s a concurrently booting pre-5b pod and the migration wait on each other and either can win. Each SET is paired with a RESET after the ALTER TABLE with the session-leak reason, and 20260829134210_create_npm_virtual_upstream_rules.sql's Down is cited as the accumulation precedent: 65 drops under one SET lock_timeout = '5s' at :614, paired with RESET lock_timeout at :758. constraint-missing-not-valid and prefer-robust-stmts join the directives 5b rules on, both owed by the Down's ADD CONSTRAINT ... CHECK. Only the prefer-robust-stmts reason carries over from 20260904170000_add_maven_remote_encrypted_credentials.sql: its constraint-missing-not-valid reason at :5 is written for the foreign key and the same directive says the CHECK above that key does take NOT VALID on every version, so 5b's Down is given its own reason instead. The Tests line gains the RESET beside the bound it already pinned.

Round six answered two of three blocking findings from a fourth branch review. The third, on Step 5b's migration bounds, is answered in round seven above.

  • The rollback remedy named an action with no operator route, and the Open Questions deadlines contradicted the gate stated twice elsewhere. Step 5a's access paragraph called the migration Down a third action needing psql-level access, then left it out of both the requirement and the no-route fallback, so the only remedy for the state 5b's rollback-depth rule calls unrecoverable had no named owner and no named route. It now covers all three actions, and says the Down is the one with no second answer: without the route, the two-release rollback is unrecoverable in place. Open Questions carried one shared "before Step 5b opens" deadline over entries whose gates differ. Each entry now carries its own, the read deviation is marked owed before 5a merges as Dependencies and Step 5a's Scope already said, and the psql-route question is added, owed before 5a opens.
  • Step 5b's off-glob enumeration named two Container files as Maven's, and its searches could not reach every site. settings_read_integration_test.go:390 and update_delete_integration_test.go:840 are both statements against container_remote_repositories, so executed as written 5b would have edited two Container suites during Maven's drop. Both move to Step 8b, and the counts they inflated are corrected. The literal search widens to the bare tmp_plaintext stem, because three assertions spell the columns as a fragment in the predicate the drop falsifies, and a third search on check_maven_remote_repositories_credentials_all_or_none is added for the cluster that reaches the guard through its constraint name. Step 3 is credited with creating credential_table_integration_test.go.

Round five answered one Duo finding, on the Step 5b Acceptance line.

  • The gate reading's home named no merge request, then named the wrong one. The Acceptance asked for the reading "recorded in a merge-request description" without saying whose, and a later push resolved that to 5b's. The same paragraph says the reading is taken before 5a deploys rather than before 5b opens, so 5b's description is not open when the reading happens and a reviewer had no way to locate it. It is 5a's own description, !2629 (merged), now. The fact lived at three sites and all three moved together: Step 5a's Depends on, Step 5b's Acceptance, and the cross-reference beside Step 5b's image-check readings, which themselves stay in 5b's description. Steps 8 and 11 take this through their "as Step 5a then Step 5b" Acceptance and needed no edit of their own.

Round four answered a third full branch review: four blocking findings and seventeen smaller ones, all still in this one plan file. Three of the four blocking ones sat in a gate or an enumeration an earlier round had already corrected once, which is the shape worth naming rather than the count.

  • Step 5b's positive control passed the exact failed reading it was added to catch. Round three replaced an empty-vector pass with a positive control and then got the verdict rule backwards: it read a non-empty control beside an empty gate result as "a genuinely idle environment" and passed it. That combination means the opposite. build_info is emitted by the running processes, so a non-empty control proves the fleet is up while the gate query found nothing, which is a broken query. The rule now passes only when both controls are empty, and the two controls stop being interchangeable: one tests the readiness and image-pull legs, the other the datasource and label legs. The paragraph also contradicted itself, since fifteen lines above it already said an empty production result is "a failed reading, not a pass".
  • The gate read a label the series does not carry. It asked for "no series carrying a version below 5a's"; kube_pod_container_info carries image and image_id, and releases.md says the image tag is the deployed version. version is a real label on a different series named in the same paragraph, scoped environment= where the kube series is scoped env=. Both are stated now, with a warning not to paste one selector into the other. Staging also loses its exemption from the control: it is not a review environment, it is Flux-reconciled and persistent, and it is the one environment with no manual play, so it is where a second check matters most.
  • The release-separation gate was described as structural when it only orders two merges. The MR dependency plus auto-retarget gives the order and two separate merge commits. It does not hold the one-release gap: the moment the un-bind merges the blocker clears, which !2629 (merged)'s own description records in writing. The plan never mentioned the mechanism that does hold it. The gate is now the release-tag wait, stated as the single condition; the dependency is demoted to ordering; the Draft flag is named with its owner; and the collapse routes stop being a closed set of two, because any two merges close enough that semantic-release has not cut a tag between them collapse into one version. Steps 8b and 11b inherit the corrected form.
  • A Maven suite the drop breaks was invisible to all four derivations that claimed to close the set. credential_table_integration_test.go is the shared credential table's real-database floor instantiated for maven_remote_repositories, and it names the dropped columns in raw SQL, so it carries neither a maven_ filename nor the jet identifier the searches use. Step 5a's glob, the .md sweep, Step 5b's four named suites and the Acceptance identifier search each miss it structurally, and the entry still closed with "the set is closed". Three more files are in the same position. Both drop halves now run a literal column-name grep beside the identifier one, Step 5b names all four off-glob files with their sites, and Step 5a no longer implies its six-file glob is Maven's whole test surface.
  • The column-encryption.md edit left the two adjacent lines stating the remedy it removes. The bullet ruled on :137, ruled that the five-shapes list stays true, and presented the file as settled. :134 still names re-entering the credential as the remedy, and :136 still counts "two things make that recoverable" where the un-bind leaves one. Its delete branch also rested on a clause that does not cover Maven: after the un-bind Maven's write path has moved and leaves the pair standing, a third state neither clause describes. All three lines move together now and that third state is written down.

Smaller, and mostly one member of a set that was checked everywhere else:

  • Container has five statements naming the pair and three writing it. The two extra are reads, findContainerRemoteCredentialsStmt and containerRemoteHasCredentialsProjection, and they are the read half 8a inherits as the declared read deviation, so the omission was not bookkeeping. This description carried the same wrong count.
  • updateContainerRemoteCredentialsStmt is not Maven's "exact twin" today; it is its opposite. Container's statement binds the live credential into the pair, Maven's binds NULL on both arms. It becomes the counterpart only at Step 7's head, so the claim is future-tense now and the two-statement enumeration rests on the grep rather than on a resemblance.
  • The lock clause gave two contradictory orders on one decision, fourteen lines apart: rule on it "rather than citing #548 and stopping", then "take that position by citing #548 rather than re-opening it". It takes the bounds now, stated once. It also cited the wrong precedent: the tree's only forward drop-column migration gives its own metadata-only and CONCURRENTLY rationale and never defers to #548, and that rationale does not transfer to a migration with no concurrent index work and 65 accumulating locks.
  • The NO TRANSACTION ruling rested on the 65 locks rolling back together, which a lone DDL statement gets from PostgreSQL either way. The reason that fires is goose's goose_db_version write sharing the transaction with the DDL, so a failure cannot leave the recorded version and the schema disagreeing.
  • The lock clause's premise was falsified by the evidence beneath it. It rested on "the absence of concurrent sessions" and triggered a second look on "the table taking traffic", then named two readers reaching all 65 relations regardless of load: the health sweep's ListRemotes on every five-minute fire, and the boot count. Conflicting sessions are permanent, so the bound is their duration, which is where the row count does its work.
  • 5a's leftover-plaintext reading was bound to merge and justified only for staging. For production merge and deploy are different events, which 5b's own entry proves, and the stale-count trigger fired "before 5b", after the API remedy is already gone. 5a now carries the two-role structure 5b has, with the second reading taken immediately before the production play, and 8a and 11a inherit it.
  • S13 carries three generic sites, not two: :93, :94 and :98. :94 is the password row, the member an amendment most easily leaves standing, which would leave the spec asserting Maven still stores it.
  • S04-a:406 is Maven's alone, not generic over Maven and Container. It sits between two ALTER TABLE maven_remote_repositories statements and names Maven's own constraint, so it finishes at 5b rather than narrowing again at 8b. The reason previously given, that it "governs both columns generically", is about the two columns and not the two formats.
  • column-encryption.md:137 is generic over Maven and Container, not all three formats: it says "the interim pair" and npm carries a single tmp_plaintext_auth_token. So 8a is no longer told to narrow it "to npm alone", which would have written an operator-facing sentence claiming npm's moved write path nulls a pair.
  • 5a's Acceptance asserted that the boot report still counts such rows and no test observed it. The one assertion over a moving leftover count is driven by a raw UPDATE, so it stays green either way. That subtest is commissioned now, with its falsifiability check.
  • The sixth-test-file fact was stated three ways, two of them stale, and the stale one cited Files as authority for the opposite of what Files says. Round three fixed one of the three sites.
  • clear_credentials is not an API field; one hit repo-wide, in the plan itself. The wire form is PATCH … {"settings":{"credentials":null}}, and this is the passage 5a moves into the incident runbook.
  • The npm-side NotContains pair, :41-42 and its twin :108-109, is ruled on together. The list previously included the first and omitted the second, which would have left the count and page suites asymmetric.
  • Smaller still: the "thirteenth and fourteenth site" ordinals are dropped, because the wider tmp_plaintext grep returns 21 files rather than a closed fourteen; the one-release rollback's safety also rests on goose ignoring an applied version with no embedded file, not only on the report's error arm; the operator SQL route is owed before 5a opens, since the only documented operator path runs two subcommands and neither reads the pair; wire.go's registration carries two Maven bindings and the load-bearing one is the reader, not the named constant; LeftoverPlaintextSampleLimit is referenced from wire_remote.go only inside a comment, so "a wrong deletion fails the build" does not cover it; the read deviation lives at :385 as well as :566; the failed-read metric splits between the errors and cancellations families; the e2e row's has_credentials: false is column-derived and unmoved only because Step 10 repoints the projection; the amendment-basis paragraph names the 14-to-12 blast-radius delta; the crash-loop quote moves from :80-83 to :77-79; the precedent's rollback sentence is in its file header, not its Down comment; the releases.md type claim gains "absent a breaking-change marker"; :406's five-shapes list is :152-156; the operator-SQL remedy is :164-165; the wantUnit literal is :430; the :204-205 quote is :205; and "a clear or a rotation" becomes "a clear or a credential replace", with a note that a key rotation never erased the pair.

Round three answered a second full branch review: three blocking findings and eighteen smaller ones, all still in this one plan file. Twelve of the eighteen are listed individually below and the last bullet carries the other six.

  • Step 5b's image gate read an empty PromQL result as a pass. A mistyped label, a wrong datasource and a genuinely idle environment all return the same empty vector, and this is the gate standing between the production manifest-upload play and 42703 on every Maven credential write. It is a failed read for env="production" now, resolved with a positive control (gitlab_artifact_registry_build_info), which is the standard the same entry already applies to its leftover-plaintext gate under Acceptance. Round four found the replacement's verdict rule inverted; see above.
  • Step 5a's Acceptance still gave the reason its own Scope denies. It kept the boot report "because 5b's gate reads it"; Scope says the reason is the residual that reading leaves, not the reading. An earlier round fixed the Scope site and left this one, which is the field a reviewer verifies 5a against.
  • Nothing structural kept the two halves in two releases. 5a's fix type stops one collapse route. A shared merge train and a single merge into main reach the same state, and the merge-train rule in Dependencies could not separate them, because it is written over Steps 2, 5, 6, 8, 9 and 11 and both halves are "Step 5". Step 5b's Depends on now carries the mechanism this stack actually uses: the drop half targets the un-bind half's branch under a hard MR block, never one train, and waits for the un-bind's release tag. Steps 8b and 11b inherit it.

Smaller, and mostly the same shape, a fact corrected at one site and left standing at another:

  • credentials_cleared reports the action the update took, not the encrypted unit: update.go:485 is verdict.urlChanged && verdict.action == remote.CredentialClear, reading no column, so a plain clear returns 200 with the key absent. Both sites said "the encrypted unit". This is the passage 5a moves into key-rotation.md for an incident responder, so the wrong operation was named in runbook text.
  • The release keeps one predicate reached by two statements, not "one read": countMavenRemoteLeftoverPlaintextStmt and selectMavenRemoteLeftoverPlaintextPageStmt. The one-metric-sample figure rests on the early return at maven_remote_credentials.go:349, not on there being one query.
  • Step 5a deleting the column-encryption.md clause left Step 8a nothing to narrow and Step 11a nothing to remove, while both entries presupposed a surviving clause. The two branches are spelled out, and Steps 7 and 10 now restore the clause for their format when they move its write path.
  • "Nothing else couples the formats" is falsified by five passages the same entry adds. It reads "no other document couples the un-bind halves", with the drop halves' extra couplings named.
  • The lock clause isolated acquisition as the hazard and then discharged it with the row count, which buys hold duration through scan cost and nothing else; 5b has no scan. It rests on concurrent traffic now, names the two in-tree readers that reach all 65 relations regardless of row count, and rules on database-migrations.md:511-515 and the upTimeout crash-loop consequence rather than citing #548 and stopping.
  • A rollback depth rule for the far side of the drop: after 5b's migration runs, no image older than 5a may reach the fleet until the Down is applied, owned by the role that plays the production upload. The plan reasoned carefully about everything up to the deploy and treated the post-drop world as covered by "it never fails boot".
  • "Re-read the line before 5b" had no route: the report is emitted once per process at component start. The mid-window check is a direct count now, with the same owner.
  • The ReplicaSet residual check's hedge covered the wrong operand. kube-state-metrics publishes no kube_replicaset_* series carrying an image, so the likely failure is that the counts resolve and the image does not.
  • The spec half of the twelve-file sweep took one file-level ruling, but S13:93/:98 and S04-a:406 are generic over Maven and Container, so they narrow at 5b and again at 8b. That amendment is now in Dependencies with the step it gates, distinct from the backfill one. Round eight found that gate named 5b only; see above.
  • credential_leftover_plaintext_test.go breaks unconditionally, not conditionally: the Maven wrappers it drives are removed unconditionally.
  • (Modify, comment only) was wrong; :519-520 is an assertion's failure message.
  • The deviation from database-migrations.md had no owner and no gate. It is a merge gate in Dependencies now, and Steps 8a and 11a say they inherit it.
  • Smaller still: key-rotation.md is the fourteenth site and column-encryption.md the thirteenth; the report suite is :447-522; column-encryption.md:144-145 states no duration, so the 5s figure is cited where it lives; the error_message discriminator is conclusive on its own and version corroborates it; the sampled ids are a locator, not the set, so clear by predicate above ten rows; and Open Questions lists the three things genuinely unsettled instead of saying None.

Round two answered a full branch review: fifteen findings, all in this one plan file. Nine are listed individually below and the last bullet carries the other six. The load-bearing ones were passages the split left stale and enumerations derived by literal grep.

  • Approach still put the clean-log gate reading "at the drop". The split moved it before the un-bind releases, which the Step 5a and 5b entries already said in three places. Planning 8a or 11a from Approach would have taken the reading after the un-bind shipped, which is when the API remedy has stopped working.
  • key-rotation.md is a second document coupling the three un-bind halves, and Approach said none existed. Its API-clear sentence is generic over all three formats exactly as column-encryption.md's is, so it now sits in all three Shared seams under the same 5a, 8a, 11a order. Steps 5b and 11b say how the remedy changes again once the columns go.
  • The plan declared one basis, ee9e5063c, where the twelve-file sweep returns seven and maven_remote_credentials.go is 434 lines rather than 666, so the amendment's own :575, :632 and :638-639 cannot resolve there. The amendment's basis, 85530efe0, is declared separately now.
  • The .md sweep bullet said four sidecars in one sentence and three in the next. It says three, with Container's fourth waiting for Step 8.
  • Step 5a said the writes_test.go pair assertions are not reached through mavenRemoteCredentialUnitAssignments. They are, through mavenRemoteUpdateAssignments, which appends it at :433 and :442; only the direct call was absent. The old wording sent a reader hunting a second render path.
  • The nine report subtests are not parameterised over format. Eight hard-code "maven" and one names no format. An earlier round said seven, exempting wire_remote_test.go:646 as asserting only that the two names differ; it hard-codes the literal twice, at :649 and again at :652, which pins the rendered leftover-plaintext-report/maven.
  • Step 5b now retargets Maven's own four NotContains sites (maven_remote_credentials_test.go:532, :566, maven_remote_repositories_writes_test.go:85, :101), which the drop makes unfalsifiable. Steps 7 and 11 already give that ruling to Container and npm; Maven's were the members of the set nobody opened.
  • The drop's lock clause rested on the statement holding no scan, which bounds the hold and not the acquisition. It now rests on the acquisition queue and the measured row count, and rules on require-timeout-settings, #548 and the NO TRANSACTION choice beside ban-drop-column.
  • Step 5b's release gate named an owner for the reading but not for the action that sequences the releases. It now names the role that plays 📤 Upload Resource Manifests: [production] as a second owner, requires the reading immediately before that play, and replaces "confirm no ReplicaSet on a pre-5a image is left able to scale up" with a series that can answer it.
  • Smaller: the metric an on-call is told to watch carries its gitlab_artifact_registry_ prefix; the error-line discriminator needs both signals rather than either; the restored-database residual gets an owner; the credentials_cleared response inside the window is named; the three new fix steps get their e2e ruling; and the step DAG gains the two shared-document edges.

Round one answered three blocking findings and the rest of an earlier branch review. Two of the three blocking ones are the Step 4 scope clause and the Container statement pair above. The third is among the bullets below, which this description does not mark individually; treat the list as the round's findings rather than as a severity split. Beyond the first two:

  • Step 5a's Tests routed four seeded subtests through one shared helper. There are two. The clear arms use assertCredentialColumnsNull; the set arms use assertStoredCredentialUnit, which was unnamed and carries the same "no plaintext may be left in the row" assertion across 14 leaf call sites, 12 of them on rows that never held plaintext. Deleting its arm was the silent failure the entry was written to prevent.
  • Step 5b's wire_remote.go entry deleted format-generic machinery that Steps 7 and 10 register their own arms through. Only the wire.go registration is Maven-specific.
  • Step 5b's Tests owned none of the four suites that pin the report its Files removes. They are named now, with their subtest counts.
  • Dropping Maven's LeftoverPlaintextCredentials leaves npm's //nolint:dupl dead once Step 10 lands, because dupl needs both bodies in one lint scope. No job reports it: .golangci.yaml lists nolintlint under disable. So it is a cleanup to do with the deletion, not a gate that will catch it. The first draft of this round claimed the opposite; the claim was checked against the real config and corrected.
  • Steps 5a, 8a and 11a all edit one sentence in column-encryption.md in an order that Approach previously denied existed. The order is stated in all three Shared seams and in Approach.
  • Step 11a anchored on npm_remote_repositories_update.go, which stops naming the column at Step 10's head; it points at npmRemoteCredentialAssignments now.
  • The .md sweep handed an implementer twelve hits and ruled on eight.
  • Corrected citations: credential_table.md runs :85-93 and carries a second falsified claim; column-encryption.md's rollback sentence is :149; the report suites are :519-703 and :448-522.

An unrelated flaky test was blocking the image build

TestRebuildPackumentCache_WaitsOutItsBudgetForARenderingSlot failed the build-current-artifact-registry-arm64 job on head 04cf80f6, reporting gave up after 6.3854ms against its own 10ms floor. The untagged Go suite runs in the checkPhase of the Nix derivation that builds the service image, so one red test there costs any branch its image, a branch that touches no Go file included. #1014 has the flake open with the diagnosis: the test arms a 20ms budget at context.WithTimeout and starts measuring two statements later at time.Now(), so a pause between the two is spent off the measured clock and subtracted from every reading.

d211b0b arms the deadline off start instead, which leaves one clock, and derives the floor from the budget rather than repeating it as a literal. The floor still reddens on the behavior it defends: a scratch copy of reserveRenderingSlot given a default arm that refuses instantly fails it at 100.875us. The case passes 50 of 50 runs locally, and the internal/format/npm suite and golangci-lint are both clean.

Size

1894 reviewable LOC (1827 added, 67 removed), measured at f9c98bc9d, over the 500 line development-model.md asks about. 1884 of it (1820 added, 64 removed) is prose in the plan file, one concern: the Step 5/8/11 split and the findings against it. Splitting that would not help, because the blocking findings are cross-references between Step 5a, Step 5b, Step 8 and Step 11, so a split would put each half in a state where its own cross-references are wrong. The other 10 (7 added, 3 removed) is the flaky-test fix described in ## An unrelated flaky test was blocking the image build.

Merge order

Order Merge request Gate
1 !2628 (merged), this one none
2 !2629 (merged), stop binding the pair after !2628 (merged)
3 !2630 (merged), drop the columns after !2629 (merged)'s release is running in every environment its migration will run in

!2629 (merged) and !2630 (merged) are hard-blocked in that order through GitLab MR dependencies, so the stack cannot merge out of it.

Three other open MRs edit this plan file:

  • !2543 (merged), correcting four Step 4 claims. It edits the same Step 4 Scope hunk this MR now touches.
  • !2631 (merged), recording Step 10's three merge requests and its Maven file. Its Step 10 entry is where the //nolint:dupl twin that this MR's Step 5b conditional handles is introduced, so the two are complementary rather than competing.
  • !2633 (closed), moving Step 5's gate reading to the merging engineer. This one conflicts; it has its own subsection below.

!2543 (merged) and !2631 (merged) merge cleanly against this branch and against main, checked with git merge-tree at f9c98bc9d, and the merged prose reads correctly in each case, so they need no order between them.

!2633 (closed) conflicts on this file, and this MR merges first

!2633 (closed) rewrites the same Step 5 Acceptance sentence round five rewrote. git merge-tree at f9c98bc9d reports a conflict in docs/plans/2026-09-03-s04a-per-format-credential-columns.md. !2633 (closed) against main is still clean at that same measurement, so the conflict is with this branch specifically and no pipeline on either branch will report it.

Required order: this MR first, then !2633 (closed). The cost is asymmetric. !2633 (closed) is a one-sentence ruling plus two paragraphs written against the unsplit Step 5, whose single Acceptance this MR replaces with separate 5a and 5b entries. Landing this MR first leaves !2633 (closed) a small re-expression against the split shape. The reverse order puts this MR's whole Step 5 rewrite on the conflicted side, which is much more text to resolve by hand and the side a mis-resolution is easiest to hide in.

Whichever lands second resolves this plan file by hand. Two things the resolver has to settle rather than merge, because the two MRs do not simply reword each other:

  • When the reading is taken. This MR has the engineer shipping 5a take it before 5a's image reaches the pods, because once 5a is out the API clear no longer erases those rows. !2633 (closed) has it taken after !2629 (merged)'s rollout completes, on the premise that an earlier count is measured against a table pods are still writing the pair into. Both cannot hold. This MR's Step 4 analysis is what bears on that premise: under Step 4's release both update arms bind the pair to NULL and the insert arm names neither column, so the count can shrink but not grow.
  • Whose description records it. This MR records the reading in 5a's own description, !2629 (merged). !2633 (closed) records it in the drop step's description. The resolver picks one and makes all three sites agree with it, because the fact lives at Step 5a's Depends on, Step 5b's Acceptance and the cross-reference beside Step 5b's image-check readings.

!2633 (closed)'s own description currently states that no other open merge request touches this plan file. This MR, !2543 (merged) and !2631 (merged) all did when it was written. That correction belongs to !2633 (closed) and is not made here.

Apart from the flaky-test fix in internal/format/npm/packument_cache_internal_test.go, this MR only edits the plan.

Related to #417 (closed)

Edited by Dzmitry (Dima) Meshcharakou

Merge request reports

Loading
Loading