Skip to content

Commit f524d44

Browse files
theletterfclaude
andcommitted
Report what the docs-builder installer actually did
The upstream installer exits 0 whether it installs, skips an existing binary, or is declined at its overwrite prompt, and docs-utils reported every one of those as "Installed docs-builder." A user who answered "n" saw Installation aborted immediately followed by [OK] Installed docs-builder, contradicted two lines later by the status table still offering the update. The outcome now comes from the tool's reported version either side of the run, not from the exit code: installed, updated, left unchanged, or no binary on PATH. installerOutcome is a pure function so the four cases are covered by a table test. Also collapses the duplicate "Installing documentation tools" header. update called installOptionalTools once per tool and the function prints its own section heading, so a full update printed the heading twice and read like two separate phases. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H3vJ764kbRQMFqEtZxh42b
1 parent ecca9bf commit f524d44

3 files changed

Lines changed: 82 additions & 7 deletions

File tree

‎cmd/elastic-docs-utils/main.go‎

Lines changed: 46 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -228,16 +228,55 @@ func installOptionalTools(r *ui.Renderer, vale, docsBuilder, dryRun, force, assu
228228
}
229229
updateDownloadProgress(progress, "docs-builder", completed, total)
230230
}
231+
// The upstream installer exits 0 whether it installs, skips an existing
232+
// binary, or is declined at its overwrite prompt, so its exit code alone
233+
// cannot tell us what happened. Compare the version either side of it.
234+
before := updates.BinaryVersion("docs-builder", "--version")
231235
if err := bootstrap.InstallDocsBuilderWithProgress(force, assumeYes, callback); err != nil {
232236
failures = append(failures, fmt.Errorf("install docs-builder: %w", err))
233237
} else {
234-
r.Success("Installed docs-builder.")
238+
progress.Stop()
239+
level, message := installerOutcome("docs-builder", before, updates.BinaryVersion("docs-builder", "--version"))
240+
switch level {
241+
case outcomeMissing:
242+
r.Warn("%s", message)
243+
case outcomeUnchanged:
244+
r.Info("%s", message)
245+
default:
246+
r.Success("%s", message)
247+
}
235248
}
236249
progress.Stop()
237250
}
238251
return failures
239252
}
240253

254+
type installerOutcomeLevel int
255+
256+
const (
257+
outcomeInstalled installerOutcomeLevel = iota
258+
outcomeUpdated
259+
outcomeUnchanged
260+
outcomeMissing
261+
)
262+
263+
// installerOutcome decides what an upstream installer actually did, from the
264+
// tool's reported version either side of the run. The installers exit 0 whether
265+
// they install, skip an existing binary, or are declined at an overwrite
266+
// prompt, so a zero exit on its own must not be reported as an install.
267+
func installerOutcome(tool, before, after string) (installerOutcomeLevel, string) {
268+
switch {
269+
case after == "":
270+
return outcomeMissing, fmt.Sprintf("The %s installer finished, but no %s binary is on PATH.", tool, tool)
271+
case before != "" && after == before:
272+
return outcomeUnchanged, fmt.Sprintf("%s left unchanged at %s; the installer did not replace it.", tool, after)
273+
case before == "":
274+
return outcomeInstalled, fmt.Sprintf("Installed %s %s.", tool, after)
275+
default:
276+
return outcomeUpdated, fmt.Sprintf("Updated %s %s to %s.", tool, before, after)
277+
}
278+
}
279+
241280
func reportToolFailures(r *ui.Renderer, failures []error) {
242281
for _, failure := range failures {
243282
r.Warn("%v", failure)
@@ -452,16 +491,16 @@ func commandUpdate(r *ui.Renderer, args []string) error {
452491
// A failed component must not stop the remaining ones, and the refreshed
453492
// status below is most useful precisely when something went wrong.
454493
var toolFailures []error
455-
if selected.vale {
456-
toolFailures = append(toolFailures, installOptionalTools(r, true, false, *dryRun, *force, false)...)
457-
} else {
494+
if !selected.vale {
458495
r.Info("Skipping Vale and Elastic Vale rules.")
459496
}
460-
if selected.docsBuilder {
461-
toolFailures = append(toolFailures, installOptionalTools(r, false, true, *dryRun, *force, false)...)
462-
} else {
497+
if !selected.docsBuilder {
463498
r.Info("Skipping docs-builder.")
464499
}
500+
// One call for both tools: installOptionalTools prints its own section
501+
// header, so calling it per tool printed "Installing documentation tools"
502+
// twice and read like two separate phases.
503+
toolFailures = append(toolFailures, installOptionalTools(r, selected.vale, selected.docsBuilder, *dryRun, *force, false)...)
465504
reportToolFailures(r, toolFailures)
466505
if *dryRun {
467506
r.Info("Would refresh documentation tool status after updates.")

‎cmd/elastic-docs-utils/main_test.go‎

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,3 +130,31 @@ func TestRenderUpdateStatusShowsHintsForActionableRows(t *testing.T) {
130130
t.Fatalf("current component printed a hint:\n%s", out.String())
131131
}
132132
}
133+
134+
// The upstream installers exit 0 whether they install, skip, or are declined,
135+
// so the reported outcome has to come from the version either side of the run.
136+
func TestInstallerOutcome(t *testing.T) {
137+
cases := []struct {
138+
name string
139+
before, after string
140+
want installerOutcomeLevel
141+
contains string
142+
}{
143+
{"declined leaves version untouched", "1.57.1", "1.57.1", outcomeUnchanged, "left unchanged at 1.57.1"},
144+
{"fresh install", "", "1.58.0", outcomeInstalled, "Installed docs-builder 1.58.0"},
145+
{"real update", "1.57.1", "1.58.0", outcomeUpdated, "1.57.1 to 1.58.0"},
146+
{"binary absent afterwards", "1.57.1", "", outcomeMissing, "no docs-builder binary is on PATH"},
147+
{"absent before and after", "", "", outcomeMissing, "no docs-builder binary is on PATH"},
148+
}
149+
for _, c := range cases {
150+
t.Run(c.name, func(t *testing.T) {
151+
level, msg := installerOutcome("docs-builder", c.before, c.after)
152+
if level != c.want {
153+
t.Fatalf("level = %v, want %v (msg %q)", level, c.want, msg)
154+
}
155+
if !strings.Contains(msg, c.contains) {
156+
t.Fatalf("message %q does not contain %q", msg, c.contains)
157+
}
158+
})
159+
}
160+
}

‎internal/updates/updates.go‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -260,6 +260,14 @@ func lookupHint(err error) string {
260260
return "Could not query GitHub to determine the latest version"
261261
}
262262

263+
// BinaryVersion reports the version a locally installed tool prints, or an
264+
// empty string when the tool is absent or prints nothing usable. Callers use it
265+
// to confirm that an installer actually changed the binary, because the
266+
// upstream installers exit 0 whether they install, skip, or are declined.
267+
func BinaryVersion(command string, args ...string) string {
268+
return binaryVersion(command, args...)
269+
}
270+
263271
func binaryVersion(command string, args ...string) string {
264272
path, err := exec.LookPath(command)
265273
if err != nil {

0 commit comments

Comments
 (0)