release-it 0.0.0-pl.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +421 -0
- package/bin/release-it.js +42 -0
- package/config/release-it.json +70 -0
- package/lib/cli.js +44 -0
- package/lib/config.js +139 -0
- package/lib/index.js +152 -0
- package/lib/log.js +69 -0
- package/lib/plugin/GitBase.js +125 -0
- package/lib/plugin/GitRelease.js +58 -0
- package/lib/plugin/Plugin.js +73 -0
- package/lib/plugin/factory.js +89 -0
- package/lib/plugin/git/Git.js +220 -0
- package/lib/plugin/git/prompts.js +19 -0
- package/lib/plugin/github/GitHub.js +403 -0
- package/lib/plugin/github/prompts.js +16 -0
- package/lib/plugin/github/util.js +39 -0
- package/lib/plugin/gitlab/GitLab.js +277 -0
- package/lib/plugin/gitlab/prompts.js +9 -0
- package/lib/plugin/npm/npm.js +281 -0
- package/lib/plugin/npm/prompts.js +12 -0
- package/lib/plugin/version/Version.js +129 -0
- package/lib/prompt.js +33 -0
- package/lib/shell.js +91 -0
- package/lib/spinner.js +29 -0
- package/lib/util.js +109 -0
- package/package.json +122 -0
- package/test/cli.js +20 -0
- package/test/config.js +144 -0
- package/test/git.init.js +250 -0
- package/test/git.js +358 -0
- package/test/github.js +487 -0
- package/test/gitlab.js +252 -0
- package/test/log.js +143 -0
- package/test/npm.js +417 -0
- package/test/plugin-name.js +9 -0
- package/test/plugins.js +238 -0
- package/test/prompt.js +97 -0
- package/test/resources/file-v2.0.1.txt +1 -0
- package/test/resources/file-v2.0.2.txt +1 -0
- package/test/resources/file1 +1 -0
- package/test/shell.js +74 -0
- package/test/spinner.js +58 -0
- package/test/stub/config/default/.release-it.json +5 -0
- package/test/stub/config/invalid-config-rc +1 -0
- package/test/stub/config/invalid-config-txt +2 -0
- package/test/stub/config/merge/.release-it.json +5 -0
- package/test/stub/config/merge/package.json +7 -0
- package/test/stub/config/toml/.release-it.toml +2 -0
- package/test/stub/config/yaml/.release-it.yaml +2 -0
- package/test/stub/config/yml/.release-it.yml +2 -0
- package/test/stub/github.js +130 -0
- package/test/stub/gitlab.js +44 -0
- package/test/stub/plugin-context.js +36 -0
- package/test/stub/plugin-replace.js +9 -0
- package/test/stub/plugin.js +39 -0
- package/test/stub/shell.js +24 -0
- package/test/tasks.interactive.js +208 -0
- package/test/tasks.js +585 -0
- package/test/util/helpers.js +32 -0
- package/test/util/index.js +78 -0
- package/test/util/setup.js +5 -0
- package/test/utils.js +97 -0
- package/test/version.js +173 -0
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import got from 'got';
|
|
4
|
+
import { globby } from 'globby';
|
|
5
|
+
import { FormData, fileFromSync } from 'node-fetch';
|
|
6
|
+
import allSettled from 'promise.allsettled';
|
|
7
|
+
import _ from 'lodash';
|
|
8
|
+
import Release from '../GitRelease.js';
|
|
9
|
+
import { format, e } from '../../util.js';
|
|
10
|
+
import prompts from './prompts.js';
|
|
11
|
+
|
|
12
|
+
const docs = 'https://git.io/release-it-gitlab';
|
|
13
|
+
|
|
14
|
+
const noop = Promise.resolve();
|
|
15
|
+
|
|
16
|
+
class GitLab extends Release {
|
|
17
|
+
constructor(...args) {
|
|
18
|
+
super(...args);
|
|
19
|
+
this.registerPrompts(prompts);
|
|
20
|
+
this.assets = [];
|
|
21
|
+
const { certificateAuthorityFile } = this.options;
|
|
22
|
+
this.certificateAuthorityOption = certificateAuthorityFile
|
|
23
|
+
? { https: { certificateAuthority: fs.readFileSync(certificateAuthorityFile) } }
|
|
24
|
+
: {};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
get client() {
|
|
28
|
+
if (this._client) return this._client;
|
|
29
|
+
const { tokenHeader } = this.options;
|
|
30
|
+
const { baseUrl } = this.getContext();
|
|
31
|
+
this._client = got.extend({
|
|
32
|
+
prefixUrl: baseUrl,
|
|
33
|
+
method: 'POST',
|
|
34
|
+
headers: {
|
|
35
|
+
'user-agent': 'webpro/release-it',
|
|
36
|
+
[tokenHeader]: this.token
|
|
37
|
+
},
|
|
38
|
+
...this.certificateAuthorityOption
|
|
39
|
+
});
|
|
40
|
+
return this._client;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async init() {
|
|
44
|
+
await super.init();
|
|
45
|
+
|
|
46
|
+
const { skipChecks, tokenRef, tokenHeader } = this.options;
|
|
47
|
+
const { repo } = this.getContext();
|
|
48
|
+
const hasJobToken = (tokenHeader || '').toLowerCase() === 'job-token';
|
|
49
|
+
const origin = this.options.origin || `https://${repo.host}`;
|
|
50
|
+
this.setContext({
|
|
51
|
+
id: encodeURIComponent(repo.repository),
|
|
52
|
+
origin,
|
|
53
|
+
baseUrl: `${origin}/api/v4`
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
if (skipChecks) return;
|
|
57
|
+
|
|
58
|
+
if (!this.token) {
|
|
59
|
+
throw e(`Environment variable "${tokenRef}" is required for GitLab releases.`, docs);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (!hasJobToken) {
|
|
63
|
+
if (!(await this.isAuthenticated())) {
|
|
64
|
+
throw e(`Could not authenticate with GitLab using environment variable "${tokenRef}".`, docs);
|
|
65
|
+
}
|
|
66
|
+
if (!(await this.isCollaborator())) {
|
|
67
|
+
const { user, repo } = this.getContext();
|
|
68
|
+
throw e(`User ${user.username} is not a collaborator for ${repo.repository}.`, docs);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async isAuthenticated() {
|
|
74
|
+
if (this.config.isDryRun) return true;
|
|
75
|
+
const endpoint = `user`;
|
|
76
|
+
try {
|
|
77
|
+
const { id, username } = await this.request(endpoint, { method: 'GET' });
|
|
78
|
+
this.setContext({ user: { id, username } });
|
|
79
|
+
return true;
|
|
80
|
+
} catch (err) {
|
|
81
|
+
this.debug(err);
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async isCollaborator() {
|
|
87
|
+
if (this.config.isDryRun) return true;
|
|
88
|
+
const { id, user } = this.getContext();
|
|
89
|
+
const endpoint = `projects/${id}/members/all/${user.id}`;
|
|
90
|
+
try {
|
|
91
|
+
const { access_level } = await this.request(endpoint, { method: 'GET' });
|
|
92
|
+
return access_level && access_level >= 30;
|
|
93
|
+
} catch (err) {
|
|
94
|
+
this.debug(err);
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async beforeRelease() {
|
|
100
|
+
await super.beforeRelease();
|
|
101
|
+
await this.checkReleaseMilestones();
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async checkReleaseMilestones() {
|
|
105
|
+
if (this.options.skipChecks) return;
|
|
106
|
+
|
|
107
|
+
const releaseMilestones = this.getReleaseMilestones();
|
|
108
|
+
if (releaseMilestones.length < 1) {
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
this.log.exec(`gitlab releases#checkReleaseMilestones`);
|
|
113
|
+
|
|
114
|
+
const { id } = this.getContext();
|
|
115
|
+
const endpoint = `projects/${id}/milestones`;
|
|
116
|
+
const requests = releaseMilestones.map(milestone => {
|
|
117
|
+
const options = {
|
|
118
|
+
method: 'GET',
|
|
119
|
+
searchParams: {
|
|
120
|
+
title: milestone,
|
|
121
|
+
include_parent_milestones: true
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
return this.request(endpoint, options).then(response => {
|
|
125
|
+
if (!Array.isArray(response)) {
|
|
126
|
+
const { baseUrl } = this.getContext();
|
|
127
|
+
throw new Error(
|
|
128
|
+
`Unexpected response from ${baseUrl}/${endpoint}. Expected an array but got: ${JSON.stringify(response)}`
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
if (response.length === 0) {
|
|
132
|
+
const error = new Error(`Milestone '${milestone}' does not exist.`);
|
|
133
|
+
this.log.warn(error.message);
|
|
134
|
+
throw error;
|
|
135
|
+
}
|
|
136
|
+
this.log.verbose(`gitlab releases#checkReleaseMilestones: milestone '${milestone}' exists`);
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
try {
|
|
140
|
+
await allSettled(requests).then(results => {
|
|
141
|
+
for (const result of results) {
|
|
142
|
+
if (result.status === 'rejected') {
|
|
143
|
+
throw e('Missing one or more milestones in GitLab. Creating a GitLab release will fail.', docs);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
} catch (err) {
|
|
148
|
+
this.debug(err);
|
|
149
|
+
throw err;
|
|
150
|
+
}
|
|
151
|
+
this.log.verbose('gitlab releases#checkReleaseMilestones: done');
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
getReleaseMilestones() {
|
|
155
|
+
const { milestones } = this.options;
|
|
156
|
+
return (milestones || []).map(milestone => format(milestone, this.config.getContext()));
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
async release() {
|
|
160
|
+
const glRelease = () => this.createRelease();
|
|
161
|
+
const glUploadAssets = () => this.uploadAssets();
|
|
162
|
+
|
|
163
|
+
if (this.config.isCI) {
|
|
164
|
+
await this.step({ enabled: this.options.assets, task: glUploadAssets, label: 'GitLab upload assets' });
|
|
165
|
+
return await this.step({ task: glRelease, label: 'GitLab release' });
|
|
166
|
+
} else {
|
|
167
|
+
const release = () => glUploadAssets().then(() => glRelease());
|
|
168
|
+
return await this.step({ task: release, label: 'GitLab release', prompt: 'release' });
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
async request(endpoint, options) {
|
|
173
|
+
const { baseUrl } = this.getContext();
|
|
174
|
+
this.debug(Object.assign({ url: `${baseUrl}/${endpoint}` }, options));
|
|
175
|
+
const method = (options.method || 'POST').toLowerCase();
|
|
176
|
+
const response = await this.client[method](endpoint, options);
|
|
177
|
+
const body = typeof response.body === 'string' ? JSON.parse(response.body) : response.body || {};
|
|
178
|
+
this.debug(body);
|
|
179
|
+
return body;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
async createRelease() {
|
|
183
|
+
const { releaseName } = this.options;
|
|
184
|
+
const { tagName } = this.config.getContext();
|
|
185
|
+
const { id, releaseNotes, repo, origin } = this.getContext();
|
|
186
|
+
const { isDryRun } = this.config;
|
|
187
|
+
const name = format(releaseName, this.config.getContext());
|
|
188
|
+
const description = releaseNotes || '-';
|
|
189
|
+
const releaseUrl = `${origin}/${repo.repository}/-/releases`;
|
|
190
|
+
const releaseMilestones = this.getReleaseMilestones();
|
|
191
|
+
|
|
192
|
+
this.log.exec(`gitlab releases#createRelease "${name}" (${tagName})`, { isDryRun });
|
|
193
|
+
|
|
194
|
+
if (isDryRun) {
|
|
195
|
+
this.setContext({ isReleased: true, releaseUrl });
|
|
196
|
+
return true;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const endpoint = `projects/${id}/releases`;
|
|
200
|
+
const options = {
|
|
201
|
+
json: {
|
|
202
|
+
name,
|
|
203
|
+
tag_name: tagName,
|
|
204
|
+
description
|
|
205
|
+
}
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
if (this.assets.length) {
|
|
209
|
+
options.json.assets = {
|
|
210
|
+
links: this.assets
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (releaseMilestones.length) {
|
|
215
|
+
options.json.milestones = releaseMilestones;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
try {
|
|
219
|
+
await this.request(endpoint, options);
|
|
220
|
+
this.log.verbose('gitlab releases#createRelease: done');
|
|
221
|
+
this.setContext({ isReleased: true, releaseUrl });
|
|
222
|
+
this.config.setContext({ isReleased: true, releaseUrl });
|
|
223
|
+
return true;
|
|
224
|
+
} catch (err) {
|
|
225
|
+
this.debug(err);
|
|
226
|
+
throw err;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
async uploadAsset(filePath) {
|
|
231
|
+
const name = path.basename(filePath);
|
|
232
|
+
const { id, origin, repo } = this.getContext();
|
|
233
|
+
const endpoint = `projects/${id}/uploads`;
|
|
234
|
+
|
|
235
|
+
const body = new FormData();
|
|
236
|
+
body.set('file', fileFromSync(filePath));
|
|
237
|
+
const options = { body };
|
|
238
|
+
|
|
239
|
+
try {
|
|
240
|
+
const body = await this.request(endpoint, options);
|
|
241
|
+
this.log.verbose(`gitlab releases#uploadAsset: done (${body.url})`);
|
|
242
|
+
this.assets.push({
|
|
243
|
+
name,
|
|
244
|
+
url: `${origin}/${repo.repository}${body.url}`
|
|
245
|
+
});
|
|
246
|
+
} catch (err) {
|
|
247
|
+
this.debug(err);
|
|
248
|
+
throw err;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
uploadAssets() {
|
|
253
|
+
const { assets } = this.options;
|
|
254
|
+
const { isDryRun } = this.config;
|
|
255
|
+
const context = this.config.getContext();
|
|
256
|
+
|
|
257
|
+
const patterns = _.castArray(assets).map(pattern => format(pattern, context));
|
|
258
|
+
|
|
259
|
+
this.log.exec('gitlab releases#uploadAssets', patterns, { isDryRun });
|
|
260
|
+
|
|
261
|
+
if (!assets) {
|
|
262
|
+
return noop;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
return globby(patterns).then(files => {
|
|
266
|
+
if (!files.length) {
|
|
267
|
+
this.log.warn(`gitlab releases#uploadAssets: could not find "${assets}" relative to ${process.cwd()}`);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
if (isDryRun) return Promise.resolve();
|
|
271
|
+
|
|
272
|
+
return Promise.all(files.map(filePath => this.uploadAsset(filePath)));
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
export default GitLab;
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import semver from 'semver';
|
|
3
|
+
import urlJoin from 'url-join';
|
|
4
|
+
import Plugin from '../Plugin.js';
|
|
5
|
+
import { hasAccess, rejectAfter, parseVersion, readJSON, e } from '../../util.js';
|
|
6
|
+
import prompts from './prompts.js';
|
|
7
|
+
|
|
8
|
+
const docs = 'https://git.io/release-it-npm';
|
|
9
|
+
|
|
10
|
+
const options = { write: false };
|
|
11
|
+
|
|
12
|
+
const MANIFEST_PATH = './package.json';
|
|
13
|
+
const DEFAULT_TAG = 'latest';
|
|
14
|
+
const DEFAULT_TAG_PRERELEASE = 'next';
|
|
15
|
+
const NPM_BASE_URL = 'https://www.npmjs.com';
|
|
16
|
+
const NPM_PUBLIC_PATH = '/package';
|
|
17
|
+
|
|
18
|
+
const fixArgs = args => (args ? (typeof args === 'string' ? args.split(' ') : args) : []);
|
|
19
|
+
|
|
20
|
+
class npm extends Plugin {
|
|
21
|
+
static isEnabled(options) {
|
|
22
|
+
return hasAccess(MANIFEST_PATH) && options !== false;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
constructor(...args) {
|
|
26
|
+
super(...args);
|
|
27
|
+
this.registerPrompts(prompts);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async init() {
|
|
31
|
+
const { name, version: latestVersion, private: isPrivate, publishConfig } = readJSON(path.resolve(MANIFEST_PATH));
|
|
32
|
+
this.setContext({ name, latestVersion, private: isPrivate, publishConfig });
|
|
33
|
+
this.config.setContext({ npm: { name } });
|
|
34
|
+
|
|
35
|
+
const { publish, skipChecks } = this.options;
|
|
36
|
+
|
|
37
|
+
const timeout = Number(this.options.timeout) * 1000;
|
|
38
|
+
|
|
39
|
+
if (publish === false || isPrivate) return;
|
|
40
|
+
|
|
41
|
+
if (skipChecks) return;
|
|
42
|
+
|
|
43
|
+
const validations = Promise.all([this.isRegistryUp(), this.isAuthenticated(), this.getLatestRegistryVersion()]);
|
|
44
|
+
|
|
45
|
+
await Promise.race([validations, rejectAfter(timeout, e(`Timed out after ${timeout}ms.`, docs))]);
|
|
46
|
+
|
|
47
|
+
const [isRegistryUp, isAuthenticated, latestVersionInRegistry] = await validations;
|
|
48
|
+
|
|
49
|
+
if (!isRegistryUp) {
|
|
50
|
+
throw e(`Unable to reach npm registry (timed out after ${timeout}ms).`, docs);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (!isAuthenticated) {
|
|
54
|
+
throw e('Not authenticated with npm. Please `npm login` and try again.', docs);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (!(await this.isCollaborator())) {
|
|
58
|
+
const { username } = this.getContext();
|
|
59
|
+
throw e(`User ${username} is not a collaborator for ${name}.`, docs);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (!latestVersionInRegistry) {
|
|
63
|
+
this.log.warn('No version found in npm registry. Assuming new package.');
|
|
64
|
+
} else {
|
|
65
|
+
if (!semver.eq(latestVersion, latestVersionInRegistry)) {
|
|
66
|
+
this.log.warn(
|
|
67
|
+
`Latest version in registry (${latestVersionInRegistry}) does not match package.json (${latestVersion}).`
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
getName() {
|
|
74
|
+
return this.getContext('name');
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
getLatestVersion() {
|
|
78
|
+
return this.options.ignoreVersion ? null : this.getContext('latestVersion');
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async bump(version) {
|
|
82
|
+
const tag = this.options.tag || (await this.resolveTag(version));
|
|
83
|
+
this.setContext({ version, tag });
|
|
84
|
+
|
|
85
|
+
if (!this.config.isIncrement) return false;
|
|
86
|
+
|
|
87
|
+
const { versionArgs, allowSameVersion } = this.options;
|
|
88
|
+
const args = [version, '--no-git-tag-version', allowSameVersion && '--allow-same-version', ...fixArgs(versionArgs)];
|
|
89
|
+
const task = () => this.exec(`npm version ${args.filter(Boolean).join(' ')}`);
|
|
90
|
+
return this.spinner.show({ task, label: 'npm version' });
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
release() {
|
|
94
|
+
if (this.options.publish === false) return false;
|
|
95
|
+
if (this.getContext('private')) return false;
|
|
96
|
+
const publish = () => this.publish({ otpCallback });
|
|
97
|
+
const otpCallback =
|
|
98
|
+
this.config.isCI && !this.config.isPromptOnlyVersion ? null : task => this.step({ prompt: 'otp', task });
|
|
99
|
+
return this.step({ task: publish, label: 'npm publish', prompt: 'publish' });
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
isRegistryUp() {
|
|
103
|
+
const registry = this.getRegistry();
|
|
104
|
+
const registryArg = registry ? ` --registry ${registry}` : '';
|
|
105
|
+
return this.exec(`npm ping${registryArg}`, { options }).then(
|
|
106
|
+
() => true,
|
|
107
|
+
err => {
|
|
108
|
+
if (/code E40[04]|404.*(ping not found|No content for path)/.test(err)) {
|
|
109
|
+
this.log.warn('Ignoring response from unsupported `npm ping` command.');
|
|
110
|
+
return true;
|
|
111
|
+
}
|
|
112
|
+
return false;
|
|
113
|
+
}
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
isAuthenticated() {
|
|
118
|
+
const registry = this.getRegistry();
|
|
119
|
+
const registryArg = registry ? ` --registry ${registry}` : '';
|
|
120
|
+
return this.exec(`npm whoami${registryArg}`, { options }).then(
|
|
121
|
+
output => {
|
|
122
|
+
const username = output ? output.trim() : null;
|
|
123
|
+
this.setContext({ username });
|
|
124
|
+
return true;
|
|
125
|
+
},
|
|
126
|
+
err => {
|
|
127
|
+
this.debug(err);
|
|
128
|
+
if (/code E40[04]/.test(err)) {
|
|
129
|
+
this.log.warn('Ignoring response from unsupported `npm whoami` command.');
|
|
130
|
+
return true;
|
|
131
|
+
}
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
async isCollaborator() {
|
|
138
|
+
const registry = this.getRegistry();
|
|
139
|
+
const registryArg = registry ? ` --registry ${registry}` : '';
|
|
140
|
+
const name = this.getName();
|
|
141
|
+
const { username } = this.getContext();
|
|
142
|
+
if (username === undefined) return true;
|
|
143
|
+
if (username === null) return false;
|
|
144
|
+
|
|
145
|
+
try {
|
|
146
|
+
let npmVersion = await this.exec('npm --version', { options });
|
|
147
|
+
|
|
148
|
+
let accessCommand;
|
|
149
|
+
if (semver.gt(npmVersion, '9.0.0')) {
|
|
150
|
+
accessCommand = 'npm access list collaborators --json';
|
|
151
|
+
} else {
|
|
152
|
+
accessCommand = 'npm access ls-collaborators';
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const output = await this.exec(`${accessCommand} ${name}${registryArg}`, { options });
|
|
156
|
+
|
|
157
|
+
try {
|
|
158
|
+
const collaborators = JSON.parse(output);
|
|
159
|
+
const permissions = collaborators[username];
|
|
160
|
+
return permissions && permissions.includes('write');
|
|
161
|
+
} catch (err) {
|
|
162
|
+
this.debug(err);
|
|
163
|
+
return false;
|
|
164
|
+
}
|
|
165
|
+
} catch (err) {
|
|
166
|
+
this.debug(err);
|
|
167
|
+
if (/code E400/.test(err)) {
|
|
168
|
+
this.log.warn('Ignoring response from unsupported `npm access` command.');
|
|
169
|
+
} else {
|
|
170
|
+
this.log.warn(`Unable to verify if user ${username} is a collaborator for ${name}.`);
|
|
171
|
+
}
|
|
172
|
+
return true;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
async getLatestRegistryVersion() {
|
|
177
|
+
const registry = this.getRegistry();
|
|
178
|
+
const registryArg = registry ? ` --registry ${registry}` : '';
|
|
179
|
+
const name = this.getName();
|
|
180
|
+
const latestVersion = this.getLatestVersion();
|
|
181
|
+
const tag = await this.resolveTag(latestVersion);
|
|
182
|
+
return this.exec(`npm show ${name}@${tag} version${registryArg}`, { options }).catch(() => null);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
getRegistryPreReleaseTags() {
|
|
186
|
+
return this.exec(`npm view ${this.getName()} dist-tags --json`, { options }).then(
|
|
187
|
+
output => {
|
|
188
|
+
try {
|
|
189
|
+
const tags = JSON.parse(output);
|
|
190
|
+
return Object.keys(tags).filter(tag => tag !== DEFAULT_TAG);
|
|
191
|
+
} catch (err) {
|
|
192
|
+
this.debug(err);
|
|
193
|
+
return [];
|
|
194
|
+
}
|
|
195
|
+
},
|
|
196
|
+
() => []
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
getPackageUrl() {
|
|
201
|
+
const baseUrl = this.getRegistry() || NPM_BASE_URL;
|
|
202
|
+
const publicPath = this.getPublicPath() || NPM_PUBLIC_PATH;
|
|
203
|
+
return urlJoin(baseUrl, publicPath, this.getName());
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
getRegistry() {
|
|
207
|
+
const { publishConfig } = this.getContext();
|
|
208
|
+
const registries = publishConfig
|
|
209
|
+
? publishConfig.registry
|
|
210
|
+
? [publishConfig.registry]
|
|
211
|
+
: Object.keys(publishConfig)
|
|
212
|
+
.filter(key => key.endsWith('registry'))
|
|
213
|
+
.map(key => publishConfig[key])
|
|
214
|
+
: [];
|
|
215
|
+
return registries[0];
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
getPublicPath() {
|
|
219
|
+
const { publishConfig } = this.getContext();
|
|
220
|
+
return (publishConfig && publishConfig.publicPath) ?? '';
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
async guessPreReleaseTag() {
|
|
224
|
+
const [tag] = await this.getRegistryPreReleaseTags();
|
|
225
|
+
if (tag) {
|
|
226
|
+
return tag;
|
|
227
|
+
} else {
|
|
228
|
+
this.log.warn(`Unable to get pre-release tag(s) from npm registry. Using "${DEFAULT_TAG_PRERELEASE}".`);
|
|
229
|
+
return DEFAULT_TAG_PRERELEASE;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
async resolveTag(version) {
|
|
234
|
+
const { tag } = this.options;
|
|
235
|
+
const { isPreRelease, preReleaseId } = parseVersion(version);
|
|
236
|
+
if (!isPreRelease) {
|
|
237
|
+
return DEFAULT_TAG;
|
|
238
|
+
} else {
|
|
239
|
+
return tag || preReleaseId || (await this.guessPreReleaseTag());
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
async publish({ otp = this.options.otp, otpCallback } = {}) {
|
|
244
|
+
const { publishPath = '.', publishArgs } = this.options;
|
|
245
|
+
const { private: isPrivate, tag = DEFAULT_TAG } = this.getContext();
|
|
246
|
+
const otpArg = otp ? `--otp ${otp}` : '';
|
|
247
|
+
const dryRunArg = this.config.isDryRun ? '--dry-run' : '';
|
|
248
|
+
const registry = this.getRegistry();
|
|
249
|
+
const registryArg = registry ? `--registry ${registry}` : '';
|
|
250
|
+
if (isPrivate) {
|
|
251
|
+
this.log.warn('Skip publish: package is private.');
|
|
252
|
+
return false;
|
|
253
|
+
}
|
|
254
|
+
const args = [publishPath, `--tag ${tag}`, otpArg, dryRunArg, registryArg, ...fixArgs(publishArgs)].filter(Boolean);
|
|
255
|
+
return this.exec(`npm publish ${args.join(' ')}`, { options })
|
|
256
|
+
.then(() => {
|
|
257
|
+
this.setContext({ isReleased: true });
|
|
258
|
+
})
|
|
259
|
+
.catch(err => {
|
|
260
|
+
this.debug(err);
|
|
261
|
+
if (/one-time pass/.test(err)) {
|
|
262
|
+
if (otp != null) {
|
|
263
|
+
this.log.warn('The provided OTP is incorrect or has expired.');
|
|
264
|
+
}
|
|
265
|
+
if (otpCallback) {
|
|
266
|
+
return otpCallback(otp => this.publish({ otp, otpCallback }));
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
throw err;
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
afterRelease() {
|
|
274
|
+
const { isReleased } = this.getContext();
|
|
275
|
+
if (isReleased) {
|
|
276
|
+
this.log.log(`🔗 ${this.getPackageUrl()}`);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
export default npm;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export default {
|
|
2
|
+
publish: {
|
|
3
|
+
type: 'confirm',
|
|
4
|
+
message: context =>
|
|
5
|
+
`Publish ${context.npm.name}${context.npm.tag === 'latest' ? '' : `@${context.npm.tag}`} to npm?`,
|
|
6
|
+
default: true
|
|
7
|
+
},
|
|
8
|
+
otp: {
|
|
9
|
+
type: 'input',
|
|
10
|
+
message: () => `Please enter OTP for npm:`
|
|
11
|
+
}
|
|
12
|
+
};
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import semver from 'semver';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import Plugin from '../Plugin.js';
|
|
4
|
+
|
|
5
|
+
const { green, red, redBright } = chalk;
|
|
6
|
+
|
|
7
|
+
const RELEASE_TYPES = ['patch', 'minor', 'major'];
|
|
8
|
+
const PRERELEASE_TYPES = ['prepatch', 'preminor', 'premajor'];
|
|
9
|
+
const CONTINUATION_TYPES = ['prerelease', 'pre'];
|
|
10
|
+
const ALL_RELEASE_TYPES = [...RELEASE_TYPES, ...PRERELEASE_TYPES, ...CONTINUATION_TYPES];
|
|
11
|
+
|
|
12
|
+
const CHOICES = {
|
|
13
|
+
latestIsPreRelease: [...RELEASE_TYPES, CONTINUATION_TYPES[0]],
|
|
14
|
+
preRelease: PRERELEASE_TYPES,
|
|
15
|
+
default: [...RELEASE_TYPES, ...PRERELEASE_TYPES]
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const getIncrementChoices = context => {
|
|
19
|
+
const { latestIsPreRelease, isPreRelease, preReleaseId } = context.version;
|
|
20
|
+
const types = latestIsPreRelease ? CHOICES.latestIsPreRelease : isPreRelease ? CHOICES.preRelease : CHOICES.default;
|
|
21
|
+
const choices = types.map(increment => ({
|
|
22
|
+
name: `${increment} (${semver.inc(context.latestVersion, increment, preReleaseId)})`,
|
|
23
|
+
value: increment
|
|
24
|
+
}));
|
|
25
|
+
const otherChoice = {
|
|
26
|
+
name: 'Other, please specify...',
|
|
27
|
+
value: null
|
|
28
|
+
};
|
|
29
|
+
return [...choices, otherChoice];
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const versionTransformer = context => input =>
|
|
33
|
+
semver.valid(input) ? (semver.gt(input, context.latestVersion) ? green(input) : red(input)) : redBright(input);
|
|
34
|
+
|
|
35
|
+
const prompts = {
|
|
36
|
+
incrementList: {
|
|
37
|
+
type: 'list',
|
|
38
|
+
message: () => 'Select increment (next version):',
|
|
39
|
+
choices: context => getIncrementChoices(context),
|
|
40
|
+
pageSize: 9
|
|
41
|
+
},
|
|
42
|
+
version: {
|
|
43
|
+
type: 'input',
|
|
44
|
+
message: () => `Please enter a valid version:`,
|
|
45
|
+
transformer: context => versionTransformer(context),
|
|
46
|
+
validate: input => !!semver.valid(input) || 'The version must follow the semver standard.'
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
class Version extends Plugin {
|
|
51
|
+
constructor(...args) {
|
|
52
|
+
super(...args);
|
|
53
|
+
this.registerPrompts(prompts);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
getIncrement(options) {
|
|
57
|
+
return options.increment;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
getIncrementedVersionCI(options) {
|
|
61
|
+
return this.incrementVersion(options);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async getIncrementedVersion(options) {
|
|
65
|
+
const { isCI } = this.config;
|
|
66
|
+
const version = this.incrementVersion(options);
|
|
67
|
+
return version || (isCI ? null : await this.promptIncrementVersion(options));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
promptIncrementVersion(options) {
|
|
71
|
+
return new Promise(resolve => {
|
|
72
|
+
this.step({
|
|
73
|
+
prompt: 'incrementList',
|
|
74
|
+
task: increment =>
|
|
75
|
+
increment
|
|
76
|
+
? resolve(this.incrementVersion(Object.assign({}, options, { increment })))
|
|
77
|
+
: this.step({ prompt: 'version', task: resolve })
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
isPreRelease(version) {
|
|
83
|
+
return Boolean(semver.prerelease(version));
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
isValid(version) {
|
|
87
|
+
return Boolean(semver.valid(version));
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
incrementVersion({ latestVersion, increment, isPreRelease, preReleaseId }) {
|
|
91
|
+
if (increment === false) return latestVersion;
|
|
92
|
+
|
|
93
|
+
const latestIsPreRelease = this.isPreRelease(latestVersion);
|
|
94
|
+
const isValidVersion = this.isValid(increment);
|
|
95
|
+
|
|
96
|
+
if (latestVersion) {
|
|
97
|
+
this.setContext({ latestIsPreRelease });
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (isValidVersion && semver.gte(increment, latestVersion)) {
|
|
101
|
+
return increment;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (isPreRelease && !increment && latestIsPreRelease) {
|
|
105
|
+
return semver.inc(latestVersion, 'prerelease', preReleaseId);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (this.config.isCI && !increment) {
|
|
109
|
+
if (isPreRelease) {
|
|
110
|
+
return semver.inc(latestVersion, 'prepatch', preReleaseId);
|
|
111
|
+
} else {
|
|
112
|
+
return semver.inc(latestVersion, 'patch');
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const normalizedType = RELEASE_TYPES.includes(increment) && isPreRelease ? `pre${increment}` : increment;
|
|
117
|
+
if (ALL_RELEASE_TYPES.includes(normalizedType)) {
|
|
118
|
+
return semver.inc(latestVersion, normalizedType, preReleaseId);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const coercedVersion = !isValidVersion && semver.coerce(increment);
|
|
122
|
+
if (coercedVersion) {
|
|
123
|
+
this.log.warn(`Coerced invalid semver version "${increment}" into "${coercedVersion}".`);
|
|
124
|
+
return coercedVersion.toString();
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export default Version;
|