From 836af5d4197a7f8605035f890e83399b8bed59a1 Mon Sep 17 00:00:00 2001 From: Harsh-2002 Date: Thu, 4 Jun 2026 05:06:30 +0000 Subject: [PATCH] fix(upgrade): pin self-update asset to exact os-arch (was picking wrong-OS binary) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit go-selfupdate's loose '^orva-cli-' filter let it fall back to arch-only matching, so on linux/amd64 it could select orva-cli-darwin-amd64 (a Mach-O binary) whenever that asset sorted first — release build-matrix uploads run in parallel, so asset order is non-deterministic. Result: an intermittent 'exec format error' after a self-reported-successful 'orva upgrade' (caught by cli-e2e's upgrade round-trip). Anchor the filter to the full '^orva-cli--' token via upgradeAssetFilter() so only the running platform's asset can match; the trailing .exe on Windows assets is still covered by the prefix. Verified end-to-end: a fixed binary upgrading against the live repo now pulls the ELF x86-64 asset and runs, where the old binary pulled Mach-O. --- cli/CLAUDE.md | 10 ++++++---- cli/commands/commands_test.go | 18 ++++++++++++++++++ cli/commands/upgrade.go | 16 +++++++++++++++- 3 files changed, 39 insertions(+), 5 deletions(-) diff --git a/cli/CLAUDE.md b/cli/CLAUDE.md index 5e27e21..be4f94b 100644 --- a/cli/CLAUDE.md +++ b/cli/CLAUDE.md @@ -124,10 +124,12 @@ weekly schedule to catch GH-API / release-asset drift. ## Self-update (`orva upgrade`) Uses `github.com/creativeprojects/go-selfupdate`. The library queries -GitHub for the latest release matching `Filters: ["^orva-cli-"]`, -downloads the right OS/arch asset, verifies against `checksums.txt`, -and atomically replaces the running binary via rename-and-hide on -Windows / unlink-and-replace on Unix. +GitHub for the latest release, downloads the OS/arch asset matching +`Filters: ["^orva-cli--"]` (pinned to the exact platform token +by `upgradeAssetFilter` — a loose `^orva-cli-` filter let it fall back to +arch-only matching and pick a wrong-OS binary → intermittent "exec format +error"), verifies against `checksums.txt`, and atomically replaces the +running binary via rename-and-hide on Windows / unlink-and-replace on Unix. If the install path is not writable, `orva upgrade` exits non-zero with a "re-run with `sudo orva upgrade`" hint. Never silently elevates. diff --git a/cli/commands/commands_test.go b/cli/commands/commands_test.go index 0afce6f..e08a9b4 100644 --- a/cli/commands/commands_test.go +++ b/cli/commands/commands_test.go @@ -136,6 +136,24 @@ func TestKeysCreateDefaultPermission(t *testing.T) { } } +// TestUpgradeAssetFilterPinsOSArch guards the upgrade asset matcher: it must +// anchor to the exact orva-cli-- token so go-selfupdate can't pick a +// wrong-OS binary that merely shares the arch (the "exec format error" bug). +func TestUpgradeAssetFilterPinsOSArch(t *testing.T) { + cases := []struct { + goos, goarch, want string + }{ + {"linux", "amd64", "^orva-cli-linux-amd64"}, + {"darwin", "arm64", "^orva-cli-darwin-arm64"}, + {"windows", "amd64", "^orva-cli-windows-amd64"}, + } + for _, c := range cases { + if got := upgradeAssetFilter(c.goos, c.goarch); got != c.want { + t.Errorf("upgradeAssetFilter(%q,%q) = %q, want %q", c.goos, c.goarch, got, c.want) + } + } +} + // TestNewRootSetsVersion confirms the version template is wired up so // `orva --version` returns the value of commands.Version (set by main()). func TestNewRootSetsVersion(t *testing.T) { diff --git a/cli/commands/upgrade.go b/cli/commands/upgrade.go index 48e5609..a15828a 100644 --- a/cli/commands/upgrade.go +++ b/cli/commands/upgrade.go @@ -15,6 +15,20 @@ import ( // orvaRepo is the GitHub repo to query for releases. Overridable for tests. var orvaRepo = "Harsh-2002/Orva" +// upgradeAssetFilter pins go-selfupdate's asset match to the exact +// orva-cli-- release artifact for the running platform. +// +// A loose "^orva-cli-" filter let go-selfupdate fall back to matching on +// arch alone, so on a linux/amd64 host it could pick orva-cli-darwin-amd64 +// (a Mach-O binary) whenever that asset happened to sort first — releases +// upload the build matrix in parallel, so asset order is non-deterministic. +// The result was an intermittent "exec format error" after a "successful" +// upgrade. Anchoring to the full os-arch token removes the ambiguity; the +// trailing .exe on Windows assets is still matched by the prefix. +func upgradeAssetFilter(goos, goarch string) string { + return fmt.Sprintf("^orva-cli-%s-%s", goos, goarch) +} + var upgradeCmd = &cobra.Command{ Use: "upgrade", Short: "Upgrade orva to the latest GitHub release", @@ -53,7 +67,7 @@ func runUpgrade(cmd *cobra.Command, _ []string) error { }, OS: runtime.GOOS, Arch: runtime.GOARCH, - Filters: []string{"^orva-cli-"}, + Filters: []string{upgradeAssetFilter(runtime.GOOS, runtime.GOARCH)}, }) if err != nil { return fmt.Errorf("init updater: %w", err)