Skip to content

Commit 5afcc60

Browse files
committed
feat: set site execution order
1 parent 61a2404 commit 5afcc60

21 files changed

+586
-46
lines changed
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
kind: Added
2+
body: Added option to set site execution order
3+
time: 2025-07-08T11:40:44.556019581+02:00

internal/batcher/batcher.go

Lines changed: 54 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,57 @@
11
package batcher
22

3-
import "github.com/mach-composer/mach-composer-cli/internal/graph"
3+
import (
4+
"fmt"
5+
"github.com/mach-composer/mach-composer-cli/internal/config"
6+
"github.com/mach-composer/mach-composer-cli/internal/graph"
7+
"slices"
8+
)
49

5-
type BatchFunc func(g *graph.Graph) map[int][]graph.Node
10+
type BatchFunc func(g *graph.Graph) (map[int][]graph.Node, error)
11+
12+
type Batcher string
13+
14+
func Factory(cfg *config.MachConfig) (BatchFunc, error) {
15+
switch cfg.MachComposer.Batcher.Type {
16+
case "":
17+
fallthrough
18+
case "simple":
19+
return simpleBatchFunc(), nil
20+
case "site":
21+
var siteOrder, err = DetermineSiteOrder(cfg)
22+
if err != nil {
23+
return nil, fmt.Errorf("failed determining site order: %w", err)
24+
}
25+
26+
return siteBatchFunc(siteOrder), nil
27+
default:
28+
return nil, fmt.Errorf("unknown batch type %s", cfg.MachComposer.Batcher.Type)
29+
}
30+
}
31+
32+
func DetermineSiteOrder(cfg *config.MachConfig) ([]string, error) {
33+
var identifiers = cfg.Sites.Identifiers()
34+
var siteOrder = make([]string, len(identifiers))
35+
36+
if len(cfg.MachComposer.Batcher.SiteOrder) > 0 {
37+
// Use the site order from the configuration if provided
38+
siteOrder = cfg.MachComposer.Batcher.SiteOrder
39+
40+
// Make sure the site order contains the same fields as the identifiers
41+
if len(siteOrder) != len(identifiers) {
42+
return nil, fmt.Errorf("site order length %d does not match identifiers length %d", len(siteOrder), len(identifiers))
43+
}
44+
for _, siteIdentifier := range siteOrder {
45+
if !slices.Contains(identifiers, siteIdentifier) {
46+
return nil, fmt.Errorf("site order contains siteIdentifier %s that is not in the identifiers list", siteIdentifier)
47+
}
48+
}
49+
50+
} else {
51+
for i, identifier := range identifiers {
52+
siteOrder[i] = identifier
53+
}
54+
}
55+
56+
return siteOrder, nil
57+
}

