diff --git a/ansible/executor.go b/ansible/executor.go index c13591e..08ea851 100644 --- a/ansible/executor.go +++ b/ansible/executor.go @@ -590,7 +590,7 @@ func (e *Executor) gatherFacts(ctx context.Context, host string, play *Play) err // OS info stdout, _, _, _ = client.Run(ctx, "cat /etc/os-release 2>/dev/null | grep -E '^(ID|VERSION_ID)=' | head -2") - for _, line := range strings.Split(stdout, "\n") { + for line := range strings.SplitSeq(stdout, "\n") { if strings.HasPrefix(line, "ID=") { facts.Distribution = strings.Trim(strings.TrimPrefix(line, "ID="), "\"") } diff --git a/ansible/modules.go b/ansible/modules.go index 6819cf8..6082aaa 100644 --- a/ansible/modules.go +++ b/ansible/modules.go @@ -970,7 +970,7 @@ func ctxSleep(ctx context.Context, seconds int) <-chan struct{} { func sleepChan(seconds int) <-chan struct{} { ch := make(chan struct{}) go func() { - for i := 0; i < seconds; i++ { + for range seconds { select { case <-ch: return diff --git a/ansible/parser.go b/ansible/parser.go index b050c6e..1956ac6 100644 --- a/ansible/parser.go +++ b/ansible/parser.go @@ -2,8 +2,11 @@ package ansible import ( "fmt" + "iter" + "maps" "os" "path/filepath" + "slices" "strings" "forge.lthn.ai/core/go/pkg/log" @@ -46,6 +49,21 @@ func (p *Parser) ParsePlaybook(path string) ([]Play, error) { return plays, nil } +// ParsePlaybookIter returns an iterator for plays in an Ansible playbook file. +func (p *Parser) ParsePlaybookIter(path string) (iter.Seq[Play], error) { + plays, err := p.ParsePlaybook(path) + if err != nil { + return nil, err + } + return func(yield func(Play) bool) { + for _, play := range plays { + if !yield(play) { + return + } + } + }, nil +} + // ParseInventory parses an Ansible inventory file. func (p *Parser) ParseInventory(path string) (*Inventory, error) { data, err := os.ReadFile(path) @@ -82,6 +100,21 @@ func (p *Parser) ParseTasks(path string) ([]Task, error) { return tasks, nil } +// ParseTasksIter returns an iterator for tasks in a tasks file. +func (p *Parser) ParseTasksIter(path string) (iter.Seq[Task], error) { + tasks, err := p.ParseTasks(path) + if err != nil { + return nil, err + } + return func(yield func(Task) bool) { + for _, task := range tasks { + if !yield(task) { + return + } + } + }, nil +} + // ParseRole parses a role and returns its tasks. func (p *Parser) ParseRole(name string, tasksFrom string) ([]Task, error) { if tasksFrom == "" { @@ -319,6 +352,18 @@ func GetHosts(inv *Inventory, pattern string) []string { return nil } +// GetHostsIter returns an iterator for hosts matching a pattern from inventory. +func GetHostsIter(inv *Inventory, pattern string) iter.Seq[string] { + hosts := GetHosts(inv, pattern) + return func(yield func(string) bool) { + for _, host := range hosts { + if !yield(host) { + return + } + } + } +} + func getAllHosts(group *InventoryGroup) []string { if group == nil { return nil @@ -334,6 +379,33 @@ func getAllHosts(group *InventoryGroup) []string { return hosts } +// AllHostsIter returns an iterator for all hosts in an inventory group. +func AllHostsIter(group *InventoryGroup) iter.Seq[string] { + return func(yield func(string) bool) { + if group == nil { + return + } + // Sort keys for deterministic iteration + keys := slices.Sorted(maps.Keys(group.Hosts)) + for _, name := range keys { + if !yield(name) { + return + } + } + + // Sort children keys for deterministic iteration + childKeys := slices.Sorted(maps.Keys(group.Children)) + for _, name := range childKeys { + child := group.Children[name] + for host := range AllHostsIter(child) { + if !yield(host) { + return + } + } + } + } +} + func getGroupHosts(group *InventoryGroup, name string) []string { if group == nil { return nil diff --git a/build/config.go b/build/config.go index 3dd5ab0..b2dd3e7 100644 --- a/build/config.go +++ b/build/config.go @@ -4,6 +4,7 @@ package build import ( "fmt" + "iter" "os" "path/filepath" @@ -159,11 +160,22 @@ func ConfigExists(fs io.Medium, dir string) bool { return fileExists(fs, ConfigPath(dir)) } +// TargetsIter returns an iterator for the build targets. +func (cfg *BuildConfig) TargetsIter() iter.Seq[TargetConfig] { + return func(yield func(TargetConfig) bool) { + for _, t := range cfg.Targets { + if !yield(t) { + return + } + } + } +} + // ToTargets converts TargetConfig slice to Target slice for use with builders. func (cfg *BuildConfig) ToTargets() []Target { targets := make([]Target, len(cfg.Targets)) for i, t := range cfg.Targets { - targets[i] = Target(t) + targets[i] = Target{OS: t.OS, Arch: t.Arch} } return targets } diff --git a/cmd/dev/cmd_health.go b/cmd/dev/cmd_health.go index 869e5fc..1eda3b1 100644 --- a/cmd/dev/cmd_health.go +++ b/cmd/dev/cmd_health.go @@ -1,9 +1,10 @@ package dev import ( + "cmp" "context" "fmt" - "sort" + "slices" "strings" "forge.lthn.ai/core/cli/pkg/cli" @@ -66,8 +67,8 @@ func runHealth(registryPath string, verbose bool) error { }) // Sort for consistent verbose output - sort.Slice(statuses, func(i, j int) bool { - return statuses[i].Name < statuses[j].Name + slices.SortFunc(statuses, func(a, b git.RepoStatus) int { + return cmp.Compare(a.Name, b.Name) }) // Aggregate stats @@ -162,14 +163,7 @@ func formatRepoList(reposList []string) string { } func joinRepos(reposList []string) string { - result := "" - for i, r := range reposList { - if i > 0 { - result += ", " - } - result += r - } - return result + return strings.Join(reposList, ", ") } func statusPart(count int, label string, style *cli.AnsiStyle) string { diff --git a/cmd/dev/cmd_issues.go b/cmd/dev/cmd_issues.go index 726e0f6..20bc169 100644 --- a/cmd/dev/cmd_issues.go +++ b/cmd/dev/cmd_issues.go @@ -4,7 +4,7 @@ import ( "encoding/json" "errors" "os/exec" - "sort" + "slices" "strings" "time" @@ -107,8 +107,8 @@ func runIssues(registryPath string, limit int, assignee string) error { cli.Print("\033[2K\r") // Clear progress line // Sort by created date (newest first) - sort.Slice(allIssues, func(i, j int) bool { - return allIssues[i].CreatedAt.After(allIssues[j].CreatedAt) + slices.SortFunc(allIssues, func(a, b GitHubIssue) int { + return b.CreatedAt.Compare(a.CreatedAt) }) // Print issues diff --git a/cmd/dev/cmd_reviews.go b/cmd/dev/cmd_reviews.go index 4802e5a..8a1f3bb 100644 --- a/cmd/dev/cmd_reviews.go +++ b/cmd/dev/cmd_reviews.go @@ -4,7 +4,7 @@ import ( "encoding/json" "errors" "os/exec" - "sort" + "slices" "strings" "time" @@ -111,14 +111,17 @@ func runReviews(registryPath string, author string, showAll bool) error { cli.Print("\033[2K\r") // Clear progress line // Sort: pending review first, then by date - sort.Slice(allPRs, func(i, j int) bool { + slices.SortFunc(allPRs, func(a, b GitHubPR) int { // Pending reviews come first - iPending := allPRs[i].ReviewDecision == "" || allPRs[i].ReviewDecision == "REVIEW_REQUIRED" - jPending := allPRs[j].ReviewDecision == "" || allPRs[j].ReviewDecision == "REVIEW_REQUIRED" - if iPending != jPending { - return iPending + aPending := a.ReviewDecision == "" || a.ReviewDecision == "REVIEW_REQUIRED" + bPending := b.ReviewDecision == "" || b.ReviewDecision == "REVIEW_REQUIRED" + if aPending != bPending { + if aPending { + return -1 + } + return 1 } - return allPRs[i].CreatedAt.After(allPRs[j].CreatedAt) + return b.CreatedAt.Compare(a.CreatedAt) }) // Print PRs diff --git a/cmd/dev/cmd_work.go b/cmd/dev/cmd_work.go index 5b970c8..e602999 100644 --- a/cmd/dev/cmd_work.go +++ b/cmd/dev/cmd_work.go @@ -1,14 +1,15 @@ package dev import ( + "cmp" "context" "os" "os/exec" - "sort" + "slices" "strings" - "forge.lthn.ai/core/go-agentic" "forge.lthn.ai/core/cli/pkg/cli" + "forge.lthn.ai/core/go-agentic" "forge.lthn.ai/core/go-scm/git" "forge.lthn.ai/core/go/pkg/i18n" ) @@ -94,8 +95,8 @@ func runWork(registryPath string, statusOnly, autoCommit bool) error { statuses := result.([]git.RepoStatus) // Sort by repo name for consistent output - sort.Slice(statuses, func(i, j int) bool { - return statuses[i].Name < statuses[j].Name + slices.SortFunc(statuses, func(a, b git.RepoStatus) int { + return cmp.Compare(a.Name, b.Name) }) // Display status table diff --git a/cmd/dev/cmd_workflow.go b/cmd/dev/cmd_workflow.go index dd66979..18dae0e 100644 --- a/cmd/dev/cmd_workflow.go +++ b/cmd/dev/cmd_workflow.go @@ -1,13 +1,16 @@ package dev import ( + "cmp" + "maps" "path/filepath" - "sort" + "slices" "strings" "forge.lthn.ai/core/cli/pkg/cli" "forge.lthn.ai/core/go/pkg/i18n" "forge.lthn.ai/core/go/pkg/io" + "forge.lthn.ai/core/go/pkg/repos" ) // Workflow command flags @@ -79,8 +82,8 @@ func runWorkflowList(registryPath string) error { } // Sort repos by name for consistent output - sort.Slice(repoList, func(i, j int) bool { - return repoList[i].Name < repoList[j].Name + slices.SortFunc(repoList, func(a, b *repos.Repo) int { + return cmp.Compare(a.Name, b.Name) }) // Collect all unique workflow files across all repos @@ -97,11 +100,7 @@ func runWorkflowList(registryPath string) error { } // Sort workflow names - var workflowNames []string - for wf := range workflowSet { - workflowNames = append(workflowNames, wf) - } - sort.Strings(workflowNames) + workflowNames := slices.Sorted(maps.Keys(workflowSet)) if len(workflowNames) == 0 { cli.Text(i18n.T("cmd.dev.workflow.no_workflows")) @@ -168,8 +167,8 @@ func runWorkflowSync(registryPath string, workflowFile string, dryRun bool) erro } // Sort repos by name for consistent output - sort.Slice(repoList, func(i, j int) bool { - return repoList[i].Name < repoList[j].Name + slices.SortFunc(repoList, func(a, b *repos.Repo) int { + return cmp.Compare(a.Name, b.Name) }) if dryRun { diff --git a/cmd/dev/service.go b/cmd/dev/service.go index a2fef35..f2eeccd 100644 --- a/cmd/dev/service.go +++ b/cmd/dev/service.go @@ -1,14 +1,15 @@ package dev import ( + "cmp" "context" - "sort" + "slices" "strings" - "forge.lthn.ai/core/go-agentic" "forge.lthn.ai/core/cli/pkg/cli" - "forge.lthn.ai/core/go/pkg/framework" + "forge.lthn.ai/core/go-agentic" "forge.lthn.ai/core/go-scm/git" + "forge.lthn.ai/core/go/pkg/framework" ) // Tasks for dev service @@ -90,8 +91,8 @@ func (s *Service) runWork(task TaskWork) error { statuses := result.([]git.RepoStatus) // Sort by name - sort.Slice(statuses, func(i, j int) bool { - return statuses[i].Name < statuses[j].Name + slices.SortFunc(statuses, func(a, b git.RepoStatus) int { + return cmp.Compare(a.Name, b.Name) }) // Display status table @@ -234,8 +235,8 @@ func (s *Service) runStatus(task TaskStatus) error { } statuses := result.([]git.RepoStatus) - sort.Slice(statuses, func(i, j int) bool { - return statuses[i].Name < statuses[j].Name + slices.SortFunc(statuses, func(a, b git.RepoStatus) int { + return cmp.Compare(a.Name, b.Name) }) s.printStatusTable(statuses) diff --git a/cmd/monitor/cmd_monitor.go b/cmd/monitor/cmd_monitor.go index 055fa21..e2c387b 100644 --- a/cmd/monitor/cmd_monitor.go +++ b/cmd/monitor/cmd_monitor.go @@ -10,10 +10,12 @@ package monitor import ( + "cmp" "encoding/json" "fmt" + "maps" "os/exec" - "sort" + "slices" "strings" "forge.lthn.ai/core/cli/pkg/cli" @@ -434,13 +436,11 @@ func sortBySeverity(findings []Finding) { "low": 3, } - sort.Slice(findings, func(i, j int) bool { - oi := severityOrder[findings[i].Severity] - oj := severityOrder[findings[j].Severity] - if oi != oj { - return oi < oj - } - return findings[i].RepoName < findings[j].RepoName + slices.SortFunc(findings, func(a, b Finding) int { + return cmp.Or( + cmp.Compare(severityOrder[a.Severity], severityOrder[b.Severity]), + cmp.Compare(a.RepoName, b.RepoName), + ) }) } @@ -491,11 +491,7 @@ func outputTable(findings []Finding) error { } // Sort repos for consistent output - repoNames := make([]string, 0, len(byRepo)) - for repo := range byRepo { - repoNames = append(repoNames, repo) - } - sort.Strings(repoNames) + repoNames := slices.Sorted(maps.Keys(byRepo)) // Print by repo for _, repo := range repoNames { diff --git a/cmd/qa/cmd_docblock.go b/cmd/qa/cmd_docblock.go index fd66157..fba4df2 100644 --- a/cmd/qa/cmd_docblock.go +++ b/cmd/qa/cmd_docblock.go @@ -8,6 +8,7 @@ package qa import ( + "cmp" "encoding/json" "fmt" "go/ast" @@ -15,7 +16,7 @@ import ( "go/token" "os" "path/filepath" - "sort" + "slices" "strings" "forge.lthn.ai/core/cli/pkg/cli" @@ -92,11 +93,11 @@ func RunDocblockCheck(paths []string, threshold float64, verbose, jsonOutput boo } // Sort missing by file then line - sort.Slice(result.Missing, func(i, j int) bool { - if result.Missing[i].File != result.Missing[j].File { - return result.Missing[i].File < result.Missing[j].File - } - return result.Missing[i].Line < result.Missing[j].Line + slices.SortFunc(result.Missing, func(a, b MissingDocblock) int { + return cmp.Or( + cmp.Compare(a.File, b.File), + cmp.Compare(a.Line, b.Line), + ) }) // Print result diff --git a/cmd/qa/cmd_health.go b/cmd/qa/cmd_health.go index 93c28cb..37451de 100644 --- a/cmd/qa/cmd_health.go +++ b/cmd/qa/cmd_health.go @@ -7,9 +7,10 @@ package qa import ( + "cmp" "encoding/json" "os/exec" - "sort" + "slices" "strings" "forge.lthn.ai/core/cli/pkg/cli" @@ -99,8 +100,8 @@ func runHealth() error { cli.Print("\033[2K\r") // Clear progress // Sort: problems first, then passing - sort.Slice(healthResults, func(i, j int) bool { - return healthPriority(healthResults[i].Status) < healthPriority(healthResults[j].Status) + slices.SortFunc(healthResults, func(a, b RepoHealth) int { + return cmp.Compare(healthPriority(a.Status), healthPriority(b.Status)) }) // Filter if --problems flag diff --git a/cmd/qa/cmd_issues.go b/cmd/qa/cmd_issues.go index e47d25c..06620a6 100644 --- a/cmd/qa/cmd_issues.go +++ b/cmd/qa/cmd_issues.go @@ -9,9 +9,10 @@ package qa import ( + "cmp" "encoding/json" "os/exec" - "sort" + "slices" "strings" "time" @@ -203,8 +204,8 @@ func categoriseIssues(issues []Issue) map[string][]Issue { // Sort each category by priority for cat := range result { - sort.Slice(result[cat], func(i, j int) bool { - return result[cat][i].Priority < result[cat][j].Priority + slices.SortFunc(result[cat], func(a, b Issue) int { + return cmp.Compare(a.Priority, b.Priority) }) } @@ -392,10 +393,3 @@ func printTriagedIssue(issue Issue) { cli.Print(" %s %s\n", dimStyle.Render("->"), issue.ActionHint) } } - -func min(a, b int) int { - if a < b { - return a - } - return b -} diff --git a/cmd/setup/cmd_wizard.go b/cmd/setup/cmd_wizard.go index c676cac..35e747a 100644 --- a/cmd/setup/cmd_wizard.go +++ b/cmd/setup/cmd_wizard.go @@ -2,9 +2,10 @@ package setup import ( + "cmp" "fmt" "os" - "sort" + "slices" "forge.lthn.ai/core/cli/pkg/cli" "forge.lthn.ai/core/go/pkg/i18n" @@ -43,8 +44,8 @@ func runPackageWizard(reg *repos.Registry, preselectedTypes []string) ([]string, var options []string // Sort by name - sort.Slice(allRepos, func(i, j int) bool { - return allRepos[i].Name < allRepos[j].Name + slices.SortFunc(allRepos, func(a, b *repos.Repo) int { + return cmp.Compare(a.Name, b.Name) }) for _, repo := range allRepos { diff --git a/container/templates.go b/container/templates.go index 7c16c37..011c269 100644 --- a/container/templates.go +++ b/container/templates.go @@ -3,9 +3,12 @@ package container import ( "embed" "fmt" + "iter" + "maps" "os" "path/filepath" "regexp" + "slices" "strings" "forge.lthn.ai/core/go/pkg/io" @@ -42,17 +45,29 @@ var builtinTemplates = []Template{ // It combines embedded templates with any templates found in the user's // .core/linuxkit directory. func ListTemplates() []Template { - templates := make([]Template, len(builtinTemplates)) - copy(templates, builtinTemplates) + return slices.Collect(ListTemplatesIter()) +} - // Check for user templates in .core/linuxkit/ - userTemplatesDir := getUserTemplatesDir() - if userTemplatesDir != "" { - userTemplates := scanUserTemplates(userTemplatesDir) - templates = append(templates, userTemplates...) +// ListTemplatesIter returns an iterator for all available LinuxKit templates. +func ListTemplatesIter() iter.Seq[Template] { + return func(yield func(Template) bool) { + // Yield builtin templates + for _, t := range builtinTemplates { + if !yield(t) { + return + } + } + + // Check for user templates in .core/linuxkit/ + userTemplatesDir := getUserTemplatesDir() + if userTemplatesDir != "" { + for _, t := range scanUserTemplates(userTemplatesDir) { + if !yield(t) { + return + } + } + } } - - return templates } // GetTemplate returns the content of a template by name. @@ -182,9 +197,7 @@ func ExtractVariables(content string) (required []string, optional map[string]st } // Convert set to slice - for v := range requiredSet { - required = append(required, v) - } + required = slices.Sorted(maps.Keys(requiredSet)) return required, optional } diff --git a/devkit/complexity_test.go b/devkit/complexity_test.go index 0b6bbaa..c95aade 100644 --- a/devkit/complexity_test.go +++ b/devkit/complexity_test.go @@ -55,7 +55,7 @@ func loopy(items []int) int { for _, v := range items { total += v } - for i := 0; i < 10; i++ { + for i := range 10 { total += i } return total @@ -146,8 +146,8 @@ func monster(x, y, z int) int { } else if x < -10 { result = 4 } - for i := 0; i < x; i++ { - for j := 0; j < y; j++ { + for i := range x { + for j := range y { if i > j && j > 0 { result += i } else if i == j || i < 0 { diff --git a/devkit/devkit.go b/devkit/devkit.go index a7dec8d..d7866ee 100644 --- a/devkit/devkit.go +++ b/devkit/devkit.go @@ -161,7 +161,7 @@ func (t *Toolkit) FindTODOs(dir string) ([]TODO, error) { var todos []TODO re := regexp.MustCompile(pattern) - for _, line := range strings.Split(strings.TrimSpace(stdout), "\n") { + for line := range strings.SplitSeq(strings.TrimSpace(stdout), "\n") { if line == "" { continue } @@ -276,7 +276,7 @@ func (t *Toolkit) UncommittedFiles() ([]string, error) { return nil, fmt.Errorf("git status failed: %s\n%s", err, stderr) } var files []string - for _, line := range strings.Split(strings.TrimSpace(stdout), "\n") { + for line := range strings.SplitSeq(strings.TrimSpace(stdout), "\n") { if len(line) > 3 { files = append(files, strings.TrimSpace(line[3:])) } @@ -295,7 +295,7 @@ func (t *Toolkit) Lint(pkg string) ([]Finding, error) { } var findings []Finding - for _, line := range strings.Split(strings.TrimSpace(stderr), "\n") { + for line := range strings.SplitSeq(strings.TrimSpace(stderr), "\n") { if line == "" { continue } @@ -325,7 +325,7 @@ func (t *Toolkit) ScanSecrets(dir string) ([]SecretLeak, error) { } var leaks []SecretLeak - for _, line := range strings.Split(strings.TrimSpace(stdout), "\n") { + for line := range strings.SplitSeq(strings.TrimSpace(stdout), "\n") { if line == "" || strings.HasPrefix(line, "RuleID") { continue } @@ -374,7 +374,7 @@ func (t *Toolkit) TestCount(pkg string) (int, error) { return 0, fmt.Errorf("go test -list failed: %s\n%s", err, stderr) } count := 0 - for _, line := range strings.Split(strings.TrimSpace(stdout), "\n") { + for line := range strings.SplitSeq(strings.TrimSpace(stdout), "\n") { if strings.HasPrefix(line, "Test") || strings.HasPrefix(line, "Benchmark") { count++ } diff --git a/devkit/vulncheck.go b/devkit/vulncheck.go index e5778a1..70fff8a 100644 --- a/devkit/vulncheck.go +++ b/devkit/vulncheck.go @@ -49,8 +49,8 @@ type govulncheckOSV struct { } type govulncheckAffect struct { - Package *govulncheckPkg `json:"package,omitempty"` - Ranges []govulncheckRange `json:"ranges,omitempty"` + Package *govulncheckPkg `json:"package,omitempty"` + Ranges []govulncheckRange `json:"ranges,omitempty"` Severity []govulncheckSeverity `json:"database_specific,omitempty"` } @@ -72,8 +72,8 @@ type govulncheckSeverity struct { } type govulncheckFind struct { - OSV string `json:"osv"` - Trace []govulncheckTrace `json:"trace"` + OSV string `json:"osv"` + Trace []govulncheckTrace `json:"trace"` } type govulncheckTrace struct { @@ -108,7 +108,7 @@ func ParseVulnCheckJSON(stdout, stderr string) (*VulnResult, error) { // Parse line-by-line to gracefully skip malformed entries. // json.Decoder.More() hangs on non-JSON input, so we split first. - for _, line := range strings.Split(stdout, "\n") { + for line := range strings.SplitSeq(stdout, "\n") { line = strings.TrimSpace(line) if line == "" { continue diff --git a/devops/devops.go b/devops/devops.go index 5ca2456..52ea755 100644 --- a/devops/devops.go +++ b/devops/devops.go @@ -154,7 +154,7 @@ func (d *DevOps) Boot(ctx context.Context, opts BootOptions) error { // Wait for SSH to be ready and scan host key // We try for up to 60 seconds as the VM takes a moment to boot var lastErr error - for i := 0; i < 30; i++ { + for range 30 { select { case <-ctx.Done(): return ctx.Err() diff --git a/go.sum b/go.sum index 7984770..f1fd50c 100644 --- a/go.sum +++ b/go.sum @@ -1,14 +1,21 @@ cloud.google.com/go v0.123.0 h1:2NAUJwPR47q+E35uaJeYoNhuNEM9kM8SjgRgdeOJUSE= cloud.google.com/go v0.123.0/go.mod h1:xBoMV08QcqUGuPW65Qfm1o9Y4zKZBpGS+7bImXLTAZU= forge.lthn.ai/core/cli v0.0.1 h1:nqpc4Tv8a4H/ERei+/71DVQxkCFU8HPFJP4120qPXgk= +forge.lthn.ai/core/cli v0.0.1/go.mod h1:xa3Nqw3sUtYYJ1k+1jYul18tgs6sBevCUsGsHJI1hHA= forge.lthn.ai/core/go v0.0.1 h1:ubk4nmkA3treOUNgPS28wKd1jB6cUlEQUV7jDdGa3zM= +forge.lthn.ai/core/go v0.0.1/go.mod h1:59YsnuMaAGQUxIhX68oK2/HnhQJEPWL1iEZhDTrNCbY= forge.lthn.ai/core/go-agentic v0.0.1 h1:GSFIyLaP1nSmagUYtqh8Y0ETwoFRlH9VXZB8gKlYpcY= +forge.lthn.ai/core/go-agentic v0.0.1/go.mod h1:b14WpcpYfg5DQCoqRHcMskQ/2HaOKfCU49EOB++OZOk= forge.lthn.ai/core/go-crypt v0.0.1 h1:fmFc2SJ/VOXDRjkcYoLWfL7lI4HfPJeVS/Na6zHHcvw= +forge.lthn.ai/core/go-crypt v0.0.1/go.mod h1:/j/rUN2ZMV7x1B5BYxH3QdwkgZg0HNBw5XuyFZeyxBY= forge.lthn.ai/core/go-scm v0.0.1 h1:boiH2zK+28ChgM+KTjKFWEwyOt4sbdnkpnmLpo0aUgY= +forge.lthn.ai/core/go-scm v0.0.1/go.mod h1:71zxrM+2nXlTzgnMctnpRmT/ZAFwHVxDj0bPK0pGPnY= forge.lthn.ai/core/go-store v0.1.0 h1:ONO4NfnFVey2QOE5JAZp5dQPI2pxRCHWAtQ+oYFJgGE= +forge.lthn.ai/core/go-store v0.1.0/go.mod h1:FpUlLEX/ebyoxpk96F7ktr0vYvmFtC5Rpi9fi88UVqw= github.com/ProtonMail/go-crypto v1.3.0 h1:ILq8+Sf5If5DCpHQp4PbZdS1J7HDFRXz/+xKBiRGFrw= github.com/ProtonMail/go-crypto v1.3.0/go.mod h1:9whxjD8Rbs29b4XWbB8irEcE8KHMqaR2e7GWU1R+/PE= github.com/Snider/Borg v0.2.0 h1:iCyDhY4WTXi39+FexRwXbn2YpZ2U9FUXVXDZk9xRCXQ= +github.com/Snider/Borg v0.2.0/go.mod h1:TqlKnfRo9okioHbgrZPfWjQsztBV0Nfskz4Om1/vdMY= github.com/TwiN/go-color v1.4.1 h1:mqG0P/KBgHKVqmtL5ye7K0/Gr4l6hTksPgTgMk3mUzc= github.com/TwiN/go-color v1.4.1/go.mod h1:WcPf/jtiW95WBIsEeY1Lc/b8aaWoiqQpu5cf8WFxu+s= github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= @@ -83,6 +90,7 @@ github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLf github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= +github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= github.com/kluctl/go-embed-python v0.0.0-3.13.1-20241219-1 h1:x1cSEj4Ug5mpuZgUHLvUmlc5r//KHFn6iYiRSrRcVy4= github.com/kluctl/go-embed-python v0.0.0-3.13.1-20241219-1/go.mod h1:3ebNU9QBrNpUO+Hj6bHaGpkh5pymDHQ+wwVPHTE4mCE= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= @@ -174,6 +182,7 @@ github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhso github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= github.com/ugorji/go/codec v1.3.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY= +github.com/ugorji/go/codec v1.3.1/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4= github.com/ulikunitz/xz v0.5.15 h1:9DNdB5s+SgV3bQ2ApL10xRc35ck0DuIX/isZvIk+ubY= github.com/ulikunitz/xz v0.5.15/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= github.com/wI2L/jsondiff v0.7.0 h1:1lH1G37GhBPqCfp/lrs91rf/2j3DktX6qYAKZkLuCQQ= @@ -185,6 +194,7 @@ github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJu github.com/yargevad/filepathx v1.0.0 h1:SYcT+N3tYGi+NvazubCNlvgIPbzAk7i7y2dwg3I5FYc= github.com/yargevad/filepathx v1.0.0/go.mod h1:BprfX/gpYNJHJfc35GjRRpVcwWXS89gGulUIU5tK3tA= github.com/zeebo/xxh3 v1.1.0 h1:s7DLGDK45Dyfg7++yxI0khrfwq9661w9EN78eP/UZVs= +github.com/zeebo/xxh3 v1.1.0/go.mod h1:IisAie1LELR4xhVinxWS5+zf1lA4p0MW4T+w+W07F5s= go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= diff --git a/release/config.go b/release/config.go index 18e81c8..2042aa7 100644 --- a/release/config.go +++ b/release/config.go @@ -3,6 +3,7 @@ package release import ( "fmt" + "iter" "os" "path/filepath" @@ -166,6 +167,17 @@ type ChangelogConfig struct { Exclude []string `yaml:"exclude"` } +// PublishersIter returns an iterator for the publishers. +func (c *Config) PublishersIter() iter.Seq[PublisherConfig] { + return func(yield func(PublisherConfig) bool) { + for _, p := range c.Publishers { + if !yield(p) { + return + } + } + } +} + // LoadConfig loads release configuration from the .core/release.yaml file in the given directory. // If the config file does not exist, it returns DefaultConfig(). // Returns an error if the file exists but cannot be parsed. diff --git a/sdk/generators/generator.go b/sdk/generators/generator.go index 3a37f2e..3e65f8a 100644 --- a/sdk/generators/generator.go +++ b/sdk/generators/generator.go @@ -4,8 +4,11 @@ package generators import ( "context" "fmt" + "iter" + "maps" "os" "runtime" + "slices" ) // Options holds common generation options. @@ -62,11 +65,19 @@ func (r *Registry) Register(g Generator) { // Languages returns all registered language identifiers. func (r *Registry) Languages() []string { - langs := make([]string, 0, len(r.generators)) - for lang := range r.generators { - langs = append(langs, lang) + return slices.Collect(r.LanguagesIter()) +} + +// LanguagesIter returns an iterator for all registered language identifiers. +func (r *Registry) LanguagesIter() iter.Seq[string] { + return func(yield func(string) bool) { + // Sort keys for deterministic iteration + for _, lang := range slices.Sorted(maps.Keys(r.generators)) { + if !yield(lang) { + return + } + } } - return langs } // dockerUserArgs returns Docker --user args for the current user on Unix systems.