Yank and deprecate
Two operations that sound similar and are not.
| Effect on resolution | Reversible | Deletes anything | |
|---|---|---|---|
| Yank | stops being chosen | yes | no |
| Deprecate | none | yes | no |
| Unpublish | version is gone | no | yes |
Yank a version nobody should get by accident: a bad build, a wrong commit, a release with a secret in it. Deprecate a version you would still rather people did not pick, but which works.
Neither deletes anything. unpublish, with its 72-hour window, remains the
only destructive operation.
Yank
A yanked version:
- is not deleted, and unyanking restores it;
- stays downloadable by exact version, so lockfiles that already pin it keep resolving and existing builds do not break;
- is flagged wherever the protocol has a place for the flag, and excluded where it does not;
- never answers as latest.
packr-cli yank cargo/serde@1.0.0 --reason "built from the wrong commit"
packr-cli unyank cargo/serde@1.0.0
Ecosystem-agnostic API:
POST /api/v1/admin/packages/yank
{"ecosystem":"cargo","name":"serde","version":"1.0.0","yanked":true,"reason":"β¦"}
Cargo also has native verbs, which the registry implements:
DELETE /cargo/api/v1/crates/{crate}/{version}/yank and PUT .../unyank.
How each ecosystem shows it
| Ecosystem | Representation |
|---|---|
| cargo | yanked: true in the sparse index |
| PyPI | PEP 592 data-yanked / "yanked" in the simple index |
| JVM | yanked on the index line |
| NuGet | listed: false β the protocol's own word for it |
| Go | absent from @v/list; .info, .mod and .zip keep serving so go.sum still verifies |
| Maven | absent from <versions> in maven-metadata.xml; the jar and pom keep serving |
| npm | excluded from latest resolution |
| Dart | retracted: true on the version entry; still downloadable by exact pin |
| Zig | absent from the version index; the pinned tarball URL keeps serving |
| OCI | absent from GET /v2/{name}/tags/list; the manifest and blobs keep serving by tag or digest |
| generic | absent from the version listing; the per-version files keep serving |
| gems | absent from the compact index; the pinned <name>-<version>.gem keeps serving |
| Swift | absent from the release map; the metadata and archive URLs keep serving by pin |
| Conan | absent from /v2/conans/search and excluded from β¦/{name}/*/_/_/latest resolution β the load-bearing half, because nothing in a range install reads the search results; the exact β¦/{name}/{version}/_/_/latest still resolves |
| Lua / LuaRocks | absent from manifest-<luaVersion>; the rock keeps serving by exact pin |
Five of these β Swift, Lua, gems, Go and Maven β have no yank field in their protocol at all, so absence from the listing is not a fallback, it is the only representation available. The rocks-server case is the sharpest of them: the manifest is a Lua source file whose type checker rejects unknown top-level keys, so a yank flag could not be added to it even if the client understood one.
Conan is the one row that is easy to get half right, and the half that looks
right is the useless one. There is no yank field either, so a yanked recipe
leaves the search index β and that part genuinely works, because an exact pin
never consults search (measured: a search returning [] still installed). But
a version range resolves server-side at β¦/{name}/*/_/_/latest, and nothing
in a range install reads the search results. So excluding a version from
search does not yank it: anyone who writes zlib/* still gets it. Conan's yank
is therefore two conditions, and only the second one is load-bearing.
TestEachProtocolRendersYankItsOwnWay in internal/conformance/yank_test.go
checks eleven of these rows against the adapters rather than trusting this table β
cargo, PyPI, NuGet, JVM, Go, Maven, gems, Swift, Dart and Lua. The npm, Zig,
OCI and generic rows are not covered by that test; they are here on the same
reasoning as the rest, which is to say they are unverified.
For those five, absence from the listing is the only representation, which is why the exact-pin routes deliberately keep working.
Deprecate
A deprecated version still resolves, still answers as latest if it is newest, and still installs. The only effect is that clients whose protocol has somewhere to put the message show it.
packr-cli deprecate pypi/widget@1.0.0 --message "use widget2"
packr-cli undeprecate pypi/widget@1.0.0
POST /api/v1/admin/packages/deprecate
{"ecosystem":"pypi","name":"widget","version":"1.0.0","message":"use widget2"}
An empty message clears the notice, matching npm deprecate "". A missing
message field is a 400 rather than a clear, so a malformed request cannot
silently undo a publisher's warning.
npm's own PUT /-/v1/deprecate still works, but only reaches npm packages.
Where it is rendered
| Ecosystem | Representation |
|---|---|
| npm | deprecated on the version, in the packument and the version manifest |
| NuGet | catalogEntry.deprecation, the protocol's own model |
| JVM | deprecated on the index line |
| generic | deprecated on the version entry in the version listing (internal/generic/generic.go:218) |
| cargo, PyPI, Maven, Go, OCI | not rendered |
The last row is deliberate. The cargo sparse index, PEP 503/691 and
maven-metadata.xml define no deprecation field, and Go expresses deprecation
as a // Deprecated: comment inside go.mod β which the registry must not
rewrite, because that changes the file's hash and breaks every go.sum that
already records it. Inventing a non-standard field those clients ignore would
look like support without being it. The notice is still stored and still
readable through the API for the seven ecosystems listed above.
Nine of sixteen. The registry has sixteen adapters, all sixteen enabled in production (
ENABLED_ECOSYSTEMSinblueforge.config.json: npm, go, cargo, jvm, maven, pypi, nuget, gem, composer, dart, zig, swift, oci, generic, lua, conan). Of the sixteen, gems, composer, zig, dart, swift, lua and oci have no deprecation rendering at all β grep those packages forDeprecatand you get nothing. The tables above cover the eight that do. An earlier revision of this line said "all seven" without noting that the other four were simply absent, which reads as coverage rather than a gap.Gems is the awkward one: a yanked gem is withdrawn from the compact index (
internal/gems/index.go:35), so yank half-works by a different mechanism, but the deprecation metadata surface npm and Maven have does not exist there. Composer is a GET-only mirror (internal/composer/composer.go:96-98) and has no publish path to deprecate in the first place. Dart and Zig do publish, and both render yank in their own protocol's terms β Dart asretracted:trueon the version entry, still downloadable by pin, and Zig through the version index β so the deprecation half is the only thing missing, not the lifecycle. OCI is the same shape one ecosystem over: tags leave/v2/{name}/tags/listwhen yanked (internal/oci/manifests.go:199) while the manifest keeps serving by tag or digest, but the OCI spec has no deprecation field to write, and a client that ignored an invented one would be no better informed than before. Swift is the same story: SE-0292 has no yank field either, so a yanked version leaves the release map while its metadata and archive URLs keep serving (internal/swift/read.go:94-97).The Conan row shipped with the adapter (
internal/conan/); it was previously listed as spec-only, fromdocs/spec-2026-10-conan.mdΒ§MEASURED Β§9, when the row described behaviour to build rather than shipped code. Its two halves are pinned separately: search-absence in the table above, range-exclusion inTestConanRangeExcludesYanked, because nothing in a range install reads search.Yank covers every publishable ecosystem; deprecation covers eight of sixteen. Fifteen of the sixteen have somewhere to represent a yank, and the sixteenth is Composer, which is a GET-only mirror with no publish path to yank in the first place. The two operations share a database column, which is why it is easy to assume the coverage matches β it does not. The two operations share a database column, which is why it is easy to assume the coverage matches β it does not.