From f89b7be64d19bcc9e4d7f924333c941229170700 Mon Sep 17 00:00:00 2001 From: Andy Postnikov Date: Sun, 8 Feb 2026 07:54:20 +0100 Subject: [PATCH 1/4] Fix bump not working in git worktrees In a git worktree, .git is a file (not a directory) pointing to the main repository's .git/worktrees// directory. go-git's PlainOpen() uses EnableDotGitCommonDir: false by default, which means it does not read the commondir file to discover shared data (objects, refs). This causes operations like CommitObject() and Log() to fail because the objects database is not found in the worktree-specific directory. Replace git.PlainOpen() with git.PlainOpenWithOptions() using EnableDotGitCommonDir: true in all four locations: - pkg/repository/git.go: NewBumper() - actionSync.resources.go: findResourcesChangeTime(), processResource() - actionSync.variables.go: findVariableUpdateTime() EnableDotGitCommonDir: true is safe for non-worktree repos: the commondir file won't exist, so go-git falls through to normal behavior. Add tests verifying NewBumper() and PlainOpenWithOptions work correctly on both regular repositories and git worktrees. Co-Authored-By: Claude Opus 4.6 --- actionSync.resources.go | 4 +- actionSync.variables.go | 2 +- pkg/repository/git.go | 2 +- pkg/repository/git_test.go | 449 +++++++++++++++++++++++++++++++++++++ 4 files changed, 453 insertions(+), 4 deletions(-) create mode 100644 pkg/repository/git_test.go diff --git a/actionSync.resources.go b/actionSync.resources.go index b2af5e0..09efc01 100644 --- a/actionSync.resources.go +++ b/actionSync.resources.go @@ -224,7 +224,7 @@ func collectResourcesCommits(r *git.Repository, beforeDate string) (*sync.Ordere } func (s *syncAction) findResourcesChangeTime(ctx context.Context, namespaceResources *sync.OrderedMap[*sync.Resource], gitPath string, mx *async.Mutex, p *pterm.ProgressbarPrinter) error { - repo, err := git.PlainOpen(gitPath) + repo, err := git.PlainOpenWithOptions(gitPath, &git.PlainOpenOptions{EnableDotGitCommonDir: true}) if err != nil { return fmt.Errorf("%s - %w", gitPath, err) } @@ -296,7 +296,7 @@ func (s *syncAction) findResourcesChangeTime(ctx context.Context, namespaceResou } func (s *syncAction) processResource(resource *sync.Resource, commitsGroups *sync.OrderedMap[*CommitsGroup], commitsMap map[string]map[string]string, _ *git.Repository, gitPath string, mx *async.Mutex) error { - repo, err := git.PlainOpen(gitPath) + repo, err := git.PlainOpenWithOptions(gitPath, &git.PlainOpenOptions{EnableDotGitCommonDir: true}) if err != nil { return fmt.Errorf("%s - %w", gitPath, err) } diff --git a/actionSync.variables.go b/actionSync.variables.go index a9ce2be..8ad176a 100644 --- a/actionSync.variables.go +++ b/actionSync.variables.go @@ -110,7 +110,7 @@ func (s *syncAction) populateTimelineVars(buildInv *sync.Inventory) error { } func (s *syncAction) findVariableUpdateTime(varsFile string, inv *sync.Inventory, gitPath string, mx *async.Mutex) error { - repo, err := git.PlainOpen(gitPath) + repo, err := git.PlainOpenWithOptions(gitPath, &git.PlainOpenOptions{EnableDotGitCommonDir: true}) if err != nil { return fmt.Errorf("%s - %w", gitPath, err) } diff --git a/pkg/repository/git.go b/pkg/repository/git.go index 6e5d54f..ff9641a 100644 --- a/pkg/repository/git.go +++ b/pkg/repository/git.go @@ -38,7 +38,7 @@ type Commit struct { // NewBumper returns new instance of [Bumper]. func NewBumper() (*Bumper, error) { - r, err := git.PlainOpen("./") + r, err := git.PlainOpenWithOptions("./", &git.PlainOpenOptions{EnableDotGitCommonDir: true}) if err != nil { return nil, err } diff --git a/pkg/repository/git_test.go b/pkg/repository/git_test.go new file mode 100644 index 0000000..2b68735 --- /dev/null +++ b/pkg/repository/git_test.go @@ -0,0 +1,449 @@ +package repository + +import ( + "os" + "os/exec" + "path/filepath" + "testing" + "time" + + "github.com/go-git/go-git/v5" + "github.com/go-git/go-git/v5/plumbing/object" + "github.com/go-git/go-git/v5/plumbing/storer" +) + +// initTestRepo creates a temporary git repository with one commit. +func initTestRepo(t *testing.T) string { + t.Helper() + dir := t.TempDir() + + repo, err := git.PlainInit(dir, false) + if err != nil { + t.Fatalf("git init: %v", err) + } + + w, err := repo.Worktree() + if err != nil { + t.Fatalf("worktree: %v", err) + } + + if err = os.WriteFile(filepath.Join(dir, "README.md"), []byte("test"), 0600); err != nil { + t.Fatalf("write file: %v", err) + } + + if _, err = w.Add("README.md"); err != nil { + t.Fatalf("git add: %v", err) + } + + _, err = w.Commit("initial commit", &git.CommitOptions{ + Author: &object.Signature{ + Name: "Test", + Email: "test@test.com", + When: time.Now(), + }, + }) + if err != nil { + t.Fatalf("commit: %v", err) + } + + return dir +} + +// initTestRepoWithResource creates a git repo with an Ansible role resource +// structure and multiple commits, including a bump commit. +func initTestRepoWithResource(t *testing.T) string { + t.Helper() + dir := t.TempDir() + + repo, err := git.PlainInit(dir, false) + if err != nil { + t.Fatalf("git init: %v", err) + } + + w, err := repo.Worktree() + if err != nil { + t.Fatalf("worktree: %v", err) + } + + // Commit 1: initial resource with meta/plasma.yaml. + metaDir := filepath.Join(dir, "interaction", "softwares", "roles", "grafana", "meta") + if err = os.MkdirAll(metaDir, 0750); err != nil { + t.Fatalf("mkdir: %v", err) + } + + tasksDir := filepath.Join(dir, "interaction", "softwares", "roles", "grafana", "tasks") + if err = os.MkdirAll(tasksDir, 0750); err != nil { + t.Fatalf("mkdir: %v", err) + } + + plasmaYaml := "plasma:\n version: \"aaa1111111111\"\n description: Test resource\n" + if err = os.WriteFile(filepath.Join(metaDir, "plasma.yaml"), []byte(plasmaYaml), 0600); err != nil { + t.Fatalf("write plasma.yaml: %v", err) + } + + configYaml := "- include_role:\n name: interaction.softwares.postgres\n" + if err = os.WriteFile(filepath.Join(tasksDir, "configuration.yaml"), []byte(configYaml), 0600); err != nil { + t.Fatalf("write configuration.yaml: %v", err) + } + + if _, err = w.Add("."); err != nil { + t.Fatalf("git add: %v", err) + } + + _, err = w.Commit("add grafana resource", &git.CommitOptions{ + Author: &object.Signature{ + Name: "Developer", + Email: "dev@test.com", + When: time.Now().Add(-2 * time.Hour), + }, + }) + if err != nil { + t.Fatalf("commit 1: %v", err) + } + + // Commit 2: bump commit (by Bumper author). + plasmaYaml = "plasma:\n version: \"bbb2222222222\"\n description: Test resource\n" + if err = os.WriteFile(filepath.Join(metaDir, "plasma.yaml"), []byte(plasmaYaml), 0600); err != nil { + t.Fatalf("write plasma.yaml: %v", err) + } + + if _, err = w.Add("."); err != nil { + t.Fatalf("git add: %v", err) + } + + _, err = w.Commit(BumpMessage, &git.CommitOptions{ + Author: &object.Signature{ + Name: Author, + Email: "no-reply@skilld.cloud", + When: time.Now().Add(-1 * time.Hour), + }, + }) + if err != nil { + t.Fatalf("commit 2: %v", err) + } + + // Commit 3: a regular change after bump. + templateDir := filepath.Join(dir, "interaction", "softwares", "roles", "grafana", "templates") + if err = os.MkdirAll(templateDir, 0750); err != nil { + t.Fatalf("mkdir: %v", err) + } + + if err = os.WriteFile(filepath.Join(templateDir, "config.j2"), []byte("port={{ grafana_port }}"), 0600); err != nil { + t.Fatalf("write template: %v", err) + } + + if _, err = w.Add("."); err != nil { + t.Fatalf("git add: %v", err) + } + + _, err = w.Commit("update grafana template", &git.CommitOptions{ + Author: &object.Signature{ + Name: "Developer", + Email: "dev@test.com", + When: time.Now(), + }, + }) + if err != nil { + t.Fatalf("commit 3: %v", err) + } + + return dir +} + +// createWorktree creates a git worktree using the git CLI. +func createWorktree(t *testing.T, repoDir string) string { + t.Helper() + + if _, err := exec.LookPath("git"); err != nil { + t.Skip("git CLI not available") + } + + cmd := exec.Command("git", "branch", "test-wt") + cmd.Dir = repoDir + if out, err := cmd.CombinedOutput(); err != nil { + t.Fatalf("git branch: %s: %v", out, err) + } + + wtDir := filepath.Join(t.TempDir(), "wt") + cmd = exec.Command("git", "worktree", "add", wtDir, "test-wt") + cmd.Dir = repoDir + if out, err := cmd.CombinedOutput(); err != nil { + t.Fatalf("git worktree add: %s: %v", out, err) + } + + return wtDir +} + +func TestNewBumper(t *testing.T) { + repoDir := initTestRepo(t) + + orig, err := os.Getwd() + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { _ = os.Chdir(orig) }) + + if err = os.Chdir(repoDir); err != nil { + t.Fatal(err) + } + + bumper, err := NewBumper() + if err != nil { + t.Fatalf("NewBumper: %v", err) + } + + if bumper.IsOwnCommit() { + t.Error("expected IsOwnCommit() to be false") + } + + commits, err := bumper.GetCommits(false) + if err != nil { + t.Fatalf("GetCommits: %v", err) + } + + if len(commits) == 0 { + t.Error("expected at least one commit") + } +} + +func TestNewBumperWorktree(t *testing.T) { + repoDir := initTestRepo(t) + wtDir := createWorktree(t, repoDir) + + orig, err := os.Getwd() + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { _ = os.Chdir(orig) }) + + if err = os.Chdir(wtDir); err != nil { + t.Fatal(err) + } + + bumper, err := NewBumper() + if err != nil { + t.Fatalf("NewBumper in worktree: %v", err) + } + + if bumper.IsOwnCommit() { + t.Error("expected IsOwnCommit() to be false") + } + + commits, err := bumper.GetCommits(false) + if err != nil { + t.Fatalf("GetCommits in worktree: %v", err) + } + + if len(commits) == 0 { + t.Error("expected at least one commit") + } +} + +func TestPlainOpenWithOptionsRegularRepo(t *testing.T) { + repoDir := initTestRepo(t) + + repo, err := git.PlainOpenWithOptions(repoDir, &git.PlainOpenOptions{EnableDotGitCommonDir: true}) + if err != nil { + t.Fatalf("PlainOpenWithOptions on regular repo: %v", err) + } + + head, err := repo.Head() + if err != nil { + t.Fatalf("Head: %v", err) + } + + _, err = repo.CommitObject(head.Hash()) + if err != nil { + t.Fatalf("CommitObject: %v", err) + } +} + +func TestPlainOpenWithOptionsWorktree(t *testing.T) { + repoDir := initTestRepo(t) + wtDir := createWorktree(t, repoDir) + + repo, err := git.PlainOpenWithOptions(wtDir, &git.PlainOpenOptions{EnableDotGitCommonDir: true}) + if err != nil { + t.Fatalf("PlainOpenWithOptions on worktree: %v", err) + } + + // Verify commit objects are accessible (requires shared objects database from main repo). + head, err := repo.Head() + if err != nil { + t.Fatalf("Head: %v", err) + } + + _, err = repo.CommitObject(head.Hash()) + if err != nil { + t.Fatalf("CommitObject in worktree: %v", err) + } + + // Verify log iteration works (used by sync timeline population). + cIter, err := repo.Log(&git.LogOptions{From: head.Hash()}) + if err != nil { + t.Fatalf("Log in worktree: %v", err) + } + + count := 0 + _ = cIter.ForEach(func(c *object.Commit) error { + count++ + return nil + }) + + if count == 0 { + t.Error("expected at least one commit from log iteration") + } +} + +// TestBumpWorkflowInWorktree is an integration test that exercises the full +// bump workflow from a git worktree: opening the repo, detecting the bumper +// commit, collecting commits with changed files, and accessing file contents +// from commit objects (the pattern used by sync/propagation). +func TestBumpWorkflowInWorktree(t *testing.T) { + repoDir := initTestRepoWithResource(t) + wtDir := createWorktree(t, repoDir) + + orig, err := os.Getwd() + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { _ = os.Chdir(orig) }) + + if err = os.Chdir(wtDir); err != nil { + t.Fatal(err) + } + + // Phase 1: Bumper detects own commit correctly. + bumper, err := NewBumper() + if err != nil { + t.Fatalf("NewBumper in worktree: %v", err) + } + + // HEAD is a developer commit (commit 3), not a bump commit. + if bumper.IsOwnCommit() { + t.Error("expected IsOwnCommit() to be false for developer commit") + } + + // Phase 2: GetCommits returns only the post-bump commit (stops at Bumper author). + commits, err := bumper.GetCommits(false) + if err != nil { + t.Fatalf("GetCommits: %v", err) + } + + if len(commits) == 0 { + t.Fatal("expected at least one commit") + } + + // The commit after bump should contain the template file change. + foundTemplate := false + for _, c := range commits { + for _, f := range c.Files { + if filepath.Base(f) == "config.j2" { + foundTemplate = true + } + } + } + + if !foundTemplate { + t.Error("expected to find config.j2 in post-bump commits") + } + + // Phase 3: Sync patterns - open by path, access commit objects and files. + // This mirrors what findResourcesChangeTime and processResource do. + repo, err := git.PlainOpenWithOptions(wtDir, &git.PlainOpenOptions{EnableDotGitCommonDir: true}) + if err != nil { + t.Fatalf("PlainOpenWithOptions: %v", err) + } + + head, err := repo.Head() + if err != nil { + t.Fatalf("Head: %v", err) + } + + headCommit, err := repo.CommitObject(head.Hash()) + if err != nil { + t.Fatalf("CommitObject: %v", err) + } + + // Access resource file from commit tree (pattern from processResource). + metaPath := filepath.Join("interaction", "softwares", "roles", "grafana", "meta", "plasma.yaml") + file, err := headCommit.File(metaPath) + if err != nil { + t.Fatalf("File(%s) from commit: %v", metaPath, err) + } + + contents, err := file.Contents() + if err != nil { + t.Fatalf("file.Contents: %v", err) + } + + if contents == "" { + t.Error("expected non-empty plasma.yaml contents") + } + + // Phase 4: Log iteration with author detection (pattern from collectResourcesCommits). + cIter, err := repo.Log(&git.LogOptions{From: head.Hash()}) + if err != nil { + t.Fatalf("Log: %v", err) + } + + var bumperCommits, devCommits int + _ = cIter.ForEach(func(c *object.Commit) error { + if c.Author.Name == Author { + bumperCommits++ + } else { + devCommits++ + } + return nil + }) + + if bumperCommits != 1 { + t.Errorf("expected 1 bumper commit, got %d", bumperCommits) + } + + if devCommits != 2 { + t.Errorf("expected 2 developer commits, got %d", devCommits) + } + + // Phase 5: Tree diff between commits (pattern from GetCommits). + cIter, err = repo.Log(&git.LogOptions{From: head.Hash()}) + if err != nil { + t.Fatalf("Log: %v", err) + } + + var prevCommit *object.Commit + diffFound := false + + _ = cIter.ForEach(func(c *object.Commit) error { + if prevCommit == nil { + prevCommit = c + return nil + } + + prevTree, errT := prevCommit.Tree() + if errT != nil { + return errT + } + + currentTree, errT := c.Tree() + if errT != nil { + return errT + } + + diff, errT := currentTree.Diff(prevTree) + if errT != nil { + return errT + } + + if len(diff) > 0 { + diffFound = true + } + + prevCommit = c + return storer.ErrStop + }) + + if !diffFound { + t.Error("expected to find diffs between commits") + } +} From 726eb4fefa5e5de520d678d04587419680d7121f Mon Sep 17 00:00:00 2001 From: Andy Postnikov Date: Sun, 8 Feb 2026 08:07:11 +0100 Subject: [PATCH 2/4] Add CLAUDE.md docs and symlinked files Co-Authored-By: Claude Opus 4.6 --- AGENTS.md | 1 + CLAUDE.md | 79 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ GEMINI.md | 1 + 3 files changed, 81 insertions(+) create mode 120000 AGENTS.md create mode 100644 CLAUDE.md create mode 120000 GEMINI.md diff --git a/AGENTS.md b/AGENTS.md new file mode 120000 index 0000000..681311e --- /dev/null +++ b/AGENTS.md @@ -0,0 +1 @@ +CLAUDE.md \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..3999ccc --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,79 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +`plasmactl-bump` is a launchr plugin to update the version of Ansible roles which were updated in last commit. It detects modified resources (Ansible roles) via git history, bumps their versions, and propagates version changes through dependency chains across the Plasma platform. + +**Module**: `github.com/skilld-labs/plasmactl-bump/v2` +**Go version**: 1.25.0 (CGO disabled) +**Framework**: [launchr](https://github.com/launchrctl/launchr) plugin system + +## Build & Development Commands + +```bash +make build # Build binary to ./bin/launchr +make test # Run all tests (requires gotestfmt) +make test-short # Run short tests only +make lint # Run golangci-lint with auto-fix +make deps # Download Go dependencies +make all # deps + test-short + build +make clean # Remove ./bin/ +DEBUG=1 make build # Build with debug symbols +``` + +## Architecture + +### Plugin Registration + +The plugin registers via `init()` in `plugin.go`, using `//go:embed` for YAML action definitions. Entry point is `cmd/launchr/main.go` which imports the plugin as a blank import. + +### Three Actions + +1. **bump** (`actionBump.go`): Detects files changed since the last "Bumper"-authored commit, maps them to resources, and sets each resource's version to the commit's short hash (first 13 chars) in `meta/plasma.yaml`. Creates a commit authored by `Bumper `. + +2. **bump --sync** (`actionSync.go`, `actionSync.resources.go`, `actionSync.variables.go`): Propagates versions through dependency chains. Works on a post-`compose` build directory (`.compose/build`). Builds a chronological timeline of version changes, then applies propagation so dependent resources get composite versions like `original-propagated`. + +3. **dependencies** (`dependencies.go`): Shows dependency tree for a given resource. + +### Core Packages + +- **`pkg/repository`** (`git.go`): Git operations via `go-git`. `Bumper` struct handles commit traversal, detecting own commits, and creating bump commits. All `PlainOpen` calls use `PlainOpenWithOptions` with `EnableDotGitCommonDir: true` to support git worktrees. + +- **`pkg/sync`**: Resource/variable management and dependency resolution. + - `inventory.go`: Builds resource dependency graph by walking filesystem, parsing `tasks/*.yaml` for `include_role` references, topologically sorting with `topsort`. + - `inventory.resource.go`: `Resource` type, `OrderedMap[T]` generic ordered map, MRN (Machine Resource Name) format `platform__kind__role`, version read/write from `meta/plasma.yaml`. + - `inventory.variable.go`: Variable tracking from `group_vars/vars.yaml` and `vault.yaml`, variable-to-variable and variable-to-resource dependency maps. + - `timeline.go`: `TimelineItem` interface with `TimelineResourcesItem` and `TimelineVariablesItem` implementations, chronological sorting. + - `filesCrawler.go`: File discovery with Jinja2 template variable extraction. + - `yaml.go`: YAML parsing with ansible-vault support. + +### Key Conventions + +- **MRN format**: `platform__kind__role` (double underscore separator), e.g. `interaction__softwares__grafana` +- **Resource path pattern**: `{platform}/{kind}/roles/{role}/meta/plasma.yaml` +- **Valid kinds**: applications, services, softwares, executors, flows, skills, functions, libraries, entities +- **Version format**: Short git hash (`abc1234567890`) or composite `baseversion-propagatedversion` +- **Bump commit author**: `Bumper` with email `no-reply@skilld.cloud`, message `versions bump` + +### Sync Flow (Propagation) + +1. Initialize inventory from `.compose/build` directory +2. Gather resources from domain (`.`) and packages (`.compose/packages/`) +3. Resolve duplicate resources across namespaces by matching build version +4. Build timeline: find commits where each resource version was set +5. Build propagation map: iterate timeline chronologically, find dependents +6. Update resources: compose `original-propagated` version strings + +### Concurrency + +Worker goroutines (bounded by `runtime.NumCPU()`) process packages in parallel with mutex-protected shared state. The `sync` stdlib package is imported as `async` to avoid name collision with the `pkg/sync` package. + +## Linting + +Uses golangci-lint v2 with: dupl (threshold: 100), errcheck, goconst, gosec, govet, ineffassign, revive, staticcheck, unused. Formatter: goimports. + +## CI + +GitHub Actions workflow (`.github/workflows/commit.yml`) runs four jobs: sync with vault integration, basic bump commands, go-linters, go-tests. diff --git a/GEMINI.md b/GEMINI.md new file mode 120000 index 0000000..681311e --- /dev/null +++ b/GEMINI.md @@ -0,0 +1 @@ +CLAUDE.md \ No newline at end of file From 5f839ab442e1361d5423f3b1920089957f59b179 Mon Sep 17 00:00:00 2001 From: Andy Postnikov Date: Sun, 8 Feb 2026 08:10:50 +0100 Subject: [PATCH 3/4] Fix review comments Reuse git.Repository in processResource instead of reopening on each call. Check ForEach errors in tests. Co-Authored-By: Claude Opus 4.6 --- actionSync.resources.go | 9 ++------- pkg/repository/git_test.go | 15 ++++++++++++--- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/actionSync.resources.go b/actionSync.resources.go index 09efc01..5f6ec83 100644 --- a/actionSync.resources.go +++ b/actionSync.resources.go @@ -250,7 +250,7 @@ func (s *syncAction) findResourcesChangeTime(ctx context.Context, namespaceResou if !ok { return } - if err = s.processResource(r, groups, commitsMap, repo, gitPath, mx); err != nil { + if err = s.processResource(r, groups, commitsMap, repo, mx); err != nil { if p != nil { _, _ = p.Stop() } @@ -295,12 +295,7 @@ func (s *syncAction) findResourcesChangeTime(ctx context.Context, namespaceResou return nil } -func (s *syncAction) processResource(resource *sync.Resource, commitsGroups *sync.OrderedMap[*CommitsGroup], commitsMap map[string]map[string]string, _ *git.Repository, gitPath string, mx *async.Mutex) error { - repo, err := git.PlainOpenWithOptions(gitPath, &git.PlainOpenOptions{EnableDotGitCommonDir: true}) - if err != nil { - return fmt.Errorf("%s - %w", gitPath, err) - } - +func (s *syncAction) processResource(resource *sync.Resource, commitsGroups *sync.OrderedMap[*CommitsGroup], commitsMap map[string]map[string]string, repo *git.Repository, mx *async.Mutex) error { buildResource := sync.NewResource(resource.GetName(), s.buildDir) currentVersion, debug, err := buildResource.GetVersion() for _, d := range debug { diff --git a/pkg/repository/git_test.go b/pkg/repository/git_test.go index 2b68735..4f04a33 100644 --- a/pkg/repository/git_test.go +++ b/pkg/repository/git_test.go @@ -285,10 +285,13 @@ func TestPlainOpenWithOptionsWorktree(t *testing.T) { } count := 0 - _ = cIter.ForEach(func(c *object.Commit) error { + err = cIter.ForEach(func(c *object.Commit) error { count++ return nil }) + if err != nil { + t.Fatalf("Log.ForEach in worktree: %v", err) + } if count == 0 { t.Error("expected at least one commit from log iteration") @@ -388,7 +391,7 @@ func TestBumpWorkflowInWorktree(t *testing.T) { } var bumperCommits, devCommits int - _ = cIter.ForEach(func(c *object.Commit) error { + err = cIter.ForEach(func(c *object.Commit) error { if c.Author.Name == Author { bumperCommits++ } else { @@ -396,6 +399,9 @@ func TestBumpWorkflowInWorktree(t *testing.T) { } return nil }) + if err != nil { + t.Fatalf("Log.ForEach: %v", err) + } if bumperCommits != 1 { t.Errorf("expected 1 bumper commit, got %d", bumperCommits) @@ -414,7 +420,7 @@ func TestBumpWorkflowInWorktree(t *testing.T) { var prevCommit *object.Commit diffFound := false - _ = cIter.ForEach(func(c *object.Commit) error { + err = cIter.ForEach(func(c *object.Commit) error { if prevCommit == nil { prevCommit = c return nil @@ -442,6 +448,9 @@ func TestBumpWorkflowInWorktree(t *testing.T) { prevCommit = c return storer.ErrStop }) + if err != nil { + t.Fatalf("Log.ForEach diff: %v", err) + } if !diffFound { t.Error("expected to find diffs between commits") From d618346db4eabd3d04652379980245c3653c42a2 Mon Sep 17 00:00:00 2001 From: Andy Postnikov Date: Sun, 8 Feb 2026 08:22:04 +0100 Subject: [PATCH 4/4] Open git repository once in populateTimelineVars Move PlainOpenWithOptions call from findVariableUpdateTime (called per vars file from worker goroutines) to populateTimelineVars (called once before spawning workers). Pass *git.Repository instead of gitPath. Co-Authored-By: Claude Opus 4.6 --- actionSync.variables.go | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/actionSync.variables.go b/actionSync.variables.go index 8ad176a..cfef895 100644 --- a/actionSync.variables.go +++ b/actionSync.variables.go @@ -41,6 +41,11 @@ func (s *syncAction) populateTimelineVars(buildInv *sync.Inventory) error { varsFiles = append(varsFiles, paths...) } + repo, err := git.PlainOpenWithOptions(s.domainDir, &git.PlainOpenOptions{EnableDotGitCommonDir: true}) + if err != nil { + return fmt.Errorf("%s - %w", s.domainDir, err) + } + ctx, cancel := context.WithCancel(context.Background()) defer cancel() @@ -66,7 +71,7 @@ func (s *syncAction) populateTimelineVars(buildInv *sync.Inventory) error { if !ok { return } - if err = s.findVariableUpdateTime(varsFile, buildInv, s.domainDir, &mx); err != nil { + if err = s.findVariableUpdateTime(varsFile, buildInv, repo, &mx); err != nil { if p != nil { _, _ = p.Stop() } @@ -109,12 +114,7 @@ func (s *syncAction) populateTimelineVars(buildInv *sync.Inventory) error { return nil } -func (s *syncAction) findVariableUpdateTime(varsFile string, inv *sync.Inventory, gitPath string, mx *async.Mutex) error { - repo, err := git.PlainOpenWithOptions(gitPath, &git.PlainOpenOptions{EnableDotGitCommonDir: true}) - if err != nil { - return fmt.Errorf("%s - %w", gitPath, err) - } - +func (s *syncAction) findVariableUpdateTime(varsFile string, inv *sync.Inventory, repo *git.Repository, mx *async.Mutex) error { ref, err := repo.Head() if err != nil { return fmt.Errorf("can't get HEAD ref > %w", err)