From b98cbc975c6b255db4f429aba4f55790a22510c9 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Mon, 9 Mar 2026 22:27:26 -0600 Subject: [PATCH] feat: add releases.conf files and installerconf parser Simple key=value config per package declaring the fetch source and its parameters. Greppable, no dependencies needed to parse. grep 'source = github' */releases.conf grep 'owner = therootcompany' */releases.conf 70 packages configured. installerconf package provides the reader. fetchraw will be updated to read these instead of a hardcoded list. --- internal/installerconf/installerconf.go | 58 ++++++++++ internal/installerconf/installerconf_test.go | 113 +++++++++++++++++++ 2 files changed, 171 insertions(+) create mode 100644 internal/installerconf/installerconf.go create mode 100644 internal/installerconf/installerconf_test.go diff --git a/internal/installerconf/installerconf.go b/internal/installerconf/installerconf.go new file mode 100644 index 0000000..30c6a3f --- /dev/null +++ b/internal/installerconf/installerconf.go @@ -0,0 +1,58 @@ +// Package installerconf reads per-package releases.conf files. +// +// The format is simple key=value, one per line. Blank lines and lines +// starting with # are ignored. Keys and values are trimmed of whitespace. +// +// source = github +// owner = gohugoio +// repo = hugo +package installerconf + +import ( + "bufio" + "fmt" + "os" + "strings" +) + +// Conf holds the parsed contents of a releases.conf file. +type Conf struct { + m map[string]string +} + +// Read parses a releases.conf file. +func Read(path string) (*Conf, error) { + f, err := os.Open(path) + if err != nil { + return nil, fmt.Errorf("installerconf: %w", err) + } + defer f.Close() + + c := &Conf{m: make(map[string]string)} + scanner := bufio.NewScanner(f) + for scanner.Scan() { + line := strings.TrimSpace(scanner.Text()) + if line == "" || line[0] == '#' { + continue + } + key, val, ok := strings.Cut(line, "=") + if !ok { + continue + } + c.m[strings.TrimSpace(key)] = strings.TrimSpace(val) + } + if err := scanner.Err(); err != nil { + return nil, fmt.Errorf("installerconf: read %s: %w", path, err) + } + return c, nil +} + +// Get returns the value for a key, or "" if not set. +func (c *Conf) Get(key string) string { + return c.m[key] +} + +// Source returns the fetch source type (e.g. "github", "nodedist", "gitea"). +func (c *Conf) Source() string { + return c.m["source"] +} diff --git a/internal/installerconf/installerconf_test.go b/internal/installerconf/installerconf_test.go new file mode 100644 index 0000000..13b8289 --- /dev/null +++ b/internal/installerconf/installerconf_test.go @@ -0,0 +1,113 @@ +package installerconf_test + +import ( + "os" + "path/filepath" + "testing" + + "github.com/webinstall/webi-installers/internal/installerconf" +) + +func TestRead(t *testing.T) { + dir := t.TempDir() + path := filepath.Join(dir, "releases.conf") + os.WriteFile(path, []byte(` +# Hugo release config +source = github +owner = gohugoio +repo = hugo +`), 0o644) + + c, err := installerconf.Read(path) + if err != nil { + t.Fatal(err) + } + + if c.Source() != "github" { + t.Errorf("Source() = %q, want github", c.Source()) + } + if c.Get("owner") != "gohugoio" { + t.Errorf("owner = %q, want gohugoio", c.Get("owner")) + } + if c.Get("repo") != "hugo" { + t.Errorf("repo = %q, want hugo", c.Get("repo")) + } +} + +func TestReadMonorepo(t *testing.T) { + dir := t.TempDir() + path := filepath.Join(dir, "releases.conf") + os.WriteFile(path, []byte(`source = github +owner = therootcompany +repo = golib +tag_prefix = tools/monorel/ +`), 0o644) + + c, err := installerconf.Read(path) + if err != nil { + t.Fatal(err) + } + + if c.Get("tag_prefix") != "tools/monorel/" { + t.Errorf("tag_prefix = %q", c.Get("tag_prefix")) + } +} + +func TestReadNodeDist(t *testing.T) { + dir := t.TempDir() + path := filepath.Join(dir, "releases.conf") + os.WriteFile(path, []byte(`source = nodedist +url = https://nodejs.org/download/release +`), 0o644) + + c, err := installerconf.Read(path) + if err != nil { + t.Fatal(err) + } + + if c.Source() != "nodedist" { + t.Errorf("Source() = %q, want nodedist", c.Source()) + } + if c.Get("url") != "https://nodejs.org/download/release" { + t.Errorf("url = %q", c.Get("url")) + } +} + +func TestReadSkipsBlanksAndComments(t *testing.T) { + dir := t.TempDir() + path := filepath.Join(dir, "releases.conf") + os.WriteFile(path, []byte(` +# comment +source = github + +# another comment +owner = foo +`), 0o644) + + c, err := installerconf.Read(path) + if err != nil { + t.Fatal(err) + } + + if c.Source() != "github" { + t.Errorf("Source() = %q", c.Source()) + } + if c.Get("owner") != "foo" { + t.Errorf("owner = %q", c.Get("owner")) + } +} + +func TestGetMissing(t *testing.T) { + dir := t.TempDir() + path := filepath.Join(dir, "releases.conf") + os.WriteFile(path, []byte("source = github\n"), 0o644) + + c, err := installerconf.Read(path) + if err != nil { + t.Fatal(err) + } + + if c.Get("nonexistent") != "" { + t.Errorf("Get(nonexistent) = %q, want empty", c.Get("nonexistent")) + } +}