first commit

This commit is contained in:
2025-10-26 23:10:15 +08:00
commit 8f0345b7be
14961 changed files with 2356381 additions and 0 deletions

27
node_modules/release-it/test/args.js generated vendored Normal file
View File

@@ -0,0 +1,27 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { parseCliArguments } from '../lib/args.js';
test('should parse boolean arguments', () => {
const args = [
'--dry-run=false',
'--ci',
'--github=false',
'--no-npm',
'--git.addUntrackedFiles=true',
'--git.commit=false',
'--no-git.tag',
'--git.commitMessage=test'
];
const result = parseCliArguments(args);
assert.equal(result['dry-run'], false);
assert.equal(result.ci, true);
assert.equal(result.github, false);
assert.equal(result.npm, false);
assert.equal(result.git.addUntrackedFiles, true);
assert.equal(result.git.commit, false);
assert.equal(result.git.tag, false);
assert.equal(result.git.commitMessage, 'test');
});

21
node_modules/release-it/test/cli.js generated vendored Normal file
View File

@@ -0,0 +1,21 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import mockStdIo from 'mock-stdio';
import { version, help } from '../lib/cli.js';
import { readJSON } from '../lib/util.js';
const pkg = readJSON(new URL('../package.json', import.meta.url));
test('should print version', () => {
mockStdIo.start();
version();
const { stdout } = mockStdIo.end();
assert.equal(stdout, `v${pkg.version}\n`);
});
test('should print help', () => {
mockStdIo.start();
help();
const { stdout } = mockStdIo.end();
assert.match(stdout, new RegExp(`Release It!.+${pkg.version}`));
});

333
node_modules/release-it/test/config.js generated vendored Normal file
View File

@@ -0,0 +1,333 @@
import test, { describe, before, after, afterEach } from 'node:test';
import assert from 'node:assert/strict';
import { isCI } from 'ci-info';
import Config from '../lib/config.js';
import { readJSON } from '../lib/util.js';
import { mockFetch } from './util/mock.js';
import { createTarBlobByRawContents } from './util/fetch.js';
const defaultConfig = readJSON(new URL('../config/release-it.json', import.meta.url));
const projectConfig = readJSON(new URL('../.release-it.json', import.meta.url));
const localConfig = { github: { release: true } };
describe('config', async () => {
test("should read this project's own configuration", async () => {
const config = new Config();
await config.init();
assert.deepEqual(config.constructorConfig, {});
assert.deepEqual(config.localConfig, projectConfig);
assert.deepEqual(config.defaultConfig, defaultConfig);
});
test('should contain default values', async () => {
const config = new Config({ configDir: './test/stub/config/default' });
await config.init();
assert.deepEqual(config.constructorConfig, { configDir: './test/stub/config/default' });
assert.deepEqual(config.localConfig, localConfig);
assert.deepEqual(config.defaultConfig, defaultConfig);
});
test('should merge provided options', async () => {
const config = new Config({
configDir: './test/stub/config/merge',
increment: '1.0.0',
verbose: true,
github: {
release: true
}
});
await config.init();
const { options } = config;
assert.equal(config.isVerbose, true);
assert.equal(config.isDryRun, false);
assert.equal(options.increment, '1.0.0');
assert.equal(options.git.push, false);
assert.equal(options.github.release, true);
});
test('should set CI mode', async () => {
const config = new Config({ ci: true });
await config.init();
assert.equal(config.isCI, true);
});
test('should detect CI mode', async () => {
const config = new Config();
await config.init();
assert.equal(config.options.ci, isCI);
assert.equal(config.isCI, isCI);
});
test('should override --no-npm.publish', async () => {
const config = new Config({ npm: { publish: false } });
await config.init();
assert.equal(config.options.npm.publish, false);
});
test('should read YAML config', async () => {
const config = new Config({ configDir: './test/stub/config/yaml' });
await config.init();
assert.deepEqual(config.options.foo, { bar: 1 });
});
test('should read YML config', async () => {
const config = new Config({ configDir: './test/stub/config/yml' });
await config.init();
assert.deepEqual(config.options.foo, { bar: 1 });
});
test('should read TOML config', async () => {
const config = new Config({ configDir: './test/stub/config/toml' });
await config.init();
assert.deepEqual(config.options.foo, { bar: 1 });
});
test('should throw if provided config file is invalid (cosmiconfig exception)', async () => {
await assert.rejects(async () => {
const config = new Config({ config: './test/stub/config/invalid-config-txt' });
await config.init();
}, /Invalid configuration file at/);
});
test('should throw if provided config file is invalid (no object)', async () => {
await assert.rejects(async () => {
const config = new Config({ config: './test/stub/config/invalid-config-rc' });
await config.init();
}, /Invalid configuration file at/);
});
test('should not set default increment (for CI mode)', async () => {
const config = new Config({ ci: true });
await config.init();
assert.equal(config.options.version.increment, undefined);
});
test('should not set default increment (for interactive mode)', async () => {
const config = new Config({ ci: false });
await config.init();
assert.equal(config.options.version.increment, undefined);
});
test('should expand pre-release shortcut', async () => {
const config = new Config({ increment: 'major', preRelease: 'beta' });
await config.init();
assert.deepEqual(config.options.version, {
increment: 'major',
isPreRelease: true,
preReleaseBase: undefined,
preReleaseId: 'beta'
});
});
test('should expand pre-release shortcut (preRelease boolean)', async () => {
const config = new Config({ ci: true, preRelease: true });
await config.init();
assert.deepEqual(config.options.version, {
increment: undefined,
isPreRelease: true,
preReleaseBase: undefined,
preReleaseId: undefined
});
});
test('should expand pre-release shortcut (without increment)', async () => {
const config = new Config({ ci: false, preRelease: 'alpha' });
await config.init();
assert.deepEqual(config.options.version, {
increment: undefined,
isPreRelease: true,
preReleaseBase: undefined,
preReleaseId: 'alpha'
});
});
test('should expand pre-release shortcut (including increment and npm.tag)', async () => {
const config = new Config({ increment: 'minor', preRelease: 'rc' });
await config.init();
assert.deepEqual(config.options.version, {
increment: 'minor',
isPreRelease: true,
preReleaseBase: undefined,
preReleaseId: 'rc'
});
});
test('should use pre-release base', async () => {
const config = new Config({ increment: 'minor', preRelease: 'next', preReleaseBase: '1' });
await config.init();
assert.deepEqual(config.options.version, {
increment: 'minor',
isPreRelease: true,
preReleaseBase: '1',
preReleaseId: 'next'
});
});
test('should expand pre-release shortcut (snapshot)', async () => {
const config = new Config({ snapshot: 'feat' });
await config.init();
assert.deepEqual(config.options.version, {
increment: 'prerelease',
isPreRelease: true,
preReleaseBase: undefined,
preReleaseId: 'feat'
});
assert.equal(config.options.git.tagMatch, '0.0.0-feat.[0-9]*');
assert.equal(config.options.git.getLatestTagFromAllRefs, true);
});
});
describe('fetch extended configuration', () => {
const [mocker, server] = mockFetch('https://api.github.com');
before(() => {
mocker.mockGlobal();
});
afterEach(() => {
mocker.clearAll();
});
after(() => {
mocker.unmockGlobal();
});
test('should fetch extended configuration with default file and default branch', async () => {
const extendedConfiguration = {
git: {
commitMessage: 'Released version ${version}'
}
};
server.head('/repos/release-it/release-it-configuration/tarball/main', {
status: 200,
headers: {}
});
server.get('/repos/release-it/release-it-configuration/tarball/main', {
status: 200,
body: await new Response(
createTarBlobByRawContents({
'.release-it.json': JSON.stringify(extendedConfiguration)
})
).arrayBuffer()
});
const config = new Config({
extends: 'github:release-it/release-it-configuration'
});
await config.init();
assert(mocker.allRoutesCalled());
assert.equal(config.options.git.commitMessage, extendedConfiguration.git.commitMessage);
});
test('should fetch extended configuration with default file and specific tag', async () => {
const extendedConfiguration = {
git: {
commitMessage: 'Released version ${version}'
}
};
server.head('/repos/release-it/release-it-configuration/tarball/1.0.0', {
status: 200,
headers: {}
});
server.get('/repos/release-it/release-it-configuration/tarball/1.0.0', {
status: 200,
body: await new Response(
createTarBlobByRawContents({
'.release-it.json': JSON.stringify(extendedConfiguration)
})
).arrayBuffer()
});
const config = new Config({
extends: 'github:release-it/release-it-configuration#1.0.0'
});
await config.init();
assert(mocker.allRoutesCalled());
assert.equal(config.options.git.commitMessage, extendedConfiguration.git.commitMessage);
});
test('should fetch extended configuration with sub dir and specific tag', async () => {
const extendedConfiguration = {
git: {
commitMessage: 'Released version ${version}'
}
};
const extendedSubConfiguration = {
git: {
commitMessage: 'Released pkg version ${version}'
}
};
server.head('/repos/release-it/release-it-configuration/tarball/1.0.0', {
status: 200,
headers: {}
});
server.get('/repos/release-it/release-it-configuration/tarball/1.0.0', {
status: 200,
body: await new Response(
createTarBlobByRawContents({
'.release-it.json': JSON.stringify(extendedConfiguration),
'sub/.release-it.json': JSON.stringify(extendedSubConfiguration)
})
).arrayBuffer()
});
const config = new Config({
extends: 'github:release-it/release-it-configuration/sub#1.0.0'
});
await config.init();
assert(mocker.allRoutesCalled());
assert.equal(config.options.git.commitMessage, extendedSubConfiguration.git.commitMessage);
});
test('should fetch extended configuration with custom file and default branch', async () => {
const extendedConfiguration = {
git: {
commitMessage: 'Released version ${version}'
}
};
const extendedSubConfiguration = {
git: {
commitMessage: 'Released pkg version ${version}'
}
};
server.head('/repos/release-it/release-it-configuration/tarball/main', {
status: 200,
headers: {}
});
server.get('/repos/release-it/release-it-configuration/tarball/main', {
status: 200,
body: await new Response(
createTarBlobByRawContents({
'.release-it.json': JSON.stringify(extendedConfiguration),
'sub/.release-it.json': JSON.stringify(extendedSubConfiguration)
})
).arrayBuffer()
});
const config = new Config({
extends: 'github:release-it/release-it-configuration/sub'
});
await config.init();
assert(mocker.allRoutesCalled());
assert.equal(config.options.git.commitMessage, extendedSubConfiguration.git.commitMessage);
});
});

255
node_modules/release-it/test/git.init.js generated vendored Normal file
View File

@@ -0,0 +1,255 @@
import childProcess from 'node:child_process';
import test, { beforeEach, describe } from 'node:test';
import { mkdirSync } from 'node:fs';
import assert from 'node:assert/strict';
import Shell from '../lib/shell.js';
import Git from '../lib/plugin/git/Git.js';
import { execOpts, readJSON } from '../lib/util.js';
import sh from './util/sh.js';
import { factory } from './util/index.js';
import { mkTmpDir, gitAdd } from './util/helpers.js';
describe('git.init', () => {
const { git } = readJSON(new URL('../config/release-it.json', import.meta.url));
let bare;
let target;
beforeEach(() => {
bare = mkTmpDir();
target = mkTmpDir();
process.chdir(bare);
sh.exec(`git init --bare .`, execOpts);
sh.exec(`git clone ${bare} ${target}`, execOpts);
process.chdir(target);
gitAdd('line', 'file', 'Add file');
});
test('should throw if on wrong branch', async () => {
const options = { git: { requireBranch: 'dev' } };
const gitClient = await factory(Git, { options });
childProcess.execSync('git remote remove origin', execOpts);
await assert.rejects(gitClient.init(), /Must be on branch dev/);
});
test('should throw if on negated branch', async () => {
const options = { git: { requireBranch: '!main' } };
const gitClient = await factory(Git, { options });
sh.exec('git checkout -b main', execOpts);
await assert.rejects(gitClient.init(), /Must be on branch !main/);
});
test('should not throw if required branch matches', async () => {
const options = { git: { requireBranch: 'ma?*' } };
const gitClient = await factory(Git, { options });
await assert.doesNotReject(gitClient.init());
});
test('should not throw if one of required branch matches', async () => {
const options = { git: { requireBranch: ['release/*', 'hotfix/*'] } };
const gitClient = await factory(Git, { options });
childProcess.execSync('git checkout -b release/v1', execOpts);
await assert.doesNotReject(gitClient.init());
});
test('should throw if there is no remote Git url', async () => {
const gitClient = await factory(Git, { options: { git } });
childProcess.execSync('git remote remove origin', execOpts);
await assert.rejects(gitClient.init(), /Could not get remote Git url/);
});
test('should throw if working dir is not clean', async () => {
const gitClient = await factory(Git, { options: { git } });
childProcess.execSync('rm file', execOpts);
await assert.rejects(gitClient.init(), /Working dir must be clean/);
});
test('should throw if no upstream is configured', async () => {
const gitClient = await factory(Git, { options: { git } });
childProcess.execSync('git checkout -b foo', execOpts);
await assert.rejects(gitClient.init(), /No upstream configured for current branch/);
});
test('should throw if there are no commits', async () => {
const options = { git: { requireCommits: true } };
const gitClient = await factory(Git, { options });
childProcess.execSync('git tag 1.0.0', execOpts);
await assert.rejects(gitClient.init(), /There are no commits since the latest tag/);
});
test('should not throw if there are commits', async () => {
const options = { git: { requireCommits: true } };
const gitClient = await factory(Git, { options });
childProcess.execSync('git tag 1.0.0', execOpts);
gitAdd('line', 'file', 'Add file');
await assert.doesNotReject(gitClient.init(), 'There are no commits since the latest tag');
});
test('should fail (exit code 1) if there are no commits', async () => {
const options = { git: { requireCommits: true } };
const gitClient = await factory(Git, { options });
childProcess.execSync('git tag 1.0.0', execOpts);
await assert.rejects(gitClient.init(), { code: 1 });
});
test('should not fail (exit code 0) if there are no commits', async () => {
const options = { git: { requireCommits: true, requireCommitsFail: false } };
const gitClient = await factory(Git, { options });
childProcess.execSync('git tag 1.0.0', execOpts);
await assert.rejects(gitClient.init(), { code: 0 });
});
test('should throw if there are no commits in specified path', async () => {
const options = { git: { requireCommits: true, commitsPath: 'dir' } };
const gitClient = await factory(Git, { options });
mkdirSync('dir', { recursive: true });
sh.exec('git tag 1.0.0', execOpts);
await assert.rejects(gitClient.init(), { message: /^There are no commits since the latest tag/ });
});
test('should not throw if there are commits in specified path', async () => {
const options = { git: { requireCommits: true, commitsPath: 'dir' } };
const gitClient = await factory(Git, { options });
sh.exec('git tag 1.0.0', execOpts);
gitAdd('line', 'dir/file', 'Add file');
await assert.doesNotReject(gitClient.init());
});
test('should not throw if there are no tags', async () => {
const options = { git: { requireCommits: true } };
const gitClient = await factory(Git, { options });
gitAdd('line', 'file', 'Add file');
await assert.doesNotReject(gitClient.init());
});
test('should not throw if origin remote is renamed', async () => {
childProcess.execSync('git remote rename origin upstream', execOpts);
const gitClient = await factory(Git);
await assert.doesNotReject(gitClient.init());
});
test('should detect and include version prefix ("v")', async () => {
const gitClient = await factory(Git, { options: { git } });
childProcess.execSync('git tag v1.0.0', execOpts);
await gitClient.init();
await gitClient.bump('1.0.1');
assert.equal(gitClient.config.getContext('tagName'), 'v1.0.1');
});
test('should detect and exclude version prefix', async () => {
const gitClient = await factory(Git, { options: { git } });
childProcess.execSync('git tag 1.0.0', execOpts);
await gitClient.init();
await gitClient.bump('1.0.1');
assert.equal(gitClient.config.getContext('tagName'), '1.0.1');
});
test('should detect and exclude version prefix (configured)', async () => {
const gitClient = await factory(Git, { options: { git: { tagName: 'v${version}' } } });
childProcess.execSync('git tag 1.0.0', execOpts);
await gitClient.init();
await gitClient.bump('1.0.1');
assert.equal(gitClient.config.getContext('tagName'), 'v1.0.1');
});
test('should honor custom tagName configuration', async () => {
const gitClient = await factory(Git, { options: { git: { tagName: 'TAGNAME-${repo.project}-v${version}' } } });
childProcess.execSync('git tag 1.0.0', execOpts);
await gitClient.init();
await gitClient.bump('1.0.1');
const { project } = gitClient.getContext('repo');
assert.equal(gitClient.config.getContext('tagName'), `TAGNAME-${project}-v1.0.1`);
});
test('should get the latest tag after fetch', async () => {
const shell = await factory(Shell);
const gitClient = await factory(Git, { container: { shell } });
const other = mkTmpDir();
childProcess.execSync('git push', execOpts);
childProcess.execSync(`git clone ${bare} ${other}`, execOpts);
process.chdir(other);
childProcess.execSync('git tag 1.0.0', execOpts);
childProcess.execSync('git push --tags', execOpts);
process.chdir(target);
await gitClient.init();
assert.equal(gitClient.config.getContext('latestTag'), '1.0.0');
});
test('should get the latest custom tag after fetch when tagName is configured', async () => {
const shell = await factory(Shell);
const gitClient = await factory(Git, {
options: { git: { tagName: 'TAGNAME-v${version}' } },
container: { shell }
});
const other = mkTmpDir();
childProcess.execSync('git push', execOpts);
childProcess.execSync(`git clone ${bare} ${other}`, execOpts);
process.chdir(other);
childProcess.execSync('git tag TAGNAME-OTHER-v2.0.0', execOpts);
childProcess.execSync('git tag TAGNAME-v1.0.0', execOpts);
childProcess.execSync('git tag TAGNAME-OTHER-v2.0.2', execOpts);
childProcess.execSync('git push --tags', execOpts);
process.chdir(target);
await gitClient.init();
assert.equal(gitClient.config.getContext('latestTag'), 'TAGNAME-v1.0.0');
});
test('should get the latest tag based on tagMatch', async () => {
const shell = await factory(Shell);
const gitClient = await factory(Git, {
options: { git: { tagMatch: '[0-9][0-9]\\.[0-1][0-9]\\.[0-9]*' } },
container: { shell }
});
childProcess.execSync('git tag 1.0.0', execOpts);
childProcess.execSync('git tag 21.04.3', execOpts);
childProcess.execSync('git tag 1.0.1', execOpts);
childProcess.execSync('git push --tags', execOpts);
await gitClient.init();
assert.equal(gitClient.config.getContext('latestTag'), '21.04.3');
});
test('should get the latest tag based on tagExclude', async () => {
const shell = await factory(Shell);
const gitClient = await factory(Git, {
options: { git: { tagExclude: '*[-]*' } },
container: { shell }
});
childProcess.execSync('git tag 1.0.0', execOpts);
childProcess.execSync('git commit --allow-empty -m "commit 1"', execOpts);
childProcess.execSync('git tag 1.0.1-rc.0', execOpts);
childProcess.execSync('git tag 1.0.1', execOpts);
childProcess.execSync('git commit --allow-empty -m "commit 2"', execOpts);
childProcess.execSync('git tag 1.1.0-rc.0', execOpts);
childProcess.execSync('git push --tags', execOpts);
await gitClient.init();
assert.equal(gitClient.config.getContext('latestTag'), '1.0.1');
});
test('should generate correct changelog', async () => {
const gitClient = await factory(Git, { options: { git } });
childProcess.execSync('git tag 1.0.0', execOpts);
gitAdd('line', 'file', 'Add file');
gitAdd('line', 'file', 'Add file');
await gitClient.init();
const changelog = await gitClient.getChangelog();
assert.match(changelog, /\* Add file \(\w{7}\)\n\* Add file \(\w{7}\)/);
});
test('should get the full changelog since latest major tag', async () => {
const shell = await factory(Shell);
const gitClient = await factory(Git, {
options: { git: { tagMatch: '[0-9]\\.[0-9]\\.[0-9]', changelog: git.changelog } },
container: { shell }
});
childProcess.execSync('git tag 1.0.0', execOpts);
gitAdd('line', 'file', 'Add file');
childProcess.execSync('git tag 2.0.0-rc.0', execOpts);
gitAdd('line', 'file', 'Add file');
childProcess.execSync('git tag 2.0.0-rc.1', execOpts);
gitAdd('line', 'file', 'Add file');
await gitClient.init();
assert.equal(gitClient.config.getContext('latestTag'), '1.0.0');
const changelog = await gitClient.getChangelog();
assert.match(changelog, /\* Add file \(\w{7}\)\n\* Add file \(\w{7}\)\n\* Add file \(\w{7}\)/);
});
});

409
node_modules/release-it/test/git.js generated vendored Normal file
View File

