feat(selfupdate): auto-upgrade on every install layout and platform (canonical fallback + boot trampoline)#62
Merged
Merged
Conversation
…uto-upgrade on every install layout and platform A manually-installed runner (root-owned /usr/local/bin, run as a regular user) could never self-update: the in-place swap needs a writable binary directory, so the handler logged a warn and stayed behind forever. Windows never worked at all (syscall.Exec is an EWINDOWS stub; releases ship zip but only tar.gz extraction existed), and darwin misjudged the installer's symlink layout because os.Executable() returns the unresolved link path. Root fix: converge every layout onto a user-writable canonical location instead of insisting on in-place swap. - ResolveTarget: EvalSymlinks the exe; writable dir -> in-place (systemd state-dir layout, root installs, dev builds: unchanged). Read-only dir -> upgrades land at <state home>/bin/flashduty-runner, seeded from the running binary first so .bak rollback always has a predecessor. - Boot trampoline (run cmd): a relocated runner that finds a strictly newer canonical binary execs into it, so the stale PATH entry becomes a permanent launcher for the current version. Version-gated via 'canonical version' + semver: a manually-reinstalled newer PATH binary is never downgraded (matters under --disable-auto-update) and dev builds never trampoline. - Windows: restartSelf = StartProcess+Exit with CREATE_BREAKAWAY_FROM_JOB then plain-spawn fallback (service-manager Job Objects); zip extraction keyed by artifact URL; swap/restore via rename-aside sequences since a running exe can be renamed but not replaced. - Probation anchors at the resolved running binary, deliberately not the canonical path: a PATH binary must not clear a crashed canonical's in-flight marker and reset its crash-loop accounting. Backend needs no change: fc-safari already resolves per-OS/arch assets. Existing fleet note: runners older than this release still lack the fallback, so manual installs need one last by-hand upgrade to cross over. Verified: full suite on darwin + linux (docker, non-root so read-only fixtures bind); cross-compiled all six GOOS/GOARCH targets; live E2E on macOS against a local backend — read-only-dir runner downloaded 0.0.2, swapped at the canonical path, re-exec'd, committed after handshake, and a later start of the old PATH binary trampolined straight into 0.0.2.
Go's os.Chmod maps to FILE_ATTRIBUTE_READONLY, which does not write-protect a directory on Windows — the 0555 fixture silently stays writable and ResolveTarget correctly reports in-place, failing the relocated-path expectations. Real Windows read-only dirs are ACL-based; the dirWritable probe handles them, but a unit test can't cheaply set one up. Platform-shared mechanics (seed, Apply, rollback, zip) remain covered on Windows by the fixtures that don't need a locked dir.
…x platforms The runner is unix-only in practice: it shells out to bash/ripgrep, ships no Windows installer or service wrapper, and its README documents only Linux + macOS. Windows was merely a goreleaser build target producing a zip that can't actually run. Carrying Windows-specific self-update code (rename-aside swap, StartProcess re-exec, zip extraction) was building for a platform we don't ship — so remove it rather than maintain unrunnable paths. - goreleaser: drop windows from goos; remove the zip format override. - CI: drop windows-latest from the test matrix. - selfupdate: delete swap_windows.go / restart_windows.go; collapse the _unix.go files to swap.go / restart.go (restart.go now //go:build unix, syscall.Exec being unix-only); drop the zip extractor + isZipURL and their archive/zip + net/url imports; runnerBinaryName is now a plain const (no .exe variant); drop the x/sys/windows dependency. - README: note the auto-update download URL is backend-decided — a private mirror must also be configured as the backend's install_script_url, or pushed upgrades resolve from the public GitHub host. Self-update on the platforms we ship (Linux + macOS, in-place and the read-only-dir canonical fallback) is unchanged and stays fully covered. Verified: go build + full test suite on darwin and linux; all four shipped GOOS/GOARCH targets cross-compile; golangci-lint 0 issues; gofumpt clean.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
A manually-installed runner (
sudo mvinto root-owned/usr/local/bin, run as a regular user) could never self-update — it loggedcannot self-update (binary directory not writable)on every advertisement and stayed behind forever (hit by a colleague on macOS, runner stuck while 0.0.18 was advertised). Two more platform gaps hid behind that warn:syscall.Execis a stub returningEWINDOWS, releases ship.zipbut only tar.gz extraction existed, andos.Renamecannot replace a running exe.os.Executable()returns the unresolved symlink path (/usr/local/bin/...), so the writability probe checked the wrong directory even when the real binary sat in a writable state dir.Design
Converge every install layout onto a user-writable canonical location instead of insisting on in-place swap (rustup-shim model):
ResolveTarget—EvalSymlinksthe exe. Writable dir → in-place swap, byte-for-byte the old behavior (systemd state-dir layout, root installs, dev builds). Read-only dir → upgrades target<state home>/bin/flashduty-runner(default~/.flashduty/bin/), seeded from the running binary first so.bakrollback always has a predecessor.runcmd) — a relocated runner that finds a strictly newer canonical binary execs into it. The stale PATH entry becomes a permanent launcher for the current version (reboot/manual restart both land on the newest binary). Version-gated by runningcanonical version+ semver compare: a manually-reinstalled newer PATH binary is never downgraded (matters under--disable-auto-update), dev builds never trampoline, unparseable canonical output never gets exec'd.restartSelf=StartProcess+exit, tryingCREATE_BREAKAWAY_FROM_JOBfirst then plain spawn (NSSM/WinSW Job Objects); even if the job kills the child, the service manager restarts through the old path and the trampoline converges the version. Zip extraction keyed by artifact URL. Swap = rename-aside sequences (a running exe can be renamed, not replaced); rollback renames the running binary to.failedbefore restoring.bak.Backend needs no change: fc-safari's artifact resolver already serves per-OS/arch assets (zip for windows).
Notes for rollout
/usr/local/binkeeps its installed mtime/version;flashduty-runner versionand the running service always reflect the current version (README documents this).Verification
go vetclean on windows; golangci-lint 0 issues; gofumpt clean.binary directory not writable; upgrading at the canonical state-home path→ download/verify/swap → real exec → v0.0.2 handshake →self-update committed. Restarting the untouched old PATH binary →newer self-updated binary found at canonical path; restarting into it→ v0.0.2 connected. Probation/rollback exercised in unit tests.🤖 Generated with Claude Code