From 8ba6eaa3a3df37f9be4eb7a666b2ece236a9c89b Mon Sep 17 00:00:00 2001 From: Veerendra <8393701+veerendra2@users.noreply.github.com> Date: Sat, 25 Jul 2026 17:13:42 +0200 Subject: [PATCH 1/7] feat: expand change detection to git diff and compose dependency tree (#35) --- docs/GettingStarted.md | 2 +- docs/Introduction.md | 28 ++++++----- internal/reconcile/deploy.go | 21 -------- internal/reconcile/discover.go | 10 +--- internal/reconcile/sync.go | 54 +++++++++++++++------ pkg/dockercompose/compose.go | 59 ++++++++++++++++++++++ pkg/source/git.go | 89 +++++++++++++++++++++++++++++----- 7 files changed, 192 insertions(+), 71 deletions(-) diff --git a/docs/GettingStarted.md b/docs/GettingStarted.md index eb6aac8..162fff1 100644 --- a/docs/GettingStarted.md +++ b/docs/GettingStarted.md @@ -95,7 +95,7 @@ Run "composeflux --help" for more information on a command. Git repository for changes at configured intervals (default: 5 minutes). - **`sync`** - One-shot sync and deploy. Manually triggers immediate synchronization. Useful when you update secrets in your secrets manager but haven't made Git changes. See - [Hash-Based Change Detection](Introduction.md#hash-based-change-detection). + [Git Diff & Dependency Change Detection](Introduction.md#git-diff--dependency-change-detection). ```bash # Daemon mode (initial sync at startup, then checks Git every 5 minutes) diff --git a/docs/Introduction.md b/docs/Introduction.md index 9899edf..3ae03be 100644 --- a/docs/Introduction.md +++ b/docs/Introduction.md @@ -17,12 +17,12 @@ automatically deploys stacks when changes are detected. ComposeFlux runs a Git sync loop in daemon mode (`run` command). It performs an initial sync at startup, then checks the remote Git repository for changes and syncs again when updates are detected. -1. Pulls latest commits +1. Pulls latest commits and tracks changed file paths 2. Fetches secrets from secrets manager 3. Loads environment variables from [`stack.yml`](#stack-configuration) (if present) 4. Discovers compose stacks (one level deep in `STACK_PATH`) -5. Calculates SHA256 hash for each stack -6. Deploys stacks with changed hashes (respects [`startup_order`](#stack-configuration)) +5. Builds dependency file set for each stack (compose files, include blocks, env files, mounted configs, secrets, build context) +6. Deploys stacks that have file updates or are missing from Docker (respects [`startup_order`](#stack-configuration)) 7. Prunes stacks deleted from Git Optionally, a separate cron-scheduled image update check (`IMAGE_UPDATE_SCHEDULE`) pulls new images and redeploys stacks @@ -35,18 +35,20 @@ Two additional background loops run independently: - **Docker resource prune** — prunes unused images, volumes, and build cache on `PRUNE_INTERVAL` (default: 24h), but only when all managed stacks are healthy (see [Periodic Docker Resource Pruning](#periodic-docker-resource-pruning)) -## Hash-Based Change Detection +## Git Diff & Dependency Change Detection -ComposeFlux uses a hash-based approach to decide whether a stack needs redeploying: +ComposeFlux uses a Git diff and dependency-tree-based approach to decide whether a stack needs redeploying: + +- **Git Diff Path Matching**: ComposeFlux tracks modified, added, or deleted file paths between Git commits. +- **Dependency Tree Resolution**: Each stack's Compose project resolves all related file dependencies, including: + - Compose files and `include` directives + - Environment files (`env_file`) + - Mounted configuration files (`configs`) and secrets (`secrets`) + - Host bind mounts (`volumes`) + - Local build context and Dockerfiles (`build`) +- **Base / Overlay Support**: Changes in shared base directories (e.g., `base/app1` included by `overlays/prod/app1`) are automatically mapped to dependent stacks. +- **Targeted Redeployment**: A stack is redeployed only if any changed file in Git overlaps with its dependency file set, or if the stack is missing from Docker. -- SHA256 hash is calculated from the entire Compose project (after variable substitution with secrets) -- **Includes secrets**: The hash includes resolved secrets at sync time. If secrets change, you can run - `composeflux sync` (or wait for the next Git change) to fetch them and update the hash. -- To take full advantage of hash-based detection for app config changes, prefer Docker Compose - [`configs`](https://docs.docker.com/reference/compose-file/configs/) in your Compose files instead of mounting plain - app config files directly into containers. -- Stack is redeployed only when the hash changes; otherwise it is skipped (no unnecessary redeployment) -- Hash is stored in the `composeflux.stack-hash` label on deployed containers ## Image Update Exclusion diff --git a/internal/reconcile/deploy.go b/internal/reconcile/deploy.go index ded935f..70d6545 100644 --- a/internal/reconcile/deploy.go +++ b/internal/reconcile/deploy.go @@ -2,8 +2,6 @@ package reconcile import ( "context" - "crypto/sha256" - "fmt" "time" "github.com/compose-spec/compose-go/v2/types" @@ -14,32 +12,14 @@ const ( LabelAppVersion = "composeflux.version" LabelDeployedAt = "composeflux.deployed-at" LabelManaged = "composeflux.managed" - LabelStackHash = "composeflux.stack-hash" LabelImageUpdateExclude = "composeflux.image-update.exclude" LabelSuspend = "composeflux.health.suspend" LabelInit = "composeflux.init" ValueTrue = "true" ) -// projectChecksum computes sha256 of docker compose yaml content -func projectChecksum(project *types.Project) (string, error) { - content, err := project.MarshalYAML() - if err != nil { - return "", err - } - - hash := sha256.Sum256(content) - return fmt.Sprintf("sha256:%x", hash), nil -} - // Deploy deploys the docker compose project with custom labels and environmental variables. func (r *Reconciler) Deploy(ctx context.Context, project *types.Project) error { - // Calculate hash for change detection - stackHash, err := projectChecksum(project) - if err != nil { - return err - } - deployedAt := time.Now().Format(time.RFC3339) // Add composeflux management labels @@ -48,7 +28,6 @@ func (r *Reconciler) Deploy(ctx context.Context, project *types.Project) error { svc.Labels = make(types.Labels) } - svc.Labels[LabelStackHash] = stackHash svc.Labels[LabelManaged] = ValueTrue svc.Labels[LabelAppVersion] = version.Version svc.Labels[LabelDeployedAt] = deployedAt diff --git a/internal/reconcile/discover.go b/internal/reconcile/discover.go index 39d25fe..370be4f 100644 --- a/internal/reconcile/discover.go +++ b/internal/reconcile/discover.go @@ -25,7 +25,6 @@ var ( type StackStateMap map[string]StackInfo type StackInfo struct { - Hash string Healthy bool Suspend bool } @@ -80,7 +79,7 @@ func (r *Reconciler) discoverComposeStack(envs []string) ([]dockercompose.Compos return stacks, nil } -// getStackStates returns a StackStateMap keyed by stack name containing each stack's hash +// getStackStates returns a StackStateMap keyed by stack name containing each stack's health and suspend info func (r *Reconciler) getStackStates(ctx context.Context) (StackStateMap, error) { stackStateMap := make(StackStateMap) stacks, err := r.dClient.List(ctx) @@ -96,16 +95,10 @@ func (r *Reconciler) getStackStates(ctx context.Context) (StackStateMap, error) } // Ignore the stack if it's not managed by composeflux. - // isManagedStack guarantees len(containers) > 0, so containers[0] below is safe. if !isManagedStack(containers) { continue } - containerHash := "" - if hash, ok := containers[0].Labels[LabelStackHash]; ok { - containerHash = hash - } - stackHealthy := true stackSuspend := false for _, container := range containers { @@ -121,7 +114,6 @@ func (r *Reconciler) getStackStates(ctx context.Context) (StackStateMap, error) } stackStateMap[stack.Name] = StackInfo{ - Hash: containerHash, Healthy: stackHealthy, Suspend: stackSuspend, } diff --git a/internal/reconcile/sync.go b/internal/reconcile/sync.go index 8e7d32b..c0c8c2e 100644 --- a/internal/reconcile/sync.go +++ b/internal/reconcile/sync.go @@ -11,6 +11,7 @@ import ( "github.com/compose-spec/compose-go/v2/types" "github.com/veerendra2/composeflux/internal/metrics" + "github.com/veerendra2/composeflux/pkg/dockercompose" ) // GitSync pulls changes from the Git repository and deploys stacks which are changed or new @@ -18,10 +19,20 @@ func (r *Reconciler) GitSync(ctx context.Context) error { r.reconcileMu.Lock() defer r.reconcileMu.Unlock() - if err := r.gClient.Pull(ctx); err != nil { + changedFiles, err := r.gClient.Pull(ctx) + if err != nil { return err } + repoPath := r.gClient.Path() + + // Convert relative git changed files to absolute cleaned paths + changedPathMap := make(map[string]struct{}) + for _, f := range changedFiles { + absPath := filepath.Clean(filepath.Join(repoPath, f)) + changedPathMap[absPath] = struct{}{} + } + envs, startupOrder, err := r.loadEnvAndConfig() if err != nil { return err @@ -61,23 +72,36 @@ func (r *Reconciler) GitSync(ctx context.Context) error { continue } - sourceHash, err := projectChecksum(project) - if err != nil { - slog.Warn("Failed to calculate project checksum, deploying stack anyway", - "stack_name", project.Name, "error", err) - // Deploy anyway if hash calculation fails + // Check if stack needs deployment + if _, exists := currentStackMap[project.Name]; !exists { + slog.Info("New stack detected", "stack_name", project.Name) toDeploy[project.Name] = project - continue - } - - if stackInfo, exists := currentStackMap[project.Name]; exists { - if stackInfo.Hash != sourceHash { - slog.Info("Stack hash changed, redeploying", "stack_name", project.Name) + } else if len(changedFiles) > 0 { + // Stack is running, check if any changed file in git overlaps with stack's dependency tree + deps := dockercompose.GetDependencyPaths(project) + hasMatch := false + sep := string(filepath.Separator) + + for changedPath := range changedPathMap { + for _, dep := range deps { + depPrefix := dep + if !strings.HasSuffix(depPrefix, sep) { + depPrefix += sep + } + + if changedPath == dep || strings.HasPrefix(changedPath, depPrefix) { + hasMatch = true + slog.Info("Changed dependency file detected in stack", "stack_name", project.Name, "file", changedPath, "dir", dep) + break + } + } + if hasMatch { + break + } + } + if hasMatch { toDeploy[project.Name] = project } - } else { - slog.Info("New stack detected", "stack_name", project.Name) - toDeploy[project.Name] = project } } diff --git a/pkg/dockercompose/compose.go b/pkg/dockercompose/compose.go index ba9bff2..cedf0dd 100644 --- a/pkg/dockercompose/compose.go +++ b/pkg/dockercompose/compose.go @@ -69,6 +69,65 @@ func (c *client) LoadProject(ctx context.Context, composeCfg ComposeConfig) (*ty }) } +// GetDependencyPaths extracts all file paths that this compose project depends on +// (compose files, included files, env files, bind mount host sources, config/secret files, build context/dockerfiles). +func GetDependencyPaths(project *types.Project) []string { + if project == nil { + return nil + } + + pathSet := make(map[string]struct{}) + + addPath := func(p string) { + if p == "" { + return + } + if !filepath.IsAbs(p) { + p = filepath.Join(project.WorkingDir, p) + } + cleaned := filepath.Clean(p) + pathSet[cleaned] = struct{}{} + } + + for _, f := range project.ComposeFiles { + addPath(f) + } + + for _, cfg := range project.Configs { + addPath(cfg.File) + } + for _, sec := range project.Secrets { + addPath(sec.File) + } + + for _, svc := range project.Services { + for _, envFile := range svc.EnvFiles { + addPath(envFile.Path) + } + for _, vol := range svc.Volumes { + if vol.Type == types.VolumeTypeBind { + addPath(vol.Source) + } + } + if svc.Build != nil { + addPath(svc.Build.Context) + if svc.Build.Dockerfile != "" { + if filepath.IsAbs(svc.Build.Dockerfile) { + addPath(svc.Build.Dockerfile) + } else { + addPath(filepath.Join(svc.Build.Context, svc.Build.Dockerfile)) + } + } + } + } + + paths := make([]string, 0, len(pathSet)) + for p := range pathSet { + paths = append(paths, p) + } + return paths +} + func (c *client) Down(ctx context.Context, projectName string) error { return c.compose.Down(ctx, projectName, api.DownOptions{ RemoveOrphans: c.removeOrphans, diff --git a/pkg/source/git.go b/pkg/source/git.go index 71e7904..5c9a471 100644 --- a/pkg/source/git.go +++ b/pkg/source/git.go @@ -26,8 +26,9 @@ type Config struct { } type Client interface { - Pull(ctx context.Context) error + Pull(ctx context.Context) ([]string, error) HasUpdates(ctx context.Context) (bool, string, string, error) + GetChangedFiles(ctx context.Context, oldSHA, newSHA string) ([]string, error) Path() string } @@ -38,34 +39,98 @@ type client struct { sshAuth *ssh.PublicKeys } -// Pull syncs latest changes from remote -func (c *client) Pull(ctx context.Context) error { - err := c.repo.FetchContext(ctx, &git.FetchOptions{ +// Pull syncs latest changes from remote and returns a list of changed relative file paths since previous HEAD. +func (c *client) Pull(ctx context.Context) ([]string, error) { + localRef, err := c.repo.Head() + oldSHA := "" + if err == nil { + oldSHA = localRef.Hash().String() + } + + err = c.repo.FetchContext(ctx, &git.FetchOptions{ RemoteName: remoteName, Auth: c.sshAuth, Force: true, // required for force-pushed branches to update remote tracking refs }) if err != nil && !errors.Is(err, git.NoErrAlreadyUpToDate) { - return fmt.Errorf("failed to fetch: %w", err) + return nil, fmt.Errorf("failed to fetch: %w", err) } - // Hard reset to remote tracking ref — handles force-push and cases where - // HasUpdates() already fetched (making PullContext return NoErrAlreadyUpToDate early - // without actually updating the worktree) remoteRef, err := c.repo.Reference(plumbing.NewRemoteReferenceName(remoteName, c.branch), true) if err != nil { - return fmt.Errorf("failed to resolve remote ref: %w", err) + return nil, fmt.Errorf("failed to resolve remote ref: %w", err) + } + + newSHA := remoteRef.Hash().String() + + var changedFiles []string + if oldSHA != "" && oldSHA != newSHA { + changedFiles, err = c.GetChangedFiles(ctx, oldSHA, newSHA) + if err != nil { + slog.Warn("Failed to compute git diff file list", "old_sha", shortSHA(oldSHA), "new_sha", shortSHA(newSHA), "error", err) + } } w, err := c.repo.Worktree() if err != nil { - return err + return nil, err } if err := w.Reset(&git.ResetOptions{Commit: remoteRef.Hash(), Mode: git.HardReset}); err != nil { - return fmt.Errorf("failed to reset to %s/%s: %w", remoteName, c.branch, err) + return nil, fmt.Errorf("failed to reset to %s/%s: %w", remoteName, c.branch, err) } - return nil + + return changedFiles, nil +} + +// GetChangedFiles compares two commit SHAs and returns relative paths of modified, added, or deleted files. +func (c *client) GetChangedFiles(ctx context.Context, oldSHA, newSHA string) ([]string, error) { + if oldSHA == "" || newSHA == "" || oldSHA == newSHA { + return nil, nil + } + + oldCommit, err := c.repo.CommitObject(plumbing.NewHash(oldSHA)) + if err != nil { + return nil, fmt.Errorf("failed to get old commit %s: %w", oldSHA, err) + } + + newCommit, err := c.repo.CommitObject(plumbing.NewHash(newSHA)) + if err != nil { + return nil, fmt.Errorf("failed to get new commit %s: %w", newSHA, err) + } + + oldTree, err := oldCommit.Tree() + if err != nil { + return nil, fmt.Errorf("failed to get old tree: %w", err) + } + + newTree, err := newCommit.Tree() + if err != nil { + return nil, fmt.Errorf("failed to get new tree: %w", err) + } + + changes, err := oldTree.DiffContext(ctx, newTree) + if err != nil { + return nil, fmt.Errorf("failed to diff trees: %w", err) + } + + var filePaths []string + pathMap := make(map[string]struct{}) + + for _, change := range changes { + if change.From.Name != "" { + pathMap[change.From.Name] = struct{}{} + } + if change.To.Name != "" { + pathMap[change.To.Name] = struct{}{} + } + } + + for path := range pathMap { + filePaths = append(filePaths, path) + } + + return filePaths, nil } // HasUpdates checks for remote changes and returns update status with short commit SHAs (for logging) From bd13872333c9a3e26f770e165e9db48c1f7df95a Mon Sep 17 00:00:00 2001 From: Veerendra <8393701+veerendra2@users.noreply.github.com> Date: Sat, 25 Jul 2026 18:53:16 +0200 Subject: [PATCH 2/7] fix: filter dependency paths to git repository boundaries --- internal/reconcile/sync.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/internal/reconcile/sync.go b/internal/reconcile/sync.go index c0c8c2e..f158563 100644 --- a/internal/reconcile/sync.go +++ b/internal/reconcile/sync.go @@ -84,6 +84,12 @@ func (r *Reconciler) GitSync(ctx context.Context) error { for changedPath := range changedPathMap { for _, dep := range deps { + // Only consider dependency paths that are inside the git repository + rel, err := filepath.Rel(repoPath, dep) + if err != nil || strings.HasPrefix(rel, "..") || rel == "." { + continue + } + depPrefix := dep if !strings.HasSuffix(depPrefix, sep) { depPrefix += sep From a910ee63ce1c8de2d794f9efce8d7b83b0a54986 Mon Sep 17 00:00:00 2001 From: Veerendra <8393701+veerendra2@users.noreply.github.com> Date: Sat, 25 Jul 2026 20:33:01 +0200 Subject: [PATCH 3/7] refactor: remove prometheus metrics --- AGENTS.md | 12 - README.md | 1 - cmd/composeflux/run.go | 42 -- compose-dev.yml | 57 --- docs/GettingStarted.md | 7 - docs/Metrics.md | 21 - docs/dashboards/grafana-dashboard.json | 674 ------------------------- docs/index.md | 1 - go.mod | 2 +- internal/metrics/metrics.go | 33 -- internal/reconcile/prune.go | 2 - internal/reconcile/sync.go | 5 +- internal/reconcile/update.go | 5 - mkdocs.yml | 1 - 14 files changed, 2 insertions(+), 861 deletions(-) delete mode 100644 docs/Metrics.md delete mode 100644 docs/dashboards/grafana-dashboard.json delete mode 100644 internal/metrics/metrics.go diff --git a/AGENTS.md b/AGENTS.md index 85da1f5..a2f3227 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -14,13 +14,11 @@ ComposeFlux is a Go application that implements a GitOps reconciliation loop for ``` cmd/composeflux/ # Main binary entry point (CLI setup via kong) cmd/prune-playground/ # Dev scratch binary -internal/metrics/ # Prometheus metric definitions (promauto counters) internal/reconcile/ # Core reconciliation logic (private to module) pkg/dockercompose/ # Docker Compose SDK wrapper (exported) pkg/secrets/ # Secrets manager integrations (Bitwarden, Infisical) — optional pkg/source/ # Git client (go-git) docs/ # MkDocs documentation -docs/dashboards/ # Grafana dashboard JSON and screenshot ``` --- @@ -176,16 +174,6 @@ ctx.FatalIfErrorf(ctx.Run()) --- -## Observability - -- Prometheus metrics defined in `internal/metrics/metrics.go` using `promauto.NewCounterVec`. -- All counters use `stack_name` as the sole label to avoid high cardinality. -- Metrics are incremented in reconciler paths (`Sync`, `SyncImages`, `Prune`) — never in the Docker Compose client layer. -- The `/metrics` HTTP endpoint is served from `cmd/composeflux/run.go` (daemon mode only). -- Grafana dashboard JSON lives in `docs/dashboards/` with a `${DS_PROMETHEUS}` variable datasource for portability. - ---- - ## CI / GitHub Actions - **`ci.yml`**: Runs `golangci-lint` on all pull requests. Run `task lint` locally before opening a PR. diff --git a/README.md b/README.md index f458241..5d0a9fd 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,6 @@ and automatically deploy your Docker stacks—all without manual intervention. | Secrets Management | Optional Bitwarden Secrets Manager and Infisical support | | Automatic Image Updates | Scheduled registry checks redeploy stacks when newer images are available | | Flexible Configuration | Startup order and shared environment variables | -| Prometheus Metrics | Built-in metrics endpoint for deployment, image update, and prune observability | | Simple & Headless | No UI, no backend database | **[Full Documentation](https://veerendra2.github.io/composeflux/)** — Getting started, configuration, guides, and more. diff --git a/cmd/composeflux/run.go b/cmd/composeflux/run.go index b69b99d..7ec7141 100644 --- a/cmd/composeflux/run.go +++ b/cmd/composeflux/run.go @@ -1,19 +1,7 @@ package main -import ( - "context" - "fmt" - "log/slog" - "net" - "net/http" - "time" - - "github.com/prometheus/client_golang/prometheus/promhttp" -) - type RunCmd struct { CommonConfig `embed:""` - MetricsAddr string `name:"metrics-addr" help:"Prometheus metrics listen address. Empty to disable." env:"METRICS_ADDR" default:":9090" group:"Metrics Options:"` } func (r *RunCmd) AfterApply() error { @@ -27,36 +15,6 @@ func (r *RunCmd) Run() error { } defer cleanup() - if r.MetricsAddr != "" { - ln, err := net.Listen("tcp", r.MetricsAddr) - if err != nil { - return fmt.Errorf("metrics server failed to bind %s: %w", r.MetricsAddr, err) - } - - mux := http.NewServeMux() - mux.Handle("/metrics", promhttp.Handler()) - srv := &http.Server{ - Handler: mux, - ReadHeaderTimeout: 10 * time.Second, - } - - go func() { - slog.Info("Starting metrics server", "addr", r.MetricsAddr) - if err := srv.Serve(ln); err != nil && err != http.ErrServerClosed { - slog.Error("Metrics server failed", "error", err) - } - }() - - defer func() { - shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second) - defer cancel() - if err := srv.Shutdown(shutdownCtx); err != nil { - slog.Error("Metrics server shutdown failed", "error", err) - } - slog.Info("Metrics server stopped") - }() - } - rClient.Run(ctx) return nil } diff --git a/compose-dev.yml b/compose-dev.yml index 33fc4dc..4aeb390 100644 --- a/compose-dev.yml +++ b/compose-dev.yml @@ -19,9 +19,6 @@ services: # GIT_CLONE_PATH: /opt/compose-stack GIT_SSH_KEY_PATH: ${GIT_SSH_KEY_PATH:-/.ssh/composeflux_id_rsa} - # Metrics - METRICS_ADDR: ":9090" - # Logging LOG_ADD_SOURCE: ${LOG_ADD_SOURCE} LOG_LEVEL: ${LOG_LEVEL} @@ -41,65 +38,11 @@ services: # INFISICAL_SECRET_PATH: / # INFISICAL_SITE_URL: https://app.infisical.com hostname: composeflux - ports: - - 9090:9090 volumes: - /var/run/docker.sock:/var/run/docker.sock:ro - ~/.ssh/composeflux_id_rsa:/tmp/composeflux_id_rsa - compose-stack:/opt/compose-stack - prometheus: - image: prom/prometheus:latest - container_name: prometheus - command: - - --config.file=/etc/prometheus/prometheus.yml - - --storage.tsdb.retention.time=7d - configs: - - source: prometheus-config - target: /etc/prometheus/prometheus.yml - ports: - - 9091:9090 - depends_on: - - composeflux - - grafana: - image: grafana/grafana:latest - container_name: grafana - environment: - GF_SECURITY_ADMIN_USER: admin - GF_SECURITY_ADMIN_PASSWORD: admin - GF_AUTH_ANONYMOUS_ENABLED: "true" - GF_AUTH_ANONYMOUS_ORG_ROLE: Admin - configs: - - source: grafana-datasource - target: /etc/grafana/provisioning/datasources/prometheus.yml - ports: - - 3000:3000 - depends_on: - - prometheus - -configs: - prometheus-config: - content: | - global: - scrape_interval: 15s - - scrape_configs: - - job_name: composeflux - static_configs: - - targets: - - composeflux:9090 - - grafana-datasource: - content: | - apiVersion: 1 - datasources: - - name: Prometheus - type: prometheus - access: proxy - url: http://prometheus:9090 - isDefault: true - volumes: compose-stack: name: compose-stack diff --git a/docs/GettingStarted.md b/docs/GettingStarted.md index 162fff1..eede380 100644 --- a/docs/GettingStarted.md +++ b/docs/GettingStarted.md @@ -62,7 +62,6 @@ Deploy ComposeFlux and manage Docker Compose stacks via GitOps. | `LOG_ADD_SOURCE` | Add source location to logs | `false` | | `REMOVE_ORPHANS` | Remove orphan containers during deploy | `true` | | `PRUNE_INTERVAL` | Interval for periodic Docker resource pruning (images, volumes, build cache). Only runs when all managed stacks are healthy. Set to `0` to disable. | `24h` | -| `METRICS_ADDR` | Prometheus metrics listen address. Empty to disable. | `:9090` | !!! warning @@ -186,12 +185,6 @@ services: # LOG_LEVEL: info # LOG_FORMAT: console # console or json - # Metrics - # METRICS_ADDR: ":9090" # Prometheus metrics endpoint, empty to disable - - ports: - - "9090:9090" # Prometheus metrics - volumes: - /var/run/docker.sock:/var/run/docker.sock diff --git a/docs/Metrics.md b/docs/Metrics.md deleted file mode 100644 index 84037fa..0000000 --- a/docs/Metrics.md +++ /dev/null @@ -1,21 +0,0 @@ -# Prometheus Metrics - -ComposeFlux exposes Prometheus metrics on the address configured via `METRICS_ADDR` (default `:9090`). Set it to empty to disable. - -## Available Metrics - -| Metric | Type | Labels | Description | -| ------ | ---- | ------ | ----------- | -| `composeflux_deployments_total` | Counter | `stack_name` | Total number of stack deployment attempts | -| `composeflux_deployment_failures_total` | Counter | `stack_name` | Total number of failed stack deployments | -| `composeflux_image_updates_total` | Counter | `stack_name` | Total number of stack image update attempts | -| `composeflux_image_update_failures_total` | Counter | `stack_name` | Total number of failed stack image updates | -| `composeflux_stacks_pruned_total` | Counter | `stack_name` | Total number of managed stacks removed during pruning | - -## Grafana Dashboard - -A pre-built Grafana dashboard is available at [`docs/dashboards/grafana-dashboard.json`](dashboards/grafana-dashboard.json). - -Import it via Grafana UI (**Dashboards → Import → Upload JSON file**). - -![ComposeFlux Dashboard](dashboards/dashboard.png) diff --git a/docs/dashboards/grafana-dashboard.json b/docs/dashboards/grafana-dashboard.json deleted file mode 100644 index 55067fa..0000000 --- a/docs/dashboards/grafana-dashboard.json +++ /dev/null @@ -1,674 +0,0 @@ -{ - "annotations": { - "list": [ - { - "builtIn": 1, - "datasource": { - "type": "grafana", - "uid": "-- Grafana --" - }, - "enable": true, - "hide": true, - "iconColor": "rgba(0, 211, 255, 1)", - "name": "Annotations & Alerts", - "type": "dashboard" - } - ] - }, - "editable": true, - "fiscalYearStartMonth": 0, - "graphTooltip": 0, - "links": [ - { - "asDropdown": false, - "icon": "external link", - "includeVars": false, - "keepTime": false, - "tags": [], - "targetBlank": true, - "title": "ComposeFlux GitHub", - "tooltip": "", - "type": "link", - "url": "https://github.com/veerendra2/composeflux" - } - ], - "liveNow": false, - "panels": [ - { - "datasource": { - "type": "prometheus", - "uid": "${DS_PROMETHEUS}" - }, - "description": "Percentage of successful deployments across all stacks", - "fieldConfig": { - "defaults": { - "max": 100, - "min": 0, - "noValue": "N/A", - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "red", - "value": 0 - }, - { - "color": "orange", - "value": 80 - }, - { - "color": "green", - "value": 95 - } - ] - }, - "unit": "percent" - } - }, - "gridPos": { - "h": 6, - "w": 8, - "x": 0, - "y": 0 - }, - "id": 1, - "options": { - "colorMode": "background", - "graphMode": "none", - "justifyMode": "auto", - "orientation": "auto", - "percentChangeColorMode": "standard", - "reduceOptions": { - "calcs": [ - "lastNotNull" - ], - "fields": "", - "values": false - }, - "showPercentChange": false, - "textMode": "value", - "wideLayout": true - }, - "pluginVersion": "13.0.1", - "targets": [ - { - "datasource": { - "type": "prometheus", - "uid": "${DS_PROMETHEUS}" - }, - "editorMode": "code", - "expr": "(\n sum(rate(composeflux_deployments_total[5m]))\n - sum(rate(composeflux_deployment_failures_total[5m]))\n)\n/ sum(rate(composeflux_deployments_total[5m]))\n* 100", - "legendFormat": "Success Rate", - "range": true, - "refId": "A" - } - ], - "title": "Deployment Success Rate", - "type": "stat" - }, - { - "datasource": { - "type": "prometheus", - "uid": "${DS_PROMETHEUS}" - }, - "description": "Percentage of successful image updates across all stacks", - "fieldConfig": { - "defaults": { - "max": 100, - "min": 0, - "noValue": "N/A", - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "red", - "value": 0 - }, - { - "color": "orange", - "value": 80 - }, - { - "color": "green", - "value": 95 - } - ] - }, - "unit": "percent" - } - }, - "gridPos": { - "h": 6, - "w": 8, - "x": 8, - "y": 0 - }, - "id": 2, - "options": { - "colorMode": "background", - "graphMode": "none", - "justifyMode": "auto", - "orientation": "auto", - "percentChangeColorMode": "standard", - "reduceOptions": { - "calcs": [ - "lastNotNull" - ], - "fields": "", - "values": false - }, - "showPercentChange": false, - "textMode": "value", - "wideLayout": true - }, - "pluginVersion": "13.0.1", - "targets": [ - { - "datasource": { - "type": "prometheus", - "uid": "${DS_PROMETHEUS}" - }, - "editorMode": "code", - "expr": "(\n sum(rate(composeflux_image_updates_total[5m]))\n - sum(rate(composeflux_image_update_failures_total[5m]))\n)\n/ sum(rate(composeflux_image_updates_total[5m]))\n* 100", - "legendFormat": "Success Rate", - "range": true, - "refId": "A" - } - ], - "title": "Image Update Success Rate", - "type": "stat" - }, - { - "datasource": { - "type": "prometheus", - "uid": "${DS_PROMETHEUS}" - }, - "description": "Total managed stacks removed", - "fieldConfig": { - "defaults": { - "noValue": "0", - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "purple", - "value": 0 - } - ] - } - } - }, - "gridPos": { - "h": 6, - "w": 8, - "x": 16, - "y": 0 - }, - "id": 5, - "options": { - "colorMode": "background", - "graphMode": "none", - "justifyMode": "auto", - "orientation": "auto", - "percentChangeColorMode": "standard", - "reduceOptions": { - "calcs": [ - "lastNotNull" - ], - "fields": "", - "values": false - }, - "showPercentChange": false, - "textMode": "value", - "wideLayout": true - }, - "pluginVersion": "13.0.1", - "targets": [ - { - "datasource": { - "type": "prometheus", - "uid": "${DS_PROMETHEUS}" - }, - "expr": "sum(increase(composeflux_stacks_pruned_total[$__range]))", - "legendFormat": "Pruned", - "refId": "A" - } - ], - "title": "Stacks Pruned", - "type": "stat" - }, - { - "datasource": { - "type": "prometheus", - "uid": "${DS_PROMETHEUS}" - }, - "description": "Rate of deployments and failures per stack", - "fieldConfig": { - "defaults": { - "color": { - "mode": "palette-classic" - }, - "custom": { - "axisBorderShow": false, - "axisCenteredZero": false, - "axisColorMode": "text", - "axisLabel": "", - "axisPlacement": "auto", - "barAlignment": 0, - "barWidthFactor": 0.6, - "drawStyle": "line", - "fillOpacity": 38, - "gradientMode": "opacity", - "hideFrom": { - "legend": false, - "tooltip": false, - "viz": false - }, - "insertNulls": false, - "lineInterpolation": "linear", - "lineWidth": 1, - "pointSize": 5, - "scaleDistribution": { - "type": "linear" - }, - "showPoints": "auto", - "showValues": false, - "spanNulls": true, - "stacking": { - "group": "A", - "mode": "normal" - }, - "thresholdsStyle": { - "mode": "off" - } - }, - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": 0 - }, - { - "color": "red", - "value": 80 - } - ] - } - }, - "overrides": [ - { - "matcher": { - "id": "byRegexp", - "options": ".*failures.*" - }, - "properties": [ - { - "id": "color", - "value": { - "fixedColor": "red", - "mode": "fixed" - } - } - ] - }, - { - "matcher": { - "id": "byRegexp", - "options": ".*attempts.*" - }, - "properties": [ - { - "id": "color", - "value": { - "fixedColor": "green", - "mode": "fixed" - } - } - ] - } - ] - }, - "gridPos": { - "h": 8, - "w": 8, - "x": 0, - "y": 6 - }, - "id": 6, - "options": { - "annotations": { - "clustering": -1, - "multiLane": false - }, - "legend": { - "calcs": [ - "lastNotNull" - ], - "displayMode": "table", - "placement": "right", - "showLegend": true - }, - "tooltip": { - "hideZeros": false, - "mode": "multi", - "sort": "none" - } - }, - "pluginVersion": "13.0.1", - "targets": [ - { - "datasource": { - "type": "prometheus", - "uid": "${DS_PROMETHEUS}" - }, - "editorMode": "code", - "expr": "sum(rate(composeflux_deployments_total[5m])) by (stack_name)", - "legendFormat": "{{stack_name}} attempts", - "range": true, - "refId": "A" - }, - { - "datasource": { - "type": "prometheus", - "uid": "${DS_PROMETHEUS}" - }, - "editorMode": "code", - "expr": "sum(rate(composeflux_deployment_failures_total[5m])) by (stack_name)", - "legendFormat": "{{stack_name}} - Failures", - "range": true, - "refId": "B" - } - ], - "title": "Deployments Over Time", - "type": "timeseries" - }, - { - "datasource": { - "type": "prometheus", - "uid": "${DS_PROMETHEUS}" - }, - "description": "Rate of image update attempts and failures per stack. Individual image names are not tracked to avoid high cardinality.", - "fieldConfig": { - "defaults": { - "color": { - "mode": "palette-classic" - }, - "custom": { - "axisBorderShow": false, - "axisCenteredZero": false, - "axisColorMode": "text", - "axisLabel": "", - "axisPlacement": "auto", - "barAlignment": 0, - "barWidthFactor": 0.6, - "drawStyle": "line", - "fillOpacity": 50, - "gradientMode": "opacity", - "hideFrom": { - "legend": false, - "tooltip": false, - "viz": false - }, - "insertNulls": false, - "lineInterpolation": "linear", - "lineStyle": { - "fill": "solid" - }, - "lineWidth": 1, - "pointSize": 5, - "scaleDistribution": { - "type": "linear" - }, - "showPoints": "auto", - "showValues": false, - "spanNulls": true, - "stacking": { - "group": "A", - "mode": "normal" - }, - "thresholdsStyle": { - "mode": "off" - } - }, - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": 0 - }, - { - "color": "red", - "value": 80 - } - ] - } - }, - "overrides": [ - { - "matcher": { - "id": "byRegexp", - "options": ".*failures.*" - }, - "properties": [ - { - "id": "color", - "value": { - "fixedColor": "red", - "mode": "fixed" - } - } - ] - }, - { - "matcher": { - "id": "byRegexp", - "options": ".*attempts.*" - }, - "properties": [ - { - "id": "color", - "value": { - "fixedColor": "blue", - "mode": "fixed" - } - } - ] - } - ] - }, - "gridPos": { - "h": 8, - "w": 8, - "x": 8, - "y": 6 - }, - "id": 7, - "options": { - "annotations": { - "clustering": -1, - "multiLane": false - }, - "legend": { - "calcs": [ - "lastNotNull" - ], - "displayMode": "table", - "placement": "right", - "showLegend": true - }, - "tooltip": { - "hideZeros": false, - "mode": "multi", - "sort": "none" - } - }, - "pluginVersion": "13.0.1", - "targets": [ - { - "datasource": { - "type": "prometheus", - "uid": "${DS_PROMETHEUS}" - }, - "editorMode": "code", - "expr": "sum(rate(composeflux_image_updates_total[5m])) by (stack_name)", - "legendFormat": "{{stack_name}} attempts", - "range": true, - "refId": "A" - }, - { - "datasource": { - "type": "prometheus", - "uid": "${DS_PROMETHEUS}" - }, - "editorMode": "code", - "expr": "sum(rate(composeflux_image_update_failures_total[5m])) by (stack_name)", - "legendFormat": "{{stack_name}} - Failures", - "range": true, - "refId": "B" - } - ], - "title": "Image Updates Over Time (by Stack)", - "type": "timeseries" - }, - { - "datasource": { - "type": "prometheus", - "uid": "${DS_PROMETHEUS}" - }, - "description": "Pruned stacks over time by stack name", - "fieldConfig": { - "defaults": { - "color": { - "mode": "palette-classic" - }, - "custom": { - "axisBorderShow": false, - "axisCenteredZero": false, - "axisColorMode": "text", - "axisLabel": "", - "axisPlacement": "auto", - "barAlignment": 0, - "barWidthFactor": 0.6, - "drawStyle": "bars", - "fillOpacity": 50, - "gradientMode": "none", - "hideFrom": { - "legend": false, - "tooltip": false, - "viz": false - }, - "insertNulls": false, - "lineInterpolation": "linear", - "lineWidth": 1, - "pointSize": 5, - "scaleDistribution": { - "type": "linear" - }, - "showPoints": "auto", - "showValues": false, - "spanNulls": false, - "stacking": { - "group": "A", - "mode": "none" - }, - "thresholdsStyle": { - "mode": "off" - } - }, - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": 0 - }, - { - "color": "red", - "value": 80 - } - ] - } - } - }, - "gridPos": { - "h": 8, - "w": 8, - "x": 16, - "y": 6 - }, - "id": 10, - "options": { - "annotations": { - "clustering": -1, - "multiLane": false - }, - "legend": { - "calcs": [ - "sum" - ], - "displayMode": "table", - "placement": "bottom", - "showLegend": true - }, - "tooltip": { - "hideZeros": false, - "mode": "multi", - "sort": "none" - } - }, - "pluginVersion": "13.0.1", - "targets": [ - { - "datasource": { - "type": "prometheus", - "uid": "${DS_PROMETHEUS}" - }, - "expr": "sum(rate(composeflux_stacks_pruned_total[5m]))", - "legendFormat": "{{stack_name}}", - "refId": "A" - } - ], - "title": "Stacks Pruned Over Time", - "type": "timeseries" - } - ], - "preload": false, - "refresh": "30s", - "schemaVersion": 42, - "tags": [ - "composeflux" - ], - "time": { - "from": "now-1h", - "to": "now" - }, - "timepicker": { - "refresh_intervals": [ - "5s", - "10s", - "30s", - "1m", - "5m", - "15m", - "30m", - "1h", - "2h", - "1d" - ] - }, - "timezone": "browser", - "title": "ComposeFlux", - "uid": "composeflux-metrics", - "__inputs": [ - { - "name": "DS_PROMETHEUS", - "label": "Prometheus", - "description": "", - "type": "datasource", - "pluginId": "prometheus", - "pluginName": "Prometheus" - } - ] -} \ No newline at end of file diff --git a/docs/index.md b/docs/index.md index c184e6f..8eeb87e 100644 --- a/docs/index.md +++ b/docs/index.md @@ -30,5 +30,4 @@ and automatically deploy your Docker stacks—all without manual intervention. | Secrets Management | Optional Bitwarden Secrets Manager and Infisical support | | Automatic Image Updates | Scheduled registry checks redeploy stacks when newer images are available | | Flexible Configuration | Startup order and shared environment variables | -| Prometheus Metrics | Built-in metrics endpoint for deployment, image update, and prune observability | | Simple & Headless | No UI, no backend database | diff --git a/go.mod b/go.mod index 8d9808b..a4528ff 100644 --- a/go.mod +++ b/go.mod @@ -16,7 +16,6 @@ require ( github.com/go-git/go-git/v5 v5.19.1 github.com/infisical/go-sdk v0.8.0 github.com/moby/moby/client v0.5.0 - github.com/prometheus/client_golang v1.23.2 github.com/robfig/cron/v3 v3.0.1 github.com/sirupsen/logrus v1.9.4 github.com/veerendra2/gopackages v1.2.3 @@ -136,6 +135,7 @@ require ( github.com/pjbgf/sha1cd v0.6.0 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect + github.com/prometheus/client_golang v1.23.2 // indirect github.com/prometheus/client_model v0.6.2 // indirect github.com/prometheus/common v0.67.5 // indirect github.com/prometheus/procfs v0.20.1 // indirect diff --git a/internal/metrics/metrics.go b/internal/metrics/metrics.go deleted file mode 100644 index c1ce867..0000000 --- a/internal/metrics/metrics.go +++ /dev/null @@ -1,33 +0,0 @@ -package metrics - -import ( - "github.com/prometheus/client_golang/prometheus" - "github.com/prometheus/client_golang/prometheus/promauto" -) - -var ( - DeploymentsTotal = promauto.NewCounterVec(prometheus.CounterOpts{ - Name: "composeflux_deployments_total", - Help: "Total number of stack deployment attempts.", - }, []string{"stack_name"}) - - DeploymentFailuresTotal = promauto.NewCounterVec(prometheus.CounterOpts{ - Name: "composeflux_deployment_failures_total", - Help: "Total number of failed stack deployments.", - }, []string{"stack_name"}) - - ImageUpdatesTotal = promauto.NewCounterVec(prometheus.CounterOpts{ - Name: "composeflux_image_updates_total", - Help: "Total number of stack image update attempts.", - }, []string{"stack_name"}) - - ImageUpdateFailuresTotal = promauto.NewCounterVec(prometheus.CounterOpts{ - Name: "composeflux_image_update_failures_total", - Help: "Total number of failed stack image updates.", - }, []string{"stack_name"}) - - StacksPrunedTotal = promauto.NewCounterVec(prometheus.CounterOpts{ - Name: "composeflux_stacks_pruned_total", - Help: "Total number of managed stacks removed during pruning.", - }, []string{"stack_name"}) -) diff --git a/internal/reconcile/prune.go b/internal/reconcile/prune.go index f3785d4..889f181 100644 --- a/internal/reconcile/prune.go +++ b/internal/reconcile/prune.go @@ -6,7 +6,6 @@ import ( "path/filepath" "strings" - "github.com/veerendra2/composeflux/internal/metrics" "github.com/veerendra2/composeflux/pkg/dockercompose" ) @@ -87,7 +86,6 @@ func (r *Reconciler) PruneStacks(ctx context.Context, srcStack []dockercompose.C slog.Warn("Failed to prune stack", "stack_name", stack.Name, "error", err) continue } - metrics.StacksPrunedTotal.WithLabelValues(stack.Name).Inc() prunedStacks = append(prunedStacks, stack.Name) } } diff --git a/internal/reconcile/sync.go b/internal/reconcile/sync.go index f158563..4db1964 100644 --- a/internal/reconcile/sync.go +++ b/internal/reconcile/sync.go @@ -10,7 +10,6 @@ import ( "strings" "github.com/compose-spec/compose-go/v2/types" - "github.com/veerendra2/composeflux/internal/metrics" "github.com/veerendra2/composeflux/pkg/dockercompose" ) @@ -97,7 +96,7 @@ func (r *Reconciler) GitSync(ctx context.Context) error { if changedPath == dep || strings.HasPrefix(changedPath, depPrefix) { hasMatch = true - slog.Info("Changed dependency file detected in stack", "stack_name", project.Name, "file", changedPath, "dir", dep) + slog.Debug("Changed dependency file detected in stack", "stack_name", project.Name, "file", changedPath, "dir", dep) break } } @@ -135,10 +134,8 @@ func (r *Reconciler) GitSync(ctx context.Context) error { } for _, name := range deployOrder { - metrics.DeploymentsTotal.WithLabelValues(name).Inc() if err := r.Deploy(ctx, toDeploy[name]); err != nil { slog.Warn("Failed to deploy the stack", "stack_name", name, "error", err) - metrics.DeploymentFailuresTotal.WithLabelValues(name).Inc() continue } slog.Info("Successfully deployed the stack", "stack_name", name) diff --git a/internal/reconcile/update.go b/internal/reconcile/update.go index 8415edb..4c7660f 100644 --- a/internal/reconcile/update.go +++ b/internal/reconcile/update.go @@ -5,7 +5,6 @@ import ( "log/slog" "github.com/compose-spec/compose-go/v2/types" - "github.com/veerendra2/composeflux/internal/metrics" ) // UpdateImages checks all discovered stacks for Docker image updates and redeploys any that have new images. @@ -47,17 +46,13 @@ func (r *Reconciler) UpdateImages(ctx context.Context) error { continue } - metrics.ImageUpdatesTotal.WithLabelValues(project.Name).Inc() - if err := r.dClient.Pull(ctx, project); err != nil { slog.Warn("Failed to pull updated images, skipping redeploy", "stack_name", project.Name, "error", err) - metrics.ImageUpdateFailuresTotal.WithLabelValues(project.Name).Inc() continue } if err := r.Deploy(ctx, project); err != nil { slog.Warn("Failed to redeploy stack after image update", "stack_name", project.Name, "error", err) - metrics.ImageUpdateFailuresTotal.WithLabelValues(project.Name).Inc() continue } diff --git a/mkdocs.yml b/mkdocs.yml index 3a1cc71..9f56165 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -13,7 +13,6 @@ nav: - Home: index.md - Introduction: Introduction.md - Getting Started: GettingStarted.md - - Metrics: Metrics.md - Development: Development.md - How-to Guides: - Bitwarden Secrets Manager Setup: how-to-guides/Bitwarden.md From 218fe0dd52514635ccbb650e7945634766bab688 Mon Sep 17 00:00:00 2001 From: Veerendra <8393701+veerendra2@users.noreply.github.com> Date: Mon, 27 Jul 2026 22:06:59 +0200 Subject: [PATCH 4/7] fix: redeploy unhealthy stacks during git sync --- internal/reconcile/sync.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/internal/reconcile/sync.go b/internal/reconcile/sync.go index 4db1964..688a49d 100644 --- a/internal/reconcile/sync.go +++ b/internal/reconcile/sync.go @@ -72,9 +72,13 @@ func (r *Reconciler) GitSync(ctx context.Context) error { } // Check if stack needs deployment - if _, exists := currentStackMap[project.Name]; !exists { + stackInfo, exists := currentStackMap[project.Name] + if !exists { slog.Info("New stack detected", "stack_name", project.Name) toDeploy[project.Name] = project + } else if !stackInfo.Healthy && !stackInfo.Suspend { + slog.Info("Unhealthy stack detected", "stack_name", project.Name) + toDeploy[project.Name] = project } else if len(changedFiles) > 0 { // Stack is running, check if any changed file in git overlaps with stack's dependency tree deps := dockercompose.GetDependencyPaths(project) From 5e87609aedc50f89d6b3502793da822daf490fc1 Mon Sep 17 00:00:00 2001 From: Veerendra <8393701+veerendra2@users.noreply.github.com> Date: Mon, 27 Jul 2026 22:26:16 +0200 Subject: [PATCH 5/7] fix: return error if git diff calculation fails during pull --- pkg/source/git.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/source/git.go b/pkg/source/git.go index 5c9a471..3f792d9 100644 --- a/pkg/source/git.go +++ b/pkg/source/git.go @@ -67,7 +67,7 @@ func (c *client) Pull(ctx context.Context) ([]string, error) { if oldSHA != "" && oldSHA != newSHA { changedFiles, err = c.GetChangedFiles(ctx, oldSHA, newSHA) if err != nil { - slog.Warn("Failed to compute git diff file list", "old_sha", shortSHA(oldSHA), "new_sha", shortSHA(newSHA), "error", err) + return nil, fmt.Errorf("failed to compute git diff file list (%s..%s): %w", shortSHA(oldSHA), shortSHA(newSHA), err) } } From 6c579013fc78e7bec92daba62ba5b62633229a52 Mon Sep 17 00:00:00 2001 From: Veerendra <8393701+veerendra2@users.noreply.github.com> Date: Tue, 28 Jul 2026 21:40:22 +0200 Subject: [PATCH 6/7] fix: enable build options during compose up for build context services --- pkg/dockercompose/compose.go | 1 + 1 file changed, 1 insertion(+) diff --git a/pkg/dockercompose/compose.go b/pkg/dockercompose/compose.go index cedf0dd..142cf6b 100644 --- a/pkg/dockercompose/compose.go +++ b/pkg/dockercompose/compose.go @@ -165,6 +165,7 @@ func (c *client) Up(ctx context.Context, project *types.Project) error { Recreate: api.RecreateDiverged, RecreateDependencies: api.RecreateDiverged, Inherit: true, + Build: &api.BuildOptions{}, }, Start: api.StartOptions{ Project: project, From 65e821e0fd3a69284c463894f9a090507aa4687b Mon Sep 17 00:00:00 2001 From: Veerendra <8393701+veerendra2@users.noreply.github.com> Date: Tue, 28 Jul 2026 22:23:48 +0200 Subject: [PATCH 7/7] fix: set quiet build option during compose up --- pkg/dockercompose/compose.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pkg/dockercompose/compose.go b/pkg/dockercompose/compose.go index 142cf6b..115e8fd 100644 --- a/pkg/dockercompose/compose.go +++ b/pkg/dockercompose/compose.go @@ -165,7 +165,9 @@ func (c *client) Up(ctx context.Context, project *types.Project) error { Recreate: api.RecreateDiverged, RecreateDependencies: api.RecreateDiverged, Inherit: true, - Build: &api.BuildOptions{}, + Build: &api.BuildOptions{ + Quiet: true, + }, }, Start: api.StartOptions{ Project: project,