Add 'ghost upgrade' command to self-upgrade the CLI#5
Conversation
4c16954 to
a87770d
Compare
|
|
||
| cmd := &cobra.Command{ | ||
| Use: "update", | ||
| Aliases: []string{"upgrade"}, |
There was a problem hiding this comment.
Are 100% sure we want to take ghost upgrade as an alias for this? Main reason I ask is because it was previously proposed as a way to upgrade a standard/free instance to a dedicated/paid instance (though this was pretty early on, before we actually settled on the "dedicated" terminology). As of right now, you can't do that kind of upgrade (you have to fork to a dedicated instance instead), but I imagine it might be something we want in the future, and ghost upgrade could be a useful command for it.
There was a problem hiding this comment.
Discussed in this Slack thread - we agreed to make upgrade the primary command, with update as an alias.
This is the most common verb used in related CLI commands in our experience, and we can always change it in the future if we really need to.
Adds a new 'ghost update' command (alias 'upgrade') that downloads the latest published release archive and replaces the currently running binary in place, using the same CloudFront-hosted artifacts as scripts/install.sh and scripts/install.ps1. Key behaviors: - Fetches the target version via common.CheckVersion (reuses the existing version-check plumbing), or accepts --version vX.Y.Z to pin a specific release. - Refuses to run if the binary was installed via a package manager (Homebrew / apt / yum-dnf), and tells the user which command to use instead. Pass --force to override. - Refuses to replace a local dev build without --force, and reports which version would be installed. - Short-circuits with 'ghost is already at version X' when the current version matches the target (unless --force). - Pre-flight write check on the install directory so we fail fast before downloading a ~15 MB archive. - Downloads the archive + its .sha256, verifies with crypto/sha256, and extracts just the ghost / ghost.exe entry using archive/tar + gzip (Linux/macOS) or archive/zip (Windows). No new dependencies. Cross-OS binary replacement: - Linux/macOS: atomic os.Rename onto the current path. POSIX keeps the running inode alive via the process's open handle, so overwriting a running executable is safe. - Windows: the loader refuses to delete or overwrite a running .exe, but renaming is permitted. We move the live binary to ghost.exe.old.<unixNano>, then os.Rename the staged binary into place, with a rollback if the second rename fails. Leftover .old.* files are opportunistically cleaned up on subsequent updates. - Symlinks are resolved via filepath.EvalSymlinks so we touch the real binary rather than clobbering the symlink. Tests cover --version argument validation (for both 'update' and 'upgrade' aliases), the dev-build refusal, and the wrapped 'failed to check for latest version' network error.
a87770d to
ff19965
Compare
Adds a new
ghost upgradecommand (aliasupdate) that downloads the latest published release archive and replaces the currently running binary in place, using the same CloudFront-hosted artifacts (${releases_url}/latest.txtand${releases_url}/releases/<version>/<archive>) asscripts/install.shandscripts/install.ps1.Behavior
common.CheckVersionplumbing), or--version vX.Y.Zto pin.brew upgrade ghost). Pass--forceto override — a warning is printed and the install proceeds anyway.--force, and reports which release version would be installed so the user knows what they're switching to.ghost is already at version Xwhen current == target (unless--force)..sha256, verifies withcrypto/sha256, and extracts just theghost/ghost.exeentry usingarchive/tar+compress/gzip(Linux/macOS) orarchive/zip(Windows). No new dependencies.Cross-OS binary replacement
os.Renameonto the current path. POSIX keeps the running inode alive via the process's open handle, so overwriting a running executable is safe and the running process keeps running off the old inode..exe, but renaming is permitted. We move the live binary toghost.exe.old.<unixNano>, thenos.Renamethe staged binary into place, with a rollback if the second rename fails. Leftover.old.*files are opportunistically cleaned up on subsequent upgrades — they stay locked until the previous process exits, after which the nextghost upgraderun can delete them.Symlinks are resolved via
filepath.EvalSymlinksfirst, so we touch the real binary rather than clobbering a symlink.Tests
internal/cmd/upgrade_test.gocovers:--versionargument validation rejects non-semver input (for bothupgradeandupdatealiases), before any network work.config.Version == "dev"in the test binary.failed to check for latest version:network error.Full end-to-end install success isn't covered in tests because it would require simulating real binary replacement of the test process itself; the extraction, checksum, and replacement helpers are pure-io functions that are easy to exercise manually.
Manual testing
Confirmed locally on darwin/arm64:
Dev-build refusal (binary built outside of any package-managed path):
Docs
README.md— new row in the Commands table.CLAUDE.md—upgradeadded to theinternal/cmd/command list.docs/cli/regenerated (newghost_upgrade.md, updatedghost.md).