refactor: modernise to Go 1.26 — iterators, slices, maps, strings

- Add ParsePlaybookIter, ParseTasksIter, GetHostsIter, AllHostsIter (ansible)
- Add ListTemplatesIter (container), TargetsIter (build), LanguagesIter (sdk)
- Replace sort.Slice with slices.SortFunc across cmd/dev, cmd/qa, cmd/monitor, cmd/setup
- Replace manual map-key-sort with slices.Sorted(maps.Keys(...))
- Replace strings.Split with strings.SplitSeq where result is iterated (devkit)
- Replace range-over-int in complexity_test, ansible/modules, devops
- Remove redundant manual min() in favour of built-in
- 22 files, all tests pass

Co-Authored-By: Gemini <noreply@google.com>
Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
Snider 2026-02-23 06:24:26 +00:00
parent 171becfdd6
commit f9eb54b856
23 changed files with 231 additions and 110 deletions

View file

@ -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="), "\"")
}

View file

@ -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

View file

@ -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

View file

@ -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
}

View file

@ -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 {

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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 {

View file

@ -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)

View file

@ -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 {

View file

@ -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

View file

@ -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

View file

@ -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
}

View file

@ -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 {

View file

@ -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
}

View file

@ -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 {

View file

@ -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++
}

View file

@ -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

View file

@ -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()

10
go.sum
View file

@ -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=

View file

@ -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.

View file

@ -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.