n8n/packages/@n8n/node-cli/src/commands/release.test.ts
Garrit Franke 34894af3fa
feat: Add --init-workflow flag to scaffold publish.yml (#27292)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-02 05:45:37 +00:00

191 lines
5.4 KiB
TypeScript

import fs from 'node:fs/promises';
import { CommandTester } from '../test-utils/command-tester';
import { mockSpawn } from '../test-utils/mock-child-process';
import { tmpdirTest } from '../test-utils/temp-fs';
describe('release command', () => {
const originalEnv = process.env;
const releaseItArgs = [
'exec',
'--',
'release-it',
'-n',
'--git.requireBranch main',
'--git.requireCleanWorkingDir',
'--git.requireUpstream',
'--git.requireCommits',
'--git.commit',
'--git.tag',
'--git.push',
'--git.changelog="npx auto-changelog --stdout --unreleased --commit-limit false -u --hide-credit"',
'--github.release',
'--hooks.before:init="pnpm run lint && pnpm run build"',
'--hooks.after:bump="npx auto-changelog -p"',
];
beforeEach(() => {
vi.clearAllMocks();
process.env = { ...originalEnv };
delete process.env.npm_config_user_agent;
delete process.env.GITHUB_ACTIONS;
});
afterEach(() => {
process.env = originalEnv;
});
tmpdirTest('successful release - runs release-it with correct arguments', async ({ tmpdir }) => {
await fs.writeFile(
`${tmpdir}/package.json`,
JSON.stringify({
name: 'test-node',
version: '1.0.0',
n8n: {
nodes: ['dist/nodes/TestNode.node.js'],
},
}),
);
await fs.writeFile(`${tmpdir}/pnpm-lock.yaml`, '# pnpm lock file');
mockSpawn('pnpm', [...releaseItArgs, '--npm.publish=false'], { exitCode: 0 });
const result = await CommandTester.run('release');
expect(result).toBeDefined();
});
tmpdirTest('--publish flag - runs release-it with npm publish', async ({ tmpdir }) => {
await fs.writeFile(
`${tmpdir}/package.json`,
JSON.stringify({
name: 'test-node',
version: '1.0.0',
n8n: {
nodes: ['dist/nodes/TestNode.node.js'],
},
}),
);
await fs.writeFile(`${tmpdir}/pnpm-lock.yaml`, '# pnpm lock file');
mockSpawn('pnpm', releaseItArgs, { exitCode: 0 });
const result = await CommandTester.run('release --publish');
expect(result).toBeDefined();
});
tmpdirTest('release-it failure - exits with error code', async ({ tmpdir }) => {
await fs.writeFile(
`${tmpdir}/package.json`,
JSON.stringify({
name: 'test-node',
version: '1.0.0',
n8n: {
nodes: ['dist/nodes/TestNode.node.js'],
},
}),
);
mockSpawn('npm', expect.any(Array) as string[], {
exitCode: 1,
stderr: 'Release failed: Git working directory is not clean',
});
await expect(CommandTester.run('release')).rejects.toThrow('EEXIT: 1');
});
tmpdirTest('CI mode - runs lint, build, then npm publish with provenance', async ({ tmpdir }) => {
process.env.GITHUB_ACTIONS = 'true';
await fs.writeFile(
`${tmpdir}/package.json`,
JSON.stringify({
name: 'test-node',
version: '1.0.0',
n8n: {
nodes: ['dist/nodes/TestNode.node.js'],
},
}),
);
await fs.writeFile(`${tmpdir}/pnpm-lock.yaml`, '# pnpm lock file');
mockSpawn([
{ command: 'pnpm', args: ['run', 'lint'], options: { exitCode: 0 } },
{ command: 'pnpm', args: ['run', 'build'], options: { exitCode: 0 } },
{ command: 'npm', args: ['publish'], options: { exitCode: 0 } },
]);
const result = await CommandTester.run('release');
expect(result).toBeDefined();
});
tmpdirTest('local release without publish.yml - shows init-workflow hint', async ({ tmpdir }) => {
await fs.writeFile(`${tmpdir}/pnpm-lock.yaml`, '# pnpm lock file');
mockSpawn('pnpm', [...releaseItArgs, '--npm.publish=false'], { exitCode: 0 });
const result = await CommandTester.run('release');
expect(result.getLogMessages('info')).toEqual(
expect.arrayContaining([expect.stringContaining('--init-workflow')]),
);
});
tmpdirTest(
'--init-workflow - creates publish.yml with package manager rendered',
async ({ tmpdir }) => {
await fs.writeFile(`${tmpdir}/pnpm-lock.yaml`, '# pnpm lock file');
const result = await CommandTester.run('release --init-workflow');
const workflowPath = `${tmpdir}/.github/workflows/publish.yml`;
const content = await fs.readFile(workflowPath, 'utf-8');
expect(content).toContain('pnpm install');
expect(content).toContain('pnpm run release');
expect(content).not.toContain('{{packageManager');
expect(result.getLogMessages('success')).toEqual(
expect.arrayContaining([expect.stringContaining('publish.yml')]),
);
},
);
tmpdirTest('--init-workflow - skips if publish.yml already exists', async ({ tmpdir }) => {
await fs.mkdir(`${tmpdir}/.github/workflows`, { recursive: true });
const workflowPath = `${tmpdir}/.github/workflows/publish.yml`;
const originalContent = '# existing workflow';
await fs.writeFile(workflowPath, originalContent);
const result = await CommandTester.run('release --init-workflow');
const content = await fs.readFile(workflowPath, 'utf-8');
expect(content).toBe(originalContent);
expect(result.getLogMessages('warning')).toEqual(
expect.arrayContaining([expect.stringContaining('already exists')]),
);
});
tmpdirTest('CI mode - exits with error code on lint failure', async ({ tmpdir }) => {
process.env.GITHUB_ACTIONS = 'true';
await fs.writeFile(
`${tmpdir}/package.json`,
JSON.stringify({
name: 'test-node',
version: '1.0.0',
n8n: {
nodes: ['dist/nodes/TestNode.node.js'],
},
}),
);
await fs.writeFile(`${tmpdir}/pnpm-lock.yaml`, '# pnpm lock file');
mockSpawn([{ command: 'pnpm', args: ['run', 'lint'], options: { exitCode: 1 } }]);
await expect(CommandTester.run('release')).rejects.toThrow('EEXIT: 1');
});
});