fix: emit group.member_added only for the group named in the mutation (closes #10314) - #10316
fix: emit group.member_added only for the group named in the mutation (closes #10314)#10316iddocohen wants to merge 7 commits into
Conversation
There was a problem hiding this comment.
All reported issues were addressed across 1 file
Shadow auto-approve: would not auto-approve because issues were found.
Re-trigger cubic
Secondary changelogs looked up the peer's relationship by identifier alone. When both sides of an identifier live on the same kind that returns the wrong side: a group enrolled into another group has only its own `members` under `group_member`, so the changelog claimed the enrolled group had gained the enclosing group as a member and a second `group.member_added` event fired against it. Resolve the peer side by requiring the opposite direction, and never treat a bidirectional relationship as its own reverse, since both sides of a bidirectional pair are stored asymmetrically. This also disambiguates `parent` from `children`, which share one identifier on hierarchical kinds.
|
AGENT_REVIEW_VERDICT: TEST_APPROVED Overall verdict: APPROVED WITH SUGGESTIONSThe test A. Test realism — PASS
B. Test correctness — PASS
C. Test quality — PASS
D. Alignment with analysis — PASS
Suggestions (non-blocking)
Recommended next steps
AGENT_REVIEW_ITERATION: test-1
|
There was a problem hiding this comment.
1 issue found across 4 files (changes from recent commits).
Confidence score: 3/5
- In
backend/infrahub/core/schema/basenode_schema.py, the name-only guard can drop a valid peer relationship when different kinds reuse the same bidirectional relationship name, causing incorrect relationship behavior; compare schema/relationship identity so only genuinely self-referential relationships are excluded.
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="backend/infrahub/core/schema/basenode_schema.py">
<violation number="1" location="backend/infrahub/core/schema/basenode_schema.py:400">
P2: When two different kinds use the same name for a bidirectional relationship, this name-only guard drops the valid peer relationship. Compare schema/relationship identity so only a genuinely self-referential relationship is skipped.</violation>
</file>
Shadow auto-approve: would not auto-approve because issues were found.
Re-trigger cubic
| for candidate in self.get_relationships_by_identifier(id=relationship.get_identifier()): | ||
| if candidate.direction != expected_direction: | ||
| continue | ||
| if candidate.direction == RelationshipDirection.BIDIR and candidate.name == relationship.name: |
There was a problem hiding this comment.
P2: When two different kinds use the same name for a bidirectional relationship, this name-only guard drops the valid peer relationship. Compare schema/relationship identity so only a genuinely self-referential relationship is skipped.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At backend/infrahub/core/schema/basenode_schema.py, line 400:
<comment>When two different kinds use the same name for a bidirectional relationship, this name-only guard drops the valid peer relationship. Compare schema/relationship identity so only a genuinely self-referential relationship is skipped.</comment>
<file context>
@@ -378,6 +383,26 @@ def get_relationships_by_identifier(self, id: str) -> list[RelationshipSchema]:
+ for candidate in self.get_relationships_by_identifier(id=relationship.get_identifier()):
+ if candidate.direction != expected_direction:
+ continue
+ if candidate.direction == RelationshipDirection.BIDIR and candidate.name == relationship.name:
+ continue
+ return candidate
</file context>
There was a problem hiding this comment.
The mechanism is real but the conclusion is inverted for this codebase, so keeping the name comparison.
The reported failure is two different kinds using the same name: CoreRepositoryGroup.members and CoreGeneratorGroup.members, both inherited from CoreGroup with identifier group_member. Skipping that is the fix, not a regression.
Comparing relationship identity cannot separate the two cases. Inherited relationships get id = None (node_inheritance_handler.py:81) and, unlike attributes, carry no source_*_id, so there is no token shared between the two members. Comparing owning kinds instead only skips a same-kind pair, which leaves the reported cross-kind case emitting the spurious event. Verified: swapping the guard for an identity comparison makes the replication test fail again.
Your comment did expose a genuine gap, and thanks for it: the test used one kind on both sides, so it could not tell the two implementations apart. 41b14eb8 enrols a CoreStandardGroup into a TestTrackingGroup instead, matching the reported scenario, and it now fails under the identity-based guard.
On the residual risk: a same-name bidirectional pair across two kinds that is a genuine two-sided relationship would lose its peer-side changelog. No such pair exists in the core schema — every two-sided pair names its sides differently (checks/validator, comments/thread, roles/permissions, ip_addresses/ip_prefix), and the one identifier shared by two same-named relationships (ippool__resource) is on sibling pool kinds that are never each other's peer.
There was a problem hiding this comment.
0 issues found across 1 file (changes from recent commits).
Confidence score: 5/5
- Automated review surfaced no issues in the provided summaries.
- No files require special attention.
Shadow auto-approve: would not auto-approve. Auto-approval blocked by 1 unresolved issue from previous reviews.
Re-trigger cubic
|
AGENT_REVIEW_VERDICT: FIX_APPROVED Overall verdict: APPROVED WITH SUGGESTIONSThe fix resolves the root cause precisely and minimally. Enrolling group B into group A now emits a single A. Correctness — PASS
B. Code quality — PASS
C. Documentation alignment — PASS
D. Test quality — PASS
Suggestions (non-blocking)
Recommended next steps
AGENT_REVIEW_ITERATION: fix-1
|
The reported failure enrols a group into a tracking group of another kind, so covering it with one kind on both sides let a narrower peer-side check pass while the reported case stayed broken.
There was a problem hiding this comment.
0 issues found across 1 file (changes from recent commits).
Confidence score: 5/5
- Automated review surfaced no issues in the provided summaries.
- No files require special attention.
Shadow auto-approve: would auto-approve. Fixes a bug that emitted a spurious group.member_added event for the enrolled group when adding a group as a member of another group. The change corrects reverse-relationship resolution and adds a focused regression test; no schema, API, or operational tradeoffs are introduced.
Re-trigger cubic
| if candidate.direction != expected_direction: | ||
| continue | ||
| if candidate.direction == RelationshipDirection.BIDIR and candidate.name == relationship.name: |
There was a problem hiding this comment.
Can we unify this if in one and also leave a code comment explaining the logic?
There was a problem hiding this comment.
Done in bc26b3d: one condition plus a comment on why the name is what separates an inherited same-name pair (CoreGroup.members on two kinds) from a genuine two-sided relationship, and I checked the new guard against the old two-if version across every schema/relationship pair in the core schema (33,028 comparisons, 2,830 resolved peers, 0 mismatches).
Fold the direction check and the bidirectional same-name check into a single condition and document why the name is what separates an inherited same-name pair from a genuine two-sided relationship.
ogenstad
left a comment
There was a problem hiding this comment.
Adding a group as a member of another group
I think the premise of this bug report and fix needs some work. In Infrahub it's not currently possible to add a group as a member of another group. So it feels like something is wrong today. Possibly the root cause is that the mutation doesn't immediately reject such actions. Is this the case?
While we can argue about what should be possible the CoreGroup member relationship (https://github.com/opsmill/infrahub/blob/infrahub-v1.10.8/backend/infrahub/core/schema/definitions/core/group.py#L46) points to CoreNode and a group is not a CoreNode type object.
There was a problem hiding this comment.
0 issues found across 1 file (changes from recent commits).
Confidence score: 5/5
- Automated review surfaced no issues in the provided summaries.
- No files require special attention.
Shadow auto-approve: would require human review. Fixes spurious group.member_added event by adding a name-based rejection rule in get_reverse_relationship; this heuristic may reject legitimate reverse relationships and the author explicitly requests a maintainer's opinion, so human review is needed.
Re-trigger cubic
|
@ogenstad groups, as far as I understand the code, can be members of other groups today. The check allows any kind that inherits something, and every group kind inherits CoreGroup. Infrahub relies on this. Every repository import puts the repository's groups into a CoreRepositoryGroup so they appear in the repository's Objects tab, which is exactly the mutation in question. So rejecting the mutation is, as far as I think, not the right choice. The use case: a customer's repository declares a group in objects/groups.yml and a CoreGroupTriggerRule that runs a generator when something is added to that group — the documented enrol-then-materialise pattern for auto-provisioning services. That is a normal, supported way to wire a repository. The harm: every time they push a commit, the import fabricates a "member added" event for that group, so the generator runs against Infrahub's internal repository group and fails. They get a red generator run per trigger rule on every import, forever — failures that look exactly like real generator failures in the task list. Why it matters: it makes "did my pipeline run clean?" unanswerable without opening each failure by hand, and it rules out any automated gate on failed tasks, since the baseline is never zero. For a customer running this in CI or during a DR rebuild, that is the difference between a usable signal and permanent noise they learn to ignore. |
Why
Adding a group as a member of another group emitted a second
infrahub.group.member_addedevent naming the enrolled group as the one that gained a member, with the enclosing group as the member. Every repository import hits this, because object import wraps each pass in aCoreRepositoryGrouptracking group and enrols every group defined inobjects/*.ymlinto it. ACoreGroupTriggerRulewatching one of those groups matched the spurious event and ran itsCoreGeneratorActionagainst the tracking group, which then failed withValueError: Target <id> is not part of the group <id>.Goal: one membership change produces one
member_addedevent, for the group named in the mutation.Non-goals: this PR does not change the import path, the tracking group, or which peers it collects (#6886, #9248), the import ordering for repository-defined trigger rules (#10063 / #10196), or the fact that a group-to-group membership is readable from both groups (see Risky or uncertain parts).
Closes #10314
What changed
Behavioral changes:
member_addedevent, for A with B as the member. Previously a second event fired for B, claiming it had gained A.NodeUpdatedEventthat accompanied that spurious event is gone too, so aCoreNodeTriggerRulewatchingmemberson B no longer fires either.parentandchildrenshare one identifier on hierarchical kinds, and the previous lookup returned whichever appeared first in the schema's relationship list.Implementation notes:
RelationshipChangelogGetter: it resolved the peer's "reverse" relationship withget_relationship_by_identifier, which matches on identifier alone. Group kinds never receivemember_of_groups(add_groups()skips anything inheritingCoreGroup), so a group peer's onlygroup_memberrelationship is its own inheritedmembers— the same side that was just changed, not the reverse.BaseNodeSchema.get_reverse_relationship(), which requires the candidate to point the opposite way (RelationshipDirection.neighbor_direction) and never accepts a bidirectional relationship as its own reverse. It reuses the existingget_relationships_by_identifierrather than introducing new machinery._process_added_peersand_process_removed_peersnow call it. The existingif peer_relation:guard already handles "no reverse relationship", so no control flow changed.What stayed the same:
add_groups()was deliberately not changed to injectmember_of_groupsonto group kinds: that would add a field to every group in the public API and still leave two bidirectionalgroup_memberrelationships on one kind to disambiguate.get_relationship_by_identifieris untouched and keeps its eight other callers.How to review
Focus on
backend/infrahub/core/schema/basenode_schema.py— specifically whether the two rejection rules inget_reverse_relationshipare the right ones. Everything else follows from it; the changelog change is a two-line call-site swap.The second rule compares relationship names, which deliberately also rejects two different kinds that share a name: the reported failure is exactly that shape,
CoreRepositoryGroup.membersandCoreGeneratorGroup.members, both inherited fromCoreGroup. Restricting the rule to a self-referential relationship instead does not fix the report, and there is no identity token to compare on — inherited relationships getid = Noneand carry nosource_*_id. The replication test therefore enrols a group into a group of a different kind; it fails again if the rule is narrowed to an identity comparison.Risky or uncertain parts:
members, reloading B also lists A. Both kinds read the one stored link through the samemembersrelationship, so the graph genuinely cannot say which group gained a member; only the mutation can. This makes the issue's statement that "nothing in the graph changed" inaccurate, and it means suppressing the second event is a decision about event semantics (report the intent of the mutation) rather than a correction of a claim the graph contradicts. The test pins the current storage behavior so a future change to the group data model surfaces here. If you would rather group-to-group membership were asymmetric, that is a separate modelling change, adjacent to CoreAccountRole cannot be added to CoreRepositoryGroup.members — repository import fails (also affects CoreGraphQLQuery) #9248.OUTBOUND, oneBIDIR). Core schemas contain no such pair: the only explicit directions are the properly inverse hierarchy pair and the resource-pool relationship, whose identifier exists on one side only and already resolved to nothing. A user-defined mismatched pair cannot be traversed from the peer's side anyway.Alternatives considered: guarding in
GroupNodeMutationParseror inCoreGroupTriggerRule, and tolerating an empty intersection in_run_generators. All three leave the fabricated changelog in place and only hide its effects; the last one would also mask genuine misconfiguration, which is what that error exists to report. Gating oninfrahub.node.actiondoes not work: it isCREATEDfor everyGroupMemberAddedEvent.How to test
Results locally: 2 passed in the test file (it failed as
assert [enclosing] == [enclosing, enrolled]before the fix), 158 passed across the blast-radius suites, 1435 unit tests passed.uv run invoke format,main.lintandbackend.lint(ruff, ty, mypy) clean.backend.generate,schema.generate-graphqlschema,schema.generate-jsonschemaanddocs.generateproduce no diff, so frontend codegen cannot be affected either.docs.lint/docs.formatcould not run locally (markdownlint-cli2not installed); no markdown other than the changelog fragment changed.Impact & rollout
Checklist
uv run towncrier create ...)dev/knowledge/backend/events.mdonce maintainers decide whether it is intended