Skip to content

Commit 1295069

Browse files
toothbrushclaude
andauthored
Upgrade git-remote-entire alongside entire (#3)
* Upgrade git-remote-entire alongside entire The go install path only built cmd/entire; install both binaries so it matches the Homebrew and install.sh paths, which unpack both from the release tarball. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Entire-Checkpoint: d95ca3cae73e * Model release binaries as one independently-versioned set Drive detection, gating, go-install, and verification from a single releaseBinaries catalog instead of special-casing entire and bolting on git-remote-entire. Each binary carries its own detected version and is upgraded independently, so a missing or stale helper beside a current entire is fixed on its own. Version resolution (--version then build info) is written once in binaryVersion and shared by every binary. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Entire-Checkpoint: 391b8dd6192d * Authenticate release check in Homebrew smoke job The Homebrew job's Exercise step was the only upgrade step without GITHUB_TOKEN, so entire-upgrade's release-version check hit GitHub's API unauthenticated and 403'd on the shared runner IP's 60/hr limit. Set GITHUB_TOKEN like the curl and go jobs already do. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Entire-Checkpoint: 9ac8d58d7318 * Reinstall when a binary's version can't be determined A locally-built dev entire reports "dev" from --version and "(devel)" build info, so neither source yields a version. That was a hard, cryptic failure ("could not determine entire version from --version output or Go build info"). Now an unreadable version is treated as absent for any binary — anchor included — so the gate sees it as out of date and reinstalls, with a clear note instead of an error. The error that remains (when even reinstall can't proceed) names the binary and reports both attempts. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Entire-Checkpoint: 8f7160ecff67 * Report every managed binary in detection and completion Now that the upgrade manages entire and git-remote-entire independently, print a line per binary (version + path, or "not installed"/"version unreadable") instead of only the anchor. Same per-binary listing on completion. Makes "already up to date" verifiable — you can see git-remote-entire's detected version, not just entire's. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Entire-Checkpoint: 9297c7de62eb * Column-align binary names in detection/completion output Pad each binary name to the widest one so the version/path columns line up across entire and git-remote-entire. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Entire-Checkpoint: 5eb342cccb6e --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 40e01c7 commit 1295069

8 files changed

Lines changed: 427 additions & 66 deletions

File tree

.github/workflows/upgrade-smoke.yml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,10 @@ jobs:
4444
brew install --cask entire
4545
entire --version
4646
- name: Exercise upgrade plugin
47+
env:
48+
# Authenticate the release-check API calls; the shared runner IP
49+
# blows past GitHub's unauthenticated 60/hr limit otherwise.
50+
GITHUB_TOKEN: ${{ github.token }}
4751
run: |
4852
set -o pipefail
4953
./entire-upgrade --yes | tee stable.log
@@ -117,6 +121,7 @@ jobs:
117121
./entire-upgrade --nightly --yes | tee nightly.log
118122
grep -F "(go install)" nightly.log
119123
entire --version
124+
test -x "$GOBIN/git-remote-entire"
120125
121126
go-windows:
122127
name: go install path (Windows)
@@ -158,3 +163,6 @@ jobs:
158163
throw "nightly upgrade did not detect a go install"
159164
}
160165
entire --version
166+
if (-not (Test-Path (Join-Path $env:GOBIN "git-remote-entire.exe"))) {
167+
throw "git-remote-entire was not installed beside entire"
168+
}

