myopensoft/php-version-bumper

Bump your project's semantic version from Conventional Commits and generate a Keep-a-Changelog entry. Framework-free, zero dependencies.

Maintainers

Package info

gitlab.com/myopensoft/php-version-bumper

Issues

pkg:composer/myopensoft/php-version-bumper

Transparency log

Statistics

Installs: 34

Dependents: 1

Suggesters: 0

Stars: 0

1.1.0 2026-08-14 02:33 UTC

This package is auto-updated.

Last update: 2026-08-13 18:35:41 UTC


README

Bump your project's semantic version from Conventional Commits and generate a Keep a Changelog entry, using the commits since your last git tag. Framework-free, zero runtime dependencies — a single git binary on PATH is all it needs.

Installation

composer require --dev myopensoft/php-version-bumper

CLI usage

vendor/bin/version-bump [options]
OptionEffect
--dry-runPreview the next version and changelog without writing
--commitStage and commit the version + changelog changes
--tagCreate a git tag for the new version
--pushPush the branch and tag to the configured remote
--help, -hShow usage
--version, -VShow the version-bump version

The bump level follows Conventional Commits: feat → minor, fix/perf → patch, and any commit marked breaking (feat!: or a BREAKING CHANGE: footer) → major. The baseline is your most recent git tag; if no commit since that tag warrants a release, the command prints a notice and exits 0.

Committing, tagging, and pushing are each opt-in — a bare vendor/bin/version-bump only writes the version file and the changelog.

Worked example

Given a repo tagged 1.0.0 with these commits since the tag:

feat(api): add export endpoint
fix(cli): correct exit code on failure
$ vendor/bin/version-bump --dry-run
Bumping 1.0.0 → 1.1.0 (Minor).
## [1.1.0] — 2026-08-02

### Added
- api: add export endpoint

### Fixed
- cli: correct exit code on failure

Dry run — nothing written, committed, or pushed.

Dropping --dry-run writes VERSION and prepends the entry to CHANGELOG.md. Adding --commit --tag then creates the release commit and tag:

$ vendor/bin/version-bump --commit --tag
Bumping 1.0.0 → 1.1.0 (Minor).
## [1.1.0] — 2026-08-02
...
Released 1.1.0.

$ git log --oneline -1 && git tag --list '1.1.0'
4c7bc98 chore(release): 1.1.0
1.1.0

Configuration: version-bumper.php

Drop a version-bumper.php in your project root and the CLI will pick it up automatically; it is entirely optional — with no file present, the defaults below already give you a working setup (a VERSION file plus a CHANGELOG.md, both in the current working directory).

<?php

return [
    // Where the current version lives: 'file', 'config_file', or 'git_tag'.
    'source' => 'file',

    // Path to the plain-text version file, used when source is 'file'.
    'version_file' => 'VERSION',

    'config_file' => [
        // Path to the file to regex-patch, used when source is 'config_file'.
        'path' => '',

        // A regex with exactly one capture group around the version literal.
        'pattern' => '',
    ],

    // Path to the Keep-a-Changelog file that new entries are prepended to.
    'changelog' => 'CHANGELOG.md',

    // Format passed to DateTimeImmutable::format() for the changelog entry date.
    'date_format' => 'Y-m-d',

    // How many segments the version has, and which bump level moves each one.
    // Tokens: major | minor | patch | manual. A `manual` segment to the left
    // of the bumped one is yours — the tool copies it through untouched. A
    // `manual` segment to the right of the bumped one is zeroed like any
    // other. Each level token may appear at most once, and at least one must
    // be present.
    'scheme' => 'major.minor.patch',

    // Commit type => changelog section heading. Types left out of this map
    // are parsed but never appear in the changelog (e.g. docs, chore, test).
    'sections' => [
        'feat' => 'Added',
        'fix' => 'Fixed',
        'perf' => 'Changed',
    ],

    // Commit type => bump level ('major', 'minor', or 'patch'). Types left
    // out of this map never trigger a release on their own. A commit marked
    // breaking (`feat!:` or a `BREAKING CHANGE:` footer) always bumps major,
    // regardless of this map.
    'bumps' => [
        'feat' => 'minor',
        'fix' => 'patch',
        'perf' => 'patch',
    ],

    'git' => [
        // Remote pushed to when --push is passed.
        'remote' => 'origin',

        // Branch pushed to when --push is passed. null (the default)
        // resolves to the current branch at run time.
        'branch' => null,
    ],
];

Paths are resolved relative to the current working directory unless they are absolute.

Version scheme

scheme describes the shape of your version. It reads like the version it produces, so the segment count is implied by the dots:

schemeversionfix:feat:feat!:
major.minor.patch (default)1.2.31.2.41.3.02.0.0
manual.manual.minor.patch1.2.3.41.2.3.51.2.4.01.2.4.0
major.minor1.21.31.32.0

Segments to the left of the bumped one are copied through unchanged; the bumped segment and everything to its right is rewritten. A manual segment is only left alone when it sits to the left of the bumped one — a manual segment to the right is zeroed like any other, so it is not a place to keep a hand-maintained build number.

When a bump level has no segment of its own, it falls back to the nearest less significant mapped level, and only failing that to the nearest more significant one. That is why row 2 turns a breaking change into a minor bump — there is no major segment — and why row 3 turns a fix: into a minor bump, since there is nothing below minor. A breaking change never blocks a release; the run warns and the break still shows up in the changelog.

If your baseline tag has a different number of segments than the scheme, it is zero-padded or truncated to fit and the run warns once, so switching schemes is a config change rather than a manual re-tagging exercise:

$ vendor/bin/version-bump --dry-run
Baseline 1.2.3 read as 1.2.3.0 to match the 4-segment scheme.
Bumping 1.2.3.0 → 1.2.4.0 (Minor).

An invalid template — an unknown token, an empty segment, a repeated level, or an all-manual scheme — throws an InvalidArgumentException naming the template.

Version sources

  • file (default) — reads and writes a plain-text version string in version_file. A missing or empty file reads as 0.0.0.
  • config_file — regex-patches a version literal inside an arbitrary file, e.g. a 'version' entry in some other config file. Both config_file.path and config_file.pattern are required: selecting this source while either is left at its empty-string default throws an InvalidArgumentException before anything is read or written.
  • git_tag — reads the current version from the latest git tag; write() is a no-op, since the tag created by --tag is itself the new source of truth.

Library usage

Everything the CLI does is reachable directly, if you'd rather drive a release from your own script:

use Myopensoft\VersionBumper\Config;
use Myopensoft\VersionBumper\ReleaseFactory;
use Myopensoft\VersionBumper\ReleaseOptions;

$release = (new ReleaseFactory(Config::fromArray([]), (string) getcwd()))
    ->releaseRunner()
    ->run(new ReleaseOptions(dryRun: true));

echo $release->to;             // '1.1.0'
echo $release->changelogEntry; // '## [1.1.0] — 2026-08-02 …'

Config::fromArray([]) applies the same built-in defaults as an absent version-bumper.php. Release also carries from, bump (the effective level actually applied — under a custom scheme this can differ from the level the commits implied, e.g. a breaking change demoted to Minor when there is no major segment), status, and the buffered messages the CLI prints.

Laravel

myopensoft/laravel-version-bumper wraps this package as an Artisan command (php artisan version:bump) with a published config file. If you're on Laravel, install that package instead of wiring this one up by hand.

Testing & static analysis

composer test       # Pest
composer analyse    # PHPStan level 10
composer psalm      # Psalm errorLevel 1
composer format     # Pint

License

The MIT License (MIT). Please see the License File for more information.