adachsoft / release-validator
release-validator
Requires
- php: ^8.3
- adachsoft/changelog-linter: ^0.7
- adachsoft/collection: ^3.0
- adachsoft/gitlib: ^4.0
Requires (Dev)
- adachsoft/php-code-style: ^0.5.0
- friendsofphp/php-cs-fixer: ^3.95
- phpstan/phpstan: ^2.2
- phpunit/phpunit: ^13.2
- rector/rector: ^2.5
README
Release-validator is a small PHP library that validates the correctness of a package release in a local Git repository. It focuses on typical mistakes made during automated releases:
- tag does not point to the latest commit,
- missing changelog entry for the released version,
- invalid changelog format,
- truncated changelog (lossy regeneration),
- dirty working tree at release time.
The library is designed to be used by other tools (CLI, CI pipelines, agents) as a backend for pre-release checks.
Requirements
- PHP >= 8.3
- Git repository available on the local filesystem
- Composer for installation
Runtime dependencies (installed automatically via Composer):
adachsoft/gitlibadachsoft/changelog-linteradachsoft/collection
Installation
Install via Composer:
composer require adachsoft/release-validator
This will also pull the required supporting libraries (gitlib, changelog-linter, collection).
Architecture overview
The library follows a few simple patterns:
- Strategy – each validation rule is implemented as a separate class that implements
ReleaseValidatorInterface. Adding a new rule means adding a new class. - Ports & Adapters – validators do not talk to Git or the changelog directly. They depend only on:
GitReleaseInfoInterface(Git port),ChangelogInspectorInterface(changelog port).
- Facade + Factory –
ReleaseValidationFacadeis the main entry point. It is assembled byReleaseValidationFacadeFactory, which wires all validators, adapters and services together. - Immutable collections & DTOs – all collections are based on
adachsoft/collection, and DTOs arefinal readonlyobjects.
This separation makes the library easy to test and extend.
Quick start
The most common usage is to validate a release for the current repository and changelog file.
<?php
declare(strict_types=1);
use AdachSoft\ReleaseValidator\Collection\ValidatorCodeCollection;
use AdachSoft\ReleaseValidator\Dto\ReleaseValidationConfigDto;
use AdachSoft\ReleaseValidator\Dto\ReleaseValidationRequestDto;
use AdachSoft\ReleaseValidator\Facade\ReleaseValidationFacadeFactory;
$config = new ReleaseValidationConfigDto(
repositoryPath: __DIR__,
changelogPath: 'CHANGELOG.md',
);
$facade = ReleaseValidationFacadeFactory::create($config);
// Validate the latest semver tag (version is resolved automatically):
$request = new ReleaseValidationRequestDto();
$result = $facade->validateRelease($request);
if ($result->valid) {
echo sprintf("Release %s (%s) is valid.\n", $result->version, $result->tagName);
return;
}
echo sprintf("Release %s (%s) is INVALID.\n", $result->version, $result->tagName);
foreach ($result->violations->toArray() as $violation) {
echo sprintf(
"[%s] %s: %s\n",
$violation->validatorCode,
$violation->code,
$violation->message,
);
}
You can also target a specific version and/or restrict which validators should run:
// Validate version 1.2.3 using only selected validators
$request = new ReleaseValidationRequestDto(
version: '1.2.3',
validatorCodes: new ValidatorCodeCollection([
'tag_points_to_head',
'changelog_entry_exists',
]),
);
$result = $facade->validateRelease($request);
The ReleaseValidationResultDto::toArray() method is convenient when you want to serialize the result, e.g. to JSON:
$json = json_encode($result->toArray(), JSON_PRETTY_PRINT);
Built-in validators
The library ships with the following validators (codes are stable and intended to be used in tooling):
| Code | Class name | Description |
|---|---|---|
tag_points_to_head | TagPointsToHeadValidator | Ensures that the release tag exists and points to the current HEAD commit. Detects both missing tags and tags pointing to old commits. |
changelog_entry_exists | ChangelogEntryExistsValidator | Ensures that the changelog contains an entry for the validated version. Missing changelog or unparsable file is treated as a release problem (violations). |
changelog_format | ChangelogFormatValidator | Validates the changelog format using adachsoft/changelog-linter and reports each format error as a separate violation. |
changelog_not_truncated | ChangelogNotTruncatedValidator | Compares the set of versions in the previous release changelog with the current one and reports missing versions (truncated history). |
clean_working_tree | CleanWorkingTreeValidator | Ensures that the working tree is clean (no staged/modified/untracked/deleted files) when validating the release. |
All validators implement ReleaseValidatorInterface and return a ViolationCollection. An empty collection means the validator passed.
Violations and exceptions
The library clearly distinguishes between:
- violations – issues with the release itself (e.g. missing tag, invalid changelog format),
- exceptions – infrastructure problems (e.g. Git command fails, changelog file cannot be read).
Violations are represented by ViolationDto and collected in ViolationCollection:
validatorCode– identifier of the validator that reported the problem,code– short machine-readable code (e.g.tag_not_on_head,changelog_truncated),message– human-readable explanation with concrete details (tags, versions, file paths).
Infrastructure-level issues are represented by exceptions implementing ReleaseValidatorExceptionInterface, for example:
GitOperationFailedException,ChangelogReadException,UnknownValidatorCodeException,InvalidConfigurationException.
Special case: when no version is explicitly requested and the repository has no semver tags, this is reported as a violation with code no_semver_tags (validatorCode: version_resolution) in the result, not as an exception
— since it is an expected precondition failure of the release process, not an infrastructure problem.
Your code is expected to catch these exceptions at the boundary (e.g. in a CLI command) and decide how to report them.
Extending the validator set
You can plug in your own validators without modifying the library code. A custom validator must implement ReleaseValidatorInterface:
use AdachSoft\ReleaseValidator\Collection\ViolationCollection;
use AdachSoft\ReleaseValidator\Contract\ReleaseValidatorInterface;
use AdachSoft\ReleaseValidator\Dto\ReleaseValidationContextDto;
use AdachSoft\ReleaseValidator\Dto\ViolationDto;
final readonly class MyCustomValidator implements ReleaseValidatorInterface
{
public const string CODE = 'my_custom_rule';
public function getCode(): string
{
return self::CODE;
}
public function validate(ReleaseValidationContextDto $context): ViolationCollection
{
// Implement your logic and return a ViolationCollection
return new ViolationCollection([]);
}
}
To wire additional validators, use ReleaseValidationFacadeFactory::createWithValidators():
use AdachSoft\ReleaseValidator\Collection\ReleaseValidatorCollection;
use AdachSoft\ReleaseValidator\Facade\ReleaseValidationFacadeFactory;
$config = new ReleaseValidationConfigDto(__DIR__, 'CHANGELOG.md');
$extraValidators = new ReleaseValidatorCollection([
new MyCustomValidator(),
]);
$facade = ReleaseValidationFacadeFactory::createWithValidators($config, $extraValidators);
Your validators will be executed in addition to the built-in ones. They can also depend on the same ports (GitReleaseInfoInterface, ChangelogInspectorInterface) if you build them manually and inject the adapters yourself.
Configuration notes
repositoryPathmust point to a directory that contains a.gitfolder; otherwiseInvalidConfigurationExceptionis thrown by the factory.changelogPathcan be relative to the repository root or an absolute path.- When
versioninReleaseValidationRequestDtoisnull, the facade resolves the latest semver tag automatically (e.g.v1.2.3). - Versions in changelog are normalized to not contain the leading
v/Vprefix.
License
This library is open source software released under the MIT License.