internal/batcher/batcher_test.go

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
package batcher
2+
3+
import (
4+
"github.com/mach-composer/mach-composer-cli/internal/config"
5+
"github.com/stretchr/testify/assert"
6+
"testing"
7+
)
8+
9+
func TestReturnsErrorWhenUnknownBatchType(t *testing.T) {
10+
cfg := &config.MachConfig{
11+
MachComposer: config.MachComposer{
12+
Batcher: config.Batcher{
13+
Type: "unknown",
14+
},
15+
},
16+
}
17+
18+
_, err := Factory(cfg)
19+
assert.Error(t, err)
20+
assert.Contains(t, err.Error(), "unknown batch type unknown")
21+
}
22+
23+
func TestReturnsSimpleBatchFuncWhenTypeIsEmpty(t *testing.T) {
24+
cfg := &config.MachConfig{
25+
MachComposer: config.MachComposer{
26+
Batcher: config.Batcher{
27+
Type: "",
28+
},
29+
},
30+
}
31+
32+
batchFunc, err := Factory(cfg)
33+
assert.NoError(t, err)
34+
assert.NotNil(t, batchFunc)
35+
}
36+
37+
func TestReturnsSimpleBatchFuncWhenTypeIsSimple(t *testing.T) {
38+
cfg := &config.MachConfig{
39+
MachComposer: config.MachComposer{
40+
Batcher: config.Batcher{
41+
Type: "simple",
42+
},
43+
},
44+
}
45+
46+
batchFunc, err := Factory(cfg)
47+
assert.NoError(t, err)
48+
assert.NotNil(t, batchFunc)
49+
}
50+
51+
func TestReturnsSiteBatchFunc(t *testing.T) {
52+
cfg := &config.MachConfig{
53+
MachComposer: config.MachComposer{
54+
Batcher: config.Batcher{
55+
Type: "site",
56+
},
57+
},
58+
Sites: config.SiteConfigs{
59+
{
60+
Identifier: "site-1",
61+
},
62+
{
63+
Identifier: "site-2",
64+
},
65+
},
66+
}
67+
68+
batchFunc, err := Factory(cfg)
69+
assert.NoError(t, err)
70+
assert.NotNil(t, batchFunc)
71+
}
72+
73+
func TestDetermineSiteOrderReturnsIdentifiersWhenNoSiteOrderProvided(t *testing.T) {
74+
cfg := &config.MachConfig{
75+
MachComposer: config.MachComposer{
76+
Batcher: config.Batcher{},
77+
},
78+
Sites: config.SiteConfigs{
79+
{Identifier: "site-1"},
80+
{Identifier: "site-2"},
81+
},
82+
}
83+
order, err := DetermineSiteOrder(cfg)
84+
assert.NoError(t, err)
85+
assert.Equal(t, []string{"site-1", "site-2"}, order)
86+
}
87+
88+
func TestDetermineSiteOrderReturnsSiteOrderWhenProvided(t *testing.T) {
89+
cfg := &config.MachConfig{
90+
MachComposer: config.MachComposer{
91+
Batcher: config.Batcher{
92+
SiteOrder: []string{"site-2", "site-1"},
93+
},
94+
},
95+
Sites: config.SiteConfigs{
96+
{Identifier: "site-1"},
97+
{Identifier: "site-2"},
98+
},
99+
}
100+
order, err := DetermineSiteOrder(cfg)
101+
assert.NoError(t, err)
102+
assert.Equal(t, []string{"site-2", "site-1"}, order)
103+
}
104+
105+
func TestDetermineSiteOrderReturnsErrorWhenSiteOrderLengthMismatch(t *testing.T) {
106+
cfg := &config.MachConfig{
107+
MachComposer: config.MachComposer{
108+
Batcher: config.Batcher{
109+
SiteOrder: []string{"site-1", "site-2"},
110+
},
111+
},
112+
Sites: config.SiteConfigs{
113+
{Identifier: "site-1"},
114+
},
115+
}
116+
order, err := DetermineSiteOrder(cfg)
117+
assert.Error(t, err)
118+
assert.Nil(t, order)
119+
assert.Contains(t, err.Error(), "site order length 2 does not match identifiers length 1")
120+
}
121+
122+
func TestDetermineSiteOrderReturnsErrorWhenSiteOrderContainsUnknownIdentifier(t *testing.T) {
123+
cfg := &config.MachConfig{
124+
MachComposer: config.MachComposer{
125+
Batcher: config.Batcher{
126+
SiteOrder: []string{"site-1", "unknown-site"},
127+
},
128+
},
129+
Sites: config.SiteConfigs{
130+
{Identifier: "site-1"},
131+
{Identifier: "site-2"},
132+
},
133+
}
134+
order, err := DetermineSiteOrder(cfg)
135+
assert.Error(t, err)
136+
assert.Nil(t, order)
137+
assert.Contains(t, err.Error(), "site order contains siteIdentifier unknown-site that is not in the identifiers list")
138+
}
139+
140+
func TestDetermineSiteOrderReturnsEmptyWhenNoSites(t *testing.T) {
141+
cfg := &config.MachConfig{
142+
MachComposer: config.MachComposer{
143+
Batcher: config.Batcher{},
144+
},
145+
Sites: config.SiteConfigs{},
146+
}
147+
order, err := DetermineSiteOrder(cfg)
148+
assert.NoError(t, err)
149+
assert.Empty(t, order)
150+
}

internal/batcher/naive_batcher.go renamed to internal/batcher/simple_batcher.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@ package batcher
22

33
import "github.com/mach-composer/mach-composer-cli/internal/graph"
44

