Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Upgrade Subcommand Design

Date: 2026-08-05
Status: Approved for implementation planning
Summary: Add an opt-in pinner upgrade command that re-resolves declared dependencies to the newest available versions per ecosystem (via package managers, registries, or other means), then reuses pin’s rewrite → lock → walkthrough/agent path with a side-by-side upgrade TUI.

Problem

pinner pin freezes floating refs and then prefers pinner.lock.json / native locks, so a second pin is idempotent and will not bump already-exact pins. Teams still need a deliberate, reviewable way to move libraries, binaries, images, and modules to the most recent versions across ecosystems — without learning each package manager’s upgrade UX.

Pinner should eat that complexity: one optional subcommand, per-provider upgrade strategies (prefer Rust-based tools like uv where applicable), the same walkthrough/agent contract as pin, and clear documentation of what each provider supports.

Goals

  1. Opt-in upgrade CLI: pinner upgrade — never implied by check or CI; humans/agents must invoke it.
  2. Latest-by-default policy: resolve to the newest available version/digest/SHA appropriate for each ecosystem (including major bumps). Walkthrough accept/skip/edit is the safety valve.
  3. Same write path as pin: rewrite manifests → regenerate pinner.lock.json; honor --dry-run, --ecosystem, --offline, --walkthrough, --agent / --format json.
  4. Side-by-side review: walkthrough shows current pin → proposed upgrade (not only requested → proposed).
  5. Per-provider upgrade means: each ecosystem declares preferred tool(s) and fallback channels (HTTP/registry/git); prefer Rust-based PMs (uv) when choosing among peers.
  6. Documented support matrix: root README.md plus per-provider pages under docs/guide/ecosystems/.
  7. Install docs via mise: document cargo: and github: backends as the supported mise install targets for Pinner itself.

Non-goals

  • Replacing Renovate/Dependabot or opening PRs.
  • Constrained upgrades by default (--semver patch|minor|major may be a later flag; v1 is latest-only).
  • Guaranteeing post-upgrade build/test success.
  • Upgrading path/git/VCS/local module sources (same skips as pin).
  • Changing pinner check semantics (still drift vs committed lock).

Decisions

TopicChoice
Command namepinner upgrade
Default version policyLatest available (majors allowed)
CandidatesAll upgradeable declared deps from extract (floating and exact), minus pin’s existing skips (path/git/VCS/local)
Lock preferenceBypass pinner.lock.json and native locks for upgrade resolve; test seams via PINNER_*_RESOLVE_MAP still win when set
ArchitectureResolveMode::{Pin, Upgrade} on EcosystemCtx + upgrade / upgrade_with_filter in core sharing pin’s rewrite/lock pipeline
OfflineFail closed unless resolve map supplies every candidate (same exit 2 contract)
WalkthroughSame accept/skip/edit/quit; columns show current → proposed
Agent modeSame as pin: JSON, no prompts; --walkthrough + agent/non-TTY → exit 2
DocsREADME matrix + docs/guide/ecosystems/<kind>.md for every EcosystemKind
mise install of PinnerDocument cargo:pinner and github:zloeber/Pinner backends (not a vague mise install pinner)

Approaches considered

A — Separate Upgrade trait method per ecosystem (rejected)

Add fn upgrade_resolve(...) beside resolve. Clear intent, but doubles every crate’s surface and duplicates rewrite/lock orchestration.

B — Fork orchestration only; hack resolve with empty lock (rejected)

Call existing resolve with lock_pins: &[] and somehow ignore native locks. Brittle: many crates still short-circuit on native locks / treat exact requested as already resolved.

C — ResolveMode on context + shared upgrade orchestration (chosen)

Extend EcosystemCtx with resolve_mode. Ecosystems branch at the top of resolve: Upgrade skips freeze evidence and runs “newest” strategies. Core adds upgrade / upgrade_with_filter that select all upgradeable findings and reuse pin’s staging/rewrite/lock/walkthrough hooks.

Why C: Minimal CLI/core duplication, one evidence model, testable mode flag, incremental per-ecosystem enablement.

Architecture

pinner upgrade [--walkthrough|--agent|…]
    → core::upgrade_with_filter
         discover / extract (all findings)
         filter upgradeable (!skipped kinds; respect allow_floating? no — upgrades still apply)
         ecosystem.resolve(findings, ctx with ResolveMode::Upgrade)
         walkthrough filter (optional)
         rewrite + write pinner.lock.json   # same as pin
