From 4f387d0c70b2578bc84b97fb0745727fa6886f04 Mon Sep 17 00:00:00 2001 From: Marcio Altoe Date: Mon, 11 May 2026 08:11:06 -0300 Subject: [PATCH 1/4] fix: handle duplicate sidecar blobs --- internal/sidecar/reconcile.go | 17 ++++++------ internal/sidecar/service_test.go | 44 ++++++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+), 8 deletions(-) diff --git a/internal/sidecar/reconcile.go b/internal/sidecar/reconcile.go index c00b363..445f59c 100644 --- a/internal/sidecar/reconcile.go +++ b/internal/sidecar/reconcile.go @@ -464,7 +464,7 @@ func (s *Service) sidecarSnapshot( } files := map[string]reconcile.SnapshotFile{} objects := make([]string, 0) - objectPaths := map[string]string{} + objectPaths := map[string][]string{} prefix := namespace + "/" for raw := range strings.SplitSeq(out.Stdout, "\x00") { if raw == "" { @@ -489,7 +489,7 @@ func (s *Service) sidecarSnapshot( object := fields[2] files[rel] = reconcile.SnapshotFile{Path: rel, Size: size, Blob: object} objects = append(objects, object) - objectPaths[object] = rel + objectPaths[object] = append(objectPaths[object], rel) } if len(objects) == 0 { return files, nil @@ -499,12 +499,13 @@ func (s *Service) sidecarSnapshot( return nil, err } for object, content := range contents { - rel := objectPaths[object] - file := files[rel] - file.Content = content - file.Size = int64(len(content)) - file.SHA256 = sha256String(content) - files[rel] = file + for _, rel := range objectPaths[object] { + file := files[rel] + file.Content = content + file.Size = int64(len(content)) + file.SHA256 = sha256String(content) + files[rel] = file + } } return files, nil } diff --git a/internal/sidecar/service_test.go b/internal/sidecar/service_test.go index 8de84a2..587f02c 100644 --- a/internal/sidecar/service_test.go +++ b/internal/sidecar/service_test.go @@ -219,6 +219,50 @@ func TestServiceVerifyAndFSCKUseLockfile(t *testing.T) { } } +func TestServiceFSCKHandlesDuplicateSidecarBlobs(t *testing.T) { + setGitIdentity(t) + + ctx := context.Background() + root := newMainRepo(t) + remote := newBareRepo(t) + cfg := singleNamespaceConfig(remote, "project", []string{"**/SPEC.md"}) + bootstrapRepo(t, root, cfg) + writeFile(t, root, "src/auth/SPEC.md", "# Shared\n") + writeFile(t, root, "src/billing/SPEC.md", "# Shared\n") + + service := sidecar.New(&gitexec.ExecRunner{}) + if _, err := service.Sync(ctx, root, sidecar.SyncOptions{}); err != nil { + t.Fatalf("sync: %v", err) + } + + fsck, err := service.FSCK(ctx, root, sidecar.FSCKOptions{}) + if err != nil { + t.Fatalf("fsck: %v", err) + } + if !fsck.OK { + t.Fatalf("expected duplicate-content specs to fsck clean: %#v", fsck) + } + + journalPath := filepath.Join(root, ".git", "skeeper", "hydration.json") + data, err := os.ReadFile(journalPath) + if err != nil { + t.Fatalf("read hydration journal: %v", err) + } + var journal state.HydrationJournal + if err := json.Unmarshal(data, &journal); err != nil { + t.Fatalf("decode hydration journal: %v", err) + } + files := journal.Namespaces["project"].Files + auth := files["src/auth/SPEC.md"] + billing := files["src/billing/SPEC.md"] + if auth.SHA256 == "" || billing.SHA256 == "" { + t.Fatalf("expected duplicate blob entries to keep sha256: %#v", files) + } + if auth.SHA256 != billing.SHA256 || auth.SidecarBlob != billing.SidecarBlob { + t.Fatalf("expected duplicate files to share digest and blob: %#v", files) + } +} + func TestServiceHydrateBlocksLocalOnlyByDefaultAndPrunesToRescue(t *testing.T) { setGitIdentity(t) From 39be74ed406a41c56cc655553d9a26cac2df5ea9 Mon Sep 17 00:00:00 2001 From: Marcio Altoe Date: Mon, 11 May 2026 08:28:16 -0300 Subject: [PATCH 2/4] test(sidecar): wrap duplicate blob fsck case --- internal/sidecar/service_test.go | 80 ++++++++++++++++---------------- 1 file changed, 41 insertions(+), 39 deletions(-) diff --git a/internal/sidecar/service_test.go b/internal/sidecar/service_test.go index 587f02c..073f9ef 100644 --- a/internal/sidecar/service_test.go +++ b/internal/sidecar/service_test.go @@ -220,47 +220,49 @@ func TestServiceVerifyAndFSCKUseLockfile(t *testing.T) { } func TestServiceFSCKHandlesDuplicateSidecarBlobs(t *testing.T) { - setGitIdentity(t) - - ctx := context.Background() - root := newMainRepo(t) - remote := newBareRepo(t) - cfg := singleNamespaceConfig(remote, "project", []string{"**/SPEC.md"}) - bootstrapRepo(t, root, cfg) - writeFile(t, root, "src/auth/SPEC.md", "# Shared\n") - writeFile(t, root, "src/billing/SPEC.md", "# Shared\n") - - service := sidecar.New(&gitexec.ExecRunner{}) - if _, err := service.Sync(ctx, root, sidecar.SyncOptions{}); err != nil { - t.Fatalf("sync: %v", err) - } + t.Run("Should handle duplicate sidecar blobs", func(t *testing.T) { + setGitIdentity(t) + + ctx := context.Background() + root := newMainRepo(t) + remote := newBareRepo(t) + cfg := singleNamespaceConfig(remote, "project", []string{"**/SPEC.md"}) + bootstrapRepo(t, root, cfg) + writeFile(t, root, "src/auth/SPEC.md", "# Shared\n") + writeFile(t, root, "src/billing/SPEC.md", "# Shared\n") + + service := sidecar.New(&gitexec.ExecRunner{}) + if _, err := service.Sync(ctx, root, sidecar.SyncOptions{}); err != nil { + t.Fatalf("sync: %v", err) + } - fsck, err := service.FSCK(ctx, root, sidecar.FSCKOptions{}) - if err != nil { - t.Fatalf("fsck: %v", err) - } - if !fsck.OK { - t.Fatalf("expected duplicate-content specs to fsck clean: %#v", fsck) - } + fsck, err := service.FSCK(ctx, root, sidecar.FSCKOptions{}) + if err != nil { + t.Fatalf("fsck: %v", err) + } + if !fsck.OK { + t.Fatalf("expected duplicate-content specs to fsck clean: %#v", fsck) + } - journalPath := filepath.Join(root, ".git", "skeeper", "hydration.json") - data, err := os.ReadFile(journalPath) - if err != nil { - t.Fatalf("read hydration journal: %v", err) - } - var journal state.HydrationJournal - if err := json.Unmarshal(data, &journal); err != nil { - t.Fatalf("decode hydration journal: %v", err) - } - files := journal.Namespaces["project"].Files - auth := files["src/auth/SPEC.md"] - billing := files["src/billing/SPEC.md"] - if auth.SHA256 == "" || billing.SHA256 == "" { - t.Fatalf("expected duplicate blob entries to keep sha256: %#v", files) - } - if auth.SHA256 != billing.SHA256 || auth.SidecarBlob != billing.SidecarBlob { - t.Fatalf("expected duplicate files to share digest and blob: %#v", files) - } + journalPath := filepath.Join(root, ".git", "skeeper", "hydration.json") + data, err := os.ReadFile(journalPath) + if err != nil { + t.Fatalf("read hydration journal: %v", err) + } + var journal state.HydrationJournal + if err := json.Unmarshal(data, &journal); err != nil { + t.Fatalf("decode hydration journal: %v", err) + } + files := journal.Namespaces["project"].Files + auth := files["src/auth/SPEC.md"] + billing := files["src/billing/SPEC.md"] + if auth.SHA256 == "" || billing.SHA256 == "" { + t.Fatalf("expected duplicate blob entries to keep sha256: %#v", files) + } + if auth.SHA256 != billing.SHA256 || auth.SidecarBlob != billing.SidecarBlob { + t.Fatalf("expected duplicate files to share digest and blob: %#v", files) + } + }) } func TestServiceHydrateBlocksLocalOnlyByDefaultAndPrunesToRescue(t *testing.T) { From 96c901364b0d5227bc091d3152a55db3602229ba Mon Sep 17 00:00:00 2001 From: Marcio Altoe Date: Mon, 11 May 2026 08:41:54 -0300 Subject: [PATCH 3/4] build: add install target --- Makefile | 5 ++++- magefile.go | 5 +++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 0eb5d52..85315c6 100644 --- a/Makefile +++ b/Makefile @@ -4,7 +4,7 @@ MAGE_RUN = go run github.com/magefile/mage@$(MAGE_VERSION) DOCKER_IMAGE ?= skeeper DOCKER_TAG ?= dev -.PHONY: deps fmt lint modernize test test-integration cover build verify tools \ +.PHONY: deps fmt lint modernize test test-integration cover build install verify tools \ bun-lint bun-fmt bun-fmt-check hooks-install release-snapshot docker-build help deps: @@ -31,6 +31,9 @@ cover: build: @$(MAGE_RUN) build +install: build + @$(MAGE_RUN) install + verify: @$(MAGE_RUN) verify diff --git a/magefile.go b/magefile.go index 7f1378b..96dde0d 100644 --- a/magefile.go +++ b/magefile.go @@ -114,6 +114,11 @@ func Build() error { return sh.RunV("go", "build", "-trimpath", "-ldflags", buildLDFlags(), "-o", out, "./cmd/skeeper") } +// Install installs the application binary into GOBIN/GOPATH/bin with version ldflags. +func Install() error { + return sh.RunV("go", "install", "-trimpath", "-ldflags", buildLDFlags(), "./cmd/skeeper") +} + // Verify runs the blocking gate: fmt -> lint -> test -> build. func Verify() { mg.SerialDeps(Fmt, Lint, Test, Build) From 7de3cf20bd34a3b20cbf9dcf8f55dd49a739e90a Mon Sep 17 00:00:00 2001 From: Marcio Altoe Date: Mon, 11 May 2026 08:47:17 -0300 Subject: [PATCH 4/4] build: report install status --- magefile.go | 42 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 41 insertions(+), 1 deletion(-) diff --git a/magefile.go b/magefile.go index 96dde0d..5e3d2a8 100644 --- a/magefile.go +++ b/magefile.go @@ -116,7 +116,16 @@ func Build() error { // Install installs the application binary into GOBIN/GOPATH/bin with version ldflags. func Install() error { - return sh.RunV("go", "install", "-trimpath", "-ldflags", buildLDFlags(), "./cmd/skeeper") + installPath, err := goInstallPath() + if err != nil { + return err + } + fmt.Printf("Installing %s to %s\n", appBinary, installPath) + if err := sh.RunV("go", "install", "-trimpath", "-ldflags", buildLDFlags(), "./cmd/skeeper"); err != nil { + return fmt.Errorf("install %s: %w", appBinary, err) + } + fmt.Printf("Installed %s to %s\n", appBinary, installPath) + return nil } // Verify runs the blocking gate: fmt -> lint -> test -> build. @@ -244,6 +253,37 @@ func gitOutput(args ...string) string { return strings.TrimSpace(string(out)) } +func goInstallPath() (string, error) { + gobin, err := goEnv("GOBIN") + if err != nil { + return "", err + } + if gobin != "" { + return filepath.Join(gobin, appBinary), nil + } + gopath, err := goEnv("GOPATH") + if err != nil { + return "", err + } + if gopath == "" { + return "", fmt.Errorf("go env GOPATH returned an empty path") + } + firstGoPath := filepath.SplitList(gopath)[0] + if firstGoPath == "" { + return "", fmt.Errorf("go env GOPATH returned an empty first path") + } + return filepath.Join(firstGoPath, "bin", appBinary), nil +} + +func goEnv(key string) (string, error) { + cmd := exec.Command("go", "env", key) + out, err := cmd.Output() + if err != nil { + return "", fmt.Errorf("go env %s: %w", key, err) + } + return strings.TrimSpace(string(out)), nil +} + func runWithEnv(env map[string]string, name string, args ...string) error { cmd := exec.CommandContext(context.Background(), name, args...) cmd.Stdout = os.Stdout