Pull-based GitOps-style Docker Compose deployer: polls a (private) Git repo, detects changed stacks and reconciles only the affected
1package core
2
3import (
4 "fmt"
5 "os"
6 "os/exec"
7 "strings"
8)
9
10func PullAndDetectChanges(repoPath, branch string) ([]string, error) {
11 if err := ensureGitRepository(repoPath); err != nil {
12 return nil, err
13 }
14
15 prevHead, err := getGitHead(repoPath)
16 if err != nil {
17 return nil, fmt.Errorf("failed to get previous HEAD: %w", err)
18 }
19
20 if err := gitFetchPull(repoPath, branch); err != nil {
21 return nil, fmt.Errorf("failed to pull: %w", err)
22 }
23
24 newHead, err := getGitHead(repoPath)
25 if err != nil {
26 return nil, fmt.Errorf("failed to get new HEAD: %w", err)
27 }
28 if prevHead == newHead {
29 return []string{}, nil
30 }
31
32 output, err := gitCommand(repoPath, "diff", "--name-only", prevHead, newHead)
33 if err != nil {
34 return nil, fmt.Errorf("failed to get changed files: %w", err)
35 }
36 changedFiles := strings.Split(strings.TrimSpace(string(output)), "\n")
37 return changedFiles, nil
38}
39
40/**
41 * Git helper functions
42 */
43
44func gitCommand(repoPath string, args ...string) ([]byte, error) {
45 cmd := exec.Command("git", args...)
46 cmd.Dir = repoPath
47 return cmd.Output()
48}
49
50func detectCurrentBranch(repoPath string) string {
51 output, err := gitCommand(repoPath, "rev-parse", "--abbrev-ref", "HEAD")
52 if err != nil {
53 return ""
54 }
55 return strings.TrimSpace(string(output))
56}
57
58func ensureGitRepository(repoPath string) error {
59 if _, err := os.Stat(repoPath); os.IsNotExist(err) {
60 return fmt.Errorf("repository path does not exist: %s", repoPath)
61 }
62
63 if _, err := os.Stat(fmt.Sprintf("%s/.git", repoPath)); os.IsNotExist(err) {
64 return fmt.Errorf("path is not a git repository: %s", repoPath)
65 }
66 return nil
67}
68
69func gitFetchPull(repoPath, branch string) error {
70 if _, err := gitCommand(repoPath, "fetch", "origin", branch); err != nil {
71 return fmt.Errorf("git fetch failed: %w", err)
72 }
73
74 currentBranch, _ := gitCommand(repoPath, "rev-parse", "--abbrev-ref", "HEAD")
75 if strings.TrimSpace(string(currentBranch)) != branch {
76 if _, err := gitCommand(repoPath, "checkout", branch); err != nil {
77 return fmt.Errorf("git checkout failed: %w", err)
78 }
79 }
80
81 cmd := exec.Command("git", "pull", "origin", branch)
82 cmd.Dir = repoPath
83 output, err := cmd.CombinedOutput()
84 if err != nil {
85 return fmt.Errorf("git pull failed: %s, %w", string(output), err)
86 }
87 return nil
88}
89
90func getGitHead(repoPath string) (string, error) {
91 output, err := gitCommand(repoPath, "rev-parse", "HEAD")
92 if err != nil {
93 return "", fmt.Errorf("failed to get HEAD: %w", err)
94 }
95 return strings.TrimSpace(string(output)), nil
96}