this repo has no description
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

fix(controller): skip git save step if app is not changed

Change-Id: Ibc09dd3855fa2fd3ead1c0cdb40f50cee1be2372

Khue Doan 9ed5051b 8e41d5fc

+75 -35
+24 -18
controller/activities/app.go
··· 90 90 Tag string 91 91 } 92 92 93 - func updateImageTags(node *yaml.Node, newImages []Image) error { 93 + func updateImageTags(node *yaml.Node, newImages []Image) (bool, error) { 94 + changed := false 94 95 var walk func(n *yaml.Node) 95 96 walk = func(n *yaml.Node) { 96 97 if n.Kind != yaml.MappingNode { ··· 116 117 } 117 118 if repoNode != nil && tagNode != nil { 118 119 for _, img := range newImages { 119 - if repoNode.Value == img.Repository { 120 + if repoNode.Value == img.Repository && tagNode.Value != img.Tag { 120 121 tagNode.Value = img.Tag 122 + changed = true 121 123 } 122 124 } 123 125 } ··· 127 129 } 128 130 } 129 131 walk(node) 130 - return nil 132 + return changed, nil 131 133 } 132 134 133 - func UpdateAppVersion(ctx context.Context, appsDir, namespace, app, cluster string, newImages []Image) error { 135 + func UpdateAppVersion(ctx context.Context, appsDir, namespace, app, cluster string, newImages []Image) (bool, error) { 134 136 path := filepath.Join(appsDir, namespace, app, fmt.Sprintf("%s.yaml", cluster)) 135 137 136 138 data, err := os.ReadFile(path) 137 139 if err != nil { 138 - return fmt.Errorf("failed to read file: %w", err) 140 + return false, fmt.Errorf("failed to read file: %w", err) 139 141 } 140 142 141 143 var node yaml.Node 142 144 if err := yaml.Unmarshal(data, &node); err != nil { 143 - return fmt.Errorf("failed to unmarshal YAML: %w", err) 145 + return false, fmt.Errorf("failed to unmarshal YAML: %w", err) 144 146 } 145 147 146 - if err := updateImageTags(&node, newImages); err != nil { 147 - return fmt.Errorf("failed to update image tags: %w", err) 148 + changed, err := updateImageTags(&node, newImages) 149 + if err != nil { 150 + return false, fmt.Errorf("failed to update image tags: %w", err) 148 151 } 149 152 150 - var buf bytes.Buffer 151 - encoder := yaml.NewEncoder(&buf) 152 - encoder.SetIndent(2) 153 + if changed { 154 + var buf bytes.Buffer 155 + encoder := yaml.NewEncoder(&buf) 156 + encoder.SetIndent(2) 153 157 154 - if err := encoder.Encode(&node); err != nil { 155 - return fmt.Errorf("failed to encode YAML: %w", err) 156 - } 157 - encoder.Close() 158 + if err := encoder.Encode(&node); err != nil { 159 + return false, fmt.Errorf("failed to encode YAML: %w", err) 160 + } 161 + encoder.Close() 158 162 159 - if err := os.WriteFile(path, buf.Bytes(), 0644); err != nil { 160 - return fmt.Errorf("failed to write YAML file: %w", err) 163 + newData := buf.Bytes() 164 + if err := os.WriteFile(path, newData, 0644); err != nil { 165 + return false, fmt.Errorf("failed to write YAML file: %w", err) 166 + } 161 167 } 162 168 163 - return nil 169 + return changed, nil 164 170 }
+6 -5
controller/activities/app_test.go
··· 204 204 205 205 // Execute UpdateAppVersion 206 206 ctx := context.Background() 207 - err = UpdateAppVersion(ctx, tempDir, namespace, app, cluster, tt.newImages) 207 + changed, err := UpdateAppVersion(ctx, tempDir, namespace, app, cluster, tt.newImages) 208 208 209 209 if tt.expectError { 210 210 assert.Error(t, err) ··· 212 212 } 213 213 214 214 require.NoError(t, err) 215 + assert.Equal(t, tt.expectedUpdate, changed, "Expected change result doesn't match") 215 216 216 217 // Read the updated file 217 218 updatedContent, err := os.ReadFile(yamlPath) ··· 237 238 defer os.RemoveAll(tempDir) 238 239 239 240 t.Run("non-existent file", func(t *testing.T) { 240 - err := UpdateAppVersion(ctx, tempDir, "ns", "app", "cluster", []Image{}) 241 + _, err := UpdateAppVersion(ctx, tempDir, "ns", "app", "cluster", []Image{}) 241 242 assert.Error(t, err) 242 243 assert.Contains(t, err.Error(), "failed to read file") 243 244 }) ··· 255 256 err = os.WriteFile(yamlPath, []byte("invalid: yaml: content: ["), 0644) 256 257 require.NoError(t, err) 257 258 258 - err = UpdateAppVersion(ctx, tempDir, namespace, app, cluster, []Image{}) 259 + _, err = UpdateAppVersion(ctx, tempDir, namespace, app, cluster, []Image{}) 259 260 assert.Error(t, err) 260 261 assert.Contains(t, err.Error(), "failed to unmarshal YAML") 261 262 }) ··· 359 360 err := yaml.Unmarshal([]byte(tt.yamlContent), &node) 360 361 require.NoError(t, err) 361 362 362 - err = updateImageTags(&node, tt.newImages) 363 + _, err = updateImageTags(&node, tt.newImages) 363 364 require.NoError(t, err) 364 365 365 366 // Marshall back to verify changes ··· 451 452 } 452 453 453 454 ctx := context.Background() 454 - err = UpdateAppVersion(ctx, tempDir, namespace, app, cluster, newImages) 455 + _, err = UpdateAppVersion(ctx, tempDir, namespace, app, cluster, newImages) 455 456 require.NoError(t, err) 456 457 457 458 // Read the updated file and check indentation
+10 -2
controller/workflows/app_update.go
··· 48 48 }() 49 49 50 50 appsDir := filepath.Join(workspace, "apps") 51 + var changed bool 51 52 if err := workflow.ExecuteActivity( 52 53 workflow.WithActivityOptions(ctx, workflow.ActivityOptions{ 53 54 StartToCloseTimeout: 30 * time.Second, ··· 58 59 input.App, 59 60 input.Cluster, 60 61 input.NewImages, 61 - ).Get(ctx, nil); err != nil { 62 + ).Get(ctx, &changed); err != nil { 62 63 logger.Error("failed to update app version", "error", err) 63 64 return fmt.Errorf("failed to update app version: %w", err) 64 65 } ··· 66 67 logger.Info("App version updated successfully", 67 68 "namespace", input.Namespace, 68 69 "app", input.App, 69 - "cluster", input.Cluster) 70 + "cluster", input.Cluster, 71 + "changed", changed) 72 + 73 + // Skip remaining steps if no changes were made 74 + if !changed { 75 + logger.Info("No changes detected, skipping remaining steps") 76 + return nil 77 + } 70 78 71 79 // Step 3: Git add changes 72 80 appFilePath := filepath.Join(appsDir, input.Namespace, input.App, fmt.Sprintf("%s.yaml", input.Cluster))
+35 -10
controller/workflows/app_update_test.go
··· 49 49 50 50 s.env.OnActivity(activities.Clone, mock.Anything, input.Url, input.Revision).Return(workspace, nil) 51 51 s.env.OnActivity(activities.UpdateAppVersion, mock.Anything, 52 - workspace+"/apps", input.Namespace, input.App, input.Cluster, input.NewImages).Return(nil) 52 + workspace+"/apps", input.Namespace, input.App, input.Cluster, input.NewImages).Return(true, nil) 53 53 s.env.OnActivity(activities.GitAdd, mock.Anything, appFilePath).Return(nil) 54 54 s.env.OnActivity(activities.GitCommit, mock.Anything, workspace, "chore(khuedoan/blog): update production version").Return(nil) 55 55 s.env.OnActivity(activities.GitPush, mock.Anything, workspace).Return(nil) ··· 100 100 101 101 s.env.OnActivity(activities.Clone, mock.Anything, input.Url, input.Revision).Return(workspace, nil) 102 102 s.env.OnActivity(activities.UpdateAppVersion, mock.Anything, 103 - workspace+"/apps", input.Namespace, input.App, input.Cluster, input.NewImages).Return( 103 + workspace+"/apps", input.Namespace, input.App, input.Cluster, input.NewImages).Return(false, 104 104 errors.New("failed to read file: no such file or directory")) 105 105 106 106 s.env.ExecuteWorkflow(AppUpdate, input) ··· 127 127 128 128 s.env.OnActivity(activities.Clone, mock.Anything, input.Url, input.Revision).Return(workspace, nil) 129 129 s.env.OnActivity(activities.UpdateAppVersion, mock.Anything, 130 - workspace+"/apps", input.Namespace, input.App, input.Cluster, input.NewImages).Return(nil) 130 + workspace+"/apps", input.Namespace, input.App, input.Cluster, input.NewImages).Return(true, nil) 131 131 s.env.OnActivity(activities.GitAdd, mock.Anything, appFilePath).Return( 132 132 errors.New("git add failed: file not found")) 133 133 ··· 155 155 156 156 s.env.OnActivity(activities.Clone, mock.Anything, input.Url, input.Revision).Return(workspace, nil) 157 157 s.env.OnActivity(activities.UpdateAppVersion, mock.Anything, 158 - workspace+"/apps", input.Namespace, input.App, input.Cluster, input.NewImages).Return(nil) 158 + workspace+"/apps", input.Namespace, input.App, input.Cluster, input.NewImages).Return(true, nil) 159 159 s.env.OnActivity(activities.GitAdd, mock.Anything, appFilePath).Return(nil) 160 160 s.env.OnActivity(activities.GitCommit, mock.Anything, workspace, "chore(khuedoan/notes): update production version").Return( 161 161 errors.New("git commit failed: nothing to commit")) ··· 184 184 185 185 s.env.OnActivity(activities.Clone, mock.Anything, input.Url, input.Revision).Return(workspace, nil) 186 186 s.env.OnActivity(activities.UpdateAppVersion, mock.Anything, 187 - workspace+"/apps", input.Namespace, input.App, input.Cluster, input.NewImages).Return(nil) 187 + workspace+"/apps", input.Namespace, input.App, input.Cluster, input.NewImages).Return(true, nil) 188 188 s.env.OnActivity(activities.GitAdd, mock.Anything, appFilePath).Return(nil) 189 189 s.env.OnActivity(activities.GitCommit, mock.Anything, workspace, "chore(khuedoan/notes): update production version").Return(nil) 190 190 s.env.OnActivity(activities.GitPush, mock.Anything, workspace).Return( ··· 214 214 215 215 s.env.OnActivity(activities.Clone, mock.Anything, input.Url, input.Revision).Return(workspace, nil) 216 216 s.env.OnActivity(activities.UpdateAppVersion, mock.Anything, 217 - workspace+"/apps", input.Namespace, input.App, input.Cluster, input.NewImages).Return(nil) 217 + workspace+"/apps", input.Namespace, input.App, input.Cluster, input.NewImages).Return(true, nil) 218 218 s.env.OnActivity(activities.GitAdd, mock.Anything, appFilePath).Return(nil) 219 219 s.env.OnActivity(activities.GitCommit, mock.Anything, workspace, "chore(khuedoan/notes): update production version").Return(nil) 220 220 s.env.OnActivity(activities.GitPush, mock.Anything, workspace).Return(nil) ··· 251 251 252 252 s.env.OnActivity(activities.Clone, mock.Anything, input.Url, input.Revision).Return(workspace, nil) 253 253 s.env.OnActivity(activities.UpdateAppVersion, mock.Anything, 254 - workspace+"/apps", input.Namespace, input.App, input.Cluster, input.NewImages).Return(nil) 254 + workspace+"/apps", input.Namespace, input.App, input.Cluster, input.NewImages).Return(true, nil) 255 255 s.env.OnActivity(activities.GitAdd, mock.Anything, appFilePath).Return(nil) 256 256 s.env.OnActivity(activities.GitCommit, mock.Anything, workspace, "chore(test/example): update local version").Return(nil) 257 257 s.env.OnActivity(activities.GitPush, mock.Anything, workspace).Return(nil) ··· 286 286 287 287 s.env.OnActivity(activities.Clone, mock.Anything, input.Url, input.Revision).Return(workspace, nil) 288 288 s.env.OnActivity(activities.UpdateAppVersion, mock.Anything, 289 - workspace+"/apps", input.Namespace, input.App, input.Cluster, input.NewImages).Return(nil) 289 + workspace+"/apps", input.Namespace, input.App, input.Cluster, input.NewImages).Return(true, nil) 290 290 s.env.OnActivity(activities.GitAdd, mock.Anything, appFilePath).Return(nil) 291 291 s.env.OnActivity(activities.GitCommit, mock.Anything, workspace, "chore(khuedoan/blog): update production version").Return(nil) 292 292 s.env.OnActivity(activities.GitPush, mock.Anything, workspace).Return(nil) ··· 340 340 341 341 s.env.OnActivity(activities.Clone, mock.Anything, input.Url, input.Revision).Return(workspace, nil) 342 342 s.env.OnActivity(activities.UpdateAppVersion, mock.Anything, 343 - workspace+"/apps", input.Namespace, input.App, input.Cluster, input.NewImages).Return(nil) 343 + workspace+"/apps", input.Namespace, input.App, input.Cluster, input.NewImages).Return(true, nil) 344 344 s.env.OnActivity(activities.GitAdd, mock.Anything, appFilePath).Return(nil) 345 345 s.env.OnActivity(activities.GitCommit, mock.Anything, workspace, "chore(test/app): update local version").Return(nil) 346 346 s.env.OnActivity(activities.GitPush, mock.Anything, workspace).Return(nil) ··· 353 353 s.NoError(s.env.GetWorkflowError()) 354 354 } 355 355 356 + func (s *AppUpdateWorkflowTestSuite) TestAppUpdate_NoChanges() { 357 + input := AppUpdateInput{ 358 + Url: "https://github.com/example/cloudlab.git", 359 + Revision: "main", 360 + Namespace: "test", 361 + App: "app", 362 + Cluster: "local", 363 + Registry: "registry.example.com", 364 + NewImages: []activities.Image{ 365 + {Repository: "docker.io/test/app", Tag: "existing-tag"}, // Same tag as already in file 366 + }, 367 + } 368 + workspace := "/tmp/cloudlab-repos/nochange123" 369 + 370 + s.env.OnActivity(activities.Clone, mock.Anything, input.Url, input.Revision).Return(workspace, nil) 371 + s.env.OnActivity(activities.UpdateAppVersion, mock.Anything, 372 + workspace+"/apps", input.Namespace, input.App, input.Cluster, input.NewImages).Return(false, nil) 373 + // Note: No other activities should be called when there are no changes 374 + 375 + s.env.ExecuteWorkflow(AppUpdate, input) 376 + 377 + s.True(s.env.IsWorkflowCompleted()) 378 + s.NoError(s.env.GetWorkflowError()) 379 + } 380 + 356 381 func (s *AppUpdateWorkflowTestSuite) TestAppUpdate_SpecialCharactersInPath() { 357 382 input := AppUpdateInput{ 358 383 Url: "https://github.com/example/cloudlab.git", ··· 374 399 375 400 s.env.OnActivity(activities.Clone, mock.Anything, input.Url, input.Revision).Return(workspace, nil) 376 401 s.env.OnActivity(activities.UpdateAppVersion, mock.Anything, 377 - workspace+"/apps", input.Namespace, input.App, input.Cluster, input.NewImages).Return(nil) 402 + workspace+"/apps", input.Namespace, input.App, input.Cluster, input.NewImages).Return(true, nil) 378 403 s.env.OnActivity(activities.GitAdd, mock.Anything, appFilePath).Return(nil) 379 404 s.env.OnActivity(activities.GitCommit, mock.Anything, workspace, "chore(test-namespace/app-with-dashes): update staging-env version").Return(nil) 380 405 s.env.OnActivity(activities.GitPush, mock.Anything, workspace).Return(nil)