diff --git a/Makefile b/Makefile index 22c4e8d..e52bab9 100644 --- a/Makefile +++ b/Makefile @@ -110,6 +110,15 @@ test: .install-gotestfmt echo "$(BOLD)$(RED)πŸ§ͺ ❌ Some tests failed$(RESET)" @echo +# Run all tests with race detector +.PHONY: test-race +test-race: .install-gotestfmt + $(call print_step,"Running all tests with race detector...") + @go test -json -race -v ./... | $(GOTESTFMT_BIN) -hide all && \ + echo "$(BOLD)$(GREEN)πŸ§ͺ βœ… All tests passed (race detector clean)$(RESET)" || \ + echo "$(BOLD)$(RED)πŸ§ͺ ❌ Some tests failed$(RESET)" + @echo + # Run short tests .PHONY: test-short test-short: .install-gotestfmt @@ -119,6 +128,14 @@ test-short: .install-gotestfmt echo "$(BOLD)$(RED)πŸ§ͺ ❌ Some short tests failed$(RESET)" @echo +# Run integration tests keeping work dir for inspection +# Usage: make test-integration [TEST=sync_basic_propagation] +.PHONY: test-integration +test-integration: .install-gotestfmt + $(call print_step,"Running integration tests β€” keeping work dir...") + @go test -json -v -run "Test(Bump|Sync)$(if $(TEST),/$(TEST),)" ./test/ -args -testwork | $(GOTESTFMT_BIN) + @echo + # Build launchr .PHONY: build build: @@ -137,6 +154,13 @@ install: all @cp $(LOCAL_BIN)/launchr $(GOBIN)/launchr $(call print_success,"πŸš€ launchr installed to $(GOBIN)/launchr") +# Install launchr without running tests +.PHONY: install-bin +install-bin: deps build + $(call print_step,"Installing launchr to GOPATH...") + @cp $(LOCAL_BIN)/launchr $(GOBIN)/launchr + $(call print_success,"πŸš€ launchr installed to $(GOBIN)/launchr") + # Install and run linters .PHONY: lint lint: .install-lint .lint-fix @@ -189,19 +213,22 @@ help: $(call print_header) @echo "$(BOLD)$(WHITE)Available targets:$(RESET)" @echo "" - @echo " $(BOLD)$(GREEN)all$(RESET) 🎯 Run deps, test, and build" - @echo " $(BOLD)$(GREEN)deps$(RESET) πŸ“¦ Install go dependencies" - @echo " $(BOLD)$(GREEN)test$(RESET) πŸ§ͺ Run all tests" - @echo " $(BOLD)$(GREEN)test-short$(RESET) ⚑ Run short tests only" - @echo " $(BOLD)$(GREEN)build$(RESET) πŸ”¨ Build launchr binary" - @echo " $(BOLD)$(GREEN)install$(RESET) πŸš€ Install launchr to GOPATH" - @echo " $(BOLD)$(GREEN)lint$(RESET) πŸ” Run linters with auto-fix" - @echo " $(BOLD)$(GREEN)clean$(RESET) 🧹 Clean build artifacts" - @echo " $(BOLD)$(GREEN)help$(RESET) ❓ Show this help message" + @echo " $(BOLD)$(GREEN)all$(RESET) 🎯 Run deps, test, and build" + @echo " $(BOLD)$(GREEN)deps$(RESET) πŸ“¦ Install go dependencies" + @echo " $(BOLD)$(GREEN)test$(RESET) πŸ§ͺ Run all tests" + @echo " $(BOLD)$(GREEN)test-race$(RESET) 🏁 Run all tests with race detector" + @echo " $(BOLD)$(GREEN)test-integration$(RESET) πŸ”¬ Run integration tests keeping work dir (TEST=name to filter)" + @echo " $(BOLD)$(GREEN)test-short$(RESET) ⚑ Run short tests only" + @echo " $(BOLD)$(GREEN)build$(RESET) πŸ”¨ Build launchr binary" + @echo " $(BOLD)$(GREEN)install$(RESET) πŸš€ Install launchr to GOPATH (with tests)" + @echo " $(BOLD)$(GREEN)install-bin$(RESET) πŸš€ Install launchr to GOPATH (without tests)" + @echo " $(BOLD)$(GREEN)lint$(RESET) πŸ” Run linters with auto-fix" + @echo " $(BOLD)$(GREEN)clean$(RESET) 🧹 Clean build artifacts" + @echo " $(BOLD)$(GREEN)help$(RESET) ❓ Show this help message" @echo "" @echo "$(BOLD)$(CYAN)Environment variables:$(RESET)" - @echo " $(BOLD)$(YELLOW)DEBUG=1$(RESET) Enable debug build" - @echo " $(BOLD)$(YELLOW)BIN=path$(RESET) Custom binary output path" + @echo " $(BOLD)$(YELLOW)DEBUG=1$(RESET) Enable debug build" + @echo " $(BOLD)$(YELLOW)BIN=path$(RESET) Custom binary output path" @echo "" # Default target shows help diff --git a/actionSync.go b/actionSync.go index 75d998d..ac3c5ed 100644 --- a/actionSync.go +++ b/actionSync.go @@ -29,7 +29,6 @@ var ( const ( vaultpassKey = "vaultpass" domainNamespace = "domain" - buildHackAuthor = "override" ) // syncAction is a type representing a resources version synchronization action. @@ -60,9 +59,10 @@ type syncAction struct { } type hashStruct struct { - hash string - hashTime time.Time - author string + hash string + hashTime time.Time + author string + overridden bool // true = version was manually overridden, not sourced from git } // Execute the sync action to propagate resources' versions. @@ -192,16 +192,15 @@ func (s *syncAction) getResourcesMaps(buildInv *sync.Inventory) (map[string]*syn resourcesMap := make(map[string]*sync.OrderedMap[*sync.Resource]) packagePathMap := make(map[string]string) - plasmaCompose, err := compose.Lookup(os.DirFS(s.domainDir)) + lock, err := compose.LookupLock(os.DirFS(s.buildDir)) if err != nil { return nil, nil, err } var priorityOrder []string - for _, dep := range plasmaCompose.Dependencies { - pkg := dep.ToPackage(dep.Name) - packagePathMap[dep.Name] = filepath.Join(s.packagesDir, pkg.GetName(), pkg.GetTarget()) - priorityOrder = append(priorityOrder, dep.Name) + for _, entry := range lock.Packages { + packagePathMap[entry.Name] = filepath.Join(s.domainDir, entry.Path) + priorityOrder = append(priorityOrder, entry.Name) } packagePathMap[domainNamespace] = s.domainDir @@ -633,21 +632,14 @@ func (s *syncAction) updateResources(resourceVersionMap map[string]string, toPro } func composeVersion(oldVersion string, newVersion string) string { - var version string if len(strings.Split(newVersion, "-")) > 1 { - version = newVersion - } else { - split := strings.Split(oldVersion, "-") - if len(split) == 1 { - version = fmt.Sprintf("%s-%s", oldVersion, newVersion) - } else if len(split) > 1 { - version = fmt.Sprintf("%s-%s", split[0], newVersion) - } else { - version = newVersion - } + return newVersion } - - return version + split := strings.Split(oldVersion, "-") + if len(split) > 1 { + return fmt.Sprintf("%s-%s", split[0], newVersion) + } + return fmt.Sprintf("%s-%s", oldVersion, newVersion) } func (s *syncAction) getResourcesMapFrom(dir string) (*sync.OrderedMap[*sync.Resource], error) { diff --git a/actionSync.resources.go b/actionSync.resources.go index b2af5e0..6a284f0 100644 --- a/actionSync.resources.go +++ b/actionSync.resources.go @@ -41,6 +41,9 @@ func (s *syncAction) populateTimelineResources(resources map[string]*sync.Ordere ctx, cancel := context.WithCancel(context.Background()) defer cancel() + // @todo this goroutine-pool pattern (workChan + errorChan + wg + maxWorkers) is duplicated + // 4 times across actionSync.go, actionSync.resources.go, actionSync.variables.go and + // inventory.variable.go. Consider extracting a generic runWorkers[T] helper. errorChan := make(chan error, 1) maxWorkers := min(runtime.NumCPU(), len(packagePathMap)) workChan := make(chan map[string]any, len(packagePathMap)) @@ -236,7 +239,6 @@ func (s *syncAction) findResourcesChangeTime(ctx context.Context, namespaceResou var wg async.WaitGroup errorChan := make(chan error, 1) - //maxWorkers := 3 maxWorkers := runtime.NumCPU() resourcesChan := make(chan *sync.Resource, namespaceResources.Len()) @@ -250,13 +252,13 @@ func (s *syncAction) findResourcesChangeTime(ctx context.Context, namespaceResou if !ok { return } - if err = s.processResource(r, groups, commitsMap, repo, gitPath, mx); err != nil { + if errR := s.processResource(r, groups, commitsMap, repo, gitPath, mx); errR != nil { if p != nil { _, _ = p.Stop() } select { - case errorChan <- err: + case errorChan <- errR: default: } } @@ -311,9 +313,8 @@ func (s *syncAction) processResource(resource *sync.Resource, commitsGroups *syn } versionHash := &hashStruct{ - hash: buildHackAuthor, - hashTime: time.Now(), - author: buildHackAuthor, + hashTime: time.Now(), + overridden: true, } head, err := repo.Head() @@ -346,7 +347,6 @@ func (s *syncAction) processResource(resource *sync.Resource, commitsGroups *syn // If override is not allowed, return error. // In other case add new timeline item with overridden version. - overridden := false if currentVersion != headVersion { msg := fmt.Sprintf("Version of `%s` doesn't match HEAD commit", resource.GetName()) if !s.allowOverride { @@ -354,18 +354,15 @@ func (s *syncAction) processResource(resource *sync.Resource, commitsGroups *syn } s.Log().Warn(msg) - overridden = true } else { versionHash.hash = headCommit.Hash.String() versionHash.hashTime = headCommit.Author.When versionHash.author = headCommit.Author.Name + versionHash.overridden = false } - if !overridden { - // @todo rewrite to concurrent map ? - //mx.Lock() + if !versionHash.overridden { item, ok := commitsMap[currentVersion] - //mx.Unlock() if !ok { s.Log().Warn(fmt.Sprintf("Latest version of `%s` doesn't match any existing commit", resource.GetName())) } @@ -411,7 +408,7 @@ func (s *syncAction) processResource(resource *sync.Resource, commitsGroups *syn slog.Time("date", versionHash.hashTime), ) - if versionHash.author != repository.Author && versionHash.author != buildHackAuthor { + if !versionHash.overridden && versionHash.author != repository.Author { s.Log().Warn(fmt.Sprintf("Latest commit of %s is not a bump commit", resource.GetName())) } @@ -551,11 +548,11 @@ func (s *syncAction) processUnknownSection(commitsGroups *sync.OrderedMap[*Commi if errItem != nil { // How it's possible to not have meta file in commit before bump ? // @todo case looks impossible, maybe makes sense to panic here - if errors.Is(err, object.ErrFileNotFound) { + if errors.Is(errItem, object.ErrFileNotFound) { return nil, errRunBruteProcess } - return nil, fmt.Errorf("can't hash meta file from commit %s > %w", itemCommit.Hash.String(), err) + return nil, fmt.Errorf("can't hash meta file from commit %s > %w", itemCommit.Hash.String(), errItem) } // Hashes don't match, as expected @@ -623,11 +620,11 @@ func (s *syncAction) processBumpSection(group *CommitsGroup, resourceMetaPath, c if errItem != nil { // How it's possible to not have meta file in commit before bump ? // @todo case looks impossible, maybe makes sense to panic here - if errors.Is(err, object.ErrFileNotFound) { + if errors.Is(errItem, object.ErrFileNotFound) { return nil, errRunBruteProcess } - return nil, fmt.Errorf("can't hash meta file from commit %s - %w", itemCommit.Hash.String(), err) + return nil, fmt.Errorf("can't hash meta file from commit %s - %w", itemCommit.Hash.String(), errItem) } // Hashes don't match, as expected diff --git a/actionSync.variables.go b/actionSync.variables.go index a9ce2be..1d1389e 100644 --- a/actionSync.variables.go +++ b/actionSync.variables.go @@ -150,7 +150,7 @@ func (s *syncAction) findVariableUpdateTime(varsFile string, inv *sync.Inventory hashesMap[k].hash = fmt.Sprint(v.GetHash()) hashesMap[k].hashTime = time.Now() - hashesMap[k].author = buildHackAuthor + hashesMap[k].overridden = true } varsFileHash := "" @@ -193,6 +193,7 @@ func (s *syncAction) findVariableUpdateTime(varsFile string, inv *sync.Inventory hashesMap[k].hash = danglingCommit.Hash.String() hashesMap[k].hashTime = danglingCommit.Author.When hashesMap[k].author = danglingCommit.Author.Name + hashesMap[k].overridden = false } danglingCommit = nil @@ -203,6 +204,8 @@ func (s *syncAction) findVariableUpdateTime(varsFile string, inv *sync.Inventory s.Log().Debug(d) } if errIt != nil { + // @todo replace strings.Contains checks with sentinel errors or errors.As once + // ansible-vault-go and go-yaml expose typed errors (currently they don't). if strings.Contains(errIt.Error(), "did not find expected key") || strings.Contains(errIt.Error(), "did not find expected comment or line break") || strings.Contains(errIt.Error(), "could not find expected") { @@ -255,6 +258,7 @@ func (s *syncAction) findVariableUpdateTime(varsFile string, inv *sync.Inventory hashesMap[k].hash = c.Hash.String() hashesMap[k].hashTime = c.Author.When hashesMap[k].author = c.Author.Name + hashesMap[k].overridden = false } return nil @@ -269,6 +273,7 @@ func (s *syncAction) findVariableUpdateTime(varsFile string, inv *sync.Inventory hashesMap[k].hash = danglingCommit.Hash.String() hashesMap[k].hashTime = danglingCommit.Author.When hashesMap[k].author = danglingCommit.Author.Name + hashesMap[k].overridden = false } danglingCommit = nil @@ -287,7 +292,7 @@ func (s *syncAction) findVariableUpdateTime(varsFile string, inv *sync.Inventory slog.String("path", v.GetPath()), ) - if hm.author == buildHackAuthor { + if hm.overridden { msg := fmt.Sprintf("Value of `%s` doesn't match HEAD commit", n) if !s.allowOverride { return errors.New(msg) diff --git a/actionSync_resources_test.go b/actionSync_resources_test.go new file mode 100644 index 0000000..73c9219 --- /dev/null +++ b/actionSync_resources_test.go @@ -0,0 +1,243 @@ +package plasmactlbump + +import ( + "testing" + "time" + + "github.com/go-git/go-billy/v5/memfs" + "github.com/go-git/go-git/v5" + "github.com/go-git/go-git/v5/plumbing/object" + "github.com/go-git/go-git/v5/storage/memory" + + "github.com/skilld-labs/plasmactl-bump/v2/pkg/repository" +) + +// commitOpts defines a single commit to add to the test repo. +type commitOpts struct { + message string + author string + date time.Time +} + +// buildTestRepo creates an in-memory git repo with the given sequence of commits (oldest first). +// Returns the repo and the commit hashes in the same order. +func buildTestRepo(t *testing.T, commits []commitOpts) (*git.Repository, []string) { + t.Helper() + + r, err := git.Init(memory.NewStorage(), memfs.New()) + if err != nil { + t.Fatalf("git.Init: %v", err) + } + + wt, err := r.Worktree() + if err != nil { + t.Fatalf("Worktree: %v", err) + } + + var hashes []string + for i, c := range commits { + // Write a unique file so each commit has a non-empty tree diff. + f, errF := wt.Filesystem.Create("file" + string(rune('a'+i))) + if errF != nil { + t.Fatalf("create file: %v", errF) + } + _, _ = f.Write([]byte(c.message)) + _ = f.Close() + + _, _ = wt.Add(".") + + sig := &object.Signature{Name: c.author, Email: c.author + "@test", When: c.date} + hash, errC := wt.Commit(c.message, &git.CommitOptions{Author: sig, Committer: sig}) + if errC != nil { + t.Fatalf("commit: %v", errC) + } + hashes = append(hashes, hash.String()) + } + + return r, hashes +} + +func TestCollectResourcesCommits_NoBumps(t *testing.T) { + // No bump commits β†’ single "head" group containing all commits. + d := time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC) + r, hashes := buildTestRepo(t, []commitOpts{ + {message: "initial", author: "Developer", date: d}, + {message: "change A", author: "Developer", date: d.Add(time.Hour)}, + {message: "change B", author: "Developer", date: d.Add(2 * time.Hour)}, + }) + + groups, hashesMap, err := collectResourcesCommits(r, "") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if groups.Len() != 1 { + t.Fatalf("expected 1 group, got %d", groups.Len()) + } + + keys := groups.Keys() + g, _ := groups.Get(keys[0]) + if g.name != headGroupName { + t.Errorf("group name = %q, want %q", g.name, headGroupName) + } + // All 3 commits belong to the head group (HEAD + 2 earlier). + if len(g.items) != 3 { + t.Errorf("items count = %d, want 3", len(g.items)) + } + // The group is keyed by the HEAD commit's full hash. + headHash := hashes[2] + if keys[0] != headHash { + t.Errorf("group key = %q, want HEAD hash %s", keys[0], headHash[:13]) + } + // HEAD commit itself is assigned sectionName = headGroupName. + if hashesMap[headHash[:13]]["section"] != headGroupName { + t.Errorf("HEAD hash section = %q, want %q", hashesMap[headHash[:13]]["section"], headGroupName) + } + // Earlier commits are assigned to the HEAD full hash as the section key. + for _, h := range hashes[:2] { + short := h[:13] + if hashesMap[short]["section"] != headHash { + t.Errorf("hash %s section = %q, want HEAD hash %s", short, hashesMap[short]["section"], headHash[:13]) + } + } +} + +func TestCollectResourcesCommits_HeadIsBump(t *testing.T) { + // HEAD is a bump commit β†’ it starts its own section, no items yet in head. + d := time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC) + r, hashes := buildTestRepo(t, []commitOpts{ + {message: "initial", author: "Developer", date: d}, + {message: "change", author: "Developer", date: d.Add(time.Hour)}, + {message: "[bump]", author: repository.Author, date: d.Add(2 * time.Hour)}, + }) + + groups, hashesMap, err := collectResourcesCommits(r, "") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + // One group: the bump section containing the two developer commits. + if groups.Len() != 1 { + t.Fatalf("expected 1 group, got %d", groups.Len()) + } + + bumpHash := hashes[2] + g, ok := groups.Get(bumpHash) + if !ok { + t.Fatalf("expected group keyed by bump commit %s", bumpHash[:13]) + } + if len(g.items) != 2 { + t.Errorf("bump section items = %d, want 2 (the two developer commits)", len(g.items)) + } + // HEAD bump hash is assigned to its own section name. + if hashesMap[bumpHash[:13]]["section"] != bumpHash { + t.Errorf("bump hash section = %q, want %q", hashesMap[bumpHash[:13]]["section"], bumpHash) + } + // Developer commits are in the bump section. + for _, h := range hashes[:2] { + if hashesMap[h[:13]]["section"] != bumpHash { + t.Errorf("dev commit %s section = %q, want bump %s", h[:13], hashesMap[h[:13]]["section"], bumpHash[:13]) + } + } +} + +func TestCollectResourcesCommits_TwoBumps(t *testing.T) { + // Two bump commits β†’ two sections plus a head group. + // Timeline (oldestβ†’newest): + // initial (dev) β†’ change-1 (dev) β†’ bump-1 (Bumper) β†’ change-2 (dev) β†’ bump-2 (Bumper) β†’ change-3 (dev) + d := time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC) + r, hashes := buildTestRepo(t, []commitOpts{ + {message: "initial", author: "Developer", date: d}, + {message: "change-1", author: "Developer", date: d.Add(time.Hour)}, + {message: "[bump]", author: repository.Author, date: d.Add(2 * time.Hour)}, + {message: "change-2", author: "Developer", date: d.Add(3 * time.Hour)}, + {message: "[bump]", author: repository.Author, date: d.Add(4 * time.Hour)}, + {message: "change-3", author: "Developer", date: d.Add(5 * time.Hour)}, + }) + + groups, hashesMap, err := collectResourcesCommits(r, "") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + // 3 groups: head, bump-2, bump-1. + if groups.Len() != 3 { + t.Fatalf("expected 3 groups, got %d", groups.Len()) + } + + bump1Hash := hashes[2] + bump2Hash := hashes[4] + + // bump-1 section contains: initial + change-1. + g1, ok := groups.Get(bump1Hash) + if !ok { + t.Fatalf("bump-1 group not found") + } + if len(g1.items) != 2 { + t.Errorf("bump-1 items = %d, want 2", len(g1.items)) + } + + // bump-2 section contains: change-2. + g2, ok := groups.Get(bump2Hash) + if !ok { + t.Fatalf("bump-2 group not found") + } + if len(g2.items) != 1 { + t.Errorf("bump-2 items = %d, want 1", len(g2.items)) + } + + // Head section: keyed by HEAD hash (hashes[5]), contains change-3. + headKey := hashes[5] + g3, ok := groups.Get(headKey) + if !ok { + t.Fatalf("head group not found") + } + if g3.name != headGroupName { + t.Errorf("head group name = %q, want %q", g3.name, headGroupName) + } + if len(g3.items) != 1 { + t.Errorf("head items = %d, want 1", len(g3.items)) + } + + // Verify section assignments in hashesMap. + for _, h := range hashes[:2] { + if hashesMap[h[:13]]["section"] != bump1Hash { + t.Errorf("commit %s should be in bump-1 section", h[:13]) + } + } + if hashesMap[hashes[3][:13]]["section"] != bump2Hash { + t.Errorf("change-2 should be in bump-2 section") + } + // HEAD commit (change-3) gets sectionName = headGroupName in hashesMap. + if hashesMap[hashes[5][:13]]["section"] != headGroupName { + t.Errorf("change-3 should be in head section") + } +} + +func TestCollectResourcesCommits_BeforeDate(t *testing.T) { + // beforeDate filters out commits older than the cutoff. + d := time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC) + r, hashes := buildTestRepo(t, []commitOpts{ + {message: "old commit", author: "Developer", date: d}, + {message: "new commit", author: "Developer", date: d.Add(48 * time.Hour)}, + }) + + // Cut off before d+48h β†’ only "new commit" visible. + groups, hashesMap, err := collectResourcesCommits(r, "2000-01-02") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if groups.Len() != 1 { + t.Fatalf("expected 1 group, got %d", groups.Len()) + } + + // Old commit should not appear in hashesMap. + if _, ok := hashesMap[hashes[0][:13]]; ok { + t.Error("old commit should be filtered out by beforeDate") + } + // New commit should be present. + if _, ok := hashesMap[hashes[1][:13]]; !ok { + t.Error("new commit should be present") + } +} diff --git a/actionSync_test.go b/actionSync_test.go new file mode 100644 index 0000000..3e4e5b6 --- /dev/null +++ b/actionSync_test.go @@ -0,0 +1,48 @@ +package plasmactlbump + +import ( + "testing" +) + +func TestComposeVersion(t *testing.T) { + tests := []struct { + name string + oldVersion string + newVersion string + want string + }{ + { + name: "old without dash: appends new", + oldVersion: "ver1", + newVersion: "abc1234567890", + want: "ver1-abc1234567890", + }, + { + name: "old with dash: replaces propagated part", + oldVersion: "ver1-old1234567890", + newVersion: "new1234567890", + want: "ver1-new1234567890", + }, + { + name: "new already contains dash: returned as is", + oldVersion: "ver1", + newVersion: "ver2-abc1234567890", + want: "ver2-abc1234567890", + }, + { + name: "new with dash overrides old with dash", + oldVersion: "ver1-old1234567890", + newVersion: "ver2-new1234567890", + want: "ver2-new1234567890", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := composeVersion(tt.oldVersion, tt.newVersion) + if got != tt.want { + t.Errorf("composeVersion(%q, %q) = %q, want %q", tt.oldVersion, tt.newVersion, got, tt.want) + } + }) + } +} diff --git a/go.mod b/go.mod index c9af06a..9361ce4 100644 --- a/go.mod +++ b/go.mod @@ -6,15 +6,18 @@ require ( atomicgo.dev/cursor v0.2.0 atomicgo.dev/schedule v0.1.0 github.com/cespare/xxhash/v2 v2.3.0 - github.com/go-git/go-git/v5 v5.16.3 + github.com/go-git/go-git/v5 v5.16.5 github.com/launchrctl/compose v0.16.0 - github.com/launchrctl/keyring v0.7.0 + github.com/launchrctl/keyring v0.9.1 github.com/launchrctl/launchr v0.22.0 github.com/pterm/pterm v0.12.82 + github.com/rogpeppe/go-internal v1.14.1 github.com/sosedoff/ansible-vault-go v0.2.0 github.com/stevenle/topsort v0.2.0 ) +replace github.com/launchrctl/compose => ../compose + require ( atomicgo.dev/keyboard v0.2.9 // indirect dario.cat/mergo v1.0.2 // indirect @@ -54,7 +57,7 @@ require ( github.com/fsnotify/fsnotify v1.6.0 // indirect github.com/fxamacker/cbor/v2 v2.9.0 // indirect github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect - github.com/go-git/go-billy/v5 v5.6.2 // indirect + github.com/go-git/go-billy/v5 v5.6.2 github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-openapi/jsonpointer v0.22.1 // indirect @@ -135,14 +138,15 @@ require ( go.uber.org/mock v0.6.0 // indirect go.yaml.in/yaml/v2 v2.4.3 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect - golang.org/x/crypto v0.43.0 // indirect + golang.org/x/crypto v0.45.0 // indirect golang.org/x/mod v0.29.0 // indirect - golang.org/x/net v0.46.0 // indirect + golang.org/x/net v0.47.0 // indirect golang.org/x/oauth2 v0.32.0 // indirect - golang.org/x/sys v0.37.0 // indirect - golang.org/x/term v0.36.0 // indirect - golang.org/x/text v0.30.0 // indirect + golang.org/x/sys v0.38.0 // indirect + golang.org/x/term v0.37.0 // indirect + golang.org/x/text v0.31.0 // indirect golang.org/x/time v0.14.0 // indirect + golang.org/x/tools v0.38.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20241007155032-5fefd90f89a9 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20241007155032-5fefd90f89a9 // indirect google.golang.org/protobuf v1.36.10 // indirect diff --git a/go.sum b/go.sum index e3a7432..020af0f 100644 --- a/go.sum +++ b/go.sum @@ -178,8 +178,8 @@ github.com/go-git/go-billy/v5 v5.6.2 h1:6Q86EsPXMa7c3YZ3aLAQsMA0VlWmy43r6FHqa/UN github.com/go-git/go-billy/v5 v5.6.2/go.mod h1:rcFC2rAsp/erv7CMz9GczHcuD0D32fWzH+MJAU+jaUU= github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4= github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII= -github.com/go-git/go-git/v5 v5.16.3 h1:Z8BtvxZ09bYm/yYNgPKCzgWtaRqDTgIKRgIRHBfU6Z8= -github.com/go-git/go-git/v5 v5.16.3/go.mod h1:4Ge4alE/5gPs30F2H1esi2gPd69R0C39lolkucHBOp8= +github.com/go-git/go-git/v5 v5.16.5 h1:mdkuqblwr57kVfXri5TTH+nMFLNUxIj9Z7F5ykFbw5s= +github.com/go-git/go-git/v5 v5.16.5/go.mod h1:QOMLpNf1qxuSY4StA/ArOdfFR2TrKEjJiye2kel2m+M= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= @@ -358,10 +358,8 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/launchrctl/compose v0.16.0 h1:FQT2A0ODyrKpR1afEg/SCwNtLG1Eo7bFfegeBxAWTv4= -github.com/launchrctl/compose v0.16.0/go.mod h1:9qYgSPfUYh1JGLAUCEd0Lk4lEv2NRQNkHXuAigH7Osk= -github.com/launchrctl/keyring v0.7.0 h1:IrmhjnguZrcNZHg2vSVajVdWvmxYh6tnpsu/Q4Rw8ck= -github.com/launchrctl/keyring v0.7.0/go.mod h1:HE8OurqAYUt3x0ArDEDa6w9TAL4mbUwxqp/PJr/EPJE= +github.com/launchrctl/keyring v0.9.1 h1:XaiP7vCavaitvhx+Pbq6WuUjCoSLj0Ba3CTWeYOD5uA= +github.com/launchrctl/keyring v0.9.1/go.mod h1:HE8OurqAYUt3x0ArDEDa6w9TAL4mbUwxqp/PJr/EPJE= github.com/launchrctl/launchr v0.22.0 h1:JJJgvaSBjaxwX7vfyZ+ujwt7kIXqp/V0xy5k3JgtMyU= github.com/launchrctl/launchr v0.22.0/go.mod h1:cLETGKQKp6WBg1uPQ2WmrHTjPcWTfseUI0R5NyODxUs= github.com/lithammer/fuzzysearch v1.1.8 h1:/HIuJnjHuXS8bKaiTMeeDlW2/AyIWk2brx1V8LFgLN4= @@ -594,8 +592,8 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04= -golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0= +golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= +golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8= golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= @@ -631,8 +629,8 @@ golang.org/x/net v0.0.0-20210410081132-afb366fc7cd1/go.mod h1:9tjilg8BloeKEkVJvy golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4= -golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210= +golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= +golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -692,15 +690,15 @@ golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= -golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= +golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= -golang.org/x/term v0.36.0 h1:zMPR+aF8gfksFprF/Nc/rd1wRS1EI6nDBGyWAvDzx2Q= -golang.org/x/term v0.36.0/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss= +golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU= +golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20181227161524-e6919f6577db/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= @@ -710,8 +708,8 @@ golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k= -golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM= +golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= +golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= @@ -728,8 +726,8 @@ golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4f golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= -golang.org/x/tools v0.37.0 h1:DVSRzp7FwePZW356yEAChSdNcQo6Nsp+fex1SUW09lE= -golang.org/x/tools v0.37.0/go.mod h1:MBN5QPQtLMHVdvsbtarmTNukZDdgwdwlO5qGacAzF0w= +golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ= +golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/pkg/sync/inventory.go b/pkg/sync/inventory.go index d2f5ff7..4359b2a 100644 --- a/pkg/sync/inventory.go +++ b/pkg/sync/inventory.go @@ -275,20 +275,3 @@ func (i *Inventory) lookupDependenciesRecursively(resourceName string, resources } } -// GetChangedResources returns an OrderedResourceMap containing the resources that have been modified, based on the provided list of modified files. -// It iterates over the modified files, builds a resource from each file path, and adds it to the result map if it is not already present. -//func (i *Inventory) GetChangedResources(files []string) *OrderedMap[*Resource] { -// resources := NewOrderedMap[*Resource]() -// for _, path := range files { -// resource := BuildResourceFromPath(path, i.sourceDir) -// if resource == nil { -// continue -// } -// if _, ok := resources.Get(resource.GetName()); ok { -// continue -// } -// resources.Set(resource.GetName(), resource) -// } -// -// return resources -//} diff --git a/pkg/sync/inventory.variable.go b/pkg/sync/inventory.variable.go index b2fa263..8fd76a7 100644 --- a/pkg/sync/inventory.variable.go +++ b/pkg/sync/inventory.variable.go @@ -163,7 +163,7 @@ func (i *Inventory) CalculateVariablesUsage(vaultpass string) error { // Iterate all files to find {{ and/or }}, get these lines // iterate potential lines with vars usage and check each variable in it. - variableResourcesDependencyMap, err := i.buildVariableResourcesDependencies(keys, false) + variableResourcesDependencyMap, err := i.buildVariableResourcesDependencies(keys) if err != nil { return err } @@ -327,15 +327,6 @@ func findDependencies(group, value string, currentGroupKeys, platformKeys map[st return dependencies } -// Helper function to convert map[string]bool to []string -//func getMapKeys(m map[string]bool) []string { -// keys := make([]string, 0, len(m)) -// for key := range m { -// keys = append(keys, key) -// } -// return keys -//} - func (i *Inventory) processGroup(ctx context.Context, vaultPass, group string, files []string, groupKeys map[string]map[string]bool, groupVars map[string]map[string]string, mx *sync.Mutex) error { mx.Lock() if _, exists := groupKeys[group]; !exists { @@ -450,13 +441,6 @@ func (i *Inventory) extractKeysAndVars(data interface{}, group string, groupKeys } mx.Unlock() - // Recurse into list items - //for _, item := range v { - // if test { - // panic(currentKey) - // } - // i.extractKeysAndVars(item, group, groupKeys, groupVars, currentKey, mx) - //} case string: if strings.Contains(v, "{{") || strings.Contains(v, "}}") { mx.Lock() @@ -469,7 +453,7 @@ func (i *Inventory) extractKeysAndVars(data interface{}, group string, groupKeys } } -func (i *Inventory) buildVariableResourcesDependencies(groupKeys map[string]map[string]bool, filesOnly bool) (map[string]map[string][]string, error) { +func (i *Inventory) buildVariableResourcesDependencies(groupKeys map[string]map[string]bool) (map[string]map[string][]string, error) { groupFiles, err := i.fc.FindResourcesFiles("") if err != nil { return nil, err @@ -501,10 +485,6 @@ func (i *Inventory) buildVariableResourcesDependencies(groupKeys map[string]map[ } } - if filesOnly { - return reverseDependencyMap, nil - } - varToResourcesDependencyMap := make(map[string]map[string][]string) for v, pl := range reverseDependencyMap { for p, files := range pl { @@ -638,6 +618,10 @@ func extractLinesWithVariables(filePath string) ([]string, error) { for scanner.Scan() { line := scanner.Text() + // @todo operator precedence bug: `}}` without `{{` always passes the filter due to missing + // parentheses around the OR condition. Fix: add parentheses around (Contains("{{") || Contains("}}")). + // Before fixing, verify there are no real cases where {{ and }} appear on separate lines + // in Ansible templates (e.g. multiline Jinja2 expressions), as the fix would break those. if len(line) > 0 && !strings.HasPrefix(line, "#") && strings.Contains(line, "{{") || strings.Contains(line, "}}") { linesWithVariables = append(linesWithVariables, line) } diff --git a/pkg/sync/inventory_resource_test.go b/pkg/sync/inventory_resource_test.go new file mode 100644 index 0000000..cee1781 --- /dev/null +++ b/pkg/sync/inventory_resource_test.go @@ -0,0 +1,354 @@ +package sync + +import ( + "os" + "path/filepath" + "testing" +) + +const testMRN = "interaction__skills__skill-e" + +func TestProcessResourcePath(t *testing.T) { + tests := []struct { + name string + path string + wantPlatform string + wantKind string + wantRole string + wantErr bool + }{ + { + name: "valid path", + path: "interaction/skills/roles/skill-e/tasks/main.yaml", + wantPlatform: "interaction", + wantKind: "skills", + wantRole: "skill-e", + }, + { + name: "valid meta path", + path: "platform/applications/roles/app-x/meta/plasma.yaml", + wantPlatform: "platform", + wantKind: "applications", + wantRole: "app-x", + }, + { + name: "too short path", + path: "interaction/skills/roles", + wantErr: true, + }, + { + name: "empty string", + path: "", + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + platform, kind, role, err := ProcessResourcePath(tt.path) + if tt.wantErr { + if err == nil { + t.Fatalf("expected error, got nil") + } + return + } + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if platform != tt.wantPlatform { + t.Errorf("platform = %q, want %q", platform, tt.wantPlatform) + } + if kind != tt.wantKind { + t.Errorf("kind = %q, want %q", kind, tt.wantKind) + } + if role != tt.wantRole { + t.Errorf("role = %q, want %q", role, tt.wantRole) + } + }) + } +} + +func TestConvertMRNtoPath(t *testing.T) { + tests := []struct { + name string + mrn string + want string + wantErr bool + }{ + { + name: "valid MRN", + mrn: testMRN, + want: "interaction/skills/roles/skill-e", + }, + { + name: "too few parts", + mrn: "interaction__skills", + wantErr: true, + }, + { + name: "too many parts", + mrn: "interaction__skills__skill-e__extra", + wantErr: true, + }, + { + name: "empty string", + mrn: "", + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := ConvertMRNtoPath(tt.mrn) + if tt.wantErr { + if err == nil { + t.Fatalf("expected error, got nil") + } + return + } + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if got != tt.want { + t.Errorf("got %q, want %q", got, tt.want) + } + }) + } +} + +func TestGetMetaVersion(t *testing.T) { + tests := []struct { + name string + meta map[string]any + want string + }{ + { + name: "normal version", + meta: map[string]any{ + "plasma": map[string]any{"version": "abc1234567890"}, + }, + want: "abc1234567890", + }, + { + name: "propagated version", + meta: map[string]any{ + "plasma": map[string]any{"version": "ver1-abc1234567890"}, + }, + want: "ver1-abc1234567890", + }, + { + name: "version is nil", + meta: map[string]any{ + "plasma": map[string]any{"version": nil}, + }, + want: "", + }, + { + name: "plasma key missing", + meta: map[string]any{ + "other": "value", + }, + want: "", + }, + { + name: "empty map", + meta: map[string]any{}, + want: "", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := GetMetaVersion(tt.meta) + if got != tt.want { + t.Errorf("got %q, want %q", got, tt.want) + } + }) + } +} + +func writeMetaFile(t *testing.T, dir, mrn, content string) { + t.Helper() + parts := splitMRN(mrn) + metaDir := filepath.Join(dir, parts[0], parts[1], "roles", parts[2], "meta") + if err := os.MkdirAll(metaDir, 0o750); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(metaDir, "plasma.yaml"), []byte(content), 0o600); err != nil { + t.Fatal(err) + } +} + +func splitMRN(mrn string) []string { + var parts []string + cur := "" + for i := 0; i < len(mrn); i++ { + if i+1 < len(mrn) && mrn[i] == '_' && mrn[i+1] == '_' { + parts = append(parts, cur) + cur = "" + i++ + } else { + cur += string(mrn[i]) + } + } + parts = append(parts, cur) + return parts +} + +func TestGetBaseVersion(t *testing.T) { + tests := []struct { + name string + yamlContent string + wantBase string + wantFull string + wantDebugLen int + }{ + { + name: "plain version (no dash)", + yamlContent: "plasma:\n version: abc1234567890\n", + wantBase: "abc1234567890", + wantFull: "abc1234567890", + }, + { + name: "propagated version (one dash)", + yamlContent: "plasma:\n version: ver1-abc1234567890\n", + wantBase: "ver1", + wantFull: "ver1-abc1234567890", + }, + { + name: "malformed version (two dashes)", + yamlContent: "plasma:\n version: ver1-hash1-hash2\n", + wantBase: "ver1", + wantFull: "ver1-hash1-hash2", + wantDebugLen: 1, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + dir := t.TempDir() + mrn := testMRN + writeMetaFile(t, dir, mrn, tt.yamlContent) + + r := NewResource(mrn, dir) + base, full, debug, err := r.GetBaseVersion() + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if base != tt.wantBase { + t.Errorf("base = %q, want %q", base, tt.wantBase) + } + if full != tt.wantFull { + t.Errorf("full = %q, want %q", full, tt.wantFull) + } + if len(debug) != tt.wantDebugLen { + t.Errorf("debug len = %d, want %d: %v", len(debug), tt.wantDebugLen, debug) + } + }) + } +} + +func TestIsUpdatableKind(t *testing.T) { + validKinds := []string{ + "applications", "services", "softwares", "executors", + "flows", "skills", "functions", "libraries", "entities", + } + for _, kind := range validKinds { + if !IsUpdatableKind(kind) { + t.Errorf("expected %q to be updatable", kind) + } + } + + invalidKinds := []string{"group_vars", "roles", "builders", "", "unknown"} + for _, kind := range invalidKinds { + if IsUpdatableKind(kind) { + t.Errorf("expected %q to NOT be updatable", kind) + } + } +} + +func TestOrderedMap(t *testing.T) { + t.Run("Set and Get", func(t *testing.T) { + m := NewOrderedMap[string]() + m.Set("a", "1") + m.Set("b", "2") + m.Set("c", "3") + + v, ok := m.Get("b") + if !ok || v != "2" { + t.Errorf("Get(b) = %q, %v; want %q, true", v, ok, "2") + } + _, ok = m.Get("missing") + if ok { + t.Error("Get(missing) should return false") + } + }) + + t.Run("preserves insertion order", func(t *testing.T) { + m := NewOrderedMap[int]() + keys := []string{"c", "a", "b"} + for i, k := range keys { + m.Set(k, i) + } + if got := m.Keys(); len(got) != 3 || got[0] != "c" || got[1] != "a" || got[2] != "b" { + t.Errorf("Keys() = %v, want [c a b]", got) + } + }) + + t.Run("Set existing key does not duplicate", func(t *testing.T) { + m := NewOrderedMap[string]() + m.Set("a", "1") + m.Set("a", "2") + if m.Len() != 1 { + t.Errorf("Len() = %d, want 1", m.Len()) + } + v, _ := m.Get("a") + if v != "2" { + t.Errorf("Get(a) = %q, want %q", v, "2") + } + }) + + t.Run("Unset removes key", func(t *testing.T) { + m := NewOrderedMap[string]() + m.Set("a", "1") + m.Set("b", "2") + m.Unset("a") + if m.Len() != 1 { + t.Errorf("Len() = %d, want 1", m.Len()) + } + if _, ok := m.Get("a"); ok { + t.Error("Get(a) should return false after Unset") + } + }) + + t.Run("Unset non-existing key is no-op", func(t *testing.T) { + m := NewOrderedMap[string]() + m.Set("a", "1") + m.Unset("missing") // must not panic + if m.Len() != 1 { + t.Errorf("Len() = %d, want 1", m.Len()) + } + }) + + t.Run("OrderBy reorders keys", func(t *testing.T) { + m := NewOrderedMap[int]() + m.Set("c", 3) + m.Set("a", 1) + m.Set("b", 2) + m.OrderBy([]string{"a", "b", "c"}) + if got := m.Keys(); len(got) != 3 || got[0] != "a" || got[1] != "b" || got[2] != "c" { + t.Errorf("Keys() after OrderBy = %v, want [a b c]", got) + } + }) + + t.Run("SortKeysAlphabetically", func(t *testing.T) { + m := NewOrderedMap[int]() + m.Set("z", 3) + m.Set("a", 1) + m.Set("m", 2) + m.SortKeysAlphabetically() + if got := m.Keys(); got[0] != "a" || got[1] != "m" || got[2] != "z" { + t.Errorf("Keys() after SortKeysAlphabetically = %v, want [a m z]", got) + } + }) +} diff --git a/pkg/sync/inventory_variable_test.go b/pkg/sync/inventory_variable_test.go new file mode 100644 index 0000000..aaac559 --- /dev/null +++ b/pkg/sync/inventory_variable_test.go @@ -0,0 +1,186 @@ +package sync + +import ( + "os" + "path/filepath" + "testing" + + "github.com/launchrctl/launchr" +) + +// mkfile creates file with content, including parent dirs. +func mkfile(t *testing.T, root, rel, content string) { + t.Helper() + full := filepath.Join(root, rel) + if err := os.MkdirAll(filepath.Dir(full), 0o750); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(full, []byte(content), 0o600); err != nil { + t.Fatal(err) + } +} + +// newTestInventory builds an Inventory from a temp dir with CalculateVariablesUsage already called. +func newTestInventory(t *testing.T, dir, vaultPass string) *Inventory { + t.Helper() + inv, err := NewInventory(dir, launchr.Log()) + if err != nil { + t.Fatalf("NewInventory: %v", err) + } + if err = inv.CalculateVariablesUsage(vaultPass); err != nil { + t.Fatalf("CalculateVariablesUsage: %v", err) + } + return inv +} + +func TestCalculateVariablesUsage_VarUsedInTemplate(t *testing.T) { + dir := t.TempDir() + + // plain var + mkfile(t, dir, "interaction/group_vars/all/vars.yaml", "plain_var:\n value: hello\n") + // resource meta (needed for inventory init) + mkfile(t, dir, "interaction/skills/roles/skill-a/meta/plasma.yaml", "plasma:\n version: ver1\n") + // template referencing plain_var + mkfile(t, dir, "interaction/skills/roles/skill-a/templates/config.j2", "value: {{ plain_var.value }}\n") + + inv := newTestInventory(t, dir, "") + + if !inv.IsUsedVariable(false, "plain_var", "interaction") { + t.Fatal("plain_var should be detected as used by skill-a template") + } + + resources := inv.GetVariableResources("plain_var", "interaction") + if len(resources) != 1 || resources[0] != "interaction__skills__skill-a" { + t.Errorf("GetVariableResources = %v, want [interaction__skills__skill-a]", resources) + } +} + +func TestCalculateVariablesUsage_VarUsedInConfiguration(t *testing.T) { + dir := t.TempDir() + + mkfile(t, dir, "interaction/group_vars/all/vars.yaml", "cfg_var:\n value: hello\n") + mkfile(t, dir, "interaction/skills/roles/skill-b/meta/plasma.yaml", "plasma:\n version: ver1\n") + // configuration.yaml referencing cfg_var + mkfile(t, dir, "interaction/skills/roles/skill-b/tasks/configuration.yaml", "- name: set\n set_fact:\n x: \"{{ cfg_var.value }}\"\n") + + inv := newTestInventory(t, dir, "") + + if !inv.IsUsedVariable(false, "cfg_var", "interaction") { + t.Fatal("cfg_var should be detected via tasks/configuration.yaml") + } + resources := inv.GetVariableResources("cfg_var", "interaction") + if len(resources) != 1 || resources[0] != "interaction__skills__skill-b" { + t.Errorf("GetVariableResources = %v, want [interaction__skills__skill-b]", resources) + } +} + +func TestCalculateVariablesUsage_VarNotUsed(t *testing.T) { + dir := t.TempDir() + + mkfile(t, dir, "interaction/group_vars/all/vars.yaml", "unused_var:\n value: hello\n") + mkfile(t, dir, "interaction/skills/roles/skill-a/meta/plasma.yaml", "plasma:\n version: ver1\n") + // template does NOT reference unused_var + mkfile(t, dir, "interaction/skills/roles/skill-a/templates/config.j2", "value: static\n") + + inv := newTestInventory(t, dir, "") + + if inv.IsUsedVariable(false, "unused_var", "interaction") { + t.Fatal("unused_var should NOT be detected as used") + } + resources := inv.GetVariableResources("unused_var", "interaction") + if len(resources) != 0 { + t.Errorf("expected no resources, got %v", resources) + } +} + +func TestCalculateVariablesUsage_VarToVarDependency(t *testing.T) { + dir := t.TempDir() + + // Real-world pattern: vars reference other vars as "{{ other_var }}" (no dot suffix). + // e.g. account_api_password: "{{ account_admin_password }}" + // Note: "{{ base_var.field }}" is NOT detected β€” findDependencies matches " key " with + // spaces on both sides, so a dot following the name breaks detection. + mkfile(t, dir, "interaction/group_vars/all/vars.yaml", + "base_var: secret\nderived_var: \"{{ base_var }}\"\n") + mkfile(t, dir, "interaction/skills/roles/skill-a/meta/plasma.yaml", "plasma:\n version: ver1\n") + mkfile(t, dir, "interaction/skills/roles/skill-a/templates/config.j2", "x: {{ derived_var.value }}\n") + + inv := newTestInventory(t, dir, "") + + // derived_var directly used in template + if !inv.IsUsedVariable(false, "derived_var", "interaction") { + t.Fatal("derived_var should be used") + } + // base_var used transitively via derived_var + if !inv.IsUsedVariable(false, "base_var", "interaction") { + t.Fatal("base_var should be used transitively through derived_var") + } +} + +func TestCalculateVariablesUsage_MultipleResourcesDependOnSameVar(t *testing.T) { + dir := t.TempDir() + + mkfile(t, dir, "interaction/group_vars/all/vars.yaml", "shared_var:\n value: x\n") + mkfile(t, dir, "interaction/skills/roles/skill-a/meta/plasma.yaml", "plasma:\n version: ver1\n") + mkfile(t, dir, "interaction/skills/roles/skill-a/templates/cfg.j2", "v: {{ shared_var.value }}\n") + mkfile(t, dir, "interaction/skills/roles/skill-b/meta/plasma.yaml", "plasma:\n version: ver1\n") + mkfile(t, dir, "interaction/skills/roles/skill-b/templates/cfg.j2", "v: {{ shared_var.value }}\n") + + inv := newTestInventory(t, dir, "") + + resources := inv.GetVariableResources("shared_var", "interaction") + if len(resources) != 2 { + t.Errorf("expected 2 resources, got %v", resources) + } +} + +func TestCalculateVariablesUsage_VaultVar(t *testing.T) { + dir := t.TempDir() + + // Pre-generated ansible-vault content (password: "test"), decrypts to: + // vault_secret_var: + // value: secret + mkfile(t, dir, "interaction/group_vars/all/vault.yaml", + "$ANSIBLE_VAULT;1.1;AES256\n"+ + "39333862393031373163646433343062383363373233323839613561343163666164646334363331\n"+ + "6666616163383131343661616463643138393636393465630a333835653363393963633030613030\n"+ + "66313238353431386237303237623432313264376163633964373063623338363264633730353864\n"+ + "3537323263643562380a373364643434613933313239326436326134303730623963656362653263\n"+ + "61643238303033393733303865306463363032663330333366303265333039653661386664643039\n"+ + "3539336461383032316134663038326262313038393435626663\n") + mkfile(t, dir, "interaction/skills/roles/skill-a/meta/plasma.yaml", "plasma:\n version: ver1\n") + mkfile(t, dir, "interaction/skills/roles/skill-a/templates/secret.j2", "s: {{ vault_secret_var.value }}\n") + + inv := newTestInventory(t, dir, "test") + + if !inv.IsUsedVariable(false, "vault_secret_var", "interaction") { + t.Fatal("vault_secret_var should be detected as used") + } + resources := inv.GetVariableResources("vault_secret_var", "interaction") + if len(resources) != 1 || resources[0] != "interaction__skills__skill-a" { + t.Errorf("GetVariableResources = %v, want [interaction__skills__skill-a]", resources) + } +} + +func TestCalculateVariablesUsage_VaultWrongPassword(t *testing.T) { + dir := t.TempDir() + + mkfile(t, dir, "interaction/group_vars/all/vault.yaml", + "$ANSIBLE_VAULT;1.1;AES256\n"+ + "39333862393031373163646433343062383363373233323839613561343163666164646334363331\n"+ + "6666616163383131343661616463643138393636393465630a333835653363393963633030613030\n"+ + "66313238353431386237303237623432313264376163633964373063623338363264633730353864\n"+ + "3537323263643562380a373364643434613933313239326436326134303730623963656362653263\n"+ + "61643238303033393733303865306463363032663330333366303265333039653661386664643039\n"+ + "3539336461383032316134663038326262313038393435626663\n") + + _, err := NewInventory(dir, launchr.Log()) + if err != nil { + t.Fatalf("NewInventory: %v", err) + } + inv, _ := NewInventory(dir, launchr.Log()) + err = inv.CalculateVariablesUsage("wrongpassword") + if err == nil { + t.Fatal("expected error with wrong vault password, got nil") + } +} diff --git a/pkg/sync/timeline_test.go b/pkg/sync/timeline_test.go new file mode 100644 index 0000000..30e94c0 --- /dev/null +++ b/pkg/sync/timeline_test.go @@ -0,0 +1,126 @@ +package sync + +import ( + "testing" + "time" +) + +var ( + t1 = time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC) + t2 = time.Date(2000, 1, 2, 0, 0, 0, 0, time.UTC) + t3 = time.Date(2000, 1, 3, 0, 0, 0, 0, time.UTC) +) + +func newResItem(version string, date time.Time) *TimelineResourcesItem { + return NewTimelineResourcesItem(version, "commit-"+version, date, nil) +} + +func newVarItem(version string, date time.Time) *TimelineVariablesItem { + return NewTimelineVariablesItem(version, "commit-"+version, date, nil) +} + +func TestAddToTimeline(t *testing.T) { + t.Run("new item is appended", func(t *testing.T) { + list := CreateTimeline() + item := newResItem("v1", t1) + list = AddToTimeline(list, item) + if len(list) != 1 { + t.Fatalf("len = %d, want 1", len(list)) + } + }) + + t.Run("same version and date merges resources item", func(t *testing.T) { + list := CreateTimeline() + r1 := NewResource("interaction__skills__skill-a", "") + r2 := NewResource("interaction__skills__skill-b", "") + + item1 := newResItem("v1", t1) + item1.AddResource(r1) + list = AddToTimeline(list, item1) + + item2 := newResItem("v1", t1) + item2.AddResource(r2) + list = AddToTimeline(list, item2) + + if len(list) != 1 { + t.Fatalf("expected merge, len = %d", len(list)) + } + res := list[0].(*TimelineResourcesItem).GetResources() + if res.Len() != 2 { + t.Errorf("merged resources count = %d, want 2", res.Len()) + } + }) + + t.Run("different version creates new item", func(t *testing.T) { + list := CreateTimeline() + list = AddToTimeline(list, newResItem("v1", t1)) + list = AddToTimeline(list, newResItem("v2", t1)) + if len(list) != 2 { + t.Fatalf("len = %d, want 2", len(list)) + } + }) + + t.Run("different date creates new item", func(t *testing.T) { + list := CreateTimeline() + list = AddToTimeline(list, newResItem("v1", t1)) + list = AddToTimeline(list, newResItem("v1", t2)) + if len(list) != 2 { + t.Fatalf("len = %d, want 2", len(list)) + } + }) + + t.Run("resources and variables with same version/date do not merge", func(t *testing.T) { + list := CreateTimeline() + list = AddToTimeline(list, newResItem("v1", t1)) + list = AddToTimeline(list, newVarItem("v1", t1)) + if len(list) != 2 { + t.Fatalf("different types must not merge, len = %d", len(list)) + } + }) +} + +func TestSortTimeline(t *testing.T) { + tests := []struct { + name string + order string + wantOrder []string + }{ + {name: "ascending", order: SortAsc, wantOrder: []string{"v1", "v2", "v3"}}, + {name: "descending", order: SortDesc, wantOrder: []string{"v3", "v2", "v1"}}, + } + + for _, tt := range tests { + t.Run(tt.name+" by date", func(t *testing.T) { + list := CreateTimeline() + list = AddToTimeline(list, newResItem("v3", t3)) + list = AddToTimeline(list, newResItem("v1", t1)) + list = AddToTimeline(list, newResItem("v2", t2)) + SortTimeline(list, tt.order) + for i, want := range tt.wantOrder { + if list[i].GetVersion() != want { + t.Errorf("[%d] = %q, want %q", i, list[i].GetVersion(), want) + } + } + }) + } + + t.Run("equal dates asc: Variables before Resources", func(t *testing.T) { + list := CreateTimeline() + list = AddToTimeline(list, newResItem("res", t1)) + list = AddToTimeline(list, newVarItem("var", t1)) + SortTimeline(list, SortAsc) + if _, ok := list[0].(*TimelineVariablesItem); !ok { + t.Error("expected Variables first in asc sort with equal dates") + } + }) + + t.Run("equal dates desc: Resources before Variables", func(t *testing.T) { + list := CreateTimeline() + list = AddToTimeline(list, newVarItem("var", t1)) + list = AddToTimeline(list, newResItem("res", t1)) + SortTimeline(list, SortDesc) + if _, ok := list[0].(*TimelineResourcesItem); !ok { + t.Error("expected Resources first in desc sort with equal dates") + } + }) +} diff --git a/plugin.go b/plugin.go index 4bf27ad..3a01b67 100644 --- a/plugin.go +++ b/plugin.go @@ -7,6 +7,7 @@ import ( "fmt" "os" + _ "github.com/launchrctl/compose" // manually add compose plugin, good for tests "github.com/launchrctl/keyring" "github.com/launchrctl/launchr" "github.com/launchrctl/launchr/pkg/action" diff --git a/test/README.md b/test/README.md new file mode 100644 index 0000000..9aea2e1 --- /dev/null +++ b/test/README.md @@ -0,0 +1,208 @@ +# Integration Tests + +This directory contains integration tests, test scripts, and test helpers designed to provide comprehensive end-to-end +testing capabilities for the application. + +## Overview + +Our integration testing framework extends the standard [testscript](https://github.com/rogpeppe/go-internal) +functionality with custom commands and enhancements tailored to our specific testing needs. These tests validate the +complete application workflow, from binary execution to complex text processing scenarios. + +## Custom Testscript Commands + +We have extended the standard testscript command set with several custom commands to enhance testing capabilities: + +### Text Processing Commands + +#### `txtproc` - Advanced Text Processing + +Provides flexible text processing capabilities for manipulating test files and outputs. + +**Available Operations:** + +| Operation | Description | Usage | +|-----------------|-------------------------------------|----------------------------------------------------------------------| +| `replace` | Replace literal text strings | `txtproc replace 'old_text' 'new_text' input.txt output.txt` | +| `replace-regex` | Replace using regular expressions | `txtproc replace-regex 'pattern' 'replacement' input.txt output.txt` | +| `remove-lines` | Remove lines matching a pattern | `txtproc remove-lines 'pattern' input.txt output.txt` | +| `remove-regex` | Remove text matching regex pattern | `txtproc remove-regex 'pattern' input.txt output.txt` | +| `extract-lines` | Extract lines matching a pattern | `txtproc extract-lines 'pattern' input.txt output.txt` | +| `extract-regex` | Extract text matching regex pattern | `txtproc extract-regex 'pattern' input.txt output.txt` | + +**Examples:** + +```bash +# Replace version numbers in output +txtproc replace 'v1.0.0' 'v2.0.0' app_output.txt expected.txt + +# Extract error messages using regex +txtproc extract-regex 'ERROR: .*' log.txt errors.txt + +# Remove debug lines from output +txtproc remove-lines 'DEBUG:' verbose_output.txt clean_output.txt +``` + +### Utility Commands + +#### `sleep` - Execution Delay + +Pauses test execution for a specified duration, useful for timing-sensitive tests or waiting for asynchronous +operations. + +**Usage:** + +```bash +sleep +``` + +**Supported Duration Formats:** + +- `ns` - nanoseconds +- `us` - microseconds +- `ms` - milliseconds +- `s` - seconds +- `m` - minutes +- `h` - hours + +**Examples:** + +```bash +# Wait for 1 second +sleep 1s + +# Wait for 500 milliseconds +sleep 500ms + +# Wait for 2 minutes +sleep 2m + +# Wait for background process +sleep 100ms +``` + +#### `dlv` - Debug Integration + +Runs the specified binary with [Delve](https://github.com/go-delve/delve) debugger support for interactive debugging +during tests. + +**Usage:** + +```bash +dlv +``` + +**Prerequisites:** + +- Binary must be compiled with debug headers (`-gcflags="all=-N -l"`) +- Delve must be installed in the testing environment + +**Examples:** + +```bash +# Debug the main application +dlv launchr +``` + +## Command Overrides + +### Enhanced `kill` Command + +We override the default testscript `kill` command to provide broader signal support beyond the standard implementation. + +**Enhanced Features:** + +- Support for additional POSIX signals +- Cross-platform signal handling +- Improved process termination reliability + +**Usage:** + +```bash +# Standard termination +kill bg-name + +# Graceful shutdown with SIGTERM +kill -TERM bg-name +``` + +## Integration Tests + +All tests live in `testdata/compose/` as `.txtar` files. + +### Download + +| File | What it tests | +|-----------------------------------|--------------------------------------------------------------------------------------------| +| `download_flat.txtar` | Two flat packages via `path` source β€” both appear in the build | +| `download_deep_deps.txtar` | Deep chain `root β†’ pkg-a β†’ lib-common β†’ lib-base` β€” all transitive deps downloaded | +| `download_version_conflict.txtar` | Same package required with different `ref` by two packages β€” error with `version conflict` | + +### Build + +| File | What it tests | +|-----------------------------------|-----------------------------------------------------------------------------------------| +| `build_conflict_last_wins.txtar` | Same file in two packages β€” package declared last in YAML wins | +| `build_platform_wins.txtar` | Same file in package and platform dir β€” platform always wins | +| `build_diamond.txtar` | Diamond dep (`pkg-a` and `pkg-b` both depend on `lib-common`) β€” dedup + last-wins order | +| `build_conflicts_verbosity.txtar` | `--conflicts-verbosity` flag β€” conflict lines printed to stdout | +| `build_clean.txtar` | `--clean` flag β€” packages dir removed; build dir always recreated (stale files gone) | + +### Strategies + +| File | What it tests | +|---------------------------------------|----------------------------------------------------------------------------------------------------| +| `strategy_filter.txtar` | `filter-package-files` β€” only whitelisted paths from package enter the build | +| `strategy_filter_last_wins.txtar` | `filter-package-files` + last-wins β€” later package overwrites within its whitelist | +| `strategy_ignore.txtar` | `ignore-extra-package-files` β€” matching package files are skipped entirely | +| `strategy_ignore_keeps_earlier.txtar` | `ignore-extra-package-files` β€” earlier package file preserved when later package ignores that path | +| `strategy_remove_local.txtar` | `remove-extra-local-files` β€” matching platform files excluded from the build | + +## Writing Integration Tests + +### Basic Test Structure + +Integration tests use the [txtar format](https://pkg.go.dev/github.com/rogpeppe/go-internal/txtar) to bundle test +scripts with their required files. See [examples](./testdata). + +### Best Practices + +#### Test Organization + +- Group related tests in logical directories +- Use descriptive test file names +- Include both positive and negative test cases + +#### File Management + +- Keep test files small and focused +- Use meaningful fixture names +- Clean up temporary files when possible + +#### Error Handling + +- Test error conditions explicitly +- Verify error messages and exit codes +- Use `! exec` for commands expected to fail + +#### Performance Considerations + +- Use `sleep` judiciously to avoid slow tests +- Prefer deterministic waits over arbitrary delays +- Consider using `make test-short` for development + +## Contributing + +When adding new integration tests: + +1. Follow the existing naming conventions +2. Include comprehensive test documentation +3. Test on multiple platforms when relevant +4. Add appropriate error handling +5. Update this README if adding new custom commands + +## Related Resources + +- [Testscript Documentation](https://github.com/rogpeppe/go-internal/tree/master/testscript) +- [Txtar Format Specification](https://pkg.go.dev/github.com/rogpeppe/go-internal/txtar) +- [Delve Debugger](https://github.com/go-delve/delve) diff --git a/test/bump_test.go b/test/bump_test.go new file mode 100644 index 0000000..6cb0273 --- /dev/null +++ b/test/bump_test.go @@ -0,0 +1,35 @@ +// Package test contains testscript-based integration tests. +package test + +import ( + "testing" + + "github.com/rogpeppe/go-internal/testscript" + + _ "github.com/launchrctl/compose" // registers compose plugin + "github.com/launchrctl/launchr" + launchrtest "github.com/launchrctl/launchr/test" + _ "github.com/skilld-labs/plasmactl-bump/v2" // registers bump plugin +) + +func TestMain(m *testing.M) { + testscript.Main(m, map[string]func(){ + "launchr": launchr.RunAndExit, + }) +} + +func TestBump(t *testing.T) { + testscript.Run(t, testscript.Params{ + Dir: "testdata/bump", + Cmds: launchrtest.CmdsTestScript(), + RequireExplicitExec: true, + }) +} + +func TestSync(t *testing.T) { + testscript.Run(t, testscript.Params{ + Dir: "testdata/sync", + Cmds: launchrtest.CmdsTestScript(), + RequireExplicitExec: true, + }) +} diff --git a/test/testdata/bump/bump_across_multiple_commits.txtar b/test/testdata/bump/bump_across_multiple_commits.txtar new file mode 100644 index 0000000..fa15771 --- /dev/null +++ b/test/testdata/bump/bump_across_multiple_commits.txtar @@ -0,0 +1,52 @@ +# Several commits after the last bump: bump uses the hash of the most recent commit. +# lib-a changed in commit A, then again in commit B β†’ bumped to commit B hash. + +exec git init +exec git config user.email 'dev@example.com' +exec git config user.name 'Developer' + +env GIT_AUTHOR_DATE='2000-01-01T00:00:00' +env GIT_COMMITTER_DATE='2000-01-01T00:00:00' +exec git add . +exec git commit -m 'initial state' + +# First change +env GIT_AUTHOR_DATE='2000-01-02T00:00:00' +env GIT_COMMITTER_DATE='2000-01-02T00:00:00' +txtproc replace 'msg: hello' 'msg: world' interaction/softwares/roles/my-role/tasks/main.yaml interaction/softwares/roles/my-role/tasks/main.yaml + +exec git add . +exec git commit -m 'change one' + +# Second change in a separate commit +env GIT_AUTHOR_DATE='2000-01-03T00:00:00' +env GIT_COMMITTER_DATE='2000-01-03T00:00:00' +txtproc replace 'msg: world' 'msg: final' interaction/softwares/roles/my-role/tasks/main.yaml interaction/softwares/roles/my-role/tasks/main.yaml + +exec git add . +exec git commit -m 'change two' + +exec launchr bump --hide-progress + +stdout 'Processing resource interaction__softwares__my-role' + +# version must be the hash of the latest commit ('change two'), not 'change one' +! grep 'version: ver1' interaction/softwares/roles/my-role/meta/plasma.yaml +grep 'version: [0-9a-f]{13}' interaction/softwares/roles/my-role/meta/plasma.yaml + +# verify it matches 'change two' commit (HEAD~1 = bump commit, HEAD~2 = change two) +exec sh -c 'git log --format=%H HEAD~2..HEAD~1 | cut -c1-13 > /tmp/change_two_hash.txt' +exec sh -c 'grep -o "[0-9a-f]\{13\}" interaction/softwares/roles/my-role/meta/plasma.yaml > /tmp/ver_hash.txt' +exec sh -c 'diff /tmp/change_two_hash.txt /tmp/ver_hash.txt' + +-- plasma-compose.yaml -- +dependencies: [] + +-- interaction/softwares/roles/my-role/meta/plasma.yaml -- +plasma: + version: ver1 + +-- interaction/softwares/roles/my-role/tasks/main.yaml -- +- name: example task + debug: + msg: hello diff --git a/test/testdata/bump/bump_basic.txtar b/test/testdata/bump/bump_basic.txtar new file mode 100644 index 0000000..9b2589a --- /dev/null +++ b/test/testdata/bump/bump_basic.txtar @@ -0,0 +1,31 @@ +# Resource file changed in last commit β†’ bump updates meta/plasma.yaml version to commit hash. + +exec git init +exec git config user.email 'dev@example.com' +exec git config user.name 'Developer' + +exec git add . +exec git commit -m 'initial state' + +# Simulate work: modify a resource task file +txtproc replace 'msg: hello' 'msg: world' interaction/softwares/roles/my-role/tasks/main.yaml interaction/softwares/roles/my-role/tasks/main.yaml + +exec git add . +exec git commit -m 'update my-role task' + +exec launchr bump --hide-progress + +stdout 'Processing resource interaction__softwares__my-role' + +# Version must be updated to 13-char commit hash, not the original ver1 +! grep 'version: ver1' interaction/softwares/roles/my-role/meta/plasma.yaml +grep 'version: [0-9a-f]{13}' interaction/softwares/roles/my-role/meta/plasma.yaml + +-- interaction/softwares/roles/my-role/meta/plasma.yaml -- +plasma: + version: ver1 + +-- interaction/softwares/roles/my-role/tasks/main.yaml -- +- name: example task + debug: + msg: hello diff --git a/test/testdata/bump/bump_dry_run.txtar b/test/testdata/bump/bump_dry_run.txtar new file mode 100644 index 0000000..c51fefc --- /dev/null +++ b/test/testdata/bump/bump_dry_run.txtar @@ -0,0 +1,29 @@ +# --dry-run logs what would be changed but does not modify meta/plasma.yaml or create a commit. + +exec git init +exec git config user.email 'dev@example.com' +exec git config user.name 'Developer' + +exec git add . +exec git commit -m 'initial state' + +txtproc replace 'msg: hello' 'msg: world' interaction/softwares/roles/my-role/tasks/main.yaml interaction/softwares/roles/my-role/tasks/main.yaml + +exec git add . +exec git commit -m 'update my-role' + +exec launchr bump --dry-run --hide-progress + +stdout 'Processing resource interaction__softwares__my-role' + +# Version must NOT be written (dry-run) +grep 'version: ver1' interaction/softwares/roles/my-role/meta/plasma.yaml + +-- interaction/softwares/roles/my-role/meta/plasma.yaml -- +plasma: + version: ver1 + +-- interaction/softwares/roles/my-role/tasks/main.yaml -- +- name: example task + debug: + msg: hello diff --git a/test/testdata/bump/bump_multiple_in_one_commit.txtar b/test/testdata/bump/bump_multiple_in_one_commit.txtar new file mode 100644 index 0000000..63aae4b --- /dev/null +++ b/test/testdata/bump/bump_multiple_in_one_commit.txtar @@ -0,0 +1,52 @@ +# Two resources changed in one commit β†’ both get bumped to the same commit hash. + +exec git init +exec git config user.email 'dev@example.com' +exec git config user.name 'Developer' + +env GIT_AUTHOR_DATE='2000-01-01T00:00:00' +env GIT_COMMITTER_DATE='2000-01-01T00:00:00' +exec git add . +exec git commit -m 'initial state' + +env GIT_AUTHOR_DATE='2000-01-02T00:00:00' +env GIT_COMMITTER_DATE='2000-01-02T00:00:00' +txtproc replace 'msg: lib-a task' 'msg: lib-a task v2' interaction/libraries/roles/lib-a/tasks/main.yaml interaction/libraries/roles/lib-a/tasks/main.yaml +txtproc replace 'msg: lib-b task' 'msg: lib-b task v2' interaction/libraries/roles/lib-b/tasks/main.yaml interaction/libraries/roles/lib-b/tasks/main.yaml + +exec git add . +exec git commit -m 'update lib-a and lib-b' + +exec launchr bump --hide-progress + +# both resources bumped to the same commit hash +! grep 'version: ver1' interaction/libraries/roles/lib-a/meta/plasma.yaml +! grep 'version: ver1' interaction/libraries/roles/lib-b/meta/plasma.yaml +grep 'version: [0-9a-f]{13}' interaction/libraries/roles/lib-a/meta/plasma.yaml +grep 'version: [0-9a-f]{13}' interaction/libraries/roles/lib-b/meta/plasma.yaml + +# versions must be identical (same commit) +exec sh -c 'grep -o "version: .*" interaction/libraries/roles/lib-a/meta/plasma.yaml > /tmp/va.txt' +exec sh -c 'grep -o "version: .*" interaction/libraries/roles/lib-b/meta/plasma.yaml > /tmp/vb.txt' +exec sh -c 'diff /tmp/va.txt /tmp/vb.txt' + +-- plasma-compose.yaml -- +dependencies: [] + +-- interaction/libraries/roles/lib-a/meta/plasma.yaml -- +plasma: + version: ver1 + +-- interaction/libraries/roles/lib-a/tasks/main.yaml -- +- name: lib-a task + debug: + msg: lib-a task + +-- interaction/libraries/roles/lib-b/meta/plasma.yaml -- +plasma: + version: ver1 + +-- interaction/libraries/roles/lib-b/tasks/main.yaml -- +- name: lib-b task + debug: + msg: lib-b task diff --git a/test/testdata/bump/bump_new_resource.txtar b/test/testdata/bump/bump_new_resource.txtar new file mode 100644 index 0000000..daee97f --- /dev/null +++ b/test/testdata/bump/bump_new_resource.txtar @@ -0,0 +1,28 @@ +# Case 4: a newly added resource gets bumped. + +exec git init +exec git config user.email 'dev@example.com' +exec git config user.name 'Developer' + +env GIT_AUTHOR_DATE='2000-01-01T00:00:00' +env GIT_COMMITTER_DATE='2000-01-01T00:00:00' +exec git add . +exec git commit -m 'initial state' + +# Create a new resource that does not exist yet +env GIT_AUTHOR_DATE='2000-01-02T00:00:00' +env GIT_COMMITTER_DATE='2000-01-02T00:00:00' +exec sh -c 'mkdir -p interaction/softwares/roles/new-role/meta interaction/softwares/roles/new-role/tasks' +exec sh -c 'printf "plasma:\n version: ver1\n" > interaction/softwares/roles/new-role/meta/plasma.yaml' +exec sh -c 'printf -- "- name: task\n debug:\n msg: hello\n" > interaction/softwares/roles/new-role/tasks/main.yaml' +exec git add . +exec git commit -m 'add new-role' + +exec launchr bump --hide-progress + +# new-role receives a commit hash version +! grep 'version: ver1' interaction/softwares/roles/new-role/meta/plasma.yaml +grep 'version: [0-9a-f]{13}' interaction/softwares/roles/new-role/meta/plasma.yaml + +-- plasma-compose.yaml -- +dependencies: [] diff --git a/test/testdata/bump/bump_removed_resource.txtar b/test/testdata/bump/bump_removed_resource.txtar new file mode 100644 index 0000000..1a008a1 --- /dev/null +++ b/test/testdata/bump/bump_removed_resource.txtar @@ -0,0 +1,33 @@ +# Case 3: a resource is removed before bump β†’ bump finds nothing to update. + +exec git init +exec git config user.email 'dev@example.com' +exec git config user.name 'Developer' + +env GIT_AUTHOR_DATE='2000-01-01T00:00:00' +env GIT_COMMITTER_DATE='2000-01-01T00:00:00' +exec git add . +exec git commit -m 'initial state' + +# Remove the resource (delete meta file so it is no longer a valid resource) +env GIT_AUTHOR_DATE='2000-01-02T00:00:00' +env GIT_COMMITTER_DATE='2000-01-02T00:00:00' +exec git rm interaction/softwares/roles/my-role/meta/plasma.yaml +exec git rm interaction/softwares/roles/my-role/tasks/main.yaml +exec git commit -m 'remove my-role' + +exec launchr bump --hide-progress + +stdout 'No resource to update' + +-- plasma-compose.yaml -- +dependencies: [] + +-- interaction/softwares/roles/my-role/meta/plasma.yaml -- +plasma: + version: ver1 + +-- interaction/softwares/roles/my-role/tasks/main.yaml -- +- name: task + debug: + msg: hello diff --git a/test/testdata/bump/bump_skips_own_commit.txtar b/test/testdata/bump/bump_skips_own_commit.txtar new file mode 100644 index 0000000..553918a --- /dev/null +++ b/test/testdata/bump/bump_skips_own_commit.txtar @@ -0,0 +1,26 @@ +# When the latest commit is already by the Bumper, bump should skip without error. + +exec git init +exec git config user.email 'dev@example.com' +exec git config user.name 'Developer' + +exec git add . +exec git commit -m 'initial state' + +env GIT_AUTHOR_NAME=Bumper +env GIT_AUTHOR_EMAIL=no-reply@skilld.cloud +env GIT_COMMITTER_NAME=Bumper +env GIT_COMMITTER_EMAIL=no-reply@skilld.cloud +exec git commit --allow-empty -m 'versions bump' + +exec launchr bump --hide-progress +stdout 'skipping bump' + +-- interaction/softwares/roles/my-role/meta/plasma.yaml -- +plasma: + version: ver1 + +-- interaction/softwares/roles/my-role/tasks/main.yaml -- +- name: example task + debug: + msg: hello diff --git a/test/testdata/bump/bump_skips_unversioned_files.txtar b/test/testdata/bump/bump_skips_unversioned_files.txtar new file mode 100644 index 0000000..8137d5a --- /dev/null +++ b/test/testdata/bump/bump_skips_unversioned_files.txtar @@ -0,0 +1,33 @@ +# Changes to README.md and README.svg are excluded from bump β€” no resource update occurs. + +exec git init +exec git config user.email 'dev@example.com' +exec git config user.name 'Developer' + +exec git add . +exec git commit -m 'initial state' + +# Modify only the excluded file +txtproc replace 'original readme' 'updated readme' interaction/softwares/roles/my-role/README.md interaction/softwares/roles/my-role/README.md + +exec git add . +exec git commit -m 'update readme' + +exec launchr bump --hide-progress + +stdout 'No resource to update' + +# Version must remain unchanged +grep 'version: ver1' interaction/softwares/roles/my-role/meta/plasma.yaml + +-- interaction/softwares/roles/my-role/meta/plasma.yaml -- +plasma: + version: ver1 + +-- interaction/softwares/roles/my-role/tasks/main.yaml -- +- name: example task + debug: + msg: hello + +-- interaction/softwares/roles/my-role/README.md -- +original readme diff --git a/test/testdata/sync/sync_allow_override.txtar b/test/testdata/sync/sync_allow_override.txtar new file mode 100644 index 0000000..bfa2a68 --- /dev/null +++ b/test/testdata/sync/sync_allow_override.txtar @@ -0,0 +1,65 @@ +# --allow-override: build dir has a version that doesn't match HEAD git commit. +# This happens when a resource was manually edited in place without running bump. +# Without --allow-override sync errors out; with it sync continues and propagates. +# +# Layout: +# $WORK/platform/ β€” domain (git repo), compose and bump run here + +exec git -C platform init +exec git -C platform config user.email 'dev@example.com' +exec git -C platform config user.name 'Developer' + +env GIT_AUTHOR_DATE='2000-01-01T00:00:00' +env GIT_COMMITTER_DATE='2000-01-01T00:00:00' +exec git -C platform add . +exec git -C platform commit -m 'initial state' + +# Bump lib-a normally so it has a real commit hash version +env GIT_AUTHOR_DATE='2000-01-02T00:00:00' +env GIT_COMMITTER_DATE='2000-01-02T00:00:00' +txtproc replace 'msg: lib task' 'msg: lib task v2' platform/interaction/libraries/roles/lib-a/tasks/main.yaml platform/interaction/libraries/roles/lib-a/tasks/main.yaml + +exec git -C platform add . +exec git -C platform commit -m 'update lib-a' + +exec sh -c 'cd platform && launchr bump --hide-progress' + +! grep 'version: ver1' platform/interaction/libraries/roles/lib-a/meta/plasma.yaml + +exec sh -c 'cd platform && launchr compose --interactive=false' + +# Manually overwrite lib-a version in build dir without updating git β€” mismatch! +txtproc replace 'version: ' 'version: manually-set-' platform/.compose/build/interaction/libraries/roles/lib-a/meta/plasma.yaml platform/.compose/build/interaction/libraries/roles/lib-a/meta/plasma.yaml + +# Without --allow-override sync fails with version mismatch error +! exec sh -c 'cd platform && launchr bump --sync --vault-pass test --keyring-passphrase testpass --playbook-filter=false --hide-progress' +stdout 'match HEAD commit' + +# With --allow-override sync continues using HEAD commit time for the overridden resource +exec sh -c 'cd platform && launchr bump --sync --allow-override --vault-pass test --keyring-passphrase testpass --playbook-filter=false --hide-progress' + +# skill-b depends on lib-a and must receive a propagated version based on the overridden lib-a value. +# The overridden version is "manually-set-" which already contains "-", so composeVersion +# passes it through as-is (no additional "ver1-" prefix). +grep 'version: manually-set-' platform/.compose/build/interaction/skills/roles/skill-b/meta/plasma.yaml + +-- platform/plasma-compose.yaml -- +dependencies: [] + +-- platform/interaction/libraries/roles/lib-a/meta/plasma.yaml -- +plasma: + version: ver1 + +-- platform/interaction/libraries/roles/lib-a/tasks/main.yaml -- +- name: lib task + debug: + msg: lib task + +-- platform/interaction/skills/roles/skill-b/meta/plasma.yaml -- +plasma: + version: ver1 + +-- platform/interaction/skills/roles/skill-b/tasks/dependencies.yaml -- +- name: include lib-a + include_role: + name: interaction.libraries.lib-a diff --git a/test/testdata/sync/sync_basic_propagation.txtar b/test/testdata/sync/sync_basic_propagation.txtar new file mode 100644 index 0000000..f0c6b9e --- /dev/null +++ b/test/testdata/sync/sync_basic_propagation.txtar @@ -0,0 +1,60 @@ +# Cases 1+2: lib-a is bumped; skill-b depends on lib-a. +# After compose + sync, skill-b gets propagated version: ver1-{lib_a_bump_hash}. +# +# Layout: +# $WORK/platform/ β€” domain (git repo), compose and bump run here + +exec git -C platform init +exec git -C platform config user.email 'dev@example.com' +exec git -C platform config user.name 'Developer' + +env GIT_AUTHOR_DATE='2000-01-01T00:00:00' +env GIT_COMMITTER_DATE='2000-01-01T00:00:00' +exec git -C platform add . +exec git -C platform commit -m 'initial state' + +# Modify lib-a to trigger bump (skill-b is untouched) +env GIT_AUTHOR_DATE='2000-01-02T00:00:00' +env GIT_COMMITTER_DATE='2000-01-02T00:00:00' +txtproc replace 'msg: lib task' 'msg: lib task v2' platform/interaction/libraries/roles/lib-a/tasks/main.yaml platform/interaction/libraries/roles/lib-a/tasks/main.yaml + +exec git -C platform add . +exec git -C platform commit -m 'update lib-a' + +exec sh -c 'cd platform && launchr bump --hide-progress' + +! grep 'version: ver1' platform/interaction/libraries/roles/lib-a/meta/plasma.yaml +grep 'version: [0-9a-f]{13}' platform/interaction/libraries/roles/lib-a/meta/plasma.yaml +# skill-b is unchanged +grep 'version: ver1' platform/interaction/skills/roles/skill-b/meta/plasma.yaml + +exec sh -c 'cd platform && launchr compose --interactive=false' + +exec sh -c 'cd platform && launchr bump --sync --vault-pass test --keyring-passphrase testpass --playbook-filter=false --hide-progress' + +# skill-b in build gets propagated version: original_version-bump_hash +grep 'version: ver1-[0-9a-f]{13}' platform/.compose/build/interaction/skills/roles/skill-b/meta/plasma.yaml + +# lib-a itself is not in propagation (it is the source, not a dependent) +! grep 'version: ver1-' platform/.compose/build/interaction/libraries/roles/lib-a/meta/plasma.yaml + +-- platform/plasma-compose.yaml -- +dependencies: [] + +-- platform/interaction/libraries/roles/lib-a/meta/plasma.yaml -- +plasma: + version: ver1 + +-- platform/interaction/libraries/roles/lib-a/tasks/main.yaml -- +- name: lib task + debug: + msg: lib task + +-- platform/interaction/skills/roles/skill-b/meta/plasma.yaml -- +plasma: + version: ver1 + +-- platform/interaction/skills/roles/skill-b/tasks/dependencies.yaml -- +- name: include lib-a + include_role: + name: interaction.libraries.lib-a diff --git a/test/testdata/sync/sync_chain_two_bumps.txtar b/test/testdata/sync/sync_chain_two_bumps.txtar new file mode 100644 index 0000000..aad1aed --- /dev/null +++ b/test/testdata/sync/sync_chain_two_bumps.txtar @@ -0,0 +1,86 @@ +# Case 14 simplified: lib-a (domain) β†’ func-b β†’ skill-c, both lib-a and func-b are bumped. +# lib-a bumped first (bump_1), func-b bumped second (bump_2). +# Expected: func-b gets bump_2 (its own bump overrides propagation from lib-a). +# skill-c gets ver1-bump_2 (propagated from func-b, the later bump in its chain). +# +# Layout: +# $WORK/platform/ β€” domain (git repo), compose and bump run here + +exec git -C platform init +exec git -C platform config user.email 'dev@example.com' +exec git -C platform config user.name 'Developer' + +env GIT_AUTHOR_DATE='2000-01-01T00:00:00' +env GIT_COMMITTER_DATE='2000-01-01T00:00:00' +exec git -C platform add . +exec git -C platform commit -m 'initial state' + +# First bump: update lib-a +env GIT_AUTHOR_DATE='2000-01-02T00:00:00' +env GIT_COMMITTER_DATE='2000-01-02T00:00:00' +txtproc replace 'msg: lib task' 'msg: lib task v2' platform/interaction/libraries/roles/lib-a/tasks/main.yaml platform/interaction/libraries/roles/lib-a/tasks/main.yaml + +exec git -C platform add . +exec git -C platform commit -m 'update lib-a' + +exec sh -c 'cd platform && launchr bump --hide-progress' + +! grep 'version: ver1' platform/interaction/libraries/roles/lib-a/meta/plasma.yaml + +# Second bump: update func-b +env GIT_AUTHOR_DATE='2000-01-03T00:00:00' +env GIT_COMMITTER_DATE='2000-01-03T00:00:00' +txtproc replace 'msg: func task' 'msg: func task v2' platform/interaction/functions/roles/func-b/tasks/main.yaml platform/interaction/functions/roles/func-b/tasks/main.yaml + +exec git -C platform add . +exec git -C platform commit -m 'update func-b' + +exec sh -c 'cd platform && launchr bump --hide-progress' + +! grep 'version: ver1' platform/interaction/functions/roles/func-b/meta/plasma.yaml +grep 'version: ver1' platform/interaction/skills/roles/skill-c/meta/plasma.yaml + +exec sh -c 'cd platform && launchr compose --interactive=false' + +exec sh -c 'cd platform && launchr bump --sync --vault-pass test --keyring-passphrase testpass --playbook-filter=false --hide-progress' + +# func-b was bumped directly β†’ not propagated (it is the source) +! grep 'version: ver1-' platform/.compose/build/interaction/functions/roles/func-b/meta/plasma.yaml + +# skill-c depends on func-b (the later bump) β†’ gets ver1-{func-b bump hash} +grep 'version: ver1-[0-9a-f]{13}' platform/.compose/build/interaction/skills/roles/skill-c/meta/plasma.yaml + +-- platform/plasma-compose.yaml -- +dependencies: [] + +-- platform/interaction/libraries/roles/lib-a/meta/plasma.yaml -- +plasma: + version: ver1 + +-- platform/interaction/libraries/roles/lib-a/tasks/main.yaml -- +- name: lib task + debug: + msg: lib task + +-- platform/interaction/functions/roles/func-b/meta/plasma.yaml -- +plasma: + version: ver1 + +-- platform/interaction/functions/roles/func-b/tasks/main.yaml -- +- name: func task + debug: + msg: func task + +-- platform/interaction/functions/roles/func-b/tasks/dependencies.yaml -- +- name: include lib-a + include_role: + name: interaction.libraries.lib-a + +-- platform/interaction/skills/roles/skill-c/meta/plasma.yaml -- +plasma: + version: ver1 + +-- platform/interaction/skills/roles/skill-c/tasks/dependencies.yaml -- +- name: include func-b + include_role: + name: interaction.functions.func-b diff --git a/test/testdata/sync/sync_chain_with_variable.txtar b/test/testdata/sync/sync_chain_with_variable.txtar new file mode 100644 index 0000000..f0efae0 --- /dev/null +++ b/test/testdata/sync/sync_chain_with_variable.txtar @@ -0,0 +1,114 @@ +# Case 16 simplified: lib-a bumped + skill-b (depends on lib-a) also bumped independently +# + variable changed that skill-b uses via template. +# Final state: skill-c (depends on skill-b) gets the latest of the two propagations. +# +# Timeline (newest first): +# variable change commit β†’ propagates to skill-b, skill-c +# skill-b bump β†’ skill-c gets ver1-skill_b_hash +# lib-a bump β†’ skill-b would get ver1-lib_a_hash, skill-c too +# but skill-b is bumped directly so it is skipped (processed) +# +# Expected result in build: +# lib-a: lib_a_hash (own bump, not propagated) +# skill-b: skill_b_hash (own bump, overrides propagation from lib-a) +# skill-c: ver1-var_hash (variable is newest β†’ overrides skill-b propagation) +# +# Layout: +# $WORK/platform/ β€” domain (git repo), compose and bump run here + +exec git -C platform init +exec git -C platform config user.email 'dev@example.com' +exec git -C platform config user.name 'Developer' + +env GIT_AUTHOR_DATE='2000-01-01T00:00:00' +env GIT_COMMITTER_DATE='2000-01-01T00:00:00' +exec git -C platform add . +exec git -C platform commit -m 'initial state' + +# First bump: lib-a +env GIT_AUTHOR_DATE='2000-01-02T00:00:00' +env GIT_COMMITTER_DATE='2000-01-02T00:00:00' +txtproc replace 'msg: lib task' 'msg: lib task v2' platform/interaction/libraries/roles/lib-a/tasks/main.yaml platform/interaction/libraries/roles/lib-a/tasks/main.yaml + +exec git -C platform add . +exec git -C platform commit -m 'update lib-a' + +exec sh -c 'cd platform && launchr bump --hide-progress' + +! grep 'version: ver1' platform/interaction/libraries/roles/lib-a/meta/plasma.yaml + +# Second bump: skill-b +env GIT_AUTHOR_DATE='2000-01-03T00:00:00' +env GIT_COMMITTER_DATE='2000-01-03T00:00:00' +txtproc replace 'msg: skill task' 'msg: skill task v2' platform/interaction/skills/roles/skill-b/tasks/main.yaml platform/interaction/skills/roles/skill-b/tasks/main.yaml + +exec git -C platform add . +exec git -C platform commit -m 'update skill-b' + +exec sh -c 'cd platform && launchr bump --hide-progress' + +! grep 'version: ver1' platform/interaction/skills/roles/skill-b/meta/plasma.yaml + +# Variable change (newest) β€” no bump needed for variables +env GIT_AUTHOR_DATE='2000-01-04T00:00:00' +env GIT_COMMITTER_DATE='2000-01-04T00:00:00' +txtproc replace 'value: original' 'value: updated' platform/interaction/group_vars/all/vars.yaml platform/interaction/group_vars/all/vars.yaml + +exec git -C platform add . +exec git -C platform commit -m 'update test_var' + +exec sh -c 'cd platform && launchr compose --interactive=false' + +exec sh -c 'cd platform && launchr bump --sync --vault-pass test --keyring-passphrase testpass --playbook-filter=false --hide-progress' + +# lib-a is a bump source β€” not propagated +! grep 'version: ver1-' platform/.compose/build/interaction/libraries/roles/lib-a/meta/plasma.yaml + +# skill-b was bumped directly β†’ not propagated +! grep 'version: ver1-' platform/.compose/build/interaction/skills/roles/skill-b/meta/plasma.yaml + +# skill-c depends on skill-b AND uses the variable (via template). +# Variable commit is the newest β†’ skill-c gets ver1-{var_commit_hash} +grep 'version: ver1-[0-9a-f]{13}' platform/.compose/build/interaction/skills/roles/skill-c/meta/plasma.yaml + +-- platform/plasma-compose.yaml -- +dependencies: [] + +-- platform/interaction/group_vars/all/vars.yaml -- +test_var: + value: original + +-- platform/interaction/libraries/roles/lib-a/meta/plasma.yaml -- +plasma: + version: ver1 + +-- platform/interaction/libraries/roles/lib-a/tasks/main.yaml -- +- name: lib task + debug: + msg: lib task + +-- platform/interaction/skills/roles/skill-b/meta/plasma.yaml -- +plasma: + version: ver1 + +-- platform/interaction/skills/roles/skill-b/tasks/main.yaml -- +- name: skill task + debug: + msg: skill task + +-- platform/interaction/skills/roles/skill-b/tasks/dependencies.yaml -- +- name: include lib-a + include_role: + name: interaction.libraries.lib-a + +-- platform/interaction/skills/roles/skill-c/meta/plasma.yaml -- +plasma: + version: ver1 + +-- platform/interaction/skills/roles/skill-c/tasks/dependencies.yaml -- +- name: include skill-b + include_role: + name: interaction.skills.skill-b + +-- platform/interaction/skills/roles/skill-c/templates/config.j2 -- +value: {{ test_var.value }} diff --git a/test/testdata/sync/sync_complex_full.txtar b/test/testdata/sync/sync_complex_full.txtar new file mode 100644 index 0000000..f21c8c8 --- /dev/null +++ b/test/testdata/sync/sync_complex_full.txtar @@ -0,0 +1,188 @@ +# Complex full scenario combining deep package dependencies, plain variables, +# vault variables, and variableβ†’variable dependencies. +# +# Layout: +# $WORK/src/pkg-a/ β€” func-d (depends on lib-c from pkg-b) +# $WORK/src/pkg-b/ β€” lib-c (will be bumped) +# $WORK/platform/ β€” domain with skill-e, skill-f, group_vars +# +# Dependency graph (resources): +# lib-c (pkg-b) β†’ func-d (pkg-a) β†’ skill-e (domain) +# +# Variable dependencies: +# plain_var β†’ skill-e (via templates/config.j2) +# vault_secret_var β†’ skill-f (via templates/secret.j2) +# +# Timeline (newest first after setup): +# 2000-01-04 plain_var changed β†’ skill-e propagated (overrides lib-c chain) +# 2000-01-03 vault_secret_var changed β†’ skill-f propagated +# 2000-01-02 lib-c bumped β†’ func-d, skill-e propagated +# 2000-01-01 initial state +# +# Expected results: +# func-d: ver1-{lib_c_hash} (propagated from lib-c bump) +# skill-e: ver1-{plain_var_commit_hash} (plain_var is newer than lib-c chain) +# skill-f: ver1-{vault_commit_hash} (propagated from vault var change) + +exec git -C src/pkg-b init +exec git -C src/pkg-b config user.email 'dev@example.com' +exec git -C src/pkg-b config user.name 'Developer' + +exec git -C src/pkg-a init +exec git -C src/pkg-a config user.email 'dev@example.com' +exec git -C src/pkg-a config user.name 'Developer' + +exec git -C platform init +exec git -C platform config user.email 'dev@example.com' +exec git -C platform config user.name 'Developer' + +env GIT_AUTHOR_DATE='2000-01-01T00:00:00' +env GIT_COMMITTER_DATE='2000-01-01T00:00:00' + +exec git -C src/pkg-b add . +exec git -C src/pkg-b commit -m 'initial state' + +exec git -C src/pkg-a add . +exec git -C src/pkg-a commit -m 'initial state' + +exec git -C platform add . +exec git -C platform commit -m 'initial state' + +# Step 1: bump lib-c in pkg-b (oldest change) β€” simulate bump manually to control commit date +env GIT_AUTHOR_DATE='2000-01-02T00:00:00' +env GIT_COMMITTER_DATE='2000-01-02T00:00:00' + +txtproc replace 'msg: lib-c task' 'msg: lib-c task v2' src/pkg-b/interaction/libraries/roles/lib-c/tasks/main.yaml src/pkg-b/interaction/libraries/roles/lib-c/tasks/main.yaml + +exec git -C src/pkg-b add . +exec git -C src/pkg-b commit -m 'update lib-c' + +# Get the commit hash and write it as version (simulating what launchr bump would do) +exec sh -c 'git -C src/pkg-b log --format="%H" -1 | cut -c1-13 > /tmp/lib_c_hash.txt' +exec sh -c 'printf "plasma:\n version: $(cat /tmp/lib_c_hash.txt)\n" > src/pkg-b/interaction/libraries/roles/lib-c/meta/plasma.yaml' +exec git -C src/pkg-b add . +exec git -C src/pkg-b -c user.name='Bumper' -c user.email='no-reply@skilld.cloud' commit -m '[bump]' + +! grep 'version: ver1' src/pkg-b/interaction/libraries/roles/lib-c/meta/plasma.yaml +grep 'version: [0-9a-f]{13}' src/pkg-b/interaction/libraries/roles/lib-c/meta/plasma.yaml + +# Step 2: change vault variable (skill-f depends on it) +env GIT_AUTHOR_DATE='2000-01-03T00:00:00' +env GIT_COMMITTER_DATE='2000-01-03T00:00:00' + +cp platform/interaction/group_vars/all/vault_v2.yaml platform/interaction/group_vars/all/vault.yaml + +exec git -C platform add . +exec git -C platform commit -m 'update vault_secret_var' + +# Step 3: change plain_var (skill-e depends on it via template; newest β†’ wins over lib-c chain) +env GIT_AUTHOR_DATE='2000-01-04T00:00:00' +env GIT_COMMITTER_DATE='2000-01-04T00:00:00' + +txtproc replace 'value: original' 'value: updated' platform/interaction/group_vars/all/vars.yaml platform/interaction/group_vars/all/vars.yaml + +exec git -C platform add . +exec git -C platform commit -m 'update plain_var' + +exec sh -c 'cd platform && launchr compose --interactive=false' + +# verify lock contains both transitive packages +grep 'name: pkg-b' platform/.compose/build/plasma-compose.lock +grep 'name: pkg-a' platform/.compose/build/plasma-compose.lock + +exec sh -c 'cd platform && launchr bump --sync --vault-pass test --keyring-passphrase testpass --playbook-filter=false --hide-progress' + +# func-d: propagated from lib-c bump (no variable dep on func-d) +grep 'version: ver1-[0-9a-f]{13}' platform/.compose/build/interaction/functions/roles/func-d/meta/plasma.yaml + +# skill-e: plain_var change is newest (2000-01-04) β†’ overrides lib-c propagation (2000-01-02) +grep 'version: ver1-[0-9a-f]{13}' platform/.compose/build/interaction/skills/roles/skill-e/meta/plasma.yaml + +# skill-f: propagated from vault_secret_var change +grep 'version: ver1-[0-9a-f]{13}' platform/.compose/build/interaction/skills/roles/skill-f/meta/plasma.yaml + +# verify skill-e propagated hash matches plain_var commit (not lib-c commit) +exec sh -c 'git -C platform log --format="%H" -1 HEAD~1 | cut -c1-13 > /tmp/vault_commit.txt' +exec sh -c 'git -C platform log --format="%H" -1 HEAD | cut -c1-13 > /tmp/plain_commit.txt' +exec sh -c 'grep -o "[0-9a-f]\{13\}$" platform/.compose/build/interaction/skills/roles/skill-e/meta/plasma.yaml > /tmp/skill_e_hash.txt' +exec sh -c 'grep -o "[0-9a-f]\{13\}$" platform/.compose/build/interaction/skills/roles/skill-f/meta/plasma.yaml > /tmp/skill_f_hash.txt' +exec sh -c 'diff /tmp/plain_commit.txt /tmp/skill_e_hash.txt' +exec sh -c 'diff /tmp/vault_commit.txt /tmp/skill_f_hash.txt' + +-- src/pkg-b/interaction/libraries/roles/lib-c/meta/plasma.yaml -- +plasma: + version: ver1 + +-- src/pkg-b/interaction/libraries/roles/lib-c/tasks/main.yaml -- +- name: lib-c task + debug: + msg: lib-c task + +-- src/pkg-a/plasma-compose.yaml -- +dependencies: + - name: pkg-b + source: + type: path + url: ../src/pkg-b + +-- src/pkg-a/interaction/functions/roles/func-d/meta/plasma.yaml -- +plasma: + version: ver1 + +-- src/pkg-a/interaction/functions/roles/func-d/tasks/dependencies.yaml -- +- name: include lib-c + include_role: + name: interaction.libraries.lib-c + +-- platform/plasma-compose.yaml -- +dependencies: + - name: pkg-a + source: + type: path + url: ../src/pkg-a + +-- platform/interaction/group_vars/all/vars.yaml -- +plain_var: + value: original + +-- platform/interaction/group_vars/all/vault.yaml -- +$ANSIBLE_VAULT;1.1;AES256 +39333862393031373163646433343062383363373233323839613561343163666164646334363331 +6666616163383131343661616463643138393636393465630a333835653363393963633030613030 +66313238353431386237303237623432313264376163633964373063623338363264633730353864 +3537323263643562380a373364643434613933313239326436326134303730623963656362653263 +61643238303033393733303865306463363032663330333366303265333039653661386664643039 +3539336461383032316134663038326262313038393435626663 + +-- platform/interaction/group_vars/all/vault_v2.yaml -- +$ANSIBLE_VAULT;1.1;AES256 +66363731363130386233623435363764633034343762373164646661623163613930396265663330 +3165633862396435386230316464323737303066323637650a653834363866303039313739366232 +66663137653965376636336539646136376435316139613165346161626264346638313239353564 +6562343038623632360a633631653739383363316133313831636666353439633263326532323961 +31353665316636376537623133383363373732383563353462346534363436373334393863303165 +3037303832656561353463396466613533326666343266613035 + +-- platform/interaction/skills/roles/skill-e/meta/plasma.yaml -- +plasma: + version: ver1 + +-- platform/interaction/skills/roles/skill-e/tasks/dependencies.yaml -- +- name: include func-d + include_role: + name: interaction.functions.func-d + +-- platform/interaction/skills/roles/skill-e/templates/config.j2 -- +value: {{ plain_var.value }} + +-- platform/interaction/skills/roles/skill-f/meta/plasma.yaml -- +plasma: + version: ver1 + +-- platform/interaction/skills/roles/skill-f/tasks/main.yaml -- +- name: run skill-f + debug: + msg: hello + +-- platform/interaction/skills/roles/skill-f/templates/secret.j2 -- +secret: {{ vault_secret_var.value }} diff --git a/test/testdata/sync/sync_deep_transitive.txtar b/test/testdata/sync/sync_deep_transitive.txtar new file mode 100644 index 0000000..9e322d9 --- /dev/null +++ b/test/testdata/sync/sync_deep_transitive.txtar @@ -0,0 +1,147 @@ +# Deep transitive dependency chain across packages + playbook-filter. +# +# Layout: +# $WORK/src/pkg-a/ β€” source of pkg-a (git repo) +# $WORK/src/pkg-b/ β€” source of pkg-b (git repo, transitive dep of pkg-a) +# $WORK/platform/ β€” domain (git repo), compose and bump run here +# +# Dependency graph: +# skill-e (domain) β†’ func-d (pkg-a) β†’ lib-c (pkg-b) [in playbook β†’ propagated] +# skill-f (domain) β†’ lib-c (pkg-b) [NOT in playbook β†’ skipped with --playbook-filter] +# +# plasma-compose.yaml (domain) references pkg-a via ../src/pkg-a (relative to platform/). +# pkg-a's plasma-compose.yaml references pkg-b via ../src/pkg-b (relative to platform/, same CWD). +# compose copies both into platform/.compose/packages/pkg-a/latest and pkg-b/latest. +# The lock file lists both (topological order: pkg-b first, then pkg-a). +# bump --sync reads the lock β†’ sees pkg-b even though domain plasma-compose.yaml only lists pkg-a. +# +# Part 1: --playbook-filter=false β†’ both skill-e and skill-f get propagated. +# Part 2: reset build, rerun sync with --playbook-filter=true β†’ only skill-e (+ dep func-d) propagated. + +exec git -C src/pkg-b init +exec git -C src/pkg-b config user.email 'dev@example.com' +exec git -C src/pkg-b config user.name 'Developer' + +exec git -C src/pkg-a init +exec git -C src/pkg-a config user.email 'dev@example.com' +exec git -C src/pkg-a config user.name 'Developer' + +exec git -C platform init +exec git -C platform config user.email 'dev@example.com' +exec git -C platform config user.name 'Developer' + +env GIT_AUTHOR_DATE='2000-01-01T00:00:00' +env GIT_COMMITTER_DATE='2000-01-01T00:00:00' + +exec git -C src/pkg-b add . +exec git -C src/pkg-b commit -m 'initial state' + +exec git -C src/pkg-a add . +exec git -C src/pkg-a commit -m 'initial state' + +exec git -C platform add . +exec git -C platform commit -m 'initial state' + +# Bump lib-c in pkg-b (transitive package) +env GIT_AUTHOR_DATE='2000-01-02T00:00:00' +env GIT_COMMITTER_DATE='2000-01-02T00:00:00' + +txtproc replace 'msg: lib-c task' 'msg: lib-c task v2' src/pkg-b/interaction/libraries/roles/lib-c/tasks/main.yaml src/pkg-b/interaction/libraries/roles/lib-c/tasks/main.yaml + +exec git -C src/pkg-b add . +exec git -C src/pkg-b commit -m 'update lib-c' + +exec sh -c 'cd src/pkg-b && launchr bump --hide-progress' + +! grep 'version: ver1' src/pkg-b/interaction/libraries/roles/lib-c/meta/plasma.yaml +grep 'version: [0-9a-f]{13}' src/pkg-b/interaction/libraries/roles/lib-c/meta/plasma.yaml + +grep 'version: ver1' platform/interaction/skills/roles/skill-e/meta/plasma.yaml +grep 'version: ver1' platform/interaction/skills/roles/skill-f/meta/plasma.yaml + +exec sh -c 'cd platform && launchr compose --interactive=false' + +# .compose/packages/ created by compose +grep 'version: [0-9a-f]{13}' platform/.compose/packages/pkg-b/latest/interaction/libraries/roles/lib-c/meta/plasma.yaml + +# lock file must contain both pkg-b (transitive) and pkg-a (direct) +grep 'name: pkg-b' platform/.compose/build/plasma-compose.lock +grep 'name: pkg-a' platform/.compose/build/plasma-compose.lock + +# Part 1: --playbook-filter=false β†’ both skills propagated +exec sh -c 'cd platform && launchr bump --sync --vault-pass test --keyring-passphrase testpass --playbook-filter=false --hide-progress' + +grep 'version: ver1-[0-9a-f]{13}' platform/.compose/build/interaction/skills/roles/skill-e/meta/plasma.yaml +grep 'version: ver1-[0-9a-f]{13}' platform/.compose/build/interaction/skills/roles/skill-f/meta/plasma.yaml +grep 'version: ver1-[0-9a-f]{13}' platform/.compose/build/interaction/functions/roles/func-d/meta/plasma.yaml + +# Part 2: reset build and rerun with --playbook-filter=true +exec sh -c 'cd platform && launchr compose --interactive=false' + +exec sh -c 'cd platform && launchr bump --sync --vault-pass test --keyring-passphrase testpass --playbook-filter=true --hide-progress' + +# skill-e is in playbook β†’ propagated +grep 'version: ver1-[0-9a-f]{13}' platform/.compose/build/interaction/skills/roles/skill-e/meta/plasma.yaml +# func-d is a dependency of skill-e β†’ also propagated +grep 'version: ver1-[0-9a-f]{13}' platform/.compose/build/interaction/functions/roles/func-d/meta/plasma.yaml +# skill-f is NOT in playbook β†’ not propagated +! grep 'version: ver1-[0-9a-f]{13}' platform/.compose/build/interaction/skills/roles/skill-f/meta/plasma.yaml +grep 'version: ver1' platform/.compose/build/interaction/skills/roles/skill-f/meta/plasma.yaml + +-- src/pkg-b/interaction/libraries/roles/lib-c/meta/plasma.yaml -- +plasma: + version: ver1 + +-- src/pkg-b/interaction/libraries/roles/lib-c/tasks/main.yaml -- +- name: lib-c task + debug: + msg: lib-c task + +-- src/pkg-a/plasma-compose.yaml -- +dependencies: + - name: pkg-b + source: + type: path + url: ../src/pkg-b + +-- src/pkg-a/interaction/functions/roles/func-d/meta/plasma.yaml -- +plasma: + version: ver1 + +-- src/pkg-a/interaction/functions/roles/func-d/tasks/dependencies.yaml -- +- name: include lib-c + include_role: + name: interaction.libraries.lib-c + +-- platform/plasma-compose.yaml -- +dependencies: + - name: pkg-a + source: + type: path + url: ../src/pkg-a + +-- platform/platform/platform.yaml -- +- import_playbook: ../interaction/interaction.yaml + +-- platform/interaction/interaction.yaml -- +- hosts: platform.interaction + roles: + - interaction.skills.skill-e + +-- platform/interaction/skills/roles/skill-e/meta/plasma.yaml -- +plasma: + version: ver1 + +-- platform/interaction/skills/roles/skill-e/tasks/dependencies.yaml -- +- name: include func-d + include_role: + name: interaction.functions.func-d + +-- platform/interaction/skills/roles/skill-f/meta/plasma.yaml -- +plasma: + version: ver1 + +-- platform/interaction/skills/roles/skill-f/tasks/dependencies.yaml -- +- name: include lib-c directly + include_role: + name: interaction.libraries.lib-c diff --git a/test/testdata/sync/sync_domain_and_package.txtar b/test/testdata/sync/sync_domain_and_package.txtar new file mode 100644 index 0000000..bf4672c --- /dev/null +++ b/test/testdata/sync/sync_domain_and_package.txtar @@ -0,0 +1,103 @@ +# Case 12: resources bumped in both domain and package independently. +# lib-a in domain β†’ skill-b depends on it. +# lib-c in package β†’ skill-d (domain) depends on it. +# After compose + sync both skills get propagated versions. +# +# Layout: +# $WORK/src/pkg/ β€” package source (git repo) +# $WORK/platform/ β€” domain (git repo), compose and bump run here + +exec git -C src/pkg init +exec git -C src/pkg config user.email 'dev@example.com' +exec git -C src/pkg config user.name 'Developer' + +exec git -C platform init +exec git -C platform config user.email 'dev@example.com' +exec git -C platform config user.name 'Developer' + +env GIT_AUTHOR_DATE='2000-01-01T00:00:00' +env GIT_COMMITTER_DATE='2000-01-01T00:00:00' + +exec git -C src/pkg add . +exec git -C src/pkg commit -m 'initial state' + +exec git -C platform add . +exec git -C platform commit -m 'initial state' + +# Bump lib-a in domain +env GIT_AUTHOR_DATE='2000-01-02T00:00:00' +env GIT_COMMITTER_DATE='2000-01-02T00:00:00' + +txtproc replace 'msg: lib-a task' 'msg: lib-a task v2' platform/interaction/libraries/roles/lib-a/tasks/main.yaml platform/interaction/libraries/roles/lib-a/tasks/main.yaml + +exec git -C platform add . +exec git -C platform commit -m 'update lib-a' + +exec sh -c 'cd platform && launchr bump --hide-progress' + +! grep 'version: ver1' platform/interaction/libraries/roles/lib-a/meta/plasma.yaml + +# Bump lib-c in package +env GIT_AUTHOR_DATE='2000-01-03T00:00:00' +env GIT_COMMITTER_DATE='2000-01-03T00:00:00' + +txtproc replace 'msg: lib-c task' 'msg: lib-c task v2' src/pkg/interaction/libraries/roles/lib-c/tasks/main.yaml src/pkg/interaction/libraries/roles/lib-c/tasks/main.yaml + +exec git -C src/pkg add . +exec git -C src/pkg commit -m 'update lib-c' + +exec sh -c 'cd src/pkg && launchr bump --hide-progress' + +! grep 'version: ver1' src/pkg/interaction/libraries/roles/lib-c/meta/plasma.yaml + +exec sh -c 'cd platform && launchr compose --interactive=false' + +exec sh -c 'cd platform && launchr bump --sync --vault-pass test --keyring-passphrase testpass --playbook-filter=false --hide-progress' + +# skill-b (depends on domain lib-a) gets propagated version +grep 'version: ver1-[0-9a-f]{13}' platform/.compose/build/interaction/skills/roles/skill-b/meta/plasma.yaml +# skill-d (depends on package lib-c) gets propagated version +grep 'version: ver1-[0-9a-f]{13}' platform/.compose/build/interaction/skills/roles/skill-d/meta/plasma.yaml + +-- src/pkg/interaction/libraries/roles/lib-c/meta/plasma.yaml -- +plasma: + version: ver1 + +-- src/pkg/interaction/libraries/roles/lib-c/tasks/main.yaml -- +- name: lib-c task + debug: + msg: lib-c task + +-- platform/plasma-compose.yaml -- +dependencies: + - name: pkg + source: + type: path + url: ../src/pkg + +-- platform/interaction/libraries/roles/lib-a/meta/plasma.yaml -- +plasma: + version: ver1 + +-- platform/interaction/libraries/roles/lib-a/tasks/main.yaml -- +- name: lib-a task + debug: + msg: lib-a task + +-- platform/interaction/skills/roles/skill-b/meta/plasma.yaml -- +plasma: + version: ver1 + +-- platform/interaction/skills/roles/skill-b/tasks/dependencies.yaml -- +- name: include lib-a + include_role: + name: interaction.libraries.lib-a + +-- platform/interaction/skills/roles/skill-d/meta/plasma.yaml -- +plasma: + version: ver1 + +-- platform/interaction/skills/roles/skill-d/tasks/dependencies.yaml -- +- name: include lib-c + include_role: + name: interaction.libraries.lib-c diff --git a/test/testdata/sync/sync_domain_overrides_package.txtar b/test/testdata/sync/sync_domain_overrides_package.txtar new file mode 100644 index 0000000..0b7b078 --- /dev/null +++ b/test/testdata/sync/sync_domain_overrides_package.txtar @@ -0,0 +1,102 @@ +# Namespace priority: lib-a exists in both package and domain. +# Domain version wins in the build (last-writer-wins in compose). +# Sync must use the domain git repo for lib-a, not the package git repo. +# skill-b depends on lib-a β†’ gets domain lib-a bump hash as propagated version. +# +# Layout: +# $WORK/src/pkg/ β€” package source (git repo) +# $WORK/platform/ β€” domain (git repo), compose and bump run here + +exec git -C src/pkg init +exec git -C src/pkg config user.email 'dev@example.com' +exec git -C src/pkg config user.name 'Developer' + +exec git -C platform init +exec git -C platform config user.email 'dev@example.com' +exec git -C platform config user.name 'Developer' + +env GIT_AUTHOR_DATE='2000-01-01T00:00:00' +env GIT_COMMITTER_DATE='2000-01-01T00:00:00' + +exec git -C src/pkg add . +exec git -C src/pkg commit -m 'initial state' + +exec git -C platform add . +exec git -C platform commit -m 'initial state' + +# Bump lib-a in package first (older) +env GIT_AUTHOR_DATE='2000-01-02T00:00:00' +env GIT_COMMITTER_DATE='2000-01-02T00:00:00' + +txtproc replace 'msg: pkg lib task' 'msg: pkg lib task v2' src/pkg/interaction/libraries/roles/lib-a/tasks/main.yaml src/pkg/interaction/libraries/roles/lib-a/tasks/main.yaml + +exec git -C src/pkg add . +exec git -C src/pkg commit -m 'update lib-a in pkg' + +exec sh -c 'cd src/pkg && launchr bump --hide-progress' + +! grep 'version: ver1' src/pkg/interaction/libraries/roles/lib-a/meta/plasma.yaml + +# Bump lib-a in domain (newer β†’ domain wins in build) +env GIT_AUTHOR_DATE='2000-01-03T00:00:00' +env GIT_COMMITTER_DATE='2000-01-03T00:00:00' + +txtproc replace 'msg: domain lib task' 'msg: domain lib task v2' platform/interaction/libraries/roles/lib-a/tasks/main.yaml platform/interaction/libraries/roles/lib-a/tasks/main.yaml + +exec git -C platform add . +exec git -C platform commit -m 'update lib-a in domain' + +exec sh -c 'cd platform && launchr bump --hide-progress' + +! grep 'version: ver1' platform/interaction/libraries/roles/lib-a/meta/plasma.yaml + +exec sh -c 'cd platform && launchr compose --interactive=false' + +# domain lib-a wins in build (domain is processed last β†’ overrides package) +grep 'version: [0-9a-f]{13}' platform/.compose/build/interaction/libraries/roles/lib-a/meta/plasma.yaml + +exec sh -c 'cd platform && launchr bump --sync --vault-pass test --keyring-passphrase testpass --playbook-filter=false --hide-progress' + +# skill-b gets propagated version from domain lib-a bump +grep 'version: ver1-[0-9a-f]{13}' platform/.compose/build/interaction/skills/roles/skill-b/meta/plasma.yaml + +# the propagated hash must match domain lib-a bump, not package bump +exec sh -c 'grep -o "[0-9a-f]\{13\}" platform/interaction/libraries/roles/lib-a/meta/plasma.yaml > /tmp/domain_hash.txt' +exec sh -c 'grep -o "[0-9a-f]\{13\}" src/pkg/interaction/libraries/roles/lib-a/meta/plasma.yaml > /tmp/pkg_hash.txt' +exec sh -c 'grep -o "[0-9a-f]\{13\}" platform/.compose/build/interaction/skills/roles/skill-b/meta/plasma.yaml > /tmp/skill_hash.txt' +exec sh -c 'diff /tmp/domain_hash.txt /tmp/skill_hash.txt' +! exec sh -c 'diff /tmp/pkg_hash.txt /tmp/skill_hash.txt' + +-- src/pkg/interaction/libraries/roles/lib-a/meta/plasma.yaml -- +plasma: + version: ver1 + +-- src/pkg/interaction/libraries/roles/lib-a/tasks/main.yaml -- +- name: pkg lib task + debug: + msg: pkg lib task + +-- platform/plasma-compose.yaml -- +dependencies: + - name: pkg + source: + type: path + url: ../src/pkg + +-- platform/interaction/libraries/roles/lib-a/meta/plasma.yaml -- +plasma: + version: ver1 + +-- platform/interaction/libraries/roles/lib-a/tasks/main.yaml -- +- name: domain lib task + debug: + msg: domain lib task + +-- platform/interaction/skills/roles/skill-b/meta/plasma.yaml -- +plasma: + version: ver1 + +-- platform/interaction/skills/roles/skill-b/tasks/dependencies.yaml -- +- name: include lib-a + include_role: + name: interaction.libraries.lib-a diff --git a/test/testdata/sync/sync_multiple_resources.txtar b/test/testdata/sync/sync_multiple_resources.txtar new file mode 100644 index 0000000..45e2f3c --- /dev/null +++ b/test/testdata/sync/sync_multiple_resources.txtar @@ -0,0 +1,77 @@ +# Case 10: two independent bumped resources each propagate to their own dependents. +# lib-a β†’ skill-b (no relation to lib-c β†’ skill-d). +# Both pairs propagate independently. +# +# Layout: +# $WORK/platform/ β€” domain (git repo), compose and bump run here + +exec git -C platform init +exec git -C platform config user.email 'dev@example.com' +exec git -C platform config user.name 'Developer' + +env GIT_AUTHOR_DATE='2000-01-01T00:00:00' +env GIT_COMMITTER_DATE='2000-01-01T00:00:00' +exec git -C platform add . +exec git -C platform commit -m 'initial state' + +env GIT_AUTHOR_DATE='2000-01-02T00:00:00' +env GIT_COMMITTER_DATE='2000-01-02T00:00:00' +txtproc replace 'msg: lib-a task' 'msg: lib-a task v2' platform/interaction/libraries/roles/lib-a/tasks/main.yaml platform/interaction/libraries/roles/lib-a/tasks/main.yaml +txtproc replace 'msg: lib-c task' 'msg: lib-c task v2' platform/interaction/libraries/roles/lib-c/tasks/main.yaml platform/interaction/libraries/roles/lib-c/tasks/main.yaml + +exec git -C platform add . +exec git -C platform commit -m 'update lib-a and lib-c' + +exec sh -c 'cd platform && launchr bump --hide-progress' + +! grep 'version: ver1' platform/interaction/libraries/roles/lib-a/meta/plasma.yaml +! grep 'version: ver1' platform/interaction/libraries/roles/lib-c/meta/plasma.yaml +grep 'version: ver1' platform/interaction/skills/roles/skill-b/meta/plasma.yaml +grep 'version: ver1' platform/interaction/skills/roles/skill-d/meta/plasma.yaml + +exec sh -c 'cd platform && launchr compose --interactive=false' + +exec sh -c 'cd platform && launchr bump --sync --vault-pass test --keyring-passphrase testpass --playbook-filter=false --hide-progress' + +# each skill gets the propagated version of its own library +grep 'version: ver1-[0-9a-f]{13}' platform/.compose/build/interaction/skills/roles/skill-b/meta/plasma.yaml +grep 'version: ver1-[0-9a-f]{13}' platform/.compose/build/interaction/skills/roles/skill-d/meta/plasma.yaml + +-- platform/plasma-compose.yaml -- +dependencies: [] + +-- platform/interaction/libraries/roles/lib-a/meta/plasma.yaml -- +plasma: + version: ver1 + +-- platform/interaction/libraries/roles/lib-a/tasks/main.yaml -- +- name: lib-a task + debug: + msg: lib-a task + +-- platform/interaction/libraries/roles/lib-c/meta/plasma.yaml -- +plasma: + version: ver1 + +-- platform/interaction/libraries/roles/lib-c/tasks/main.yaml -- +- name: lib-c task + debug: + msg: lib-c task + +-- platform/interaction/skills/roles/skill-b/meta/plasma.yaml -- +plasma: + version: ver1 + +-- platform/interaction/skills/roles/skill-b/tasks/dependencies.yaml -- +- name: include lib-a + include_role: + name: interaction.libraries.lib-a + +-- platform/interaction/skills/roles/skill-d/meta/plasma.yaml -- +plasma: + version: ver1 + +-- platform/interaction/skills/roles/skill-d/tasks/dependencies.yaml -- +- name: include lib-c + include_role: + name: interaction.libraries.lib-c diff --git a/test/testdata/sync/sync_no_propagation.txtar b/test/testdata/sync/sync_no_propagation.txtar new file mode 100644 index 0000000..34f7f40 --- /dev/null +++ b/test/testdata/sync/sync_no_propagation.txtar @@ -0,0 +1,40 @@ +# A standalone resource with no dependents: sync has nothing to propagate. +# +# Layout: +# $WORK/platform/ β€” domain (git repo), compose and bump run here + +exec git -C platform init +exec git -C platform config user.email 'dev@example.com' +exec git -C platform config user.name 'Developer' + +env GIT_AUTHOR_DATE='2000-01-01T00:00:00' +env GIT_COMMITTER_DATE='2000-01-01T00:00:00' +exec git -C platform add . +exec git -C platform commit -m 'initial state' + +env GIT_AUTHOR_DATE='2000-01-02T00:00:00' +env GIT_COMMITTER_DATE='2000-01-02T00:00:00' +txtproc replace 'msg: task' 'msg: task v2' platform/interaction/softwares/roles/standalone/tasks/main.yaml platform/interaction/softwares/roles/standalone/tasks/main.yaml + +exec git -C platform add . +exec git -C platform commit -m 'update standalone' + +exec sh -c 'cd platform && launchr bump --hide-progress' + +exec sh -c 'cd platform && launchr compose --interactive=false' + +exec sh -c 'cd platform && launchr bump --sync --vault-pass test --keyring-passphrase testpass --playbook-filter=false --hide-progress' + +stdout 'No version to propagate' + +-- platform/plasma-compose.yaml -- +dependencies: [] + +-- platform/interaction/softwares/roles/standalone/meta/plasma.yaml -- +plasma: + version: ver1 + +-- platform/interaction/softwares/roles/standalone/tasks/main.yaml -- +- name: standalone task + debug: + msg: task diff --git a/test/testdata/sync/sync_package_overrides_domain.txtar b/test/testdata/sync/sync_package_overrides_domain.txtar new file mode 100644 index 0000000..f872cd5 --- /dev/null +++ b/test/testdata/sync/sync_package_overrides_domain.txtar @@ -0,0 +1,101 @@ +# Namespace conflict: lib-a exists in both domain and package with different bump hashes. +# Domain is processed last in compose β†’ domain version wins in build. +# Sync identifies domain as the winner and propagates domain hash to skill-b. +# The package namespace is dropped from resourcesMap (its baseVersion != buildVersion). +# +# Layout: +# $WORK/src/pkg/ β€” package source (git repo) +# $WORK/platform/ β€” domain (git repo), compose and bump run here + +exec git -C src/pkg init +exec git -C src/pkg config user.email 'dev@example.com' +exec git -C src/pkg config user.name 'Developer' + +exec git -C platform init +exec git -C platform config user.email 'dev@example.com' +exec git -C platform config user.name 'Developer' + +env GIT_AUTHOR_DATE='2000-01-01T00:00:00' +env GIT_COMMITTER_DATE='2000-01-01T00:00:00' + +exec git -C src/pkg add . +exec git -C src/pkg commit -m 'initial state' + +exec git -C platform add . +exec git -C platform commit -m 'initial state' + +# Bump lib-a in package +env GIT_AUTHOR_DATE='2000-01-02T00:00:00' +env GIT_COMMITTER_DATE='2000-01-02T00:00:00' + +txtproc replace 'msg: pkg lib task' 'msg: pkg lib task v2' src/pkg/interaction/libraries/roles/lib-a/tasks/main.yaml src/pkg/interaction/libraries/roles/lib-a/tasks/main.yaml + +exec git -C src/pkg add . +exec git -C src/pkg commit -m 'update lib-a in pkg' + +exec sh -c 'cd src/pkg && launchr bump --hide-progress' + +! grep 'version: ver1' src/pkg/interaction/libraries/roles/lib-a/meta/plasma.yaml + +# Bump lib-a in domain (newer date, different hash) +env GIT_AUTHOR_DATE='2000-01-03T00:00:00' +env GIT_COMMITTER_DATE='2000-01-03T00:00:00' + +txtproc replace 'msg: domain lib task' 'msg: domain lib task v2' platform/interaction/libraries/roles/lib-a/tasks/main.yaml platform/interaction/libraries/roles/lib-a/tasks/main.yaml + +exec git -C platform add . +exec git -C platform commit -m 'update lib-a in domain' + +exec sh -c 'cd platform && launchr bump --hide-progress' + +! grep 'version: ver1' platform/interaction/libraries/roles/lib-a/meta/plasma.yaml + +# domain and package now have different bump hashes +exec sh -c 'cd platform && launchr compose --interactive=false' + +# domain wins in build (last-writer-wins) +exec sh -c 'cd platform && launchr bump --sync --vault-pass test --keyring-passphrase testpass --playbook-filter=false --hide-progress' + +# skill-b gets propagated version +grep 'version: ver1-[0-9a-f]{13}' platform/.compose/build/interaction/skills/roles/skill-b/meta/plasma.yaml + +# propagated hash matches domain lib-a (the winner), not package lib-a +exec sh -c 'grep -o "[0-9a-f]\{13\}" platform/interaction/libraries/roles/lib-a/meta/plasma.yaml > /tmp/domain_hash.txt' +exec sh -c 'grep -o "[0-9a-f]\{13\}" src/pkg/interaction/libraries/roles/lib-a/meta/plasma.yaml > /tmp/pkg_hash.txt' +exec sh -c 'grep -o "[0-9a-f]\{13\}" platform/.compose/build/interaction/skills/roles/skill-b/meta/plasma.yaml | tail -1 > /tmp/skill_hash.txt' +exec sh -c 'diff /tmp/domain_hash.txt /tmp/skill_hash.txt' +! exec sh -c 'diff /tmp/pkg_hash.txt /tmp/skill_hash.txt' + +-- src/pkg/interaction/libraries/roles/lib-a/meta/plasma.yaml -- +plasma: + version: ver1 + +-- src/pkg/interaction/libraries/roles/lib-a/tasks/main.yaml -- +- name: pkg lib task + debug: + msg: pkg lib task + +-- platform/plasma-compose.yaml -- +dependencies: + - name: pkg + source: + type: path + url: ../src/pkg + +-- platform/interaction/libraries/roles/lib-a/meta/plasma.yaml -- +plasma: + version: ver1 + +-- platform/interaction/libraries/roles/lib-a/tasks/main.yaml -- +- name: domain lib task + debug: + msg: domain lib task + +-- platform/interaction/skills/roles/skill-b/meta/plasma.yaml -- +plasma: + version: ver1 + +-- platform/interaction/skills/roles/skill-b/tasks/dependencies.yaml -- +- name: include lib-a + include_role: + name: interaction.libraries.lib-a diff --git a/test/testdata/sync/sync_package_propagation.txtar b/test/testdata/sync/sync_package_propagation.txtar new file mode 100644 index 0000000..aa9f13f --- /dev/null +++ b/test/testdata/sync/sync_package_propagation.txtar @@ -0,0 +1,71 @@ +# Case 11: lib-a lives in a package; skill-b is in domain and depends on lib-a. +# After bumping lib-a in the package, compose, sync β†’ skill-b gets propagated version. +# +# Layout: +# $WORK/src/pkg/ β€” package source (git repo) +# $WORK/platform/ β€” domain (git repo), compose and bump run here + +exec git -C src/pkg init +exec git -C src/pkg config user.email 'dev@example.com' +exec git -C src/pkg config user.name 'Developer' + +exec git -C platform init +exec git -C platform config user.email 'dev@example.com' +exec git -C platform config user.name 'Developer' + +env GIT_AUTHOR_DATE='2000-01-01T00:00:00' +env GIT_COMMITTER_DATE='2000-01-01T00:00:00' + +exec git -C src/pkg add . +exec git -C src/pkg commit -m 'initial state' + +exec git -C platform add . +exec git -C platform commit -m 'initial state' + +# Modify lib-a in the package +env GIT_AUTHOR_DATE='2000-01-02T00:00:00' +env GIT_COMMITTER_DATE='2000-01-02T00:00:00' + +txtproc replace 'msg: lib task' 'msg: lib task v2' src/pkg/interaction/libraries/roles/lib-a/tasks/main.yaml src/pkg/interaction/libraries/roles/lib-a/tasks/main.yaml + +exec git -C src/pkg add . +exec git -C src/pkg commit -m 'update lib-a' + +exec sh -c 'cd src/pkg && launchr bump --hide-progress' + +! grep 'version: ver1' src/pkg/interaction/libraries/roles/lib-a/meta/plasma.yaml + +# skill-b is in domain β€” not bumped +grep 'version: ver1' platform/interaction/skills/roles/skill-b/meta/plasma.yaml + +exec sh -c 'cd platform && launchr compose --interactive=false' + +exec sh -c 'cd platform && launchr bump --sync --vault-pass test --keyring-passphrase testpass --playbook-filter=false --hide-progress' + +# skill-b should receive propagated version from lib-a's bump +grep 'version: ver1-[0-9a-f]{13}' platform/.compose/build/interaction/skills/roles/skill-b/meta/plasma.yaml + +-- src/pkg/interaction/libraries/roles/lib-a/meta/plasma.yaml -- +plasma: + version: ver1 + +-- src/pkg/interaction/libraries/roles/lib-a/tasks/main.yaml -- +- name: lib task + debug: + msg: lib task + +-- platform/plasma-compose.yaml -- +dependencies: + - name: pkg + source: + type: path + url: ../src/pkg + +-- platform/interaction/skills/roles/skill-b/meta/plasma.yaml -- +plasma: + version: ver1 + +-- platform/interaction/skills/roles/skill-b/tasks/dependencies.yaml -- +- name: include lib-a + include_role: + name: interaction.libraries.lib-a diff --git a/test/testdata/sync/sync_skip_identical.txtar b/test/testdata/sync/sync_skip_identical.txtar new file mode 100644 index 0000000..d6468d7 --- /dev/null +++ b/test/testdata/sync/sync_skip_identical.txtar @@ -0,0 +1,67 @@ +# When lib-a and skill-b are bumped in the same commit, they share the same bump hash. +# In buildPropagationMap both appear in the same timeline entry; skill-b is marked +# processed before lib-a looks for its dependents β†’ skill-b is never added to toPropagate. +# Result: "No version to propagate" (skill-b already has the correct version). +# +# Layout: +# $WORK/platform/ β€” domain (git repo), compose and bump run here + +exec git -C platform init +exec git -C platform config user.email 'dev@example.com' +exec git -C platform config user.name 'Developer' + +env GIT_AUTHOR_DATE='2000-01-01T00:00:00' +env GIT_COMMITTER_DATE='2000-01-01T00:00:00' +exec git -C platform add . +exec git -C platform commit -m 'initial state' + +# Change both lib-a and skill-b in the same commit β†’ bump sets both to the same hash +env GIT_AUTHOR_DATE='2000-01-02T00:00:00' +env GIT_COMMITTER_DATE='2000-01-02T00:00:00' +txtproc replace 'msg: lib task' 'msg: lib task v2' platform/interaction/libraries/roles/lib-a/tasks/main.yaml platform/interaction/libraries/roles/lib-a/tasks/main.yaml +txtproc replace 'msg: skill task' 'msg: skill task v2' platform/interaction/skills/roles/skill-b/tasks/main.yaml platform/interaction/skills/roles/skill-b/tasks/main.yaml + +exec git -C platform add . +exec git -C platform commit -m 'update lib-a and skill-b' + +exec sh -c 'cd platform && launchr bump --hide-progress' + +! grep 'version: ver1' platform/interaction/libraries/roles/lib-a/meta/plasma.yaml +! grep 'version: ver1' platform/interaction/skills/roles/skill-b/meta/plasma.yaml + +exec sh -c 'cd platform && launchr compose --interactive=false' + +exec sh -c 'cd platform && launchr bump --sync --vault-pass test --keyring-passphrase testpass --playbook-filter=false --hide-progress' + +# skill-b already has the same bump hash as lib-a β†’ nothing to propagate +stdout 'No version to propagate' + +# skill-b keeps its bump hash, no propagated suffix +! grep 'version: [0-9a-f]{13}-[0-9a-f]{13}' platform/.compose/build/interaction/skills/roles/skill-b/meta/plasma.yaml +grep 'version: [0-9a-f]{13}' platform/.compose/build/interaction/skills/roles/skill-b/meta/plasma.yaml + +-- platform/plasma-compose.yaml -- +dependencies: [] + +-- platform/interaction/libraries/roles/lib-a/meta/plasma.yaml -- +plasma: + version: ver1 + +-- platform/interaction/libraries/roles/lib-a/tasks/main.yaml -- +- name: lib task + debug: + msg: lib task + +-- platform/interaction/skills/roles/skill-b/meta/plasma.yaml -- +plasma: + version: ver1 + +-- platform/interaction/skills/roles/skill-b/tasks/main.yaml -- +- name: skill task + debug: + msg: skill task + +-- platform/interaction/skills/roles/skill-b/tasks/dependencies.yaml -- +- name: include lib-a + include_role: + name: interaction.libraries.lib-a diff --git a/test/testdata/sync/sync_transitive_propagation.txtar b/test/testdata/sync/sync_transitive_propagation.txtar new file mode 100644 index 0000000..645c0e1 --- /dev/null +++ b/test/testdata/sync/sync_transitive_propagation.txtar @@ -0,0 +1,68 @@ +# Cases 14+15 simplified (domain only): lib-a β†’ func-b β†’ skill-c (transitive chain). +# lib-a is bumped; both func-b and skill-c get propagated version. +# +# Layout: +# $WORK/platform/ β€” domain (git repo), compose and bump run here + +exec git -C platform init +exec git -C platform config user.email 'dev@example.com' +exec git -C platform config user.name 'Developer' + +env GIT_AUTHOR_DATE='2000-01-01T00:00:00' +env GIT_COMMITTER_DATE='2000-01-01T00:00:00' +exec git -C platform add . +exec git -C platform commit -m 'initial state' + +# Modify lib-a only +env GIT_AUTHOR_DATE='2000-01-02T00:00:00' +env GIT_COMMITTER_DATE='2000-01-02T00:00:00' +txtproc replace 'msg: lib task' 'msg: lib task v2' platform/interaction/libraries/roles/lib-a/tasks/main.yaml platform/interaction/libraries/roles/lib-a/tasks/main.yaml + +exec git -C platform add . +exec git -C platform commit -m 'update lib-a' + +exec sh -c 'cd platform && launchr bump --hide-progress' + +! grep 'version: ver1' platform/interaction/libraries/roles/lib-a/meta/plasma.yaml +grep 'version: ver1' platform/interaction/functions/roles/func-b/meta/plasma.yaml +grep 'version: ver1' platform/interaction/skills/roles/skill-c/meta/plasma.yaml + +exec sh -c 'cd platform && launchr compose --interactive=false' + +exec sh -c 'cd platform && launchr bump --sync --vault-pass test --keyring-passphrase testpass --playbook-filter=false --hide-progress' + +# func-b depends directly on lib-a β†’ gets propagated version +grep 'version: ver1-[0-9a-f]{13}' platform/.compose/build/interaction/functions/roles/func-b/meta/plasma.yaml + +# skill-c depends on func-b (transitively on lib-a) β†’ also gets propagated version +grep 'version: ver1-[0-9a-f]{13}' platform/.compose/build/interaction/skills/roles/skill-c/meta/plasma.yaml + +-- platform/plasma-compose.yaml -- +dependencies: [] + +-- platform/interaction/libraries/roles/lib-a/meta/plasma.yaml -- +plasma: + version: ver1 + +-- platform/interaction/libraries/roles/lib-a/tasks/main.yaml -- +- name: lib task + debug: + msg: lib task + +-- platform/interaction/functions/roles/func-b/meta/plasma.yaml -- +plasma: + version: ver1 + +-- platform/interaction/functions/roles/func-b/tasks/dependencies.yaml -- +- name: include lib-a + include_role: + name: interaction.libraries.lib-a + +-- platform/interaction/skills/roles/skill-c/meta/plasma.yaml -- +plasma: + version: ver1 + +-- platform/interaction/skills/roles/skill-c/tasks/dependencies.yaml -- +- name: include func-b + include_role: + name: interaction.functions.func-b diff --git a/test/testdata/sync/sync_variable_new_file.txtar b/test/testdata/sync/sync_variable_new_file.txtar new file mode 100644 index 0000000..68a3fd5 --- /dev/null +++ b/test/testdata/sync/sync_variable_new_file.txtar @@ -0,0 +1,45 @@ +# Case 8: a new group_vars/vars.yaml file is added. +# The creation commit propagates to all resources that use variables from the new file. +# +# Layout: +# $WORK/platform/ β€” domain (git repo), compose and bump run here + +exec git -C platform init +exec git -C platform config user.email 'dev@example.com' +exec git -C platform config user.name 'Developer' + +env GIT_AUTHOR_DATE='2000-01-01T00:00:00' +env GIT_COMMITTER_DATE='2000-01-01T00:00:00' +exec git -C platform add . +exec git -C platform commit -m 'initial state' + +# Add a new group_vars file with a variable used by skill-a +env GIT_AUTHOR_DATE='2000-01-02T00:00:00' +env GIT_COMMITTER_DATE='2000-01-02T00:00:00' +exec sh -c 'mkdir -p platform/interaction/group_vars/newgroup' +exec sh -c 'printf "new_var:\n value: hello\n" > platform/interaction/group_vars/newgroup/vars.yaml' + +exec git -C platform add . +exec git -C platform commit -m 'add new group_vars file' + +exec sh -c 'cd platform && launchr compose --interactive=false' + +exec sh -c 'cd platform && launchr bump --sync --vault-pass test --keyring-passphrase testpass --playbook-filter=false --hide-progress' + +# skill-a uses new_var via template β†’ gets propagated version from the file-creation commit +grep 'version: ver1-[0-9a-f]{13}' platform/.compose/build/interaction/skills/roles/skill-a/meta/plasma.yaml + +-- platform/plasma-compose.yaml -- +dependencies: [] + +-- platform/interaction/skills/roles/skill-a/meta/plasma.yaml -- +plasma: + version: ver1 + +-- platform/interaction/skills/roles/skill-a/tasks/main.yaml -- +- name: run skill + debug: + msg: hello + +-- platform/interaction/skills/roles/skill-a/templates/config.j2 -- +value: {{ new_var.value }} diff --git a/test/testdata/sync/sync_variable_playbook_filter.txtar b/test/testdata/sync/sync_variable_playbook_filter.txtar new file mode 100644 index 0000000..a4fa995 --- /dev/null +++ b/test/testdata/sync/sync_variable_playbook_filter.txtar @@ -0,0 +1,118 @@ +# Test that --playbook-filter=true filters variable propagation: +# only variables used by resources in playbook trigger propagation. +# +# Layout: +# platform/ β€” domain with skill-e (in playbook), skill-f (NOT in playbook), group_vars +# +# Variable dependencies: +# plain_var β†’ skill-e (via templates/config.j2) β€” skill-e IS in playbook +# vault_secret_var β†’ skill-f (via templates/secret.j2) β€” skill-f is NOT in playbook +# +# Timeline: +# 2000-01-03 plain_var changed β†’ skill-e in playbook β†’ propagated +# 2000-01-02 vault_secret_var changed β†’ skill-f NOT in playbook β†’ skipped +# 2000-01-01 initial state +# +# Expected with --playbook-filter=true: +# skill-e: ver1-{plain_var_commit_hash} (propagated) +# skill-f: ver1 (unchanged β€” vault var filtered out) + +exec git -C platform init +exec git -C platform config user.email 'dev@example.com' +exec git -C platform config user.name 'Developer' + +env GIT_AUTHOR_DATE='2000-01-01T00:00:00' +env GIT_COMMITTER_DATE='2000-01-01T00:00:00' + +exec git -C platform add . +exec git -C platform commit -m 'initial state' + +# Step 1: change vault_secret_var (skill-f depends on it, but skill-f is NOT in playbook) +env GIT_AUTHOR_DATE='2000-01-02T00:00:00' +env GIT_COMMITTER_DATE='2000-01-02T00:00:00' + +cp platform/interaction/group_vars/all/vault_v2.yaml platform/interaction/group_vars/all/vault.yaml + +exec git -C platform add . +exec git -C platform commit -m 'update vault_secret_var' + +# Step 2: change plain_var (skill-e depends on it, skill-e IS in playbook) +env GIT_AUTHOR_DATE='2000-01-03T00:00:00' +env GIT_COMMITTER_DATE='2000-01-03T00:00:00' + +txtproc replace 'value: original' 'value: updated' platform/interaction/group_vars/all/vars.yaml platform/interaction/group_vars/all/vars.yaml + +exec git -C platform add . +exec git -C platform commit -m 'update plain_var' + +exec sh -c 'cd platform && launchr compose --interactive=false' + +exec sh -c 'cd platform && launchr bump --sync --vault-pass test --keyring-passphrase testpass --playbook-filter=true --hide-progress' + +# skill-e: propagated (plain_var is used and skill-e is in playbook) +grep 'version: ver1-[0-9a-f]{13}' platform/.compose/build/interaction/skills/roles/skill-e/meta/plasma.yaml + +# skill-f: NOT propagated (vault_secret_var filtered because skill-f not in playbook) +grep 'version: ver1$' platform/.compose/build/interaction/skills/roles/skill-f/meta/plasma.yaml + +# verify skill-e hash matches plain_var commit +exec sh -c 'git -C platform log --format="%H" -1 HEAD | cut -c1-13 > /tmp/plain_commit.txt' +exec sh -c 'grep -o "[0-9a-f]\{13\}$" platform/.compose/build/interaction/skills/roles/skill-e/meta/plasma.yaml > /tmp/skill_e_hash.txt' +exec sh -c 'diff /tmp/plain_commit.txt /tmp/skill_e_hash.txt' + +-- platform/plasma-compose.yaml -- +dependencies: [] + +-- platform/platform/platform.yaml -- +- import_playbook: ../interaction/interaction.yaml + +-- platform/interaction/interaction.yaml -- +- hosts: platform.interaction + roles: + - interaction.skills.skill-e + +-- platform/interaction/group_vars/all/vars.yaml -- +plain_var: + value: original + +-- platform/interaction/group_vars/all/vault.yaml -- +$ANSIBLE_VAULT;1.1;AES256 +39333862393031373163646433343062383363373233323839613561343163666164646334363331 +6666616163383131343661616463643138393636393465630a333835653363393963633030613030 +66313238353431386237303237623432313264376163633964373063623338363264633730353864 +3537323263643562380a373364643434613933313239326436326134303730623963656362653263 +61643238303033393733303865306463363032663330333366303265333039653661386664643039 +3539336461383032316134663038326262313038393435626663 + +-- platform/interaction/group_vars/all/vault_v2.yaml -- +$ANSIBLE_VAULT;1.1;AES256 +66363731363130386233623435363764633034343762373164646661623163613930396265663330 +3165633862396435386230316464323737303066323637650a653834363866303039313739366232 +66663137653965376636336539646136376435316139613165346161626264346638313239353564 +6562343038623632360a633631653739383363316133313831636666353439633263326532323961 +31353665316636376537623133383363373732383563353462346534363436373334393863303165 +3037303832656561353463396466613533326666343266613035 + +-- platform/interaction/skills/roles/skill-e/meta/plasma.yaml -- +plasma: + version: ver1 + +-- platform/interaction/skills/roles/skill-e/tasks/main.yaml -- +- name: run skill-e + debug: + msg: hello + +-- platform/interaction/skills/roles/skill-e/templates/config.j2 -- +value: {{ plain_var.value }} + +-- platform/interaction/skills/roles/skill-f/meta/plasma.yaml -- +plasma: + version: ver1 + +-- platform/interaction/skills/roles/skill-f/tasks/main.yaml -- +- name: run skill-f + debug: + msg: hello + +-- platform/interaction/skills/roles/skill-f/templates/secret.j2 -- +secret: {{ vault_secret_var.value }} diff --git a/test/testdata/sync/sync_variable_propagation.txtar b/test/testdata/sync/sync_variable_propagation.txtar new file mode 100644 index 0000000..ed4447d --- /dev/null +++ b/test/testdata/sync/sync_variable_propagation.txtar @@ -0,0 +1,48 @@ +# Cases 5-7: a group_vars variable is changed; skill-a uses it via a template. +# No bump needed: after compose + sync, skill-a gets a propagated version based on the variable change commit. +# +# Layout: +# $WORK/platform/ β€” domain (git repo), compose and bump run here + +exec git -C platform init +exec git -C platform config user.email 'dev@example.com' +exec git -C platform config user.name 'Developer' + +env GIT_AUTHOR_DATE='2000-01-01T00:00:00' +env GIT_COMMITTER_DATE='2000-01-01T00:00:00' +exec git -C platform add . +exec git -C platform commit -m 'initial state' + +# Update variable value β€” no bump run, variable changes go straight to sync +env GIT_AUTHOR_DATE='2000-01-02T00:00:00' +env GIT_COMMITTER_DATE='2000-01-02T00:00:00' +txtproc replace 'value: original' 'value: updated' platform/interaction/group_vars/all/vars.yaml platform/interaction/group_vars/all/vars.yaml + +exec git -C platform add . +exec git -C platform commit -m 'update test_var' + +exec sh -c 'cd platform && launchr compose --interactive=false' + +exec sh -c 'cd platform && launchr bump --sync --vault-pass test --keyring-passphrase testpass --playbook-filter=false --hide-progress' + +# skill-a uses test_var via template β†’ gets propagated version +grep 'version: ver1-[0-9a-f]{13}' platform/.compose/build/interaction/skills/roles/skill-a/meta/plasma.yaml + +-- platform/plasma-compose.yaml -- +dependencies: [] + +-- platform/interaction/group_vars/all/vars.yaml -- +test_var: + value: original + +-- platform/interaction/skills/roles/skill-a/meta/plasma.yaml -- +plasma: + version: ver1 + +-- platform/interaction/skills/roles/skill-a/tasks/main.yaml -- +- name: run skill + debug: + msg: hello + +-- platform/interaction/skills/roles/skill-a/templates/config.j2 -- +value: {{ test_var.value }}