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

Pinner

Docs


Pin floating dependency versions across mise, Node, Python, Docker, GitHub Actions, Terraform, Helm, and Kubernetes. Rewrite manifests to exact pins, commit a unified pinner.lock.json, and fail CI when the graph drifts.

Install

Linux / macOS (curl installer):

curl -fsSL https://raw.githubusercontent.com/zloeber/Pinner/main/scripts/install.sh | bash

Installs to ~/.local/bin/pinner (override with PINNER_INSTALL_DIR). Pin a version with PINNER_VERSION=0.2.0, or dry-run with PINNER_INSTALL_DRY_RUN=1.

Download a multi-platform binary manually from GitHub Releases (Linux/macOS/Windows), or build from source:

cargo install --locked --path crates/pinner

Or via mise backends (availability depends on mise version and published package name):

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

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

Curl installer and cargo install remain first-class.

Quick start

# Resolve floating refs, rewrite sources, write pinner.lock.json
pinner pin
pinner pin --recursive             # include subdirectories
pinner pin --path ./services/api  # target a specific directory scope

# Bump exact pins to latest (rewrite + lock)
pinner upgrade
pinner upgrade --walkthrough   # interactive accept/skip/edit (TTY)
pinner upgrade --script        # emit native package-manager upgrade script
pinner upgrade --continue-on-ecosystem-error  # warn and continue when one ecosystem fails

# Drift gate (no writes) — exit 1 on mismatch, 2 on tool/config errors
pinner check

# Report floating refs; optionally apply pins
pinner audit
pinner audit --fix

# Explain why a pin was chosen
pinner explain <name-or-path>

Agents and CI should use --agent / --format json — never --walkthrough in automation.

Provider support

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; tasks map-only until marketplace HTTP

Per-provider Pin / Upgrade / Check / Gaps: docs/guide/ecosystems/.

Config (pinner.toml)

Optional repo-root overrides:

[ecosystems]
mise = true
node = true
python = true
docker = true
actions = true
terraform = true   # default on
helm = true        # opt-in (default off)
k8s = true         # opt-in (default off)

ignore = ["**/node_modules/**", "**/.git/**", "**/vendor/**", "**/tests/fixtures/**"]

[toolchain]
install = true

[pinning]
exact_ranges = true

Global flags: --config, --offline, --dry-run, --ecosystem mise,node, --format text|json.

--ecosystem filters already-enabled kinds from policy; it does not turn on opt-in ecosystems. To pin Helm or Kubernetes, set helm = true / k8s = true in pinner.toml (or rely on defaults for Terraform).

IaC ecosystems (Terraform, Helm, Kubernetes)

EcosystemDefaultPin styleSources
terraformonexact semver for registry modules/providers; git/HTTP module sources → full commit SHA in ?ref=*.tf / *.tofu — remote module blocks and required_providers
helmoff (opt-in)exact chart version strings; floating images in values*.yaml → name@sha256:…Chart.yaml dependencies; Flux HelmRelease; Argo CD Application; values.yaml / values*.yaml images
k8soff (opt-in)container images → name@sha256:…YAML workloads: Deployment, StatefulSet, DaemonSet, Job, CronJob

What is skipped

  • Terraform: local module sources (./, ../, absolute paths); CLI required_version; .terraform/ directory during discovery.
  • Kubernetes: non-workload kinds (ConfigMap, HelmRelease, etc.).

Resolution (lock, native evidence, env maps)

Resolve order matches other ecosystems: existing pinner.lock.json pins first, then ecosystem-specific evidence, then network/tool resolve when online.

  • Terraform providers: may use .terraform.lock.hcl provider selections when present.
  • Terraform git modules: when online, git ls-remote can resolve a floating ref to a full SHA (rewritten as ?ref=<full-sha>).
  • Terraform registry modules/providers: when online (and map/lock miss), HTTP GET against registry.terraform.io version APIs selects the latest matching version. Offline still requires a lock entry or PINNER_TERRAFORM_RESOLVE_MAP.
  • Helm charts: when online (and map miss), HTTP repo index.yaml (and OCI tags list) can resolve chart versions. Prefer PINNER_HELM_RESOLVE_MAP for offline/tests.
  • Helm values images / K8s images: use the shared Docker/buildx digest helper when online; maps preferred in tests.

Test / CI resolve-map env vars

Comma-separated {name}@{requested}=pinned pairs (see pinner_iac_common::parse_resolve_map / resolve_map_lookup). Prefer the name@requested form so shared constraints (e.g. ~> 5.0) do not collide across artifacts. Lookup tries name@requested first, then a legacy bare requested key.

Keys may contain = (e.g. Terraform git ?ref= URLs); use the last = as the separator. Empty requested (missing Helm chart version) uses {name}@= → key {name}@.

