marcocesarato/php-conventional-changelog
Automatically generate changelogs and release notes from your Git commit history using Conventional Commits and SemVer. Configurable, Composer-friendly, and easy to run from the terminal for streamlined releases.
Install as a dev dependency (never ship it in production):
composer require --dev marcocesarato/php-conventional-changelog
Requires PHP >= 7.1.3 and git >= 2.1.4 — the PHP constraint applies to the tool, not your app.
Add composer scripts so the whole team uses the same entry points:
"scripts": {
"changelog": "conventional-changelog",
"release": "conventional-changelog --commit",
"release:patch": "conventional-changelog --patch --commit",
"release:minor": "conventional-changelog --minor --commit",
"release:major": "conventional-changelog --major --commit"
}
First use case — bootstrap an existing repo once:
php vendor/bin/conventional-changelog --history
--history rebuilds the entire changelog from all releases. Warning: it overwrites CHANGELOG.md, so run it once deliberately and commit the result. For a brand-new/unreleased project, use --first-release instead (defaults to 1.0.0 unless you pass --ver).
Where to look first:
php vendor/bin/conventional-changelog --help — the CLI is the source of truth for flags.docs/config.md in the vendor package — the full .changelog schema.CHANGELOG.md — a live example of the output shape.Day-to-day loop is just:
composer release:minor # or patch/major — generates changelog, commits, tags
The tool reads commits since the last tag, parses conventional commit types, bumps the semver version (!/BREAKING CHANGE → MAJOR, feat → MINOR, everything else → PATCH), writes CHANGELOG.md, commits, and creates a tag.
Enforce conventional commits before the tool ever runs. The generator can only group what it can parse. Add a commit-msg hook (Husky, CaptainHook, or a simple shell hook) that rejects messages not matching type(scope): subject. Without this, malformed commits silently disappear from the changelog or land in an "uncategorized" bucket.
Prefer squash-merge with a conventional PR title. The squash commit message becomes the changelog entry, so one well-formed PR title = one clean line in the release notes. This is the lowest-friction way to keep history parseable on a team.
Split "generate" from "release". During feature work, run composer changelog (no --commit) to preview CHANGELOG.md and confirm grouping/bump level. Only run composer release:* when you actually intend to commit and tag. This gives you a free dry-run.
Pin the bump level explicitly in CI/release branches. Auto-detection is convenient but a stray feat on a hotfix branch will bump MINOR. Use --patch / --minor / --major to force intent.
Use --ver for hand-managed version numbers (e.g. coordinating with an app version or a prerelease train):
php vendor/bin/conventional-changelog --ver="2.0.1" --commit
Prerelease trains are handled with --rc, --beta, --alpha, which append the suffix to the bumped version. Keep prerelease tags on a dedicated branch so final-release bumps compute from the right base.
Regenerate a specific historical release without touching the rest of the file:
php vendor/bin/conventional-changelog --from-tag="v1.0.2" --to-tag="1.0.4"
php vendor/bin/conventional-changelog --from-date="2020-12-01" --to-date="2021-01-01"
Useful for backfilling releases that were published by hand or before the tool was adopted.
Configure once via .changelog in the repo root (or point at it with --config). It returns a PHP array merged over the defaults — omit keys you don't care about:
return [
'types' => ['feat', 'fix', 'perf', 'refactor'], // whitelist shown in the changelog
'ignoreTypes' => ['chore', 'ci', 'docs', 'style', 'test', 'build'],
'annotateTag' => true,
'signTag' => false,
];
Defaults already ignore build, chore, ci, docs, perf, refactor, revert, style, test. To include all types, set both types and ignoreTypes to empty arrays.
Annotated / signed tags for supply-chain hygiene:
php vendor/bin/conventional-changelog --commit --annotate-tag="Release version"
php vendor/bin/conventional-changelog --commit --sign-tag
--sign-tag implies annotated — don't combine both flags.
Wire the tag, not the changelog, into your pipeline. Let the tool commit + tag locally (or in a release job), push with git push --follow-tags, and have CD trigger on the tag. The changelog file is a byproduct; the tag is the release event.
--commit-all vs --commit: --commit commits the changelog artifacts, --commit-all stages everything in the working tree. In a release job, ensure the working tree is otherwise clean and prefer the narrower --commit so you don't sweep in unrelated files.
Amending instead of stacking: --amend folds the changelog into the current HEAD commit — handy on a release PR branch where you want one commit before merge.
Shallow clones break tag discovery. This is the #1 CI failure. The tool diffs "since last tag"; with actions/checkout defaults (fetch-depth: 1) no tags exist, so it treats everything as new or produces wrong ranges. Set fetch-depth: 0 (or git fetch --tags --unshallow) before running.
--history is destructive to CHANGELOG.md. It rewrites the whole file. Never run it in an automated job; use it once at adoption and review the diff by hand.
Detached HEAD / read-only CI tokens can't tag. --commit performs a real git commit and git tag. Run releases on a checkout with write access and a configured user identity (user.name, user.email), or run locally and push tags. If you must skip tagging, use --no-tag.
Unparseable commits vanish silently. Non-conventional messages are excluded, not flagged. If a release's changelog looks thin, inspect git log <lastTag>..HEAD --oneline — the missing entries are almost always malformed subjects. --no-change-without-commits exists to avoid writing an empty release when nothing parseable is found.
Squash merges can erase the type prefix if your merge tool rewrites the subject (e.g. strips feat:). Verify a merge's resulting commit message before trusting the next changelog.
Merge noise: --merged restricts to commits whose tips are reachable from HEAD, filtering out abandoned branch commits that otherwise pollute the range.
Hooks fight the release commit. Repos with pre-commit/commit-msg hooks (linters, PHPStan) will run during the tool's internal commit and can fail or reformat files mid-release. --no-verify bypasses both hooks for that commit — use it deliberately in release jobs.
--first-release doesn't imply "look at all history". It anchors version 1.0.0; older commits aren't backfilled. If you want the full past, use --history. These are different jobs.
.changelog returns PHP, so a syntax error manifests as a confusing failure rather than a config message. If options are being ignored, php -l .changelog first. Also confirm the file is at the working directory root or passed via --config — the tool resolves it relative to the working dir, not the package path (the positional path argument changes where output goes).
Config is merged, not replaced. Removing a line doesn't remove the default — the default ignored types still apply. To opt out, explicitly set the key to [].
Commit links come from repo metadata (git remote / composer info). In a mirrored or fork-based workflow, verify the generated links point where you expect before publishing release notes.
Scopes and non-ASCII descriptions have historically caused grouping/formatting bugs (fixed in 1.17.x). Keep scopes simple (api, auth, cli) — avoid spaces and punctuation — and pin a recent version (^1.17) if you have non-Latin commit messages.
Version pinning matters for Symfony compatibility. Recent releases added Symfony 7 and 8 support; on an older Symfony-based app, allow the constraint to resolve rather than hard-pinning, or you'll hit the console version mismatch that 1.17.1 patched.
License note: GPL-3.0. As a dev-only tool that never ships in your vendor/ production bundle it's normally fine, but it's a policy check worth raising with legal for closed-source products before making it a required part of the release pipeline.
Debugging a bad bump: run without --commit, read the generated CHANGELOG.md, and git describe --tags --abbrev=0 to confirm the "last tag" the tool is diffing from. Most versioning surprises trace back to a missing/incorrect tag or a commit type the whitelist excludes.
How can I help you explore Laravel packages today?