fix(state): preserve state.components on flat single-component write - #630
Merged
Conversation
The flat state map models environments only, so parsing a component-scoped
manifest lifted state.components into it as an empty entry keyed "components".
Rebuilding the whole state node from that map collapsed state.components to {}
and silently dropped every recorded component row while the write returned
success. A finalize invoked without --component on such a manifest could land
that loss on trunk.
Treat components as a reserved, unowned subtree on the single-component write
path: never rebuild it from the flat map, and preserve any existing subtree
verbatim. A single-component manifest with no components subtree emits
byte-identical output.
Signed-off-by: Joshua Temple <joshua.temple@stablekernel.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
The flat state map (
CICDFile.State,map[string]*EnvState) models environments only. Parsing a component-scoped manifest liftsstate.componentsinto that map as a bogus, emptyEnvStatekeyedcomponents.WriteManifestStatethen rebuilds the wholestatenode from that map, which collapsesstate.componentsto{}and silently destroys every recorded per-component row while the write returns success. ViaCommitWithRetrythis can land on trunk.Reproduced executably: a component-scoped manifest with
state.components.api.{staging,prod}rows, run throughParseManifestBytesthenWriteManifestState(exactly what a finalize invoked without--componentruns), emitsstate.components: {}with all rows gone.Reachability
Not generator-reachable. Generated per-component workflows always emit
--component(the single-component workflow is the only one that omits it), and the finalize dispatch routes any--componentinvocation to the node-patchingWriteScopedState, which preserves siblings. The flat wipe is reachable only via a stale pre-migration flat workflow (a repo that added components but kept an old non-component workflow) or a manual invocation. No runtime guard refusescomponent == ""on a component manifest. Verdict: a Major silent-corruption hazard, narrower than a generator-reachable Blocker, but a real path to silent trunk state loss.Fix
Root-cause fix at the single write point, covering every flat-path caller (promote, hotfix, rollback, reset, orchestrate, external) at once.
componentsis now a reserved, unowned subtree on the single-component write path: it is never rebuilt from the flat map, and any existingstate.componentssubtree is preserved verbatim across the rebuild. Chosen over a per-caller refuse because it is provably non-destructive, keeps byte-identity, and does not risk breaking legitimate flat operations on component manifests.Verification
components:subtree) against the whole-node-replace oracle.--componentwrite.go build ./...,go test ./...(3450 pass),-raceon state/finalize packages,golangci-lint run ./...all clean. e2e module builds and vets.cascade verify --own-repo: no drift.No e2e scenario: the generated-workflow harness always passes
--component, so it cannot naturally express a flat finalize on a component manifest; the unit test at the real write path is the correct coverage.