Skip to content
Open
5 changes: 2 additions & 3 deletions cmd/gorepomod/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ The argument _{version}_ defaults to recent version of _{module}_.
_{version}_ should be in semver form, e.g. `v1.2.3`.


#### `gorepomod release {module} [patch|minor|major]`
#### `gorepomod release {module} [patch|minor|major] --release-branch {branch}`

Computes a new version for the module, tags the repo
with that version, and pushes the tag to the remote.
Expand All @@ -82,7 +82,7 @@ If the existing version is _v1.2.7_, then the new version will be:

After establishing the version, the command looks for a branch named

> _release-{module}/-v{major}.{minor}_
> _release-v{major}.{minor}.{patch}_

If the branch doesn't exist, the command creates it and pushes it to the remote.

Expand All @@ -107,4 +107,3 @@ imported the module at the given tag, then don't do this,
because it will confuse module caches.

Do a new patch release instead.

49 changes: 36 additions & 13 deletions cmd/gorepomod/internal/arguments/args.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,22 +6,24 @@ package arguments
import (
"fmt"
"os"
"strings"

"sigs.k8s.io/kustomize/cmd/gorepomod/internal/misc"
"sigs.k8s.io/kustomize/cmd/gorepomod/internal/semver"
"sigs.k8s.io/kustomize/cmd/gorepomod/internal/utils"
)

const (
doItFlag = "--doIt"
localFlag = "--local"
cmdPin = "pin"
cmdUnPin = "unpin"
cmdTidy = "tidy"
cmdList = "list"
cmdRelease = "release"
cmdUnRelease = "unrelease"
cmdDebug = "debug"
doItFlag = "--doIt"
localFlag = "--local"
releaseBranchFlag = "--release-branch"
cmdPin = "pin"
cmdUnPin = "unpin"
cmdTidy = "tidy"
cmdList = "list"
cmdRelease = "release"
cmdUnRelease = "unrelease"
cmdDebug = "debug"
)

