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.
This commit is contained in:
AJ ONeal
2026-03-09 22:27:26 -06:00
parent 2c3b21a5ae
commit b98cbc975c
2 changed files with 171 additions and 0 deletions

View File

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

View File

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