Programming Languages Bug Report

Composer "could not find a version matching your minimum-stability" for our brand-new package

We published an internal package to our private Packagist and every app failed to require it: could not find a version matching your minimum-stability (stable). The package had no tags, only a main branch, so Composer saw only dev-main. How stability flags, tags and branch aliases work.

Rafael Domínguez Rafael Domínguez @rafaeldominguez · April 09, 2026 · 7 min read · 21 Views
Banner: Composer could not find a version matching your minimum-stability for a brand-new package

We extracted shared audit logging code into a package, acme/audit-log, pushed it to a private Git repository and added it to our private Packagist instance. The first application to use it ran:

composer require acme/audit-log
  [InvalidArgumentException]
  Could not find a version of package acme/audit-log matching your minimum-stability (stable).
  Require it with an explicit version constraint allowing its desired stability.

The package page on our private Packagist showed it, with one version: dev-main.

What Composer sees

composer show acme/audit-log --all
name     : acme/audit-log
versions : dev-main

No numbered versions. Composer builds versions from git tags (v1.0.0 becomes 1.0.0) and branches (main becomes dev-main). The repository had no tags yet; the author planned to tag "when it is stable".

The application's settings

{
  "minimum-stability": "stable",
  "prefer-stable": true
}

minimum-stability: stable means Composer only accepts stable versions unless told otherwise. dev-main has stability dev, so it was filtered out, and there was nothing left.

What we ruled out

  • Authentication to the private repository. Composer listed the package and its dev-main version, so it could read the repository.
  • A typo in the package name. The error named the right package.
  • Platform requirements. PHP and extension mismatches produce a different error explaining which requirement failed.

Root cause

The new package had no tagged releases. The only available version was the branch version dev-main, with dev stability. The application's minimum-stability of stable excluded it, so Composer found no acceptable version.

Stability levels

Composer assigns each version a stability, from least to most stable: dev, alpha, beta, RC, stable. It derives it from the version string:

Version source Example Stability
Branch dev-main, 2.x-dev dev
Tag with suffix 1.0.0-alpha.1, 1.0.0-beta2, 1.0.0-RC1 alpha, beta, RC
Plain tag v1.0.0 stable

minimum-stability sets the lowest stability allowed for all packages by default. Most applications use stable, which is the right default: you do not want development versions of transitive dependencies slipping in.

Ways to allow a less stable version

  • A stability flag on one constraint: "acme/audit-log": "dev-main" or "^1.0@beta". Only that package may use a lower stability.
  • Lower minimum-stability for the whole project, usually with prefer-stable: true. Affects every dependency.
  • Branch alias: the package declares that dev-main represents an upcoming version, so constraints like ^1.0@dev can match it.

Who is affected

Anyone publishing a new package without tags, internal packages consumed before their first release, and forks used through VCS repositories. It also appears when a package only has pre-release tags such as 1.0.0-beta1 and the application requires ^1.0 with stable minimum stability.

Reading the error message correctly

The second sentence of the error, "Require it with an explicit version constraint allowing its desired stability", is Composer telling you exactly which lever exists. It is easy to read it as "lower your minimum-stability", which is the wrong lever. The intended meaning is a constraint such as dev-main or ^1.0@beta on that one package, and even better, a real tagged release so no special constraint is needed.

Environment

Component Version / value
Composer 2.8
Package acme/audit-log, private Git repository, private Packagist
Versions available dev-main only
App settings minimum-stability: stable, prefer-stable: true

The real fix: tag a release

The package was ready for use by the first app, so it deserved a version. We tagged it:

git tag v0.1.0
git push origin v0.1.0

Our private Packagist picked up the tag through its webhook. Then:

composer require acme/audit-log:^0.1

Versions below 1.0 signal that the API may still change. With Composer's caret operator, ^0.1 allows 0.1.x but not 0.2.0, because for 0.x versions the minor number is treated as breaking. That matched our intent: apps opt in to each minor release while the API settles.

If you must use the branch: a scoped stability flag

During development, before the first tag, an app can require the branch explicitly without lowering stability for everything:

"require": {
  "acme/audit-log": "dev-main"
}

This pins the app to whatever is on main, which changes under it. We used this only in a feature branch of the first consuming app, never in main.

Branch alias for continuous development

The package now declares a branch alias, so dev-main is understood as the upcoming 0.2 line:

"extra": {
  "branch-alias": {
    "dev-main": "0.2.x-dev"
  }
}

An app testing unreleased changes can require "acme/audit-log": "^0.2@dev", which matches dev-main through the alias now and the real 0.2.0 tag later, without changing the constraint.

What not to do

Changing the application's minimum-stability to dev makes Composer consider development versions of every dependency. With prefer-stable: true it usually still picks stable ones, but any package whose constraints cannot be met by stable versions will silently come from a branch. We reverted that change in a pull request where someone had tried it.

Alternatives and trade-offs

  • Path repositories for local development of a package next to an app, with symlinks. Convenient while developing both at once; not for production installs.
  • Pre-release tags (v1.0.0-beta.1) with @beta flags, for packages that want explicit testing phases.

Private Packagist and tags

Our private Packagist updates a package when the repository's webhook fires. Tags pushed before the webhook was configured were not picked up until we triggered a manual update. If a new tag does not appear, check the webhook deliveries in the repository settings before suspecting Composer.

How to test the fix

composer show acme/audit-log --all
# versions : dev-main, 0.1.0
composer why-not acme/audit-log 0.2.0      # explains which constraints block a version

composer why-not (alias prohibits) is the fastest way to understand why a specific version cannot be installed.

Confirming in production

The first app installed 0.1.0 from the lock file in CI and production builds. Two more apps adopted the package at ^0.1, and the 0.2.0 release a month later was rolled out app by app, each with its own pull request.

What the team agreed

The underlying disagreement was about when a package is "ready" for a version. We agreed that the first time another repository depends on a package, it gets a 0.x tag, and 1.0.0 comes when its API has been stable through two minor releases. That made tagging a routine step rather than a statement of quality.

Regression guard

  • The package's release checklist requires a tag and a changelog entry; CI on the package repository creates releases from tags.
  • A check in application CI rejects dev-* constraints and minimum-stability other than stable on the main branch.

Security implications

Installing from branches means production runs whatever was last pushed, without a reviewed release, and supply chain controls such as signed tags or release approvals do not apply. Tagged releases, lock files and stable minimum stability keep what reaches production deliberate. Private Packagist access tokens used in CI are read-only and scoped to the organisation's packages.

Prevention checklist

  • Tag a version as soon as another project depends on a package.
  • Keep applications at minimum-stability: stable.
  • Use scoped stability flags, not global stability changes, for temporary dev versions.
  • Add a branch alias for packages under active development.
  • Use composer why-not to diagnose unresolvable versions.

References

Add a label

#laravel #php #composer #packagist #semantic-versioning
Written by
Rafael Domínguez @rafaeldominguez

I have been writing PHP since before Composer existed and Laravel since its early versions. Today I build modular Laravel applications for payment and subscription businesses and maintain a few open-source packages in my spare time. I write about the practical side of that work: structuring large Laravel codebases, publishing and versioning packages, two-factor authentication that actually works for every user, and what really happens during an on-call night on a payments platform. Each post comes from something I shipped or fixed, with code you can adapt.

Advertisement

Comments

0

No comments yet. Be the first to comment!

Sign In to post a comment.