@@ -0,0 +1,409 @@
import test, { beforeEach, describe } from 'node:test';
import assert from 'node:assert/strict';
import { EOL } from 'node:os';
import childProcess from 'node:child_process';
import { appendFileSync } from 'node:fs';
import Git from '../lib/plugin/git/Git.js';
import { execOpts, touch } from '../lib/util.js';
import sh from './util/sh.js';
import { factory } from './util/index.js';
import { mkTmpDir, readFile, gitAdd } from './util/helpers.js';
describe('git', () => {
beforeEach(() => {
const tmp = mkTmpDir();
process.chdir(tmp);
});
test('should return whether repo has upstream branch', async () => {
const gitClient = await factory(Git);
childProcess.execSync('git init', execOpts);
gitAdd('line', 'file', 'Add file');
assert.equal(await gitClient.hasUpstreamBranch(), false);
});
test('should return branch name', async () => {
const gitClient = await factory(Git);
childProcess.execSync('git init', execOpts);
assert.equal(await gitClient.getBranchName(), null);
childProcess.execSync('git checkout -b feat', execOpts);
gitAdd('line', 'file', 'Add file');
assert.equal(await gitClient.getBranchName(), 'feat');
});
test('should return whether tag exists and if working dir is clean', async () => {
const gitClient = await factory(Git);
childProcess.execSync('git init', execOpts);
assert.equal(await gitClient.tagExists('1.0.0'), false);
touch('file');
assert.equal(await gitClient.isWorkingDirClean(), false);
gitAdd('line', 'file', 'Add file');
childProcess.execSync('git tag 1.0.0', execOpts);
assert(await gitClient.tagExists('1.0.0'));
assert(await gitClient.isWorkingDirClean());
});
test('should throw if tag exists', async () => {
const gitClient = await factory(Git);
childProcess.execSync('git init', execOpts);
touch('file');
gitAdd('line', 'file', 'Add file');
childProcess.execSync('git tag 0.0.2', execOpts);
gitClient.config.setContext({ latestTag: '0.0.1', tagName: '0.0.2' });
await assert.rejects(gitClient.tag({ name: '0.0.2' }), /fatal: tag '0\.0\.2' already exists/);
});
test('should only warn if tag exists intentionally', async t => {
const gitClient = await factory(Git);
const warn = t.mock.method(gitClient.log, 'warn');
childProcess.execSync('git init', execOpts);
touch('file');
gitAdd('line', 'file', 'Add file');
childProcess.execSync('git tag 1.0.0', execOpts);
gitClient.config.setContext({ latestTag: '1.0.0', tagName: '1.0.0' });
await assert.doesNotReject(gitClient.tag());
assert.equal(warn.mock.callCount(), 1);
assert.equal(warn.mock.calls[0].arguments[0], 'Tag "1.0.0" already exists');
});
test('should return the remote url', async () => {
childProcess.execSync(`git init`, execOpts);
{
const options = { git: { pushRepo: 'origin' } };
const gitClient = await factory(Git, { options });
assert.equal(await gitClient.getRemoteUrl(), null);
childProcess.execSync(`git remote add origin foo`, execOpts);
assert.equal(await gitClient.getRemoteUrl(), 'foo');
}
{
const options = { git: { pushRepo: 'another' } };
const gitClient = await factory(Git, { options });
assert.equal(await gitClient.getRemoteUrl(), null);
childProcess.execSync(`git remote add another bar`, execOpts);
assert.equal(await gitClient.getRemoteUrl(), 'bar');
}
{
const options = { git: { pushRepo: 'git://github.com/webpro/release-it.git' } };
const gitClient = await factory(Git, { options });
assert.equal(await gitClient.getRemoteUrl(), 'git://github.com/webpro/release-it.git');
}
});
test('should return the non-origin remote', async () => {
const bare = mkTmpDir();
childProcess.execSync(`git init --bare ${bare}`, execOpts);
childProcess.execSync(`git clone ${bare} .`, execOpts);
gitAdd('line', 'file', 'Add file');
childProcess.execSync('git remote rename origin upstream', execOpts);
const gitClient = await factory(Git);
assert.equal(await gitClient.getRemoteUrl(), bare);
});
test('should stage, commit, tag and push', async () => {
const bare = mkTmpDir();
childProcess.execSync(`git init --bare ${bare}`, execOpts);
childProcess.execSync(`git clone ${bare} .`, execOpts);
const version = '1.2.3';
gitAdd(`{"version":"${version}"}`, 'package.json', 'Add package.json');
{
const gitClient = await factory(Git);
childProcess.execSync(`git tag ${version}`, execOpts);
assert.equal(await gitClient.getLatestTagName(), version);
}
{
const gitClient = await factory(Git);
gitAdd('line', 'file', 'Add file');
childProcess.execSync('npm --no-git-tag-version version patch', execOpts);
await gitClient.stage('package.json');
await gitClient.commit({ message: `Release v1.2.4` });
await gitClient.tag({ name: 'v1.2.4', annotation: 'Release v1.2.4' });
assert.equal(await gitClient.getLatestTagName(), 'v1.2.4');
await gitClient.push();
const stdout = childProcess.execSync('git status -uno', { encoding: 'utf-8' });
assert.match(stdout, /nothing to commit/);
}
});
test('should commit, tag and push with extra args', async t => {
const bare = mkTmpDir();
childProcess.execSync(`git init --bare ${bare}`, execOpts);
childProcess.execSync(`git clone ${bare} .`, execOpts);
gitAdd('line', 'file', 'Add file');
const options = { git: { commitArgs: '-S', tagArgs: ['-T', 'foo'], pushArgs: ['-U', 'bar', '-V'] } };
const gitClient = await factory(Git, { options });
const stub = t.mock.method(gitClient.shell, 'exec', () => Promise.resolve());
await gitClient.stage('package.json');
await gitClient.commit({ message: `Release v1.2.4` });
await gitClient.tag({ name: 'v1.2.4', annotation: 'Release v1.2.4' });
await gitClient.push();
assert(stub.mock.calls[1].arguments[0].includes('-S'));
assert.equal(stub.mock.calls[2].arguments[0][5], '-T');
assert.equal(stub.mock.calls[2].arguments[0][6], 'foo');
assert(stub.mock.calls.at(-1).arguments[0].join(' ').includes('-U bar -V'));
});
test('should amend commit without message if not provided', async t => {
const bare = mkTmpDir();
childProcess.execSync(`git init --bare ${bare}`, execOpts);
childProcess.execSync(`git clone ${bare} .`, execOpts);
gitAdd('line', 'file', 'Add file');
const options = { git: { commitArgs: ['--amend', '--no-edit', '--no-verify'] } };
const gitClient = await factory(Git, { options });
const exec = t.mock.method(gitClient.shell, 'exec', () => Promise.resolve());
await gitClient.stage('package.json');
await gitClient.commit();
assert.deepEqual(exec.mock.calls[1].arguments[0], ['git', 'commit', '--amend', '--no-edit', '--no-verify']);
});
test('should commit and tag with quoted characters', async () => {
const bare = mkTmpDir();
childProcess.execSync(`git init --bare ${bare}`, execOpts);
childProcess.execSync(`git clone ${bare} .`, execOpts);
const gitClient = await factory(Git, {
options: { git: { commitMessage: 'Release ${version}', tagAnnotation: 'Release ${version}\n\n${changelog}' } }
});
touch('file');
const changelog = `- Foo's${EOL}- "$bar"${EOL}- '$baz'${EOL}- foo`;
gitClient.config.setContext({ version: '1.0.0', changelog });
await gitClient.stage('file');
await gitClient.commit();
await gitClient.tag({ name: '1.0.0' });
await gitClient.push();
{
const stdout = childProcess.execSync('git log -1 --format=%s', { encoding: 'utf-8' });
assert.equal(stdout.trim(), 'Release 1.0.0');
}
{
const stdout = childProcess.execSync('git tag -n99', { encoding: 'utf-8' });
assert.equal(
stdout.trim(),
`1.0.0 Release 1.0.0\n \n - Foo's\n - "$bar"\n - '$baz'\n - foo`
);
}
});
test('should push to origin', async t => {
const bare = mkTmpDir();
childProcess.execSync(`git init --bare ${bare}`, execOpts);
childProcess.execSync(`git clone ${bare} .`, execOpts);
gitAdd('line', 'file', 'Add file');
const gitClient = await factory(Git);
const spy = t.mock.method(gitClient.shell, 'exec');
await gitClient.push();
assert.deepEqual(spy.mock.calls.at(-1).arguments[0], ['git', 'push']);
const stdout = childProcess.execSync('git ls-tree -r HEAD --name-only', {
cwd: bare,
encoding: 'utf-8'
});
assert.equal(stdout.trim(), 'file');
});
test('should push to tracked upstream branch', async t => {
const bare = mkTmpDir();
childProcess.execSync(`git init --bare ${bare}`, execOpts);
childProcess.execSync(`git clone ${bare} .`, execOpts);
childProcess.execSync(`git remote rename origin upstream`, execOpts);
gitAdd('line', 'file', 'Add file');
const gitClient = await factory(Git);
const spy = t.mock.method(gitClient.shell, 'exec');
await gitClient.push();
assert.deepEqual(spy.mock.calls.at(-1).arguments[0], ['git', 'push']);
const stdout = childProcess.execSync('git ls-tree -r HEAD --name-only', {
cwd: bare,
encoding: 'utf-8'
});
assert.equal(stdout.trim(), 'file');
});
test('should push to repo url', async t => {
const bare = mkTmpDir();
childProcess.execSync(`git init --bare ${bare}`, execOpts);
childProcess.execSync(`git clone ${bare} .`, execOpts);
gitAdd('line', 'file', 'Add file');
const options = { git: { pushRepo: 'https://host/repo.git' } };
const gitClient = await factory(Git, { options });
const spy = t.mock.method(gitClient.shell, 'exec');
try {
await gitClient.push();
} catch (err) {
assert.deepEqual(spy.mock.calls.at(-1).arguments[0], ['git', 'push', 'https://host/repo.git']);
}
});
test('should push to remote name (not "origin")', async t => {
const bare = mkTmpDir();
childProcess.execSync(`git init --bare ${bare}`, execOpts);
childProcess.execSync(`git clone ${bare} .`, execOpts);
gitAdd('line', 'file', 'Add file');
childProcess.execSync(
`git remote add upstream ${childProcess.execSync('git config --get remote.origin.url', {
encoding: 'utf-8'
})}`,
execOpts
);
const options = { git: { pushRepo: 'upstream' } };
const gitClient = await factory(Git, { options });
const spy = t.mock.method(gitClient.shell, 'exec');
await gitClient.push();
assert.deepEqual(spy.mock.calls.at(-1).arguments[0], ['git', 'push', 'upstream']);
const stdout = childProcess.execSync('git ls-tree -r HEAD --name-only', {
cwd: bare,
encoding: 'utf-8'
});
assert.equal(stdout.trim(), 'file');
{
childProcess.execSync(`git checkout -b foo`, execOpts);
gitAdd('line', 'file', 'Add file');
await gitClient.push();
assert.deepEqual(spy.mock.calls.at(-1).arguments[0], ['git', 'push', '--set-upstream', 'upstream', 'foo']);
assert.match(
await spy.mock.calls.at(-1).result,
/branch .?foo.? set up to track (remote branch .?foo.? from .?upstream.?|.?upstream\/foo.?)/i
);
}
});
test('should return repo status', async () => {
const gitClient = await factory(Git);
childProcess.execSync('git init', execOpts);
gitAdd('line', 'file1', 'Add file');
appendFileSync('file1', 'line');
appendFileSync('file2', 'line');
childProcess.execSync('git add file2', execOpts);
assert.equal(await gitClient.status(), ' M file1\nA file2');
});
test('should reset files', async t => {
const gitClient = await factory(Git);
childProcess.execSync('git init', execOpts);
gitAdd('line', 'file', 'Add file');
appendFileSync('file', 'line');
assert.match(await readFile('file'), /^line\s*line\s*$/);
await gitClient.reset('file');
assert.match(await readFile('file'), /^line\s*$/);
const warn = t.mock.method(gitClient.log, 'warn');
await gitClient.reset(['file2, file3']);
assert.match(warn.mock.calls[0].arguments[0], /Could not reset file2, file3/);
});
test('should roll back when cancelled', async t => {
childProcess.execSync('git init', execOpts);
childProcess.execSync(`git remote add origin file://foo`, execOpts);
const version = '1.2.3';
gitAdd(`{"version":"${version}"}`, 'package.json', 'Add package.json');
const options = { git: { requireCleanWorkingDir: true, commit: true, tag: true, tagName: 'v${version}' } };
const gitClient = await factory(Git, { options });
const exec = t.mock.method(gitClient.shell, 'execFormattedCommand');
childProcess.execSync(`git tag ${version}`, execOpts);
gitAdd('line', 'file', 'Add file');
await gitClient.init();
childProcess.execSync('npm --no-git-tag-version version patch', execOpts);
gitClient.bump('1.2.4');
await gitClient.beforeRelease();
await gitClient.stage('package.json');
await gitClient.commit({ message: 'Add this' });
await gitClient.tag();
await gitClient.rollbackOnce();
assert.equal(exec.mock.calls[11].arguments[0], 'git tag --delete v1.2.4');
assert.equal(exec.mock.calls[12].arguments[0], 'git reset --hard HEAD~1');
});
// To get this test to pass, I had to switch between spawnsync and execsync somehow
test('should remove remote tag when push to branch failed', async t => {
childProcess.execSync('git init', execOpts);
childProcess.execSync(`git remote add origin file://foo`, execOpts);
sh.exec(`git remote update`, execOpts);
const version = '1.2.3';
gitAdd(`{"version":"${version}"}`, 'package.json', 'Add package.json');
const options = { git: { requireCleanWorkingDir: true, commit: true, tag: true, tagName: 'v${version}' } };
const gitClient = await factory(Git, { options });
const exec = t.mock.method(gitClient.shell, 'execFormattedCommand');
sh.exec(`git push`, execOpts);
sh.exec(`git checkout HEAD~1`, execOpts);
gitAdd('line', 'file', 'Add file');
await gitClient.init();
childProcess.execSync('npm --no-git-tag-version version patch', execOpts);
gitClient.bump('1.2.4');
await gitClient.beforeRelease();
await gitClient.stage('package.json');
await gitClient.commit({ message: 'Add this' });
await gitClient.tag();
try {
await gitClient.push();
} catch (e) {
// push would fail with an error since HEAD is behind origin
}
assert.equal(exec.mock.calls[15].arguments[0], 'git push origin --delete v1.2.4');
});
test('should not touch existing history when rolling back', async t => {
childProcess.execSync('git init', execOpts);
const version = '1.2.3';
gitAdd(`{"version":"${version}"}`, 'package.json', 'Add package.json');
const options = { git: { requireCleanWorkingDir: true, commit: true, tag: true } };
const gitClient = await factory(Git, { options });
childProcess.execSync(`git tag ${version}`, execOpts);
const exec = t.mock.method(gitClient.shell, 'execFormattedCommand');
gitClient.config.setContext({ version: '1.2.4' });
await gitClient.beforeRelease();
await gitClient.commit();
await gitClient.rollbackOnce();
assert.equal(exec.mock.calls[3].arguments[0], 'git reset --hard HEAD');
});
test.skip('should not roll back with risky config', async () => {
childProcess.execSync('git init', execOpts);
const options = { git: { requireCleanWorkingDir: false, commit: true, tag: true } };
const gitClient = await factory(Git, { options });
await gitClient.beforeRelease();
assert.equal('rollbackOnce' in gitClient, false);
});
test('should return latest tag from default branch (not parent commit)', async () => {
childProcess.execSync('git init', execOpts);
{
const options = { git: { getLatestTagFromAllRefs: true } };
const gitClient = await factory(Git, { options });
gitAdd('main', 'file', 'Add file in main');
const defaultBranchName = await gitClient.getBranchName();
const developBranchName = 'develop';
const featureBranchPrefix = 'feature';
await gitClient.tag({ name: '1.0.0' });
childProcess.execSync(`git branch ${developBranchName} ${defaultBranchName}`, execOpts);
childProcess.execSync(`git checkout -b ${featureBranchPrefix}/first ${developBranchName}`, execOpts);
gitAdd('feature/1', 'file', 'Update file in feature branch (1)');
childProcess.execSync(`git checkout ${developBranchName}`, execOpts);
childProcess.execSync(`git merge --no-ff ${featureBranchPrefix}/first`, execOpts);
await gitClient.tag({ name: '1.1.0-rc.1' });
childProcess.execSync(`git checkout ${defaultBranchName}`, execOpts);
childProcess.execSync(`git merge --no-ff ${developBranchName}`, execOpts);
await gitClient.tag({ name: '1.1.0' });
childProcess.execSync(`git checkout -b ${featureBranchPrefix}/second ${developBranchName}`, execOpts);
gitAdd('feature/2', 'file', 'Update file again, in feature branch (2)');
childProcess.execSync(`git checkout ${developBranchName}`, execOpts);
childProcess.execSync(`git merge --no-ff ${featureBranchPrefix}/second`, execOpts);
assert.equal(await gitClient.getLatestTagName(), '1.1.0');
}
{
const options = { git: { getLatestTagFromAllRefs: false } };
const gitClient = await factory(Git, { options });
assert.equal(await gitClient.getLatestTagName(), '1.1.0-rc.1');
}
});
});

630
node_modules/release-it/test/github.js generated vendored Normal file
View File

@@ -0,0 +1,630 @@
import test, { describe, before, after, afterEach } from 'node:test';
import assert from 'node:assert/strict';
import { RequestError } from '@octokit/request-error';
import GitHub from '../lib/plugin/github/GitHub.js';
import { getSearchQueries } from '../lib/plugin/github/util.js';
import { factory, runTasks } from './util/index.js';
import {
interceptAuthentication,
interceptCollaborator,
interceptListReleases,
interceptCreate,
interceptUpdate,
interceptAsset
} from './stub/github.js';
import { mockFetch } from './util/mock.js';
describe('github', () => {
const tokenRef = 'GITHUB_TOKEN';
const pushRepo = 'git://github.com/user/repo';
const host = 'github.com';
const git = { changelog: '' };
const requestErrorOptions = { request: { url: '', headers: {} }, response: { headers: {} } };
const [mocker, api, assets, example, custom] = mockFetch([
'https://api.github.com',
'https://uploads.github.com',
'https://github.example.org/api/v3',
'https://custom.example.org/api/v3'
]);
before(() => {
mocker.mockGlobal();
});
afterEach(() => {
mocker.clearAll();
});
after(() => {
mocker.unmockGlobal();
});
test('should check token and perform checks', async () => {
const tokenRef = 'MY_GITHUB_TOKEN';
const options = { github: { release: true, tokenRef, pushRepo } };
const github = await factory(GitHub, { options });
interceptAuthentication(api);
interceptCollaborator(api);
await assert.doesNotReject(github.init());
});
test('should check token and warn', async () => {
const tokenRef = 'MY_GITHUB_TOKEN';
const options = { github: { release: true, tokenRef, pushRepo } };
const github = await factory(GitHub, { options });
delete process.env[tokenRef];
await assert.doesNotReject(github.init());
assert.equal(
github.log.warn.mock.calls[0].arguments[0],
'Environment variable "MY_GITHUB_TOKEN" is required for automated GitHub Releases.'
);
assert.equal(github.log.warn.mock.calls[1].arguments[0], 'Falling back to web-based GitHub Release.');
});
test('should release and upload assets', async t => {
const options = {
git,
github: {
pushRepo,
tokenRef,
release: true,
releaseName: 'Release ${tagName}',
releaseNotes: 'echo Custom notes',
assets: 'test/resources/file-v${version}.txt'
}
};
const github = await factory(GitHub, { options });
const original = github.shell.exec.bind(github.shell);
t.mock.method(github.shell, 'exec', (...args) => {
if (args[0] === 'git log --pretty=format:"* %s (%h)" ${from}...${to}') return Promise.resolve('');
if (args[0] === 'git describe --tags --match=* --abbrev=0') return Promise.resolve('2.0.1');
return original(...args);
});
interceptAuthentication(api);
interceptCollaborator(api);
interceptCreate(api, { body: { tag_name: '2.0.2', name: 'Release 2.0.2', body: 'Custom notes' } });
interceptAsset(assets, { body: '*' });
await runTasks(github);
const { isReleased, releaseUrl } = github.getContext();
assert(isReleased);
assert.equal(releaseUrl, 'https://github.com/user/repo/releases/tag/2.0.2');
});
test('should create a pre-release and draft release notes', async t => {
const options = {
git,
github: {
pushRepo,
tokenRef,
release: true,
releaseName: 'Release ${tagName}',
preRelease: true,
draft: true
}
};
const github = await factory(GitHub, { options });
const original = github.shell.exec.bind(github.shell);
t.mock.method(github.shell, 'exec', (...args) => {
if (args[0] === 'git log --pretty=format:"* %s (%h)" ${from}...${to}') return Promise.resolve('');
if (args[0] === 'git describe --tags --match=* --abbrev=0') return Promise.resolve('2.0.1');
return original(...args);
});
interceptAuthentication(api);
interceptCollaborator(api);
interceptCreate(api, { body: { tag_name: '2.0.2', name: 'Release 2.0.2', prerelease: true, draft: true } });
await runTasks(github);
const { isReleased, releaseUrl } = github.getContext();
assert(isReleased);
assert.equal(releaseUrl, 'https://github.com/user/repo/releases/tag/2.0.2');
});
test('should create auto-generated release notes', async t => {
const options = {
git,
github: {
pushRepo,
tokenRef,
release: true,
releaseName: 'Release ${tagName}',
autoGenerate: true
}
};
const github = await factory(GitHub, { options });
const original = github.shell.exec.bind(github.shell);
t.mock.method(github.shell, 'exec', (...args) => {
if (args[0] === 'git describe --tags --match=* --abbrev=0') return Promise.resolve('2.0.1');
return original(...args);
});
interceptAuthentication(api);
interceptCollaborator(api);
interceptCreate(api, {
body: { tag_name: '2.0.2', name: 'Release 2.0.2', generate_release_notes: true, body: '' }
});
await runTasks(github);
const { isReleased, releaseUrl } = github.getContext();
assert(isReleased);
assert.equal(releaseUrl, 'https://github.com/user/repo/releases/tag/2.0.2');
});
test('should update release and upload assets', async t => {
const asset = 'file1';
const options = {
increment: false,
git,
github: {
update: true,
pushRepo,
tokenRef,
release: true,
releaseName: 'Release ${tagName}',
releaseNotes: 'echo Custom notes',
assets: `test/resources/${asset}`
}
};
const github = await factory(GitHub, { options });
const original = github.shell.exec.bind(github.shell);
t.mock.method(github.shell, 'exec', (...args) => {
if (args[0] === 'git log --pretty=format:"* %s (%h)" ${from}...${to}') return Promise.resolve('');
if (args[0] === 'git describe --tags --match=* --abbrev=0') return Promise.resolve('2.0.1');
if (args[0] === 'git rev-list 2.0.1 --tags --max-count=1') return Promise.resolve('a123456');
if (args[0] === 'git describe --tags --match=* --abbrev=0') return Promise.resolve('2.0.1');
return original(...args);
});
interceptAuthentication(api);
interceptCollaborator(api);
interceptListReleases(api, { tag_name: '2.0.1' });
interceptUpdate(api, { body: { tag_name: '2.0.1', name: 'Release 2.0.1', body: 'Custom notes' } });
interceptAsset(assets, { body: asset });
await runTasks(github);
const { isReleased, releaseUrl } = github.getContext();
assert(isReleased);
assert.equal(releaseUrl, 'https://github.com/user/repo/releases/tag/2.0.1');
});
test('should create custom release notes using releaseNotes function', async t => {
const options = {
git,
github: {
pushRepo,
tokenRef,
release: true,
releaseName: 'Release ${tagName}',
releaseNotes(context) {
return `Custom notes for tag ${context.tagName}`;
}
}
};
const github = await factory(GitHub, { options });
const original = github.shell.exec.bind(github.shell);
t.mock.method(github.shell, 'exec', (...args) => {
if (args[0] === 'git describe --tags --match=* --abbrev=0') return Promise.resolve('2.0.1');
return original(...args);
});
interceptAuthentication(api);
interceptCollaborator(api);
interceptCreate(api, {
body: { tag_name: '2.0.2', name: 'Release 2.0.2', body: 'Custom notes for tag 2.0.2' }
});
await runTasks(github);
const { isReleased, releaseUrl } = github.getContext();
assert(isReleased);
assert.equal(releaseUrl, 'https://github.com/user/repo/releases/tag/2.0.2');
});
test('should create new release for unreleased tag', async t => {
const options = {
increment: false,
git,
github: {
update: true,
pushRepo,
tokenRef,
release: true,
releaseName: 'Release ${tagName}',
releaseNotes: 'echo Custom notes'
}
};
const github = await factory(GitHub, { options });
const original = github.shell.exec.bind(github.shell);
t.mock.method(github.shell, 'exec', (...args) => {
if (args[0] === 'git log --pretty=format:"* %s (%h)" ${from}...${to}') return Promise.resolve('');
if (args[0] === 'git describe --tags --match=* --abbrev=0') return Promise.resolve('2.0.1');
if (args[0] === 'git rev-list 2.0.1 --tags --max-count=1') return Promise.resolve('b123456');
if (args[0] === 'git describe --tags --abbrev=0 "b123456^"') return Promise.resolve('2.0.1');
return original(...args);
});
interceptAuthentication(api);
interceptCollaborator(api);
interceptListReleases(api, { tag_name: '2.0.0' });
interceptCreate(api, { body: { tag_name: '2.0.1', name: 'Release 2.0.1', body: 'Custom notes' } });
await runTasks(github);
const { isReleased, releaseUrl } = github.getContext();
assert(isReleased);
assert.equal(releaseUrl, 'https://github.com/user/repo/releases/tag/2.0.1');
});
test('should release to enterprise host', async t => {
const options = { git, github: { tokenRef, pushRepo: 'git://github.example.org/user/repo' } };
const github = await factory(GitHub, { options });
const original = github.shell.exec.bind(github.shell);
t.mock.method(github.shell, 'exec', (...args) => {
if (args[0] === 'git remote get-url origin') return Promise.resolve(`https://github.example.org/user/repo`);
if (args[0] === 'git config --get remote.origin.url')
return Promise.resolve('https://github.example.org/user/repo');
if (args[0] === 'git describe --tags --match=* --abbrev=0') return Promise.resolve('1.0.0');
return original(...args);
});
interceptAuthentication(example);
interceptCollaborator(example);
interceptCreate(example, {
api: 'https://github.example.org/api/v3',
host: 'github.example.org',
body: { tag_name: '1.0.1' }
});
await runTasks(github);
const { isReleased, releaseUrl } = github.getContext();
assert(isReleased);
assert.equal(releaseUrl, 'https://github.example.org/user/repo/releases/tag/1.0.1');
});
test('should release to alternative host and proxy', async t => {
const options = {
git,
github: {
tokenRef,
pushRepo: `git://custom.example.org/user/repo`,
host: 'custom.example.org',
proxy: 'http://proxy:8080'
}
};
const github = await factory(GitHub, { options });
const original = github.shell.exec.bind(github.shell);
t.mock.method(github.shell, 'exec', (...args) => {
if (args[0] === 'git log --pretty=format:"* %s (%h)" ${from}...${to}') return Promise.resolve('');
if (args[0] === 'git describe --tags --match=* --abbrev=0') return Promise.resolve('1.0.0');
return original(...args);
});
interceptAuthentication(custom);
interceptCollaborator(custom);
interceptCreate(custom, {
api: 'https://custom.example.org/api/v3',
host: 'custom.example.org',
body: { tag_name: '1.0.1' }
});
await runTasks(github);
const { isReleased, releaseUrl } = github.getContext();
assert(isReleased);
assert.equal(releaseUrl, 'https://custom.example.org/user/repo/releases/tag/1.0.1');
assert.equal(github.options.proxy, 'http://proxy:8080');
});
test('should release to git.pushRepo', async t => {
const options = { git: { pushRepo: 'upstream', changelog: '' }, github: { tokenRef, skipChecks: true } };
const github = await factory(GitHub, { options });
const original = github.shell.exec.bind(github.shell);
t.mock.method(github.shell, 'exec', (...args) => {
if (args[0] === 'git log --pretty=format:"* %s (%h)" ${from}...${to}') return Promise.resolve('');
if (args[0] === 'git describe --tags --match=* --abbrev=0') return Promise.resolve('1.0.0');
if (args[0] === 'git remote get-url upstream') return Promise.resolve('https://custom.example.org/user/repo');
return original(...args);
});
interceptCreate(custom, {
api: 'https://custom.example.org/api/v3',
host: 'custom.example.org',
body: { tag_name: '1.0.1' }
});
await runTasks(github);
const { isReleased, releaseUrl } = github.getContext();
assert(isReleased);
assert.equal(releaseUrl, 'https://custom.example.org/user/repo/releases/tag/1.0.1');
});
const testSkipOnActions = process.env.GITHUB_ACTIONS ? test.skip : test;
testSkipOnActions('should throw for unauthenticated user', async t => {
const options = { github: { tokenRef, pushRepo, host } };
const github = await factory(GitHub, { options });
const getAuthenticated = t.mock.method(github.client.users, 'getAuthenticated', () => {
throw new RequestError('Bad credentials', 401, requestErrorOptions);
});
await assert.rejects(runTasks(github), {
message: /Could not authenticate with GitHub using environment variable "GITHUB_TOKEN"/
});
assert.equal(getAuthenticated.mock.callCount(), 1);
});
testSkipOnActions('should throw for non-collaborator', async t => {
const options = { github: { tokenRef, pushRepo, host } };
const github = await factory(GitHub, { options });
t.mock.method(github.client.repos, 'checkCollaborator', () => {
throw new RequestError('HttpError', 401, requestErrorOptions);
});
interceptAuthentication(api, { username: 'john' });
await assert.rejects(runTasks(github), /User john is not a collaborator for user\/repo/);
});
test('should skip authentication and collaborator checks when running on GitHub Actions', async t => {
const { GITHUB_ACTIONS, GITHUB_ACTOR } = process.env;
if (!GITHUB_ACTIONS) {
process.env.GITHUB_ACTIONS = 1;
process.env.GITHUB_ACTOR = 'webpro';
}
const options = { github: { tokenRef } };
const github = await factory(GitHub, { options });
const authStub = t.mock.method(github, 'isAuthenticated');
const collaboratorStub = t.mock.method(github, 'isCollaborator');
await assert.doesNotReject(github.init());
assert.equal(authStub.mock.callCount(), 0);
assert.equal(collaboratorStub.mock.callCount(), 0);
assert.equal(github.getContext('username'), process.env.GITHUB_ACTOR);
if (!GITHUB_ACTIONS) {
process.env.GITHUB_ACTIONS = GITHUB_ACTIONS || '';
process.env.GITHUB_ACTOR = GITHUB_ACTOR || '';
}
});
test('should handle octokit client error (without retries)', async t => {
const github = await factory(GitHub, { options: { github: { tokenRef, pushRepo, host } } });
const createRelease = t.mock.method(github.client.repos, 'createRelease', () => {
throw new RequestError('Not found', 404, requestErrorOptions);
});
interceptAuthentication(api);
interceptCollaborator(api);
await assert.rejects(runTasks(github), /404 \(Not found\)/);
assert.equal(createRelease.mock.callCount(), 1);
});
test('should handle octokit client error (with retries)', async t => {
const options = { github: { tokenRef, pushRepo, host, retryMinTimeout: 0 } };
const github = await factory(GitHub, { options });
const createRelease = t.mock.method(github.client.repos, 'createRelease', () => {
throw new RequestError('Request failed', 500, requestErrorOptions);
});
interceptAuthentication(api);
interceptCollaborator(api);
await assert.rejects(runTasks(github), /500 \(Request failed\)/);
assert.equal(createRelease.mock.callCount(), 3);
});
test('should not call octokit client in dry run', async t => {
const options = {
'dry-run': true,
git,
github: { tokenRef, pushRepo, releaseName: 'R ${version}', assets: ['*'] }
};
const github = await factory(GitHub, { options });
const get = t.mock.getter(github, 'client');
const original = github.shell.exec.bind(github.shell);
t.mock.method(github.shell, 'exec', (...args) => {
if (args[0] === 'git log --pretty=format:"* %s (%h)" ${from}...${to}') return Promise.resolve('');
if (args[0] === 'git describe --tags --match=* --abbrev=0') return Promise.resolve('v1.0.0');
return original(...args);
});
await runTasks(github);
assert.equal(get.mock.callCount(), 0);
assert.equal(github.log.exec.mock.calls[1].arguments[0], 'octokit repos.createRelease "R 1.0.1" (v1.0.1)');
assert.equal(github.log.exec.mock.calls.at(-1).arguments[0], 'octokit repos.uploadReleaseAssets');
const { isReleased, releaseUrl } = github.getContext();
assert(isReleased);
assert.equal(releaseUrl, 'https://github.com/user/repo/releases/tag/v1.0.1');
});
test('should generate GitHub web release url', async t => {
const options = {
github: {
pushRepo,
release: true,
web: true,
releaseName: 'Release ${tagName}',
releaseNotes: 'echo Custom notes'
}
};
const github = await factory(GitHub, { options });
const original = github.shell.exec.bind(github.shell);
t.mock.method(github.shell, 'exec', (...args) => {
if (args[0] === 'git log --pretty=format:"* %s (%h)" ${from}...${to}') return Promise.resolve('');
if (args[0] === 'git describe --tags --match=* --abbrev=0') return Promise.resolve('2.0.1');
return original(...args);
});
await runTasks(github);
const { isReleased, releaseUrl } = github.getContext();
assert(isReleased);
assert.equal(
releaseUrl,
'https://github.com/user/repo/releases/new?tag=2.0.2&title=Release+2.0.2&body=Custom+notes&prerelease=false'
);
});
test('should generate GitHub web release url for enterprise host', async t => {
const options = {
git,
github: {
pushRepo: 'git@custom.example.org:user/repo',
release: true,
web: true,
host: 'custom.example.org',
releaseName: 'The Launch',
releaseNotes: 'echo It happened'
}
};
const github = await factory(GitHub, { options });
const original = github.shell.exec.bind(github.shell);
t.mock.method(github.shell, 'exec', (...args) => {
if (args[0] === 'git log --pretty=format:"* %s (%h)" ${from}...${to}') return Promise.resolve('');
if (args[0] === 'git describe --tags --match=* --abbrev=0') return Promise.resolve('2.0.1');
return original(...args);
});
await runTasks(github);
const { isReleased, releaseUrl } = github.getContext();
assert(isReleased);
assert.equal(
releaseUrl,
'https://custom.example.org/user/repo/releases/new?tag=2.0.2&title=The+Launch&body=It+happened&prerelease=false'
);
});
test.skip('should truncate long body', async t => {
const releaseNotes = 'a'.repeat(125001);
const body = 'a'.repeat(124000) + '...';
const options = {
git,
github: {
pushRepo,
tokenRef,
release: true,
releaseName: 'Release ${tagName}',
releaseNotes: 'echo ' + releaseNotes
}
};
const github = await factory(GitHub, { options });
const original = github.shell.exec.bind(github.shell);
t.mock.method(github.shell, 'exec', (...args) => {
if (args[0] === 'git log --pretty=format:"* %s (%h)" ${from}...${to}') return Promise.resolve('');
if (args[0] === 'git describe --tags --match=* --abbrev=0') return Promise.resolve('2.0.1');
return original(...args);
});
interceptAuthentication(api);
interceptCollaborator(api);
interceptCreate(api, { body: { tag_name: '2.0.2', name: 'Release 2.0.2', body } });
await runTasks(github);
const { isReleased, releaseUrl } = github.getContext();
assert(isReleased);
assert.equal(releaseUrl, 'https://github.com/user/repo/releases/tag/2.0.2');
});
test('should generate search queries correctly', () => {
const generateCommit = () => Math.random().toString(36).substring(2, 9);
const base = 'repo:owner/repo+type:pr+is:merged';
const commits = Array.from({ length: 5 }, generateCommit);
const separator = '+';
const result = getSearchQueries(base, commits, separator);
// Test case 1: Check if all commits are included in the search queries
const allCommitsIncluded = commits.every(commit => result.some(query => query.includes(commit)));
assert(allCommitsIncluded, 'All commits should be included in the search queries');
assert.equal(
commits.every(commit => result.some(query => query.includes(commit))),
true
);
// Test case 2: Check if the function respects the 256 character limit
const manyCommits = Array.from({ length: 100 }, generateCommit);
const longResult = getSearchQueries(base, manyCommits, separator);
assert(longResult.length > 1, 'Many commits should be split into multiple queries');
assert(
longResult.every(query => encodeURIComponent(query).length <= 256),
'Each query should not exceed 256 characters after encoding'
);
});
test('should create auto-generated discussion', async t => {
const options = {
git,
github: {
pushRepo,
tokenRef,
release: true,
releaseName: 'Release ${tagName}',
autoGenerate: false,
discussionCategoryName: 'Announcement'
}
};
const github = await factory(GitHub, { options });
const original = github.shell.exec.bind(github.shell);
t.mock.method(github.shell, 'exec', (...args) => {
if (args[0] === 'git describe --tags --match=* --abbrev=0') return Promise.resolve('2.0.1');
return original(...args);
});
interceptAuthentication(api);
interceptCollaborator(api);
interceptCreate(api, {
body: {
tag_name: '2.0.2',
name: 'Release 2.0.2',
generate_release_notes: false,
body: null,
discussion_category_name: 'Announcement'
}
});
await runTasks(github);
const { isReleased, releaseUrl, discussionUrl } = github.getContext();
assert(isReleased);
assert.equal(releaseUrl, 'https://github.com/user/repo/releases/tag/2.0.2');
assert.equal(discussionUrl, 'https://github.com/user/repo/discussions/1');
});
});

