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

fix(release): only auto-publish the highest semver tag to the tap

43ac61a6bf7c2856bdc497403b403ccca92521f4
humdrum <me@humdrum.me> · 2026-06-18 13:02

parent 13fbaa8a

fix(release): only auto-publish the highest semver tag to the tap

The release webhook published whatever tag fired, in chronological order.
An out-of-order tag (e.g. re-tagging v0.1.0 after v0.2.0 already exists)
would rewrite the tap formula to the lower version — and brew treats a
lower version string as a downgrade, so users never upgrade.

Guard publishRelease with gitread.IsHighestVersion: skip unless the fired
tag is the highest semver in the repo. custard never invents a number; it
just refuses to move the formula backwards. Matches the forge header pill,
which already shows the highest semver tag.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

2 files changed

internal/gitread/gitread.go +16 −0
@@ -212,6 +212,22 @@ 	t, _ := s.LatestTag(name)
 	return t.Name
 }
 
+// IsHighestVersion reports whether tag is the highest semver tag in the repo
+// (ties allowed). Used to guard the release webhook against out-of-order tags
+// downgrading the published formula. A non-semver tag is never highest.
+func (s *Store) IsHighestVersion(name, tag string) bool {
+	v, ok := parseSemver(tag)
+	if !ok {
+		return false
+	}
+	latest, have := s.LatestTag(name)
+	if !have {
+		return true
+	}
+	lv, _ := parseSemver(latest.Name)
+	return !semverLess(v, lv) // v >= latest
+}
+
 // firstLine returns the first non-empty line of s, trimmed.
 func firstLine(s string) string {
 	s = strings.TrimSpace(s)
internal/server/webhook.go +7 −0
@@ -98,6 +98,13 @@ 	if !cfg.Brew.Enabled {
 		log.Printf("hook: ignored (not brew-enabled) repo=%s tag=%s", repo, tag)
 		return
 	}
+	// Guard against out-of-order tags: a tag pushed after a higher version
+	// (e.g. re-tagging v0.1.0 once v0.2.0 exists) must not downgrade the
+	// published formula. Only publish when this tag is the repo's highest semver.
+	if !s.store.IsHighestVersion(repo, tag) {
+		log.Printf("hook: ignored (not highest version; %s superseded by a higher tag) repo=%s", tag, repo)
+		return
+	}
 	ver, err := release.Publish(release.Options{
 		Store:     s.store,
 		ReposPath: s.cfg.ReposPath,