@@ -84,9 +84,59 @@ type MaterializationPolicy struct {
8484}
8585
8686type MaterializationAssignment struct {
87- Selector string `json:"selector"`
88- WorkspaceRoot string `json:"workspace_root"`
89- Mode string `json:"mode"`
87+ Selector string `json:"selector,omitempty"`
88+ Match * PlacementMatch `json:"match,omitempty"`
89+ WorkspaceRoot string `json:"workspace_root"`
90+ Mode string `json:"mode"`
91+ }
92+
93+ // PlacementMatch selects repositories by their own facts -- provider owner
94+ // login, repository name and visibility contract -- so placement does not
95+ // depend on a label the anchor must carry. Every declared field must match;
96+ // an empty field matches nothing by itself but narrows nothing either.
97+ type PlacementMatch struct {
98+ OwnerLogin string `json:"owner_login,omitempty"`
99+ Names []string `json:"names,omitempty"`
100+ NamePrefixes []string `json:"name_prefixes,omitempty"`
101+ Visibility []string `json:"visibility,omitempty"`
102+ }
103+
104+ func (match PlacementMatch ) satisfiedBy (anchor domain.RepositoryAnchor ) bool {
105+ if match .OwnerLogin != "" &&
106+ ! strings .EqualFold (match .OwnerLogin , anchor .Provider .Owner ) {
107+ return false
108+ }
109+ if len (match .Names ) != 0 {
110+ found := false
111+ for _ , name := range match .Names {
112+ if strings .EqualFold (name , anchor .Provider .Name ) {
113+ found = true
114+ break
115+ }
116+ }
117+ if ! found {
118+ return false
119+ }
120+ }
121+ if len (match .NamePrefixes ) != 0 {
122+ found := false
123+ for _ , prefix := range match .NamePrefixes {
124+ if strings .HasPrefix (
125+ strings .ToLower (anchor .Provider .Name ), strings .ToLower (prefix ),
126+ ) {
127+ found = true
128+ break
129+ }
130+ }
131+ if ! found {
132+ return false
133+ }
134+ }
135+ if len (match .Visibility ) != 0 &&
136+ ! contains (match .Visibility , anchor .Classification .VisibilityContract ) {
137+ return false
138+ }
139+ return true
90140}
91141
92142type DeviceStatePolicy struct {
@@ -165,13 +215,27 @@ func ResolvePlacement(
165215 anchor domain.RepositoryAnchor ,
166216 environment Environment ,
167217) (Placement , []domain.Finding ) {
168- matches := make ([]MaterializationAssignment , 0 , 1 )
169- for _ , assignment := range descriptor .Materialization .Include {
170- if contains (anchor .Classification .Portfolios , assignment .Selector ) {
171- matches = append (matches , assignment )
218+ // Includes are evaluated in declaration order and the first matching
219+ // assignment wins. A specialized rule therefore precedes the generic rule
220+ // it narrows, and a `selector` membership test and a `match` trait test may
221+ // coexist in one list without ambiguity findings.
222+ var assignment * MaterializationAssignment
223+ for index := range descriptor .Materialization .Include {
224+ candidate := descriptor .Materialization .Include [index ]
225+ if candidate .Match != nil {
226+ if candidate .Match .satisfiedBy (anchor ) {
227+ assignment = & descriptor .Materialization .Include [index ]
228+ break
229+ }
230+ continue
231+ }
232+ if candidate .Selector != "" &&
233+ contains (anchor .Classification .Portfolios , candidate .Selector ) {
234+ assignment = & descriptor .Materialization .Include [index ]
235+ break
172236 }
173237 }
174- if len ( matches ) == 0 {
238+ if assignment == nil {
175239 return Placement {
176240 DeviceID : descriptor .Device .ID , RepositoryID : anchor .Repository .ID ,
177241 Mode : descriptor .Materialization .DefaultMode ,
@@ -180,13 +244,6 @@ func ResolvePlacement(
180244 "Repository does not match a device materialization assignment." , anchor .Repository .ID ,
181245 )}
182246 }
183- if len (matches ) != 1 {
184- return Placement {}, []domain.Finding {workspaceFinding (
185- "GDS_WORKSPACE_PLACEMENT_AMBIGUOUS" ,
186- "Repository matches more than one device materialization assignment." , anchor .Repository .ID ,
187- )}
188- }
189- assignment := matches [0 ]
190247 portableRoot , found := descriptor .WorkspaceRoots [assignment .WorkspaceRoot ]
191248 if ! found {
192249 return Placement {}, []domain.Finding {workspaceFinding (
0 commit comments