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 writewide 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 besidecredentials_cleared, with its emitter, the call site that booksaudit_action=clearand theobservability.mdrow that owns the field. 5a's Files bullet formaven_remote_repositories_update.gonamed one comment where three invert::133and:173-175sit in the doc comment of a file the step already opens, and:173-175already 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,:505and:519assert absolute values against it, the suite is//nolint:paralleltestso order is load-bearing, and the count query pins nonamespace_id, so seeding a leftover row before:509would 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 atmaven_remote_repositories_writes_test.go:159-162was called vacuous after the un-bind. It is not: it still asserts that the supplied plaintext never rides as a bound argument, and re-bindingpg.String(username)in the builder fires it. Only its message at:161goes 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:133joins the comments that invert. - Four smaller items, batched. The four
.gitlab-ci.ymlcitations in the merge-train bullet were four to five lines high. Re-derived:target_latestis:526-529, the added set:536-538, the loop:544-549, andlint: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 onmain. 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 bookscancellation_reason=deadline, whichobservability.md:27designates 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-112compares 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 onlycontainer_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.mdandS13-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,:94and: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_repositoriescarrying bothDROP COLUMNclauses, bracketed bySET statement_timeout = '120s'andSET lock_timeout = '10s'. The plan says the single statement is what bounds the Up, so the ceiling is 120s rather than 65 timeslock_timeout, and two statements would each take a freshstatement_timeoutand double it to 240s; 120s leaves 180s of the 5 minuteupTimeout(internal/datastore/migrations/runner.go:28) for the rest of the pending set and the goose advisory-lock retry.lock_timeoutis 10s rather than the tree's 5s, with the reason beside the value: 5s is exactlyleftoverPlaintextReadTimeout(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. EachSETis paired with aRESETafter theALTER TABLEwith the session-leak reason, and20260829134210_create_npm_virtual_upstream_rules.sql's Down is cited as the accumulation precedent: 65 drops under oneSET lock_timeout = '5s'at:614, paired withRESET lock_timeoutat:758.constraint-missing-not-validandprefer-robust-stmtsjoin the directives 5b rules on, both owed by the Down'sADD CONSTRAINT ... CHECK. Only theprefer-robust-stmtsreason carries over from20260904170000_add_maven_remote_encrypted_credentials.sql: itsconstraint-missing-not-validreason at:5is written for the foreign key and the same directive says the CHECK above that key does takeNOT VALIDon every version, so 5b's Down is given its own reason instead. The Tests line gains theRESETbeside 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
Downa 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 theDownis 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:390andupdate_delete_integration_test.go:840are both statements againstcontainer_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 baretmp_plaintextstem, because three assertions spell the columns as a fragment in the predicate the drop falsifies, and a third search oncheck_maven_remote_repositories_credentials_all_or_noneis added for the cluster that reaches the guard through its constraint name. Step 3 is credited with creatingcredential_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_infois 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_infocarriesimageandimage_id, andreleases.mdsays the image tag is the deployed version.versionis a real label on a different series named in the same paragraph, scopedenvironment=where the kube series is scopedenv=. 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.gois the shared credential table's real-database floor instantiated formaven_remote_repositories, and it names the dropped columns in raw SQL, so it carries neither amaven_filename nor the jet identifier the searches use. Step 5a's glob, the.mdsweep, 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.mdedit 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.:134still names re-entering the credential as the remedy, and:136still 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,
findContainerRemoteCredentialsStmtandcontainerRemoteHasCredentialsProjection, 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. updateContainerRemoteCredentialsStmtis 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
CONCURRENTLYrationale 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 TRANSACTIONruling rested on the 65 locks rolling back together, which a lone DDL statement gets from PostgreSQL either way. The reason that fires is goose'sgoose_db_versionwrite 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
ListRemoteson 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.
S13carries three generic sites, not two::93,:94and:98.:94is the password row, the member an amendment most easily leaves standing, which would leave the spec asserting Maven still stores it.S04-a:406is Maven's alone, not generic over Maven and Container. It sits between twoALTER TABLE maven_remote_repositoriesstatements 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:137is generic over Maven and Container, not all three formats: it says "the interim pair" and npm carries a singletmp_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_credentialsis not an API field; one hit repo-wide, in the plan itself. The wire form isPATCH … {"settings":{"credentials":null}}, and this is the passage 5a moves into the incident runbook.- The npm-side
NotContainspair,:41-42and 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_plaintextgrep 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 thereader, not the named constant;LeftoverPlaintextSampleLimitis referenced fromwire_remote.goonly inside a comment, so "a wrong deletion fails the build" does not cover it; the read deviation lives at:385as well as:566; the failed-read metric splits between the errors and cancellations families; the e2e row'shas_credentials: falseis 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-83to:77-79; the precedent's rollback sentence is in its file header, not its Down comment; thereleases.mdtype claim gains "absent a breaking-change marker";:406's five-shapes list is:152-156; the operator-SQL remedy is:164-165; thewantUnitliteral is:430; the:204-205quote 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
42703on every Maven credential write. It is a failed read forenv="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
fixtype stops one collapse route. A shared merge train and a single merge intomainreach 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_clearedreports the action the update took, not the encrypted unit:update.go:485isverdict.urlChanged && verdict.action == remote.CredentialClear, reading no column, so a plain clear returns200with the key absent. Both sites said "the encrypted unit". This is the passage 5a moves intokey-rotation.mdfor an incident responder, so the wrong operation was named in runbook text.- The release keeps one predicate reached by two statements, not "one
read":
countMavenRemoteLeftoverPlaintextStmtandselectMavenRemoteLeftoverPlaintextPageStmt. The one-metric-sample figure rests on the early return atmaven_remote_credentials.go:349, not on there being one query. - Step 5a deleting the
column-encryption.mdclause 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-515and theupTimeoutcrash-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/:98andS04-a:406are 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.gobreaks unconditionally, not conditionally: the Maven wrappers it drives are removed unconditionally.(Modify, comment only)was wrong;:519-520is an assertion's failure message.- The deviation from
database-migrations.mdhad 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.mdis the fourteenth site andcolumn-encryption.mdthe thirteenth; the report suite is:447-522;column-encryption.md:144-145states no duration, so the 5s figure is cited where it lives; theerror_messagediscriminator is conclusive on its own andversioncorroborates 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.mdis 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 ascolumn-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 andmaven_remote_credentials.gois 434 lines rather than 666, so the amendment's own:575,:632and:638-639cannot resolve there. The amendment's basis,85530efe0, is declared separately now. - The
.mdsweep 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.gopair assertions are not reached throughmavenRemoteCredentialUnitAssignments. They are, throughmavenRemoteUpdateAssignments, which appends it at:433and: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, exemptingwire_remote_test.go:646as asserting only that the two names differ; it hard-codes the literal twice, at:649and again at:652, which pins the renderedleftover-plaintext-report/maven. - Step 5b now retargets Maven's own four
NotContainssites (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 theNO TRANSACTIONchoice besideban-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; thecredentials_clearedresponse inside the window is named; the three newfixsteps 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 useassertStoredCredentialUnit, 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.goentry deleted format-generic machinery that Steps 7 and 10 register their own arms through. Only thewire.goregistration 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
LeftoverPlaintextCredentialsleaves npm's//nolint:dupldead once Step 10 lands, becauseduplneeds both bodies in one lint scope. No job reports it:.golangci.yamllistsnolintlintunderdisable. 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.mdin 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 atnpmRemoteCredentialAssignmentsnow. - The
.mdsweep handed an implementer twelve hits and ruled on eight. - Corrected citations:
credential_table.mdruns:85-93and carries a second falsified claim;column-encryption.md's rollback sentence is:149; the report suites are:519-703and: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:dupltwin 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
NULLand 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)