export PINNER_TERRAFORM_RESOLVE_MAP='vpc@~> 5.0=5.1.0,hashicorp/aws@~> 5.0=5.100.0,git_mod@git::https://example.com/org/mod.git?ref=main=11bd71901bbe5b1630ceea73d27597364c9af683'
export PINNER_HELM_RESOLVE_MAP='redis@^1.0.0=1.2.3,ingress-nginx@=4.10.0,nginx@nginx:latest=nginx@sha256:abc123…'
export PINNER_K8S_RESOLVE_MAP='nginx@nginx:latest=nginx@sha256:abc123…'

Network integration tests may require PINNER_NETWORK=1. With --offline, resolution fails closed unless the lock or resolve map supplies every pin.

Example opt-in Helm/K8s in pinner.toml:

[ecosystems]
helm = true
k8s = true

Toolchain

Resolver binaries (mise, node/npm, uv, docker, gh) can be detected or installed when policy allows:

pinner toolchain status
pinner toolchain ensure

Prefer a preinstalled mise (checked on PATH and common locations such as ~/.local/bin/mise). toolchain.install = true (or ensure with install allowed) may install tools through mise, but will not download mise via curl|sh unless you also set:

export PINNER_BOOTSTRAP_MISE=1
pinner toolchain ensure

That bootstrap prints a warning to stderr. Prefer installing mise yourself in CI images and developer machines.

Runtime resolution prefers pinner.lock.json and native locks (package-lock.json / pnpm-lock.yaml / yarn.lock, uv.lock / poetry.lock / pdm.lock), so tools are not required for every check once the lock is complete.

Actions / Compose rewrite notes

GitHub Actions uses: and Compose image: rewrites are line-oriented and preserve indentation for common list forms (- uses: …, nested steps:). Full YAML comment round-trips are not guaranteed for exotic multi-line or flow-style nodes.

CI

See .github/workflows/consumer-example.yml for a documentation-only sample. Typical gate:

- uses: actions/checkout@v4
- run: cargo install --locked --path crates/pinner # or release binary
- run: pinner toolchain ensure
- run: pinner check

Lock schema

pinner.lock.json is validated against schemas/pinner.lock.schema.json.

Development

Tooling for this repo is pinned in .mise.toml and orchestrated via Taskfile.yml (taskfile.dev).

Bootstrap

# Install mise if needed: https://mise.jdx.dev/getting-started.html
mise trust
mise install          # rust (+rustfmt/clippy), node, uv, gh, task
task setup            # verifies binaries (docker is optional/host-provided)

Pinned tools:

ToolPurpose
rust 1.96.0 (+ rustfmt, clippy)Build and test Pinner
node / npmNode ecosystem resolver evidence
uvPython ecosystem resolver evidence
ghGitHub Actions resolve
taskTask runner
dockerHost-installed only (not via mise)

Common tasks

task                  # list tasks
task setup            # mise install + version smoke checks
task build            # cargo build --workspace
task test             # cargo test --workspace
task fmt              # cargo fmt
task fmt:check        # formatting gate
task clippy           # clippy -D warnings
task ci               # fmt:check + clippy + test + schema
task ci:local         # lean CI summary (preferred before push)
task docs             # build mdBook into ./book (requires mdbook)
task run -- pin --dry-run
task pinner:audit

Fixture matrix under tests/fixtures/*-floating covers mise, node, python, docker, actions, terraform, helm, and k8s.

CI, docs, and releases

WorkflowTriggerPurpose
ci.ymlpush / PRlint (fmt + clippy -D warnings) and test (workspace + schema)
docs.ymlpush to main (docs paths)Build mdBook and deploy GitHub Pages
semantic-release.ymlpush to mainConventional commits → annotated tag vX.Y.Z (needs PAT_TOKEN)
release.ymltag v*.*.*Multi-platform release binaries + GitHub Release

Cutting a release

Automated (preferred): merge conventional commits to main (feat:, fix:, feat!: / BREAKING CHANGE). Semantic-release tags vX.Y.Z; the release workflow builds binaries. Seed PAT_TOKEN once via Secretfile.yml / SecretZero (see releasing guide).

Manual:

git tag -a v0.2.0 -m "v0.2.0"
git push origin v0.2.0

No Cargo.toml pre-bump required—the release job aligns workspace version from the tag. pinner --version prefers the latest v* git tag. Details: docs/guide/releasing.md.

GitHub Pages setup (one-time)

In the repo Settings → Pages, set Source to GitHub Actions. The first successful docs workflow on main publishes the site.

Local CI before push

Agents and humans should run the lean gate before pushing:

scripts/ci-local          # fmt + clippy + test + schema (short output)

Cursor enforces this via .cursor/rules/pre-push-local-ci.mdc and a beforeShellExecution hook on git push. Optional git hook:

git config core.hooksPath .githooks   # local only

Emergency skip: PINNER_SKIP_LOCAL_CI=1 git push.