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