UNPKG

6.09 kBJavaScriptView Raw
1const {template, pick} = require('lodash');
2const marked = require('marked');
3const TerminalRenderer = require('marked-terminal');
4const envCi = require('env-ci');
5const hookStd = require('hook-std');
6const pkg = require('./package.json');
7const hideSensitive = require('./lib/hide-sensitive');
8const getConfig = require('./lib/get-config');
9const verify = require('./lib/verify');
10const getNextVersion = require('./lib/get-next-version');
11const getCommits = require('./lib/get-commits');
12const getLastRelease = require('./lib/get-last-release');
13const {extractErrors} = require('./lib/utils');
14const getGitAuthUrl = require('./lib/get-git-auth-url');
15const getLogger = require('./lib/get-logger');
16const {fetch, verifyAuth, isBranchUpToDate, getGitHead, tag, push} = require('./lib/git');
17const getError = require('./lib/get-error');
18const {COMMIT_NAME, COMMIT_EMAIL} = require('./lib/definitions/constants');
19
20marked.setOptions({renderer: new TerminalRenderer()});
21
22async function run(context, plugins) {
23 const {cwd, env, options, logger} = context;
24 const {isCi, branch: ciBranch, isPr} = envCi({env, cwd});
25
26 if (!isCi && !options.dryRun && !options.noCi) {
27 logger.warn('This run was not triggered in a known CI environment, running in dry-run mode.');
28 options.dryRun = true;
29 } else {
30 // When running on CI, set the commits author and commiter info and prevent the `git` CLI to prompt for username/password. See #703.
31 Object.assign(env, {
32 GIT_AUTHOR_NAME: COMMIT_NAME,
33 GIT_AUTHOR_EMAIL: COMMIT_EMAIL,
34 GIT_COMMITTER_NAME: COMMIT_NAME,
35 GIT_COMMITTER_EMAIL: COMMIT_EMAIL,
36 ...env,
37 GIT_ASKPASS: 'echo',
38 GIT_TERMINAL_PROMPT: 0,
39 });
40 }
41
42 if (isCi && isPr && !options.noCi) {
43 logger.log("This run was triggered by a pull request and therefore a new version won't be published.");
44 return false;
45 }
46
47 if (ciBranch !== options.branch) {
48 logger.log(
49 `This test run was triggered on the branch ${ciBranch}, while semantic-release is configured to only publish from ${
50 options.branch
51 }, therefore a new version won’t be published.`
52 );
53 return false;
54 }
55
56 logger[options.dryRun ? 'warn' : 'success'](
57 `Run automated release from branch ${ciBranch}${options.dryRun ? ' in dry-run mode' : ''}`
58 );
59
60 await verify(context);
61
62 options.repositoryUrl = await getGitAuthUrl(context);
63
64 try {
65 try {
66 await verifyAuth(options.repositoryUrl, options.branch, {cwd, env});
67 } catch (error) {
68 if (!(await isBranchUpToDate(options.branch, {cwd, env}))) {
69 logger.log(
70 `The local branch ${options.branch} is behind the remote one, therefore a new version won't be published.`
71 );
72 return false;
73 }
74
75 throw error;
76 }
77 } catch (error) {
78 logger.error(`The command "${error.cmd}" failed with the error message ${error.stderr}.`);
79 throw getError('EGITNOPERMISSION', {options});
80 }
81
82 logger.success(`Allowed to push to the Git repository`);
83
84 await plugins.verifyConditions(context);
85
86 await fetch(options.repositoryUrl, {cwd, env});
87
88 context.lastRelease = await getLastRelease(context);
89 context.commits = await getCommits(context);
90
91 const nextRelease = {type: await plugins.analyzeCommits(context), gitHead: await getGitHead({cwd, env})};
92
93 if (!nextRelease.type) {
94 logger.log('There are no relevant changes, so no new version is released.');
95 return false;
96 }
97
98 context.nextRelease = nextRelease;
99 nextRelease.version = getNextVersion(context);
100 nextRelease.gitTag = template(options.tagFormat)({version: nextRelease.version});
101
102 await plugins.verifyRelease(context);
103
104 nextRelease.notes = await plugins.generateNotes(context);
105
106 await plugins.prepare(context);
107
108 if (options.dryRun) {
109 logger.warn(`Skip ${nextRelease.gitTag} tag creation in dry-run mode`);
110 } else {
111 // Create the tag before calling the publish plugins as some require the tag to exists
112 await tag(nextRelease.gitTag, {cwd, env});
113 await push(options.repositoryUrl, {cwd, env});
114 logger.success(`Created tag ${nextRelease.gitTag}`);
115 }
116
117 context.releases = await plugins.publish(context);
118
119 await plugins.success(context);
120
121 logger.success(`Published release ${nextRelease.version}`);
122
123 if (options.dryRun) {
124 logger.log(`Release note for version ${nextRelease.version}:`);
125 if (nextRelease.notes) {
126 context.stdout.write(marked(nextRelease.notes));
127 }
128 }
129
130 return pick(context, ['lastRelease', 'commits', 'nextRelease', 'releases']);
131}
132
133function logErrors({logger, stderr}, err) {
134 const errors = extractErrors(err).sort(error => (error.semanticRelease ? -1 : 0));
135 for (const error of errors) {
136 if (error.semanticRelease) {
137 logger.error(`${error.code} ${error.message}`);
138 if (error.details) {
139 stderr.write(marked(error.details));
140 }
141 } else {
142 logger.error('An error occurred while running semantic-release: %O', error);
143 }
144 }
145}
146
147async function callFail(context, plugins, err) {
148 const errors = extractErrors(err).filter(err => err.semanticRelease);
149 if (errors.length > 0) {
150 try {
151 await plugins.fail({...context, errors});
152 } catch (error) {
153 logErrors(context, error);
154 }
155 }
156}
157
158module.exports = async (opts = {}, {cwd = process.cwd(), env = process.env, stdout, stderr} = {}) => {
159 const {unhook} = hookStd(
160 {silent: false, streams: [process.stdout, process.stderr, stdout, stderr].filter(Boolean)},
161 hideSensitive(env)
162 );
163 const context = {cwd, env, stdout: stdout || process.stdout, stderr: stderr || process.stderr};
164 context.logger = getLogger(context);
165 context.logger.log(`Running ${pkg.name} version ${pkg.version}`);
166 try {
167 const {plugins, options} = await getConfig(context, opts);
168 context.options = options;
169 try {
170 const result = await run(context, plugins);
171 unhook();
172 return result;
173 } catch (error) {
174 await callFail(context, plugins, error);
175 throw error;
176 }
177 } catch (error) {
178 logErrors(context, error);
179 unhook();
180 throw error;
181 }
182};