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,