8000 feat: pass `cwd` and `env` context to plugins · semantic-release/semantic-release@a94e08d · GitHub
[go: up one dir, main page]

Skip to content

Commit a94e08d

Browse files
committed
feat: pass cwd and env context to plugins
- Allow to run semantic-release (via API) from anywhere passing the current working directory. - Allows to simplify the tests and to run them in parallel in both the core and plugins.
1 parent 12e4155 commit a94e08d

32 files changed

+1352
-1323
lines changed

index.js

Lines changed: 45 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
const process = require('process');
12
const {template} = require('lodash');
23
const marked = require('marked');
34
const TerminalRenderer = require('marked-terminal');
@@ -19,47 +20,47 @@ const {COMMIT_NAME, COMMIT_EMAIL} = require('./lib/definitions/constants');
1920

2021
marked.setOptions({renderer: new TerminalRenderer()});
2122

22-
async function run(options, plugins) {
23-
const {isCi, branch, isPr} = envCi();
23+
async function run(context, plugins) {
24+
const {isCi, branch: ciBranch, isPr} = envCi();
25+
const {cwd, env, options, logger} = context;
2426

2527
if (!isCi && !options.dryRun && !options.noCi) {
2628
logger.log('This run was not triggered in a known CI environment, running in dry-run mode.');
2729
options.dryRun = true;
2830
} else {
2931
// When running on CI, set the commits author and commiter info and prevent the `git` CLI to prompt for username/password. See #703.
30-
process.env = {
32+
Object.assign(env, {
3133
GIT_AUTHOR_NAME: COMMIT_NAME,
3234
GIT_AUTHOR_EMAIL: COMMIT_EMAIL,
3335
GIT_COMMITTER_NAME: COMMIT_NAME,
3436
GIT_COMMITTER_EMAIL: COMMIT_EMAIL,
35-
...process.env,
3637
GIT_ASKPASS: 'echo',
3738
GIT_TERMINAL_PROMPT: 0,
38-
};
39+
});
3940
}
4041

4142
if (isCi && isPr && !options.noCi) {
4243
logger.log("This run was triggered by a pull request and therefore a new version won't be published.");
4344
return;
4445
}
4546

46-
if (branch !== options.branch) {
47+
if (ciBranch !== options.branch) {
4748
logger.log(
48-
`This test run was triggered on the branch ${branch}, while semantic-release is configured to only publish from ${
49+
`This test run was triggered on the branch ${ciBranch}, while semantic-release is configured to only publish from ${
4950
options.branch
5051
}, therefore a new version won’t be published.`
5152
);
5253
return false;
5354
}
5455

55-
await verify(options);
56+
await verify(context);
5657

57-
options.repositoryUrl = await getGitAuthUrl(options);
58+
options.repositoryUrl = await getGitAuthUrl(context);
5859

5960
try {
60-
await verifyAuth(options.repositoryUrl, options.branch);
61+
await verifyAuth(options.repositoryUrl, options.branch, {cwd, env});
6162
} catch (err) {
62-
if (!(await isBranchUpToDate(options.branch))) {
63+
if (!(await isBranchUpToDate(options.branch, {cwd, env}))) {
6364
logger.log(
6465
"The local branch %s is behind the remote one, therefore a new version won't be published.",
6566
options.branch
@@ -72,92 +73,93 @@ async function run(options, plugins) {
7273

7374
logger.log('Run automated release from branch %s', options.branch);
7475

75-
await plugins.verifyConditions({options, logger});
76+
await plugins.verifyConditions(context);
7677

77-
await fetch(options.repositoryUrl);
78+
await fetch(options.repositoryUrl, {cwd, env});
7879

79-
const lastRelease = await getLastRelease(options.tagFormat, logger);
80-
const commits = await getCommits(lastRelease.gitHead, options.branch, logger);
80+
context.lastRelease = await getLastRelease(context);
81+
context.commits = await getCommits(context);
8182

82-
const type = await plugins.analyzeCommits({options, logger, lastRelease, commits});
83-
if (!type) {
83+
const nextRelease = {type: await plugins.analyzeCommits(context), gitHead: await getGitHead({cwd, env})};
84+
85+
if (!nextRelease.type) {
8486
logger.log('There are no relevant changes, so no new version is released.');
8587
return;
8688
}
87-
const version = getNextVersion(type, lastRelease, logger);
88-
const nextRelease = {type, version, gitHead: await getGitHead(), gitTag: template(options.tagFormat)({version})};
89-
90-
await plugins.verifyRelease({options, logger, lastRelease, commits, nextRelease});
89+
context.nextRelease = nextRelease;
90+
nextRelease.version = getNextVersion(context);
91+
nextRelease.gitTag = template(options.tagFormat)({version: nextRelease.version});
9192

92-
const generateNotesParam = {options, logger, lastRelease, commits, nextRelease};
93+
await plugins.verifyRelease(context);
9394

9495
if (options.dryRun) {
95-
const notes = await plugins.generateNotes(generateNotesParam);
96+
const notes = await plugins.generateNotes(context);
9697
logger.log('Release note for version %s:\n', nextRelease.version);
9798
if (notes) {
98-
process.stdout.write(`${marked(notes)}\n`);
99+
logger.stdout(`${marked(notes)}\n`);
99100
}
100101
} else {
101-
nextRelease.notes = await plugins.generateNotes(generateNotesParam);
102-
await plugins.prepare({options, logger, lastRelease, commits, nextRelease});
102+
nextRelease.notes = await plugins.generateNotes(context);
103+
await plugins.prepare(context);
103104

104105
// Create the tag before calling the publish plugins as some require the tag to exists
105106
logger.log('Create tag %s', nextRelease.gitTag);
106-
await tag(nextRelease.gitTag);
107-
await push(options.repositoryUrl, branch);
107+
await tag(nextRelease.gitTag, {cwd, env});
108+
await push(options.repositoryUrl, options.branch, {cwd, env});
108109

109-
const releases = await plugins.publish({options, logger, lastRelease, commits, nextRelease});
110+
context.releases = await plugins.publish(context);
110111

111-
await plugins.success({options, logger, lastRelease, commits, nextRelease, releases});
112+
await plugins.success(context);
112113

113114
logger.log('Published release: %s', nextRelease.version);
114115
}
115116
return true;
116117
}
117118

118-
function logErrors(err) {
119+
function logErrors({logger}, err) {
119120
const errors = extractErrors(err).sort(error => (error.semanticRelease ? -1 : 0));
120121
for (const error of errors) {
121122
if (error.semanticRelease) {
122123
logger.log(`%s ${error.message}`, error.code);
123124
if (error.details) {
124-
process.stdout.write(`${marked(error.details)}\n`);
125+
logger.stderr(`${marked(error.details)}\n`);
125126
}
126127
} else {
127128
logger.error('An error occurred while running semantic-release: %O', error);
128129
}
129130
}
130131
}
131132

132-
async function callFail(plugins, options, error) {
133+
async function callFail(context, plugins, error) {
133134
const errors = extractErrors(error).filter(error => error.semanticRelease);
134135
if (errors.length > 0) {
135136
try {
136-
await plugins.fail({options, logger, errors});
137+
await plugins.fail({...context, errors});
137138
} catch (err) {
138-
logErrors(err);
139+
logErrors(context, err);
139140
}
140141
}
141142
}
142143

143-
module.exports = async opts => {
144-
logger.log(`Running %s version %s`, pkg.name, pkg.version);
145-
const {unhook} = hookStd({silent: false}, hideSensitive);
144+
module.exports = async (opts, {cwd = process.cwd(), env = process.env} = {}) => {
145+
const context = {cwd, env, logger};
146+
context.logger.log(`Running %s version %s`, pkg.name, pkg.version);
147+
const {unhook} = hookStd({silent: false}, hideSensitive(context.env));
146148
try {
147-
const config = await getConfig(opts, logger);
148-
const {plugins, options} = config;
149+
const {plugins, options} = await getConfig(context, opts);
150+
context.options = options;
149151
try {
150-
const result = await run(options, plugins);
152+
const result = await run(context, plugins);
151153
unhook();
152154
return result;
153155
} catch (err) {
154156
if (!options.dryRun) {
155-
await callFail(plugins, options, err);
157+
await callFail(context, plugins, err);
156158
}
157159
throw err;
158160
}
159161
} catch (err) {
160-
logErrors(err);
162+
logErrors(context, err);
161163
unhook();
162164
throw err;
163165
}

lib/definitions/constants.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,6 @@ const COMMIT_EMAIL = 'semantic-release-bot@martynus.net';
88

99
const RELEASE_NOTES_SEPARATOR = '\n\n';
1010

11-
module.exports = {RELEASE_TYPE, FIRST_RELEASE, COMMIT_NAME, COMMIT_EMAIL, RELEASE_NOTES_SEPARATOR};
11+
const SECRET_REPLACEMENT = '[secure]';
12+
13+
module.exports = {RELEASE_TYPE, FIRST_RELEASE, COMMIT_NAME, COMMIT_EMAIL, RELEASE_NOTES_SEPARATOR, SECRET_REPLACEMENT};

lib/definitions/plugins.js

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,8 @@ module.exports = {
3030
configValidator: conf => !conf || (isArray(conf) ? conf : [conf]).every(conf => validatePluginConfig(conf)),
3131
outputValidator: output => !output || isString(output),
3232
pipelineConfig: () => ({
33-
getNextInput: ({nextRelease, ...generateNotesParam}, notes) => ({
34-
...generateNotesParam,
33+
getNextInput: ({nextRelease, ...context}, notes) => ({
34+
...context,
3535
nextRelease: {
3636
...nextRelease,
3737
notes: `${nextRelease.notes ? `${nextRelease.notes}${RELEASE_NOTES_SEPARATOR}` : ''}${notes}`,
@@ -44,17 +44,17 @@ module.exports = {
4444
default: ['@semantic-release/npm'],
4545
configValidator: conf => !conf || (isArray(conf) ? conf : [conf]).every(conf => validatePluginConfig(conf)),
4646
pipelineConfig: ({generateNotes}, logger) => ({
47-
getNextInput: async ({nextRelease, ...prepareParam}) => {
48-
const newGitHead = await gitHead();
47+
getNextInput: async context => {
48+
const newGitHead = await gitHead({cwd: context.cwd});
4949
// If previous prepare plugin has created a commit (gitHead changed)
50-
if (nextRelease.gitHead !== newGitHead) {
51-
nextRelease.gitHead = newGitHead;
50+
if (context.nextRelease.gitHead !== newGitHead) {
51+
context.nextRelease.gitHead = newGitHead;
5252
// Regenerate the release notes
5353
logger.log('Call plugin %s', 'generateNotes');
54-
nextRelease.notes = await generateNotes({nextRelease, ...prepareParam});
54+
context.nextRelease.notes = await generateNotes(context);
5555
}
56-
// Call the next publish plugin with the updated `nextRelease`
57-
return {...prepareParam, nextRelease};
56+
// Call the next prepare plugin with the updated `nextRelease`
57+
return context;
5858
},
5959
}),
6060
},

lib/get-commits.js

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,27 +5,25 @@ const debug = require('debug')('semantic-release:get-commits');
55
/**
66
* Retrieve the list of commits on the current branch since the commit sha associated with the last release, or all the commits of the current branch if there is no last released version.
77
*
8-
* @param {String} gitHead The commit sha associated with the last release.
9-
* @param {String} branch The branch to release from.
10-
* @param {Object} logger Global logger.
8+
* @param {Object} context semantic-release context.
119
*
1210
* @return {Promise<Array<Object>>} The list of commits on the branch `branch` since the last release.
1311
*/
14-
module.exports = async (gitHead, branch, logger) => {
12+
module.exports = async ({cwd, env, lastRelease: {gitHead}, logger}) => {
1513
if (gitHead) {
1614
debug('Use gitHead: %s', gitHead);
1715
} else {
1816
logger.log('No previous release found, retrieving all commits');
1917
}
2018

2119
Object.assign(gitLogParser.fields, {hash: 'H', message: 'B', gitTags: 'd', committerDate: {key: 'ci', type: Date}});
22-
const commits = (await getStream.array(gitLogParser.parse({_: `${gitHead ? gitHead + '..' : ''}HEAD`}))).map(
23-
commit => {
24-
commit.message = commit.message.trim();
25-
commit.gitTags = commit.gitTags.trim();
26-
return commit;
27-
}
28-
);
20+
const commits = (await getStream.array(
21+
gitLogParser.parse({_: `${gitHead ? gitHead + '..' : ''}HEAD`}, {cwd, env: {...process.env, ...env}})
22+
)).map(commit => {
23+
commit.message = commit.message.trim();
24+
commit.gitTags = commit.gitTags.trim();
25+
return commit;
26+
});
2927
logger.log('Found %s commits since last release', commits.length);
3028
debug('Parsed commits: %o', commits);
3129
return commits;

lib/get-config.js

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,9 @@ const CONFIG_FILES = [
1818
`${CONFIG_NAME}.config.js`,
1919
];
2020

21-
module.exports = async (opts, logger) => {
22-
const {config} = (await cosmiconfig(CONFIG_NAME, {searchPlaces: CONFIG_FILES}).search()) || {};
21+
module.exports = async (context, opts) => {
22+
const {cwd, env} = context;
23+
const {config} = (await cosmiconfig(CONFIG_NAME, {searchPlaces: CONFIG_FILES}).search(cwd)) || {};
2324
// Merge config file options and CLI/API options
2425
let options = {...config, ...opts};
2526
const pluginsPath = {};
@@ -29,8 +30,7 @@ module.exports = async (opts, logger) => {
2930
// If `extends` is defined, load and merge each shareable config with `options`
3031
options = {
3132
...castArray(extendPaths).reduce((result, extendPath) => {
32-
const extendsOpts = require(resolveFrom.silent(__dirname, extendPath) ||
33-
resolveFrom(process.cwd(), extendPath));
33+
const extendsOpts = require(resolveFrom.silent(__dirname, extendPath) || resolveFrom(cwd, extendPath));
3434

3535
// For each plugin defined in a shareable config, save in `pluginsPath` the extendable config path,
3636
// so those plugin will be loaded relatively to the config file
@@ -55,18 +55,18 @@ module.exports = async (opts, logger) => {
5555
// Set default options values if not defined yet
5656
options = {
5757
branch: 'master',
58-
repositoryUrl: (await pkgRepoUrl()) || (await repoUrl()),
58+
repositoryUrl: (await pkgRepoUrl({normalize: false, cwd})) || (await repoUrl({cwd, env})),
5959
tagFormat: `v\${version}`,
6060
// Remove `null` and `undefined` options so they can be replaced with default ones
6161
...pickBy(options, option => !isUndefined(option) && !isNull(option)),
6262
};
6363

6464
debug('options values: %O', options);
6565

66-
return {options, plugins: await plugins(options, pluginsPath, logger)};
66+
return {options, plugins: await plugins({...context, options}, pluginsPath)};
6767
};
6868

69-
async function pkgRepoUrl() {
70-
const {pkg} = await readPkgUp({normalize: false});
69+
async function pkgRepoUrl(opts) {
70+
const {pkg} = await readPkgUp(opts);
7171
return pkg && (isPlainObject(pkg.repository) ? pkg.repository.url : pkg.repository);
7272
}

lib/get-git-auth-url.js

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,11 @@ const GIT_TOKENS = {
2121
*
2222
* In addition, expand shortcut URLs (`owner/repo` => `https://github.com/owner/repo.git`) and transform `git+https` / `git+http` URLs to `https` / `http`.
2323
*
24-
* @param {String} repositoryUrl The user provided Git repository URL.
24+
* @param {Object} context semantic-release context.
25+
*
2526
* @return {String} The formatted Git repository URL.
2627
*/
27-
module.exports = async ({repositoryUrl, branch}) => {
28+
module.exports = async ({cwd, env, options: {repositoryUrl, branch}}) => {
2829
const info = hostedGitInfo.fromUrl(repositoryUrl, {noGitPlus: true});
2930

3031
if (info && info.getDefaultRepresentation() === 'shortcut') {
@@ -41,10 +42,10 @@ module.exports = async ({repositoryUrl, branch}) => {
4142

4243
// Test if push is allowed without transforming the URL (e.g. is ssh keys are set up)
4344
try {
44-
await verifyAuth(repositoryUrl, branch);
45+
await verifyAuth(repositoryUrl, branch, {cwd, env});
4546
} catch (err) {
46-
const envVar = Object.keys(GIT_TOKENS).find(envVar => !isUndefined(process.env[envVar]));
47-
const gitCredentials = `${GIT_TOKENS[envVar] || ''}${process.env[envVar] || ''}`;
47+
const envVar = Object.keys(GIT_TOKENS).find(envVar => !isUndefined(env[envVar]));
48+
const gitCredentials = `${GIT_TOKENS[envVar] || ''}${env[envVar] || ''}`;
4849
const {protocols, ...parsed} = gitUrlParse(repositoryUrl);
4950
const protocol = protocols.includes('https') ? 'https' : protocols.includes('http') ? 'http' : 'https';
5051

lib/get-last-release.js

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -20,18 +20,17 @@ const {gitTags, isRefInHistory, gitTagHead} = require('./git');
2020
* - Sort the versions
2121
* - Retrive the highest version
2222
*
23-
* @param {String} tagFormat Git tag format.
24-
* @param {Object} logger Global logger.
23+
* @param {Object} context semantic-release context.
24+
*
2525
* @return {Promise<LastRelease>} The last tagged release or `undefined` if none is found.
2626
*/
27-
module.exports = async (tagFormat, logger) => {
27+
module.exports = async ({cwd, env, options: {tagFormat}, logger}) => {
2828
// Generate a regex to parse tags formatted with `tagFormat`
2929
// by replacing the `version` variable in the template by `(.+)`.
3030
// The `tagFormat` is compiled with space as the `version` as it's an invalid tag character,
3131
// so it's guaranteed to no be present in the `tagFormat`.
3232
const tagRegexp = `^${escapeRegExp(template(tagFormat)({version: ' '})).replace(' ', '(.+)')}`;
33-
34-
const tags = (await gitTags())
33+
const tags = (await gitTags({cwd, env}))
3534
.map(tag => ({gitTag: tag, version: (tag.match(tagRegexp) || new Array(2))[1]}))
3635
.filter(
3736
tag => tag.version && semver.valid(semver.clean(tag.version)) && !semver.prerelease(semver.clean(tag.version))
@@ -40,11 +39,11 @@ module.exports = async (tagFormat, logger) => {
4039

4140
debug('found tags: %o', tags);
4241

43-
const tag = await pLocate(tags, tag => isRefInHistory(tag.gitTag), {concurrency: 1, preserveOrder: true});
42+
const tag = await pLocate(tags, tag => isRefInHistory(tag.gitTag, {cwd, env}), {preserveOrder: true});
4443

4544
if (tag) {
4645
logger.log('Found git tag %s associated with version %s', tag.gitTag, tag.version);
47-
return {gitHead: await gitTagHead(tag.gitTag), ...tag};
46+
return {gitHead: await gitTagHead(tag.gitTag, {cwd, env}), ...tag};
4847
}
4948

5049
logger.log('No git tag version found');

lib/get-next-version.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
const semver = require('semver');
22
const {FIRST_RELEASE} = require('./definitions/constants');
33

4-
module.exports = (type, lastRelease, logger) => {
4+
module.exports = ({nextRelease: {type}, lastRelease, logger}) => {
55
let version;
66
if (lastRelease.version) {
77
version = semver.inc(lastRelease.version, type);

0 commit comments

Comments
 (0)
0