feat(converge): --only <member> scopes plan/apply/destroy (#306) - #324
Merged
Conversation
converge was all-or-nothing across a stack, which is a hazard for any stack holding an adopted member. Adopted guests reappear as perpetual drift, so an apply aimed at one new member also wanted to write to them: on SmartHome an unscoped plan proposes SetConfig on VM 2000 (Home Assistant, documented describe-only) plus a tag drift-fix on CT 4100. Podman phases 1 (#285) and 2a (#302) both dodged that by applying from a hand-copied stack directory holding stack.yaml plus the one member — which silently drops dependsOn ordering and cross-member context, and has to be rebuilt correctly every time. --only replaces it. converge stacks/SmartHome --only podman-host --apply converge stacks/monitoring --only prometheus,grafana Filtering happens AFTER load and topological ordering, never before, so a selected member keeps its merged stack defaults and its position in the full dependency order; the only thing that changes is which members get acted on. It narrows destroy too — arguably where scoping matters most, since the unscoped form takes out every CT in the stack. Two guards, both chosen deliberately: - An unknown name is FATAL (exit 2, nothing touched). Converging nothing on a typo would report "0 applied" and exit 0, i.e. read as success — the worst possible outcome for a flag whose purpose is making apply safer. - A dependsOn left OUTSIDE the selection is being assumed already converged, so that gets verified rather than trusted. #306 weighed implicitly pulling dependencies in against failing loudly; pulling them in would mean --only writes to members the operator did not name, which is the exact surprise it exists to prevent. One subtlety worth naming: the ConvergeContext byName map is still built from the WHOLE stack, not the selection. Service-derived secrets and the forgejo-runner create-vars resolve dependencies by name, so a scoped apply must still be able to reach a dependency it is not itself converging. Verified live against SmartHome: `--only podman-host --apply` reports 1 selected / 6 skipped and NOCHANGE, and the VM section never runs at all. This scopes the symptom, not the cause — adopted members still have no way to declare themselves unmanaged. A shape-level `manage: describe-only` marker is the complementary fix and stays open. 23 new tests (163 total).
This was referenced Jul 28, 2026
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.
Closes #306.
convergewas all-or-nothing across a stack.--only <member>[,<member>…]scopes plan, applyand destroy to named members.
converge stacks/SmartHome --only podman-host --apply converge stacks/monitoring --only prometheus,grafana converge stacks/Media --only bazarr --destroy # scoping matters most hereThe hazard, reproduced
An unscoped plan on SmartHome today:
SetConfigon VM 2000 — the adopted,stack.yaml-documented describe-only member — plus adrift-fix on an unrelated container mid-migration. Scoped:
Verified live. The VM section never runs at all.
What it replaces
Podman phases 1 (#285) and 2a (#302) were both applied from a hand-copied stack directory —
stack.yamlplus the one member — to dodge exactly the above. That workaround silently dropsdependsOnordering and cross-member context, and has to be rebuilt correctly every time. Theplan doc now points at
--onlyinstead.Design
Filtering happens after load and topological ordering, never before. A selected member keeps
its merged stack defaults and its position in the full dependency order; the only thing that
changes is which members get acted on. VM members are filtered too — the adopted member that
motivated #306 is a VM, so a filter that only narrowed the LXC side would be useless.
Two guards, both deliberate:
would print
0 appliedand exit 0, i.e. read as success. That is the worst available outcomefor a flag whose entire purpose is making apply safer. The error lists the stack's real members.
dependsOnoutside the selection is verified, not trusted. converge needs a--only <member>filter — applying a stack touches adopted describe-only members #306 weighed implicitly pullingdependencies in against failing loudly. Loud won: pulling them in would mean
--onlywrites tomembers the operator did not name, which is the exact surprise it exists to prevent. The run
checks the dependency exists live and refuses that member otherwise — before any work on it, so
there is no half-applied state.
One subtlety
ConvergeContext.ByNameis still built from the whole stack, not the selection. Service-derivedsecrets and the
forgejo-runnercreate-vars resolve dependencies by name, so a scoped apply muststill be able to reach a dependency it is not itself converging. Narrowing that map alongside the
iteration list would have broken secret derivation in a way no existing test covers.
Tests
23 new (163 total, all passing.)
MemberSelectionTestscovers parsing (--only a,b,--only=a,b, repetition,--only --applynot selecting a member literally named--apply),resolution (ordering preserved, VM filtering, case sensitivity, unknown/empty rejection) and
dependency reporting.
ConvergeOnlyApplyTestsdrivesApplyAsyncover a throwaway stack dir witha fake exec to cover the guard: fires when the dependency is absent, and — the regression that
would make
--onlyuseless — does not fire when it is present.Scope
This scopes the symptom. The root cause is that adopted members cannot declare themselves
unmanaged, so they resurface as perpetual drift in every plan. The complementary shape-level
manage: describe-onlymarker is noted in #306 and stays open.