Repository navigation
Make devpod upgrade compare versions and check permissions first - #31
Merged
Merged
Conversation
devpod upgrade installed the latest release whenever its version differed from the running one, so it could downgrade a newer build, and it only found out it couldn't replace the binary after downloading it. It now parses versions (three or more numbers plus an optional suffix), only upgrades to a newer plain major.minor.patch release, checks that it can create files next to the binary before downloading, follows symlinks to the binary, and accepts --version with or without the leading "v". The new --dry-run flag reports whether a newer version is available and whether the upgrade would succeed. The unused newer-version warning code is removed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
When the CLI runs through a symlink, devpod upgrade now downloads the new release next to the file the link points to (named devpod-<tag>) and repoints the link, leaving the previous binary alone. It finds the link from os.Args[0], since os.Executable resolves it on Linux. A link to another link is refused. Fixes from the review of the first version: - check that the binary (and, for a link, the link) can be renamed, not only that files can be created next to it (sticky directories on Unix, ACLs on Windows), still before downloading - don't require a parseable current version for --version - look up --version with the tags endpoint instead of the 100 newest releases - ignore +build metadata when comparing versions - test the whole Upgrade flow against a fake GitHub API - download and swap the binary here instead of through rhysd/go-github-selfupdate and the unmaintained go-update, which removes six modules - reuse pkg/download's GitHub release types - one helper for comparing tags; slices.Equal in tests GitHub sometimes answers the release list with an empty array, so the latest release is also looked up directly before giving up. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Fixes from the second review of devpod upgrade: - probe renaming the binary (and the link) on every OS, renaming it back, instead of approximating the sticky-bit rule on Unix, so that immutable flags and ACLs are caught before downloading too - probe creating a symlink before an upgrade through a link, since Windows needs a privilege or Developer Mode for that - cancel the download on Ctrl-C or SIGTERM so it removes its temp file, and remove temp files that killed upgrades left over an hour ago - warn when a file named after the release already exists next to the link's target and will be replaced, in the dry run too - give the .old file of a Windows upgrade a unique name, so a previous one that still runs can't make the swap fail - give the new binary the mode and, as far as allowed, the owner of the one it replaces, instead of always 0755 - make --dry-run fail when the upgrade would fail - test how the symlink is found from os.Args[0] Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The permission check renamed the running binary and the link away and back, even in a dry run, so a concurrent start of devpod could miss it, and a kill between the renames left it renamed. It now only looks: the sticky rule, immutable and append-only flags (chflags on macOS, statx or FS_IOC_GETFLAGS on Linux), and on Windows opening the file with DELETE access, which fails on an ACL that denies it or another process holding the file. Directory flags are checked before the create probe, which now also fails if the probe can't be removed. Temporary links get random names, so concurrent upgrades and leftovers don't collide, and leftovers from interrupted upgrades are removed from every directory the upgrade writes to. The upgrade warns, with the chown command to run, when the new binary can't keep the owner or group. The stalled download test cancels once the download has started instead of after a fixed time. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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.
What changed
devpod upgrade:0.8.0-alpha.1,1.2.3alpha,1.2.3-beta-1,1.2.3.4) and build metadata (+abc, ignored when comparing). The automatic upgrade only considers non-draft, non-pre-release releases tagged plainX.Y.Z, and only installs one strictly newer than the running binary. At equal numbers a suffixed version counts as older (sov0.8.0-alpha.1is offeredv0.8.0); extra numbers count (1.2.3.4>1.2.3). Before, it installed the latest release whenever it merely differed from the current one, which could downgrade a newer build.chflags uchg/schg/uappnd/sappnd, SIP andsunlnkon macOS;chattr +i/+aon Linux, viastatxorFS_IOC_GETFLAGS), and on Windows opening the binary and the link withDELETEaccess, which fails on an ACL that denies it or another process holding the file without sharing delete, plus a read-only link. Directory flags are checked before the create probe, so an append-only directory doesn't keep the probe file. For an upgrade through a symlink, it also probes creating a symlink (Windows needs a privilege or Developer Mode). Probe files and the temporary link get random names, so concurrent upgrades or dry runs and leftovers don't collide.0755. If the user may not keep the owner or group (it keeps the group if it can), it warns what changed and whichchowncommand fixes it, since the mode is kept.devpod-<tag>, repoints the link (keeping it relative if it was), and logs that the previous binary was left alone. If a file nameddevpod-<tag>already exists there, it warns that it is replaced (in the dry run too). Both the link's and the target's directories are permission-checked first. A link to another link is refused. The link is found fromos.Args[0](viaPATHif needed), checked to be the running binary, becauseos.Executableresolves links on Linux.--versionlooks the tag up directly (with or without the leadingv), so any release works, and doesn't need the current version to be parseable.--dry-runreports whether a newer version is available (or what--versionwould install), what would be replaced or linked, and fails (non-zero exit) if the upgrade would fail. Nothing is downloaded.PrintNewerVersionWarning,NewerVersionAvailableandCheckForNewerVersion.Dependencies
The download and swap are now done in
pkg/upgrade(temp file in the target directory, size check, then rename; on Windows the running exe is moved aside first, to a uniquely named hidden.oldfile that is removed once it no longer runs). This dropsrhysd/go-github-selfupdateand with itinconshreveable/go-update(unmaintained since 2016),google/go-github/v30,google/go-querystring,tcnksm/go-gitconfigandulikunitz/xz(~53k vendored lines). The licenses page drops them too (the go-licenses workflow would regenerate it anyway). GitHub release JSON usespkg/download'sGithubReleasetypes, extended with the fields needed. Requests usehttp.DefaultClient, notpkg/http's client, which skips TLS verification.Notes for reviewers
GET /releases?per_page=100with[](seen 3 times in 6 identical anonymous requests while testing). When the list has no usable release,/releases/latestis tried before giving up.GITHUB_TOKENis used if set, for GitHub's rate limit.os.Args[0], becauseos.Executableresolves links on Linux. That's unit-tested (invokedBinary) and was run for real on macOS and on Linux (Docker).replace_windows.go, symlink privilege, theDELETE-access check) are vetted and linted but not run here;windows-tests.yamlruns this package's unit tests, including one for a file held open withoutFILE_SHARE_DELETE.deleteon the binary itself aren't checked (a denied delete on the directory is, through the create-and-remove probe).Testing
pkg/upgradeunit tests, including end-to-endUpgraderuns against anhttptestfake of the GitHub API: in-place replace, nothing newer (several current versions), dry run, permission failure before any download,--version(with an unparseable current version, missing tag, same version), symlinks (absolute and relative, read-only link or target dir, link to link, target already named like the release), an empty release list, an immutable binary (macOSchflags uchg, Linuxchattr +i) in a real run and a dry run, keeping the mode, and a stalled download cancelled once it has started (temp file removed, stale ones cleaned, also in a link's directories). Unit tests for the sticky rule, append-only files and directories, concurrent permission checks (binary and link stay in place throughout), unique temporary link names, keeping the group and the owner warning. Run with-race, and on Linux in Docker as a non-root user and as root withCAP_LINUX_IMMUTABLE(for thechattrtests, on tmpfs and overlayfs). The sticky check was also run against real root-owned files in sticky directories in Docker.go vetand golangci-lint v1.60.1 for darwin, linux and windows on the changed packages.PATHsymlink, link to link, read-only target dir, a real upgrade through a relative symlink (v0.8.0-alpha.1→v0.8.0, previous binary untouched), and a real in-place upgrade.🤖 Generated with Claude Code