···2525 @temporal workflow result --workflow-id apps-manual
26262727test:
2828- cd controller && go test ./...
2928 cd test && go test
30293130fmt:
···3635 .
3736 terragrunt hcl format
3837 cd infra/_modules && tofu fmt -recursive
3939- cd controller && go fmt ./...
4038 cd infra/_modules/tfstate && go fmt ./...
4141- cd infra/staging && go fmt ./...
4239 cd test && go fmt ./...
43404441tidy: fmt
-26
controller/Dockerfile
···11-FROM docker.io/golang:1.24.3-alpine AS builder
22-33-WORKDIR /src
44-55-COPY go.mod go.sum ./
66-77-RUN go mod download
88-99-COPY . .
1010-1111-RUN go build -o /bin/worker ./worker
1212-1313-FROM docker.io/nixos/nix
1414-1515-RUN echo "experimental-features = flakes nix-command" >> /etc/nix/nix.conf
1616-1717-# TODO use native nix develop, currently it's a bit slow
1818-RUN nix-env --install --quiet --attr \
1919- nixpkgs.kubernetes-helm \
2020- nixpkgs.opentofu \
2121- nixpkgs.oras \
2222- nixpkgs.terragrunt
2323-2424-COPY --from=builder /bin/worker /bin/worker
2525-2626-CMD ["/bin/worker"]
-264
controller/activities/activity_test.go
···11-package activities
22-33-import (
44- "context"
55- "strings"
66- "testing"
77- "time"
88-99- "github.com/stretchr/testify/assert"
1010- "github.com/stretchr/testify/suite"
1111- "go.temporal.io/sdk/testsuite"
1212-)
1313-1414-type ActivityTestSuite struct {
1515- suite.Suite
1616- testsuite.WorkflowTestSuite
1717-1818- env *testsuite.TestActivityEnvironment
1919-}
2020-2121-func (s *ActivityTestSuite) SetupTest() {
2222- s.env = s.NewTestActivityEnvironment()
2323- s.env.SetTestTimeout(30 * time.Second)
2424-}
2525-2626-// Test graph pruning logic with direct calls
2727-func (s *ActivityTestSuite) TestPruneGraph_Success() {
2828- ctx := context.Background()
2929- originalGraph := &Graph{
3030- Nodes: map[string]bool{
3131- "vpc": true,
3232- "database": true,
3333- "app": true,
3434- },
3535- Edges: map[string][]string{
3636- "database": {"vpc"},
3737- "app": {"database"},
3838- },
3939- }
4040- changedFiles := []string{"database"}
4141-4242- prunedGraph, err := PruneGraph(ctx, originalGraph, changedFiles)
4343-4444- s.NoError(err)
4545- s.True(prunedGraph.Nodes["database"]) // changed module should be included
4646- s.True(prunedGraph.Nodes["app"]) // dependent should be included
4747- s.False(prunedGraph.Nodes["vpc"]) // non-dependent should be pruned
4848-}
4949-5050-func (s *ActivityTestSuite) TestPruneGraph_EmptyChanges() {
5151- ctx := context.Background()
5252- originalGraph := &Graph{
5353- Nodes: map[string]bool{
5454- "vpc": true,
5555- "database": true,
5656- },
5757- Edges: map[string][]string{
5858- "database": {"vpc"},
5959- },
6060- }
6161- changedFiles := []string{} // No changes
6262-6363- prunedGraph, err := PruneGraph(ctx, originalGraph, changedFiles)
6464-6565- s.NoError(err)
6666- s.Empty(prunedGraph.Nodes) // no changes means empty graph
6767-}
6868-6969-func (s *ActivityTestSuite) TestPruneGraph_ComplexDependencies() {
7070- ctx := context.Background()
7171- // Complex graph: monitoring -> app -> [database, cache] -> vpc
7272- originalGraph := &Graph{
7373- Nodes: map[string]bool{
7474- "vpc": true,
7575- "database": true,
7676- "cache": true,
7777- "app": true,
7878- "monitoring": true,
7979- },
8080- Edges: map[string][]string{
8181- "database": {"vpc"},
8282- "cache": {"vpc"},
8383- "app": {"database", "cache"},
8484- "monitoring": {"app"},
8585- },
8686- }
8787- changedFiles := []string{"database"} // Only database changed
8888-8989- prunedGraph, err := PruneGraph(ctx, originalGraph, changedFiles)
9090-9191- s.NoError(err)
9292- s.True(prunedGraph.Nodes["database"]) // changed module
9393- s.True(prunedGraph.Nodes["app"]) // direct dependent
9494- s.True(prunedGraph.Nodes["monitoring"]) // transitive dependent
9595- s.False(prunedGraph.Nodes["vpc"]) // not a dependent
9696- s.False(prunedGraph.Nodes["cache"]) // not a dependent
9797-}
9898-9999-// Test using TestActivityEnvironment for activities that need proper context
100100-func (s *ActivityTestSuite) TestTerragruntPrune_WithActivityEnvironment() {
101101- // Test data
102102- graph := &Graph{
103103- Nodes: map[string]bool{
104104- "vpc": true,
105105- "database": true,
106106- "app": true,
107107- "monitoring": true,
108108- },
109109- Edges: map[string][]string{
110110- "app": {"database", "vpc"},
111111- "database": {"vpc"},
112112- "monitoring": {"app"},
113113- },
114114- }
115115-116116- changedModules := []string{"database"}
117117-118118- s.env.RegisterActivity(PruneGraph)
119119-120120- val, err := s.env.ExecuteActivity(PruneGraph, graph, changedModules)
121121- s.NoError(err)
122122-123123- var result *Graph
124124- s.NoError(val.Get(&result))
125125-126126- // Only database (changed) and its dependents (app, monitoring) should be included
127127- // vpc is not included because nothing depends on it
128128- expectedNodes := []string{"database", "app", "monitoring"}
129129- actualNodes := result.GetNodes()
130130- s.ElementsMatch(expectedNodes, actualNodes)
131131-132132- s.Contains(result.Nodes, "database")
133133- s.Contains(result.Nodes, "app")
134134- s.Contains(result.Nodes, "monitoring")
135135- s.NotContains(result.Nodes, "vpc")
136136-}
137137-138138-func TestActivityTestSuite(t *testing.T) {
139139- suite.Run(t, new(ActivityTestSuite))
140140-}
141141-142142-// Additional comprehensive unit tests for graph functions
143143-func TestNewGraphFromDot_EmptyGraph(t *testing.T) {
144144- dotString := `digraph {
145145- }`
146146-147147- graph, err := NewGraphFromDot(dotString)
148148-149149- assert.NoError(t, err)
150150- assert.Empty(t, graph.Nodes)
151151-}
152152-153153-func TestNewGraphFromDot_InvalidFormat(t *testing.T) {
154154- dotString := `not a valid dot format`
155155-156156- graph, err := NewGraphFromDot(dotString)
157157-158158- assert.NoError(t, err) // Should not error, just ignore invalid lines
159159- assert.Empty(t, graph.Nodes)
160160-}
161161-162162-func TestGraph_TopologicalSort_CyclicGraph(t *testing.T) {
163163- // Create a graph with a cycle: A -> B -> C -> A
164164- graph := &Graph{
165165- Nodes: map[string]bool{
166166- "a": true,
167167- "b": true,
168168- "c": true,
169169- },
170170- Edges: map[string][]string{
171171- "a": {"b"},
172172- "b": {"c"},
173173- "c": {"a"}, // Creates cycle
174174- },
175175- }
176176-177177- levels := graph.TopologicalSort()
178178-179179- // Should handle cycles gracefully by putting remaining nodes in final level
180180- assert.Greater(t, len(levels), 0)
181181-182182- // All nodes should be present somewhere in the levels
183183- allNodes := make(map[string]bool)
184184- for _, level := range levels {
185185- for _, node := range level {
186186- allNodes[node] = true
187187- }
188188- }
189189- assert.True(t, allNodes["a"])
190190- assert.True(t, allNodes["b"])
191191- assert.True(t, allNodes["c"])
192192-}
193193-194194-func TestExtractQuoted(t *testing.T) {
195195- // Test the extractQuoted function indirectly through NewGraphFromDot
196196- dotString := `digraph {
197197- "hello" -> "world";
198198- "test";
199199- }`
200200-201201- graph, err := NewGraphFromDot(dotString)
202202-203203- assert.NoError(t, err)
204204- assert.True(t, graph.Nodes["hello"])
205205- assert.True(t, graph.Nodes["world"])
206206- assert.True(t, graph.Nodes["test"])
207207-}
208208-209209-func TestGraph_AddEdge_CreatesNodes(t *testing.T) {
210210- graph := NewGraph()
211211-212212- graph.AddEdge("a", "b")
213213-214214- assert.True(t, graph.Nodes["a"])
215215- assert.True(t, graph.Nodes["b"])
216216- assert.Contains(t, graph.Edges["a"], "b")
217217-}
218218-219219-func TestGraph_GetNodes(t *testing.T) {
220220- graph := &Graph{
221221- Nodes: map[string]bool{
222222- "vpc": true,
223223- "database": true,
224224- "app": true,
225225- },
226226- Edges: map[string][]string{},
227227- }
228228-229229- nodes := graph.GetNodes()
230230-231231- assert.Len(t, nodes, 3)
232232- assert.Contains(t, nodes, "vpc")
233233- assert.Contains(t, nodes, "database")
234234- assert.Contains(t, nodes, "app")
235235-}
236236-237237-func TestClone_PathGeneration(t *testing.T) {
238238- // Test that generateRepoPath creates deterministic paths
239239- url1 := "https://github.com/example/repo.git"
240240- revision1 := "main"
241241-242242- path1 := generateRepoPath(url1, revision1)
243243- path2 := generateRepoPath(url1, revision1)
244244-245245- // Same inputs should generate same path
246246- assert.Equal(t, path1, path2)
247247-248248- // Different inputs should generate different paths
249249- path3 := generateRepoPath(url1, "develop")
250250- assert.NotEqual(t, path1, path3)
251251-252252- path4 := generateRepoPath("https://github.com/other/repo.git", revision1)
253253- assert.NotEqual(t, path1, path4)
254254-255255- // Paths should be under /tmp/cloudlab-repos/
256256- assert.True(t, strings.HasPrefix(path1, "/tmp/cloudlab-repos/"))
257257-}
258258-259259-func TestClone_CheckRepoStatus(t *testing.T) {
260260- // Test hasCorrectRevision with non-existent directory
261261- nonExistentPath := "/tmp/non-existent-repo-12345"
262262- hasCorrect := hasCorrectRevision(context.Background(), nonExistentPath, "main")
263263- assert.False(t, hasCorrect)
264264-}