flowchart TD
  cli[pinner upgrade] --> orch[core upgrade_with_filter]
  orch --> extract[discover + extract all]
  extract --> mode{ResolveMode Upgrade}
  mode --> skipFreeze[Skip pinner.lock and native lock]
  skipFreeze --> maps[PINNER resolve maps if set]
  maps --> tools[PM tool / registry / HTTP / git]
  tools --> pins[Proposed Pins with metadata]
  pins --> wt{walkthrough?}
  wt -->|yes| tui[TUI current to proposed]
  wt -->|no agent| apply[rewrite + lock]
  tui --> apply

EcosystemCtx

#![allow(unused)]
fn main() {
pub enum ResolveMode {
    Pin,
    Upgrade,
}

pub struct EcosystemCtx<'a> {
    pub repo: &'a Path,
    pub lock_pins: &'a [Pin],
    pub offline: bool,
    pub pin_exact_ranges: bool,
    pub resolve_mode: ResolveMode,
}
}
  • Pin mode: unchanged order — lock → native lock → map → tool/registry → fail.
  • Upgrade mode: map (test/offline seam) → tool/registry/HTTP/git “latest” → fail. Do not return existing lock/native versions unless they are already newest (optional short-circuit after fetching latest: if equal, omit from proposed set or include with unchanged metadata — v1 omits unchanged so walkthrough only shows real bumps).

Pin metadata (upgrade)

When proposing a bump, set:

KeyValue
upgradetrue
previousprior exact pin / declared exact / prior digest-or-SHA string shown as “current”
upgrade_channel"tool" | "registry" | "http" | "git" | "map"

requested remains the manifest’s declared constraint/version. pinned is the new target. Walkthrough displays previous → pinned when upgrade is true, else requested → pinned.

Core orchestration

Mirror pin / pin_with_filter:

  • upgrade / upgrade_with_filter in crates/pinner-core/src/orchestrate.rs
  • Candidate set: all extracted findings that are upgradeable (not path/git/VCS/local — ecosystems already omit those in extract; allowlisted floating refs are still upgradeable unless we later add upgrade.ignore)
  • Empty proposed set (everything already latest) → success, no writes (or rewrite lock timestamps only — v1: no writes, report upgraded: 0)
  • Walkthrough abort → exit 0, nothing written

CLI

SurfaceBehavior
pinner upgradeRun upgrade orchestration
Global flagsSame as pin: --dry-run, --offline, --ecosystem, --config, --format, --walkthrough, --agent
Exit codes0 ok/abort-clean/no-op; 1 unused for upgrade success path (no “drift”); 2 tool/config/resolve/invalid mode

check / pin / audit unchanged.

TUI

Extend crates/pinner-ui/src/walkthrough.rs:

  • Column title: current → proposed when any pin has metadata.upgrade == true, else keep requested → proposed.
  • Cell text: previous → pinned for upgrades; requested → pinned otherwise.
  • Optional header subtitle: upgrade review when in upgrade mode (pass a WalkthroughKind or detect via metadata).
  • Keys unchanged: accept / skip / edit / quit.

Toolchain

Extend required_tools for upgrade-capable ecosystems that need CLIs (cargo online resolve may need cargo or HTTP; go may need go; ruby may use HTTP or gem). Prefer:

EcosystemPreferred upgrade toolFallbacks
pythonuvresolve map
nodenpm (npm view); detect pnpm/yarn locks for rewrite context onlyresolve map
misemise (mise latest / ls-remote)resolve map
cargocrates.io HTTP (Rust, no extra PM) or cargo search/cargo info if presentresolve map
gogo list -m -u when go presentproxy.golang.org HTTP; map
rubyRubyGems HTTP (no Bundler required); gem optionalmap
docker / k8s / gitlab images / actions imagesdocker buildx digests for latest tag or retarget digest for same floating tag policy: upgrade retags mutable tags to fresh digest; exact digest pins re-resolve via tag in metadata if present else skipmap
actions (uses)gh api → default branch / latest release SHA per policy: latest release tag’s commit, else default branch HEADmap
terraformregistry HTTP latest matching unconstrained latest version (not ~> floor)git ls-remote for git modules; map
helmrepo/OCI latest chart versionmap
azure tasksmarketplace/version API or map-only until HTTP existsmap
gitlab includesgit ls-remote HEAD/defaultmap

