Skip to content

fix: update renamed standalone binaries by staging under the archive member name - #204

Open
eranchetz wants to merge 3 commits into
mainfrom
fix/renamed-standalone-update
Open

eranchetz wants to merge 3 commits into
mainfrom
fix/renamed-standalone-update

Conversation

@eranchetz

@eranchetz eranchetz commented Oct 29, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

Standalone installs whose binary was renamed by its owner fail dci update (#91, reproduced by the reporter against the live v2.5.2 release):

$ ./dci-standalone update --yes
{"error":{"code":"UPDATE_FAILED","message":"replace .../dci-standalone: executable not found in tar: \"dci-standalone\"",...}}

Root cause

selfUpdateBinary passes the resolved executable path to updater.UpdateTo. go-selfupdate derives the archive-member name from that path's basename (decompressAndUpdate → filepath.Split, matched by matchExecutableName in decompress.go), and GoReleaser always ships the tar member as dci (binary: dci) — so a renamed install looks for itself in the archive. Asset detection is basename-independent, which is why the failure only surfaces at extraction.

Checked upstream first: go-selfupdate has no executable-name config on any release (v1.6.0 pinned, v1.7.0 latest) or on main, so the member name can't be pinned in the library config.

Fix (18aa0ed)

An install whose basename isn't dci/dci.exe stages the download under the archive member name in a unique .dci-update-* directory next to the binary; the library unpacks and checksum-verifies there; the final swap onto the running path goes through the library's own exported update.Apply (atomic replace, rollback, Windows rename dance), with update.RollbackError surfacing a failed rollback as its own error. Installs named dci/dci.exe take the direct path exactly as before. The staged file only needs to exist — update.Apply renames it aside and overwrites it, never reads it. Error contract unchanged (UPDATE_FAILED wrapping, manual-update hint).

Verification

  • Failing-before / passing-after on the live release: a v2.8.0-stamped scratch binary named dci-standalone hit the exact dci update fails on standalone binaries not named 'dci': executable not found in tar #91 error before the change; after, {"updated":true} → real v2.9.0 asset (checksum-verified), filename preserved, --version answering 2.9.0, no .dci-update-*/.old junk left.
  • Offline tests: staging-contract unit test (in-place paths incl. Windows dci.exe, staging path/cleanup, unwritable-dir failure leaves the original untouched) and an end-to-end test through the real DetectVersion → UpdateTo → update.Apply path against a fake HTTPSource release server (renamed install, tar member dci, checksum rejection leaves the original and no junk).
  • Full go test . green, including the pty e2e battery.

Follow-up commits on this branch

  • 1afc3cc chore: simplification pass over the new helpers/tests (no behavior change): unnamed results on stageReleaseTarget, shared writeOldExe fixture + file-scope aimExe seam, the two in-place staging cases as one loop, a readable staging-path assertion.
  • a752b01 fix: repairs command-docs/create-budget.yaml for CI: the production OpenAPI spec now requires scopes on the create-budget body, so TestCommandDocsAgainstSpec failed on pristine main as well (verified in a worktree — spec drift after main's last green run on 2026-10-16, not caused by this PR). Both flag-style examples now carry the spec-valid cloud_provider scope from the file-based example; validated against the live spec locally and Prettier-clean. A different teaching value (e.g. a tag dimension) is curation on top if preferred.

Notes

  • UPDATE-SPEC.md §5 documents the renamed-install staging contract.
  • Pre-existing observations from the audit (not part of this PR): the §5 sudo-suggestion for permission failures isn't in today's hint text, and Windows self-update has no runtime check in CI (the post-release zip smoke doesn't exercise dci update).

Fixes #91

…member name

go-selfupdate picks the executable out of the release archive by the
target path's basename, and the archives always ship it as dci — so a
standalone install renamed by its owner (dci-standalone, dci.bak) looked
for itself in the tar and failed with 'executable not found in tar'
(#91). Such installs now stage the download under the member name in a
unique .dci-update-* directory next to the binary, and the final swap
onto the running path runs through update.Apply, keeping the atomic
replace, rollback, and Windows rename dance in the library. Installs
named dci/dci.exe take the direct path exactly as before.

Fixes #91
@eranchetz
eranchetz requested review from a team, apgiorgi, chaim0m and taltultc as code owners October 29, 2026 18:06
@github-actions github-actions Bot added the documentation Improvements or additions to documentation label Oct 29, 2026

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I reviewed this PR and didn't find any bugs. It changes how the standalone updater replaces the running executable, and CODEOWNERS assigns these files to the maintainers, so a maintainer should still look at it.

What was reviewed:

  • stageReleaseTarget: installs named dci or dci.exe still update in place as before. Renamed installs stage into a .dci-update-* temp directory next to the binary, and the deferred cleanup removes it on every return path.
  • selfUpdateBinary: checksum validation still runs inside UpdateTo before anything is swapped. The staged file goes onto the real path only after that succeeds, and a rollback failure is reported as its own error.
  • Tests: a fake HTTPSource drives the full DetectVersion, UpdateTo and Apply path. They cover the renamed install, the dci install and the bad-checksum case, and check that the original file is untouched and no staging leftovers remain.
Extended reasoning...

The change touches self_update.go, the standalone path that replaces the installed binary, plus new offline tests and one UPDATE-SPEC.md bullet. The security-sensitive part is the executable swap. Checksum verification still runs before the swap, and the code never tries to escalate privileges. I deferred instead of approving because .github/CODEOWNERS assigns every path to the maintainer group. The new Apply-based swap also relies on library behaviour I couldn't inspect here, and Windows has no test coverage.

Eran Chetzroni added 2 commits October 30, 2026 14:38
Unnamed results on stageReleaseTarget, a shared writeOldExe fixture and
file-scope aimExe seam for the tests, the two in-place staging cases as
one table loop, and a readable staging-path assertion.
The production OpenAPI spec now requires scopes on the create-budget
request body, so the two flag-style examples stopped validating
(TestCommandDocsAgainstSpec fails on main as well — the spec drifted
after the v2.9.0 release CI ran). Both examples now carry the same
spec-valid cloud_provider scope as the file-based example.
@eranchetz

Copy link
Copy Markdown
Collaborator Author

The red command-docs check is not from this PR — TestCommandDocsAgainstSpec fails on pristine main too (verified in a worktree at 67ff800). The production OpenAPI spec now requires scopes on the create-budget request body; the two flag-style examples in command-docs/create-budget.yaml predate that (main's last green CI run was 2026-10-16, before the drift).

Pushed two follow-up commits to this branch:

  • 1afc3cc chore: simplification pass over the new staging helpers/tests (no behavior change).
  • a752b01 fix: adds the required scopes to both examples, reusing the spec-valid cloud_provider scope from the file-based example; validated against the live spec locally (DCI_COMMAND_DOCS_SPEC + TestCommandDocs green) and Prettier-clean per the repo rule.

If a scopes value other than cloud_provider/amazon-web-services is the better teaching example (e.g. a tag dimension for the "Team Backyard" case), that's curation on top — say the word and I'll swap it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation

Projects

None yet

Development

Successfully merging this pull request may close these issues.

dci update fails on standalone binaries not named 'dci': executable not found in tar

1 participant