Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions core/workspace/device.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ type PlacementMatch struct {
Names []string `json:"names,omitempty"`
NamePrefixes []string `json:"name_prefixes,omitempty"`
Visibility []string `json:"visibility,omitempty"`
Lifecycle []string `json:"lifecycle,omitempty"`
}

func (match PlacementMatch) satisfiedBy(anchor domain.RepositoryAnchor) bool {
Expand Down Expand Up @@ -136,6 +137,10 @@ func (match PlacementMatch) satisfiedBy(anchor domain.RepositoryAnchor) bool {
!contains(match.Visibility, anchor.Classification.VisibilityContract) {
return false
}
if len(match.Lifecycle) != 0 &&
!contains(match.Lifecycle, anchor.Repository.Lifecycle) {
return false
}
return true
}

Expand Down
30 changes: 30 additions & 0 deletions core/workspace/device_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,36 @@ func TestResolvePlacementTraitMatch(t *testing.T) {
}
}

func TestResolvePlacementLifecycleMatch(t *testing.T) {
descriptor := testDevice()
descriptor.Materialization.Include = []MaterializationAssignment{{
Match: &PlacementMatch{
OwnerLogin: "example-org",
Lifecycle: []string{"active", "maintenance"},
},
WorkspaceRoot: "personal", Mode: "active",
}}
anchor := testWorkspaceAnchor()
anchor.Provider.Owner = "example-org"
anchor.Repository.Lifecycle = "archived"
home := filepath.Join(string(filepath.Separator), "home", "owner")
placement, findings := ResolvePlacement(descriptor, anchor, Environment{
Home: home, XDGStateHome: filepath.Join(home, ".local", "state"),
})
if placement.Mode != "absent" || len(findings) != 1 ||
findings[0].Code != "GDS_WORKSPACE_PLACEMENT_NOT_SELECTED" {
t.Fatalf("placement=%#v findings=%#v", placement, findings)
}

anchor.Repository.Lifecycle = "active"
placement, findings = ResolvePlacement(descriptor, anchor, Environment{
Home: home, XDGStateHome: filepath.Join(home, ".local", "state"),
})
if len(findings) != 0 || placement.WorkspaceRoot != filepath.Join(home, "Developer", "personal") {
t.Fatalf("placement=%#v findings=%#v", placement, findings)
}
}

func testDevice() DeviceDescriptor {
return DeviceDescriptor{
SchemaVersion: 1,
Expand Down
9 changes: 5 additions & 4 deletions docs/adr/0040-tenancy-is-the-provider-account.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,13 +34,14 @@ That coupling has real costs:
**A tenant is a provider account — nothing more, nothing else.** One
personal user account, and otherwise organizations by their exact provider
login. The model keeps `portfolio:` as the reference namespace but its values
are tenant names (`portfolio:nddev-it-com`, `portfolio:example-user`), never
are tenant names (`portfolio:example-org`, `portfolio:example-user`), never
purpose names.

1. **`materialization.include[].match` places repositories by facts, not
labels.** `match.owner_login`, `match.names`, `match.name_prefixes` and
`match.visibility` are evaluated against the repository's provider
identity and visibility contract. Includes evaluate in declaration order;
labels.** `match.owner_login`, `match.names`, `match.name_prefixes`,
`match.visibility` and `match.lifecycle` are evaluated against the
repository's provider identity, visibility contract and declared
lifecycle. Includes evaluate in declaration order;
the first match wins. `selector` membership placement stays valid for
compatibility but is no longer the reference mechanism.
2. **`policy.match.name_prefixes` lets cross-owner policies exist without a
Expand Down
2 changes: 1 addition & 1 deletion docs/contracts/estate-v1.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ fields remain readable for compatibility but do not select a separate
portfolio. Archive and name-specific selectors retain their own priority.
Device placement no longer requires a purpose portfolio at all:
`materialization.include[].match` selects repositories by provider facts
(owner login, exact names, name prefixes, visibility contract) with
(owner login, exact names, name prefixes, visibility contract, lifecycle) with
first-match-wins ordering, so a `server-` family can span several owners under
one root while every other repository lands under its owner's root.

Expand Down
9 changes: 9 additions & 0 deletions schemas/v1/device.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,15 @@
"items": {
"$ref": "common.schema.json#/$defs/visibility"
}
},
"lifecycle": {
"description": "Match repositories by declared lifecycle (for example keep `archived` repositories out of device workspaces).",
"type": "array",
"minItems": 1,
"uniqueItems": true,
"items": {
"$ref": "common.schema.json#/$defs/lifecycle"
}
}
}
},
Expand Down
Loading