Skip to content

ci: validate composer manifests against their lock files - #197

Merged
CybotTM merged 1 commit into
mainfrom
ci/composer-validate
Aug 10, 2026
Merged

ci: validate composer manifests against their lock files#197
CybotTM merged 1 commit into
mainfrom
ci/composer-validate

Conversation

@CybotTM

@CybotTM CybotTM commented Aug 10, 2026

Copy link
Copy Markdown
Member

There is no composer validate --check-lock anywhere in .github/, so a manifest and its lock disagreeing surfaces only once the multi-arch build reaches composer install — roughly ninety seconds in, as a bare exit code: 4 whose real message sits four lines earlier in the log. That has already happened: Renovate raised a guzzle constraint without regenerating the lock, and four CI runs went on it.

This adds .github/workflows/composer-validate.yml, running composer validate --check-lock --no-check-publish --strict over every tracked composer.json. There are two, not one — app/composer.json and app/full/composer.json — and both are discovered via git ls-files '*composer.json' rather than named, so a third variant is covered without editing the workflow, and vendor/ or untracked leftovers on the runner cannot widen the set. It runs on pull requests and on pushes to main, matching test.yml.

No setup-php step. The check compares the lock's content hash and its recorded versions against the manifest's constraints, which does not depend on the platform PHP — verified on PHP 8.3 with composer 2.7 and on PHP 8.5 with composer 2.9, against manifests that require php >=8.5; both report ./composer.json is valid. That avoids adding a third-party action and a SHA to keep current, and the job prints php --version and composer --version so a change in the runner image is visible in the log.

This is a diagnosis-speed gate, not a replacement for build.yml or test.yml: it proves the manifests and locks agree, never that the image builds.

Verification

Against the current tree, which is self-consistent — app/full/composer.json says ^7.5 and the lock has 7.15.2:

::group::app/composer.json
./composer.json is valid
::endgroup::
::group::app/full/composer.json
./composer.json is valid
::endgroup::
STEP EXIT=0

0.54s for both manifests, and it completes with --network none.

Against a copy of the same tree in a scratch directory with guzzlehttp/guzzle raised to ^8.0 and the lock left alone — the exact shape of the incident:

::group::app/composer.json
./composer.json is valid
::endgroup::
::group::app/full/composer.json
./composer.json is valid but your composer.lock has some errors
# Lock file errors
- The lock file is not up to date with the latest changes in composer.json, it is recommended that you run `composer update` or `composer update <package name>`.
- Required package "guzzlehttp/guzzle" is in the lock file as "7.15.2" but that does not satisfy your constraint "^8.0".
::error file=app/full/composer.lock::Out of date with app/full/composer.json. Re-resolve in app/full/: 'composer update <package>' for a package named above, or 'composer update --lock' when only the content hash drifted. '--lock' alone refreshes the hash and leaves a named mismatch in place.
::endgroup::
STEP EXIT=1

Composer names the package and both versions itself. For reference, composer install --dry-run on that same tree exits 4 — the code the build was reporting.

The missing-lock branch was exercised too: deleting app/composer.lock fails that manifest with the explicit annotation while app/full/ still validates, so one bad manifest does not mask the others.

actionlint exits 0 on the new file and on all ten workflows together.

One correction worth flagging

The first version of the error annotation told you to run composer update --lock. That is wrong for the case where a package is named: --lock refreshes the content hash and leaves the version mismatch in place, so validate still fails afterwards — measured, guzzle stayed at 7.15.2 against ^8.0. It is the right fix only when the hash alone drifted, which was also measured. The annotation and the CONTRIBUTING section now distinguish the two.

Out of scope, noticed while reading

README.md carries a Security Scan badge pointing at .github/workflows/security.yml, which no longer exists — ci.yml records it as subsumed. The badge renders as a permanent failure. Left alone here.

Nothing in .github/ ran `composer validate`, so a manifest and its lock
disagreeing surfaced only once the multi-arch build reached `composer
install` — roughly ninety seconds in, as a bare `exit code: 4` whose
real message sat four lines earlier in the log. Renovate raised a guzzle
constraint without regenerating the lock and four runs went on it.

Adds composer-validate.yml, running `composer validate --check-lock` over
every tracked composer.json (app/ and app/full/ today, discovered via
git ls-files so a third variant needs no edit here). It fails in about
half a second and names the package and both versions:

  Required package "guzzlehttp/guzzle" is in the lock file as "7.15.2"
  but that does not satisfy your constraint "^8.0".

No setup-php step: the check compares the lock's content hash and
recorded versions against the manifest's constraints, which does not
depend on the platform PHP — verified on PHP 8.3/composer 2.7 and PHP
8.5/composer 2.9 against manifests requiring php>=8.5.

CONTRIBUTING gains the local command and the distinction the fix turns
on: `composer update --lock` refreshes the content hash but leaves a
named version mismatch in place, which needs `composer update <package>`.

This is a diagnosis-speed gate, not a replacement for the build.

Signed-off-by: Sebastian Mendel <github@sebastianmendel.de>
@sonarqubecloud

Copy link
Copy Markdown

@CybotTM
CybotTM requested a lite review from Copilot August 10, 2026 11:48

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated approval for maintainer PR

All automated quality gates passed. See SECURITY_CONTROLS.md for compensating controls.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@github-actions

Copy link
Copy Markdown

Dependency Review

✅ No vulnerabilities or license issues or OpenSSF Scorecard issues found.

OpenSSF Scorecard

PackageVersionScoreDetails
actions/actions/checkout 3d3c42e5aac5ba805825da76410c181273ba90b1 🟢 6.9
Details
CheckScoreReason
Binary-Artifacts🟢 10no binaries found in the repo
Code-Review🟢 10all changesets reviewed
Maintained🟢 1024 commit(s) and 0 issue activity found in the last 90 days -- score normalized to 10
Dangerous-Workflow🟢 10no dangerous workflow patterns detected
Token-Permissions⚠️ 0detected GitHub workflow tokens with excessive permissions
CII-Best-Practices⚠️ 0no effort to earn an OpenSSF best practices badge detected
Fuzzing⚠️ 0project is not fuzzed
Packaging⚠️ -1packaging workflow not detected
License🟢 10license file detected
Pinned-Dependencies🟢 3dependency not pinned by hash detected -- score normalized to 3
Signed-Releases⚠️ -1no releases found
Security-Policy🟢 9security policy file detected
SAST🟢 10SAST tool is run on all commits
Branch-Protection🟢 5branch protection is not maximal on development and all release branches

Scanned Files

  • .github/workflows/composer-validate.yml

@CybotTM
CybotTM merged commit 385c8a0 into main Aug 10, 2026
27 of 28 checks passed
@CybotTM
CybotTM deleted the ci/composer-validate branch August 10, 2026 11:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants