0d3cb498a3
Deploy local.promptfoo.app / Deploy to Cloudflare Pages (push) Waiting to run
Test and Publish Multi-arch Docker Image / test (push) Waiting to run
Test and Publish Multi-arch Docker Image / build-docker-and-push-digests (map[digest-suffix:linux-amd64 platform:linux/amd64 runner:ubuntu-latest]) (push) Blocked by required conditions
Test and Publish Multi-arch Docker Image / build-docker-and-push-digests (map[digest-suffix:linux-arm64 platform:linux/arm64 runner:ubuntu-24.04-arm]) (push) Blocked by required conditions
Test and Publish Multi-arch Docker Image / merge-docker-digests (push) Blocked by required conditions
Test and Publish Multi-arch Docker Image / Attest Multi-arch Image (push) Blocked by required conditions
Validate Renovate Config / Validate Renovate Configuration (push) Waiting to run
CI / Shell Format Check (push) Has been cancelled
CI / Check Ruby (3.4) (push) Has been cancelled
CI / CI Config (push) Has been cancelled
CI / Test on Node ${{ matrix.node }} and ${{ matrix.os }}${{ matrix.shard && format(' (shard {0}/3)', matrix.shard) || '' }} (push) Has been cancelled
CI / Build on Node ${{ matrix.node }} (push) Has been cancelled
CI / Style Check (push) Has been cancelled
CI / Generate Assets (push) Has been cancelled
CI / Check Python (3.14) (push) Has been cancelled
CI / Check Python (3.9) (push) Has been cancelled
CI / Build Docs (push) Has been cancelled
CI / Code Scan Action (push) Has been cancelled
CI / Site tests (push) Has been cancelled
CI / webui tests (push) Has been cancelled
CI / Run Integration Tests (push) Has been cancelled
CI / Run Smoke Tests (push) Has been cancelled
CI / Go Tests (push) Has been cancelled
CI / Share Test (push) Has been cancelled
CI / Redteam (Production API) (push) Has been cancelled
CI / Redteam (Staging API) (push) Has been cancelled
CI / GitHub Actions Lint (push) Has been cancelled
CI / Check Ruby (3.0) (push) Has been cancelled
release-please / release-please (push) Has been cancelled
release-please / build (push) Has been cancelled
release-please / publish-npm (push) Has been cancelled
release-please / publish-npm-backfill (push) Has been cancelled
release-please / docker (push) Has been cancelled
release-please / publish-code-scan-action (push) Has been cancelled
release-please / attest-code-scan-action (push) Has been cancelled
238 lines
6.0 KiB
JavaScript
238 lines
6.0 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* Updates CHANGELOG.md when bumping version:
|
|
* - Moves Unreleased entries to new versioned section
|
|
* - Creates fresh Unreleased section
|
|
*
|
|
* Run automatically via `npm version` postversion hook
|
|
*/
|
|
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
|
|
const CHANGELOG_PATH = path.join(__dirname, '..', 'CHANGELOG.md');
|
|
|
|
/**
|
|
* Empty template for the Unreleased section after release
|
|
*/
|
|
const EMPTY_UNRELEASED = `## [Unreleased]
|
|
|
|
### Added
|
|
|
|
### Changed
|
|
|
|
### Deprecated
|
|
|
|
### Removed
|
|
|
|
### Fixed
|
|
|
|
### Security
|
|
|
|
### Dependencies
|
|
|
|
### Documentation
|
|
|
|
### Tests
|
|
`;
|
|
|
|
/**
|
|
* All valid Keep a Changelog category headers
|
|
* https://keepachangelog.com/en/1.1.0/
|
|
*/
|
|
const CHANGELOG_CATEGORIES = [
|
|
'Added',
|
|
'Changed',
|
|
'Deprecated',
|
|
'Removed',
|
|
'Fixed',
|
|
'Security',
|
|
'Dependencies',
|
|
'Documentation',
|
|
'Tests',
|
|
];
|
|
|
|
/**
|
|
* Check if a section contains only headers and whitespace (no actual entries)
|
|
* @param {string} content - The section content to check
|
|
* @returns {boolean} True if section has no actual entries
|
|
*/
|
|
function isEmptySection(content) {
|
|
// Build regex pattern for all category headers
|
|
const headerPattern = new RegExp(`### (${CHANGELOG_CATEGORIES.join('|')})`, 'g');
|
|
const withoutHeaders = content.replace(headerPattern, '').trim();
|
|
return withoutHeaders.length === 0;
|
|
}
|
|
|
|
/**
|
|
* Extract content from the Unreleased section
|
|
* @param {string} changelog - Full changelog content
|
|
* @returns {{content: string, match: RegExpMatchArray} | null} Extracted content or null
|
|
*/
|
|
function extractUnreleasedContent(changelog) {
|
|
// Match content between ## [Unreleased] and next ## [x.y.z] version header
|
|
const unreleasedMatch = changelog.match(/## \[Unreleased\]\n([\s\S]*?)(?=\n## \[\d)/);
|
|
|
|
if (!unreleasedMatch) {
|
|
return null;
|
|
}
|
|
|
|
return {
|
|
content: unreleasedMatch[1].trim(),
|
|
match: unreleasedMatch,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Build the updated changelog with new version section
|
|
* @param {string} changelog - Original changelog content
|
|
* @param {string} version - New version number
|
|
* @param {string} unreleasedContent - Content to move to versioned section
|
|
* @param {string} date - Release date in YYYY-MM-DD format
|
|
* @returns {string} Updated changelog content
|
|
*/
|
|
function buildUpdatedChangelog(changelog, version, unreleasedContent, date) {
|
|
// Build new versioned section
|
|
const newVersionSection = `## [${version}] - ${date}\n\n${unreleasedContent}`;
|
|
|
|
// Replace Unreleased content with empty template and insert new version
|
|
const updatedChangelog = changelog.replace(
|
|
/## \[Unreleased\]\n[\s\S]*?(?=\n## \[\d)/,
|
|
`${EMPTY_UNRELEASED}\n\n${newVersionSection}\n\n`,
|
|
);
|
|
|
|
// Clean up any triple+ newlines
|
|
return updatedChangelog.replace(/\n{3,}/g, '\n\n');
|
|
}
|
|
|
|
/**
|
|
* @typedef {Object} UpdateChangelogOptions
|
|
* @property {string} [changelogPath] - Path to CHANGELOG.md
|
|
* @property {string} [packageJsonPath] - Path to package.json
|
|
* @property {string} [date] - Override date (for testing)
|
|
*/
|
|
|
|
/**
|
|
* @typedef {Object} UpdateChangelogResult
|
|
* @property {boolean} success - Whether update succeeded
|
|
* @property {string} [error] - Error message if failed
|
|
* @property {string} [version] - Version that was released
|
|
* @property {string} [changelog] - Updated changelog content
|
|
*/
|
|
|
|
/**
|
|
* Update changelog for a new version release
|
|
* @param {UpdateChangelogOptions} [options] - Configuration options
|
|
* @returns {UpdateChangelogResult} Result of the update operation
|
|
*/
|
|
function updateChangelog(options = {}) {
|
|
const changelogPath = options.changelogPath || CHANGELOG_PATH;
|
|
const packageJsonPath = options.packageJsonPath || path.join(__dirname, '..', 'package.json');
|
|
const date = options.date || new Date().toISOString().split('T')[0];
|
|
|
|
try {
|
|
// Read package.json to get new version
|
|
let packageJson;
|
|
try {
|
|
packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
|
|
} catch (err) {
|
|
return {
|
|
success: false,
|
|
error: `Failed to read package.json: ${err.message}`,
|
|
};
|
|
}
|
|
|
|
const version = packageJson.version;
|
|
if (!version) {
|
|
return {
|
|
success: false,
|
|
error: 'No version found in package.json',
|
|
};
|
|
}
|
|
|
|
// Read current changelog
|
|
let changelog;
|
|
try {
|
|
changelog = fs.readFileSync(changelogPath, 'utf8');
|
|
} catch (err) {
|
|
return {
|
|
success: false,
|
|
error: `Failed to read CHANGELOG.md: ${err.message}`,
|
|
};
|
|
}
|
|
|
|
// Extract Unreleased content
|
|
const extracted = extractUnreleasedContent(changelog);
|
|
if (!extracted) {
|
|
return {
|
|
success: false,
|
|
error: 'Could not find Unreleased section in CHANGELOG.md',
|
|
};
|
|
}
|
|
|
|
const unreleasedContent = extracted.content;
|
|
|
|
// Check if Unreleased has actual content
|
|
if (!unreleasedContent || isEmptySection(unreleasedContent)) {
|
|
return {
|
|
success: false,
|
|
error:
|
|
'No entries in Unreleased section to release. Add changelog entries before bumping version.',
|
|
};
|
|
}
|
|
|
|
// Build updated changelog
|
|
const updatedChangelog = buildUpdatedChangelog(changelog, version, unreleasedContent, date);
|
|
|
|
// Write back
|
|
try {
|
|
fs.writeFileSync(changelogPath, updatedChangelog);
|
|
} catch (err) {
|
|
return {
|
|
success: false,
|
|
error: `Failed to write CHANGELOG.md: ${err.message}`,
|
|
};
|
|
}
|
|
|
|
return {
|
|
success: true,
|
|
version,
|
|
changelog: updatedChangelog,
|
|
};
|
|
} catch (err) {
|
|
return {
|
|
success: false,
|
|
error: `Unexpected error: ${err.message}`,
|
|
};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Main entry point when run from command line
|
|
*/
|
|
function main() {
|
|
const result = updateChangelog();
|
|
|
|
if (!result.success) {
|
|
console.error(`Error: ${result.error}`);
|
|
process.exit(1);
|
|
}
|
|
|
|
console.log(`Updated CHANGELOG.md for version ${result.version}`);
|
|
}
|
|
|
|
// Export for testing
|
|
module.exports = {
|
|
updateChangelog,
|
|
isEmptySection,
|
|
extractUnreleasedContent,
|
|
buildUpdatedChangelog,
|
|
EMPTY_UNRELEASED,
|
|
CHANGELOG_CATEGORIES,
|
|
};
|
|
|
|
// Run if called directly
|
|
if (require.main === module) {
|
|
main();
|
|
}
|