README.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,10 @@ entire upgrade --stable
2424
```
2525

2626
The plugin detects whether the active `entire` binary was installed through
27-
Homebrew, `install.sh`, or `go install`, then runs the matching updater.
27+
Homebrew, `install.sh`, or `go install`, then runs the matching updater. Each
28+
release ships both `entire` and `git-remote-entire`, and the upgrade replaces
29+
both. The Homebrew and `install.sh` paths unpack the two binaries together; the
30+
`go install` path builds and installs each one beside the existing `entire`.
2831

2932
## Installation
3033

internal/upgrade/install.go

Lines changed: 106 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,31 @@ import (
77
"os"
88
"os/exec"
99
"path/filepath"
10+
"regexp"
1011
"runtime/debug"
1112
"strings"
1213
)
1314

15+
// anchorBinary is the executable we discover on PATH to locate the install
16+
// (its directory and install method). Every other release binary is resolved
17+
// beside it. remoteHelperBinary is the git remote helper for entire:// URLs.
18+
const (
19+
anchorBinary = "entire"
20+
remoteHelperBinary = "git-remote-entire"
21+
)
22+
23+
// releaseBinaries is the set of executables shipped in an Entire CLI release.
24+
// They're versioned and (re)installed independently — even though releases
25+
// normally ship them in lockstep, a user's bin dir can drift. Add a binary
26+
// here and detection, gating, install, and verification all pick it up.
27+
var releaseBinaries = []struct {
28+
Name string // executable base name
29+
GoPkg string // module path for `go install`
30+
}{
31+
{Name: anchorBinary, GoPkg: "github.com/entireio/cli/cmd/entire"},
32+
{Name: remoteHelperBinary, GoPkg: "github.com/entireio/cli/cmd/git-remote-entire"},
33+
}
34+
1435
type Method string
1536

1637
const (
@@ -24,7 +45,21 @@ type Installation struct {
2445
BinaryPath string
2546
ResolvedPath string
2647
BrewCask string
27-
Version Version
48+
// Version is the anchor (entire) version, kept for the channel/compare
49+
// logic. It mirrors Binaries[0].Version.
50+
Version Version
51+
// Binaries are all the release executables this install manages, anchor
52+
// first, each with its independently detected version (Present=false when
53+
// missing or unreadable).
54+
Binaries []ManagedBinary
55+
}
56+
57+
// ManagedBinary is one release executable located beside the anchor.
58+
type ManagedBinary struct {
59+
Name string
60+
GoPkg string
61+
Path string
62+
Version Version
2863
}
2964

3065
type Environment struct {
@@ -58,23 +93,53 @@ func DetectInstallation(ctx context.Context) (Installation, error) {
5893
return Installation{}, fmt.Errorf("unsupported Entire CLI installation at %s; supported update methods are Homebrew, install.sh, and go install", binaryPath)
5994
}
6095

61-
versionOut, err := exec.CommandContext(ctx, binaryPath, "--version").CombinedOutput()
62-
if err != nil {
63-
return Installation{}, versionCommandError(err, versionOut)
96+
// Resolve every release binary beside the anchor. An unreadable version —
97+
// a missing binary, a local dev build (`--version` reports "dev" and the Go
98+
// build info is "(devel)"), or a git-remote-entire predating --version —
99+
// leaves Version absent, which callers treat as "(re)install it" rather than
100+
// a hard failure. binDir is where the anchor lives, so the rest sit beside it.
101+
binDir := filepath.Dir(binaryPath)
102+
for _, rb := range releaseBinaries {
103+
bin := ManagedBinary{Name: rb.Name, GoPkg: rb.GoPkg, Path: filepath.Join(binDir, executableName(rb.Name))}
104+
if version, verr := binaryVersion(ctx, bin.Path); verr == nil {
105+
bin.Version = version
106+
}
107+
install.Binaries = append(install.Binaries, bin)
64108
}
65-
version, err := ParseVersion(string(versionOut))
109+
install.Version = install.Binaries[0].Version
110+
111+
return install, nil
112+
}
113+
114+
// binaryVersion reads a binary's version the same way for every release
115+
// executable: its `--version` output first, then the Go build info baked into
116+
// the binary. It returns an error only when neither yields a version; the
117+
// message names the binary and reports both attempts so the failure is
118+
// actionable rather than cryptic.
119+
func binaryVersion(ctx context.Context, path string) (Version, error) {
120+
resolved, err := filepath.EvalSymlinks(path)
66121
if err != nil {
67-
if install.Method != MethodGo {
68-
return Installation{}, err
69-
}
70-
parseErr := err
71-
version, err = versionFromGoBuildInfo(resolvedPath)
72-
if err != nil {
73-
return Installation{}, fmt.Errorf("%w; Go build info fallback failed: %v", parseErr, err)
122+
resolved = path
123+
}
124+
125+
out, cmdErr := exec.CommandContext(ctx, path, "--version").CombinedOutput()
126+
var versionAttempt string
127+
switch {
128+
case cmdErr != nil:
129+
versionAttempt = fmt.Sprintf("running %q failed: %v", filepath.Base(path)+" --version", cmdErr)
130+
default:
131+
version, parseErr := ParseVersion(string(out))
132+
if parseErr == nil {
133+
return version, nil
74134
}
135+
versionAttempt = fmt.Sprintf("could not parse %q output %q", filepath.Base(path)+" --version", strings.TrimSpace(string(out)))
75136
}
76-
install.Version = version
77-
return install, nil
137+
138+
if version, buildErr := versionFromGoBuildInfo(resolved); buildErr == nil {
139+
return version, nil
140+
}
141+
return Version{}, fmt.Errorf("could not determine %s version: %s; and reading Go build info from %s did not yield one either",
142+
filepath.Base(path), versionAttempt, resolved)
78143
}
79144

80145
func versionFromGoBuildInfo(binaryPath string) (Version, error) {
@@ -90,6 +155,17 @@ func versionFromBuildInfo(info *debug.BuildInfo) (Version, error) {
90155
return Version{}, fmt.Errorf("missing Go build info")
91156
}
92157

158+
// Mirror the CLI's versioninfo.resolve: a GoReleaser `-X ...Version=` stamp
159+
// wins over the module version. `go build -ldflags=...` records that whole
160+
// flag string in build settings (and `-s -w` doesn't strip it), so release
161+
// binaries — Homebrew and install.sh — expose their version here even
162+
// though buildinfo.Main.Version is "(devel)" for a `go build`.
163+
if raw, ok := ldflagsVersion(info.Settings); ok {
164+
if version, err := ParseVersion(raw); err == nil {
165+
return version, nil
166+
}
167+
}
168+
93169
candidates := []string{}
94170
if isEntireCLIModulePath(info.Main.Path) {
95171
candidates = append(candidates, info.Main.Version)
@@ -115,12 +191,23 @@ func isEntireCLIModulePath(path string) bool {
115191
return path == "github.com/entireio/cli" || strings.HasPrefix(path, "github.com/entireio/cli/")
116192
}
117193

118-
func versionCommandError(err error, output []byte) error {
119-
message := strings.TrimSpace(string(output))
120-
if message == "" {
121-
return fmt.Errorf("failed to read installed Entire CLI version: %w", err)
194+
// ldflagsVersionRE pulls the version out of the CLI's versioninfo stamp, as it
195+
// appears in the `-ldflags` build setting. GoReleaser emits `-X <path>=<value>`;
196+
// `go build` may render the linker flag as either `-X path=val` or `-X=path=val`.
197+
var ldflagsVersionRE = regexp.MustCompile(`-X[ =]github\.com/entireio/cli/cmd/entire/cli/versioninfo\.Version=(\S+)`)
198+
199+
// ldflagsVersion extracts the versioninfo.Version stamp from the recorded
200+
// `-ldflags` build setting, if present.
201+
func ldflagsVersion(settings []debug.BuildSetting) (string, bool) {
202+
for _, setting := range settings {
203+
if setting.Key != "-ldflags" {
204+
continue
205+
}
206+
if m := ldflagsVersionRE.FindStringSubmatch(setting.Value); m != nil {
207+
return strings.Trim(m[1], `"'`), true
208+
}
122209
}
123-
return fmt.Errorf("failed to read installed Entire CLI version: %w: %s", err, message)
210+
return "", false
124211
}
125212

126213
func ClassifyInstallation(binaryPath, resolvedPath string, env Environment) (Installation, bool) {

internal/upgrade/install_test.go

Lines changed: 34 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ package upgrade
22

33
import (
44
"context"
5-
"errors"
65
"path/filepath"
76
"reflect"
87
"runtime/debug"
@@ -124,6 +123,40 @@ func TestVersionFromBuildInfoUsesEntireDependencyVersion(t *testing.T) {
124123
}
125124
}
126125

126+
func TestVersionFromBuildInfoPrefersLdflagsStamp(t *testing.T) {
127+
// Release binaries (Homebrew, install.sh) carry "(devel)" as the module
128+
// version but a real version in the -ldflags stamp, just like git-remote-entire.
129+
got, err := versionFromBuildInfo(&debug.BuildInfo{
130+
Main: debug.Module{Path: "github.com/entireio/cli/cmd/git-remote-entire", Version: "(devel)"},
131+
Settings: []debug.BuildSetting{
132+
{Key: "-ldflags", Value: "-s -w -X github.com/entireio/cli/cmd/entire/cli/versioninfo.Version=0.7.4 -X github.com/entireio/cli/cmd/entire/cli/versioninfo.Commit=deadbeef"},
133+
},
134+
})
135+
if err != nil {
136+
t.Fatal(err)
137+
}
138+
if got.String() != "0.7.4" {
139+
t.Fatalf("version = %s, want 0.7.4", got)
140+
}
141+
}
142+
143+
func TestVersionFromBuildInfoLdflagsStampWinsOverModule(t *testing.T) {
144+
// The stamp is what `entire --version` reports, so it must win over the
145+
// module version when they differ, matching the CLI's versioninfo.resolve.
146+
got, err := versionFromBuildInfo(&debug.BuildInfo{
147+
Main: debug.Module{Path: "github.com/entireio/cli/cmd/entire", Version: "v0.6.1"},
148+
Settings: []debug.BuildSetting{
149+
{Key: "-ldflags", Value: "-X github.com/entireio/cli/cmd/entire/cli/versioninfo.Version=0.7.4"},
150+
},
151+
})
152+
if err != nil {
153+
t.Fatal(err)
154+
}
155+
if got.String() != "0.7.4" {
156+
t.Fatalf("version = %s, want 0.7.4 (ldflags stamp should win)", got)
157+
}
158+
}
159+
127160
func TestVersionFromBuildInfoRejectsDevelVersion(t *testing.T) {
128161
_, err := versionFromBuildInfo(&debug.BuildInfo{
129162
Main: debug.Module{
@@ -258,13 +291,6 @@ func TestCommandEnvWithoutGitHubToken(t *testing.T) {
258291
}
259292
}
260293

261-
func TestVersionCommandErrorIncludesOutput(t *testing.T) {
262-
err := versionCommandError(errors.New("exit status 1"), []byte("broken install\n"))
263-
if !strings.Contains(err.Error(), "broken install") {
264-
t.Fatalf("error = %q, want command output", err)
265-
}
266-
}
267-
268294
type recordRunner struct {
269295
commands []string
270296
envs [][]string

0 commit comments

Comments
 (0)