A TypeScript-based changelog management tool that works both as a GitHub Action and CLI tool. This is inspired by the Jetpack Changelogger but implemented in TypeScript and designed to work seamlessly with GitHub Actions.
- Manage changelog entries through individual change files
- Interactive CLI for adding changelog entries
- GitHub Action support for CI/CD integration
- Configurable through package.json
- Supports semantic versioning
- Validates change files format and content
- Automatically generates well-formatted changelog entries
- Multiple writing strategies (Keep a Changelog, StellarWP formats)
- Multiple versioning strategies (SemVer, StellarWP)
npm install @stellarwp/changelogger# Add a new changelog entry
npm run changelog add
# Validate all change files
npm run changelog validate
# Write changes to CHANGELOG.md
npm run changelog writeAdds a new changelog entry to the project. Can be used in interactive or non-interactive mode.
# Interactive mode - prompts for all required information
npm run changelog add
# Non-interactive mode - provide all options directly
npm run changelog add -- --significance minor --type feature --entry "Added new feature X"
# Non-interactive mode with auto-generated filename
npm run changelog add -- --significance minor --type feature --entry "Added new feature X" --auto-filenameOptions:
--significance: The significance of the change (patch, minor, major)--type: The type of change (e.g., feature, fix, enhancement)--entry: The changelog entry text--filename: The desired filename for the changelog entry (optional)--auto-filename: Automatically generate the filename based on branch name or timestamp (optional)
The command will:
- Create a new YAML file in the configured changes directory
- Generate a filename based on the branch name or timestamp
- Handle duplicate filenames by appending a timestamp
- Validate all inputs before creating the file
When using --auto-filename:
- The filename will be automatically generated from the current git branch name (if available)
- If no branch name is available, a timestamp-based filename will be used
- The filename prompt will be skipped
Validates all changelog entries in the changes directory.
npm run changelog validateThis command performs the following checks:
- Validates YAML format of all change files
- Ensures required fields are present
- Verifies significance values (patch, minor, or major)
- Validates type values against configuration
- Ensures non-patch changes have an entry description
Writes changelog entries to the configured files.
# Automatic versioning
npm run changelog write
# Manual versioning
npm run changelog write -- --overwrite-version 1.2.3
# Dry run - show what would be written without making changes
npm run changelog write -- --dry-run
# Specify a custom date (supports PHP strtotime format)
npm run changelog write -- --date "2024-03-20"
npm run changelog write -- --date "yesterday"
npm run changelog write -- --date "last monday"Options:
--overwrite-version: Optional version number to use instead of auto-determining--dry-run: If true, only show what would be written without making changes--date: Custom date to use for the changelog entry (supports PHP strtotime format)
The command will:
- Read all YAML change files from the changes directory
- Determine the next version number based on change significance (if not specified)
- Write the changes to each configured file using its specific writing strategy
- Clean up processed change files
When using --dry-run:
- Shows what would be written to each configured file
- Displays the formatted changelog entries
- No changes are actually made to any files
When using --overwrite-version:
- Uses the specified version instead of auto-determining
- If the version exists in the changelog, new changes are appended to that version
- If the version doesn't exist, a new version entry is created
When using --date:
- Uses the specified date for the changelog entry
- Supports PHP strtotime format for flexible date specification
- Examples:
--date "2024-03-20"- Specific date--date "yesterday"- Relative date--date "last monday"- Relative date--date "next friday"- Relative date
- If not specified, uses the current date
The command supports multiple output files with different writing strategies:
- Keep a Changelog format
- StellarWP changelog format
- StellarWP readme format
- Custom writing strategies
Each file is processed according to its configured strategy and the changes are written in the appropriate format.
import {
addCommand,
validateCommand,
writeCommand,
writingStrategies,
versioningStrategies,
loadConfig,
loadWritingStrategy,
loadVersioningStrategy,
WritingStrategy,
VersioningStrategy,
} from "@stellarwp/changelogger";
// Use built-in writing strategies
const keepachangelog = writingStrategies.keepachangelog;
const stellarwpChangelog = writingStrategies.stellarwpChangelog;
const stellarwpReadme = writingStrategies.stellarwpReadme;
// Use built-in versioning strategies
const semver = versioningStrategies.semverStrategy;
const stellarwp = versioningStrategies.stellarStrategy;
// Load custom strategies
// Note: Custom strategy files must be compiled JavaScript (.js) files.
// TypeScript (.ts) files are not supported and must be compiled first.
const customWritingStrategy = await loadWritingStrategy("./path/to/custom-writing.js");
const customVersioningStrategy = await loadVersioningStrategy("./path/to/custom-versioning.js");name: Verify changelog Entry.
on:
pull_request:
types: [opened, synchronize]
jobs:
validate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: stellarwp/changelogger@main
with:
command: validateConfigure the changelogger through your package.json:
{
"changelogger": {
"changelogFile": "CHANGELOG.md",
"changesDir": "changelog",
"linkTemplate": "https://github.com/owner/repo/compare/${old}...${new}",
"ordering": ["type", "content"],
"types": {
"deprecated": "Deprecated",
"feature": "Feature",
"fix": "Fix",
"removed": "Removed",
"security": "Security"
"tweak": "Tweak",
},
"versioning": "semver",
"files": [
{
"path": "CHANGELOG.md",
"strategy": "keepachangelog"
},
{
"path": "readme.txt",
"strategy": "stellarwp-readme"
}
]
}
}The changelogger supports multiple versioning strategies:
-
semver (default): Standard semantic versioning (major.minor.patch)
{ "changelogger": { "versioning": "semver" } } -
stellarwp: StellarWP versioning with hotfix support (major.minor.patch[.hotfix])
- Supports 3-part versions:
1.2.3 - Supports 4-part versions with hotfix:
1.2.3.4 - Hotfix number only appears when greater than 0
- Version handling:
major: Increments major, resets others (1.2.3.4 → 2.0.0)minor: Increments minor, resets patch/hotfix (1.2.3.4 → 1.3.0)patch:- With hotfix: Increments hotfix (1.2.3.4 → 1.2.3.5)
- Without hotfix: Increments patch (1.2.3 → 1.2.4)
{ "changelogger": { "versioning": "stellarwp" } } - Supports 3-part versions:
-
Custom Versioning: You can provide a path to a JavaScript file that implements the versioning strategy:
{ "changelogger": { "versioning": "./path/to/custom-versioning.js" } }[!IMPORTANT] Custom strategy files must be JavaScript (
.js) files. TypeScript (.ts) files are not supported at runtime and must be compiled to JavaScript first. This applies both when using the CLI and programmatically because strategy files are loaded dynamically using Node'simport(), which requires JavaScript files. If you write your custom versioning strategy in TypeScript, compile it to CommonJS JavaScript first. Use the below example and then update your configuration to use the compiled.jsfile.tsc path/to/your/custom-versioning.ts --outDir path/to/your --module CommonJS --target ES2020 --esModuleInterop false --allowSyntheticDefaultImports false --declaration false --sourceMap false --strict --skipLibCheck
The custom versioning file must export an object with these methods:
// custom-versioning.js module.exports = { /** * Calculate the next version based on current version and significance * @param {string} currentVersion - Current version string * @param {"major" | "minor" | "patch"} significance - Type of change * @returns {string} The next version */ getNextVersion(currentVersion, significance) { // Your custom logic here const parts = currentVersion.split('.'); const major = parseInt(parts[0] || '0'); const minor = parseInt(parts[1] || '0'); const patch = parseInt(parts[2] || '0'); switch (significance) { case 'major': return `${major + 1}.0.0`; case 'minor': return `${major}.${minor + 1}.0`; case 'patch': return `${major}.${minor}.${patch + 1}`; default: throw new Error(`Unknown significance: ${significance}`); } }, /** * Check if a version string is valid * @param {string} version - Version string to validate * @returns {boolean} True if valid */ isValidVersion(version) { return /^\d+\.\d+\.\d+$/.test(version); }, /** * Compare two versions * @param {string} v1 - First version * @param {string} v2 - Second version * @returns {number} -1 if v1 < v2, 0 if equal, 1 if v1 > v2 */ compareVersions(v1, v2) { const parts1 = v1.split('.').map(Number); const parts2 = v2.split('.').map(Number); for (let i = 0; i < Math.max(parts1.length, parts2.length); i++) { const part1 = parts1[i] || 0; const part2 = parts2[i] || 0; if (part1 < part2) return -1; if (part1 > part2) return 1; } return 0; } };
See examples/custom-versioning.js for a complete example.
The changelogger supports multiple writing strategies that can be configured per file in your package.json:
{
"changelogger": {
"files": [
{
"path": "CHANGELOG.md",
"strategy": "keepachangelog"
},
{
"path": "readme.txt",
"strategy": "stellarwp-readme"
}
]
}
}Available built-in strategies:
-
keepachangelog: Follows the Keep a Changelog format
Example output:
## [1.2.3] - 2024-03-22 ### Added - New feature description ### Fixed - Bug fix description [1.2.3]: https://github.com/owner/repo/compare/1.2.2...1.2.3
-
stellarwp-changelog: A WordPress-style changelog format
Example output:
### [1.2.3] 2024-03-22 - Feature - Added new feature - Fix - Fixed a bug
-
stellarwp-readme: Updates readme.txt in WordPress plugin format
Example output:
== Changelog == = [1.2.3] 2024-03-22 = * Feature - Added new feature * Fix - Fixed a bug -
Custom Writing: You can provide a path to a JavaScript file that implements the writing strategy:
{ "changelogger": { "files": [ { "path": "CHANGELOG.md", "strategy": "./path/to/custom-writing.js" } ] } }[!IMPORTANT] Custom strategy files must be compiled JavaScript (
.js) files. TypeScript (.ts) files are not supported at runtime and must be compiled to JavaScript first. This applies both when using the CLI and programmatically because strategy files are loaded dynamically using Node'simport(), which requires JavaScript files. If you write your custom writing strategy in TypeScript, compile it to CommonJS JavaScript first. Use the below example and then update your configuration to use the compiled.jsfile.tsc path/to/your/custom-writing.ts --outDir path/to/your/ --module CommonJS --target ES2020 --esModuleInterop false --allowSyntheticDefaultImports false --declaration false --sourceMap false --strict --skipLibCheck
The custom writing file must export an object with these methods:
// custom-writing.js // You can import utilities from the main package to help with formatting // Note: These are only available when using the writing strategy through changelogger const { getTypeLabel, defaultConfig } = require('@stellarwp/changelogger'); module.exports = { /** * Format the changes into a changelog entry * @param {string} version - Version being released * @param {Array<{type: string, entry: string, significance: string}>} changes - List of changes * @param {string} [previousVersion] - Previous version for comparison * @returns {string} Formatted changelog content */ formatChanges(version, changes, previousVersion) { // Group changes by type const grouped = {}; for (const change of changes) { if (!grouped[change.type]) { grouped[change.type] = []; } grouped[change.type].push(change.entry); } // Format each group let output = ''; for (const [type, entries] of Object.entries(grouped)) { // Use getTypeLabel for consistent type formatting // Falls back to capitalized type if not in config const label = getTypeLabel ? getTypeLabel(type) : type.charAt(0).toUpperCase() + type.slice(1); output += `\n### ${label}\n\n`; for (const entry of entries) { output += `- ${entry}\n`; } } return output; }, /** * Format the header for a new version * @param {string} version - Version being released * @param {string} date - Release date (YYYY-MM-DD format) * @param {string} [previousVersion] - Previous version * @returns {string} Formatted version header */ formatVersionHeader(version, date, previousVersion) { return `## [${version}] - ${date}\n`; }, /** * Optional: Format version comparison links * @param {string} version - Current version * @param {string} previousVersion - Previous version * @param {string} [template] - URL template from config * @returns {string} Formatted link */ formatVersionLink(version, previousVersion, template) { if (!template) return ''; const link = template .replace('{version}', version) .replace('{previousVersion}', previousVersion); return `\n[${version}]: ${link}\n`; }, /** * Match an existing version header in the changelog * @param {string} content - Existing changelog content * @param {string} version - Version to find * @returns {string|undefined} Matched header or undefined */ versionHeaderMatcher(content, version) { const regex = new RegExp(`^## \\[${version}\\].*$`, 'm'); const match = content.match(regex); return match ? match[0] : undefined; }, /** * Find where to insert new changelog entries * @param {string} content - Existing changelog content * @returns {number} Index where new entries should be inserted */ changelogHeaderMatcher(content) { // Look for the first version header const match = content.match(/^## \[.*?\]/m); if (match && match.index !== undefined) { return match.index; } // Look for main changelog header const headerMatch = content.match(/^# Changelog/m); if (headerMatch && headerMatch.index !== undefined) { return headerMatch.index + headerMatch[0].length + 1; } return 0; } };
See examples/custom-writing.js for a complete example.
Example output:
# Version 1.2.3 (2024-03-22) - [Feature] New feature description - [Fix] Bug fix description Compare: https://github.com/owner/repo/compare/1.2.2...1.2.3
There may be times where you want a specific writing stategy to use different type labels than the global types object.
You can do this with the optional typeLabelOverrides key in your configuration.
{
"changelogger": {
"types": {
"compatibility": "Compatibility",
"deprecated": "Deprecated",
"feature": "Feature",
"fix": "Fix",
"language": "Language",
"removed": "Removed",
"security": "Security",
"tweak": "Tweak",
},
"typeLabelOverrides": {
"keepachangelog": {
"feature": "Added",
"fix": "Fixed",
"tweak": "Changed"
},
"custom-strategy": {
"feature": "New Feature",
"fix": "Bug Fix",
"tweak": "Updated"
}
}
}
}This is particularly useful if you're outputting your changelog in multiple locations with the files key and each is configured to use a different writing strategy.
If you're using a custom writing strategy, you will need to ensure you call getTypeLabel() with the strategy parameter matching the key you set in this configuration.
When adding new changelog entries:
-
Default Filename: By default, uses the current git branch name (cleaned up) or a timestamp if no branch name is available.
-
File Naming Rules:
- Converts to lowercase
- Replaces non-alphanumeric characters with hyphens
- Removes leading/trailing hyphens
- Collapses multiple hyphens into one
Example:
Feature/Add-NEW_thing!!!→feature-add-new-thing.yaml
-
Duplicate Handling: If a file with the same name exists:
- Adds a timestamp to the filename
- Example: If
feature.yamlexists, createsfeature-1234567890.yaml
-
Interactive Prompts:
- Significance: patch, minor, or major
- Type: feature, tweak, deprecated, removed, fix, or security
- Entry: Description of the change
- Filename: Optional custom filename
-
Directory Structure:
- Creates the changes directory if it doesn't exist
- Stores all change files in the configured directory (default:
changelog/)
Change files are YAML files containing:
significance: patch|minor|major
type: feature|tweak|deprecated|removed|fix|security
entry: Description of the changeThe changelogger can also be used as a library in your Node.js applications:
import {
loadConfig,
addCommand,
validateCommand,
writeCommand,
Config,
WritingStrategy,
VersioningStrategy
} from '@stellarwp/changelogger';
// Load configuration from package.json
const config = await loadConfig();
// Add a new change entry programmatically
await addCommand({
significance: 'minor',
type: 'feature',
entry: 'New feature added',
filename: 'custom-change.yaml'
});
// Validate all change files
const validationResult = await validateCommand();
console.log(validationResult);
// Write changelog (with options)
const writeResult = await writeCommand({
overwriteVersion: '1.2.3',
dryRun: false,
date: '2024-03-20'
});
console.log(writeResult);const {
loadConfig,
addCommand,
validateCommand,
writeCommand
} = require('@stellarwp/changelogger');
// Same usage as above
(async () => {
const config = await loadConfig();
console.log('Config loaded:', config.changelogFile);
})();// TypeScript / ES6 with bundler
import {
defaultConfig,
getTypeLabel
} from '@stellarwp/changelogger';
// Use default configuration as a base
const myConfig = {
...defaultConfig,
changesDir: 'my-changes'
};
// Get formatted labels for change types
console.log(getTypeLabel('feature')); // "Feature"
console.log(getTypeLabel('fix')); // "Fix"
console.log(getTypeLabel('custom-type')); // Falls back to "custom-type" if not defined// TypeScript / ES6 with bundler
import {
loadVersioningStrategy,
loadWritingStrategy,
versioningStrategies,
writingStrategies,
getTypeLabel,
defaultConfig
} from '@stellarwp/changelogger';
// Load built-in strategies
const semverStrategy = versioningStrategies.semverStrategy;
const keepachangelog = writingStrategies.keepachangelog;
// Load custom strategies from files (must be compiled .js files).
const customVersioning = await loadVersioningStrategy('./my-versioning.js');
const customWriting = await loadWritingStrategy('./my-writing.js');
// Use strategies directly
const nextVersion = customVersioning.getNextVersion('1.2.3', 'minor');
console.log(nextVersion); // Your custom versioning logic resultThe package includes TypeScript declarations for full type support:
import {
Config,
ChangeFile,
WriteCommandOptions,
VersioningStrategy,
WritingStrategy
} from '@stellarwp/changelogger';
// All types are available for TypeScript users
const config: Config = await loadConfig();
const change: ChangeFile = {
significance: 'patch',
type: 'fix',
entry: 'Fixed a bug'
};
const options: WriteCommandOptions = {
overwriteVersion: '1.0.0',
dryRun: true
};MIT