nx 21.0.0-beta.11 → 21.0.0-beta.12
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 +10 -5
- 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 +3 -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/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/yargs-utils/shared-options.d.ts +1 -1
- package/src/command-line/yargs-utils/shared-options.js +22 -3
- package/src/config/nx-json.d.ts +8 -1
- package/src/core/graph/main.js +1 -1
- package/src/core/graph/styles.css +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/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/is-tui-enabled.d.ts +16 -1
- package/src/tasks-runner/is-tui-enabled.js +40 -28
- package/src/tasks-runner/run-command.js +3 -3
- 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-orchestrator.js +6 -3
- 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
@@ -1,6 +1,6 @@
|
|
1
1
|
"use strict";
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
3
|
-
exports.
|
3
|
+
exports.DEFAULT_VERSION_ACTIONS_PATH = exports.IMPLICIT_DEFAULT_RELEASE_GROUP = void 0;
|
4
4
|
exports.createNxReleaseConfig = createNxReleaseConfig;
|
5
5
|
exports.handleNxReleaseConfigError = handleNxReleaseConfigError;
|
6
6
|
/**
|
@@ -23,6 +23,8 @@ const find_matching_projects_1 = require("../../../utils/find-matching-projects"
|
|
23
23
|
const output_1 = require("../../../utils/output");
|
24
24
|
const path_1 = require("../../../utils/path");
|
25
25
|
const workspace_root_1 = require("../../../utils/workspace-root");
|
26
|
+
const github_1 = require("../utils/remote-release-clients/github");
|
27
|
+
const gitlab_1 = require("../utils/remote-release-clients/gitlab");
|
26
28
|
const resolve_changelog_renderer_1 = require("../utils/resolve-changelog-renderer");
|
27
29
|
const resolve_nx_json_error_message_1 = require("../utils/resolve-nx-json-error-message");
|
28
30
|
const conventional_commits_1 = require("./conventional-commits");
|
@@ -170,7 +172,7 @@ async function createNxReleaseConfig(projectGraph, projectFileMap, userConfig =
|
|
170
172
|
renderer: defaultRendererPath,
|
171
173
|
renderOptions: {
|
172
174
|
authors: true,
|
173
|
-
|
175
|
+
applyUsernameToAuthors: true,
|
174
176
|
commitReferences: true,
|
175
177
|
versionTitleDate: true,
|
176
178
|
},
|
@@ -184,7 +186,7 @@ async function createNxReleaseConfig(projectGraph, projectFileMap, userConfig =
|
|
184
186
|
renderer: defaultRendererPath,
|
185
187
|
renderOptions: {
|
186
188
|
authors: true,
|
187
|
-
|
189
|
+
applyUsernameToAuthors: true,
|
188
190
|
commitReferences: true,
|
189
191
|
versionTitleDate: true,
|
190
192
|
},
|
@@ -225,7 +227,7 @@ async function createNxReleaseConfig(projectGraph, projectFileMap, userConfig =
|
|
225
227
|
renderer: defaultRendererPath,
|
226
228
|
renderOptions: {
|
227
229
|
authors: true,
|
228
|
-
|
230
|
+
applyUsernameToAuthors: true,
|
229
231
|
commitReferences: true,
|
230
232
|
versionTitleDate: true,
|
231
233
|
},
|
@@ -930,13 +932,11 @@ const supportedCreateReleaseProviders = [
|
|
930
932
|
name: 'github-enterprise-server',
|
931
933
|
defaultApiBaseUrl: 'https://__hostname__/api/v3',
|
932
934
|
},
|
935
|
+
{
|
936
|
+
name: 'gitlab',
|
937
|
+
defaultApiBaseUrl: 'https://__hostname__/api/v4',
|
938
|
+
},
|
933
939
|
];
|
934
|
-
// User opts into the default by specifying the string value 'github'
|
935
|
-
exports.defaultCreateReleaseProvider = {
|
936
|
-
provider: 'github',
|
937
|
-
hostname: 'github.com',
|
938
|
-
apiBaseUrl: 'https://api.github.com',
|
939
|
-
};
|
940
940
|
function validateCreateReleaseConfig(changelogConfig) {
|
941
941
|
const createRelease = changelogConfig.createRelease;
|
942
942
|
// Disabled: valid
|
@@ -945,7 +945,14 @@ function validateCreateReleaseConfig(changelogConfig) {
|
|
945
945
|
}
|
946
946
|
// GitHub shorthand, expand to full object form, mark as valid
|
947
947
|
if (createRelease === 'github') {
|
948
|
-
changelogConfig.createRelease =
|
948
|
+
changelogConfig.createRelease =
|
949
|
+
github_1.defaultCreateReleaseProvider;
|
950
|
+
return null;
|
951
|
+
}
|
952
|
+
// Gitlab shorthand, expand to full object form, mark as valid
|
953
|
+
if (createRelease === 'gitlab') {
|
954
|
+
changelogConfig.createRelease =
|
955
|
+
gitlab_1.defaultCreateReleaseProvider;
|
949
956
|
return null;
|
950
957
|
}
|
951
958
|
// Object config, ensure that properties are valid
|
@@ -17,8 +17,8 @@ const use_legacy_versioning_1 = require("./config/use-legacy-versioning");
|
|
17
17
|
const version_plans_1 = require("./config/version-plans");
|
18
18
|
const publish_1 = require("./publish");
|
19
19
|
const git_1 = require("./utils/git");
|
20
|
-
const github_1 = require("./utils/github");
|
21
20
|
const print_config_1 = require("./utils/print-config");
|
21
|
+
const remote_release_client_1 = require("./utils/remote-release-clients/remote-release-client");
|
22
22
|
const resolve_nx_json_error_message_1 = require("./utils/resolve-nx-json-error-message");
|
23
23
|
const shared_1 = require("./utils/shared");
|
24
24
|
const version_1 = require("./version");
|
@@ -84,10 +84,10 @@ function createAPI(overrideReleaseConfig) {
|
|
84
84
|
const shouldCommit = userProvidedReleaseConfig.git?.commit ?? true;
|
85
85
|
const shouldStage = (shouldCommit || userProvidedReleaseConfig.git?.stageChanges) ?? false;
|
86
86
|
const shouldTag = userProvidedReleaseConfig.git?.tag ?? true;
|
87
|
-
const
|
88
|
-
// If the workspace or any of the release groups specify that a
|
89
|
-
const shouldPush = (
|
90
|
-
releaseGroups.some((group) => (
|
87
|
+
const shouldCreateWorkspaceRemoteRelease = shouldCreateRemoteRelease(nxReleaseConfig.changelog.workspaceChangelog);
|
88
|
+
// If the workspace or any of the release groups specify that a remote release should be created, we need to push the changes to the remote
|
89
|
+
const shouldPush = (shouldCreateWorkspaceRemoteRelease ||
|
90
|
+
releaseGroups.some((group) => shouldCreateRemoteRelease(group.changelog))) ??
|
91
91
|
false;
|
92
92
|
const versionResult = await releaseVersion({
|
93
93
|
...args,
|
@@ -179,19 +179,27 @@ function createAPI(overrideReleaseConfig) {
|
|
179
179
|
hasPushedChanges = true;
|
180
180
|
}
|
181
181
|
let latestCommit;
|
182
|
-
if (
|
182
|
+
if (shouldCreateWorkspaceRemoteRelease &&
|
183
|
+
changelogResult.workspaceChangelog) {
|
184
|
+
const remoteReleaseClient = await (0, remote_release_client_1.createRemoteReleaseClient)(
|
185
|
+
// shouldCreateWorkspaceRemoteRelease() ensures that the createRelease property exists and is not false
|
186
|
+
nxReleaseConfig.changelog.workspaceChangelog
|
187
|
+
.createRelease);
|
183
188
|
if (!hasPushedChanges) {
|
184
|
-
throw new Error(
|
189
|
+
throw new Error(`It is not possible to create a ${remoteReleaseClient.remoteReleaseProviderName} release for the workspace without pushing the changes to the remote, please ensure that you have not disabled git push in your nx release config`);
|
185
190
|
}
|
186
|
-
output_1.output.logSingleLine(`Creating
|
191
|
+
output_1.output.logSingleLine(`Creating ${remoteReleaseClient.remoteReleaseProviderName} Release`);
|
187
192
|
latestCommit = await (0, git_1.getCommitHash)('HEAD');
|
188
|
-
await (
|
189
|
-
? nxReleaseConfig.changelog.workspaceChangelog.createRelease
|
190
|
-
: false, changelogResult.workspaceChangelog.releaseVersion, changelogResult.workspaceChangelog.contents, latestCommit, { dryRun: args.dryRun });
|
193
|
+
await remoteReleaseClient.createOrUpdateRelease(changelogResult.workspaceChangelog.releaseVersion, changelogResult.workspaceChangelog.contents, latestCommit, { dryRun: args.dryRun });
|
191
194
|
}
|
192
195
|
for (const releaseGroup of releaseGroups) {
|
193
|
-
const
|
194
|
-
if (
|
196
|
+
const shouldCreateProjectRemoteReleases = shouldCreateRemoteRelease(releaseGroup.changelog);
|
197
|
+
if (shouldCreateProjectRemoteReleases &&
|
198
|
+
changelogResult.projectChangelogs) {
|
199
|
+
const remoteReleaseClient = await (0, remote_release_client_1.createRemoteReleaseClient)(
|
200
|
+
// shouldCreateProjectRemoteReleases() ensures that the createRelease property exists and is not false
|
201
|
+
releaseGroup.changelog
|
202
|
+
.createRelease);
|
195
203
|
const projects = args.projects?.length
|
196
204
|
? // If the user has passed a list of projects, we need to use the filtered list of projects within the release group
|
197
205
|
Array.from(releaseGroupToFilteredProjects.get(releaseGroup))
|
@@ -204,15 +212,13 @@ function createAPI(overrideReleaseConfig) {
|
|
204
212
|
continue;
|
205
213
|
}
|
206
214
|
if (!hasPushedChanges) {
|
207
|
-
throw new Error(
|
215
|
+
throw new Error(`It is not possible to create a ${remoteReleaseClient.remoteReleaseProviderName} release for the project without pushing the changes to the remote, please ensure that you have not disabled git push in your nx release config`);
|
208
216
|
}
|
209
|
-
output_1.output.logSingleLine(`Creating
|
217
|
+
output_1.output.logSingleLine(`Creating ${remoteReleaseClient.remoteReleaseProviderName} Release`);
|
210
218
|
if (!latestCommit) {
|
211
219
|
latestCommit = await (0, git_1.getCommitHash)('HEAD');
|
212
220
|
}
|
213
|
-
await (
|
214
|
-
? releaseGroup.changelog.createRelease
|
215
|
-
: false, changelog.releaseVersion, changelog.contents, latestCommit, { dryRun: args.dryRun });
|
221
|
+
await remoteReleaseClient.createOrUpdateRelease(changelog.releaseVersion, changelog.contents, latestCommit, { dryRun: args.dryRun });
|
216
222
|
}
|
217
223
|
}
|
218
224
|
}
|
@@ -253,3 +259,9 @@ async function promptForPublish() {
|
|
253
259
|
return false;
|
254
260
|
}
|
255
261
|
}
|
262
|
+
function shouldCreateRemoteRelease(changelogConfig) {
|
263
|
+
if (changelogConfig === false) {
|
264
|
+
return false;
|
265
|
+
}
|
266
|
+
return changelogConfig.createRelease !== false;
|
267
|
+
}
|
@@ -63,6 +63,7 @@ export declare function parseConventionalCommitsMessage(message: string): {
|
|
63
63
|
description: string;
|
64
64
|
breaking: boolean;
|
65
65
|
} | null;
|
66
|
+
export declare function extractReferencesFromCommit(commit: RawGitCommit): Reference[];
|
66
67
|
export declare function parseGitCommit(commit: RawGitCommit, isVersionPlanCommit?: boolean): GitCommit | null;
|
67
68
|
export declare function getCommitHash(ref: string): Promise<string>;
|
68
69
|
export declare function getFirstGitCommit(): Promise<string>;
|
@@ -8,6 +8,7 @@ exports.gitTag = gitTag;
|
|
8
8
|
exports.gitPush = gitPush;
|
9
9
|
exports.parseCommits = parseCommits;
|
10
10
|
exports.parseConventionalCommitsMessage = parseConventionalCommitsMessage;
|
11
|
+
exports.extractReferencesFromCommit = extractReferencesFromCommit;
|
11
12
|
exports.parseGitCommit = parseGitCommit;
|
12
13
|
exports.getCommitHash = getCommitHash;
|
13
14
|
exports.getFirstGitCommit = getFirstGitCommit;
|
@@ -362,17 +363,32 @@ function parseConventionalCommitsMessage(message) {
|
|
362
363
|
breaking: Boolean(match.groups.breaking),
|
363
364
|
};
|
364
365
|
}
|
365
|
-
function
|
366
|
+
function extractReferencesFromCommit(commit) {
|
366
367
|
const references = [];
|
367
|
-
|
368
|
+
// Extract GitHub style PR references from commit message
|
369
|
+
for (const m of commit.message.matchAll(PullRequestRE)) {
|
368
370
|
references.push({ type: 'pull-request', value: m[1] });
|
369
371
|
}
|
370
|
-
|
372
|
+
// Extract GitLab style merge request references from commit body
|
373
|
+
for (const m of commit.body.matchAll(GitLabMergeRequestRE)) {
|
374
|
+
if (m[1]) {
|
375
|
+
references.push({ type: 'pull-request', value: m[1] });
|
376
|
+
}
|
377
|
+
}
|
378
|
+
// Extract issue references from commit message
|
379
|
+
for (const m of commit.message.matchAll(IssueRE)) {
|
380
|
+
if (!references.some((i) => i.value === m[1])) {
|
381
|
+
references.push({ type: 'issue', value: m[1] });
|
382
|
+
}
|
383
|
+
}
|
384
|
+
// Extract issue references from commit body
|
385
|
+
for (const m of commit.body.matchAll(IssueRE)) {
|
371
386
|
if (!references.some((i) => i.value === m[1])) {
|
372
387
|
references.push({ type: 'issue', value: m[1] });
|
373
388
|
}
|
374
389
|
}
|
375
|
-
|
390
|
+
// Add commit hash reference
|
391
|
+
references.push({ value: commit.shortHash, type: 'hash' });
|
376
392
|
return references;
|
377
393
|
}
|
378
394
|
function getAllAuthorsForCommit(commit) {
|
@@ -390,7 +406,10 @@ function getAllAuthorsForCommit(commit) {
|
|
390
406
|
// https://regex101.com/r/FSfNvA/1
|
391
407
|
const ConventionalCommitRegex = /(?<type>[a-z]+)(\((?<scope>.+)\))?(?<breaking>!)?: (?<description>.+)/i;
|
392
408
|
const CoAuthoredByRegex = /co-authored-by:\s*(?<name>.+)(<(?<email>.+)>)/gim;
|
409
|
+
// GitHub style PR references
|
393
410
|
const PullRequestRE = /\([ a-z]*(#\d+)\s*\)/gm;
|
411
|
+
// GitLab style merge request references
|
412
|
+
const GitLabMergeRequestRE = /See merge request (?:[a-z0-9/-]+)?(![\d]+)/gim;
|
394
413
|
const IssueRE = /(#\d+)/gm;
|
395
414
|
const ChangedFileRegex = /(A|M|D|R\d*|C\d*)\t([^\t\n]*)\t?(.*)?/gm;
|
396
415
|
const RevertHashRE = /This reverts commit (?<hash>[\da-f]{40})./gm;
|
@@ -402,7 +421,7 @@ function parseGitCommit(commit, isVersionPlanCommit = false) {
|
|
402
421
|
description: commit.message,
|
403
422
|
type: '',
|
404
423
|
scope: '',
|
405
|
-
references:
|
424
|
+
references: extractReferencesFromCommit(commit),
|
406
425
|
// The commit message is not the source of truth for a breaking (major) change in version plans, so the value is not relevant
|
407
426
|
// TODO(v22): Make the current GitCommit interface more clearly tied to conventional commits
|
408
427
|
isBreaking: false,
|
@@ -420,9 +439,9 @@ function parseGitCommit(commit, isVersionPlanCommit = false) {
|
|
420
439
|
const scope = parsedMessage.scope;
|
421
440
|
const isBreaking = parsedMessage.breaking || commit.body.includes('BREAKING CHANGE:');
|
422
441
|
let description = parsedMessage.description;
|
423
|
-
// Extract references from
|
424
|
-
const references =
|
425
|
-
// Remove references
|
442
|
+
// Extract issue and PR references from the commit
|
443
|
+
const references = extractReferencesFromCommit(commit);
|
444
|
+
// Remove GitHub style references from description (NOTE: GitLab style references only seem to appear in the body, so we don't need to remove them here)
|
426
445
|
description = description.replace(PullRequestRE, '').trim();
|
427
446
|
let type = parsedMessage.type;
|
428
447
|
// Extract any reverted hashes, if applicable
|
@@ -0,0 +1,57 @@
|
|
1
|
+
import type { PostGitTask } from '../../changelog';
|
2
|
+
import { type ResolvedCreateRemoteReleaseProvider } from '../../config/config';
|
3
|
+
import { Reference } from '../git';
|
4
|
+
import { ReleaseVersion } from '../shared';
|
5
|
+
import { RemoteReleaseClient, RemoteReleaseOptions, RemoteReleaseResult, RemoteRepoData } from './remote-release-client';
|
6
|
+
export interface GithubRepoData extends RemoteRepoData {
|
7
|
+
}
|
8
|
+
export interface GithubRemoteRelease {
|
9
|
+
id?: string;
|
10
|
+
body: string;
|
11
|
+
tag_name: string;
|
12
|
+
target_commitish?: string;
|
13
|
+
name?: string;
|
14
|
+
draft?: boolean;
|
15
|
+
prerelease?: boolean;
|
16
|
+
make_latest?: 'legacy' | boolean;
|
17
|
+
}
|
18
|
+
export declare const defaultCreateReleaseProvider: ResolvedCreateRemoteReleaseProvider;
|
19
|
+
export declare class GithubRemoteReleaseClient extends RemoteReleaseClient<GithubRemoteRelease> {
|
20
|
+
remoteReleaseProviderName: string;
|
21
|
+
/**
|
22
|
+
* Get GitHub repository data from git remote
|
23
|
+
*/
|
24
|
+
static resolveRepoData(createReleaseConfig: false | ResolvedCreateRemoteReleaseProvider, remoteName?: string): GithubRepoData | null;
|
25
|
+
/**
|
26
|
+
* Resolve a GitHub token from environment variables or gh CLI
|
27
|
+
*/
|
28
|
+
static resolveTokenData(hostname: string): Promise<{
|
29
|
+
token: string;
|
30
|
+
headerName: string;
|
31
|
+
} | null>;
|
32
|
+
createPostGitTask(releaseVersion: ReleaseVersion, changelogContents: string, dryRun: boolean): PostGitTask;
|
33
|
+
applyUsernameToAuthors(authors: Map<string, {
|
34
|
+
email: Set<string>;
|
35
|
+
username?: string;
|
36
|
+
}>): Promise<void>;
|
37
|
+
/**
|
38
|
+
* Get a release by tag
|
39
|
+
*/
|
40
|
+
protected getReleaseByTag(tag: string): Promise<GithubRemoteRelease>;
|
41
|
+
/**
|
42
|
+
* Create a new release
|
43
|
+
*/
|
44
|
+
protected createRelease(remoteRelease: GithubRemoteRelease): Promise<any>;
|
45
|
+
protected updateRelease(id: string, remoteRelease: GithubRemoteRelease): Promise<any>;
|
46
|
+
protected getManualRemoteReleaseURL(remoteReleaseOptions: RemoteReleaseOptions): string;
|
47
|
+
protected handleAuthError(): void;
|
48
|
+
protected logReleaseAction(existingRelease: GithubRemoteRelease | undefined, gitTag: string, dryRun: boolean): void;
|
49
|
+
protected handleError(error: any, result: RemoteReleaseResult): Promise<void>;
|
50
|
+
private promptForContinueInGitHub;
|
51
|
+
/**
|
52
|
+
* Format references for the release (e.g., PRs, issues)
|
53
|
+
*/
|
54
|
+
formatReferences(references: Reference[]): string;
|
55
|
+
protected syncRelease(remoteReleaseOptions: RemoteReleaseOptions, existingRelease?: GithubRemoteRelease): Promise<RemoteReleaseResult>;
|
56
|
+
private getRequiredRemoteRepoData;
|
57
|
+
}
|
@@ -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;
|