adachsoft / release-validator
PHP library for validating Git releases, tags, changelogs, release files, and remote repository state.
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
- symplify/phpstan-rules: ^14.12
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:
- path is not a Git repository,
- requested version has an invalid format,
- release target (version/tag) cannot be resolved,
- tag does not point to the latest commit,
- release tag is missing from the configured remote or points to a different commit,
- required release files are missing from the tagged commit or forbidden files are tracked,
- 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
- Composer for installation
- A filesystem path that will be validated (Git repository presence is checked by a built-in validator and reported as a violation when missing)
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 – every release check (including preconditions such as “is this a Git repository?”) is a class implementing
ReleaseValidatorInterface. Adding a new rule means adding a new class; the facade does not grow conditional branches. - Lazy context – validators receive
ReleaseValidationContextInterface. Expensive data (HEAD hash, tags, changelog versions, resolved release target) is loaded on first access and memoized for the rest of the run. - Chain with severity –
ValidationChainRunnerexecutes selected validators in order. Violations withseverity = ERRORare collected and the chain continues; aBLOCKERstops further validators. - Ports & Adapters – validators do not talk to Git or the changelog directly. They depend only on the context and internal adapters.
- Facade + Factory –
ReleaseValidationFacadeis the main entry point. It is assembled byReleaseValidationFacadeFactory, which wires validators, adapters and services. The factory only checks thatrepositoryPathis an existing directory. - Immutable collections & DTOs – all collections are based on
adachsoft/collection, and DTOs arefinal readonlyobjects.
This separation makes the library easy to test and extend.
Public API and internal contracts
The supported public API consists of ReleaseValidationFacadeInterface, ReleaseValidationFacadeFactory, ReleaseValidatorInterface, ReleaseValidationContextInterface, and the related request, result and collection types. Implement ReleaseValidatorInterface when adding a custom validation rule.
ChangelogInspectorInterface, GitReleaseInfoInterface, and RemoteTagInfoInterface are internal implementation ports. They separate validators and adapters from external services, but are not intended to be used as the package's public functionality.
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: %s\n",
$violation->severity->value,
$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). Default execution order puts precondition validators first.
| Code | Class name | Severity | Description |
|---|---|---|---|
git_repository_exists | GitRepositoryExistsValidator | BLOCKER | Ensures the configured path is a Git repository. Failure stops the chain. |
requested_version_format | RequestedVersionFormatValidator | BLOCKER | Ensures that an explicitly requested version follows semver X.Y.Z (optional leading v/V). Skipped when version is not provided. Failure stops the chain. |
release_target_resolvable | ReleaseTargetResolvableValidator | BLOCKER | Ensures a release version/tag can be resolved (explicit version or latest semver tag). Failure stops the chain. |
tag_points_to_head | TagPointsToHeadValidator | ERROR | Ensures that the release tag exists and points to the current HEAD commit. Detects both missing tags and tags pointing to old commits. |
release_files_present | ReleaseFilesPresentValidator | ERROR | Checks that configured required files exist in the tagged commit and that no configured forbidden file patterns are tracked. |
remote_tag_points_to_head | RemoteTagPointsToHeadValidator | ERROR | Ensures that the release tag exists on the default origin remote and points to the same commit as the local release target. |
changelog_entry_exists | ChangelogEntryExistsValidator | ERROR | 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 | ERROR | Validates the changelog format using adachsoft/changelog-linter and reports each format error as a separate violation. |
changelog_not_truncated | ChangelogNotTruncatedValidator | ERROR | Compares the set of versions in the previous release changelog with the current one and reports missing versions (truncated history). |
clean_working_tree | CleanWorkingTreeValidator | ERROR | 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. not a Git repo, 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.not_a_git_repository,tag_not_on_head,changelog_truncated),message– human-readable explanation with concrete details (tags, versions, file paths),severity–ViolationSeverityEnum::ERROR(chain continues) orViolationSeverityEnum::BLOCKER(chain stops after this validator).
Infrastructure-level issues are represented by exceptions implementing ReleaseValidatorExceptionInterface, for example:
GitOperationFailedException,ChangelogReadException,UnknownValidatorCodeException,InvalidConfigurationException(e.g.repositoryPathis not an existing directory).
When an explicitly requested version has an invalid format, this is reported as a BLOCKER violation from requested_version_format (code invalid_requested_version).
When no version is explicitly requested and the repository has no resolvable semver release target, this is reported as a BLOCKER violation from release_target_resolvable (not as an exception) — it is an expected precondition failure of the release process.
Your code is expected to catch infrastructure exceptions at the boundary (e.g. in a CLI command) and decide how to report them. Domain failures should be read from $result->violations.
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\ReleaseValidationContextInterface;
use AdachSoft\ReleaseValidator\Contract\ReleaseValidatorInterface;
use AdachSoft\ReleaseValidator\Dto\ViolationDto;
use AdachSoft\ReleaseValidator\Enum\ViolationSeverityEnum;
final readonly class MyCustomValidator implements ReleaseValidatorInterface
{
public const string CODE = 'my_custom_rule';
public function getCode(): string
{
return self::CODE;
}
public function validate(ReleaseValidationContextInterface $context): ViolationCollection
{
// Implement your logic and return a ViolationCollection.
// Use ViolationSeverityEnum::BLOCKER only when later validators cannot run meaningfully.
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. Prefer reading data through ReleaseValidationContextInterface so expensive lookups stay memoized. Do not depend on ChangelogInspectorInterface, GitReleaseInfoInterface, or RemoteTagInfoInterface; these are internal ports and may change without preserving the public API.
Configuration notes
repositoryPathmust point to an existing directory; otherwiseInvalidConfigurationExceptionis thrown by the factory. Whether that directory is a Git repository is validated later byGitRepositoryExistsValidatorand surfaces as a violation (not_a_git_repository) when it is not.changelogPathcan be relative to the repository root or an absolute path.- When
versioninReleaseValidationRequestDtoisnull, the release target is resolved from the latest semver tag (e.g.v1.2.3) inside the validation chain. - When
versionis provided, it must match semverX.Y.Z(optional leadingv/V); otherwiserequested_version_formatreports a BLOCKER and the chain stops. - Versions in changelog are normalized to not contain the leading
v/Vprefix.
License
This library is open source software released under the MIT License.