Software Engineering · Dependencies

Yanking, Deprecation, and Retraction: Withdrawing a Version After Publication

The three distinct ways a publisher can withdraw a release, what each does to existing lockfiles and fresh resolutions, and why immutability is the property they all trade against.

Publishing a version is supposed to be irreversible. SemVer’s third rule says that once a version has been released, its contents must not be modified. The rule exists because dependents pin versions and verify digests, so a version number is an identifier for specific bytes, and changing the bytes under the same number breaks every guarantee built on that identity.

Publishers nonetheless need a way to withdraw a release. A version is published, a serious defect or a leaked credential is discovered, and the natural wish is to make it unavailable. Registries offer three mechanisms with different semantics, and conflating them is the source of most of the confusion around “deleting a package”.

The three are deletion, yanking, and deprecation. They differ along two axes: whether new resolutions can select the version, and whether existing pins continue to work. Getting the combination wrong either leaves a broken version installable by everyone or breaks builds that were working.

Deletion breaks the identifier

Deletion removes the artifact from the registry. Subsequent requests for that exact version fail, and any lockfile naming it fails to install.

This is the strongest and most destructive option. It violates the identifier property directly: the version number no longer resolves to anything, so a build that succeeded yesterday fails today with no change to its inputs. Reproducibility is broken retroactively, and the only recovery for a consumer is to change the version in the lockfile — which is to say, to change the program.

Most registries restrict deletion for this reason. Common policies allow deleting a version only within a short window after publication, or only if no other package depends on it, or not at all once it has been downloaded. Some allow deletion of a package name entirely only when it has no dependents. The restrictions encode the recognition that publication creates an obligation to existing consumers, and that the obligation is stronger than the publisher’s wish to withdraw.

The legitimate use of deletion is narrow: a version published by mistake (wrong contents, an accidental publish of a private package, a credential in the artifact) within a window before anyone could reasonably depend on it. Even then, the registry’s cache and mirrors may have already copied it, so deletion is not a guarantee of unavailability. Artifacts that have been fetched are outside the registry’s control.

Yanking removes the version from resolution but keeps it addressable

Yanking marks a version as not eligible for new resolution while leaving it retrievable by exact reference. A resolver selecting “the newest version satisfying ^1.2” will not consider a yanked 1.4.0 and will pick 1.3.9 instead. A lockfile that already names 1.4.0 continues to install it, because the lockfile bypasses resolution entirely.

That combination is the right default for a defective release. Existing builds keep working, since they pinned what they had. New resolutions avoid the defect. The publisher has withdrawn the version from the future without rewriting the past.

The mechanism relies on the resolver consulting the yank flag during candidate enumeration. It is therefore a registry-side property that every client must honour, and clients that ignore the flag — or cache metadata before the yank — will select the yanked version anyway. Cached metadata is the practical failure: a resolver with a stale index sees 1.4.0 as available, and the yank only takes effect once the index refreshes.

Yanking is also reversible. A version yanked by mistake can be un-yanked, which restores it to the candidate set. This matters because the operation is cheap and mistakes are common, and because irreversibility would make publishers reluctant to use it.

What yanking does not do is warn. A consumer whose lockfile contains a yanked version receives no signal from the registry during installation. Detecting that a build depends on a yanked release requires an explicit audit against current registry metadata, which is a different operation from installing. This is the gap that vulnerability scanning fills for security defects and that nothing fills for correctness defects.

Deprecation is metadata, not a constraint

Deprecation attaches a message to a version or to a package name. It changes nothing about resolution: a deprecated version is still a candidate, still satisfies ranges, still installs. The message is surfaced by clients that choose to display it, and ignored by clients that do not.

The distinction from yanking is intentional. A deprecated version may be perfectly functional; the publisher is signalling that it should not be used for new work, usually because a successor exists or because the package is unmaintained. Yanking such a version would be wrong, since it works and dependents may legitimately stay on it.

Deprecation is therefore advisory, and its effectiveness depends on where the message is displayed. A warning printed during installation reaches whoever runs the install, which in CI is a log nobody reads. A warning surfaced in a manifest review reaches a person. Neither prevents the dependency from being added.