5-
func NaiveBatchFunc() BatchFunc {
6-
return func(g *graph.Graph) map[int][]graph.Node {
5+
// simpleBatchFunc returns a BatchFunc that batches nodes based on their depth in the graph.
6+
func simpleBatchFunc() BatchFunc {
7+
return func(g *graph.Graph) (map[int][]graph.Node, error) {
78
batches := map[int][]graph.Node{}
89

910
var sets = map[string][]graph.Path{}
@@ -24,6 +25,6 @@ func NaiveBatchFunc() BatchFunc {
2425
batches[mx] = append(batches[mx], n)
2526
}
2627

27-
return batches
28+
return batches, nil
2829
}
2930
}

internal/batcher/naive_batcher_test.go renamed to internal/batcher/simple_batcher_test.go

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import (
77
"testing"
88
)
99

10-
func TestBatchNodesDepth1(t *testing.T) {
10+
func TestSimpleBatchNodesDepth1(t *testing.T) {
1111
ig := graph.New(func(n internalgraph.Node) string { return n.Path() }, graph.Directed(), graph.Tree(), graph.PreventCycles())
1212

1313
start := new(internalgraph.NodeMock)
@@ -17,12 +17,13 @@ func TestBatchNodesDepth1(t *testing.T) {
1717

1818
g := &internalgraph.Graph{Graph: ig, StartNode: start}
1919

20-
batches := NaiveBatchFunc()(g)
20+
batches, err := simpleBatchFunc()(g)
2121

22+
assert.NoError(t, err)
2223
assert.Equal(t, 1, len(batches))
2324
}
2425

25-
func TestBatchNodesDepth2(t *testing.T) {
26+
func TestSimpleBatchNodesDepth2(t *testing.T) {
2627
ig := graph.New(func(n internalgraph.Node) string { return n.Path() }, graph.Directed(), graph.Tree(), graph.PreventCycles())
2728

2829
site := new(internalgraph.NodeMock)
@@ -43,8 +44,9 @@ func TestBatchNodesDepth2(t *testing.T) {
4344

4445
g := &internalgraph.Graph{Graph: ig, StartNode: site}
4546

46-
batches := NaiveBatchFunc()(g)
47+
batches, err := simpleBatchFunc()(g)
4748

49+
assert.NoError(t, err)
4850
assert.Equal(t, 2, len(batches))
4951
assert.Equal(t, 1, len(batches[0]))
5052
assert.Equal(t, "main/site-1", batches[0][0].Path())
@@ -53,7 +55,7 @@ func TestBatchNodesDepth2(t *testing.T) {
5355
assert.Contains(t, batches[1][1].Path(), "component")
5456
}
5557

56-
func TestBatchNodesDepth3(t *testing.T) {
58+
func TestSimpleBatchNodesDepth3(t *testing.T) {
5759
ig := graph.New(func(n internalgraph.Node) string { return n.Path() }, graph.Directed(), graph.Tree(), graph.PreventCycles())
5860

5961
site := new(internalgraph.NodeMock)
@@ -74,8 +76,9 @@ func TestBatchNodesDepth3(t *testing.T) {
7476

7577
g := &internalgraph.Graph{Graph: ig, StartNode: site}
7678

77-
batches := NaiveBatchFunc()(g)
79+
batches, err := simpleBatchFunc()(g)
7880

81+
assert.NoError(t, err)
7982
assert.Equal(t, 3, len(batches))
8083
assert.Equal(t, 1, len(batches[0]))
8184
assert.Equal(t, "main/site-1", batches[0][0].Path())

internal/batcher/site_batcher.go

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
package batcher
2+
3+
import (
4+
"fmt"
5+
"github.com/mach-composer/mach-composer-cli/internal/graph"
6+
"golang.org/x/exp/maps"
7+
)
8+
9+
// siteBatchFunc returns a BatchFunc that batches nodes based on their site order before considering their depth in
10+
// the graph.
11+
func siteBatchFunc(siteOrder []string) BatchFunc {
12+
return func(g *graph.Graph) (map[int][]graph.Node, error) {
13+
batches := map[int][]graph.Node{}
14+
15+
var projects = g.Vertices().Filter(graph.ProjectType)
16+
if len(projects) != 1 {
17+
return nil, fmt.Errorf("expected 1 project, got %d", len(projects))
18+
}
19+
var project = projects[0]
20+
21+
var sites = g.Vertices().Filter(graph.SiteType)
22+
23+
batches[0] = []graph.Node{project}
24+
25+
for _, siteIdentifier := range siteOrder {
26+
var sets = map[string][]graph.Path{}
27+
var site = sites.FilterByIdentifier(siteIdentifier)
28+
if site == nil {
29+
return nil, fmt.Errorf("site with identifier %s not found", siteIdentifier)
30+
}
31+
32+
pg, err := g.ExtractSubGraph(site)
33+
if err != nil {
34+
return nil, err
35+
}
36+
37+
for _, n := range pg.Vertices() {
38+
var route, _ = pg.Routes(n.Path(), site.Path())
39+
sets[n.Path()] = route
40+
}
41+
42+
var siteBatches = map[int][]graph.Node{}
43+
44+
for k, routes := range sets {
45+
var mx int
46+
for _, route := range routes {
47+
if len(route) > mx {
48+
mx = len(route)
49+
}
50+
}
51+
n, _ := pg.Vertex(k)
52+
siteBatches[mx] = append(siteBatches[mx], n)
53+
}
54+
55+
// Get the highest int in the batches map
56+
var keys = maps.Keys(batches)
57+
var maxKey int
58+
for _, key := range keys {
59+
if key > maxKey {
60+
maxKey = key
61+
}
62+
}
63+
64+
for k, v := range siteBatches {
65+
batches[maxKey+k+1] = append(batches[maxKey+k+1], v...)
66+
}
67+
}
68+
69+
return batches, nil
70+
}
71+
}

0 commit comments

Comments
 (0)