Skip to content

Commit d8ed4bb

Browse files
wesmclaude
andauthored
Add --force flag to update command for dev builds (#116)
## Summary - Dev builds are no longer replaced by default during `roborev update` - Add `--force`/`-f` flag to explicitly install official release over dev build - Show full download details before prompting for `--force` This prevents accidental replacement of dev builds when testing, while still making it easy to switch back to official releases. ## Test plan - [x] `roborev update --check` on dev build shows "Use --force to install..." - [x] `roborev update` on dev build shows details then "Use --force to install..." - [x] `roborev update --force` on dev build proceeds to download/install - [x] `roborev update` on official release works as before (no --force needed) 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 653bd9c commit d8ed4bb

File tree

3 files changed

+73
-3
lines changed

3 files changed

+73
-3
lines changed

cmd/roborev/main.go

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1928,14 +1928,18 @@ it only updates existing installations. Used by 'roborev update'.`,
19281928
func updateCmd() *cobra.Command {
19291929
var checkOnly bool
19301930
var yes bool
1931+
var force bool
19311932

19321933
cmd := &cobra.Command{
19331934
Use: "update",
19341935
Short: "Update roborev to the latest version",
19351936
Long: `Check for and install roborev updates.
19361937
19371938
Shows exactly what will be downloaded and where it will be installed.
1938-
Requires confirmation before making changes (use --yes to skip).`,
1939+
Requires confirmation before making changes (use --yes to skip).
1940+
1941+
Dev builds are not replaced by default. Use --force to install the latest
1942+
official release over a dev build.`,
19391943
RunE: func(cmd *cobra.Command, args []string) error {
19401944
fmt.Println("Checking for updates...")
19411945

@@ -1952,7 +1956,7 @@ Requires confirmation before making changes (use --yes to skip).`,
19521956
fmt.Printf("\n Current version: %s\n", info.CurrentVersion)
19531957
fmt.Printf(" Latest version: %s\n", info.LatestVersion)
19541958
if info.IsDevBuild {
1955-
fmt.Println("\nYou're running a dev build. Latest stable release available.")
1959+
fmt.Println("\nYou're running a dev build. Latest official release available.")
19561960
} else {
19571961
fmt.Println("\nUpdate available!")
19581962
}
@@ -1975,6 +1979,15 @@ Requires confirmation before making changes (use --yes to skip).`,
19751979
fmt.Printf(" %s\n", binDir)
19761980

19771981
if checkOnly {
1982+
if info.IsDevBuild {
1983+
fmt.Println("\nUse --force to install the latest official release.")
1984+
}
1985+
return nil
1986+
}
1987+
1988+
// Dev builds require --force to update
1989+
if info.IsDevBuild && !force {
1990+
fmt.Println("\nUse --force to install the latest official release.")
19781991
return nil
19791992
}
19801993

@@ -2082,6 +2095,7 @@ Requires confirmation before making changes (use --yes to skip).`,
20822095

20832096
cmd.Flags().BoolVar(&checkOnly, "check", false, "only check for updates, don't install")
20842097
cmd.Flags().BoolVarP(&yes, "yes", "y", false, "skip confirmation prompt")
2098+
cmd.Flags().BoolVarP(&force, "force", "f", false, "replace dev build with latest official release")
20852099

20862100
return cmd
20872101
}

internal/update/update.go

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ type cachedCheck struct {
7676
// Uses a 1-hour cache to avoid hitting GitHub API too often
7777
func CheckForUpdate(forceCheck bool) (*UpdateInfo, error) {
7878
currentVersion := strings.TrimPrefix(version.Version, "v")
79-
isDevBuild := extractBaseSemver(currentVersion) == ""
79+
isDevBuild := isDevBuildVersion(currentVersion)
8080

8181
// Check cache first (unless forced)
8282
// Use shorter cache for dev builds so they learn about releases sooner
@@ -561,6 +561,24 @@ func extractBaseSemver(v string) string {
561561
return v
562562
}
563563

564+
// gitDescribePattern matches git describe format: v0.16.1-2-gabcdef
565+
// The -N-gHASH suffix indicates N commits after the tag
566+
var gitDescribePattern = regexp.MustCompile(`-\d+-g[0-9a-f]+$`)
567+
568+
// isDevBuildVersion returns true if the version is a dev build.
569+
// Dev builds are either:
570+
// - Pure hashes with no semver (e.g., "9c2baf2", "dev")
571+
// - Git describe format with commits after tag (e.g., "v0.16.1-2-gabcdef")
572+
func isDevBuildVersion(v string) bool {
573+
v = strings.TrimPrefix(v, "v")
574+
// Pure hash or "dev" - no semver base
575+
if extractBaseSemver(v) == "" {
576+
return true
577+
}
578+
// Git describe format: has commits after a tag
579+
return gitDescribePattern.MatchString(v)
580+
}
581+
564582
// isNewer returns true if v1 is newer than v2
565583
// Assumes semver format: major.minor.patch
566584
// Handles git describe format (v0.4.0-5-gabcdef) by extracting base version.

internal/update/update_test.go

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -257,6 +257,44 @@ func TestExtractBaseSemver(t *testing.T) {
257257
}
258258
}
259259

260+
func TestIsDevBuildVersion(t *testing.T) {
261+
tests := []struct {
262+
version string
263+
want bool
264+
}{
265+
// Official releases - NOT dev builds
266+
{"0.16.1", false},
267+
{"v0.16.1", false},
268+
{"1.0.0", false},
269+
{"v1.0.0", false},
270+
271+
// Git describe format - ARE dev builds
272+
{"0.16.1-2-g75d300a", true},
273+
{"v0.16.1-2-g75d300a", true},
274+
{"0.4.0-5-gabcdef", true},
275+
{"1.2.3-100-gdeadbeef", true},
276+
277+
// Pure hash/dev - ARE dev builds
278+
{"dev", true},
279+
{"abc1234", true},
280+
{"88be010", true},
281+
282+
// Prerelease tags - NOT dev builds (intentional releases)
283+
{"0.16.1-rc1", false},
284+
{"v1.0.0-beta.1", false},
285+
{"0.4.0-alpha", false},
286+
}
287+
288+
for _, tt := range tests {
289+
t.Run(tt.version, func(t *testing.T) {
290+
got := isDevBuildVersion(tt.version)
291+
if got != tt.want {
292+
t.Errorf("isDevBuildVersion(%q) = %v, want %v", tt.version, got, tt.want)
293+
}
294+
})
295+
}
296+
}
297+
260298
func TestIsNewer(t *testing.T) {
261299
tests := []struct {
262300
v1, v2 string

0 commit comments

Comments
 (0)