var (
Expand Down Expand Up @@ -64,6 +66,7 @@ type Args struct {
conditionalModule misc.ModuleShortName
version semver.SemVer
bump semver.SvBump
releaseBranch string
doIt bool
localFlag bool
}
Expand All @@ -88,6 +91,10 @@ func (a *Args) Version() semver.SemVer {
return a.version
}

func (a *Args) ReleaseBranch() string {
return a.releaseBranch
}

func (a *Args) ModuleName() misc.ModuleShortName {
return a.moduleName
}
Expand All @@ -113,9 +120,10 @@ func (a *Args) LocalFlag() bool {
}

type myArgs struct {
args []string
doIt bool
localFlag bool
args []string
doIt bool
localFlag bool
releaseBranch string
}

func (a *myArgs) next() (result string) {
Expand All @@ -138,6 +146,8 @@ func newArgs() *myArgs {
result.doIt = true
} else if a == localFlag {
result.localFlag = true
} else if strings.HasPrefix(a, releaseBranchFlag+"=") {
result.releaseBranch = strings.TrimPrefix(a, releaseBranchFlag+"=")
} else {
result.args = append(result.args, a)
}
Expand All @@ -150,6 +160,7 @@ func Parse() (result *Args, err error) {
clArgs := newArgs()
result.doIt = clArgs.doIt
result.localFlag = clArgs.localFlag
result.releaseBranch = clArgs.releaseBranch

result.moduleName = misc.ModuleUnknown
result.conditionalModule = misc.ModuleUnknown
Expand Down Expand Up @@ -191,7 +202,7 @@ func Parse() (result *Args, err error) {
}
result.moduleName = misc.ModuleShortName(clArgs.next())
bump := "patch"
if clArgs.more() {
if clArgs.more() && clArgs.args[0] != releaseBranchFlag {
bump = clArgs.next()
}
switch bump {
Expand All @@ -205,6 +216,18 @@ func Parse() (result *Args, err error) {
return nil, fmt.Errorf(
"unknown bump %s; specify one of 'major', 'minor' or 'patch'", bump)
}
if clArgs.more() {
if result.releaseBranch != "" {
return nil, fmt.Errorf("release branch specified more than once")
}
if clArgs.next() != releaseBranchFlag {
return nil, fmt.Errorf("unknown extra args: %v", clArgs.args)
}
if !clArgs.more() {
return nil, fmt.Errorf("%s requires a branch name", releaseBranchFlag)
}
result.releaseBranch = clArgs.next()
}
result.cmd = Release
case cmdUnRelease:
if !clArgs.more() {
Expand Down
38 changes: 38 additions & 0 deletions cmd/gorepomod/internal/arguments/args_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// Copyright 2026 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0

package arguments

import (
"os"
"testing"

"sigs.k8s.io/kustomize/cmd/gorepomod/internal/semver"
)

func TestParseReleaseBranchFlag(t *testing.T) {
oldArgs := os.Args
t.Cleanup(func() {
os.Args = oldArgs
})

os.Args = []string{
"gorepomod",
"release",
"kustomize",
"minor",
"--release-branch",
"release-v5.8.2",
}

args, err := Parse()
if err != nil {
t.Fatalf("Parse() error = %v", err)
}
if got, want := args.ReleaseBranch(), "release-v5.8.2"; got != want {
t.Fatalf("ReleaseBranch() = %q, want %q", got, want)
}
if got, want := args.Bump(), semver.Minor; got != want {
t.Fatalf("Bump() = %v, want %v", got, want)
}
}
6 changes: 4 additions & 2 deletions cmd/gorepomod/internal/git/runner.go
Original file line number Diff line number Diff line change
Expand Up @@ -303,8 +303,10 @@ func (gr *Runner) CheckoutReleaseBranch(
return nil
}
gr.comment("creating branch")
// The branch doesn't exist remotely. Create or reset it locally.
out, err := gr.run(noHarmDone, "checkout", "-B", branch)
// The branch doesn't exist remotely. Create it from remote main without
// switching the working tree to main first.
base := strings.Join([]string{string(remote), mainBranch}, pathSep)
out, err := gr.run(noHarmDone, "checkout", "-B", branch, base)
if err != nil {
return err
}
Expand Down
47 changes: 18 additions & 29 deletions cmd/gorepomod/internal/repo/manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,16 +80,6 @@ func (mgr *Manager) hasUnPinnedDeps(m misc.LaModule) string {
}

func (mgr *Manager) List() error {
// Auto-update local tags
gr := git.NewQuiet(mgr.AbsPath(), false, false)
for _, module := range mgr.modules {
releaseBranch := fmt.Sprintf("release-%s", module.ShortName())
_, err := gr.GetLatestTag(releaseBranch)
if err != nil {
return fmt.Errorf("failed getting latest tags for %s", module)
}
}

fmt.Printf(" src path: %s\n", mgr.dg.SrcPath())
fmt.Printf(" repo path: %s\n", mgr.RepoPath())
fmt.Printf(" remote: %s\n", mgr.remoteName)
Expand All @@ -113,14 +103,18 @@ func (mgr *Manager) List() error {
})
}

func determineBranchAndTag(
m misc.LaModule, v semver.SemVer) (string, string) {
func determineTag(m misc.LaModule, v semver.SemVer) string {
if m.ShortName() == misc.ModuleAtTop {
return fmt.Sprintf("release-%s", v.BranchLabel()), v.String()
return v.String()
}
return string(m.ShortName()) + "/" + v.String()
}

func determineReleaseBranch(releaseBranch string) (string, error) {
if releaseBranch == "" {
return "", fmt.Errorf("%s must be specified for release", "--release-branch")
}
return fmt.Sprintf(
"release-%s-%s", m.ShortName(), v.BranchLabel()),
string(m.ShortName()) + "/" + v.String()
return releaseBranch, nil
}

func (mgr *Manager) Debug(_ misc.LaModule, doIt bool, localFlag bool) error {
Expand All @@ -131,9 +125,9 @@ func (mgr *Manager) Debug(_ misc.LaModule, doIt bool, localFlag bool) error {
// Release supports a gitlab flow style release process.
//
// * All development happens in the branch named "master".
// * Each minor release gets its own branch.
// * Each release uses the explicitly provided release branch.
func (mgr *Manager) Release(
target misc.LaModule, bump semver.SvBump, doIt bool, localFlag bool) error {
target misc.LaModule, bump semver.SvBump, releaseBranch string, doIt bool, localFlag bool) error {
if reps := target.GetDisallowedReplacements(
mgr.allowedReplacements); len(reps) > 0 {
return fmt.Errorf(
Expand All @@ -155,7 +149,11 @@ func (mgr *Manager) Release(

gr := git.NewLoud(mgr.AbsPath(), doIt, localFlag)

relBranch, relTag := determineBranchAndTag(target, newVersion)
relBranch, err := determineReleaseBranch(releaseBranch)
if err != nil {
return err
}
relTag := determineTag(target, newVersion)

fmt.Printf(
"Releasing %s, stepping from %s to %s\n",
Expand All @@ -167,12 +165,6 @@ func (mgr *Manager) Release(
if err := gr.FetchRemote(mgr.remoteName); err != nil {
return err
}
if err := gr.CheckoutMainBranch(); err != nil {
return err
}
if err := gr.MergeFromRemoteMain(mgr.remoteName); err != nil {
return err
}
if err := gr.AssureCleanWorkspace(); err != nil {
return err
}
Expand All @@ -191,9 +183,6 @@ func (mgr *Manager) Release(
if err := gr.PushTagToRemote(mgr.remoteName, relTag); err != nil {
return err
}
if err := gr.CheckoutMainBranch(); err != nil {
return err
}
return nil
}

Expand All @@ -202,7 +191,7 @@ func (mgr *Manager) UnRelease(target misc.LaModule, doIt bool, localFlag bool) e
"Unreleasing %s/%s\n",
target.ShortName(), target.VersionRemote())

_, tag := determineBranchAndTag(target, target.VersionRemote())
tag := determineTag(target, target.VersionRemote())

gr := git.NewLoud(mgr.AbsPath(), doIt, localFlag)

Expand Down
56 changes: 56 additions & 0 deletions cmd/gorepomod/internal/repo/manager_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
// Copyright 2026 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0

package repo

import (
"testing"

"sigs.k8s.io/kustomize/cmd/gorepomod/internal/misc"
"sigs.k8s.io/kustomize/cmd/gorepomod/internal/semver"
)

type fakeModule struct {
shortName misc.ModuleShortName
}

func (f fakeModule) ShortName() misc.ModuleShortName { return f.shortName }
func (f fakeModule) ModulePath() string { return "" }
func (f fakeModule) ImportPath() string { return "" }
func (f fakeModule) AbsPath() string { return "" }
func (f fakeModule) VersionLocal() semver.SemVer { return semver.Zero() }
func (f fakeModule) VersionRemote() semver.SemVer { return semver.Zero() }
func (f fakeModule) DependsOn(misc.LaModule) (bool, semver.SemVer) {
return false, semver.Zero()
}
func (f fakeModule) GetReplacements() []string { return nil }
func (f fakeModule) GetDisallowedReplacements([]string) []string {
return nil
}

func TestDetermineTag(t *testing.T) {
tag := determineTag(
fakeModule{shortName: "api"},
semver.New(1, 2, 3),
)
if tag != "api/v1.2.3" {
t.Fatalf("tag = %q, want %q", tag, "api/v1.2.3")
}
}

func TestDetermineReleaseBranchUsesArgument(t *testing.T) {
branch, err := determineReleaseBranch("release-v5.8.2")
if err != nil {
t.Fatalf("determineReleaseBranch() error = %v", err)
}
if branch != "release-v5.8.2" {
t.Fatalf("branch = %q, want %q", branch, "release-v5.8.2")
}
}

func TestDetermineReleaseBranchRequiresArgument(t *testing.T) {
_, err := determineReleaseBranch("")
if err == nil {
t.Fatal("determineReleaseBranch() error = nil, want non-nil")
}
}
Loading
Loading