Docs
Referencesince 1.0.0-alpha.3

Upgrade penv

What penv upgrade replaces, the signature and the digest it checks first, and what to do when it refuses.

penv upgrade replaces the file you are running with the build for this machine from the latest release. The flags are on penv upgrade. This page is what happens between the command and the new binary.

What it replaces

penv reads the release from https://penv.cloud/releases/latest and downloads from https://penv.cloud/releases/download/<tag>/<asset>. Both addresses are compiled into the binary and neither takes an override.

The asset is the raw binary that sits beside the archives, named penv-<tag>-<triple>, with an .exe suffix on Windows. penv takes the raw file because it carries no decompressor.

MachineTarget triple
Linux x86_64x86_64-unknown-linux-musl
Linux arm64aarch64-unknown-linux-musl
macOS x86_64x86_64-apple-darwin
macOS arm64aarch64-apple-darwin
Windows x86_64x86_64-pc-windows-msvc
Windows arm64aarch64-pc-windows-msvc

A machine outside that list gets unknown_target, which names the six triples the releases carry.

The new file lands as penv.new beside the running one. penv then renames the running binary to penv.old, moves penv.new into its place, and deletes penv.old. Windows cannot delete a file it is executing, so it leaves penv.old behind and the next penv command sweeps it. When the rename fails, penv puts the old binary back and says nothing was replaced.

Which release counts as newer

Versions compare as a numeric triple, and a prerelease sorts below the release it leads to. So 1.0.0 is above 1.0.0-alpha.1, 1.0.0-alpha.10 is above 1.0.0-alpha.2, and build metadata after a + is ignored. When the release is not newer, penv prints that you are current and exits 0 without downloading anything.

Look before you replace

penv upgrade --check

--check reads the release and changes nothing. It resolves this machine's target first, so an unsupported machine is refused here too, and it prints either penv <version> is current or the version that is out with the command to run. In JSON it answers current and latest.

What penv proves before it writes

Every release signs its checksum file with penv's Ed25519 release key, and the public half of that key is compiled into your binary. The order matters: the signature stands in front of the digest, because an unsigned checksum file says nothing about the binary it lists.

Download the raw binary and the penv-<tag>-<triple>.sha256 file beside it.

Download penv-<tag>-<triple>.sha256.sig, the Ed25519 signature over that checksum file, and verify it against every release key this build carries. A rotation lists two keys, so a release signed by either one lands.

Read the digest for this exact asset name out of the checksum file. The name is matched whole, so the archive's line is never read for the raw binary, and the line only counts when it is the 64 hex characters sha256 spells.

Compare that digest with the sha256 of what was downloaded, then swap the file.

On success penv reports the version it came from, the version it went to, the path it wrote, the asset name, sha256 and ed25519.

penv upgrade is stricter than either installer. install.sh checks the signature only where OpenSSL 1.1.1 or newer is on PATH and otherwise installs on the digest alone, and install.ps1 always installs on the digest, because .NET carries no Ed25519. See install the CLI.

When a package manager owns the binary

penv resolves this machine's target, reads the release, and compares the versions before it looks at the path it is running from. So a managed install that is already on the latest release prints penv <version> is current and exits 0, and you hear about the manager only when there is something to install. At that point the path is checked ahead of the release key, so the manager's own command is what you are told to run rather than a reinstall this build could not perform anyway.

ManagerThe path that names itRun this instead
npm/node_modules/npm i -g @penvhq/cli
Homebrew/opt/homebrew/, /usr/local/Cellar/, /home/linuxbrew/brew upgrade penv
Nix/nix/store/nix profile upgrade penv
winget/Microsoft/WinGet/winget upgrade penv
Scoop/scoop/apps/scoop update penv

npm is matched first, because a global npm install lands under whichever prefix installed Node, Homebrew's included. All five answer managed_install. Replacing the file behind a manager would leave that manager describing a version that is gone.

Every refusal, and what to do

errorWhat happenedWhat to do
unsigned_buildThis build carries no release key, so it cannot tell a penv release from a file somebody put in its place.Install from https://penv.cloud/install. That binary carries the key and upgrades from then on.
signature_missingThe release lists no signature for its checksum file. Nothing was replaced.Install from https://penv.cloud/install, and report a release that stays unsigned.
signature_invalidThe checksum file is not signed by a penv release key. Nothing was replaced.Install from https://penv.cloud/install, and report it.
no_checksumThe checksum file lists no sha256 digest for this asset.Install from https://penv.cloud/install until that release is fixed.
checksum_mismatchWhat was downloaded is not the file the checksum names. Nothing was replaced.Try again, and report it if it happens twice.
missing_assetThe release carries no binary, checksum or signature for this target.Install from https://penv.cloud/install until that release is fixed.
managed_installnpm, Homebrew, Nix, winget or Scoop owns this file.Run that manager's upgrade command, or install from https://penv.cloud/install somewhere else.
unknown_targetpenv publishes no build for this processor and operating system.Build from source.
unwritable_binaryThe file could not be replaced. Nothing changed.Run it again where you may write that directory, or reinstall.
binary_lostThe new file did not land and the old one could not be put back.Reinstall from https://penv.cloud/install.
no_releaseThe release address answered 404, or the latest release carries no tag.Try again later, or install from https://penv.cloud/install.
release_unreachableThe release address could not be read.Check the network and try again.
unreadable_releaseThe address answered something that is not a release.Try again later, or install from https://penv.cloud/install.
rate_limitedThe release host caps unauthenticated requests by the hour and this machine has spent them.Wait for the reset the message names, or install from https://penv.cloud/install.
no_current_exepenv could not read its own path.Install from https://penv.cloud/install.

Every one of these exits 1. --check can answer unknown_target, the release-reading refusals and unsigned_build, and nothing else, because it never reaches a download.

Rotating the release key

A build trusts the list of keys compiled into it and reads no revocation anywhere. Rotation is three steps and no flag day: add the new key to the list, ship a release signed with the old key that carries both, then drop the old key from the release after that.

A compromised key has no revocation path in binaries already installed. The answer is to rotate, say so, and have people reinstall from the installer.

Next: error codes.