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
475 lines
12 KiB
TypeScript
475 lines
12 KiB
TypeScript
import fs from 'fs';
|
|
import os from 'os';
|
|
import path from 'path';
|
|
|
|
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
|
|
const {
|
|
updateChangelog,
|
|
isEmptySection,
|
|
extractUnreleasedContent,
|
|
buildUpdatedChangelog,
|
|
CHANGELOG_CATEGORIES,
|
|
} = require('../../scripts/update-changelog-version.cjs') as {
|
|
updateChangelog: (options?: {
|
|
changelogPath?: string;
|
|
packageJsonPath?: string;
|
|
date?: string;
|
|
}) => {
|
|
success: boolean;
|
|
error?: string;
|
|
version?: string;
|
|
changelog?: string;
|
|
};
|
|
isEmptySection: (content: string) => boolean;
|
|
extractUnreleasedContent: (
|
|
changelog: string,
|
|
) => { content: string; match: RegExpMatchArray } | null;
|
|
buildUpdatedChangelog: (
|
|
changelog: string,
|
|
version: string,
|
|
unreleasedContent: string,
|
|
date: string,
|
|
) => string;
|
|
CHANGELOG_CATEGORIES: string[];
|
|
};
|
|
|
|
describe('update-changelog-version', () => {
|
|
describe('CHANGELOG_CATEGORIES', () => {
|
|
it('should include all Keep a Changelog categories', () => {
|
|
expect(CHANGELOG_CATEGORIES).toContain('Added');
|
|
expect(CHANGELOG_CATEGORIES).toContain('Changed');
|
|
expect(CHANGELOG_CATEGORIES).toContain('Deprecated');
|
|
expect(CHANGELOG_CATEGORIES).toContain('Removed');
|
|
expect(CHANGELOG_CATEGORIES).toContain('Fixed');
|
|
expect(CHANGELOG_CATEGORIES).toContain('Security');
|
|
});
|
|
|
|
it('should include custom categories', () => {
|
|
expect(CHANGELOG_CATEGORIES).toContain('Dependencies');
|
|
expect(CHANGELOG_CATEGORIES).toContain('Documentation');
|
|
expect(CHANGELOG_CATEGORIES).toContain('Tests');
|
|
});
|
|
});
|
|
|
|
describe('isEmptySection', () => {
|
|
it('should return true for empty section with only headers', () => {
|
|
const content = `### Added
|
|
|
|
### Changed
|
|
|
|
### Fixed`;
|
|
expect(isEmptySection(content)).toBe(true);
|
|
});
|
|
|
|
it('should return false when section has entries', () => {
|
|
const content = `### Added
|
|
|
|
- feat: new feature (#1234)
|
|
|
|
### Changed`;
|
|
expect(isEmptySection(content)).toBe(false);
|
|
});
|
|
|
|
it('should return true for completely empty string', () => {
|
|
expect(isEmptySection('')).toBe(true);
|
|
});
|
|
|
|
it('should return true for whitespace only', () => {
|
|
expect(isEmptySection(' \n\n ')).toBe(true);
|
|
});
|
|
|
|
it('should handle all Keep a Changelog categories', () => {
|
|
const content = `### Added
|
|
|
|
### Changed
|
|
|
|
### Deprecated
|
|
|
|
### Removed
|
|
|
|
### Fixed
|
|
|
|
### Security`;
|
|
expect(isEmptySection(content)).toBe(true);
|
|
});
|
|
|
|
it('should handle custom categories', () => {
|
|
const content = `### Dependencies
|
|
|
|
### Documentation
|
|
|
|
### Tests`;
|
|
expect(isEmptySection(content)).toBe(true);
|
|
});
|
|
|
|
it('should detect entry under Deprecated category', () => {
|
|
const content = `### Deprecated
|
|
|
|
- chore: deprecate old API (#1234)`;
|
|
expect(isEmptySection(content)).toBe(false);
|
|
});
|
|
|
|
it('should detect entry under Security category', () => {
|
|
const content = `### Security
|
|
|
|
- fix: patch XSS vulnerability (#5678)`;
|
|
expect(isEmptySection(content)).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('extractUnreleasedContent', () => {
|
|
it('should extract content from Unreleased section', () => {
|
|
const changelog = `# Changelog
|
|
|
|
## [Unreleased]
|
|
|
|
### Added
|
|
|
|
- feat: new feature (#1234)
|
|
|
|
## [1.0.0] - 2024-01-01
|
|
|
|
### Added
|
|
|
|
- feat: old feature (#100)`;
|
|
|
|
const result = extractUnreleasedContent(changelog);
|
|
expect(result).not.toBeNull();
|
|
expect(result!.content).toContain('feat: new feature (#1234)');
|
|
});
|
|
|
|
it('should return null if no Unreleased section', () => {
|
|
const changelog = `# Changelog
|
|
|
|
## [1.0.0] - 2024-01-01
|
|
|
|
### Added
|
|
|
|
- feat: feature (#100)`;
|
|
|
|
expect(extractUnreleasedContent(changelog)).toBeNull();
|
|
});
|
|
|
|
it('should return null if no versioned section after Unreleased', () => {
|
|
const changelog = `# Changelog
|
|
|
|
## [Unreleased]
|
|
|
|
### Added
|
|
|
|
- feat: new feature (#1234)`;
|
|
|
|
expect(extractUnreleasedContent(changelog)).toBeNull();
|
|
});
|
|
|
|
it('should trim whitespace from extracted content', () => {
|
|
const changelog = `# Changelog
|
|
|
|
## [Unreleased]
|
|
|
|
|
|
### Added
|
|
|
|
- feat: new feature (#1234)
|
|
|
|
|
|
## [1.0.0] - 2024-01-01`;
|
|
|
|
const result = extractUnreleasedContent(changelog);
|
|
expect(result!.content).not.toMatch(/^\s/);
|
|
expect(result!.content).not.toMatch(/\s$/);
|
|
});
|
|
});
|
|
|
|
describe('buildUpdatedChangelog', () => {
|
|
const baseChangelog = `# Changelog
|
|
|
|
## [Unreleased]
|
|
|
|
### Added
|
|
|
|
- feat: new feature (#1234)
|
|
|
|
## [1.0.0] - 2024-01-01
|
|
|
|
### Added
|
|
|
|
- feat: old feature (#100)`;
|
|
|
|
it('should create new versioned section', () => {
|
|
const result = buildUpdatedChangelog(
|
|
baseChangelog,
|
|
'1.1.0',
|
|
'### Added\n\n- feat: new feature (#1234)',
|
|
'2024-02-01',
|
|
);
|
|
|
|
expect(result).toContain('## [1.1.0] - 2024-02-01');
|
|
expect(result).toContain('feat: new feature (#1234)');
|
|
});
|
|
|
|
it('should include empty Unreleased template', () => {
|
|
const result = buildUpdatedChangelog(
|
|
baseChangelog,
|
|
'1.1.0',
|
|
'### Added\n\n- feat: new feature (#1234)',
|
|
'2024-02-01',
|
|
);
|
|
|
|
expect(result).toContain('## [Unreleased]');
|
|
expect(result).toContain('### Deprecated');
|
|
expect(result).toContain('### Security');
|
|
});
|
|
|
|
it('should clean up triple newlines', () => {
|
|
const result = buildUpdatedChangelog(
|
|
baseChangelog,
|
|
'1.1.0',
|
|
'### Added\n\n- feat: new feature (#1234)',
|
|
'2024-02-01',
|
|
);
|
|
|
|
expect(result).not.toMatch(/\n{3,}/);
|
|
});
|
|
|
|
it('should preserve existing versions', () => {
|
|
const result = buildUpdatedChangelog(
|
|
baseChangelog,
|
|
'1.1.0',
|
|
'### Added\n\n- feat: new feature (#1234)',
|
|
'2024-02-01',
|
|
);
|
|
|
|
expect(result).toContain('## [1.0.0] - 2024-01-01');
|
|
expect(result).toContain('feat: old feature (#100)');
|
|
});
|
|
|
|
it('should place new version between Unreleased and old versions', () => {
|
|
const result = buildUpdatedChangelog(
|
|
baseChangelog,
|
|
'1.1.0',
|
|
'### Added\n\n- feat: new feature (#1234)',
|
|
'2024-02-01',
|
|
);
|
|
|
|
const unreleasedIndex = result.indexOf('## [Unreleased]');
|
|
const newVersionIndex = result.indexOf('## [1.1.0]');
|
|
const oldVersionIndex = result.indexOf('## [1.0.0]');
|
|
|
|
expect(unreleasedIndex).toBeLessThan(newVersionIndex);
|
|
expect(newVersionIndex).toBeLessThan(oldVersionIndex);
|
|
});
|
|
});
|
|
|
|
describe('updateChangelog', () => {
|
|
let tempDir: string;
|
|
let changelogPath: string;
|
|
let packageJsonPath: string;
|
|
|
|
beforeEach(() => {
|
|
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'changelog-test-'));
|
|
changelogPath = path.join(tempDir, 'CHANGELOG.md');
|
|
packageJsonPath = path.join(tempDir, 'package.json');
|
|
});
|
|
|
|
afterEach(() => {
|
|
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
});
|
|
|
|
it('should successfully update changelog', () => {
|
|
fs.writeFileSync(
|
|
changelogPath,
|
|
`# Changelog
|
|
|
|
## [Unreleased]
|
|
|
|
### Added
|
|
|
|
- feat: new feature (#1234)
|
|
|
|
## [1.0.0] - 2024-01-01
|
|
|
|
### Added
|
|
|
|
- feat: old feature (#100)`,
|
|
);
|
|
|
|
fs.writeFileSync(packageJsonPath, JSON.stringify({ version: '1.1.0' }));
|
|
|
|
const result = updateChangelog({
|
|
changelogPath,
|
|
packageJsonPath,
|
|
date: '2024-02-01',
|
|
});
|
|
|
|
expect(result.success).toBe(true);
|
|
expect(result.version).toBe('1.1.0');
|
|
expect(result.changelog).toContain('## [1.1.0] - 2024-02-01');
|
|
});
|
|
|
|
it('should fail if package.json is missing', () => {
|
|
fs.writeFileSync(changelogPath, '# Changelog\n\n## [Unreleased]');
|
|
|
|
const result = updateChangelog({
|
|
changelogPath,
|
|
packageJsonPath: path.join(tempDir, 'nonexistent.json'),
|
|
date: '2024-02-01',
|
|
});
|
|
|
|
expect(result.success).toBe(false);
|
|
expect(result.error).toContain('Failed to read package.json');
|
|
});
|
|
|
|
it('should fail if changelog is missing', () => {
|
|
fs.writeFileSync(packageJsonPath, JSON.stringify({ version: '1.0.0' }));
|
|
|
|
const result = updateChangelog({
|
|
changelogPath: path.join(tempDir, 'nonexistent.md'),
|
|
packageJsonPath,
|
|
date: '2024-02-01',
|
|
});
|
|
|
|
expect(result.success).toBe(false);
|
|
expect(result.error).toContain('Failed to read CHANGELOG.md');
|
|
});
|
|
|
|
it('should fail if no version in package.json', () => {
|
|
fs.writeFileSync(changelogPath, '# Changelog\n\n## [Unreleased]');
|
|
fs.writeFileSync(packageJsonPath, JSON.stringify({ name: 'test' }));
|
|
|
|
const result = updateChangelog({
|
|
changelogPath,
|
|
packageJsonPath,
|
|
date: '2024-02-01',
|
|
});
|
|
|
|
expect(result.success).toBe(false);
|
|
expect(result.error).toBe('No version found in package.json');
|
|
});
|
|
|
|
it('should fail if no Unreleased section', () => {
|
|
fs.writeFileSync(
|
|
changelogPath,
|
|
`# Changelog
|
|
|
|
## [1.0.0] - 2024-01-01
|
|
|
|
### Added
|
|
|
|
- feat: old feature (#100)`,
|
|
);
|
|
fs.writeFileSync(packageJsonPath, JSON.stringify({ version: '1.1.0' }));
|
|
|
|
const result = updateChangelog({
|
|
changelogPath,
|
|
packageJsonPath,
|
|
date: '2024-02-01',
|
|
});
|
|
|
|
expect(result.success).toBe(false);
|
|
expect(result.error).toBe('Could not find Unreleased section in CHANGELOG.md');
|
|
});
|
|
|
|
it('should fail if Unreleased section is empty', () => {
|
|
fs.writeFileSync(
|
|
changelogPath,
|
|
`# Changelog
|
|
|
|
## [Unreleased]
|
|
|
|
### Added
|
|
|
|
### Changed
|
|
|
|
## [1.0.0] - 2024-01-01`,
|
|
);
|
|
fs.writeFileSync(packageJsonPath, JSON.stringify({ version: '1.1.0' }));
|
|
|
|
const result = updateChangelog({
|
|
changelogPath,
|
|
packageJsonPath,
|
|
date: '2024-02-01',
|
|
});
|
|
|
|
expect(result.success).toBe(false);
|
|
expect(result.error).toContain('No entries in Unreleased section');
|
|
});
|
|
|
|
it('should write updated changelog to disk', () => {
|
|
fs.writeFileSync(
|
|
changelogPath,
|
|
`# Changelog
|
|
|
|
## [Unreleased]
|
|
|
|
### Added
|
|
|
|
- feat: new feature (#1234)
|
|
|
|
## [1.0.0] - 2024-01-01`,
|
|
);
|
|
fs.writeFileSync(packageJsonPath, JSON.stringify({ version: '1.1.0' }));
|
|
|
|
updateChangelog({
|
|
changelogPath,
|
|
packageJsonPath,
|
|
date: '2024-02-01',
|
|
});
|
|
|
|
const written = fs.readFileSync(changelogPath, 'utf8');
|
|
expect(written).toContain('## [1.1.0] - 2024-02-01');
|
|
expect(written).toContain('feat: new feature (#1234)');
|
|
});
|
|
|
|
it('should handle multiple entries in different categories', () => {
|
|
fs.writeFileSync(
|
|
changelogPath,
|
|
`# Changelog
|
|
|
|
## [Unreleased]
|
|
|
|
### Added
|
|
|
|
- feat: new feature (#1234)
|
|
|
|
### Fixed
|
|
|
|
- fix: bug fix (#5678)
|
|
|
|
### Security
|
|
|
|
- fix: security patch (#9999)
|
|
|
|
## [1.0.0] - 2024-01-01`,
|
|
);
|
|
fs.writeFileSync(packageJsonPath, JSON.stringify({ version: '1.1.0' }));
|
|
|
|
const result = updateChangelog({
|
|
changelogPath,
|
|
packageJsonPath,
|
|
date: '2024-02-01',
|
|
});
|
|
|
|
expect(result.success).toBe(true);
|
|
expect(result.changelog).toContain('feat: new feature (#1234)');
|
|
expect(result.changelog).toContain('fix: bug fix (#5678)');
|
|
expect(result.changelog).toContain('fix: security patch (#9999)');
|
|
});
|
|
|
|
it('should fail on invalid JSON in package.json', () => {
|
|
fs.writeFileSync(changelogPath, '# Changelog\n\n## [Unreleased]');
|
|
fs.writeFileSync(packageJsonPath, 'not valid json');
|
|
|
|
const result = updateChangelog({
|
|
changelogPath,
|
|
packageJsonPath,
|
|
date: '2024-02-01',
|
|
});
|
|
|
|
expect(result.success).toBe(false);
|
|
expect(result.error).toContain('Failed to read package.json');
|
|
});
|
|
});
|
|
});
|