Skip to content

composer install fails with 404 for branch versions when the lock pins an older commit #178

Description

@cappuc

Problem

This happen only when using Distribution Mirroring.

When a package is required as a branch version (dev-main), any composer.lock that pins a commit older than the branch head fails to install with a 404 error, you have to re-run composer update to download the new version.

Steps to reproduce

  1. Require a package from a repository synced by Pricore at a branch version ("acme/pkg": "dev-main"), and commit the resulting composer.lock.
  2. Push a new commit to main and let Pricore sync it.
  3. Run composer install from the unchanged lock file → 404 on the dist URL.

Root cause

A branch keeps a single package_versions row and SyncRefAction updates that row in place on every sync, moving source_reference and dist_path to the new commit.

DistController::download() then looks the archive up by source_reference, so the reference recorded in the lock no longer matches any row and it returns 404.

The archive itself is often still there: CreateDistArchiveAction stores it at a path that includes the short ref, {org}/{vendor}/{package}/{version}_{ref12}.zip, and CleanupDistArchivesAction only prunes stable() versions. So the old zip stays on disk as an orphan that nothing can reach.

Proposed Solution

  1. Resolve the archive by its deterministic path before giving up. The orphaned zip is already on disk at {org}/{package}/{version}_{ref12}.zip; checking for it costs one exists() and recovers every archive built by an earlier sync.
  2. Re-archive the requested reference from the git provider when the file is not on disk, instead of returning 404. This makes lock files reproducible for as long as the commit exists upstream, which matches the packagist.org behaviour. Worth putting behind a config flag.
  3. Keep a record of detached archives. When a branch row moves forward, retain the previous row (e.g. with a null version, reachable only by source_reference, behind a global scope so it never leaks into metadata or the UI). This would also enable retention policies and storage accounting for dev archives, which CleanupDistArchivesAction does not currently cover.

Caveat for (2): Composer verifies the dist shasum from the lock file, and a regenerated zip is not guaranteed to be byte-identical to the original. When that happens the failure moves from a 404 to a checksum error. Omitting the shasum for dev versions, as packagist does, would close that gap.
If we keep PackageVersion rows for unversioned archives we can implement the cleanup for versions without downloads in the latest x days to reduce the re-archive cases.

I think this feature should be optional but the old dist archive cleanup should be implemented anyway to reduce storage usage for unreachable archives.

Alternatives Considered

No response

Additional Context

No response

Metadata

Metadata

Assignees

Labels

enhancementNew feature or request

Type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions