You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
bug(dynamic): unportablized ruleset ids are written silently — wrong-host rulesets fail quietly, not loudly #101
Ruleset portablization only rewrites references to managed resources. Anything pointing at an
unmanaged target stays a raw numeric id — and nothing says so.
Across eqrm/ct-structure's rulesets/ (9 files), portablized: group-type-role ×22, campus ×7, group-type ×4, group ×2. Still raw in 5 of 9 files:
Ruleset
Dimensions left numeric
skidscheckinopskoblenz (adopted 2026-08-13)
ctgroup.id, role.id
sflow2_5viewer
ctgroup.groupTypeId, role.id
merkmal_alle_2_5_mz
ctgroup.id, person.campusId
sintegrationmeeting
ctgroup.id
skidscheckinopsmz
ctgroup.id
Why it matters: it fails SILENTLY and WRONGLY
ChurchTools treats a ruleset as opaque JSON. It does not validate the ids inside. So a ruleset
carrying prod's ctgroup.id applied to dev does not error — the auto-group simply collects the
wrong people, or nobody, and ct plan stays green because the ruleset round-trips byte-identically
against the host it was written for.
Compare with how #98 handled the same class of problem for permission scopes: an unresolvable { department: … } ref is a hard error, explicitly "strictly better than a silent numeric
misgrant". Rulesets currently take the opposite position for the same kind of id.
This is materially worse than a wrong permission scope, because the payload of an auto-group is
group membership — and in this instance Struktur membership carries 33 grants.
Suggested fix
Ranked by value:
Report what could not be portablized. At ct adopt --with-dynamic and at plan time, list
each dimension left numeric and why (target not managed / no ref kind for this dimension). Even
with no other change this converts silent wrongness into a known risk.
Extend the ref kinds to the dimensions that have resolvable catalogs — role.id maps to role-def (/group/roles), which already exists as a RefKind, and person.campusId / ctgroup.groupTypeId are just campus and group-type refs appearing in a ruleset position rather
than a scope position.
A strictRulesets: true config switch that refuses to write a ruleset containing an
unportablized id would let a repo opt into the safe behaviour before it becomes the default.
Acceptance
Adoption reports every dimension it left numeric, with the reason.
role.id, person.campusId and ctgroup.groupTypeId portablize when the target resolves.
ctgroup.id portablizes when the target is managed; otherwise it is surfaced, not swallowed.
A repo can opt into "refuse to write an unportablized ruleset".
Related: #76 (the original portablization work), #98 (the resolve-or-error precedent for scopes).
Live evidence: a plan that is about to write prod ids onto another host
The dev plan for eqrm/ct-structure#19, adopting sKidsCheckinOpsKoblenz from prod and applying it to dev:
+ group.skidscheckinopskoblenz
groupStatusId: 1
name: "sKidsCheckinOpsKoblenz"
groupTypeId: 15 ← correctly resolved (prod 9 → dev 15)
dynamic: {"description":"Automatische Mitgliedschaft für Gruppe #1092",
…"oneof":[{"var":"ctgroup.id"},[3200,3090]]…
…"oneof":[{"var":"role.id"},[84,85,17,16]]…
…"groupTypeRoleId":66…}
Plan: 1 to create, 0 to update, 0 to delete.
Everything ct models as a resource portablized correctly — groupTypeId resolved 9 → 15 across hosts. Everything inside the ruleset did not:
ctgroup.id: [3200, 3090] are prod group ids (3200 = Bereich Kids Koblenz on prod).
role.id: [84,85,17,16] and groupTypeRoleId: 66 are prod ids.
Even the human-readable description embeds prod's group #1092.
On dev those ids address different groups or none. CT accepts the ruleset regardless, so the outcome is an auto-group that quietly collects the wrong people — with a green plan, a green apply, and a subsequent ct plan that is a clean no-op because the ruleset round-trips byte-identically.
Note the contrast within a single plan line: the resource fields are portable and the ruleset payload is not, yet nothing in the output distinguishes them. That is the whole of this issue in one diff — the plan renders host-specific ids and portable refs identically, so a reviewer has no signal.
This is also why the dev instance cannot currently serve as a faithful rehearsal for auto-groups: the rehearsal exercises the apply path but not the ruleset semantics.
Activity
2000game commentedon Aug 13, 2026
Live evidence: a plan that is about to write prod ids onto another host
The dev plan for eqrm/ct-structure#19, adopting
sKidsCheckinOpsKoblenzfrom prod and applying it to dev:Everything ct models as a resource portablized correctly —
groupTypeIdresolved 9 → 15 across hosts. Everything inside the ruleset did not:ctgroup.id: [3200, 3090]are prod group ids (3200 =Bereich Kids Koblenzon prod).role.id: [84,85,17,16]andgroupTypeRoleId: 66are prod ids.descriptionembeds prod's group #1092.On dev those ids address different groups or none. CT accepts the ruleset regardless, so the outcome is an auto-group that quietly collects the wrong people — with a green plan, a green apply, and a subsequent
ct planthat is a clean no-op because the ruleset round-trips byte-identically.Note the contrast within a single plan line: the resource fields are portable and the ruleset payload is not, yet nothing in the output distinguishes them. That is the whole of this issue in one diff — the plan renders host-specific ids and portable refs identically, so a reviewer has no signal.
This is also why the dev instance cannot currently serve as a faithful rehearsal for auto-groups: the rehearsal exercises the apply path but not the ruleset semantics.
fix(adopt): page /groups/{id}/children and prefer domainIdentifier