nx 21.0.0-canary.20250429-cf4a1f3 → 21.0.0-canary.20250501-8f50358
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/migrations.json +16 -1
- package/package.json +11 -11
- package/release/changelog-renderer/index.d.ts +7 -7
- package/release/changelog-renderer/index.js +12 -31
- package/schemas/nx-schema.json +8 -3
- package/src/command-line/migrate/migrate-ui-api.d.ts +2 -1
- package/src/command-line/migrate/migrate-ui-api.js +4 -3
- package/src/command-line/migrate/migrate.d.ts +12 -6
- package/src/command-line/migrate/migrate.js +31 -9
- package/src/command-line/release/changelog.d.ts +3 -2
- package/src/command-line/release/changelog.js +57 -70
- package/src/command-line/release/command-object.d.ts +1 -1
- package/src/command-line/release/config/config.d.ts +8 -1
- package/src/command-line/release/config/config.js +18 -11
- package/src/command-line/release/release.js +30 -18
- package/src/command-line/release/utils/git.d.ts +1 -0
- package/src/command-line/release/utils/git.js +27 -8
- package/src/command-line/release/utils/remote-release-clients/github.d.ts +57 -0
- package/src/command-line/release/utils/remote-release-clients/github.js +309 -0
- package/src/command-line/release/utils/remote-release-clients/gitlab.d.ts +62 -0
- package/src/command-line/release/utils/remote-release-clients/gitlab.js +271 -0
- package/src/command-line/release/utils/remote-release-clients/remote-release-client.d.ts +111 -0
- package/src/command-line/release/utils/remote-release-clients/remote-release-client.js +136 -0
- package/src/command-line/repair/repair.js +8 -2
- package/src/command-line/report/report.js +1 -1
- package/src/command-line/yargs-utils/shared-options.d.ts +1 -1
- package/src/command-line/yargs-utils/shared-options.js +22 -3
- package/src/config/misc-interfaces.d.ts +9 -1
- package/src/config/nx-json.d.ts +8 -5
- package/src/core/graph/main.js +1 -1
- package/src/core/graph/styles.css +1 -1
- package/src/devkit-exports.d.ts +1 -1
- package/src/migrations/update-21-0-0/release-changelog-config-changes.d.ts +2 -0
- package/src/migrations/update-21-0-0/release-changelog-config-changes.js +38 -0
- package/src/migrations/update-21-0-0/remove-custom-tasks-runner.d.ts +2 -0
- package/src/migrations/update-21-0-0/remove-custom-tasks-runner.js +38 -0
- package/src/migrations/update-21-0-0/remove-legacy-cache.d.ts +2 -0
- package/src/migrations/update-21-0-0/remove-legacy-cache.js +17 -0
- package/src/native/index.d.ts +6 -1
- package/src/native/native-bindings.js +1 -0
- package/src/native/native-file-cache-location.js +2 -1
- package/src/native/nx.wasm32-wasi.wasm +0 -0
- package/src/project-graph/plugins/get-plugins.js +19 -14
- package/src/tasks-runner/batch/run-batch.js +1 -1
- package/src/tasks-runner/cache.d.ts +1 -2
- package/src/tasks-runner/cache.js +2 -18
- package/src/tasks-runner/is-tui-enabled.d.ts +16 -1
- package/src/tasks-runner/is-tui-enabled.js +40 -28
- package/src/tasks-runner/life-cycles/tui-summary-life-cycle.js +8 -7
- package/src/tasks-runner/pseudo-terminal.d.ts +1 -0
- package/src/tasks-runner/pseudo-terminal.js +11 -1
- package/src/tasks-runner/run-command.js +5 -27
- package/src/tasks-runner/running-tasks/node-child-process.d.ts +1 -0
- package/src/tasks-runner/running-tasks/node-child-process.js +7 -0
- package/src/tasks-runner/task-graph-utils.d.ts +3 -0
- package/src/tasks-runner/task-graph-utils.js +31 -2
- package/src/tasks-runner/task-orchestrator.js +16 -4
- package/src/utils/is-ci.d.ts +1 -1
- package/src/utils/is-ci.js +4 -1
- package/src/utils/package-manager.d.ts +1 -0
- package/src/utils/package-manager.js +29 -16
- package/src/command-line/release/utils/github.d.ts +0 -32
- package/src/command-line/release/utils/github.js +0 -326
@@ -0,0 +1,309 @@
|
|
1
|
+
"use strict";
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
3
|
+
exports.GithubRemoteReleaseClient = exports.defaultCreateReleaseProvider = void 0;
|
4
|
+
const chalk = require("chalk");
|
5
|
+
const enquirer_1 = require("enquirer");
|
6
|
+
const node_child_process_1 = require("node:child_process");
|
7
|
+
const node_fs_1 = require("node:fs");
|
8
|
+
const node_os_1 = require("node:os");
|
9
|
+
const output_1 = require("../../../../utils/output");
|
10
|
+
const path_1 = require("../../../../utils/path");
|
11
|
+
const remote_release_client_1 = require("./remote-release-client");
|
12
|
+
// axios types and values don't seem to match
|
13
|
+
const _axios = require("axios");
|
14
|
+
const axios = _axios;
|
15
|
+
exports.defaultCreateReleaseProvider = {
|
16
|
+
provider: 'github',
|
17
|
+
hostname: 'github.com',
|
18
|
+
apiBaseUrl: 'https://api.github.com',
|
19
|
+
};
|
20
|
+
class GithubRemoteReleaseClient extends remote_release_client_1.RemoteReleaseClient {
|
21
|
+
constructor() {
|
22
|
+
super(...arguments);
|
23
|
+
this.remoteReleaseProviderName = 'GitHub';
|
24
|
+
}
|
25
|
+
/**
|
26
|
+
* Get GitHub repository data from git remote
|
27
|
+
*/
|
28
|
+
static resolveRepoData(createReleaseConfig, remoteName = 'origin') {
|
29
|
+
try {
|
30
|
+
const remoteUrl = (0, node_child_process_1.execSync)(`git remote get-url ${remoteName}`, {
|
31
|
+
encoding: 'utf8',
|
32
|
+
stdio: 'pipe',
|
33
|
+
}).trim();
|
34
|
+
// Use the default provider if custom one is not specified or releases are disabled
|
35
|
+
let hostname = exports.defaultCreateReleaseProvider.hostname;
|
36
|
+
let apiBaseUrl = exports.defaultCreateReleaseProvider.apiBaseUrl;
|
37
|
+
if (createReleaseConfig !== false &&
|
38
|
+
typeof createReleaseConfig !== 'string') {
|
39
|
+
hostname = createReleaseConfig.hostname;
|
40
|
+
apiBaseUrl = createReleaseConfig.apiBaseUrl;
|
41
|
+
}
|
42
|
+
// Extract the 'user/repo' part from the URL
|
43
|
+
const escapedHostname = hostname.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
44
|
+
const regexString = `${escapedHostname}[/:]([\\w.-]+/[\\w.-]+)(\\.git)?`;
|
45
|
+
const regex = new RegExp(regexString);
|
46
|
+
const match = remoteUrl.match(regex);
|
47
|
+
if (match && match[1]) {
|
48
|
+
return {
|
49
|
+
hostname,
|
50
|
+
apiBaseUrl,
|
51
|
+
// Ensure any trailing .git is stripped
|
52
|
+
slug: match[1].replace(/\.git$/, ''),
|
53
|
+
};
|
54
|
+
}
|
55
|
+
else {
|
56
|
+
throw new Error(`Could not extract "user/repo" data from the resolved remote URL: ${remoteUrl}`);
|
57
|
+
}
|
58
|
+
}
|
59
|
+
catch (error) {
|
60
|
+
return null;
|
61
|
+
}
|
62
|
+
}
|
63
|
+
/**
|
64
|
+
* Resolve a GitHub token from environment variables or gh CLI
|
65
|
+
*/
|
66
|
+
static async resolveTokenData(hostname) {
|
67
|
+
// Try and resolve from the environment
|
68
|
+
const tokenFromEnv = process.env.GITHUB_TOKEN || process.env.GH_TOKEN;
|
69
|
+
if (tokenFromEnv) {
|
70
|
+
return { token: tokenFromEnv, headerName: 'Authorization' };
|
71
|
+
}
|
72
|
+
// Try and resolve from gh CLI installation
|
73
|
+
const ghCLIPath = (0, path_1.joinPathFragments)(process.env.XDG_CONFIG_HOME || (0, path_1.joinPathFragments)((0, node_os_1.homedir)(), '.config'), 'gh', 'hosts.yml');
|
74
|
+
if ((0, node_fs_1.existsSync)(ghCLIPath)) {
|
75
|
+
const yamlContents = await node_fs_1.promises.readFile(ghCLIPath, 'utf8');
|
76
|
+
const { load } = require('@zkochan/js-yaml');
|
77
|
+
const ghCLIConfig = load(yamlContents);
|
78
|
+
if (ghCLIConfig[hostname]) {
|
79
|
+
// Web based session (the token is already embedded in the config)
|
80
|
+
if (ghCLIConfig[hostname].oauth_token) {
|
81
|
+
return ghCLIConfig[hostname].oauth_token;
|
82
|
+
}
|
83
|
+
// SSH based session (we need to dynamically resolve a token using the CLI)
|
84
|
+
if (ghCLIConfig[hostname].user &&
|
85
|
+
ghCLIConfig[hostname].git_protocol === 'ssh') {
|
86
|
+
const token = (0, node_child_process_1.execSync)(`gh auth token`, {
|
87
|
+
encoding: 'utf8',
|
88
|
+
stdio: 'pipe',
|
89
|
+
windowsHide: false,
|
90
|
+
}).trim();
|
91
|
+
return { token, headerName: 'Authorization' };
|
92
|
+
}
|
93
|
+
}
|
94
|
+
}
|
95
|
+
if (hostname !== 'github.com') {
|
96
|
+
console.log(`Warning: It was not possible to automatically resolve a GitHub token from your environment for hostname ${hostname}. If you set the GITHUB_TOKEN or GH_TOKEN environment variable, that will be used for GitHub API requests.`);
|
97
|
+
}
|
98
|
+
return null;
|
99
|
+
}
|
100
|
+
createPostGitTask(releaseVersion, changelogContents, dryRun) {
|
101
|
+
return async (latestCommit) => {
|
102
|
+
output_1.output.logSingleLine(`Creating GitHub Release`);
|
103
|
+
await this.createOrUpdateRelease(releaseVersion, changelogContents, latestCommit, { dryRun });
|
104
|
+
};
|
105
|
+
}
|
106
|
+
async applyUsernameToAuthors(authors) {
|
107
|
+
await Promise.all([...authors.keys()].map(async (authorName) => {
|
108
|
+
const meta = authors.get(authorName);
|
109
|
+
for (const email of meta.email) {
|
110
|
+
if (email.endsWith('@users.noreply.github.com')) {
|
111
|
+
const match = email.match(/^(\d+\+)?([^@]+)@users\.noreply\.github\.com$/);
|
112
|
+
if (match && match[2]) {
|
113
|
+
meta.username = match[2];
|
114
|
+
break;
|
115
|
+
}
|
116
|
+
}
|
117
|
+
const { data } = await axios
|
118
|
+
.get(`https://ungh.cc/users/find/${email}`)
|
119
|
+
.catch(() => ({ data: { user: null } }));
|
120
|
+
if (data?.user) {
|
121
|
+
meta.username = data.user.username;
|
122
|
+
break;
|
123
|
+
}
|
124
|
+
}
|
125
|
+
}));
|
126
|
+
}
|
127
|
+
/**
|
128
|
+
* Get a release by tag
|
129
|
+
*/
|
130
|
+
async getReleaseByTag(tag) {
|
131
|
+
const githubRepoData = this.getRequiredRemoteRepoData();
|
132
|
+
return await this.makeRequest(`/repos/${githubRepoData.slug}/releases/tags/${tag}`);
|
133
|
+
}
|
134
|
+
/**
|
135
|
+
* Create a new release
|
136
|
+
*/
|
137
|
+
async createRelease(remoteRelease) {
|
138
|
+
const githubRepoData = this.getRequiredRemoteRepoData();
|
139
|
+
return await this.makeRequest(`/repos/${githubRepoData.slug}/releases`, {
|
140
|
+
method: 'POST',
|
141
|
+
data: remoteRelease,
|
142
|
+
});
|
143
|
+
}
|
144
|
+
async updateRelease(id, remoteRelease) {
|
145
|
+
const githubRepoData = this.getRequiredRemoteRepoData();
|
146
|
+
return await this.makeRequest(`/repos/${githubRepoData.slug}/releases/${id}`, {
|
147
|
+
method: 'PATCH',
|
148
|
+
data: remoteRelease,
|
149
|
+
});
|
150
|
+
}
|
151
|
+
getManualRemoteReleaseURL(remoteReleaseOptions) {
|
152
|
+
const githubRepoData = this.getRequiredRemoteRepoData();
|
153
|
+
// Parameters taken from https://github.com/isaacs/github/issues/1410#issuecomment-442240267
|
154
|
+
let url = `https://${githubRepoData.hostname}/${githubRepoData.slug}/releases/new?tag=${remoteReleaseOptions.version}&title=${remoteReleaseOptions.version}&body=${encodeURIComponent(remoteReleaseOptions.body)}&target=${remoteReleaseOptions.commit}`;
|
155
|
+
if (remoteReleaseOptions.prerelease) {
|
156
|
+
url += '&prerelease=true';
|
157
|
+
}
|
158
|
+
return url;
|
159
|
+
}
|
160
|
+
handleAuthError() {
|
161
|
+
output_1.output.error({
|
162
|
+
title: `Unable to resolve data via the GitHub API. You can use any of the following options to resolve this:`,
|
163
|
+
bodyLines: [
|
164
|
+
'- Set the `GITHUB_TOKEN` or `GH_TOKEN` environment variable to a valid GitHub token with `repo` scope',
|
165
|
+
'- Have an active session via the official gh CLI tool (https://cli.github.com) in your current terminal',
|
166
|
+
],
|
167
|
+
});
|
168
|
+
}
|
169
|
+
logReleaseAction(existingRelease, gitTag, dryRun) {
|
170
|
+
const githubRepoData = this.getRequiredRemoteRepoData();
|
171
|
+
const logTitle = `https://${githubRepoData.hostname}/${githubRepoData.slug}/releases/tag/${gitTag}`;
|
172
|
+
if (existingRelease) {
|
173
|
+
console.error(`${chalk.white('UPDATE')} ${logTitle}${dryRun ? chalk.keyword('orange')(' [dry-run]') : ''}`);
|
174
|
+
}
|
175
|
+
else {
|
176
|
+
console.error(`${chalk.green('CREATE')} ${logTitle}${dryRun ? chalk.keyword('orange')(' [dry-run]') : ''}`);
|
177
|
+
}
|
178
|
+
}
|
179
|
+
async handleError(error, result) {
|
180
|
+
if (error) {
|
181
|
+
process.exitCode = 1;
|
182
|
+
if (error.response?.data) {
|
183
|
+
// There's a nicely formatted error from GitHub we can display to the user
|
184
|
+
output_1.output.error({
|
185
|
+
title: `A GitHub API Error occurred when creating/updating the release`,
|
186
|
+
bodyLines: [
|
187
|
+
`GitHub Error: ${JSON.stringify(error.response.data)}`,
|
188
|
+
`---`,
|
189
|
+
`Request Data:`,
|
190
|
+
`Repo: ${this.getRemoteRepoData()?.slug}`,
|
191
|
+
`Token Header Data: ${this.tokenHeader}`,
|
192
|
+
`Body: ${JSON.stringify(result.requestData)}`,
|
193
|
+
],
|
194
|
+
});
|
195
|
+
}
|
196
|
+
else {
|
197
|
+
console.log(error);
|
198
|
+
console.error(`An unknown error occurred while trying to create a release on GitHub, please report this on https://github.com/nrwl/nx (NOTE: make sure to redact your GitHub token from the error message!)`);
|
199
|
+
}
|
200
|
+
}
|
201
|
+
const shouldContinueInGitHub = await this.promptForContinueInGitHub();
|
202
|
+
if (!shouldContinueInGitHub) {
|
203
|
+
return;
|
204
|
+
}
|
205
|
+
const open = require('open');
|
206
|
+
await open(result.url)
|
207
|
+
.then(() => {
|
208
|
+
console.info(`\nFollow up in the browser to manually create the release:\n\n` +
|
209
|
+
chalk.underline(chalk.cyan(result.url)) +
|
210
|
+
`\n`);
|
211
|
+
})
|
212
|
+
.catch(() => {
|
213
|
+
console.info(`Open this link to manually create a release: \n` +
|
214
|
+
chalk.underline(chalk.cyan(result.url)) +
|
215
|
+
'\n');
|
216
|
+
});
|
217
|
+
}
|
218
|
+
async promptForContinueInGitHub() {
|
219
|
+
try {
|
220
|
+
const reply = await (0, enquirer_1.prompt)([
|
221
|
+
{
|
222
|
+
name: 'open',
|
223
|
+
message: 'Do you want to finish creating the release manually in your browser?',
|
224
|
+
type: 'autocomplete',
|
225
|
+
choices: [
|
226
|
+
{
|
227
|
+
name: 'Yes',
|
228
|
+
hint: 'It will pre-populate the form for you',
|
229
|
+
},
|
230
|
+
{
|
231
|
+
name: 'No',
|
232
|
+
},
|
233
|
+
],
|
234
|
+
initial: 0,
|
235
|
+
},
|
236
|
+
]);
|
237
|
+
return reply.open === 'Yes';
|
238
|
+
}
|
239
|
+
catch {
|
240
|
+
// Ensure the cursor is always restored before exiting
|
241
|
+
process.stdout.write('\u001b[?25h');
|
242
|
+
// Handle the case where the user exits the prompt with ctrl+c
|
243
|
+
process.exit(1);
|
244
|
+
}
|
245
|
+
}
|
246
|
+
/**
|
247
|
+
* Format references for the release (e.g., PRs, issues)
|
248
|
+
*/
|
249
|
+
formatReferences(references) {
|
250
|
+
const githubRepoData = this.getRequiredRemoteRepoData();
|
251
|
+
const providerToRefSpec = {
|
252
|
+
github: { 'pull-request': 'pull', hash: 'commit', issue: 'issues' },
|
253
|
+
};
|
254
|
+
const refSpec = providerToRefSpec.github;
|
255
|
+
const formatSingleReference = (ref) => {
|
256
|
+
return `[${ref.value}](https://${githubRepoData.hostname}/${githubRepoData.slug}/${refSpec[ref.type]}/${ref.value.replace(/^#/, '')})`;
|
257
|
+
};
|
258
|
+
const pr = references.filter((ref) => ref.type === 'pull-request');
|
259
|
+
const issue = references.filter((ref) => ref.type === 'issue');
|
260
|
+
if (pr.length > 0 || issue.length > 0) {
|
261
|
+
return (' (' +
|
262
|
+
[...pr, ...issue].map((ref) => formatSingleReference(ref)).join(', ') +
|
263
|
+
')');
|
264
|
+
}
|
265
|
+
if (references.length > 0) {
|
266
|
+
return ' (' + formatSingleReference(references[0]) + ')';
|
267
|
+
}
|
268
|
+
return '';
|
269
|
+
}
|
270
|
+
async syncRelease(remoteReleaseOptions, existingRelease) {
|
271
|
+
const githubReleaseData = {
|
272
|
+
tag_name: remoteReleaseOptions.version,
|
273
|
+
name: remoteReleaseOptions.version,
|
274
|
+
body: remoteReleaseOptions.body,
|
275
|
+
prerelease: remoteReleaseOptions.prerelease,
|
276
|
+
// legacy specifies that the latest release should be determined based on the release creation date and higher semantic version.
|
277
|
+
make_latest: 'legacy',
|
278
|
+
};
|
279
|
+
try {
|
280
|
+
const newGhRelease = await (existingRelease
|
281
|
+
? this.updateRelease(existingRelease.id, githubReleaseData)
|
282
|
+
: this.createRelease({
|
283
|
+
...githubReleaseData,
|
284
|
+
target_commitish: remoteReleaseOptions.commit,
|
285
|
+
}));
|
286
|
+
return {
|
287
|
+
status: existingRelease ? 'updated' : 'created',
|
288
|
+
id: newGhRelease.id,
|
289
|
+
url: newGhRelease.html_url,
|
290
|
+
};
|
291
|
+
}
|
292
|
+
catch (error) {
|
293
|
+
return {
|
294
|
+
status: 'manual',
|
295
|
+
error,
|
296
|
+
url: this.getManualRemoteReleaseURL(remoteReleaseOptions),
|
297
|
+
requestData: githubReleaseData,
|
298
|
+
};
|
299
|
+
}
|
300
|
+
}
|
301
|
+
getRequiredRemoteRepoData() {
|
302
|
+
const githubRepoData = this.getRemoteRepoData();
|
303
|
+
if (!githubRepoData) {
|
304
|
+
throw new Error(`No remote repo data could be resolved for the current workspace`);
|
305
|
+
}
|
306
|
+
return githubRepoData;
|
307
|
+
}
|
308
|
+
}
|
309
|
+
exports.GithubRemoteReleaseClient = GithubRemoteReleaseClient;
|
@@ -0,0 +1,62 @@
|
|
1
|
+
import type { PostGitTask } from '../../changelog';
|
2
|
+
import type { ResolvedCreateRemoteReleaseProvider } from '../../config/config';
|
3
|
+
import type { Reference } from '../git';
|
4
|
+
import { ReleaseVersion } from '../shared';
|
5
|
+
import { RemoteReleaseClient, RemoteReleaseOptions, RemoteReleaseResult, RemoteRepoData } from './remote-release-client';
|
6
|
+
export interface GitLabRepoData extends RemoteRepoData {
|
7
|
+
projectId: string;
|
8
|
+
}
|
9
|
+
export interface GitLabRelease {
|
10
|
+
id?: string;
|
11
|
+
name?: string;
|
12
|
+
tag_name: string;
|
13
|
+
ref: string;
|
14
|
+
assets?: {
|
15
|
+
links?: {
|
16
|
+
name: string;
|
17
|
+
url: string;
|
18
|
+
direct_asset_path?: string;
|
19
|
+
link_type?: string;
|
20
|
+
}[];
|
21
|
+
};
|
22
|
+
released_at?: string;
|
23
|
+
description?: string;
|
24
|
+
milestones?: string[];
|
25
|
+
prerelease?: boolean;
|
26
|
+
}
|
27
|
+
export declare const defaultCreateReleaseProvider: ResolvedCreateRemoteReleaseProvider;
|
28
|
+
export declare class GitLabRemoteReleaseClient extends RemoteReleaseClient<GitLabRelease> {
|
29
|
+
remoteReleaseProviderName: string;
|
30
|
+
/**
|
31
|
+
* Get GitLab repository data from git remote
|
32
|
+
*/
|
33
|
+
static resolveRepoData(createReleaseConfig: false | ResolvedCreateRemoteReleaseProvider, remoteName?: string): GitLabRepoData | null;
|
34
|
+
/**
|
35
|
+
* Resolve a GitLab token from various environment variables
|
36
|
+
*/
|
37
|
+
static resolveTokenData(hostname: string): Promise<{
|
38
|
+
token: string;
|
39
|
+
headerName: string;
|
40
|
+
} | null>;
|
41
|
+
createPostGitTask(releaseVersion: ReleaseVersion, changelogContents: string, dryRun: boolean): PostGitTask;
|
42
|
+
applyUsernameToAuthors(): Promise<void>;
|
43
|
+
protected getReleaseByTag(tag: string): Promise<GitLabRelease>;
|
44
|
+
protected createRelease(remoteRelease: GitLabRelease): Promise<any>;
|
45
|
+
protected updateRelease(_id: string, remoteRelease: GitLabRelease): Promise<any>;
|
46
|
+
/**
|
47
|
+
* Generate a URL for manual release creation on GitLab. Sadly, unlike GitHub, GitLab does not
|
48
|
+
* seem to respect query string parameters for setting the UI form fields, so the user has to
|
49
|
+
* start from scratch.
|
50
|
+
*/
|
51
|
+
protected getManualRemoteReleaseURL(_remoteReleaseOptions: RemoteReleaseOptions): string;
|
52
|
+
protected handleAuthError(): void;
|
53
|
+
protected logReleaseAction(existingRelease: GitLabRelease | undefined, gitTag: string, dryRun: boolean): void;
|
54
|
+
protected handleError(error: any, result: RemoteReleaseResult): Promise<void>;
|
55
|
+
private promptForContinueInGitLab;
|
56
|
+
/**
|
57
|
+
* Format references for the release (e.g., MRs, issues)
|
58
|
+
*/
|
59
|
+
formatReferences(references: Reference[]): string;
|
60
|
+
protected syncRelease(remoteReleaseOptions: RemoteReleaseOptions, existingRelease?: GitLabRelease): Promise<RemoteReleaseResult>;
|
61
|
+
private getRequiredRemoteRepoData;
|
62
|
+
}
|
@@ -0,0 +1,271 @@
|
|
1
|
+
"use strict";
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
3
|
+
exports.GitLabRemoteReleaseClient = exports.defaultCreateReleaseProvider = void 0;
|
4
|
+
const chalk = require("chalk");
|
5
|
+
const enquirer_1 = require("enquirer");
|
6
|
+
const node_child_process_1 = require("node:child_process");
|
7
|
+
const output_1 = require("../../../../utils/output");
|
8
|
+
const remote_release_client_1 = require("./remote-release-client");
|
9
|
+
exports.defaultCreateReleaseProvider = {
|
10
|
+
provider: 'gitlab',
|
11
|
+
hostname: 'gitlab.com',
|
12
|
+
apiBaseUrl: 'https://gitlab.com/api/v4',
|
13
|
+
};
|
14
|
+
class GitLabRemoteReleaseClient extends remote_release_client_1.RemoteReleaseClient {
|
15
|
+
constructor() {
|
16
|
+
super(...arguments);
|
17
|
+
this.remoteReleaseProviderName = 'GitLab';
|
18
|
+
}
|
19
|
+
/**
|
20
|
+
* Get GitLab repository data from git remote
|
21
|
+
*/
|
22
|
+
static resolveRepoData(createReleaseConfig, remoteName = 'origin') {
|
23
|
+
try {
|
24
|
+
const remoteUrl = (0, node_child_process_1.execSync)(`git remote get-url ${remoteName}`, {
|
25
|
+
encoding: 'utf8',
|
26
|
+
stdio: 'pipe',
|
27
|
+
}).trim();
|
28
|
+
// Use the default provider if custom one is not specified or releases are disabled
|
29
|
+
let hostname = exports.defaultCreateReleaseProvider.hostname;
|
30
|
+
let apiBaseUrl = exports.defaultCreateReleaseProvider.apiBaseUrl;
|
31
|
+
if (createReleaseConfig !== false &&
|
32
|
+
typeof createReleaseConfig !== 'string') {
|
33
|
+
hostname = createReleaseConfig.hostname || hostname;
|
34
|
+
apiBaseUrl = createReleaseConfig.apiBaseUrl || apiBaseUrl;
|
35
|
+
}
|
36
|
+
// Extract the project path from the URL
|
37
|
+
const escapedHostname = hostname.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
38
|
+
const regexString = `${escapedHostname}[/:]([\\w.-]+/[\\w.-]+(?:/[\\w.-]+)*)(\\.git)?`;
|
39
|
+
const regex = new RegExp(regexString);
|
40
|
+
const match = remoteUrl.match(regex);
|
41
|
+
if (match && match[1]) {
|
42
|
+
// Remove trailing .git if present
|
43
|
+
const slug = match[1].replace(/\.git$/, '');
|
44
|
+
// Encode the project path for use in API URLs
|
45
|
+
const projectId = encodeURIComponent(slug);
|
46
|
+
return {
|
47
|
+
hostname,
|
48
|
+
apiBaseUrl,
|
49
|
+
slug,
|
50
|
+
projectId,
|
51
|
+
};
|
52
|
+
}
|
53
|
+
else {
|
54
|
+
throw new Error(`Could not extract project path data from the resolved remote URL: ${remoteUrl}`);
|
55
|
+
}
|
56
|
+
}
|
57
|
+
catch (err) {
|
58
|
+
if (process.env.NX_VERBOSE_LOGGING === 'true') {
|
59
|
+
console.error(err);
|
60
|
+
}
|
61
|
+
return null;
|
62
|
+
}
|
63
|
+
}
|
64
|
+
/**
|
65
|
+
* Resolve a GitLab token from various environment variables
|
66
|
+
*/
|
67
|
+
static async resolveTokenData(hostname) {
|
68
|
+
// Try and resolve from the environment
|
69
|
+
const tokenFromEnv = process.env.GITLAB_TOKEN || process.env.GL_TOKEN;
|
70
|
+
if (tokenFromEnv) {
|
71
|
+
return { token: tokenFromEnv, headerName: 'PRIVATE-TOKEN' };
|
72
|
+
}
|
73
|
+
// Try and resolve from a CI environment
|
74
|
+
if (process.env.CI_JOB_TOKEN) {
|
75
|
+
return { token: process.env.CI_JOB_TOKEN, headerName: 'JOB-TOKEN' };
|
76
|
+
}
|
77
|
+
if (hostname !== 'gitlab.com') {
|
78
|
+
console.log(`Warning: It was not possible to automatically resolve a GitLab token from your environment for hostname ${hostname}. If you set the GITLAB_TOKEN or GL_TOKEN environment variable (or you are in GitLab CI where CI_JOB_TOKEN is set automatically), that will be used for GitLab API requests.`);
|
79
|
+
}
|
80
|
+
return null;
|
81
|
+
}
|
82
|
+
createPostGitTask(releaseVersion, changelogContents, dryRun) {
|
83
|
+
return async (latestCommit) => {
|
84
|
+
output_1.output.logSingleLine(`Creating GitLab Release`);
|
85
|
+
await this.createOrUpdateRelease(releaseVersion, changelogContents, latestCommit, { dryRun });
|
86
|
+
};
|
87
|
+
}
|
88
|
+
// Not implemented for GitLab yet, the changelog renderer should not call this method
|
89
|
+
async applyUsernameToAuthors() {
|
90
|
+
throw new Error('applyUsernameToAuthors is not implemented for GitLab yet');
|
91
|
+
}
|
92
|
+
async getReleaseByTag(tag) {
|
93
|
+
const gitlabRepoData = this.getRequiredRemoteRepoData();
|
94
|
+
return await this.makeRequest(`/projects/${gitlabRepoData.projectId}/releases/${encodeURIComponent(tag)}`);
|
95
|
+
}
|
96
|
+
async createRelease(remoteRelease) {
|
97
|
+
const gitlabRepoData = this.getRequiredRemoteRepoData();
|
98
|
+
return await this.makeRequest(`/projects/${gitlabRepoData.projectId}/releases`, {
|
99
|
+
method: 'POST',
|
100
|
+
data: remoteRelease,
|
101
|
+
});
|
102
|
+
}
|
103
|
+
async updateRelease(_id, remoteRelease) {
|
104
|
+
const gitlabRepoData = this.getRequiredRemoteRepoData();
|
105
|
+
return await this.makeRequest(`/projects/${gitlabRepoData.projectId}/releases/${encodeURIComponent(remoteRelease.tag_name)}`, {
|
106
|
+
method: 'PUT',
|
107
|
+
data: remoteRelease,
|
108
|
+
});
|
109
|
+
}
|
110
|
+
/**
|
111
|
+
* Generate a URL for manual release creation on GitLab. Sadly, unlike GitHub, GitLab does not
|
112
|
+
* seem to respect query string parameters for setting the UI form fields, so the user has to
|
113
|
+
* start from scratch.
|
114
|
+
*/
|
115
|
+
getManualRemoteReleaseURL(_remoteReleaseOptions) {
|
116
|
+
const gitlabRepoData = this.getRequiredRemoteRepoData();
|
117
|
+
return `https://${gitlabRepoData.hostname}/${gitlabRepoData.slug}/-/releases/new`;
|
118
|
+
}
|
119
|
+
handleAuthError() {
|
120
|
+
output_1.output.error({
|
121
|
+
title: `Unable to resolve data via the GitLab API.`,
|
122
|
+
bodyLines: [
|
123
|
+
'- Set the `GITLAB_TOKEN` or `GL_TOKEN` environment variable to a valid GitLab token with `api` scope',
|
124
|
+
'- If running in GitLab CI, the automatically provisioned CI_JOB_TOKEN can also be used',
|
125
|
+
],
|
126
|
+
});
|
127
|
+
}
|
128
|
+
logReleaseAction(existingRelease, gitTag, dryRun) {
|
129
|
+
const gitlabRepoData = this.getRequiredRemoteRepoData();
|
130
|
+
const logTitle = `https://${gitlabRepoData.hostname}/${gitlabRepoData.slug}/-/releases/${encodeURIComponent(gitTag)}`;
|
131
|
+
if (existingRelease) {
|
132
|
+
console.error(`${chalk.white('UPDATE')} ${logTitle}${dryRun ? chalk.keyword('orange')(' [dry-run]') : ''}`);
|
133
|
+
}
|
134
|
+
else {
|
135
|
+
console.error(`${chalk.green('CREATE')} ${logTitle}${dryRun ? chalk.keyword('orange')(' [dry-run]') : ''}`);
|
136
|
+
}
|
137
|
+
}
|
138
|
+
async handleError(error, result) {
|
139
|
+
if (error) {
|
140
|
+
process.exitCode = 1;
|
141
|
+
if (error.response?.data) {
|
142
|
+
output_1.output.error({
|
143
|
+
title: `A GitLab API Error occurred when creating/updating the release`,
|
144
|
+
bodyLines: [
|
145
|
+
`GitLab Error: ${JSON.stringify(error.response.data)}`,
|
146
|
+
`---`,
|
147
|
+
`Request Data:`,
|
148
|
+
`Repo: ${this.getRemoteRepoData()?.slug}`,
|
149
|
+
`Token Header Data: ${this.tokenHeader}`,
|
150
|
+
`Body: ${JSON.stringify(result.requestData)}`,
|
151
|
+
],
|
152
|
+
});
|
153
|
+
}
|
154
|
+
else {
|
155
|
+
console.log(error);
|
156
|
+
console.error(`An unknown error occurred while trying to create a release on GitLab, please report this on https://github.com/nrwl/nx (NOTE: make sure to redact your GitLab token from the error message!)`);
|
157
|
+
}
|
158
|
+
}
|
159
|
+
const shouldContinueInGitLab = await this.promptForContinueInGitLab();
|
160
|
+
if (!shouldContinueInGitLab) {
|
161
|
+
return;
|
162
|
+
}
|
163
|
+
const open = require('open');
|
164
|
+
await open(result.url)
|
165
|
+
.then(() => {
|
166
|
+
console.info(`\nFollow up in the browser to manually create the release:\n\n` +
|
167
|
+
chalk.underline(chalk.cyan(result.url)) +
|
168
|
+
`\n`);
|
169
|
+
})
|
170
|
+
.catch(() => {
|
171
|
+
console.info(`Open this link to manually create a release: \n` +
|
172
|
+
chalk.underline(chalk.cyan(result.url)) +
|
173
|
+
'\n');
|
174
|
+
});
|
175
|
+
}
|
176
|
+
async promptForContinueInGitLab() {
|
177
|
+
try {
|
178
|
+
const reply = await (0, enquirer_1.prompt)([
|
179
|
+
{
|
180
|
+
name: 'open',
|
181
|
+
message: 'Do you want to create the release manually in your browser?',
|
182
|
+
type: 'autocomplete',
|
183
|
+
choices: [
|
184
|
+
{
|
185
|
+
name: 'Yes',
|
186
|
+
hint: 'It will open the GitLab release page for you',
|
187
|
+
},
|
188
|
+
{
|
189
|
+
name: 'No',
|
190
|
+
},
|
191
|
+
],
|
192
|
+
initial: 0,
|
193
|
+
},
|
194
|
+
]);
|
195
|
+
return reply.open === 'Yes';
|
196
|
+
}
|
197
|
+
catch {
|
198
|
+
// Ensure the cursor is always restored before exiting
|
199
|
+
process.stdout.write('\u001b[?25h');
|
200
|
+
// Handle the case where the user exits the prompt with ctrl+c
|
201
|
+
process.exit(1);
|
202
|
+
}
|
203
|
+
}
|
204
|
+
/**
|
205
|
+
* Format references for the release (e.g., MRs, issues)
|
206
|
+
*/
|
207
|
+
formatReferences(references) {
|
208
|
+
const gitlabRepoData = this.getRequiredRemoteRepoData();
|
209
|
+
const providerToRefSpec = {
|
210
|
+
gitlab: {
|
211
|
+
'pull-request': 'merge_requests',
|
212
|
+
hash: 'commit',
|
213
|
+
issue: 'issues',
|
214
|
+
},
|
215
|
+
};
|
216
|
+
const refSpec = providerToRefSpec.gitlab;
|
217
|
+
const formatSingleReference = (ref) => {
|
218
|
+
return `https://${gitlabRepoData.hostname}/${gitlabRepoData.slug}/-/${refSpec[ref.type]}/${ref.value.replace(/^[#!]/, '')}`;
|
219
|
+
};
|
220
|
+
const mr = references.filter((ref) => ref.type === 'pull-request');
|
221
|
+
const issue = references.filter((ref) => ref.type === 'issue');
|
222
|
+
if (mr.length > 0 || issue.length > 0) {
|
223
|
+
return (' (' +
|
224
|
+
[...mr, ...issue].map((ref) => formatSingleReference(ref)).join(', ') +
|
225
|
+
')');
|
226
|
+
}
|
227
|
+
if (references.length > 0) {
|
228
|
+
return ' (' + formatSingleReference(references[0]) + ')';
|
229
|
+
}
|
230
|
+
return '';
|
231
|
+
}
|
232
|
+
async syncRelease(remoteReleaseOptions, existingRelease) {
|
233
|
+
const gitlabReleaseData = {
|
234
|
+
tag_name: remoteReleaseOptions.version,
|
235
|
+
name: remoteReleaseOptions.version,
|
236
|
+
description: remoteReleaseOptions.body,
|
237
|
+
prerelease: remoteReleaseOptions.prerelease,
|
238
|
+
ref: remoteReleaseOptions.commit,
|
239
|
+
released_at: new Date().toISOString(),
|
240
|
+
assets: { links: [] },
|
241
|
+
milestones: [],
|
242
|
+
};
|
243
|
+
try {
|
244
|
+
const newGlRelease = await (existingRelease
|
245
|
+
? this.updateRelease(existingRelease.id, gitlabReleaseData)
|
246
|
+
: this.createRelease(gitlabReleaseData));
|
247
|
+
const gitlabRepoData = this.getRequiredRemoteRepoData();
|
248
|
+
return {
|
249
|
+
status: existingRelease ? 'updated' : 'created',
|
250
|
+
id: newGlRelease.tag_name,
|
251
|
+
url: `https://${gitlabRepoData.hostname}/${gitlabRepoData.slug}/-/tags/${encodeURIComponent(remoteReleaseOptions.version)}`,
|
252
|
+
};
|
253
|
+
}
|
254
|
+
catch (error) {
|
255
|
+
return {
|
256
|
+
status: 'manual',
|
257
|
+
error,
|
258
|
+
url: this.getManualRemoteReleaseURL(remoteReleaseOptions),
|
259
|
+
requestData: gitlabReleaseData,
|
260
|
+
};
|
261
|
+
}
|
262
|
+
}
|
263
|
+
getRequiredRemoteRepoData() {
|
264
|
+
const gitlabRepoData = this.getRemoteRepoData();
|
265
|
+
if (!gitlabRepoData) {
|
266
|
+
throw new Error(`No remote repo data could be resolved for the current workspace`);
|
267
|
+
}
|
268
|
+
return gitlabRepoData;
|
269
|
+
}
|
270
|
+
}
|
271
|
+
exports.GitLabRemoteReleaseClient = GitLabRemoteReleaseClient;
|