mirror of
https://github.com/therootcompany/golib.git
synced 2026-03-02 23:57:59 +00:00
feat(monorel): add subcommand dispatch (release, bump, init)
Restructure monorel to use flag.FlagSet-based subcommand dispatch so that
future subcommands can each carry their own flags cleanly.
monorel release <binary-path>...
Existing behaviour: write .goreleaser.yaml and print a bash release
script. Now a named subcommand; no behaviour change.
monorel bump [-m major|minor|patch] <binary-path>...
Create a new semver git tag at HEAD for each module.
-m defaults to "patch". Using a flag (-m) rather than a positional
argument avoids ambiguity with binary paths that might literally be
named "minor" or "patch".
monorel init <binary-path>...
For each module (in command-line order): write .goreleaser.yaml,
commit it (skipped when file is unchanged), then run bump patch.
This commit is contained in:
parent
9812f52ee9
commit
c55a869b82
@ -1,20 +1,21 @@
|
|||||||
// monorel: Monorepo Release Tool
|
// monorel: Monorepo Release Tool
|
||||||
//
|
//
|
||||||
// Pass any number of paths to Go main packages. monorel walks up from each
|
// Pass any number of paths to Go main packages. monorel walks up from each
|
||||||
// path to find its go.mod (stopping at .git so it never crosses the repo
|
// path to find its go.mod (stopping at .git so it never crosses the repo
|
||||||
// boundary), groups binaries by their module root, writes a .goreleaser.yaml
|
// boundary), groups binaries by their module root, and performs the requested
|
||||||
// for each module, and prints a ready-to-review bash release script.
|
// subcommand.
|
||||||
//
|
//
|
||||||
// Usage:
|
// Subcommands:
|
||||||
//
|
//
|
||||||
// # From within a module directory:
|
// monorel release <binary-path>...
|
||||||
// monorel . # single binary at root
|
// Generate .goreleaser.yaml and print a ready-to-review bash release script.
|
||||||
// monorel ./cmd/foo ./cmd/bar ./cmd/baz # multiple binaries
|
|
||||||
//
|
//
|
||||||
// # From any ancestor directory (e.g. the repo root):
|
// monorel bump [-m major|minor|patch] <binary-path>...
|
||||||
// monorel io/transform/gsheet2csv/cmd/gsheet2csv \
|
// Create a new semver git tag at HEAD for each module (default: patch).
|
||||||
// io/transform/gsheet2csv/cmd/gsheet2tsv \
|
//
|
||||||
// auth/csvauth/cmd/csvauth
|
// monorel init <binary-path>...
|
||||||
|
// Write .goreleaser.yaml, commit it, and run bump patch for each module
|
||||||
|
// (processed in the order their paths appear on the command line).
|
||||||
//
|
//
|
||||||
// Install:
|
// Install:
|
||||||
//
|
//
|
||||||
@ -22,6 +23,7 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
"go/parser"
|
"go/parser"
|
||||||
"go/token"
|
"go/token"
|
||||||
@ -51,30 +53,76 @@ type binary struct {
|
|||||||
|
|
||||||
// moduleGroup is all the binaries that share one module root.
|
// moduleGroup is all the binaries that share one module root.
|
||||||
type moduleGroup struct {
|
type moduleGroup struct {
|
||||||
root string // absolute path to the directory containing go.mod
|
root string // absolute path to the directory containing go.mod
|
||||||
bins []binary
|
bins []binary
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Entry point ────────────────────────────────────────────────────────────
|
// ── Entry point ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
args := os.Args[1:]
|
if len(os.Args) < 2 {
|
||||||
if len(args) == 0 {
|
usage()
|
||||||
fmt.Fprintln(os.Stderr, "usage: monorel <binary-path> [<binary-path>...]")
|
os.Exit(2)
|
||||||
|
}
|
||||||
|
switch os.Args[1] {
|
||||||
|
case "release":
|
||||||
|
runRelease(os.Args[2:])
|
||||||
|
case "bump":
|
||||||
|
runBump(os.Args[2:])
|
||||||
|
case "init":
|
||||||
|
runInit(os.Args[2:])
|
||||||
|
case "help", "--help", "-h":
|
||||||
|
usage()
|
||||||
|
default:
|
||||||
|
fmt.Fprintf(os.Stderr, "monorel: unknown subcommand %q\n", os.Args[1])
|
||||||
|
fmt.Fprintln(os.Stderr, "Run 'monorel help' for usage.")
|
||||||
|
os.Exit(2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func usage() {
|
||||||
|
fmt.Fprintln(os.Stderr, "monorel: Monorepo Release Tool")
|
||||||
|
fmt.Fprintln(os.Stderr, "")
|
||||||
|
fmt.Fprintln(os.Stderr, "Usage:")
|
||||||
|
fmt.Fprintln(os.Stderr, " monorel <subcommand> [options] <binary-path>...")
|
||||||
|
fmt.Fprintln(os.Stderr, "")
|
||||||
|
fmt.Fprintln(os.Stderr, "Subcommands:")
|
||||||
|
fmt.Fprintln(os.Stderr, " release Write .goreleaser.yaml and print a bash release script")
|
||||||
|
fmt.Fprintln(os.Stderr, " bump Create a new semver tag at HEAD (default: patch)")
|
||||||
|
fmt.Fprintln(os.Stderr, " init Write .goreleaser.yaml, commit it, and bump patch")
|
||||||
|
fmt.Fprintln(os.Stderr, "")
|
||||||
|
fmt.Fprintln(os.Stderr, "Each <binary-path> points to a Go main package directory. monorel")
|
||||||
|
fmt.Fprintln(os.Stderr, "walks up from each path to find the module root (go.mod), stopping")
|
||||||
|
fmt.Fprintln(os.Stderr, "at the repository boundary (.git directory).")
|
||||||
|
fmt.Fprintln(os.Stderr, "")
|
||||||
|
fmt.Fprintln(os.Stderr, "Run 'monorel <subcommand> --help' for subcommand-specific usage.")
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Subcommand: release ────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
func runRelease(args []string) {
|
||||||
|
fs := flag.NewFlagSet("monorel release", flag.ExitOnError)
|
||||||
|
fs.Usage = func() {
|
||||||
|
fmt.Fprintln(os.Stderr, "usage: monorel release <binary-path>...")
|
||||||
fmt.Fprintln(os.Stderr, "")
|
fmt.Fprintln(os.Stderr, "")
|
||||||
fmt.Fprintln(os.Stderr, "Each path points to a Go main package (directory).")
|
fmt.Fprintln(os.Stderr, "Writes .goreleaser.yaml next to each module's go.mod and prints a")
|
||||||
fmt.Fprintln(os.Stderr, "The module root (go.mod) is found by walking up from each path,")
|
fmt.Fprintln(os.Stderr, "ready-to-review bash release script to stdout.")
|
||||||
fmt.Fprintln(os.Stderr, "stopping at .git so it never crosses the repository boundary.")
|
|
||||||
fmt.Fprintln(os.Stderr, "")
|
fmt.Fprintln(os.Stderr, "")
|
||||||
fmt.Fprintln(os.Stderr, "Examples:")
|
fmt.Fprintln(os.Stderr, "Examples:")
|
||||||
fmt.Fprintln(os.Stderr, " monorel . # single binary at module root")
|
fmt.Fprintln(os.Stderr, " monorel release . # single binary at module root")
|
||||||
fmt.Fprintln(os.Stderr, " monorel ./cmd/foo ./cmd/bar # multiple binaries, same module")
|
fmt.Fprintln(os.Stderr, " monorel release ./cmd/foo ./cmd/bar # multiple binaries, same module")
|
||||||
fmt.Fprintln(os.Stderr, " monorel io/transform/gsheet2csv/cmd/foo \\ # from the repo root")
|
fmt.Fprintln(os.Stderr, " monorel release auth/csvauth/cmd/csvauth # from repo root")
|
||||||
fmt.Fprintln(os.Stderr, " auth/csvauth/cmd/bar")
|
fmt.Fprintln(os.Stderr, "")
|
||||||
|
fs.PrintDefaults()
|
||||||
|
}
|
||||||
|
_ = fs.Parse(args)
|
||||||
|
binPaths := fs.Args()
|
||||||
|
if len(binPaths) == 0 {
|
||||||
|
fs.Usage()
|
||||||
os.Exit(2)
|
os.Exit(2)
|
||||||
}
|
}
|
||||||
|
|
||||||
groups, err := groupByModule(args)
|
groups, err := groupByModule(binPaths)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fatalf("%v", err)
|
fatalf("%v", err)
|
||||||
}
|
}
|
||||||
@ -93,6 +141,195 @@ func main() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Subcommand: bump ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
func runBump(args []string) {
|
||||||
|
fs := flag.NewFlagSet("monorel bump", flag.ExitOnError)
|
||||||
|
var component string
|
||||||
|
fs.StringVar(&component, "m", "patch", "version component to bump: major, minor, or patch")
|
||||||
|
fs.Usage = func() {
|
||||||
|
fmt.Fprintln(os.Stderr, "usage: monorel bump [-m major|minor|patch] <binary-path>...")
|
||||||
|
fmt.Fprintln(os.Stderr, "")
|
||||||
|
fmt.Fprintln(os.Stderr, "Creates a new semver git tag at HEAD for the module of each binary path.")
|
||||||
|
fmt.Fprintln(os.Stderr, "The tag is created locally; push it with 'git push --tags'.")
|
||||||
|
fmt.Fprintln(os.Stderr, "")
|
||||||
|
fmt.Fprintln(os.Stderr, "Examples:")
|
||||||
|
fmt.Fprintln(os.Stderr, " monorel bump ./cmd/csvauth # bump patch (default)")
|
||||||
|
fmt.Fprintln(os.Stderr, " monorel bump -m minor ./cmd/csvauth # bump minor")
|
||||||
|
fmt.Fprintln(os.Stderr, " monorel bump -m major ./cmd/csvauth # bump major")
|
||||||
|
fmt.Fprintln(os.Stderr, "")
|
||||||
|
fs.PrintDefaults()
|
||||||
|
}
|
||||||
|
_ = fs.Parse(args)
|
||||||
|
|
||||||
|
switch component {
|
||||||
|
case "major", "minor", "patch":
|
||||||
|
// valid
|
||||||
|
default:
|
||||||
|
fmt.Fprintf(os.Stderr, "monorel bump: -m must be major, minor, or patch (got %q)\n", component)
|
||||||
|
os.Exit(2)
|
||||||
|
}
|
||||||
|
|
||||||
|
binPaths := fs.Args()
|
||||||
|
if len(binPaths) == 0 {
|
||||||
|
fs.Usage()
|
||||||
|
os.Exit(2)
|
||||||
|
}
|
||||||
|
|
||||||
|
groups, err := groupByModule(binPaths)
|
||||||
|
if err != nil {
|
||||||
|
fatalf("%v", err)
|
||||||
|
}
|
||||||
|
for _, group := range groups {
|
||||||
|
newTag := bumpModuleTag(group, component)
|
||||||
|
fmt.Fprintf(os.Stderr, "created tag: %s\n", newTag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Subcommand: init ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
func runInit(args []string) {
|
||||||
|
fs := flag.NewFlagSet("monorel init", flag.ExitOnError)
|
||||||
|
fs.Usage = func() {
|
||||||
|
fmt.Fprintln(os.Stderr, "usage: monorel init <binary-path>...")
|
||||||
|
fmt.Fprintln(os.Stderr, "")
|
||||||
|
fmt.Fprintln(os.Stderr, "For each module (in command-line order):")
|
||||||
|
fmt.Fprintln(os.Stderr, " 1. Writes .goreleaser.yaml next to go.mod")
|
||||||
|
fmt.Fprintln(os.Stderr, " 2. Commits it (skipped if file is unchanged)")
|
||||||
|
fmt.Fprintln(os.Stderr, " 3. Creates an initial version tag (equivalent to 'bump patch')")
|
||||||
|
fmt.Fprintln(os.Stderr, "")
|
||||||
|
fs.PrintDefaults()
|
||||||
|
}
|
||||||
|
_ = fs.Parse(args)
|
||||||
|
binPaths := fs.Args()
|
||||||
|
if len(binPaths) == 0 {
|
||||||
|
fs.Usage()
|
||||||
|
os.Exit(2)
|
||||||
|
}
|
||||||
|
|
||||||
|
groups, err := groupByModule(binPaths)
|
||||||
|
if err != nil {
|
||||||
|
fatalf("%v", err)
|
||||||
|
}
|
||||||
|
for _, group := range groups {
|
||||||
|
initModuleGroup(group)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// initModuleGroup writes .goreleaser.yaml, commits it (if changed), and
|
||||||
|
// creates an initial version tag (bump patch) for one module group.
|
||||||
|
func initModuleGroup(group *moduleGroup) {
|
||||||
|
modRoot := group.root
|
||||||
|
bins := group.bins
|
||||||
|
|
||||||
|
prefix := mustRunIn(modRoot, "git", "rev-parse", "--show-prefix")
|
||||||
|
prefix = strings.TrimSuffix(prefix, "/")
|
||||||
|
if prefix == "" {
|
||||||
|
fatalf("%s appears to be the repo root; the module must be in a subdirectory", modRoot)
|
||||||
|
}
|
||||||
|
prefixParts := strings.Split(prefix, "/")
|
||||||
|
projectName := prefixParts[len(prefixParts)-1]
|
||||||
|
|
||||||
|
// 1. Write .goreleaser.yaml.
|
||||||
|
yamlContent := goreleaserYAML(projectName, bins)
|
||||||
|
yamlPath := filepath.Join(modRoot, ".goreleaser.yaml")
|
||||||
|
if err := os.WriteFile(yamlPath, []byte(yamlContent), 0o644); err != nil {
|
||||||
|
fatalf("writing %s: %v", yamlPath, err)
|
||||||
|
}
|
||||||
|
fmt.Fprintf(os.Stderr, "wrote %s\n", yamlPath)
|
||||||
|
|
||||||
|
// 2. Stage and commit if the file changed.
|
||||||
|
mustRunIn(modRoot, "git", "add", ".goreleaser.yaml")
|
||||||
|
if status := runIn(modRoot, "git", "status", "--porcelain", "--", ".goreleaser.yaml"); status != "" {
|
||||||
|
commitMsg := "chore(release): add .goreleaser.yaml for " + projectName
|
||||||
|
mustRunIn(modRoot, "git", "commit", "-m", commitMsg)
|
||||||
|
fmt.Fprintf(os.Stderr, "committed: %s\n", commitMsg)
|
||||||
|
} else {
|
||||||
|
fmt.Fprintf(os.Stderr, "note: .goreleaser.yaml unchanged, skipping commit\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Bump patch.
|
||||||
|
newTag := bumpModuleTag(group, "patch")
|
||||||
|
fmt.Fprintf(os.Stderr, "created tag: %s\n", newTag)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Bump helpers ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
// bumpModuleTag finds the latest stable tag for the module, computes the next
|
||||||
|
// version by bumping the given component (major, minor, or patch), creates the
|
||||||
|
// git tag at HEAD, and returns the new tag name.
|
||||||
|
func bumpModuleTag(group *moduleGroup, component string) string {
|
||||||
|
modRoot := group.root
|
||||||
|
|
||||||
|
prefix := mustRunIn(modRoot, "git", "rev-parse", "--show-prefix")
|
||||||
|
prefix = strings.TrimSuffix(prefix, "/")
|
||||||
|
if prefix == "" {
|
||||||
|
fatalf("%s appears to be the repo root; the module must be in a subdirectory", modRoot)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Collect stable tags only (no pre-release suffix) for this module.
|
||||||
|
rawTags := runIn(modRoot, "git", "tag", "--list", prefix+"/v*")
|
||||||
|
var stableTags []string
|
||||||
|
for _, t := range strings.Split(rawTags, "\n") {
|
||||||
|
t = strings.TrimSpace(t)
|
||||||
|
if t == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
ver := strings.TrimPrefix(t, prefix+"/")
|
||||||
|
if !strings.Contains(ver, "-") { // pre-releases have a "-" in the version
|
||||||
|
stableTags = append(stableTags, t)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sort.Slice(stableTags, func(i, j int) bool {
|
||||||
|
vi := strings.TrimPrefix(stableTags[i], prefix+"/")
|
||||||
|
vj := strings.TrimPrefix(stableTags[j], prefix+"/")
|
||||||
|
return semverLess(vi, vj)
|
||||||
|
})
|
||||||
|
|
||||||
|
var latestStable string
|
||||||
|
if n := len(stableTags); n > 0 {
|
||||||
|
latestStable = stableTags[n-1]
|
||||||
|
}
|
||||||
|
|
||||||
|
newTag := computeBumpTag(prefix, latestStable, component)
|
||||||
|
mustRunIn(modRoot, "git", "tag", newTag)
|
||||||
|
return newTag
|
||||||
|
}
|
||||||
|
|
||||||
|
// computeBumpTag returns the new full tag string for the given bump component,
|
||||||
|
// starting from latestStableTag (empty string = no prior stable tags).
|
||||||
|
func computeBumpTag(prefix, latestStableTag, component string) string {
|
||||||
|
if latestStableTag == "" {
|
||||||
|
switch component {
|
||||||
|
case "major":
|
||||||
|
return prefix + "/v1.0.0"
|
||||||
|
default: // minor, patch
|
||||||
|
return prefix + "/v0.1.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
semver := strings.TrimPrefix(latestStableTag, prefix+"/v")
|
||||||
|
dp := strings.SplitN(semver, ".", 3)
|
||||||
|
for len(dp) < 3 {
|
||||||
|
dp = append(dp, "0")
|
||||||
|
}
|
||||||
|
major, _ := strconv.Atoi(dp[0])
|
||||||
|
minor, _ := strconv.Atoi(dp[1])
|
||||||
|
patch, _ := strconv.Atoi(dp[2])
|
||||||
|
|
||||||
|
switch component {
|
||||||
|
case "major":
|
||||||
|
major++
|
||||||
|
minor, patch = 0, 0
|
||||||
|
case "minor":
|
||||||
|
minor++
|
||||||
|
patch = 0
|
||||||
|
default: // patch
|
||||||
|
patch++
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%s/v%d.%d.%d", prefix, major, minor, patch)
|
||||||
|
}
|
||||||
|
|
||||||
// ── Module discovery ───────────────────────────────────────────────────────
|
// ── Module discovery ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
// findModuleRoot walks upward from absDir looking for a directory that
|
// findModuleRoot walks upward from absDir looking for a directory that
|
||||||
@ -191,8 +428,8 @@ func groupByModule(args []string) ([]*moduleGroup, error) {
|
|||||||
name = filepath.Base(modRoot) // e.g. "tcpfwd" or "gsheet2csv"
|
name = filepath.Base(modRoot) // e.g. "tcpfwd" or "gsheet2csv"
|
||||||
mainPath = "."
|
mainPath = "."
|
||||||
} else {
|
} else {
|
||||||
name = filepath.Base(rel) // last component
|
name = filepath.Base(rel) // last component
|
||||||
mainPath = "./" + rel // e.g. "./cmd/gsheet2csv"
|
mainPath = "./" + rel // e.g. "./cmd/gsheet2csv"
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, ok := groupMap[modRoot]; !ok {
|
if _, ok := groupMap[modRoot]; !ok {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user