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.
| Machine | Target triple |
|---|---|
| Linux x86_64 | x86_64-unknown-linux-musl |
| Linux arm64 | aarch64-unknown-linux-musl |
| macOS x86_64 | x86_64-apple-darwin |
| macOS arm64 | aarch64-apple-darwin |
| Windows x86_64 | x86_64-pc-windows-msvc |
| Windows arm64 | aarch64-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.
| Manager | The path that names it | Run 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
error | What happened | What to do |
|---|---|---|
unsigned_build | This 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_missing | The 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_invalid | The checksum file is not signed by a penv release key. Nothing was replaced. | Install from https://penv.cloud/install, and report it. |
no_checksum | The checksum file lists no sha256 digest for this asset. | Install from https://penv.cloud/install until that release is fixed. |
checksum_mismatch | What was downloaded is not the file the checksum names. Nothing was replaced. | Try again, and report it if it happens twice. |
missing_asset | The release carries no binary, checksum or signature for this target. | Install from https://penv.cloud/install until that release is fixed. |
managed_install | npm, Homebrew, Nix, winget or Scoop owns this file. | Run that manager's upgrade command, or install from https://penv.cloud/install somewhere else. |
unknown_target | penv publishes no build for this processor and operating system. | Build from source. |
unwritable_binary | The file could not be replaced. Nothing changed. | Run it again where you may write that directory, or reinstall. |
binary_lost | The new file did not land and the old one could not be put back. | Reinstall from https://penv.cloud/install. |
no_release | The release address answered 404, or the latest release carries no tag. | Try again later, or install from https://penv.cloud/install. |
release_unreachable | The release address could not be read. | Check the network and try again. |
unreadable_release | The address answered something that is not a release. | Try again later, or install from https://penv.cloud/install. |
rate_limited | The 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_exe | penv 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.