▍ humdrum codex / custard v0.3.0
license AGPL-3.0

feat(release): add brew.source flag + bookmark version parsing

1b76d4e1f1723f52030612e4137ab21b959c2cf3
humdrum <me@humdrum.me> · 2026-06-19 07:57

parent 43ac61a6

2 files changed

internal/release/release.go +36 −2
@@ -25,8 +25,10 @@
 // RepoConfig is the `.custard.yaml` at a repo's root.
 type RepoConfig struct {
 	Brew struct {
-		Enabled bool   `yaml:"enabled"`
-		Package string `yaml:"package"` // go build path, default "."
+		Enabled        bool   `yaml:"enabled"`
+		Package        string `yaml:"package"`         // go build path, default "."
+		Source         string `yaml:"source"`          // "tag" (default) | "bookmark"
+		BookmarkPrefix string `yaml:"bookmark_prefix"`  // bookmark mode; default "release/"
 	} `yaml:"brew"`
 	CI     []string `yaml:"ci"` // check commands, run in order; nonzero exit = fail
 	Deploy struct {
@@ -70,6 +72,38 @@ var semverTag = regexp.MustCompile(`^v\d+\.\d+\.\d+$`)
 
 // IsReleaseTag reports whether a tag name should trigger a release.
 func IsReleaseTag(tag string) bool { return semverTag.MatchString(tag) }
+
+// Release source modes. A repo uses exactly one; switching is an explicit edit.
+const (
+	SourceTag      = "tag"      // releases come from vX.Y.Z git tags (default)
+	SourceBookmark = "bookmark" // releases come from <prefix>vX.Y.Z bookmarks (jj)
+)
+
+// ReleaseSource returns the normalized brew source ("tag" or "bookmark").
+func (c RepoConfig) ReleaseSource() string {
+	if c.Brew.Source == SourceBookmark {
+		return SourceBookmark
+	}
+	return SourceTag
+}
+
+// Prefix returns the normalized bookmark prefix (default "release/").
+func (c RepoConfig) Prefix() string {
+	if c.Brew.BookmarkPrefix == "" {
+		return "release/"
+	}
+	return c.Brew.BookmarkPrefix
+}
+
+// BookmarkVersion extracts the vX.Y.Z version from a release bookmark's short
+// name under prefix. ok=false unless it matches prefix + a semver release tag.
+func BookmarkVersion(short, prefix string) (string, bool) {
+	rest, ok := strings.CutPrefix(short, prefix)
+	if !ok || !IsReleaseTag(rest) {
+		return "", false
+	}
+	return rest, true
+}
 
 // Options drives a single publish.
 type Options struct {
internal/release/release_test.go +41 −0
@@ -0,0 +1,41 @@
+package release
+
+import "testing"
+
+func TestReleaseSourceAndPrefix(t *testing.T) {
+	var c RepoConfig
+	if got := c.ReleaseSource(); got != SourceTag {
+		t.Errorf("default ReleaseSource = %q, want %q", got, SourceTag)
+	}
+	if got := c.Prefix(); got != "release/" {
+		t.Errorf("default Prefix = %q, want release/", got)
+	}
+	c.Brew.Source = "bookmark"
+	c.Brew.BookmarkPrefix = "rel/"
+	if got := c.ReleaseSource(); got != SourceBookmark {
+		t.Errorf("ReleaseSource = %q, want %q", got, SourceBookmark)
+	}
+	if got := c.Prefix(); got != "rel/" {
+		t.Errorf("Prefix = %q, want rel/", got)
+	}
+}
+
+func TestBookmarkVersion(t *testing.T) {
+	cases := []struct {
+		short, prefix, want string
+		ok                  bool
+	}{
+		{"release/v1.2.3", "release/", "v1.2.3", true},
+		{"rel/v0.1.0", "rel/", "v0.1.0", true},
+		{"release/v1.2.3", "rel/", "", false},     // wrong prefix
+		{"release/main", "release/", "", false},   // not semver
+		{"main", "release/", "", false},           // no prefix
+		{"release/v1.2", "release/", "", false},   // not 3-part
+	}
+	for _, c := range cases {
+		got, ok := BookmarkVersion(c.short, c.prefix)
+		if ok != c.ok || got != c.want {
+			t.Errorf("BookmarkVersion(%q,%q) = %q,%v; want %q,%v", c.short, c.prefix, got, ok, c.want, c.ok)
+		}
+	}
+}