package main import ( "fmt" "go/ast" "go/parser" "go/token" "log" "os" "strconv" hcversion "github.com/hashicorp/go-version" "github.com/urfave/cli/v2" ) const flgMode = "mode" const ( modePatch = "patch" modeMinor = "minor" modeMajor = "major" ) const versionSourceFile = "./cmd/lego/zz_gen_version.go" const ( commentRelease = "release" commentDetach = "detach" ) func main() { app := cli.NewApp() app.Name = "lego-releaser" app.Usage = "Lego releaser" app.HelpName = "releaser" app.Commands = []*cli.Command{ { Name: "release", Usage: "Update file for a release", Action: release, Before: func(ctx *cli.Context) error { mode := ctx.String("mode") switch mode { case modePatch, modeMinor, modeMajor: return nil default: return fmt.Errorf("invalid mode: %s", mode) } }, Flags: []cli.Flag{ &cli.StringFlag{ Name: flgMode, Aliases: []string{"m"}, Value: modePatch, Usage: fmt.Sprintf("The release mode: %s|%s|%s", modePatch, modeMinor, modeMajor), }, }, }, { Name: "detach", Usage: "Update file post release", Action: detach, }, } err := app.Run(os.Args) if err != nil { log.Fatal(err) } } func release(ctx *cli.Context) error { mode := ctx.String(flgMode) currentVersion, err := readCurrentVersion(versionSourceFile) if err != nil { return fmt.Errorf("read current version: %w", err) } nextVersion, err := bumpVersion(mode, currentVersion) if err != nil { return fmt.Errorf("bump version: %w", err) } err = generate(nextVersion, commentRelease) if err != nil { return err } return nil } func detach(_ *cli.Context) error { currentVersion, err := readCurrentVersion(versionSourceFile) if err != nil { return fmt.Errorf("read current version: %w", err) } v := currentVersion.Core().String() err = generate(v, commentDetach) if err != nil { return err } return nil } func readCurrentVersion(filename string) (*hcversion.Version, error) { fset := token.NewFileSet() file, err := parser.ParseFile(fset, filename, nil, parser.AllErrors) if err != nil { return nil, err } v := visitor{data: make(map[string]string)} ast.Walk(v, file) current, err := hcversion.NewSemver(v.data["defaultVersion"]) if err != nil { return nil, err } return current, nil } type visitor struct { data map[string]string } func (v visitor) Visit(n ast.Node) ast.Visitor { if n == nil { return nil } switch d := n.(type) { case *ast.GenDecl: if d.Tok == token.CONST { for _, spec := range d.Specs { valueSpec, ok := spec.(*ast.ValueSpec) if !ok { continue } if len(valueSpec.Names) != 1 || len(valueSpec.Values) != 1 { continue } va, ok := valueSpec.Values[0].(*ast.BasicLit) if !ok { continue } if va.Kind != token.STRING { continue } s, err := strconv.Unquote(va.Value) if err != nil { continue } v.data[valueSpec.Names[0].String()] = s } } default: // noop } return v } func bumpVersion(mode string, v *hcversion.Version) (string, error) { segments := v.Segments() switch mode { case modePatch: return fmt.Sprintf("%d.%d.%d", segments[0], segments[1], segments[2]+1), nil case modeMinor: return fmt.Sprintf("%d.%d.0", segments[0], segments[1]+1), nil case modeMajor: return fmt.Sprintf("%d.0.0", segments[0]+1), nil default: return "", fmt.Errorf("invalid mode: %s", mode) } }