Exact command strings are owned by each crate and listed in per-provider docs.

Per-provider support matrix (normative)

Document this table on the root README and expand each row in docs/guide/ecosystems/<kind>.md.

ProviderDefaultPreferred upgrade meansAlso supportedUpgrade pin styleNotes
miseonmise latest / mise ls-remotePINNER_MISE_RESOLVE_MAPexact tool versionNested mise configs included
nodeonnpm view <pkg> versionnative lock ignored in upgrade; mapexact version in package.jsonWorkspaces supported
pythononuv pip compile / uv resolvepoetry/pdm locks ignored in upgrade; mapexact ==Prefer uv over pip/poetry CLI
dockerondocker buildx imagetools inspectlocal inspect; mapname@sha256:…Upgrades digest for image reference
actionsongh api (latest release → SHA)docker digests for workflow images; map@<sha> + image digestsReusable/composite uses included
terraformonregistry HTTP latest versiongit ls-remote; .terraform.lock.hcl ignored in upgrade; mapexact version / full SHALocal modules skipped
helmopt-inHTTP index / OCI tags latestdocker digests for values images; mapexact chart ver / image digestRequires helm = true
k8sopt-indocker digestsmapimage digestWorkload kinds only
cargooncrates.io HTTP APIcargo CLI if used; mapexact semver in Cargo.tomlPath/git deps skipped
goongo list -m -uproxy.golang.org; mapexact module version
rubyonRubyGems HTTPgem optional; mapexact gem version
gitlabopt-indocker digests + git ls-remotemapdigest / SHAOpt-in in pinner.toml
azureopt-indocker digests; task versions via map/HTTPmapdigest / exact task verOpt-in

Support levels (for docs)

Each provider page must state:

  1. Pin — freeze floating → exact (existing).
  2. Upgrade — bump to latest (this feature): tools + fallbacks.
  3. Check — drift vs pinner.lock.json (unchanged).
  4. Gaps — what is not upgraded (path deps, etc.).

Install via mise (Pinner itself)

README Install section must replace vague mise install pinner with explicit backends:

# GitHub release backend (prebuilt)
mise use -g github:zloeber/Pinner

# Cargo backend (build from crates.io / git as configured)
mise use -g cargo:pinner

Document that backend availability depends on mise version and published package name; curl installer and cargo install remain first-class.

Config

Optional future [upgrade] table is out of scope for v1. No new required pinner.toml keys. Ecosystems still gated by existing [ecosystems] enable flags; --ecosystem filters enabled kinds only.

Errors and testing

  • Resolution failure → exit 2 with ecosystem, name, requested, hint (same as pin).
  • --offline without maps → fail closed.
  • Unit tests: ResolveMode::Upgrade bypasses lock fixtures; use resolve maps.
  • Integration: fixture with exact pins → upgrade proposes newer map/network versions → walkthrough skip leaves file unchanged; accept rewrites.
  • CLI smoke: upgrade --agent, upgrade --walkthrough --agent → exit 2.
  • Idempotency: upgrade when already latest → upgraded: 0, no diff.

Documentation deliverables

ArtifactChange
README.mdUpgrade quick-start; full provider×PM matrix; mise cargo: / github: install
docs/guide/quick-start.mdAdd pinner upgrade (+ walkthrough)
docs/guide/configuration.mdNote upgrade uses same globals; no new config keys in v1
docs/guide/ecosystems/*.mdOne page per provider (13)
docs/SUMMARY.mdLink ecosystem pages + this spec
skills/pinner/SKILL.mdAgent playbook: upgrade vs pin

Success criteria

  • pinner upgrade bumps exact pins to newer versions when evidence exists.
  • Walkthrough shows current → proposed and can skip/edit per row.
  • --agent upgrade emits JSON and writes only when not dry-run.
  • README matrix and all 13 provider pages list preferred PM/channel.
  • mise install docs mention only github:zloeber/Pinner and cargo:pinner as mise backends.
  • scripts/ci-local green after implementation.

Implementation plan

See docs/superpowers/plans/2026-08-05-upgrade-subcommand.md.