455
node_modules/release-it/test/gitlab.js generated vendored Normal file
View File

@@ -0,0 +1,455 @@
import fs from 'node:fs';
import test, { before, after, afterEach, beforeEach, describe } from 'node:test';
import assert from 'node:assert/strict';
import { Agent } from 'undici';
import Git from '../lib/plugin/git/Git.js';
import GitLab from '../lib/plugin/gitlab/GitLab.js';
import { GitlabTestServer } from './util/https-server/server.js';
import { factory, runTasks } from './util/index.js';
import {
interceptUser,
interceptCollaborator,
interceptPublish,
interceptAsset,
interceptAssetGeneric,
interceptMilestones,
interceptMembers
} from './stub/gitlab.js';
import { mockFetch } from './util/mock.js';
describe('GitLab', () => {
const tokenHeader = 'Private-Token';
const tokenRef = 'GITLAB_TOKEN';
const certificateAuthorityFileRef = 'CI_SERVER_TLS_CA_FILE';
const [mocker, api, example, local] = mockFetch([
'https://gitlab.com/api/v4',
'https://gitlab.example.org/api/v4',
'https://localhost:3000/api/v4'
]);
before(() => {
mocker.mockGlobal();
});
let originalEnv;
beforeEach(() => {
originalEnv = process.env;
process.env = { ...originalEnv };
process.env[tokenRef] = '123';
});
afterEach(() => {
if (originalEnv !== undefined) process.env = originalEnv;
mocker.clearAll();
});
after(() => {
mocker.unmockGlobal();
});
test('should validate token', async () => {
const tokenRef = 'MY_GITLAB_TOKEN';
const pushRepo = 'https://gitlab.com/user/repo';
const options = { gitlab: { release: true, tokenRef, tokenHeader, pushRepo } };
const gitlab = await factory(GitLab, { options });
delete process.env[tokenRef];
await assert.rejects(gitlab.init(), /Environment variable "MY_GITLAB_TOKEN" is required for GitLab releases/);
process.env[tokenRef] = '123';
interceptUser(api, { headers: { 'private-token': '123' } });
interceptCollaborator(api, { headers: { 'private-token': '123' } });
await assert.doesNotReject(gitlab.init());
});
test('should support CI Job token header', async () => {
const tokenRef = 'CI_JOB_TOKEN';
const tokenHeader = 'Job-Token';
process.env[tokenRef] = 'j0b-t0k3n';
const pushRepo = 'https://gitlab.com/user/repo';
const options = { git: { pushRepo }, gitlab: { release: true, tokenRef, tokenHeader } };
const gitlab = await factory(GitLab, { options });
interceptPublish(api, { headers: { 'job-token': '1' } });
await assert.doesNotReject(gitlab.init());
delete process.env[tokenRef];
});
test('should upload assets and release', async t => {
const pushRepo = 'https://gitlab.com/user/repo';
const options = {
git: { pushRepo },
gitlab: {
tokenRef,
release: true,
releaseName: 'Release ${version}',
releaseNotes: 'echo Custom notes',
assets: 'test/resources/file-v${version}.txt',
milestones: ['${version}', '${latestVersion} UAT']
}
};
const gitlab = await factory(GitLab, { options });
t.mock.method(gitlab, 'getLatestVersion', () => Promise.resolve('2.0.0'));
const git = await factory(Git);
const ref = (await git.getBranchName()) ?? 'HEAD';
interceptUser(api);
interceptCollaborator(api);
interceptMilestones(api, { query: { title: '2.0.1' }, milestones: [{ id: 17, iid: 3, title: '2.0.1' }] });
interceptMilestones(api, { query: { title: '2.0.0 UAT' }, milestones: [{ id: 42, iid: 4, title: '2.0.0 UAT' }] });
interceptAsset(api);
interceptPublish(api, {
body: {
name: 'Release 2.0.1',
ref,
tag_name: '2.0.1',
tag_message: 'Release 2.0.1',
description: 'Custom notes',
assets: {
links: [
{ name: 'file-v2.0.1.txt', url: `${pushRepo}/uploads/7e8bec1fe27cc46a4bc6a91b9e82a07c/file-v2.0.1.txt` }
]
},
milestones: ['2.0.1', '2.0.0 UAT']
}
});
await runTasks(gitlab);
assert.equal(gitlab.assets[0].url, `${pushRepo}/uploads/7e8bec1fe27cc46a4bc6a91b9e82a07c/file-v2.0.1.txt`);
const { isReleased, releaseUrl } = gitlab.getContext();
assert(isReleased);
assert.equal(releaseUrl, `${pushRepo}/-/releases/2.0.1`);
});
test('should upload assets with ID-based URLs', async t => {
const host = 'https://gitlab.com';
const pushRepo = `${host}/user/repo`;
const options = {
git: { pushRepo },
gitlab: {
tokenRef,
release: true,
assets: 'test/resources/file-v${version}.txt',
useIdsForUrls: true
}
};
const gitlab = await factory(GitLab, { options });
t.mock.method(gitlab, 'getLatestVersion', () => Promise.resolve('2.0.0'));
interceptUser(api);
interceptCollaborator(api);
interceptAsset(api);
interceptPublish(api);
await runTasks(gitlab);
assert.equal(
gitlab.assets[0].url,
`${host}/-/project/1234/uploads/7e8bec1fe27cc46a4bc6a91b9e82a07c/file-v2.0.1.txt`
);
});
test('should upload assets to generic repo', async t => {
const host = 'https://gitlab.com';
const pushRepo = `${host}/user/repo`;
const options = {
git: { pushRepo },
gitlab: {
tokenRef,
release: true,
assets: 'test/resources/file-v${version}.txt',
useGenericPackageRepositoryForAssets: true,
genericPackageRepositoryName: 'release-it'
}
};
const gitlab = await factory(GitLab, { options });
t.mock.method(gitlab, 'getLatestVersion', () => Promise.resolve('2.0.0'));
interceptUser(api);
interceptCollaborator(api);
interceptAssetGeneric(api);
interceptPublish(api);
await runTasks(gitlab);
assert.equal(
gitlab.assets[0].url,
`${host}/api/v4/projects/user%2Frepo/packages/generic/release-it/2.0.1/file-v2.0.1.txt`
);
});
test('should throw when release milestone is missing', async t => {
const pushRepo = 'https://gitlab.com/user/repo';
const options = {
git: { pushRepo },
gitlab: {
tokenRef,
release: true,
milestones: ['${version}', '${latestVersion} UAT']
}
};
const gitlab = await factory(GitLab, { options });
t.mock.method(gitlab, 'getLatestVersion', () => Promise.resolve('2.0.0'));
interceptUser(api);
interceptCollaborator(api);
interceptMilestones(api, { query: { title: '2.0.1' }, milestones: [{ id: 17, iid: 3, title: '2.0.1' }] });
interceptMilestones(api, { query: { title: '2.0.0 UAT' }, milestones: [] });
await assert.rejects(
runTasks(gitlab),
/Missing one or more milestones in GitLab. Creating a GitLab release will fail./
);
});
test('should release to self-managed host', async t => {
const host = 'https://gitlab.example.org';
const options = {
git: { pushRepo: `${host}/user/repo` },
gitlab: { releaseName: 'Release ${version}', releaseNotes: 'echo readme', tokenRef }
};
const gitlab = await factory(GitLab, { options });
t.mock.method(gitlab, 'getLatestVersion', () => Promise.resolve('1.0.0'));
interceptUser(example);
interceptCollaborator(example);
interceptPublish(example);
await runTasks(gitlab);
const { origin, baseUrl } = gitlab.getContext();
assert.equal(origin, host);
assert.equal(baseUrl, `${host}/api/v4`);
});
test('should release to sub-grouped repo', async () => {
const options = { gitlab: { tokenRef }, git: { pushRepo: 'git@gitlab.com:group/sub-group/repo.git' } };
const gitlab = await factory(GitLab, { options });
interceptUser(api, { owner: 'sub-group' });
interceptCollaborator(api, { owner: 'sub-group', group: 'group' });
interceptPublish(api, { owner: 'group', project: 'sub-group%2Frepo' });
await runTasks(gitlab);
const { isReleased, releaseUrl } = gitlab.getContext();
assert(isReleased);
assert.match(releaseUrl, /https:\/\/gitlab.com\/group\/sub-group(\/|%2F)repo\/-\/releases\//);
});
test('should throw for unauthenticated user', async () => {
const host = 'https://gitlab.com';
const pushRepo = `${host}/user/repo`;
const options = { gitlab: { tokenRef, pushRepo, host } };
const gitlab = await factory(GitLab, { options });
api.get('/user', { status: 401 });
await assert.rejects(
runTasks(gitlab),
/Could not authenticate with GitLab using environment variable "GITLAB_TOKEN"/
);
});
test('should throw for non-collaborator', async () => {
const host = 'https://gitlab.com';
const pushRepo = `${host}/john/repo`;
const options = { gitlab: { tokenRef, pushRepo, host } };
const gitlab = await factory(GitLab, { options });
interceptMembers(api, { owner: 'emma' });
interceptUser(api, { owner: 'john' });
await assert.rejects(runTasks(gitlab), /User john is not a collaborator for john\/repo/);
});
test('should throw for insufficient access level', async () => {
const host = 'https://gitlab.com';
const pushRepo = `${host}/john/repo`;
const options = { gitlab: { tokenRef, pushRepo, host } };
const gitlab = await factory(GitLab, { options });
interceptMembers(api, { owner: 'john', access_level: 10 });
interceptUser(api, { owner: 'john' });
await assert.rejects(runTasks(gitlab), /User john is not a collaborator for john\/repo/);
});
test('should not make requests in dry run', async t => {
const [host, owner, repo] = ['https://gitlab.example.org', 'user', 'repo'];
const pushRepo = `${host}/${owner}/${repo}`;
const options = { 'dry-run': true, git: { pushRepo }, gitlab: { releaseName: 'R', tokenRef } };
const gitlab = await factory(GitLab, { options });
t.mock.method(gitlab, 'getLatestVersion', () => Promise.resolve('1.0.0'));
await runTasks(gitlab);
const { isReleased, releaseUrl } = gitlab.getContext();
assert.equal(gitlab.log.exec.mock.calls[2].arguments[0], 'gitlab releases#uploadAssets');
assert.equal(gitlab.log.exec.mock.calls[3].arguments[0], 'gitlab releases#createRelease "R" (1.0.1)');
assert(isReleased);
assert.equal(releaseUrl, `${pushRepo}/-/releases/1.0.1`);
});
test('should skip checks', async () => {
const options = { gitlab: { tokenRef, skipChecks: true, release: true, milestones: ['v1.0.0'] } };
const gitlab = await factory(GitLab, { options });
await assert.doesNotReject(gitlab.init());
await assert.doesNotReject(gitlab.beforeRelease());
assert.equal(
gitlab.log.exec.mock.calls
.flatMap(call => call.arguments)
.filter(entry => /checkReleaseMilestones/.test(entry[0])).length,
0
);
});
test('should not create fetch agent', async () => {
const options = { gitlab: {} };
const gitlab = await factory(GitLab, { options });
assert.deepEqual(gitlab.certificateAuthorityOption, {});
});
test('should create fetch agent if secure == false', async () => {
const options = { gitlab: { secure: false } };
const gitlab = await factory(GitLab, { options });
const { dispatcher } = gitlab.certificateAuthorityOption;
assert(dispatcher instanceof Agent, "Fetch dispatcher should be an instance of undici's Agent class");
const kOptions = Object.getOwnPropertySymbols(dispatcher).find(symbol => symbol.description === 'options');
assert.deepEqual(dispatcher[kOptions].connect, { rejectUnauthorized: false, ca: undefined });
});
test('should create fetch agent if certificateAuthorityFile', async t => {
const readFileSync = t.mock.method(fs, 'readFileSync', () => 'test certificate');
const options = { gitlab: { certificateAuthorityFile: 'cert.crt' } };
const gitlab = await factory(GitLab, { options });
const { dispatcher } = gitlab.certificateAuthorityOption;
assert(dispatcher instanceof Agent, "Fetch dispatcher should be an instance of undici's Agent class");
const kOptions = Object.getOwnPropertySymbols(dispatcher).find(symbol => symbol.description === 'options');
assert.deepEqual(dispatcher[kOptions].connect, { rejectUnauthorized: undefined, ca: 'test certificate' });
readFileSync.mock.restore();
});
test('should create fetch agent if CI_SERVER_TLS_CA_FILE env is set', async t => {
const readFileSync = t.mock.method(fs, 'readFileSync', () => 'test certificate');
process.env[certificateAuthorityFileRef] = 'ca.crt';
const options = { gitlab: {} };
const gitlab = await factory(GitLab, { options });
const { dispatcher } = gitlab.certificateAuthorityOption;
assert(dispatcher instanceof Agent, "Fetch dispatcher should be an instance of undici's Agent class");
const kOptions = Object.getOwnPropertySymbols(dispatcher).find(symbol => symbol.description === 'options');
assert.deepEqual(dispatcher[kOptions].connect, { rejectUnauthorized: undefined, ca: 'test certificate' });
readFileSync.mock.restore();
});
test('should create fetch agent if certificateAuthorityFileRef env is set', async t => {
const readFileSync = t.mock.method(fs, 'readFileSync', () => 'test certificate');
process.env['GITLAB_CA_FILE'] = 'custom-ca.crt';
const options = { gitlab: { certificateAuthorityFileRef: 'GITLAB_CA_FILE' } };
const gitlab = await factory(GitLab, { options });
const { dispatcher } = gitlab.certificateAuthorityOption;
assert(dispatcher instanceof Agent, "Fetch dispatcher should be an instance of undici's Agent class");
const kOptions = Object.getOwnPropertySymbols(dispatcher).find(symbol => symbol.description === 'options');
assert.deepEqual(dispatcher[kOptions].connect, { rejectUnauthorized: undefined, ca: 'test certificate' });
readFileSync.mock.restore();
});
test('should throw for insecure connections to self-hosted instances', async t => {
const host = 'https://localhost:3000';
const options = {
git: { pushRepo: `${host}/user/repo` },
gitlab: { host, tokenRef, origin: host }
};
const gitlab = await factory(GitLab, { options });
const server = new GitlabTestServer();
t.after(async () => {
await server.stop();
});
await server.run();
await assert.rejects(gitlab.init(), /Could not authenticate with GitLab using environment variable "GITLAB_TOKEN"/);
});
test('should succesfully connect to self-hosted instance if insecure connection allowed', async t => {
const host = 'https://localhost:3000';
const options = {
git: { pushRepo: `${host}/user/repo` },
gitlab: {
host,
tokenRef,
origin: host,
secure: false
}
};
const gitlab = await factory(GitLab, { options });
const server = new GitlabTestServer();
t.after(async () => {
await server.stop();
});
await server.run();
interceptUser(local);
interceptCollaborator(local);
await assert.doesNotReject(gitlab.init());
});
test('should succesfully connect to self-hosted instance with valid CA file', async t => {
const host = 'https://localhost:3000';
const options = {
git: { pushRepo: `${host}/user/repo` },
gitlab: {
host,
tokenRef,
origin: host,
certificateAuthorityFile: 'test/util/https-server/client/my-private-root-ca.cert.pem'
}
};
const gitlab = await factory(GitLab, { options });
const server = new GitlabTestServer();
t.after(async () => {
await server.stop();
});
await server.run();
interceptUser(local);
interceptCollaborator(local);
await assert.doesNotReject(gitlab.init());
});
});

146
node_modules/release-it/test/log.js generated vendored Normal file
View File

@@ -0,0 +1,146 @@
import { EOL } from 'node:os';
import test, { describe } from 'node:test';
import assert from 'node:assert/strict';
import { stripVTControlCharacters } from 'node:util';
import mockStdIo from 'mock-stdio';
import Log from '../lib/log.js';
describe('log', () => {
test('should write to stdout', () => {
const log = new Log();
mockStdIo.start();
log.log('foo');
const { stdout, stderr } = mockStdIo.end();
assert.equal(stdout, 'foo\n');
assert.equal(stderr, '');
});
test('should write to stderr', () => {
const log = new Log();
mockStdIo.start();
log.error('foo');
const { stdout, stderr } = mockStdIo.end();
assert.equal(stdout, '');
assert.equal(stripVTControlCharacters(stderr), 'ERROR foo\n');
});
test('should print a warning', () => {
const log = new Log();
mockStdIo.start();
log.warn('foo');
const { stderr } = mockStdIo.end();
assert.equal(stripVTControlCharacters(stderr), 'WARNING foo\n');
});
test('should print verbose', () => {
const log = new Log({ isVerbose: true, verbosityLevel: 2 });
mockStdIo.start();
log.verbose('foo');
const { stderr } = mockStdIo.end();
assert.equal(stderr, 'foo\n');
});
test('should print external scripts verbose', () => {
const log = new Log({ isVerbose: true });
mockStdIo.start();
log.verbose('foo', { isExternal: true });
const { stderr } = mockStdIo.end();
assert.equal(stderr, 'foo\n');
});
test('should always print external scripts verbose', () => {
const log = new Log({ isVerbose: true, verbosityLevel: 2 });
mockStdIo.start();
log.verbose('foo', { isExternal: true });
const { stderr } = mockStdIo.end();
assert.equal(stderr, 'foo\n');
});
test('should not print verbose by default', () => {
const log = new Log();
mockStdIo.start();
log.verbose('foo');
const { stderr } = mockStdIo.end();
assert.equal(stderr, '');
});
test('should not print command execution by default', () => {
const log = new Log();
mockStdIo.start();
log.exec('foo');
const { stderr } = mockStdIo.end();
assert.equal(stderr.trim(), '');
});
test('should print command execution (verbose)', () => {
const log = new Log({ isVerbose: true, verbosityLevel: 2 });
mockStdIo.start();
log.exec('foo');
const { stderr } = mockStdIo.end();
assert.equal(stderr.trim(), '$ foo');
});
test('should print command execution (verbose/dry run)', () => {
const log = new Log({ isVerbose: true });
mockStdIo.start();
log.exec('foo', { isDryRun: true, isExternal: true });
const { stderr } = mockStdIo.end();
assert.equal(stderr.trim(), '! foo');
});
test('should print command execution (verbose/external)', () => {
const log = new Log({ isVerbose: true });
mockStdIo.start();
log.exec('foo', { isExternal: true });
const { stderr } = mockStdIo.end();
assert.equal(stderr.trim(), '$ foo');
});
test('should print command execution (dry run)', () => {
const log = new Log({ isDryRun: true });
mockStdIo.start();
log.exec('foo');
const { stderr } = mockStdIo.end();
assert.equal(stderr, '$ foo\n');
});
test('should print command execution (read-only)', () => {
const log = new Log({ isDryRun: true });
mockStdIo.start();
log.exec('foo', 'bar', false);
const { stderr } = mockStdIo.end();
assert.equal(stderr, '$ foo bar\n');
});
test('should print command execution (write)', () => {
const log = new Log({ isDryRun: true });
mockStdIo.start();
log.exec('foo', '--arg n', { isDryRun: true });
const { stderr } = mockStdIo.end();
assert.equal(stderr, '! foo --arg n\n');
});
test('should print obtrusive', () => {
const log = new Log({ isCI: false });
mockStdIo.start();
log.obtrusive('spacious');
const { stdout } = mockStdIo.end();
assert.equal(stdout, '\nspacious\n\n');
});
test('should not print obtrusive in CI mode', () => {
const log = new Log({ isCI: true });
mockStdIo.start();
log.obtrusive('normal');
const { stdout } = mockStdIo.end();
assert.equal(stdout, 'normal\n');
});
test('should print preview', () => {
const log = new Log();
mockStdIo.start();
log.preview({ title: 'title', text: 'changelog' });
const { stdout } = mockStdIo.end();
assert.equal(stripVTControlCharacters(stdout), `Title:${EOL}changelog\n`);
});
});

360
node_modules/release-it/test/npm.js generated vendored Normal file
View File

@@ -0,0 +1,360 @@
import { join } from 'node:path';
import test, { describe } from 'node:test';
import assert from 'node:assert/strict';
import { writeFileSync } from 'node:fs';
import npm from '../lib/plugin/npm/npm.js';
import { factory, runTasks } from './util/index.js';
import { mkTmpDir, getArgs } from './util/helpers.js';
describe('npm', async () => {
test('should return npm package url', async () => {
const options = { npm: { name: 'my-cool-package' } };
const npmClient = await factory(npm, { options });
assert.equal(npmClient.getPackageUrl(), 'https://www.npmjs.com/package/my-cool-package');
});
test('should return npm package url (custom registry)', async () => {
const options = { npm: { name: 'my-cool-package', publishConfig: { registry: 'https://registry.example.org/' } } };
const npmClient = await factory(npm, { options });
assert.equal(npmClient.getPackageUrl(), 'https://registry.example.org/package/my-cool-package');
});
test('should return npm package url (custom publicPath)', async () => {
const options = { npm: { name: 'my-cool-package', publishConfig: { publicPath: '/custom/public-path' } } };
const npmClient = await factory(npm, { options });
assert.equal(npmClient.getPackageUrl(), 'https://www.npmjs.com/custom/public-path/my-cool-package');
});
test('should return npm package url (custom registry and publicPath)', async () => {
const options = {
npm: {
name: 'my-cool-package',
publishConfig: { registry: 'https://registry.example.org/', publicPath: '/custom/public-path' }
}
};
const npmClient = await factory(npm, { options });
assert.equal(npmClient.getPackageUrl(), 'https://registry.example.org/custom/public-path/my-cool-package');
});
test('should return default tag', async () => {
const npmClient = await factory(npm);
const tag = await npmClient.resolveTag();
assert.equal(tag, 'latest');
});
test('should resolve default tag for pre-release', async t => {
const npmClient = await factory(npm);
t.mock.method(npmClient, 'getRegistryPreReleaseTags', () => []);
const tag = await npmClient.resolveTag('1.0.0-0');
assert.equal(tag, 'next');
});
test('should guess tag from registry for pre-release', async t => {
const npmClient = await factory(npm);
t.mock.method(npmClient, 'getRegistryPreReleaseTags', () => ['alpha']);
const tag = await npmClient.resolveTag('1.0.0-0');
assert.equal(tag, 'alpha');
});
test('should derive tag from pre-release version', async () => {
const npmClient = await factory(npm);
const tag = await npmClient.resolveTag('1.0.2-alpha.3');
assert.equal(tag, 'alpha');
});
test('should use provided (default) tag even for pre-release', async t => {
const options = { npm: { tag: 'latest' } };
const npmClient = await factory(npm, { options });
t.mock.method(npmClient.shell, 'exec', () => Promise.resolve());
await npmClient.bump('1.0.0-next.0');
assert.equal(npmClient.getContext('tag'), 'latest');
});
test('should throw when `npm version` fails', async t => {
const npmClient = await factory(npm);
t.mock.method(npmClient.shell, 'exec', () =>
Promise.reject(new Error('npm ERR! Version not changed, might want --allow-same-version'))
);
await assert.rejects(npmClient.bump('1.0.0-next.0'), { message: /Version not changed/ });
});
test('should return first pre-release tag from package in registry when resolving tag without pre-id', async t => {
const npmClient = await factory(npm);
const response = { latest: '1.4.1', alpha: '2.0.0-alpha.1', beta: '2.0.0-beta.3' };
t.mock.method(npmClient.shell, 'exec', () => Promise.resolve(JSON.stringify(response)));
assert.equal(await npmClient.resolveTag('2.0.0-5'), 'alpha');
});
test('should return default pre-release tag when resolving tag without pre-id', async t => {
const npmClient = await factory(npm);
const response = {
latest: '1.4.1'
};
t.mock.method(npmClient.shell, 'exec', () => Promise.resolve(JSON.stringify(response)));
assert.equal(await npmClient.resolveTag('2.0.0-0'), 'next');
});
test('should handle erroneous output when resolving tag without pre-id', async t => {
const npmClient = await factory(npm);
t.mock.method(npmClient.shell, 'exec', () => Promise.resolve(''));
assert.equal(await npmClient.resolveTag('2.0.0-0'), 'next');
});
test('should handle errored request when resolving tag without pre-id', async t => {
const npmClient = await factory(npm);
t.mock.method(npmClient.shell, 'exec', () => Promise.resolve());
assert.equal(await npmClient.resolveTag('2.0.0-0'), 'next');
});
test('should add registry to commands when specified', async t => {
const npmClient = await factory(npm);
npmClient.setContext({ publishConfig: { registry: 'registry.example.org' } });
const exec = t.mock.method(npmClient.shell, 'exec', command => {
if (command === 'npm whoami --registry registry.example.org') return Promise.resolve('john');
const re = /npm access (list collaborators --json|ls-collaborators) release-it --registry registry.example.org/;
if (re.test.command) return Promise.resolve(JSON.stringify({ john: ['write'] }));
return Promise.resolve();
});
await runTasks(npmClient);
assert.equal(exec.mock.calls[0].arguments[0], 'npm ping --registry registry.example.org');
assert.equal(exec.mock.calls[1].arguments[0], 'npm whoami --registry registry.example.org');
assert.match(
exec.mock.calls[2].arguments[0],
/npm show release-it@[a-z]+ version --registry registry\.example\.org/
);
});
test('should not throw when executing tasks', async t => {
const npmClient = await factory(npm);
t.mock.method(npmClient.shell, 'exec', command => {
if (command === 'npm whoami') return Promise.resolve('john');
const re = /npm access (list collaborators --json|ls-collaborators) release-it/;
if (re.test.command) return Promise.resolve(JSON.stringify({ john: ['write'] }));
return Promise.resolve();
});
await assert.doesNotReject(runTasks(npmClient));
});
test('should throw if npm is down', async t => {
const npmClient = await factory(npm);
t.mock.method(npmClient.shell, 'exec', command => {
if (command === 'npm ping') return Promise.reject();
return Promise.resolve();
});
await assert.rejects(runTasks(npmClient), { message: /^Unable to reach npm registry/ });
});
test('should not throw if npm returns 400/404 for unsupported ping/whoami/access', async t => {
const npmClient = await factory(npm);
const exec = t.mock.method(npmClient.shell, 'exec', () => Promise.resolve());
const pingError = "npm ERR! code E404\nnpm ERR! 404 Package '--ping' not found : ping";
const whoamiError = "npm ERR! code E404\nnpm ERR! 404 Package '--whoami' not found : whoami";
const accessError = 'npm ERR! code E400\nnpm ERR! 400 Bad Request - GET https://npm.example.org/-/collaborators';
exec.mock.mockImplementationOnce(() => Promise.reject(new Error(pingError)), 0);
exec.mock.mockImplementationOnce(() => Promise.reject(new Error(whoamiError)), 1);
exec.mock.mockImplementationOnce(() => Promise.reject(new Error(accessError)), 2);
await runTasks(npmClient);
assert.equal(exec.mock.calls.at(-1).arguments[0].trim(), 'npm publish . --tag latest');
});
test('should not throw if npm returns 400 for unsupported ping/whoami/access', async t => {
const npmClient = await factory(npm);
const exec = t.mock.method(npmClient.shell, 'exec', () => Promise.resolve());
const pingError = 'npm ERR! code E400\nnpm ERR! 400 Bad Request - GET https://npm.example.org/-/ping?write=true';
const whoamiError = 'npm ERR! code E400\nnpm ERR! 400 Bad Request - GET https://npm.example.org/-/whoami';
const accessError = 'npm ERR! code E400\nnpm ERR! 400 Bad Request - GET https://npm.example.org/-/collaborators';
exec.mock.mockImplementationOnce(() => Promise.reject(new Error(pingError)), 0);
exec.mock.mockImplementationOnce(() => Promise.reject(new Error(whoamiError)), 1);
exec.mock.mockImplementationOnce(() => Promise.reject(new Error(accessError)), 2);
await runTasks(npmClient);
assert.equal(exec.mock.calls.at(-1).arguments[0].trim(), 'npm publish . --tag latest');
});
test('should throw if user is not authenticated', async t => {
const npmClient = await factory(npm);
const exec = t.mock.method(npmClient.shell, 'exec', () => Promise.resolve());
exec.mock.mockImplementationOnce(() => Promise.reject(), 1);
await assert.rejects(runTasks(npmClient), { message: /^Not authenticated with npm/ });
});
test('should throw if user is not a collaborator (v9)', async t => {
const npmClient = await factory(npm);
t.mock.method(npmClient.shell, 'exec', command => {
if (command === 'npm whoami') return Promise.resolve('ada');
if (command === 'npm --version') return Promise.resolve('9.2.0');
if (command === 'npm access list collaborators --json release-it')
return Promise.resolve(JSON.stringify({ john: ['write'] }));
return Promise.resolve();
});
await assert.rejects(runTasks(npmClient), { message: /^User ada is not a collaborator for release-it/ });
});
test('should throw if user is not a collaborator (v8)', async t => {
const npmClient = await factory(npm);
t.mock.method(npmClient.shell, 'exec', command => {
if (command === 'npm whoami') return Promise.resolve('ada');
if (command === 'npm --version') return Promise.resolve('8.2.0');
const re = /npm access (list collaborators --json|ls-collaborators) release-it/;
if (re.test(command)) return Promise.resolve(JSON.stringify({ john: ['write'] }));
return Promise.resolve();
});
await assert.rejects(runTasks(npmClient), { message: /^User ada is not a collaborator for release-it/ });
});
test('should not throw if user is not a collaborator on a new package', async t => {
const npmClient = await factory(npm);
t.mock.method(npmClient.shell, 'exec', command => {
if (command === 'npm whoami') return Promise.resolve('ada');
const re = /npm access (list collaborators --json|ls-collaborators) release-it/;
const message =
'npm ERR! code E404\nnpm ERR! 404 Not Found - GET https://registry.npmjs.org/-/package/release-it/collaborators?format=cli - File not found';
if (re.test(command)) return Promise.reject(new Error(message));
return Promise.resolve();
});
await assert.doesNotReject(runTasks(npmClient));
});
test('should handle 2FA and publish with OTP', async t => {
const npmClient = await factory(npm);
npmClient.setContext({ name: 'pkg' });
const exec = t.mock.method(npmClient.shell, 'exec');
exec.mock.mockImplementationOnce(() => Promise.reject(new Error('Initial error with one-time pass.')), 0);
exec.mock.mockImplementationOnce(() => Promise.reject(new Error('The provided one-time pass is incorrect.')), 1);
exec.mock.mockImplementationOnce(() => Promise.resolve(), 2);
await npmClient.publish({
otpCallback: () =>
npmClient.publish({
otp: '123',
otpCallback: () => npmClient.publish({ otp: '123456' })
})
});
assert.equal(exec.mock.callCount(), 3);
assert.equal(exec.mock.calls[0].arguments[0].trim(), 'npm publish . --tag latest');
assert.equal(exec.mock.calls[1].arguments[0].trim(), 'npm publish . --tag latest --otp 123');
assert.equal(exec.mock.calls[2].arguments[0].trim(), 'npm publish . --tag latest --otp 123456');
assert.equal(npmClient.log.warn.mock.callCount(), 1);
assert.equal(npmClient.log.warn.mock.calls[0].arguments[0], 'The provided OTP is incorrect or has expired.');
});
test('should publish', async t => {
const npmClient = await factory(npm);
const exec = t.mock.method(npmClient.shell, 'exec', command => {
if (command === 'npm whoami') return Promise.resolve('john');
const re = /npm access (list collaborators --json|ls-collaborators) release-it/;
if (re.test(command)) return Promise.resolve(JSON.stringify({ john: ['write'] }));
return Promise.resolve();
});
await runTasks(npmClient);
assert.equal(exec.mock.calls.at(-1).arguments[0].trim(), 'npm publish . --tag latest');
});
test('should use extra publish arguments', async t => {
const options = { npm: { skipChecks: true, publishArgs: '--registry=http://my-internal-registry.local' } };
const npmClient = await factory(npm, { options });
const exec = t.mock.method(npmClient.shell, 'exec', () => Promise.resolve());
await runTasks(npmClient);
assert.equal(
exec.mock.calls.at(-1).arguments[0].trim(),
'npm publish . --tag latest --registry=http://my-internal-registry.local'
);
});
test('should skip checks', async () => {
const options = { npm: { skipChecks: true } };
const npmClient = await factory(npm, { options });
await assert.doesNotReject(npmClient.init());
});
test('should publish to a different/scoped registry', async t => {
const tmp = mkTmpDir();
process.chdir(tmp);
writeFileSync(
join(tmp, 'package.json'),
JSON.stringify({
name: '@my-scope/my-pkg',
version: '1.0.0',
publishConfig: {
access: 'public',
'@my-scope:registry': 'https://gitlab.com/api/v4/projects/my-scope%2Fmy-pkg/packages/npm/'
}
})
);
const options = { npm };
const npmClient = await factory(npm, { options });
const exec = t.mock.method(npmClient.shell, 'exec', command => {
const cmd = 'npm whoami --registry https://gitlab.com/api/v4/projects/my-scope%2Fmy-pkg/packages/npm/';
if (command === cmd) return Promise.resolve('john');
const re =
/npm access (list collaborators --json|ls-collaborators) @my-scope\/my-pkg --registry https:\/\/gitlab\.com\/api\/v4\/projects\/my-scope%2Fmy-pkg\/packages\/npm\//;
if (re.test(command)) return Promise.resolve(JSON.stringify({ john: ['write'] }));
return Promise.resolve();
});
await runTasks(npmClient);
assert.deepEqual(getArgs(exec, 'npm'), [
'npm ping --registry https://gitlab.com/api/v4/projects/my-scope%2Fmy-pkg/packages/npm/',
'npm whoami --registry https://gitlab.com/api/v4/projects/my-scope%2Fmy-pkg/packages/npm/',
'npm show @my-scope/my-pkg@latest version --registry https://gitlab.com/api/v4/projects/my-scope%2Fmy-pkg/packages/npm/',
'npm --version',
'npm version 1.0.1 --no-git-tag-version',
'npm publish . --tag latest --registry https://gitlab.com/api/v4/projects/my-scope%2Fmy-pkg/packages/npm/'
]);
});
test('should not publish when `npm version` fails', async t => {
const tmp = mkTmpDir();
process.chdir(tmp);
writeFileSync(join(tmp, 'package.json'), JSON.stringify({ name: '@my-scope/my-pkg', version: '1.0.0' }));
const options = { npm };
const npmClient = await factory(npm, { options });
const exec = t.mock.method(npmClient.shell, 'exec', command => {
if (command === 'npm whoami') return Promise.resolve('john');
const re = /npm access (list collaborators --json|ls-collaborators) @my-scope\/my-pkg/;
if (re.test(command)) return Promise.resolve(JSON.stringify({ john: ['write'] }));
if (command === 'npm version 1.0.1 --no-git-tag-version')
return Promise.reject('npm ERR! Version not changed, might want --allow-same-version');
return Promise.resolve();
});
await assert.rejects(runTasks(npmClient), /Version not changed/);
assert.deepEqual(getArgs(exec, 'npm'), [
'npm ping',
'npm whoami',
'npm show @my-scope/my-pkg@latest version',
'npm --version',
'npm version 1.0.1 --no-git-tag-version'
]);
});
test('should add allow-same-version argument', async t => {
const options = { npm: { skipChecks: true, allowSameVersion: true } };
const npmClient = await factory(npm, { options });
const exec = t.mock.method(npmClient.shell, 'exec', () => Promise.resolve());
await runTasks(npmClient);
const versionArgs = getArgs(exec, 'npm version');
assert.match(versionArgs[0], / --allow-same-version/);
});
test('should add version arguments', async t => {
const options = { npm: { skipChecks: true, versionArgs: ['--workspaces-update=false', '--allow-same-version'] } };
const npmClient = await factory(npm, { options });
const exec = t.mock.method(npmClient.shell, 'exec', () => Promise.resolve());
await runTasks(npmClient);
const versionArgs = getArgs(exec, 'npm version');
assert.match(versionArgs[0], / --workspaces-update=false --allow-same-version/);
});
});

10
node_modules/release-it/test/plugin-name.js generated vendored Normal file
View File

@@ -0,0 +1,10 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { getPluginName } from '../lib/plugin/factory.js';
test('pluginName can return correct name for variants', async () => {
assert.equal(getPluginName('plain-plugin'), 'plain-plugin');
assert.equal(getPluginName('@some/scoped-plugin'), '@some/scoped-plugin');
assert.equal(getPluginName('@some/nested/scoped-plugin'), '@some/nested/scoped-plugin');
assert.equal(getPluginName('./relative-plugin.cjs'), 'relative-plugin');
});

232
node_modules/release-it/test/plugins.js generated vendored Normal file
View File

@@ -0,0 +1,232 @@
import { resolve, join } from 'node:path';
import childProcess from 'node:child_process';
import fs, { appendFileSync, mkdirSync } from 'node:fs';
import test, { afterEach, describe } from 'node:test';
import assert from 'node:assert/strict';
import Config from '../lib/config.js';
import { execOpts, parseGitUrl } from '../lib/util.js';
import runTasks from '../lib/index.js';
import MyPlugin from './stub/plugin.js';
import ReplacePlugin from './stub/plugin-replace.js';
import ContextPlugin from './stub/plugin-context.js';
import { getArgs, mkTmpDir } from './util/helpers.js';
import ShellStub from './stub/shell.js';
import { LogStub, SpinnerStub } from './util/index.js';
describe('plugins', () => {
const testConfig = {
ci: true,
config: false
};
const log = new LogStub();
const spinner = new SpinnerStub();
const getContainer = options => {
const config = new Config(Object.assign({}, testConfig, options));
const shell = new ShellStub({ container: { log, config } });
return { log, spinner, config, shell };
};
childProcess.execSync('npm link', execOpts);
afterEach(() => {
log.resetCalls();
});
test('should instantiate plugins and execute all release-cycle methods', async () => {
const pluginDir = mkTmpDir();
const dir = mkTmpDir();
process.chdir(pluginDir);
appendFileSync(
join(pluginDir, 'package.json'),
JSON.stringify({ name: 'my-plugin', version: '1.0.0', type: 'module' })
);
childProcess.execSync(`npm link release-it`, execOpts);
const content = "import { Plugin } from 'release-it'; " + MyPlugin.toString() + '; export default MyPlugin;';
appendFileSync(join(pluginDir, 'index.js'), content);
process.chdir(dir);
mkdirSync(resolve('my/plugin'), { recursive: true });
process.chdir('my/plugin');
appendFileSync(join(dir, 'my', 'plugin', 'index.js'), content);
process.chdir(dir);
appendFileSync(join(dir, 'package.json'), JSON.stringify({ name: 'project', version: '1.0.0', type: 'module' }));
childProcess.execSync(`npm install ${pluginDir}`, execOpts);
childProcess.execSync(`npm link release-it`, execOpts);
const config = {
plugins: {
'my-plugin': {
name: 'foo'
},
'./my/plugin/index.js': [
'named-plugin',
{
name: 'bar'
}
]
}
};
const container = getContainer(config);
const result = await runTasks({}, container);
assert.deepEqual(
container.log.info.mock.calls.map(call => call.arguments),
[
['my-plugin:foo:init'],
['named-plugin:bar:init'],
['my-plugin:foo:getName'],
['my-plugin:foo:getLatestVersion'],
['my-plugin:foo:getIncrement'],
['my-plugin:foo:getIncrementedVersionCI'],
['named-plugin:bar:getIncrementedVersionCI'],
['my-plugin:foo:beforeBump'],
['named-plugin:bar:beforeBump'],
['my-plugin:foo:bump:1.3.0'],
['named-plugin:bar:bump:1.3.0'],
['my-plugin:foo:beforeRelease'],
['named-plugin:bar:beforeRelease'],
['my-plugin:foo:release'],
['named-plugin:bar:release'],
['my-plugin:foo:afterRelease'],
['named-plugin:bar:afterRelease']
]
);
assert.deepEqual(result, {
changelog: undefined,
name: 'new-project-name',
latestVersion: '1.2.3',
version: '1.3.0'
});
});
test('should instantiate plugins and execute all release-cycle methods for scoped plugins', async () => {
const pluginDir = mkTmpDir();
const dir = mkTmpDir();
process.chdir(pluginDir);
fs.writeFileSync(
join(pluginDir, 'package.json'),
JSON.stringify({ name: '@scoped/my-plugin', version: '1.0.0', type: 'module' })
);
childProcess.execSync(`npm link release-it`, execOpts);
const content = "import { Plugin } from 'release-it'; " + MyPlugin.toString() + '; export default MyPlugin;';
fs.writeFileSync(join(pluginDir, 'index.js'), content);
process.chdir(dir);
fs.writeFileSync(join(dir, 'package.json'), JSON.stringify({ name: 'project', version: '1.0.0', type: 'module' }));
childProcess.execSync(`npm install ${pluginDir}`, execOpts);
childProcess.execSync(`npm link release-it`, execOpts);
const config = {
plugins: {
'@scoped/my-plugin': {
name: 'foo'
}
}
};
const container = getContainer(config);
const result = await runTasks({}, container);
assert.deepEqual(
container.log.info.mock.calls.map(call => call.arguments),
[
['@scoped/my-plugin:foo:init'],
['@scoped/my-plugin:foo:getName'],
['@scoped/my-plugin:foo:getLatestVersion'],
['@scoped/my-plugin:foo:getIncrement'],
['@scoped/my-plugin:foo:getIncrementedVersionCI'],
['@scoped/my-plugin:foo:beforeBump'],
['@scoped/my-plugin:foo:bump:1.3.0'],
['@scoped/my-plugin:foo:beforeRelease'],
['@scoped/my-plugin:foo:release'],
['@scoped/my-plugin:foo:afterRelease']
]
);
assert.deepEqual(result, {
changelog: undefined,
name: 'new-project-name',
latestVersion: '1.2.3',
version: '1.3.0'
});
});
test('should disable core plugins', async () => {
const dir = mkTmpDir();
process.chdir(dir);
fs.appendFileSync(join(dir, 'package.json'), JSON.stringify({ name: 'project', version: '1.0.0' }));
const content =
"import { Plugin } from 'release-it'; " + ReplacePlugin.toString() + '; export default ReplacePlugin;';
fs.appendFileSync(join(dir, 'replace-plugin.mjs'), content);
childProcess.execSync(`npm link release-it`, execOpts);
const config = {
plugins: {
'./replace-plugin.mjs': {}
}
};
const container = getContainer(config);
const result = await runTasks({}, container);
assert.deepEqual(result, {
changelog: undefined,
name: undefined,
latestVersion: '0.0.0',
version: undefined
});
});
test('should expose context to execute commands', async t => {
const dir = mkTmpDir();
process.chdir(dir);
fs.appendFileSync(
join(dir, 'package.json'),
JSON.stringify({ name: 'pkg-name', version: '1.0.0', type: 'module' })
);
const content =
"import { Plugin } from 'release-it'; " + ContextPlugin.toString() + '; export default ContextPlugin;';
fs.appendFileSync(join(dir, 'context-plugin.js'), content);
childProcess.execSync(`npm link release-it`, execOpts);
const repo = parseGitUrl('https://github.com/user/pkg');
const container = getContainer({ plugins: { './context-plugin.js': {} } });
const exec = t.mock.method(container.shell, 'execFormattedCommand');
container.config.setContext({ repo });
container.config.setContext({ tagName: '1.0.1' });
await runTasks({}, container);
const pluginExecArgs = getArgs(exec, 'echo');
assert.deepEqual(pluginExecArgs, [
'echo false',
'echo false',
`echo pkg-name user 1.0.0 1.0.1`,
`echo pkg-name user 1.0.0 1.0.1`,
`echo user pkg user/pkg 1.0.1`,
`echo user pkg user/pkg 1.0.1`,
`echo user pkg user/pkg 1.0.1`,
`echo user pkg user/pkg 1.0.1`,
`echo pkg 1.0.0 1.0.1 1.0.1`,
`echo pkg 1.0.0 1.0.1 1.0.1`,
`echo pkg 1.0.0 1.0.1 1.0.1`,
`echo pkg 1.0.0 1.0.1 1.0.1`
]);
});
});

92
node_modules/release-it/test/prompt.js generated vendored Normal file
View File

@@ -0,0 +1,92 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import Prompt from '../lib/prompt.js';
import Config from '../lib/config.js';
import git from '../lib/plugin/git/prompts.js';
import github from '../lib/plugin/github/prompts.js';
import gitlab from '../lib/plugin/gitlab/prompts.js';
import npm from '../lib/plugin/npm/prompts.js';
import { factory } from './util/index.js';
const prompts = { git, github, gitlab, npm };
const yes = ([options]) => Promise.resolve({ [options.name]: true });
const no = ([options]) => Promise.resolve({ [options.name]: false });
test('should not create prompt if disabled', async t => {
const task = t.mock.fn();
const promptMock = t.mock.fn(yes);
const inquirer = { prompt: promptMock };
const prompt = await factory(Prompt, { container: { inquirer } });
prompt.register(prompts.git);
await prompt.show({ enabled: false, prompt: 'push', task });
assert.equal(promptMock.mock.callCount(), 0);
assert.equal(task.mock.callCount(), 0);
});
test('should create prompt', async t => {
const promptMock = t.mock.fn(yes);
const inquirer = { prompt: promptMock };
const prompt = await factory(Prompt, { container: { inquirer } });
prompt.register(prompts.git);
await prompt.show({ prompt: 'push' });
assert.equal(promptMock.mock.callCount(), 1);
assert.deepEqual(promptMock.mock.calls[0].arguments[0][0], {
type: 'confirm',
message: 'Push?',
name: 'push',
choices: false,
transformer: undefined,
default: true
});
});
[
['git', 'commit', 'Commit (Release 1.0.0)?'],
['git', 'tag', 'Tag (1.0.0)?'],
['git', 'push', 'Push?'],
['github', 'release', 'Create a pre-release on GitHub (Release 1.0.0)?'],
['gitlab', 'release', 'Create a release on GitLab (Release 1.0.0)?'],
['npm', 'publish', 'Publish my-pkg@next to npm?'],
['npm', 'otp', 'Please enter OTP for npm:']
].map(async ([namespace, prompt, message]) => {
test(`should create prompt and render template message (${namespace}.${prompt})`, async t => {
const promptMock = t.mock.fn(yes);
const config = new Config({
isPreRelease: true,
git: { tagName: 'v${version}' },
npm: { name: 'my-pkg', tag: 'next' }
});
await config.init();
config.setContext({ version: '1.0.0', tagName: '1.0.0' });
const inquirer = { prompt: promptMock };
const p = await factory(Prompt, { container: { inquirer } });
p.register(prompts[namespace], namespace);
await p.show({ namespace, prompt, context: config.getContext() });
assert.equal(promptMock.mock.callCount(), 1);
assert.equal(promptMock.mock.calls[0].arguments[0][0].message, message);
});
});
test('should execute task after positive answer', async t => {
const task = t.mock.fn();
const promptMock = t.mock.fn(yes);
const inquirer = { prompt: promptMock };
const prompt = await factory(Prompt, { container: { inquirer } });
prompt.register(prompts.git);
await prompt.show({ prompt: 'push', task });
assert.equal(promptMock.mock.callCount(), 1);
assert.equal(task.mock.callCount(), 1);
assert.equal(task.mock.calls[0].arguments[0], true);
});
test('should not execute task after negative answer', async t => {
const task = t.mock.fn();
const promptMock = t.mock.fn(no);
const inquirer = { prompt: promptMock };
const prompt = await factory(Prompt, { container: { inquirer } });
prompt.register(prompts.git);
await prompt.show({ prompt: 'push', task });
assert.equal(promptMock.mock.callCount(), 1);
assert.equal(task.mock.callCount(), 0);
});

View File

@@ -0,0 +1 @@
*

View File

@@ -0,0 +1 @@
*

1
node_modules/release-it/test/resources/file1 generated vendored Normal file
View File

@@ -0,0 +1 @@
file1

76
node_modules/release-it/test/shell.js generated vendored Normal file
View File

@@ -0,0 +1,76 @@
import childProcess from 'node:child_process';
import test, { describe } from 'node:test';
import assert from 'node:assert/strict';
import Shell from '../lib/shell.js';
import { factory } from './util/index.js';
describe('shell', async () => {
const cwd = childProcess.execSync('pwd', { encoding: 'utf8' }).trim();
const shell = await factory(Shell);
test('exec', async () => {
assert.equal(await shell.exec('echo bar'), 'bar');
});
test('exec (with context)', async () => {
const exec = cmd => shell.exec(cmd, { verbose: false }, shell.config.getContext());
assert.equal(await exec(''), undefined);
assert.equal(await exec('pwd'), cwd);
assert.equal(await exec('echo ${git.pushArgs}'), '--follow-tags');
assert.equal(await exec('echo -*- ${github.tokenRef} -*-'), '-*- GITHUB_TOKEN -*-');
});
test('exec (with args)', async () => {
assert.equal(await shell.exec([]), undefined);
assert.equal(await shell.exec(['pwd']), cwd);
assert.equal(await shell.exec(['echo', 'a', 'b']), 'a b');
assert.equal(await shell.exec(['echo', '"a"']), '"a"');
});
test('exec (dry-run/read-only)', async () => {
const shell = await factory(Shell, { options: { 'dry-run': true } });
{
const actual = await shell.exec('pwd', { write: false });
assert.equal(actual, cwd);
assert.equal(shell.log.exec.mock.callCount(), 1);
assert.equal(shell.log.exec.mock.calls[0].arguments[0], 'pwd');
}
{
const actual = await shell.exec('pwd');
assert.equal(actual, undefined);
assert.equal(shell.log.exec.mock.callCount(), 2);
assert.equal(shell.log.exec.mock.calls[1].arguments[0], 'pwd');
assert.deepEqual(shell.log.exec.mock.calls[1].arguments.at(-1), { isDryRun: true });
}
});
test('exec (verbose)', async () => {
const shell = await factory(Shell, { options: { verbose: true } });
const actual = await shell.exec('echo foo');
assert.equal(shell.log.exec.mock.calls[0].arguments[0], 'echo foo');
assert.equal(shell.log.exec.mock.callCount(), 1);
assert.equal(shell.log.verbose.mock.calls[0].arguments[0], 'foo');
assert.equal(shell.log.verbose.mock.callCount(), 1);
assert.equal(actual, 'foo');
});
test('should cache results of command execution', async () => {
const shell = await factory(Shell);
const result1 = await shell.exec('echo foo');
const result2 = await shell.exec('echo foo');
assert(result1 === result2);
assert.deepEqual(
shell.log.exec.mock.calls.map(call => call.arguments),
[
['echo foo', { isExternal: false, isCached: false }],
['echo foo', { isExternal: false, isCached: true }]
]
);
});
test('should bail out on failed command execution', async () => {
const shell = new Shell({ container: {} });
await assert.rejects(shell.exec('foo'));
});
});

57
node_modules/release-it/test/spinner.js generated vendored Normal file
View File

@@ -0,0 +1,57 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import Spinner from '../lib/spinner.js';
import Config from '../lib/config.js';
const getConfig = async options => {
const testConfig = {
ci: false,
config: false
};
const config = new Config(Object.assign({}, testConfig, options));
await config.init();
return config;
};
test('should not show spinner and not execute task if disabled', async t => {
const ora = t.mock.fn();
const task = t.mock.fn();
const spinner = new Spinner({ container: { ora } });
await spinner.show({ enabled: false, task });
assert.equal(task.mock.callCount(), 0);
assert.equal(ora.mock.callCount(), 0);
});
test('should show spinner and run task by default', async t => {
const ora = t.mock.fn();
const task = t.mock.fn(() => Promise.resolve());
const label = 'foo';
const config = await getConfig({ ci: true });
const spinner = new Spinner({ container: { ora, config } });
await spinner.show({ task, label });
assert.equal(task.mock.callCount(), 1);
assert.equal(ora.mock.callCount(), 1);
assert.equal(ora.mock.calls[0].arguments[0], task.mock.calls[0].result);
assert.equal(ora.mock.calls[0].arguments[1], label);
});
test('should run task, but not show spinner if interactive', async t => {
const ora = t.mock.fn();
const task = t.mock.fn(() => Promise.resolve());
const config = await getConfig({ ci: false });
const spinner = new Spinner({ container: { ora, config } });
await spinner.show({ task });
assert.equal(task.mock.callCount(), 1);
assert.equal(ora.mock.callCount(), 0);
});
test('should run task and show spinner if interactive, but external', async t => {
const ora = t.mock.fn();
const task = t.mock.fn(() => Promise.resolve());
const config = await getConfig({ ci: false });
const spinner = new Spinner({ container: { ora, config } });
await spinner.show({ task, external: true });
assert.equal(task.mock.callCount(), 1);
assert.equal(ora.mock.callCount(), 1);
});

View File

@@ -0,0 +1,5 @@
{
"github": {
"release": true
}
}

View File

@@ -0,0 +1 @@
foo=bar

View File

@@ -0,0 +1,2 @@
foo
bar\baz

View File

@@ -0,0 +1,5 @@
{
"github": {
"release": true
}
}

View File

@@ -0,0 +1,7 @@
{
"release-it": {
"git": {
"push": false
}
}
}

View File

@@ -0,0 +1,5 @@
{
"git": {
"commitMessage": "Released version ${version}"
}
}

View File

@@ -0,0 +1,5 @@
{
"git": {
"commitMessage": "Released with version ${version}"
}
}

View File

@@ -0,0 +1,2 @@
[foo]
bar=1

View File

@@ -0,0 +1,2 @@
foo:
bar: 1

View File

@@ -0,0 +1,2 @@
foo:
bar: 1

150
node_modules/release-it/test/stub/github.js generated vendored Normal file
View File

@@ -0,0 +1,150 @@
export const interceptAuthentication = (server, { username = 'john' } = {}) => {
server.get('/user', { status: 200, body: { login: username } });
};
export const interceptCollaborator = (server, { owner = 'user', project = 'repo', username = 'john' } = {}) => {
server.get(`/repos/${owner}/${project}/collaborators/${username}`, { status: 204 });
};
export const interceptListReleases = (
server,
{ host = 'github.com', owner = 'user', project = 'repo', tag_name } = {}
) => {
server.get(`/repos/${owner}/${project}/releases?per_page=1&page=1`, {
status: 200,
body: [
{
id: 1,
upload_url: `https://uploads.${host}/repos/${owner}/${project}/releases/1/assets{?name,label}`,
html_url: `https://${host}/${owner}/${project}/releases/tag/${tag_name}`,
tag_name,
target_commitish: 'main',
name: `Release ${tag_name}`,
body: 'Description of the release',
draft: false,
prerelease: false
}
]
});
};
export const interceptCreate = (
server,
{
api = 'https://api.github.com',
host = 'github.com',
owner = 'user',
project = 'repo',
body: {
tag_name,
name = '',
body = null,
prerelease = false,
draft = false,
generate_release_notes = false,
make_latest = 'true',
discussion_category_name = undefined
}
} = {}
) => {
const id = 1;
server.post(
{
url: `/repos/${owner}/${project}/releases`,
body: { tag_name, name, body, prerelease, draft, generate_release_notes, make_latest, discussion_category_name }
},
{
status: 200,
body: {
id,
tag_name,
name,
body,
prerelease,
draft,
generate_release_notes,
upload_url: `https://uploads.${host}/repos/${owner}/${project}/releases/${id}/assets{?name,label}`,
html_url: `https://${host}/${owner}/${project}/releases/tag/${tag_name}`,
discussion_url: discussion_category_name ? `https://${host}/${owner}/${project}/discussions/${id}` : undefined
},
headers: { location: `${api}/repos/${owner}/${project}/releases/${id}` }
}
);
};
export const interceptUpdate = (
server,
{
host = 'github.com',
owner = 'user',
project = 'repo',
body: {
tag_name,
name = '',
body = null,
prerelease = false,
draft = false,
generate_release_notes = false,
make_latest = 'true',
discussion_category_name = undefined
}
} = {}
) => {
server.patch(
{
url: `/repos/${owner}/${project}/releases/1`,
body: {
tag_name,
name,
body,
draft,
prerelease,
generate_release_notes,
make_latest,
discussion_category_name
}
},
{
status: 200,
body: {
id: 1,
tag_name,
name,
body,
prerelease,
draft,
generate_release_notes,
upload_url: `https://uploads.${host}/repos/${owner}/${project}/releases/1/assets{?name,label}`,
html_url: `https://${host}/${owner}/${project}/releases/tag/${tag_name}`
}
}
);
};
export const interceptAsset = (
server,
{ api = 'https://api.github.com', host = 'github.com', owner = 'user', project = 'repo', tagName } = {}
) => {
server.post(
{
url: `/repos/${owner}/${project}/releases/1/assets`
},
request => {
const id = 1;
const url = new URL(request.url);
const name = url.searchParams.get('name');
return {
status: 200,
body: {
id,
url: `${api}/repos/${owner}/${project}/releases/assets/${id}`,
name,
label: '',
state: 'uploaded',
size: request.headers['content-length'],
browser_download_url: `https://${host}/${owner}/${project}/releases/download/${tagName}/${name}`
}
};
}
);
};

87
node_modules/release-it/test/stub/gitlab.js generated vendored Normal file
View File

@@ -0,0 +1,87 @@
export const interceptMembers = (server, { owner = 'emma' } = {}) => {
server.get(`/projects/john%2Frepo/members/all/1`, { status: 200, username: owner });
};
export const interceptUser = (server, { owner = 'user' } = {}, options = {}) => {
server.get({ url: '/user', ...options }, { status: 200, body: { id: 1, username: owner } });
};
export const interceptCollaborator = (
server,
{ owner = 'user', project = 'repo', group, userId = 1 } = {},
options = {}
) =>
server.get(
{
url: `/projects/${group ? `${group}%2F` : ''}${owner}%2F${project}/members/all/${userId}`,
...options
},
{
status: 200,
body: { id: userId, username: owner, access_level: 30 }
}
);
export const interceptPublish = (server, { owner = 'user', project = 'repo', body } = {}) =>
server.post(
{ url: `/projects/${owner}%2F${project}/releases`, body },
{
status: 200,
body: {
_links: {
self: `https://gitlab.com/${owner}/${project}/-/releases/${body?.tag_name ?? '1.0.0'}`
}
}
}
);
export const interceptMilestones = (server, { owner = 'user', project = 'repo', query = {}, milestones = [] } = {}) =>
server.get(
{
url: `/projects/${owner}%2F${project}/milestones`,
query: {
include_parent_milestones: 'true',
...query
}
},
{
status: 200,
body: milestones
}
);
export const interceptAsset = (server, { owner = 'user', project = 'repo' } = {}) =>
server.post(
{
url: `/projects/${owner}%2F${project}/uploads`
},
async request => {
const formData = await request.formData();
const { name } = formData.get('file');
return {
status: 200,
body: {
alt: name,
url: `/uploads/7e8bec1fe27cc46a4bc6a91b9e82a07c/${name}`,
full_path: `/-/project/1234/uploads/7e8bec1fe27cc46a4bc6a91b9e82a07c/${name}`,
markdown: `[${name}](/uploads/7e8bec1fe27cc46a4bc6a91b9e82a07c/${name})`,
_links: {
self: `https://gitlab.com/${owner}/${project}/-/releases/${name}`
}
}
};
}
);
export const interceptAssetGeneric = (server, { owner = 'user', project = 'repo' } = {}) =>
server.put(
{
url: `/projects/${owner}%2F${project}/packages/generic/release-it/2.0.1/file-v2.0.1.txt`
},
{
status: 200,
body: {
message: '201 Created'
}
}
);

36
node_modules/release-it/test/stub/plugin-context.js generated vendored Normal file
View File

@@ -0,0 +1,36 @@
import Plugin from '../../lib/plugin/Plugin.js';
class ContextPlugin extends Plugin {
init() {
const context = this.config.getContext();
this.exec(`echo ${context.version.isPreRelease}`);
this.exec('echo ${version.isPreRelease}');
}
beforeBump() {
const context = this.config.getContext();
this.exec(`echo ${context.name} ${context.repo.owner} ${context.latestVersion} ${context.version}`);
this.exec('echo ${name} ${repo.owner} ${latestVersion} ${version}');
}
bump(version) {
const repo = this.config.getContext('repo');
this.exec(`echo ${repo.owner} ${repo.project} ${repo.repository} ${version}`);
this.exec('echo ${repo.owner} ${repo.project} ${repo.repository} ${version}');
}
beforeRelease() {
const { repo, tagName } = this.config.getContext();
this.exec(`echo ${repo.owner} ${repo.project} ${repo.repository} ${tagName}`);
this.exec('echo ${repo.owner} ${repo.project} ${repo.repository} ${tagName}');
}
release() {
const { repo, latestVersion, version, tagName } = this.config.getContext();
this.exec(`echo ${repo.project} ${latestVersion} ${version} ${tagName}`);
this.exec('echo ${repo.project} ${latestVersion} ${version} ${tagName}');
}
afterRelease() {
const { repo, latestVersion, version, tagName } = this.config.getContext();
this.exec(`echo ${repo.project} ${latestVersion} ${version} ${tagName}`);
this.exec('echo ${repo.project} ${latestVersion} ${version} ${tagName}');
}
}
export default ContextPlugin;

9
node_modules/release-it/test/stub/plugin-replace.js generated vendored Normal file
View File

@@ -0,0 +1,9 @@
import Plugin from '../../lib/plugin/Plugin.js';
class ReplacePlugin extends Plugin {
static disablePlugin() {
return ['version', 'git', 'npm'];
}
}
export default ReplacePlugin;

39
node_modules/release-it/test/stub/plugin.js generated vendored Normal file
View File

@@ -0,0 +1,39 @@
import Plugin from '../../lib/plugin/Plugin.js';
class MyPlugin extends Plugin {
init() {
this.log.info(`${this.namespace}:${this.getContext('name')}:init`);
}
getName() {
this.log.info(`${this.namespace}:${this.getContext('name')}:getName`);
return 'new-project-name';
}
getLatestVersion() {
this.log.info(`${this.namespace}:${this.getContext('name')}:getLatestVersion`);
return '1.2.3';
}
getIncrement() {
this.log.info(`${this.namespace}:${this.getContext('name')}:getIncrement`);
return 'minor';
}
getIncrementedVersionCI() {
this.log.info(`${this.namespace}:${this.getContext('name')}:getIncrementedVersionCI`);
}
beforeBump() {
this.log.info(`${this.namespace}:${this.getContext('name')}:beforeBump`);
}
bump(version) {
this.log.info(`${this.namespace}:${this.getContext('name')}:bump:${version}`);
}
beforeRelease() {
this.log.info(`${this.namespace}:${this.getContext('name')}:beforeRelease`);
}
release() {
this.log.info(`${this.namespace}:${this.getContext('name')}:release`);
}
afterRelease() {
this.log.info(`${this.namespace}:${this.getContext('name')}:afterRelease`);
}
}
export default MyPlugin;

24
node_modules/release-it/test/stub/shell.js generated vendored Normal file
View File

@@ -0,0 +1,24 @@
import util from 'node:util';
import Shell from '../../lib/shell.js';
const debug = util.debug('release-it:shell-stub');
class ShellStub extends Shell {
exec(command) {
if (/^(npm (ping|publish|show)|git fetch)/.test(command)) {
debug(command);
return Promise.resolve();
}
if (/^npm whoami/.test(command)) {
debug(command);
return Promise.resolve('john');
}
if (/^npm access/.test(command)) {
debug(command);
return Promise.resolve(JSON.stringify({ john: ['write'] }));
}
return super.exec(...arguments);
}
}
export default ShellStub;

274
node_modules/release-it/test/tasks.interactive.js generated vendored Normal file
View File

@@ -0,0 +1,274 @@
import path from 'node:path';
import { renameSync } from 'node:fs';
import childProcess from 'node:child_process';
import test, { afterEach, after, before, beforeEach, describe, mock } from 'node:test';
import assert from 'node:assert/strict';
import Prompt from '../lib/prompt.js';
import Config from '../lib/config.js';
import runTasks from '../lib/index.js';
import Git from '../lib/plugin/git/Git.js';
import { execOpts } from '../lib/util.js';
import { mkTmpDir, gitAdd, getArgs } from './util/helpers.js';
import ShellStub from './stub/shell.js';
import { interceptPublish as interceptGitLabPublish } from './stub/gitlab.js';
import { interceptCreate as interceptGitHubCreate } from './stub/github.js';
import { factory, LogStub, SpinnerStub } from './util/index.js';
import { mockFetch } from './util/mock.js';
import { createTarBlobByRawContents } from './util/fetch.js';
describe('tasks.interactive', () => {
const [mocker, github, gitlab] = mockFetch(['https://api.github.com', 'https://gitlab.com/api/v4']);
before(() => {
mocker.mockGlobal();
});
afterEach(() => {
mocker.clearAll();
prompt.mock.resetCalls();
log.resetCalls();
});
after(() => {
mocker.unmockGlobal();
});
const testConfig = {
ci: false,
config: false
};
const getHooks = plugins => {
const hooks = {};
['before', 'after'].forEach(prefix => {
plugins.forEach(ns => {
['init', 'beforeBump', 'bump', 'beforeRelease', 'release', 'afterRelease'].forEach(lifecycle => {
hooks[`${prefix}:${lifecycle}`] = `echo ${prefix}:${lifecycle}`;
hooks[`${prefix}:${ns}:${lifecycle}`] = `echo ${prefix}:${ns}:${lifecycle}`;
});
});
});
return hooks;
};
const log = new LogStub();
const spinner = new SpinnerStub();
const prompt = mock.fn(([options]) => {
const answer = options.type === 'list' ? options.choices[0].value : options.name === 'version' ? '0.0.1' : true;
return { [options.name]: answer };
});
const defaultInquirer = { prompt };
const getContainer = (options, inquirer = defaultInquirer) => {
const config = new Config(Object.assign({}, testConfig, options));
const shell = new ShellStub({ container: { log, config } });
const prompt = new Prompt({ container: { inquirer } });
return { log, spinner, config, shell, prompt };
};
let bare;
let target;
beforeEach(async () => {
bare = mkTmpDir();
target = mkTmpDir();
process.chdir(bare);
childProcess.execSync(`git init --bare .`, execOpts);
childProcess.execSync(`git clone ${bare} ${target}`, execOpts);
process.chdir(target);
gitAdd('line', 'file', 'Add file');
});
test('should run tasks without throwing errors', async () => {
renameSync('.git', 'foo');
const { name, latestVersion, version } = await runTasks({}, getContainer());
assert.equal(version, '0.0.1');
assert(log.obtrusive.mock.calls[0].arguments[0].includes(`release ${name} (currently at ${latestVersion})`));
assert.match(log.log.mock.calls.at(-1).arguments[0], /Done \(in [0-9]+s\.\)/);
});
test('should run tasks using extended configuration', async t => {
renameSync('.git', 'foo');
const validationExtendedConfiguration = "echo 'extended_configuration'";
github.head('/repos/release-it/release-it-configuration/tarball/main', {
status: 200,
headers: {}
});
github.get('/repos/release-it/release-it-configuration/tarball/main', {
status: 200,
body: await new Response(
createTarBlobByRawContents({
'.release-it.json': JSON.stringify({
hooks: {
'before:init': validationExtendedConfiguration
}
})
})
).arrayBuffer()
});
const config = {
$schema: 'https://unpkg.com/release-it@19/schema/release-it.json',
extends: 'github:release-it/release-it-configuration',
config: true
};
const container = getContainer(config);
const exec = t.mock.method(container.shell, 'execFormattedCommand');
const { name, latestVersion, version } = await runTasks({}, container);
const commands = getArgs(exec, 'echo');
assert(commands.includes(validationExtendedConfiguration));
assert.equal(version, '0.0.1');
assert(log.obtrusive.mock.calls[0].arguments[0].includes(`release ${name} (currently at ${latestVersion})`));
assert.match(log.log.mock.calls.at(-1).arguments[0], /Done \(in [0-9]+s\.\)/);
});
test('should run tasks not using extended configuration as it is not a string', async () => {
renameSync('.git', 'foo');
const config = {
$schema: 'https://unpkg.com/release-it@19/schema/release-it.json',
extends: false
};
const container = getContainer(config);
const { name, latestVersion, version } = await runTasks({}, container);
assert.equal(version, '0.0.1');
assert(log.obtrusive.mock.calls[0].arguments[0].includes(`release ${name} (currently at ${latestVersion})`));
assert.match(log.log.mock.calls.at(-1).arguments[0], /Done \(in [0-9]+s\.\)/);
});
test('should not run hooks for disabled release-cycle methods', async t => {
const hooks = getHooks(['version', 'git', 'github', 'gitlab', 'npm']);
const container = getContainer({
hooks,
git: { push: false },
github: { release: false },
gitlab: { release: false },
npm: { publish: false }
});
const exec = t.mock.method(container.shell, 'execFormattedCommand');
await runTasks({}, container);
const commands = getArgs(exec, 'echo');
assert(commands.includes('echo before:init'));
assert(commands.includes('echo after:afterRelease'));
assert(!commands.includes('echo after:git:release'));
assert(!commands.includes('echo after:github:release'));
assert(!commands.includes('echo after:gitlab:release'));
assert(!commands.includes('echo after:npm:release'));
});
test('should not run hooks for cancelled release-cycle methods', async t => {
const pkgName = path.basename(target);
gitAdd(`{"name":"${pkgName}","version":"1.0.0"}`, 'package.json', 'Add package.json');
childProcess.execSync('git tag 1.0.0', execOpts);
const hooks = getHooks(['version', 'git', 'github', 'gitlab', 'npm']);
const prompt = mock.fn(([options]) => ({ [options.name]: false }));
const inquirer = { prompt };
const container = getContainer(
{
increment: 'minor',
hooks,
github: { release: true, skipChecks: true },
gitlab: { release: true, skipChecks: true },
npm: { publish: true, skipChecks: true }
},
inquirer
);
const exec = t.mock.method(container.shell, 'execFormattedCommand');
await runTasks({}, container);
const commands = getArgs(exec, 'echo');
assert(commands.includes('echo before:init'));
assert(commands.includes('echo after:afterRelease'));
assert(commands.includes('echo after:git:bump'));
assert(commands.includes('echo after:npm:bump'));
assert(!commands.includes('echo after:git:release'));
assert(!commands.includes('echo after:github:release'));
assert(!commands.includes('echo after:gitlab:release'));
assert(!commands.includes('echo after:npm:release'));
});
test('should run "after:*:release" plugin hooks', async t => {
const project = path.basename(bare);
const pkgName = path.basename(target);
const owner = path.basename(path.dirname(bare));
gitAdd(`{"name":"${pkgName}","version":"1.0.0"}`, 'package.json', 'Add package.json');
childProcess.execSync('git tag 1.0.0', execOpts);
const sha = gitAdd('line', 'file', 'More file');
const git = await factory(Git);
const ref = (await git.getBranchName()) ?? 'HEAD';
interceptGitHubCreate(github, {
owner,
project,
body: { tag_name: '1.1.0', name: 'Release 1.1.0', body: `* More file (${sha})` }
});
interceptGitLabPublish(gitlab, {
owner,
project,
body: {
name: 'Release 1.1.0',
ref,
tag_name: '1.1.0',
tag_message: 'Release 1.1.0',
description: `* More file (${sha})`
}
});
const hooks = getHooks(['version', 'git', 'github', 'gitlab', 'npm']);
const container = getContainer({
increment: 'minor',
hooks,
github: { release: true, pushRepo: `https://github.com/${owner}/${project}`, skipChecks: true },
gitlab: { release: true, pushRepo: `https://gitlab.com/${owner}/${project}`, skipChecks: true },
npm: { name: pkgName, skipChecks: true }
});
const exec = t.mock.method(container.shell, 'execFormattedCommand');
await runTasks({}, container);
const commands = getArgs(exec, 'echo');
assert(commands.includes('echo after:git:bump'));
assert(commands.includes('echo after:npm:bump'));
assert(commands.includes('echo after:git:release'));
assert(commands.includes('echo after:github:release'));
assert(commands.includes('echo after:gitlab:release'));
assert(commands.includes('echo after:npm:release'));
});
test('should show only version prompt', async () => {
const config = { ci: false, 'only-version': true };
await runTasks({}, getContainer(config));
assert.equal(prompt.mock.callCount(), 1);
assert.equal(prompt.mock.calls[0].arguments[0][0].name, 'incrementList');
});
});

578
node_modules/release-it/test/tasks.js generated vendored Normal file
View File

@@ -0,0 +1,578 @@
import path from 'node:path';
import childProcess from 'node:child_process';
import { appendFileSync, mkdirSync, renameSync } from 'node:fs';
import test, { after, afterEach, before, beforeEach, describe } from 'node:test';
import assert from 'node:assert/strict';
import semver from 'semver';
import Config from '../lib/config.js';
import runTasks from '../lib/index.js';
import Git from '../lib/plugin/git/Git.js';
import { execOpts } from '../lib/util.js';
import { mkTmpDir, gitAdd, getArgs } from './util/helpers.js';
import ShellStub from './stub/shell.js';
import {
interceptUser as interceptGitLabUser,
interceptCollaborator as interceptGitLabCollaborator,
interceptPublish as interceptGitLabPublish,
interceptAsset as interceptGitLabAsset
} from './stub/gitlab.js';
import {
interceptAuthentication as interceptGitHubAuthentication,
interceptCollaborator as interceptGitHubCollaborator,
interceptCreate as interceptGitHubCreate,
interceptAsset as interceptGitHubAsset
} from './stub/github.js';
import { factory, LogStub, SpinnerStub } from './util/index.js';
import { mockFetch } from './util/mock.js';
describe('tasks', () => {
const rootDir = new URL('..', import.meta.url);
const [mocker, github, assets, gitlab] = mockFetch([
'https://api.github.com',
'https://uploads.github.com',
'https://gitlab.com/api/v4'
]);
const npmMajorVersion = semver.major(process.env.npm_config_user_agent.match(/npm\/([^ ]+)/)[1]);
const testConfig = {
ci: true,
config: false
};
const log = new LogStub();
const spinner = new SpinnerStub();
const getContainer = options => {
const config = new Config(Object.assign({}, testConfig, options));
const shell = new ShellStub({ container: { log, config } });
return { log, spinner, config, shell };
};
before(() => {
mocker.mockGlobal();
});
let bare;
let target;
beforeEach(async () => {
bare = mkTmpDir();
target = mkTmpDir();
process.chdir(bare);
childProcess.execSync(`git init --bare .`, execOpts);
childProcess.execSync(`git clone ${bare} ${target}`, execOpts);
process.chdir(target);
gitAdd('line', 'file', 'Add file');
});
afterEach(() => {
mocker.clearAll();
log.resetCalls();
});
after(() => {
mocker.unmockGlobal();
});
test('should run tasks without throwing errors', async () => {
renameSync('.git', 'foo');
const { name, latestVersion, version } = await runTasks({}, getContainer());
assert(log.obtrusive.mock.calls[0].arguments[0].includes(`release ${name} (${latestVersion}...${version})`));
assert.match(log.log.mock.calls.at(-1).arguments[0], /Done \(in [0-9]+s\.\)/);
});
test('should run tasks without package.json', async () => {
childProcess.execSync('git tag 1.0.0', execOpts);
gitAdd('line', 'file', 'Add file');
const { name } = await runTasks({}, getContainer({ increment: 'major', git: { commit: false } }));
assert(log.obtrusive.mock.calls[0].arguments[0].includes(`release ${name} (1.0.0...2.0.0)`));
assert.match(log.log.mock.calls.at(-1).arguments[0], /Done \(in [0-9]+s\.\)/);
assert.equal(log.warn.mock.callCount(), 0);
const stdout = childProcess.execSync('git describe --tags --match=* --abbrev=0', {
encoding: 'utf-8'
});
assert.equal(stdout.trim(), '2.0.0');
});
test('should disable plugins', async () => {
gitAdd('{"name":"my-package","version":"1.2.3"}', 'package.json', 'Add package.json');
childProcess.execSync('git tag 1.2.3', execOpts);
gitAdd('line', 'file', 'Add file');
const container = getContainer({ increment: 'minor', git: false, npm: false });
const { latestVersion, version } = await runTasks({}, container);
assert.equal(latestVersion, '0.0.0');
assert.equal(version, '0.1.0');
assert.match(log.log.mock.calls.at(-1).arguments[0], /Done \(in [0-9]+s\.\)/);
});
test('should run tasks with minimal config and without any warnings/errors', async () => {
gitAdd('{"name":"my-package","version":"1.2.3"}', 'package.json', 'Add package.json');
childProcess.execSync('git tag 1.2.3', execOpts);
gitAdd('line', 'file', 'More file');
await runTasks({}, getContainer({ increment: 'patch' }));
assert(log.obtrusive.mock.calls[0].arguments[0].includes('release my-package (1.2.3...1.2.4)'));
assert.match(log.log.mock.calls.at(-1).arguments[0], /Done \(in [0-9]+s\.\)/);
const stdout = childProcess.execSync('git describe --tags --match=* --abbrev=0', { encoding: 'utf-8' });
assert.equal(stdout.trim(), '1.2.4');
});
test('should use pkg.version', async () => {
gitAdd('{"name":"my-package","version":"1.2.3"}', 'package.json', 'Add package.json');
await runTasks({}, getContainer({ increment: 'minor' }));
assert(log.obtrusive.mock.calls[0].arguments[0].includes('release my-package (1.2.3...1.3.0)'));
assert.match(log.log.mock.calls.at(-1).arguments[0], /Done \(in [0-9]+s\.\)/);
const stdout = childProcess.execSync('git describe --tags --match=* --abbrev=0', { encoding: 'utf-8' });
assert.equal(stdout.trim(), '1.3.0');
});
test('should use pkg.version (in sub dir) w/o tagging repo', async t => {
gitAdd('{"name":"root-package","version":"1.0.0"}', 'package.json', 'Add package.json');
childProcess.execSync('git tag 1.0.0', execOpts);
mkdirSync('my-package');
process.chdir('my-package');
gitAdd('{"name":"my-package","version":"1.2.3"}', 'package.json', 'Add package.json');
const container = getContainer({ increment: 'minor', git: { tag: false } });
const exec = t.mock.method(container.shell, 'exec');
await runTasks({}, container);
assert(log.obtrusive.mock.calls[0].arguments[0].endsWith('release my-package (1.2.3...1.3.0)'));
assert.match(log.log.mock.calls.at(-1).arguments[0], /Done \(in [0-9]+s\.\)/);
const stdout = childProcess.execSync('git describe --tags --match=* --abbrev=0', { encoding: 'utf-8' });
assert.equal(stdout.trim(), '1.0.0');
const npmArgs = getArgs(exec, 'npm');
assert.equal(npmArgs[5], 'npm version 1.3.0 --no-git-tag-version');
});
test('should ignore version in pkg.version and use git tag instead', async () => {
gitAdd('{"name":"my-package","version":"0.0.0"}', 'package.json', 'Add package.json');
childProcess.execSync('git tag 1.1.1', execOpts);
gitAdd('line', 'file', 'More file');
await runTasks({}, getContainer({ increment: 'minor', npm: { ignoreVersion: true } }));
assert(log.obtrusive.mock.calls[0].arguments[0].includes('release my-package (1.1.1...1.2.0)'));
assert.match(log.log.mock.calls.at(-1).arguments[0], /Done \(in [0-9]+s\.\)/);
const stdout = childProcess.execSync('git describe --tags --match=* --abbrev=0', { encoding: 'utf-8' });
assert.equal(stdout.trim(), '1.2.0');
});
test('should release all the things (basic)', async t => {
const project = path.basename(bare);
const pkgName = path.basename(target);
const owner = path.basename(path.dirname(bare));
gitAdd(`{"name":"${pkgName}","version":"1.0.0"}`, 'package.json', 'Add package.json');
childProcess.execSync('git tag 1.0.0', execOpts);
const sha = gitAdd('line', 'file', 'More file');
interceptGitHubAuthentication(github);
interceptGitHubCollaborator(github, { owner, project });
interceptGitHubCreate(github, {
owner,
project,
body: { tag_name: '1.0.1', name: 'Release 1.0.1', body: `* More file (${sha})`, prerelease: false }
});
const container = getContainer({
github: { release: true, pushRepo: `https://github.com/${owner}/${project}` },
npm: { name: pkgName }
});
const exec = t.mock.method(container.shell, 'exec');
await runTasks({}, container);
const npmArgs = getArgs(exec, 'npm');
assert.deepEqual(npmArgs, [
'npm ping',
'npm whoami',
`npm show ${pkgName}@latest version`,
'npm --version',
`npm access ${npmMajorVersion >= 9 ? 'list collaborators --json' : 'ls-collaborators'} ${pkgName}`,
'npm version 1.0.1 --no-git-tag-version',
'npm publish . --tag latest'
]);
assert(log.obtrusive.mock.calls[0].arguments[0].endsWith(`release ${pkgName} (1.0.0...1.0.1)`));
assert(log.log.mock.calls[0].arguments[0].endsWith(`https://www.npmjs.com/package/${pkgName}`));
assert(log.log.mock.calls[1].arguments[0].endsWith(`https://github.com/${owner}/${project}/releases/tag/1.0.1`));
});
test('should release with correct tag name', async t => {
const project = path.basename(bare);
const pkgName = path.basename(target);
const owner = path.basename(path.dirname(bare));
const stdout = childProcess.execSync('git rev-parse --abbrev-ref HEAD', { encoding: 'utf-8' });
const branchName = stdout.trim();
gitAdd(`{"name":"${pkgName}","version":"1.0.0"}`, 'package.json', 'Add package.json');
childProcess.execSync(`git tag ${pkgName}-${branchName}-1.0.0`, execOpts);
const sha = gitAdd('line', 'file', 'More file');
interceptGitHubCreate(github, {
owner,
project,
body: {
tag_name: `${pkgName}-${branchName}-1.0.1`,
name: 'Release 1.0.1',
body: `* More file (${sha})`,
prerelease: false
}
});
const container = getContainer({
git: { tagName: '${npm.name}-${branchName}-${version}' },
github: { release: true, skipChecks: true, pushRepo: `https://github.com/${owner}/${project}` }
});
const exec = t.mock.method(container.shell, 'exec');
await runTasks({}, container);
const gitArgs = getArgs(exec, 'git');
assert(gitArgs.includes(`git tag --annotate --message Release 1.0.1 ${pkgName}-${branchName}-1.0.1`));
assert(
log.log.mock.calls[1].arguments[0].endsWith(`/${owner}/${project}/releases/tag/${pkgName}-${branchName}-1.0.1`)
);
});
test('should release all the things (pre-release, github, gitlab)', async t => {
const project = path.basename(bare);
const pkgName = path.basename(target);
const owner = path.basename(path.dirname(bare));
const url = `https://gitlab.com/${owner}/${project}`;
gitAdd(`{"name":"${pkgName}","version":"1.0.0"}`, 'package.json', 'Add package.json');
childProcess.execSync('git tag v1.0.0', execOpts);
const sha = gitAdd('line', 'file', 'More file');
childProcess.execSync('git push --follow-tags', execOpts);
const git = await factory(Git);
const ref = (await git.getBranchName()) ?? 'HEAD';
interceptGitHubAuthentication(github);
interceptGitHubCollaborator(github, { owner, project });
interceptGitHubAsset(assets, { owner, project, body: 'lineline' });
interceptGitHubCreate(github, {
owner,
project,
body: {
tag_name: 'v1.1.0-alpha.0',
name: 'Release 1.1.0-alpha.0',
body: `Notes for ${pkgName} [v1.1.0-alpha.0]: ${sha}`,
prerelease: true
}
});
interceptGitLabUser(gitlab, { owner });
interceptGitLabCollaborator(gitlab, { owner, project });
interceptGitLabAsset(gitlab, { owner, project });
interceptGitLabPublish(gitlab, {
owner,
project,
body: {
name: 'Release 1.1.0-alpha.0',
ref,
tag_name: 'v1.1.0-alpha.0',
tag_message: `${owner} ${owner}/${project} ${project}`,
description: `Notes for ${pkgName}: ${sha}`,
assets: {
links: [
{
name: 'file',
url: `${url}/uploads/7e8bec1fe27cc46a4bc6a91b9e82a07c/file`
}
]
}
}
});
const container = getContainer({
increment: 'minor',
preRelease: 'alpha',
git: {
changelog: 'git log --pretty=format:%h ${latestTag}...HEAD',
commitMessage: 'Release ${version} for ${name} (from ${latestVersion})',
tagAnnotation: '${repo.owner} ${repo.repository} ${repo.project}'
},
github: {
release: true,
pushRepo: `https://github.com/${owner}/${project}`,
releaseNotes: 'echo Notes for ${name} [v${version}]: ${changelog}',
assets: ['file']
},
gitlab: {
release: true,
pushRepo: url,
releaseNotes: 'echo Notes for ${name}: ${changelog}',
assets: ['file']
},
npm: { name: pkgName }
});
const exec = t.mock.method(container.shell, 'exec');
process.env['GITLAB_TOKEN'] = '123';
await runTasks({}, container);
const npmArgs = getArgs(exec, 'npm');
assert.deepEqual(npmArgs, [
'npm ping',
'npm whoami',
`npm show ${pkgName}@latest version`,
'npm --version',
`npm access ${npmMajorVersion >= 9 ? 'list collaborators --json' : 'ls-collaborators'} ${pkgName}`,
'npm version 1.1.0-alpha.0 --no-git-tag-version',
'npm publish . --tag alpha'
]);
const commitMessage = childProcess.execSync('git log --oneline --format=%B -n 1 HEAD', {
encoding: 'utf-8'
});
assert.equal(commitMessage.trim(), `Release 1.1.0-alpha.0 for ${pkgName} (from 1.0.0)`);
const tagName = childProcess.execSync('git describe --tags --match=* --abbrev=0', { encoding: 'utf-8' });
assert.equal(tagName.trim(), 'v1.1.0-alpha.0');
const tagAnnotation = childProcess.execSync('git for-each-ref refs/tags/v1.1.0-alpha.0 --format="%(contents)"', {
encoding: 'utf-8'
});
assert.equal(tagAnnotation.trim(), `${owner} ${owner}/${project} ${project}`);
assert(log.obtrusive.mock.calls[0].arguments[0].endsWith(`release ${pkgName} (1.0.0...1.1.0-alpha.0)`));
assert(log.log.mock.calls[0].arguments[0].endsWith(`https://www.npmjs.com/package/${pkgName}`));
assert(log.log.mock.calls[1].arguments[0].endsWith(`/${owner}/${project}/releases/tag/v1.1.0-alpha.0`));
assert(log.log.mock.calls[2].arguments[0].endsWith(`/${project}/-/releases/v1.1.0-alpha.0`));
assert.match(log.log.mock.calls.at(-1).arguments[0], /Done \(in [0-9]+s\.\)/);
});
test('should publish pre-release without pre-id with different npm.tag', async t => {
const pkgName = path.basename(target);
gitAdd(`{"name":"${pkgName}","version":"1.0.0"}`, 'package.json', 'Add package.json');
childProcess.execSync('git tag v1.0.0', execOpts);
const container = getContainer({ increment: 'major', preRelease: true, npm: { name: pkgName, tag: 'next' } });
const exec = t.mock.method(container.shell, 'exec');
await runTasks({}, container);
const npmArgs = getArgs(exec, 'npm');
assert.deepEqual(npmArgs, [
'npm ping',
'npm whoami',
`npm show ${pkgName}@latest version`,
'npm --version',
`npm access ${npmMajorVersion >= 9 ? 'list collaborators --json' : 'ls-collaborators'} ${pkgName}`,
'npm version 2.0.0-0 --no-git-tag-version',
'npm publish . --tag next'
]);
const stdout = childProcess.execSync('git describe --tags --match=* --abbrev=0', { encoding: 'utf-8' });
assert.equal(stdout.trim(), 'v2.0.0-0');
assert(log.obtrusive.mock.calls[0].arguments[0].endsWith(`release ${pkgName} (1.0.0...2.0.0-0)`));
assert(log.log.mock.calls[0].arguments[0].endsWith(`https://www.npmjs.com/package/${pkgName}`));
assert.match(log.log.mock.calls.at(-1).arguments[0], /Done \(in [0-9]+s\.\)/);
});
test('should handle private package correctly, bump lockfile', async t => {
const pkgName = path.basename(target);
gitAdd(`{"name":"${pkgName}","version":"1.0.0","private":true}`, 'package.json', 'Add package.json');
gitAdd(`{"name":"${pkgName}","version":"1.0.0","private":true}`, 'package-lock.json', 'Add package-lock.json');
const container = getContainer({ npm: { name: pkgName, private: true } });
const exec = t.mock.method(container.shell, 'exec');
await runTasks({}, container);
const npmArgs = getArgs(exec, 'npm');
assert.deepEqual(npmArgs, ['npm version 1.0.1 --no-git-tag-version']);
assert(log.obtrusive.mock.calls[0].arguments[0].endsWith(`release ${pkgName} (1.0.0...1.0.1)`));
assert.equal(log.warn.length, 0);
assert.match(log.log.mock.calls[0].arguments[0], /Done \(in [0-9]+s\.\)/);
});
test('should initially publish non-private scoped npm package privately', async t => {
const pkgName = path.basename(target);
gitAdd(`{"name":"@scope/${pkgName}","version":"1.0.0"}`, 'package.json', 'Add package.json');
const container = getContainer({ npm: { name: pkgName } });
const original = container.shell.exec.bind(container.shell);
const exec = t.mock.method(container.shell, 'exec', (...args) => {
if (args[0] === `npm show @scope/${pkgName}@latest version`) return Promise.reject();
return original(...args);
});
await runTasks({}, container);
const npmArgs = getArgs(exec, 'npm');
assert.equal(npmArgs[6], 'npm publish . --tag latest');
});
test('should use pkg.publishConfig.registry', async t => {
const pkgName = path.basename(target);
const registry = 'https://my-registry.example.org';
gitAdd(
JSON.stringify({
name: pkgName,
version: '1.2.3',
publishConfig: { registry }
}),
'package.json',
'Add package.json'
);
const container = getContainer();
const exec = t.mock.method(container.shell, 'exec');
await runTasks({}, container);
const npmArgs = getArgs(exec, 'npm');
assert.equal(npmArgs[0], `npm ping --registry ${registry}`);
assert.equal(npmArgs[1], `npm whoami --registry ${registry}`);
assert(container.log.log.mock.calls[0].arguments[0].endsWith(`${registry}/package/${pkgName}`));
});
test('should propagate errors', async () => {
const config = {
hooks: {
'before:init': 'some-failing-command'
}
};
const container = getContainer(config);
await assert.rejects(runTasks({}, container), { message: /some-failing-command/ });
assert.equal(log.error.mock.callCount(), 1);
});
test('should use custom changelog command with context', async t => {
const project = path.basename(bare);
const owner = path.basename(path.dirname(bare));
childProcess.execSync('git tag v1.0.0', execOpts);
gitAdd('line', 'file', 'More file');
interceptGitHubAuthentication(github);
interceptGitHubCollaborator(github, { owner, project });
interceptGitHubCreate(github, {
owner,
project,
body: {
tag_name: 'v1.1.0',
name: 'Release 1.1.0',
body: 'custom-changelog-generator --from=v1.0.0 --to=v1.1.0',
draft: false,
prerelease: false
}
});
const container = getContainer({
increment: 'minor',
github: {
release: true,
releaseNotes: 'echo custom-changelog-generator --from=${latestTag} --to=${tagName}',
pushRepo: `https://github.com/${owner}/${project}`
}
});
const exec = t.mock.method(container.shell, 'execStringCommand');
await runTasks({}, container);
const command = exec.mock.calls
.map(call => call.arguments)
.find(([command]) => command.includes('custom-changelog-generator'));
assert.equal(command[0], 'echo custom-changelog-generator --from=v1.0.0 --to=v1.1.0');
});
test('should run all hooks', async t => {
gitAdd(`{"name":"hooked","version":"1.0.0","type":"module"}`, 'package.json', 'Add package.json');
childProcess.execSync(`npm install ${rootDir}`, execOpts);
const plugin = "import { Plugin } from 'release-it'; class MyPlugin extends Plugin {}; export default MyPlugin;";
appendFileSync('my-plugin.js', plugin);
const hooks = {};
['before', 'after'].forEach(prefix => {
['version', 'git', 'npm', 'my-plugin'].forEach(ns => {
['init', 'beforeBump', 'bump', 'beforeRelease', 'release', 'afterRelease'].forEach(cycle => {
hooks[`${prefix}:${cycle}`] = `echo ${prefix}:${cycle}`;
hooks[`${prefix}:${ns}:${cycle}`] = `echo ${prefix}:${ns}:${cycle}`;
});
});
});
const container = getContainer({
plugins: { './my-plugin.js': {} },
git: { requireCleanWorkingDir: false },
hooks
});
const exec = t.mock.method(container.shell, 'execFormattedCommand');
await runTasks({}, container);
const commands = getArgs(exec, 'echo');
assert.deepEqual(commands, [
'echo before:init',
'echo before:my-plugin:init',
'echo after:my-plugin:init',
'echo before:npm:init',
'echo after:npm:init',
'echo before:git:init',
'echo after:git:init',
'echo before:version:init',
'echo after:version:init',
'echo after:init',
'echo before:beforeBump',
'echo before:my-plugin:beforeBump',
'echo after:my-plugin:beforeBump',
'echo before:npm:beforeBump',
'echo after:npm:beforeBump',
'echo before:git:beforeBump',
'echo after:git:beforeBump',
'echo before:version:beforeBump',
'echo after:version:beforeBump',
'echo after:beforeBump',
'echo before:bump',
'echo before:my-plugin:bump',
'echo after:my-plugin:bump',
'echo before:npm:bump',
'echo after:npm:bump',
'echo before:git:bump',
'echo after:git:bump',
'echo before:version:bump',
'echo after:version:bump',
'echo after:bump',
'echo before:beforeRelease',
'echo before:my-plugin:beforeRelease',
'echo after:my-plugin:beforeRelease',
'echo before:npm:beforeRelease',
'echo after:npm:beforeRelease',
'echo before:git:beforeRelease',
'echo after:git:beforeRelease',
'echo before:version:beforeRelease',
'echo after:version:beforeRelease',
'echo after:beforeRelease',
'echo before:release',
'echo before:npm:release',
'echo after:npm:release',
'echo before:git:release',
'echo after:git:release',
'echo before:version:release',
'echo after:version:release',
'echo before:my-plugin:release',
'echo after:my-plugin:release',
'echo after:release',
'echo before:afterRelease',
'echo before:npm:afterRelease',
'echo after:npm:afterRelease',
'echo before:git:afterRelease',
'echo after:git:afterRelease',
'echo before:version:afterRelease',
'echo after:version:afterRelease',
'echo before:my-plugin:afterRelease',
'echo after:my-plugin:afterRelease',
'echo after:afterRelease'
]);
});
});

32
node_modules/release-it/test/util/fetch.js generated vendored Normal file
View File

@@ -0,0 +1,32 @@
import { Readable } from 'node:stream';
import { create as createTar } from 'tar';
import { appendFile, mkTmpDir } from './helpers.js';
export function createTarBlob(dir) {
const stream = new Readable({
read() {}
});
createTar(
{
gzip: true,
portable: true,
sync: false,
cwd: dir
},
['.']
)
.on('data', chunk => stream.push(chunk))
.on('end', () => stream.push(null));
return stream;
}
export function createTarBlobByRawContents(contents) {
const dir = mkTmpDir();
for (const [key, value] of Object.entries(contents)) {
appendFile(value, key, dir);
}
return createTarBlob(dir);
}

40
node_modules/release-it/test/util/helpers.js generated vendored Normal file
View File

@@ -0,0 +1,40 @@
import { appendFileSync, mkdirSync, mkdtempSync, promises } from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import childProcess from 'node:child_process';
import { execOpts } from '../../lib/util.js';
const mkTmpDir = () => {
const dir = mkdtempSync(path.join(os.tmpdir(), 'release-it-'));
return dir;
};
const readFile = file => promises.readFile(path.resolve(file), 'utf8');
const gitAdd = (content, filePath, message) => {
appendFile(content, filePath);
childProcess.execSync(`git add ${filePath}`, execOpts);
const stdout = childProcess.execSync(`git commit -m "${message}"`, { encoding: 'utf-8' });
const match = stdout.match(/\[.+([a-z0-9]{7})\]/);
return match ? match[1] : null;
};
const getArgs = (fn, prefix) =>
fn.mock.calls
.map(call => call.arguments[0])
.map(arg => (typeof arg !== 'string' ? arg.join(' ') : arg))
.filter(cmd => cmd.startsWith(prefix))
.map(cmd => cmd.trim());
const appendFile = (content, filePath, cwd) => {
filePath = path.resolve(cwd ?? '', filePath);
const dirPath = path.dirname(filePath);
if (dirPath) {
mkdirSync(dirPath, { mode: parseInt('0777', 8), recursive: true });
}
appendFileSync(filePath, content);
};
export { mkTmpDir, readFile, gitAdd, getArgs, appendFile };

View File

@@ -0,0 +1,22 @@
-----BEGIN CERTIFICATE-----
MIIDrzCCApegAwIBAgIUUAyKcu86WuJQd1fdlHsXkCl1mYMwDQYJKoZIhvcNAQEL
BQAwZzELMAkGA1UEBhMCVVMxDTALBgNVBAgMBFV0YWgxDjAMBgNVBAcMBVByb3Zv
MSMwIQYDVQQKDBpBQ01FIFNpZ25pbmcgQXV0aG9yaXR5IEluYzEUMBIGA1UEAwwL
ZXhhbXBsZS5jb20wHhcNMjQxMjExMTMxMzIyWhcNMjcxMDAxMTMxMzIyWjBnMQsw
CQYDVQQGEwJVUzENMAsGA1UECAwEVXRhaDEOMAwGA1UEBwwFUHJvdm8xIzAhBgNV
BAoMGkFDTUUgU2lnbmluZyBBdXRob3JpdHkgSW5jMRQwEgYDVQQDDAtleGFtcGxl
LmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKlp3Kz278oyTDY4
+1iNv1xMzWCJBWf7YxyYkXACVvZzUqgwxotNwAE/kE131HgpBJnu7HsDdS8nLy80
mDkFCGmqEuVZMDaMGtcz3HJSklESjxYM1VNgifz2IlNmoaidNehFkCi1rz6cLMR0
rEZpi98EbVZGl3n0vDD0eEZF5QQn/eDwclt/aKVBVLcQxo6MHhQdaK85a1uNyUU3
HPUYiyPGVn9oX9PGcOpYFomJoBdg3sUVk32xKCUf15+uqVchdtyGFW/bp5KSBCnB
QD0TzN+SgBHnVTzIfoi8WY56uuwnYauvT4fRh8SfkV91BzHBtWSbulNTfTUzgWhs
VvCGw7ECAwEAAaNTMFEwHQYDVR0OBBYEFC4qTshaPoBie1ukhhM0KBi95NzJMB8G
A1UdIwQYMBaAFC4qTshaPoBie1ukhhM0KBi95NzJMA8GA1UdEwEB/wQFMAMBAf8w
DQYJKoZIhvcNAQELBQADggEBAB+fRdUHgqwpTWRfF+jQB4Af75HTfp6hgemUjapI
eZn/OugS75/jfJt9npVsHl/aa63GL/W6kQShoMVOhrYqW52J1TSsLKZR2L7sv0ji
KYfakv+aLkRKewPoVadsCL8GUmaCByE9mwlhmmZprkjDmA3hWsjEM5lyg7qleJ0k
V32FVysdhLLnftt2SJB7lyoTujhkNAjJhLT/0Qr8t59v0sViPtL8532jSXqE1GK+
zncsJDK7v2VEuurz1lPTRY6tPQOJ1Qt8vUzDH/ugcc5JPBEuHhjjrd5K65lxnGNw
lnPHIS7FJm1OMkuatQXomNuuoWDPyM7fuVyGUUpmlkbpJsg=
-----END CERTIFICATE-----

Binary file not shown.

65
node_modules/release-it/test/util/https-server/gen-cert.sh generated vendored Executable file
View File

@@ -0,0 +1,65 @@
#!/bin/bash
# Generate client and server self-signed HTTPS certificates
# Adapted from:
# https://stackoverflow.com/questions/19665863/how-do-i-use-a-self-signed-certificate-for-a-https-node-js-server/24749608#24749608
FQDN=$1
if [ -z "$FQDN" ]; then
echo -e "\nError: Missing required parameter for domain name.\n"
echo -e "Usage:\n\t./gen-cert.sh <DOMAIN_NAME>\n"
exit 1
fi;
# make directories to work from
mkdir -p server/ client/ all/
# Create your very own Root Certificate Authority
openssl genrsa \
-out all/my-private-root-ca.privkey.pem \
2048
# Self-sign your Root Certificate Authority
# Since this is private, the details can be as bogus as you like
openssl req \
-x509 \
-new \
-nodes \
-key all/my-private-root-ca.privkey.pem \
-days 1024 \
-out all/my-private-root-ca.cert.pem \
-subj "/C=US/ST=Utah/L=Provo/O=ACME Signing Authority Inc/CN=example.com"
# Create a Device Certificate for each domain,
# such as example.com, *.example.com, awesome.example.com
# NOTE: You MUST match CN to the domain name or ip address you want to use
openssl genrsa \
-out all/privkey.pem \
2048
# Create a request from your Device, which your Root CA will sign
openssl req -new \
-key all/privkey.pem \
-out all/csr.pem \
-subj "/C=US/ST=Utah/L=Provo/O=ACME Tech Inc/CN=${FQDN}"
# Sign the request from Device with your Root CA
openssl x509 \
-req -in all/csr.pem \
-CA all/my-private-root-ca.cert.pem \
-CAkey all/my-private-root-ca.privkey.pem \
-CAcreateserial \
-out all/cert.pem \
-days 500
# Put things in their proper place
rsync -a all/{privkey,cert}.pem server/
cat all/cert.pem > server/fullchain.pem # we have no intermediates in this case
rsync -a all/my-private-root-ca.cert.pem server/
rsync -a all/my-private-root-ca.cert.pem client/
# create DER format crt for iOS Mobile Safari, etc
openssl x509 -outform der -in all/my-private-root-ca.cert.pem -out client/my-private-root-ca.crt
rm -r all

View File

@@ -0,0 +1,150 @@
import { createServer } from 'node:https';
import { readFileSync } from 'node:fs';
import { join } from 'node:path';
import { debug } from 'node:util';
import { fileURLToPath } from 'node:url';
/**
* @typedef {import('http').IncomingMessage} IncomingMessage
* @typedef {import('http').ServerResponse} ServerResponse
* @typedef {ServerResponse & { req: IncomingMessage;}} RequestResponse
* @typedef {import('https').ServerOptions} ServerOptions
*/
const DIRNAME = getDirname();
/** @type {ServerOptions} */
const options = {
key: readFileSync(join(DIRNAME, './server/privkey.pem')),
cert: readFileSync(join(DIRNAME, './server/fullchain.pem'))
};
/**
* Basic https server to use for the Gitlab tests.
* Uses a self-signed HTTPS certificate to allow testing gitlab release options
* like `insecure` or `certificateAuthorityFile`.
*
* The certicates were generated using the gen-cert.sh script in this folder
* with the following command:
*
* `./gen-cert.sh localhost`
*
*/
export class GitlabTestServer {
constructor() {
this.server = createServer(options, (req, res) => this._requestHandler(req, res));
this.debug = debug('release-it:gitlab-test-server');
}
/**
* Starts the server with the given port and host
*
* @param {number} [port]
* @param {string} [host]
* @returns {Promise<void>}
*/
run(port = 3000, host) {
return new Promise((resolve, reject) => {
if (this.server.listening) {
resolve();
return;
}
this.server.listen(port, host, () => {
const address = this.server.address();
this.debug('Server listening on https://' + address.address + ':' + address.port);
resolve();
});
this.server.on('error', e => {
if (e.code === 'EADDRINUSE') {
reject(e);
return;
}
this.debug(e.message);
});
});
}
/**
* Closes the server
*
* @returns {Promise<void>}
*/
stop() {
return new Promise((resolve, reject) => {
if (!this.server.listening) {
resolve();
return;
}
this.server.removeAllListeners();
this.server.close(err => {
if (err) {
reject(err);
return;
}
this.debug('Server successfully closed.');
resolve();
});
});
}
/**
* @private
*
* Server's main request handler
*
* @param {IncomingMessage} req
* @param {RequestResponse} res
* @returns {void}
*/
_requestHandler(req, res) {
if (req.url === '/api/v4/user') {
this._json(res, { id: '1234', username: 'release_bot' });
return;
}
if (req.url.startsWith('/api/v4/projects') && req.url.endsWith('/members/all/1234')) {
this._json(res, { access_level: 50 });
return;
}
this._text(res, 'Ok');
}
/**
* @private
*
* Sends out a JSON response
*
* @param {RequestResponse} res
* @param {object} payload
*/
_json(res, payload) {
res.writeHead(200, { 'content-type': 'application/json' });
res.end(JSON.stringify(payload));
}
/**
* @private
*
* Sends out a text response
*
* @param {RequestResponse} res
* @param {string} message
*/
_text(res, message) {
res.writeHead(200, { 'content-type': 'text/plan' });
res.end(message);
}
}
function getDirname() {
if (import.meta.dirname) return import.meta.dirname;
return fileURLToPath(new URL('.', import.meta.url));
}

View File

@@ -0,0 +1,20 @@
-----BEGIN CERTIFICATE-----
MIIDRjCCAi4CFGCNNJXfcHchSO1xHnWt5wcENGznMA0GCSqGSIb3DQEBCwUAMGcx
CzAJBgNVBAYTAlVTMQ0wCwYDVQQIDARVdGFoMQ4wDAYDVQQHDAVQcm92bzEjMCEG
A1UECgwaQUNNRSBTaWduaW5nIEF1dGhvcml0eSBJbmMxFDASBgNVBAMMC2V4YW1w
bGUuY29tMB4XDTI0MTIxMTEzMTMyMloXDTI2MDQyNTEzMTMyMlowWDELMAkGA1UE
BhMCVVMxDTALBgNVBAgMBFV0YWgxDjAMBgNVBAcMBVByb3ZvMRYwFAYDVQQKDA1B
Q01FIFRlY2ggSW5jMRIwEAYDVQQDDAlsb2NhbGhvc3QwggEiMA0GCSqGSIb3DQEB
AQUAA4IBDwAwggEKAoIBAQC1+BYy7XcJoIAHqPcU7f74Dp/6N6f/hjLTADMIT1OO
49W3Rrc6xbJjeLsiTD0Rj9Z/9CSI7Vh/AOLoW7OGMOZkJZNRw8L9lc14ZDB1FAcJ
ITn8c9d0d0YltbxUOq509wEP6GSxYCqYZlvAjeyYADRW5PnP82n6MRMW9Ve5y50s
ijeaVk120je5zKvQTd0IR9rXf8K3M8CmnlEVuuf4+uXvg0gUsGvSSkBxPVGeUQHM
/zRxj/I8WYCewb9Qs2TSJjgnFsfUCF6C3xs/T8p4vJ8PxeLs6GNcQhhtNH0q7Wxa
IV4cdrXkxPi6YS8ph1WErqNR2pdsCA5bUNTCT32vPjr/AgMBAAEwDQYJKoZIhvcN
AQELBQADggEBACu1hC1yvFKF6nEjz4mdTmVIyqktfkQX4+5edpSCLM0UhOmQ9Tm3
3NnQ06YhPnAkiXLiyKxoCGqulgh1B1+3Ii0/ttDq4D4HIEVMwT5Hmko3vppUkJD7
aIacmTKxGFrtF+p1hIDXmAAYFUB2bWDgVvba2z70DkbffJBOwe/+2+hOgFXI5x3+
IJuF0bYKRlFG0yvVX+ooWAaKmCSR4reCsjWNuP7KBBJv15GZLKfHPjDUuxW3u43/
ZmyBM+1Fs91jt0v5w1dIDF0z1pxwbaRJ3w1M1kEU8i6/q+1YaahYvdeaa0jsdSsy
kpz3YhZtb324TKmjfUFMzYvfcoR8IYDkekQ=
-----END CERTIFICATE-----

View File

@@ -0,0 +1,20 @@
-----BEGIN CERTIFICATE-----
MIIDRjCCAi4CFGCNNJXfcHchSO1xHnWt5wcENGznMA0GCSqGSIb3DQEBCwUAMGcx
CzAJBgNVBAYTAlVTMQ0wCwYDVQQIDARVdGFoMQ4wDAYDVQQHDAVQcm92bzEjMCEG
A1UECgwaQUNNRSBTaWduaW5nIEF1dGhvcml0eSBJbmMxFDASBgNVBAMMC2V4YW1w
bGUuY29tMB4XDTI0MTIxMTEzMTMyMloXDTI2MDQyNTEzMTMyMlowWDELMAkGA1UE
BhMCVVMxDTALBgNVBAgMBFV0YWgxDjAMBgNVBAcMBVByb3ZvMRYwFAYDVQQKDA1B
Q01FIFRlY2ggSW5jMRIwEAYDVQQDDAlsb2NhbGhvc3QwggEiMA0GCSqGSIb3DQEB
AQUAA4IBDwAwggEKAoIBAQC1+BYy7XcJoIAHqPcU7f74Dp/6N6f/hjLTADMIT1OO
49W3Rrc6xbJjeLsiTD0Rj9Z/9CSI7Vh/AOLoW7OGMOZkJZNRw8L9lc14ZDB1FAcJ
ITn8c9d0d0YltbxUOq509wEP6GSxYCqYZlvAjeyYADRW5PnP82n6MRMW9Ve5y50s
ijeaVk120je5zKvQTd0IR9rXf8K3M8CmnlEVuuf4+uXvg0gUsGvSSkBxPVGeUQHM
/zRxj/I8WYCewb9Qs2TSJjgnFsfUCF6C3xs/T8p4vJ8PxeLs6GNcQhhtNH0q7Wxa
IV4cdrXkxPi6YS8ph1WErqNR2pdsCA5bUNTCT32vPjr/AgMBAAEwDQYJKoZIhvcN
AQELBQADggEBACu1hC1yvFKF6nEjz4mdTmVIyqktfkQX4+5edpSCLM0UhOmQ9Tm3
3NnQ06YhPnAkiXLiyKxoCGqulgh1B1+3Ii0/ttDq4D4HIEVMwT5Hmko3vppUkJD7
aIacmTKxGFrtF+p1hIDXmAAYFUB2bWDgVvba2z70DkbffJBOwe/+2+hOgFXI5x3+
IJuF0bYKRlFG0yvVX+ooWAaKmCSR4reCsjWNuP7KBBJv15GZLKfHPjDUuxW3u43/
ZmyBM+1Fs91jt0v5w1dIDF0z1pxwbaRJ3w1M1kEU8i6/q+1YaahYvdeaa0jsdSsy
kpz3YhZtb324TKmjfUFMzYvfcoR8IYDkekQ=
-----END CERTIFICATE-----

View File

@@ -0,0 +1,22 @@
-----BEGIN CERTIFICATE-----
MIIDrzCCApegAwIBAgIUUAyKcu86WuJQd1fdlHsXkCl1mYMwDQYJKoZIhvcNAQEL
BQAwZzELMAkGA1UEBhMCVVMxDTALBgNVBAgMBFV0YWgxDjAMBgNVBAcMBVByb3Zv
MSMwIQYDVQQKDBpBQ01FIFNpZ25pbmcgQXV0aG9yaXR5IEluYzEUMBIGA1UEAwwL
ZXhhbXBsZS5jb20wHhcNMjQxMjExMTMxMzIyWhcNMjcxMDAxMTMxMzIyWjBnMQsw
CQYDVQQGEwJVUzENMAsGA1UECAwEVXRhaDEOMAwGA1UEBwwFUHJvdm8xIzAhBgNV
BAoMGkFDTUUgU2lnbmluZyBBdXRob3JpdHkgSW5jMRQwEgYDVQQDDAtleGFtcGxl
LmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKlp3Kz278oyTDY4
+1iNv1xMzWCJBWf7YxyYkXACVvZzUqgwxotNwAE/kE131HgpBJnu7HsDdS8nLy80
mDkFCGmqEuVZMDaMGtcz3HJSklESjxYM1VNgifz2IlNmoaidNehFkCi1rz6cLMR0
rEZpi98EbVZGl3n0vDD0eEZF5QQn/eDwclt/aKVBVLcQxo6MHhQdaK85a1uNyUU3
HPUYiyPGVn9oX9PGcOpYFomJoBdg3sUVk32xKCUf15+uqVchdtyGFW/bp5KSBCnB
QD0TzN+SgBHnVTzIfoi8WY56uuwnYauvT4fRh8SfkV91BzHBtWSbulNTfTUzgWhs
VvCGw7ECAwEAAaNTMFEwHQYDVR0OBBYEFC4qTshaPoBie1ukhhM0KBi95NzJMB8G
A1UdIwQYMBaAFC4qTshaPoBie1ukhhM0KBi95NzJMA8GA1UdEwEB/wQFMAMBAf8w
DQYJKoZIhvcNAQELBQADggEBAB+fRdUHgqwpTWRfF+jQB4Af75HTfp6hgemUjapI
eZn/OugS75/jfJt9npVsHl/aa63GL/W6kQShoMVOhrYqW52J1TSsLKZR2L7sv0ji
KYfakv+aLkRKewPoVadsCL8GUmaCByE9mwlhmmZprkjDmA3hWsjEM5lyg7qleJ0k
V32FVysdhLLnftt2SJB7lyoTujhkNAjJhLT/0Qr8t59v0sViPtL8532jSXqE1GK+
zncsJDK7v2VEuurz1lPTRY6tPQOJ1Qt8vUzDH/ugcc5JPBEuHhjjrd5K65lxnGNw
lnPHIS7FJm1OMkuatQXomNuuoWDPyM7fuVyGUUpmlkbpJsg=
-----END CERTIFICATE-----

View File

@@ -0,0 +1,28 @@
-----BEGIN PRIVATE KEY-----
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC1+BYy7XcJoIAH
qPcU7f74Dp/6N6f/hjLTADMIT1OO49W3Rrc6xbJjeLsiTD0Rj9Z/9CSI7Vh/AOLo
W7OGMOZkJZNRw8L9lc14ZDB1FAcJITn8c9d0d0YltbxUOq509wEP6GSxYCqYZlvA
jeyYADRW5PnP82n6MRMW9Ve5y50sijeaVk120je5zKvQTd0IR9rXf8K3M8CmnlEV
uuf4+uXvg0gUsGvSSkBxPVGeUQHM/zRxj/I8WYCewb9Qs2TSJjgnFsfUCF6C3xs/
T8p4vJ8PxeLs6GNcQhhtNH0q7WxaIV4cdrXkxPi6YS8ph1WErqNR2pdsCA5bUNTC
T32vPjr/AgMBAAECggEAHeLCZpfYmpSpIljuR5o063GfdZ1pco6MT1ozh3Rb0VZ6
9bBgDH+Gpk6gUWg7CWTZwkcLLw/oHme7XJUe/XWPiTggo2em4TYWumSeDsR8yVOT
LfKqmp6yPyRDa4P9vgkJPB8bVoRoSoJZJF1K08YI0pKlsrEUITqpG3as8z9NL5C1
65QdpCwiRjLnM6b7k93Vxa8xOnj51iDXV9uqGASCYmU+uoHG8HU8RzbVI4Xi/RCr
PszmOi6AMtWPpzH2jDPiYnv96K50O/cOm/n3obuySdH0qeGR37sx94F82iCyiowo
Ij+iU8VYK6gu27z811ehSlk3/4ey8ArlHvVew3TkqQKBgQD+K70baVqolcGfkDQY
zgZ+4YlmncDrdhd0anHUEC15l7uPPHFBxrQlzRVTjhHCKn1sQTj/41isUWQAvcRR
ZzjI+YaHQvvzJCSX6c113IWeemCcuTxenW6G/5BHJxOPddFnrcAAoeHhcNvni/CZ
WrInTIcWn7eY5MMaxa4ELUoKRwKBgQC3R1R9be3BY36wPYcPkTZFxRAn2o7/Z2Ft
MDbrddDZt12F7cuD8YnipI7TkJLQTXmg6P7u2XexcEyAUy2J+jdgfpM0N/HXu3Su
YeYXHJfyGUK6bTlQT+trNuEyX5K7h2IbBstvH9A4PXEZPQ2I3aPZk3WsL630cMo6
tbTy0o3tiQKBgQCv11qxSCXsVA7scTtZnc9ooGgKkkERpVV8uNefOsH7STn9UneY
Zfvj2wpSEAvBJNw4tLbWcVa7gGOLD75uAteKUvb7RSBBilO2tY3raHEYvtlwE8bs
PkZlJxGN6D7kFUKWU+JtjZFUAlxgyLPfpJt0DMG4qS6/nCROtUw6n4qFqQKBgB2S
JrGuIORI91HcO4Rpe4Y6S2cCvnu65F9HnjTTZ4UZLr/DJEj/ma21u02rT+TH+03Z
CfjjoYpBgjZaNUjD1Fd/VKTiOeUC28qfBQ7JkEKBjOCjatHocyVzT1ZfUT9skoml
yQD+8wt/7lWSIjLo/9zFDAFiGAEOibJ7Sty62CdxAoGBAJLwVkDCT3yylDRqm2YC
1uYh/xGLW4F/Jj7PjGNrSYClkbxPTpd4Nfa8EyZmUUya1rsfK1njXZKbwwpa2fbE
+QVpXB18vaMW3hU7u63JDpLv2pCm91myPMQsF3xMc/S1vFAOWmXetYWax4/xiG2M
SW4m9mBkzzF/2SpxQd9xy1Jx
-----END PRIVATE KEY-----

104
node_modules/release-it/test/util/index.js generated vendored Normal file
View File

@@ -0,0 +1,104 @@
import { mock } from 'node:test';
import semver from 'semver';
import { parseVersion } from '../../lib/util.js';
import Config from '../../lib/config.js';
import ShellStub from '../stub/shell.js';
import Prompt from '../../lib/prompt.js';
const noop = Promise.resolve();
export class LogStub {
constructor() {
this.log = mock.fn();
this.error = mock.fn();
this.info = mock.fn();
this.warn = mock.fn();
this.verbose = mock.fn();
this.exec = mock.fn();
this.obtrusive = mock.fn();
this.preview = mock.fn();
}
resetCalls() {
this.log.mock.resetCalls();
this.error.mock.resetCalls();
this.info.mock.resetCalls();
this.warn.mock.resetCalls();
this.verbose.mock.resetCalls();
this.exec.mock.resetCalls();
this.obtrusive.mock.resetCalls();
this.preview.mock.resetCalls();
}
}
export class SpinnerStub {
show({ enabled = true, task }) {
return enabled ? task() : noop;
}
}
export let factory = async (Definition, { namespace, options = {}, container = {} } = {}) => {
options = Object.assign({}, { ci: true, verbose: false, 'dry-run': false, debug: false }, options);
const ns = namespace || Definition.name.toLowerCase();
container.config = container.config || new Config(Object.assign({ config: false }, options));
container.log = new LogStub();
await container.config.init();
container.spinner = new SpinnerStub();
container.shell = container.shell || new ShellStub({ container });
container.prompt = container.prompt || new Prompt({ container });
container.shell.cache = { set: () => {}, has: () => false };
return new Definition({
namespace: ns,
options,
container
});
};
const getIncrement = plugin =>
plugin.getIncrement(plugin.options) || plugin.getContext('increment') || plugin.config.getContext('increment');
const getVersion = async (plugin, options) => {
const { latestVersion, increment } = options;
return (
(await plugin.getIncrementedVersionCI(options)) ||
(await plugin.getIncrementedVersion(options)) ||
(increment !== false ? semver.inc(latestVersion, increment || 'patch') : latestVersion)
);
};
export let runTasks = async plugin => {
await plugin.init();
const name = (await plugin.getName()) || '__test__';
const latestVersion = (await plugin.getLatestVersion()) || '1.0.0';
const latestTag = plugin.config.getContext('latestTag');
const changelog = (await plugin.getChangelog(latestVersion)) || null;
const increment = getIncrement(plugin);
plugin.config.setContext({ name, latestVersion, latestTag, changelog });
const { preRelease } = plugin.config.options;
const isPreRelease = Boolean(preRelease);
const preReleaseId = typeof preRelease === 'string' ? preRelease : null;
const version = await getVersion(plugin, { latestVersion, increment, isPreRelease, preReleaseId });
plugin.config.setContext(parseVersion(version));
await plugin.beforeBump();
await plugin.bump(version);
const tagName = plugin.config.getContext('tagName') || version;
plugin.config.setContext({ tagName });
await plugin.beforeRelease();
await plugin.release();
await plugin.afterRelease();
return {
name,
latestVersion,
version
};
};

11
node_modules/release-it/test/util/mock.js generated vendored Normal file
View File

@@ -0,0 +1,11 @@
import { MockServer, FetchMocker } from 'mentoss';
export const mockFetch = baseUrls => {
const servers = [baseUrls].flat().map(url => new MockServer(url));
const mocker = new FetchMocker({
servers
});
return [mocker, ...servers];
};

18
node_modules/release-it/test/util/sh.js generated vendored Normal file
View File

@@ -0,0 +1,18 @@
import { spawnSync } from 'node:child_process';
const getCommandAndArgs = input => {
if (typeof input !== 'string' || !input.trim()) {
throw new Error('Invalid input: expected a non-empty string.');
}
const [command, ...args] = input.trim().split(/\s+/);
return [command, args];
};
const exec = (command, opts = { stdio: 'inherit' }) => {
const [cmd, args] = getCommandAndArgs(command);
return spawnSync(cmd, args, opts);
};
export default { getCommandAndArgs, exec };

145
node_modules/release-it/test/utils.js generated vendored Normal file
View File

@@ -0,0 +1,145 @@
import { EOL } from 'node:os';
import test from 'node:test';
import assert from 'node:assert/strict';
import { stripVTControlCharacters } from 'node:util';
import mockStdIo from 'mock-stdio';
import { format, truncateLines, parseGitUrl, parseVersion, get } from '../lib/util.js';
test('format', () => {
assert.equal(format('release v${version}', { version: '1.0.0' }), 'release v1.0.0');
assert.equal(format('release v${version} (${name})', { version: '1.0.0', name: 'foo' }), 'release v1.0.0 (foo)');
assert.equal(format('release v${version} (${name})', { version: '1.0.0', name: 'foo' }), 'release v1.0.0 (foo)');
});
test('format (throw)', () => {
mockStdIo.start();
assert.throws(() => format('release v${foo}', { version: '1.0.0' }), /foo is not defined/);
const { stdout, stderr } = mockStdIo.end();
assert.equal(stdout, '');
assert.match(
stripVTControlCharacters(stderr),
/ERROR Unable to render template with context:\s+release v\${foo}\s+{"version":"1\.0\.0"}\s+ERROR ReferenceError: foo is not defined/
);
});
test('truncateLines', () => {
const input = `1${EOL}2${EOL}3${EOL}4${EOL}5${EOL}6`;
assert.equal(truncateLines(input), input);
assert.equal(truncateLines(input, 3), `1${EOL}2${EOL}3${EOL}...and 3 more`);
assert.equal(truncateLines(input, 1, '...'), `1...`);
});
test('parseGitUrl', () => {
assert.deepEqual(parseGitUrl('https://github.com/webpro/release-it.git'), {
host: 'github.com',
owner: 'webpro',
project: 'release-it',
protocol: 'https',
remote: 'https://github.com/webpro/release-it.git',
repository: 'webpro/release-it'
});
assert.deepEqual(parseGitUrl('git@gitlab.com:org/sub-group/repo-in-sub-group.git'), {
host: 'gitlab.com',
owner: 'org/sub-group',
project: 'repo-in-sub-group',
protocol: 'ssh',
remote: 'git@gitlab.com:org/sub-group/repo-in-sub-group.git',
repository: 'org/sub-group/repo-in-sub-group'
});
assert.deepEqual(parseGitUrl('git@github.com:org/example.com.git'), {
host: 'github.com',
owner: 'org',
project: 'example.com',
protocol: 'ssh',
remote: 'git@github.com:org/example.com.git',
repository: 'org/example.com'
});
assert.deepEqual(parseGitUrl('file://Users/john/doe/owner/project'), {
host: 'users',
owner: 'owner',
project: 'project',
protocol: 'file',
remote: 'file://users/john/doe/owner/project',
repository: 'owner/project'
});
assert.deepEqual(parseGitUrl('/Users/john/doe/owner/project'), {
host: 'users',
owner: 'owner',
project: 'project',
protocol: 'file',
remote: 'file://users/john/doe/owner/project',
repository: 'owner/project'
});
assert.deepEqual(parseGitUrl('C:\\\\Users\\john\\doe\\owner\\project'), {
host: 'users',
owner: 'owner',
project: 'project',
protocol: 'file',
remote: 'file://users/john/doe/owner/project',
repository: 'owner/project'
});
});
test('parseVersion', () => {
assert.deepEqual(parseVersion(), { version: undefined, isPreRelease: false, preReleaseId: null });
assert.deepEqual(parseVersion(0), { version: '0.0.0', isPreRelease: false, preReleaseId: null });
assert.deepEqual(parseVersion(1), { version: '1.0.0', isPreRelease: false, preReleaseId: null });
assert.deepEqual(parseVersion('1'), { version: '1.0.0', isPreRelease: false, preReleaseId: null });
assert.deepEqual(parseVersion('1.0'), { version: '1.0.0', isPreRelease: false, preReleaseId: null });
assert.deepEqual(parseVersion('1.0.0'), { version: '1.0.0', isPreRelease: false, preReleaseId: null });
assert.deepEqual(parseVersion('1.0.0-0'), { version: '1.0.0-0', isPreRelease: true, preReleaseId: null });
assert.deepEqual(parseVersion('1.0.0-next.1'), { version: '1.0.0-next.1', isPreRelease: true, preReleaseId: 'next' });
assert.deepEqual(parseVersion('21.04.1'), { version: '21.04.1', isPreRelease: false, preReleaseId: null });
});
const sample = {
root: {
level1: {
level2: {
value: 'nested'
},
array: [
{ id: 1, data: 'first' },
{ id: 2, data: 'second' }
],
'key.with.dot': {
special: true
}
},
mixed: [{ deep: { value: 100 } }, { deep: { value: 200 } }]
}
};
test('get: accesses a simple nested property', () => {
assert.equal(get(sample, 'root.level1.level2.value'), 'nested');
});
test('get: accesses array elements by index', () => {
assert.equal(get(sample, 'root.level1.array[0].data'), 'first');
assert.equal(get(sample, 'root.level1.array[1].id'), 2);
});
test('get: accesses keys with dots using bracket notation', () => {
assert.equal(get(sample, 'root.level1["key.with.dot"].special'), true);
});
test('get: navigates mixed objects and arrays', () => {
assert.equal(get(sample, 'root.mixed[0].deep.value'), 100);
assert.equal(get(sample, 'root.mixed[1].deep.value'), 200);
});
test('get: returns default value for non-existent properties', () => {
assert.equal(get(sample, 'root.level1.unknown', 'default'), 'default');
assert.equal(get(sample, 'root.level1.array[10].id', null), null);
});
test('get: handles empty path and null/undefined objects', () => {
assert.equal(get(sample, '', 'default'), 'default');
assert.equal(get(null, 'any.path', 'default'), 'default');
assert.equal(get(undefined, 'any.path', 'default'), 'default');
});

217
node_modules/release-it/test/version.js generated vendored Normal file
View File

@@ -0,0 +1,217 @@
import test, { describe } from 'node:test';
import assert from 'node:assert/strict';
import Version from '../lib/plugin/version/Version.js';
import { factory, runTasks } from './util/index.js';
describe('version', () => {
test('isValidVersion', async () => {
const v = await factory(Version);
assert.equal(v.isValid('1.0.0'), true);
assert.equal(v.isValid(1.0), false);
});
test('isPreRelease', async () => {
const v = await factory(Version);
assert.equal(v.isPreRelease('1.0.0-beta.0'), true);
assert.equal(v.isPreRelease('1.0.0'), false);
});
test('should return the same version in both interactive and ci mode', async () => {
const v = await factory(Version);
const options = { latestVersion: '2.0.0-beta.1', increment: null, preReleaseId: 'rc', isPreRelease: true };
const resultInteractiveMode = await v.getIncrementedVersion(options);
assert.equal(resultInteractiveMode, '2.0.0-rc.0');
const resultCiMode = v.getIncrementedVersionCI(options);
assert.equal(resultInteractiveMode, resultCiMode);
});
test('should increment latest version', async () => {
const v = await factory(Version);
const latestVersion = '1.0.0';
assert.equal(v.incrementVersion({ latestVersion, increment: false }), '1.0.0');
assert.equal(v.incrementVersion({ latestVersion, increment: 'foo' }), undefined);
assert.equal(v.incrementVersion({ latestVersion, increment: 'patsj' }), undefined);
assert.equal(v.incrementVersion({ latestVersion, increment: 'a.b.c' }), undefined);
assert.equal(v.incrementVersion({ latestVersion, increment: '0.9.0' }), undefined);
assert.equal(v.incrementVersion({ latestVersion, increment: '1.1.0' }), '1.1.0');
assert.equal(v.incrementVersion({ latestVersion, increment: 'major' }), '2.0.0');
assert.equal(v.incrementVersion({ latestVersion, increment: '2.0.0-beta.1' }), '2.0.0-beta.1');
});
test('should not increment latest version in interactive mode', async () => {
const v = await factory(Version, { options: { ci: false } });
const latestVersion = '1.0.0';
assert.equal(v.incrementVersion({ latestVersion, increment: null }), undefined);
assert.equal(v.incrementVersion({ latestVersion, increment: false }), '1.0.0');
});
test('should always set increment version in CI mode', async () => {
const v = await factory(Version, { options: { ci: true } });
const latestVersion = '1.0.0';
assert.equal(v.getIncrementedVersionCI({ latestVersion, increment: false }), '1.0.0');
assert.equal(v.getIncrementedVersionCI({ latestVersion, increment: null }), '1.0.1');
assert.equal(v.getIncrementedVersionCI({ latestVersion, increment: '1.1.0' }), '1.1.0');
assert.equal(v.getIncrementedVersionCI({ latestVersion, increment: 'major' }), '2.0.0');
});
test('should increment latest version (coerce)', async () => {
const v = await factory(Version);
assert.equal(v.incrementVersion({ increment: '1.2' }), '1.2.0');
assert.equal(v.incrementVersion({ increment: '1' }), '1.0.0');
assert.equal(v.incrementVersion({ increment: 'v1.2.0.0' }), '1.2.0');
});
test('should increment version (pre-release continuation)', async () => {
const v = await factory(Version);
assert.equal(v.incrementVersion({ latestVersion: '1.2.3-alpha.0', increment: 'prepatch' }), '1.2.4-0');
});
test('should increment version (prepatch)', async () => {
const v = await factory(Version);
assert.equal(
v.incrementVersion({ latestVersion: '1.2.3', increment: 'prepatch', preReleaseId: 'alpha' }),
'1.2.4-alpha.0'
);
});
test('should increment version (normalized)', async () => {
const v = await factory(Version);
assert.equal(
v.incrementVersion({ latestVersion: '1.2.3', increment: 'patch', preReleaseId: 'alpha', isPreRelease: true }),
'1.2.4-alpha.0'
);
});
test('should increment version (prepatch on prerelease version)', async () => {
const v = await factory(Version);
assert.equal(
v.incrementVersion({ latestVersion: '1.2.3-alpha.5', increment: 'prepatch', preReleaseId: 'next' }),
'1.2.4-next.0'
);
});
test('should increment version (normalized on prerelease version)', async () => {
const v = await factory(Version);
assert.equal(
v.incrementVersion({
latestVersion: '1.2.3-alpha.5',
increment: 'patch',
preReleaseId: 'next',
isPreRelease: true
}),
'1.2.4-next.0'
);
});
test('should increment version (prerelease)', async () => {
const v = await factory(Version);
assert.equal(
v.incrementVersion({ latestVersion: '1.2.3', increment: 'prerelease', preReleaseId: 'alpha' }),
'1.2.4-alpha.0'
);
});
test('should increment version (prerelease cont.)', async () => {
const v = await factory(Version);
assert.equal(v.incrementVersion({ latestVersion: '1.2.3-alpha.0', increment: 'prerelease' }), '1.2.3-alpha.1');
});
test('should increment version (preReleaseId continuation)', async () => {
const v = await factory(Version);
assert.equal(
v.incrementVersion({ latestVersion: '1.2.3-alpha.0', increment: 'prerelease', preReleaseId: 'alpha' }),
'1.2.3-alpha.1'
);
});
test('should increment version (prepatch/preReleaseId continuation)', async () => {
const v = await factory(Version);
const options = {
latestVersion: '1.2.3-beta.0',
increment: 'prerelease',
preReleaseId: 'beta',
isPreRelease: true
};
assert.equal(v.incrementVersion(options), '1.2.3-beta.1');
});
test('should increment version (preReleaseId w/o preRelease)', async () => {
const v = await factory(Version);
assert.equal(
v.incrementVersion({ latestVersion: '1.2.3-alpha.0', increment: 'patch', preReleaseId: 'alpha' }),
'1.2.3'
);
});
test('should increment version (non-numeric prepatch continuation)', async () => {
const v = await factory(Version);
assert.equal(v.incrementVersion({ latestVersion: '1.2.3-alpha', increment: 'prerelease' }), '1.2.3-alpha.0');
});
test('should increment version (patch release after pre-release)', async () => {
const v = await factory(Version);
assert.equal(v.incrementVersion({ latestVersion: '1.2.3-alpha.1', increment: 'patch' }), '1.2.3');
});
test('should increment version and start at base 1', async () => {
const v = await factory(Version);
assert.equal(
v.incrementVersion({
latestVersion: '1.3.0',
increment: 'major',
isPreRelease: true,
preReleaseId: 'beta',
preReleaseBase: '1'
}),
'2.0.0-beta.1'
);
});
test('should increment prerelease version and ignore prelease base 1', async () => {
const v = await factory(Version);
assert.equal(
v.incrementVersion({
latestVersion: '1.2.3-alpha.5',
increment: 'prerelease',
preReleaseId: 'alpha',
isPreRelease: true,
preReleaseBase: '1'
}),
'1.2.3-alpha.6'
);
});
test('should run tasks without errors', async t => {
const options = { version: { increment: 'minor' } };
const v = await factory(Version, { options });
const getIncrement = t.mock.method(v, 'getIncrement');
const getIncrementedVersionCI = t.mock.method(v, 'getIncrementedVersionCI');
const incrementVersion = t.mock.method(v, 'incrementVersion');
await runTasks(v);
assert.equal(getIncrement.mock.callCount(), 1);
assert.deepEqual(getIncrement.mock.calls[0].arguments[0], { increment: 'minor' });
assert.equal(getIncrementedVersionCI.mock.callCount(), 1);
assert.deepEqual(getIncrementedVersionCI.mock.calls[0].arguments[0], {
latestVersion: '1.0.0',
increment: 'minor',
isPreRelease: false,
preReleaseId: null
});
assert.equal(await incrementVersion.mock.calls[0].result, '1.1.0');
assert.equal(incrementVersion.mock.callCount(), 1);
assert.deepEqual(incrementVersion.mock.calls[0].arguments[0], {
latestVersion: '1.0.0',
increment: 'minor',
isPreRelease: false,
preReleaseId: null
});
assert.equal(incrementVersion.mock.calls[0].result, '1.1.0');
const { latestVersion, version, isPreRelease, preReleaseId } = v.config.getContext();
assert.equal(latestVersion, '1.0.0');
assert.equal(version, '1.1.0');
assert.equal(isPreRelease, false);
assert.equal(preReleaseId, null);
});
});