Some registries allow a deprecation message to name a replacement, which makes automated tooling possible: a check can flag that a dependency is deprecated and that a named successor exists. Without that field the message is prose, and prose is not machine-actionable.

The immutability trade

All three mechanisms exist because publication is a commitment and withdrawal is sometimes necessary. The design question is which property to sacrifice.

Full immutability, meaning no deletion and no yanking, maximises reproducibility and makes every version permanently addressable. It also means a defective version stays in every resolver’s candidate set forever, and consumers must express exclusions in their own manifests. The exclusion syntax exists for exactly this purpose, and it is a per-consumer burden: every dependent of a defective release must independently discover the problem and write the exclusion.

Full mutability, with deletion permitted, makes withdrawal easy and reproducibility impossible. No lockfile can be trusted to install, since any named version may vanish.

The middle position, which most registries have converged on, is immutability of bytes plus mutability of eligibility. The artifact is never modified and never removed; its eligibility for new resolution can change. Yanking implements that. Deletion is reserved for the narrow window where no consumer could yet depend on the version.

There is a third dimension the middle position does not address: whether an artifact can be republished under the same version with different content. Registries that permit this break the digest guarantee in the lockfile: a pinned digest fails, which at least makes the problem visible, but only for consumers who verify. Registries that forbid it preserve the identifier property. The rule to check when evaluating a registry is not whether deletion is allowed but whether republishing is.

A consumer that must express an exclusion does so by splitting the range, which is the only tool the constraint language offers:

{
  "dependencies": {
    "liba": ">=1.2.3 <1.4.5 || >1.4.5 <2.0.0"
  }
}

That constraint admits 1.2.3 through 1.4.4 and everything above 1.4.5 below 2.0.0, and it rejects 1.4.5 exactly. It must be repeated in every manifest that depends on the package, because no client can inherit an exclusion from a sibling project.

What a consumer can do about a withdrawn version

The consumer’s options are limited by which mechanism the publisher used.

For a yanked version, the answer is usually to move forward. The lockfile keeps working, so the pressure is low, but the version is not receiving fixes and is not a candidate for anything new. Upgrading past it is a normal version change, and the range algebra determines whether the manifest needs editing: if the range was ^1.2.0 and the yanked version was 1.4.0, the resolver will now select 1.3.9 without any manifest change.

For a deleted version, the lockfile is broken and must change. The recovery is to find the nearest version that satisfies the constraints and is still present. If the deleted version was the only one satisfying a range, the constraints themselves must be revised, which may cascade to other packages.

For a deprecated version, nothing is forced. The decision is whether the deprecation reason applies: a deprecation due to a rename is cosmetic, one due to an unmaintained security posture is not.

The systemic response is to make withdrawal visible before it causes a failure. A periodic check that every locked version still exists and is not yanked converts a future install failure into a present report. The check requires registry metadata, so it is a network operation and cannot run inside a frozen install; it belongs in a scheduled job rather than a build.

Why the mechanisms cannot be merged

It is tempting to treat yanking and deprecation as one feature with a severity setting. They are separate because they answer different questions. Yanking answers “may this be selected”, which is a resolution property that clients must enforce. Deprecation answers “should this be selected”, which is a judgment that only a human can act on.

Merging them would either make deprecation enforceable — turning an advisory into a hard constraint, and breaking builds on the basis of a message that may be wrong — or make yanking advisory, which would return defective versions to the candidate set.

The separation is also why a registry cannot fix a defective release on the publisher’s behalf. It can remove the version from new resolutions, and it cannot remove it from the graphs that already contain it. The residual exposure is a property of the installed base, not of the registry, and it is why the audit has to be a consumer-side operation.

What applies to a withdrawn release applies with more force to a withdrawn name: a package that is abandoned, or transferred to a new maintainer, keeps resolving. The lookup mechanism has no notion of a name becoming untrustworthy, and only an explicit pin plus digest verification catches the change.

References

  1. The Cargo Book: cargo yank — The Cargo Book
  2. Python Packaging User Guide: Simple Repository API (yanked releases) — Python Packaging Authority
  3. Semantic Versioning 2.0.0, section 3 on immutability of released versions — semver.org