mirror of
https://github.com/Twigpine/zero.git
synced 2026-11-11 23:40:28 -10:00
* feat(update): add zero upgrade command to apply self-updates Extends the existing `zero update --check` (which only reported an available release) with an actual install step: - `zero update --apply` / `zero upgrade` download, sha256-verify, and install the latest release. - npm installs delegate to `npm install -g @gitlawb/zero@latest`. - Standalone installs download the release archive, verify its checksum, extract it (zip-slip guarded), and atomically replace the running binary plus any pre-existing optional sandbox helpers. On Windows, the running exe is renamed aside (NTFS allows this even while it's executing) since it can't be overwritten directly; the leftover ".old" file is cleaned up on a later invocation. Closes #459. * fix(update): correct macOS expectations in apply test The optional-helper assertion in TestApplyStandaloneUpdateReplacesBinary assumed every non-Windows platform has a linux-style "zero-seccomp" helper to refresh, but applyStandaloneUpdate correctly ships no optional helpers on macOS (matching scripts/postinstall.mjs). This made the test fail on macOS CI. Only assert the helper refresh on linux/windows. * fix(update): address CodeRabbit review findings on zero upgrade - Reject --target when combined with --apply/upgrade: applying installs onto the current machine, so a mismatched --target could replace the running binary with an incompatible-arch build. - Bound archive/checksum downloads with a dedicated timeout separate from the release-metadata check timeout, so a stalled connection can't hang zero upgrade forever. - Surface a failed rollback in the Windows replaceBinary path instead of swallowing it, so a botched install is reported rather than silently leaving the binary missing. - Run CleanupStaleBinary regardless of whether an update is available, so a leftover Windows ".old" file isn't stuck waiting for a future upgrade that may never come. - Report optional sandbox-helper refresh failures as warnings on ApplyResult (surfaced in text and JSON output) instead of discarding them silently. * fix(oauth): retry secret-lock on Windows delete-pending race to unblock CI TestLoadOrCreateSecretConcurrentConverges was failing this PR's Windows smoke job with "create token secret lock: ...Access is denied." — a pre-existing flake in internal/oauth, not in the upgrade code. On Windows a concurrent holder's os.Remove leaves the .lock in a delete-pending state, so an O_EXCL create races it with ERROR_ACCESS_DENIED (os.ErrPermission) rather than ErrExist; createSecretFile only treated ErrExist as contention and hard-failed otherwise. Treat os.ErrPermission as retryable too, mirroring acquireFileLock in lock.go. This is the same root-cause fix as #445 (diagnosis by @gnanam1990); folded in here to make this PR's CI green. Verified on Windows: the concurrent-converge test passes 30/30. --------- Co-authored-by: Vasanthdev2004 <[email protected]>
35 lines
1.3 KiB
Go
35 lines
1.3 KiB
Go
//go:build windows
|
|
|
|
package update
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
)
|
|
|
|
// replaceBinary installs newPath over targetPath. Windows will not let a
|
|
// running executable be overwritten or deleted directly, but NTFS does allow
|
|
// renaming it aside — the same trick already used for locked config files in
|
|
// internal/cli/mcp_config.go's replaceMCPWritableConfigFile.
|
|
func replaceBinary(targetPath string, newPath string) error {
|
|
oldPath := targetPath + ".old"
|
|
_ = os.Remove(oldPath) // best-effort cleanup of a leftover from a previous upgrade
|
|
if err := os.Rename(targetPath, oldPath); err != nil {
|
|
return fmt.Errorf("rename running binary aside: %w", err)
|
|
}
|
|
if err := os.Rename(newPath, targetPath); err != nil {
|
|
if restoreErr := os.Rename(oldPath, targetPath); restoreErr != nil {
|
|
return fmt.Errorf("install new binary: %w; additionally failed to restore the original binary: %v (original preserved at %s)", err, restoreErr, oldPath)
|
|
}
|
|
return fmt.Errorf("install new binary: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// CleanupStaleBinary best-effort removes a "<path>.old" file left behind by a
|
|
// previous replaceBinary call once the old process holding it has exited.
|
|
// Callers should invoke this once at startup for the current executable.
|
|
func CleanupStaleBinary(targetPath string) {
|
|
_ = os.Remove(targetPath + ".old")
|
|
}
|