Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/src/content/docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -483,7 +483,7 @@ changelog:
| Option | Description |
| ------------------------- | ---------------------------------------------------------------------------- |
| `changelog` | Path to changelog file (string) OR configuration object |
| `changelog.filePath` | Path to changelog file. Default: `CHANGELOG.md` |
| `changelog.filePath` | Path to changelog file. Defaults to `<workspace>/CHANGELOG.md` for a directory workspace, otherwise `CHANGELOG.md`. Explicit paths take precedence. |
| `changelog.policy` | Mode: `none`, `simple`, or `auto`. Default: `none` |
| `changelog.scopeGrouping` | Enable scope-based grouping. Default: `true` |
| `changelog.includePaths` | Extra workspace directories to include in automatic changelogs. Default: `[]` |
Expand Down
95 changes: 95 additions & 0 deletions src/__tests__/config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { join } from 'path';

import {
getGitTagPrefix,
getChangelogConfig,
loadConfigurationFromString,
validateConfiguration,
setActiveWorkspace,
Expand All @@ -21,6 +22,8 @@ import {
} from '../config';
import { CraftProjectConfigSchema } from '../schemas/project_config';
import { logger } from '../logger';
import { GitHubTarget } from '../targets/github';
import { NoneArtifactProvider } from '../artifact_providers/none';

describe('validateConfiguration', () => {
test('parses minimal configuration', () => {
Expand Down Expand Up @@ -316,6 +319,7 @@ describe('workspaces', () => {
rmSync(directory, { recursive: true, force: true });
}
setActiveWorkspace(undefined);
vi.unstubAllEnvs();
vi.restoreAllMocks();
});

Expand Down Expand Up @@ -362,6 +366,97 @@ describe('workspaces', () => {
expect(config.workspaces).toBeUndefined();
});

test('defaults a directory workspace changelog to its own file', () => {
const directory = mkdtempSync(join(tmpdir(), 'craft-workspace-changelog-'));
temporaryDirectories.push(directory);
mkdirSync(join(directory, 'packages', 'cli'), { recursive: true });
writeFileSync(join(directory, 'CHANGELOG.md'), '# Root changelog\n');
writeFileSync(
join(directory, 'packages', 'cli', 'CHANGELOG.md'),
'# CLI changelog\n',
);
writeFileSync(
join(directory, '.craft.yml'),
[
`minVersion: ${WORKSPACES_MIN_VERSION}`,
'workspaces:',
' packages/cli:',
' changelog:',
' policy: auto',
].join('\n'),
);
process.chdir(directory);

setActiveWorkspace('packages/cli');
expect(getChangelogConfig().filePath).toBe('packages/cli/CHANGELOG.md');
vi.stubEnv('GITHUB_TOKEN', 'test-token');
const target = new GitHubTarget(
{ name: 'github' },
new NoneArtifactProvider(),
{ owner: 'test-owner', repo: 'test-repo' },
);
expect(target.githubConfig.changelog).toBe('packages/cli/CHANGELOG.md');
});

test('uses a workspace changelog path before the file is created', () => {
const directory = mkdtempSync(join(tmpdir(), 'craft-workspace-changelog-'));
temporaryDirectories.push(directory);
mkdirSync(join(directory, 'packages', 'cli'), { recursive: true });
writeFileSync(
join(directory, '.craft.yml'),
`minVersion: ${WORKSPACES_MIN_VERSION}\nworkspaces:\n packages/cli: {}\n`,
);
process.chdir(directory);

setActiveWorkspace('packages/cli');
expect(getChangelogConfig().filePath).toBe('packages/cli/CHANGELOG.md');
});

test('preserves an explicitly configured root changelog for a workspace', () => {
const directory = mkdtempSync(join(tmpdir(), 'craft-workspace-changelog-'));
temporaryDirectories.push(directory);
mkdirSync(join(directory, 'packages', 'cli'), { recursive: true });
writeFileSync(
join(directory, '.craft.yml'),
`minVersion: ${WORKSPACES_MIN_VERSION}\nchangelog: CHANGELOG.md\nworkspaces:\n packages/cli: {}\n`,
);
process.chdir(directory);

setActiveWorkspace('packages/cli');
expect(getChangelogConfig().filePath).toBe('CHANGELOG.md');
});

test('keeps the root changelog default for a workspace alias', () => {
const directory = mkdtempSync(join(tmpdir(), 'craft-workspace-changelog-'));
temporaryDirectories.push(directory);
writeFileSync(
join(directory, '.craft.yml'),
`minVersion: ${WORKSPACES_MIN_VERSION}\nworkspaces:\n cli: {}\n`,
);
process.chdir(directory);

setActiveWorkspace('cli');
expect(getChangelogConfig().filePath).toBe('CHANGELOG.md');
});

test('rejects a workspace directory redirected outside the repository', () => {
const directory = mkdtempSync(join(tmpdir(), 'craft-workspace-changelog-'));
const outside = mkdtempSync(join(tmpdir(), 'craft-workspace-outside-'));
temporaryDirectories.push(directory, outside);
mkdirSync(join(outside, 'cli'));
symlinkSync(outside, join(directory, 'packages'), 'dir');
writeFileSync(
join(directory, '.craft.yml'),
`minVersion: ${WORKSPACES_MIN_VERSION}\nworkspaces:\n packages/cli: {}\n`,
);
process.chdir(directory);

setActiveWorkspace('packages/cli');
expect(() => getChangelogConfig()).toThrow(
'Workspace directory escapes the repository',
);
});

test('a different workspace resolves independently', () => {
setActiveWorkspace('mcp');
const config = loadConfigurationFromString(WS_CONFIG);
Expand Down
42 changes: 42 additions & 0 deletions src/__tests__/prepare-dry-run.e2e.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -672,6 +672,48 @@ targets: []
expect(combinedOutput).toContain('release/1.1.0');
}, 60000);

test('prepares a workspace changelog without changing the root changelog', async () => {
tempDir = await createTestRepo();
// eslint-disable-next-line no-restricted-syntax -- Test setup needs direct git access
const git = simpleGit(tempDir);
await mkdir(join(tempDir, 'packages', 'cli'), { recursive: true });
await writeFile(
join(tempDir, '.craft.yml'),
`minVersion: "2.29.0"
github:
owner: test-owner
repo: test-repo
preReleaseCommand: ""
workspaces:
packages/cli:
changelog:
policy: auto
versioning:
policy: auto
targets: []
`,
);
await writeFile(
join(tempDir, 'packages', 'cli', 'CHANGELOG.md'),
'# CLI changelog\n\n## 1.0.0\n\n- Initial release\n',
);
await git.add('.');
await git.commit('chore: Configure CLI workspace');
await git.push('origin', (await git.status()).current!);

const { stdout, stderr } = await execFileAsync(
CLI_BIN,
['prepare', '--workspace=packages/cli', '--dry-run', '--no-input'],
{ cwd: tempDir, env: { ...CLI_ENV, GITHUB_TOKEN: '' } },
);

expect(stdout + stderr).toContain('packages/cli/CHANGELOG.md');
expect(stdout + stderr).not.toContain('diff --git a/CHANGELOG.md');
expect(await readFile(join(tempDir, 'CHANGELOG.md'), 'utf8')).toContain(
'## 1.0.0',
);
}, 60000);

test('auto changelog policy creates CHANGELOG.md if it does not exist', async () => {
tempDir = await mkdtemp(join(tmpdir(), 'craft-e2e-'));
// eslint-disable-next-line no-restricted-syntax -- Test setup needs direct git access
Expand Down
23 changes: 6 additions & 17 deletions src/commands/prepare.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { createDryRunIsolation, safeFs } from '../utils/dryRun';
import {
CONFIG_FILE_NAME,
DEFAULT_RELEASE_BRANCH_NAME,
getChangelogConfig,
getConfigFileDir,
getConfiguration,
getGitTagPrefix,
Expand Down Expand Up @@ -909,18 +910,9 @@ export async function prepareMain(argv: PrepareOptions): Promise<any> {
// The order matters here, do not move this command above createReleaseBranch!
const oldVersion = await getLatestTag(git, getGitTagPrefix());

// Check & update the changelog
// Extract changelog path from config (can be string or object)
const changelogPath =
typeof config.changelog === 'string'
? config.changelog
: config.changelog?.filePath;
// Get policy from new format or legacy changelogPolicy
const changelogPolicy = (
typeof config.changelog === 'object' && config.changelog?.policy
? config.changelog.policy
: config.changelogPolicy
) as ChangelogPolicy | undefined;
// Check & update the changelog with the selected workspace's default path.
const { filePath: changelogPath, policy: changelogPolicy } =
getChangelogConfig();
const changelogBody = await prepareChangelog(
git,
oldVersion,
Expand All @@ -932,10 +924,7 @@ export async function prepareMain(argv: PrepareOptions): Promise<any> {
// Explicitly stage the changelog file so it is committed even when no
// pre-release command runs. git-add on an unmodified file is a no-op.
if (changelogBody) {
const resolvedChangelogPath = relative(
'',
changelogPath || DEFAULT_CHANGELOG_PATH,
);
const resolvedChangelogPath = relative('', changelogPath);
await git.add(resolvedChangelogPath);
}

Expand Down Expand Up @@ -984,7 +973,7 @@ export async function prepareMain(argv: PrepareOptions): Promise<any> {
// Build a GitHub permalink to the changelog entry on the release
// branch, including line numbers so GitHub renders it inline when
// pasted into an issue body.
const resolvedChangelogPath = changelogPath || DEFAULT_CHANGELOG_PATH;
const resolvedChangelogPath = changelogPath;
const lineRange = await getChangelogLineRange(
git,
resolvedChangelogPath,
Expand Down
31 changes: 29 additions & 2 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -906,6 +906,33 @@ export interface NormalizedChangelogConfig {

const DEFAULT_CHANGELOG_FILE_PATH = 'CHANGELOG.md';

function getDefaultChangelogFilePath(): string {
const workspace = getActiveWorkspace();
if (!workspace) {
return DEFAULT_CHANGELOG_FILE_PATH;
}

if (!isSafeWorkspacePath(workspace)) {
throw new ConfigurationError(`Invalid workspace path: "${workspace}"`);
}

const root = getConfigFileDir() || process.cwd();
const directory = path.resolve(root, workspace);
if (!existsSync(directory) || !lstatSync(directory).isDirectory()) {
return DEFAULT_CHANGELOG_FILE_PATH;
}

const realRoot = realpathSync(root);
const realDirectory = realpathSync(directory);
if (!realDirectory.startsWith(`${realRoot}${path.sep}`)) {
throw new ConfigurationError(
`Workspace directory escapes the repository: "${workspace}"`,
);
}

return path.posix.join(workspace, DEFAULT_CHANGELOG_FILE_PATH);
}

/**
* Returns the normalized changelog configuration from .craft.yml
*
Expand All @@ -916,7 +943,7 @@ export function getChangelogConfig(): NormalizedChangelogConfig {
const config = getConfiguration();

// Default values
let filePath = DEFAULT_CHANGELOG_FILE_PATH;
let filePath: string | undefined;
let policy = ChangelogPolicy.None;
let scopeGrouping = true;
let includePaths: string[] = [];
Expand Down Expand Up @@ -952,7 +979,7 @@ export function getChangelogConfig(): NormalizedChangelogConfig {
}

return {
filePath,
filePath: filePath ?? getDefaultChangelogFilePath(),
policy,
scopeGrouping,
includePaths,
Expand Down
17 changes: 4 additions & 13 deletions src/targets/github.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,14 @@ import { Octokit, RestEndpointMethodTypes } from '@octokit/rest';
import { createReadStream, promises, statSync } from 'fs';
import { basename } from 'path';

import { getConfiguration } from '../config';
import { getChangelogConfig } from '../config';
import {
ChangelogPolicy,
GitHubGlobalConfig,
TargetConfig,
TypedTargetConfig,
} from '../schemas/project_config';
import {
Changeset,
DEFAULT_CHANGELOG_PATH,
findChangeset,
} from '../utils/changelog';
import { Changeset, findChangeset } from '../utils/changelog';
import { getGitHubClient } from '../utils/githubApi';
import { isDryRun } from '../utils/helpers';
import { safeExec } from '../utils/dryRun';
Expand Down Expand Up @@ -110,11 +106,7 @@ export class GitHubTarget extends BaseTarget {
const typedConfig = this.config as TypedTargetConfig<GitHubConfigFields>;
const owner = typedConfig.owner || githubRepo.owner;
const repo = typedConfig.repo || githubRepo.repo;
const configChangelog = getConfiguration().changelog;
const changelog =
typeof configChangelog === 'string'
? configChangelog
: configChangelog?.filePath || DEFAULT_CHANGELOG_PATH;
const changelog = getChangelogConfig().filePath;

this.githubConfig = {
owner,
Expand Down Expand Up @@ -719,9 +711,8 @@ export class GitHubTarget extends BaseTarget {
return;
}

const config = getConfiguration();
let changelog;
if (config.changelogPolicy !== ChangelogPolicy.None) {
if (getChangelogConfig().policy !== ChangelogPolicy.None) {
Comment thread
BYK marked this conversation as resolved.
Outdated
changelog = await this.getChangelog(version);
}

Expand Down
Loading