feat(release): support github enterprise server (#26482)
This commit is contained in:
parent
0d37ef98da
commit
21d1696ef5
@ -4,6 +4,7 @@ import { NxReleaseConfig } from '../../src/command-line/release/config/config';
|
||||
import { DEFAULT_CONVENTIONAL_COMMITS_CONFIG } from '../../src/command-line/release/config/conventional-commits';
|
||||
import { GitCommit } from '../../src/command-line/release/utils/git';
|
||||
import {
|
||||
GithubRepoData,
|
||||
RepoSlug,
|
||||
formatReferences,
|
||||
} from '../../src/command-line/release/utils/github';
|
||||
@ -42,6 +43,7 @@ export type DependencyBump = {
|
||||
* @param {string | false} config.entryWhenNoChanges The (already interpolated) string to use as the changelog entry when there are no changes, or `false` if no entry should be generated
|
||||
* @param {ChangelogRenderOptions} config.changelogRenderOptions The options specific to the ChangelogRenderer implementation
|
||||
* @param {DependencyBump[]} config.dependencyBumps Optional list of additional dependency bumps that occurred as part of the release, outside of the commit data
|
||||
* @param {GithubRepoData} config.repoData Resolved data for the current GitHub repository
|
||||
*/
|
||||
export type ChangelogRenderer = (config: {
|
||||
projectGraph: ProjectGraph;
|
||||
@ -53,7 +55,9 @@ export type ChangelogRenderer = (config: {
|
||||
entryWhenNoChanges: string | false;
|
||||
changelogRenderOptions: DefaultChangelogRenderOptions;
|
||||
dependencyBumps?: DependencyBump[];
|
||||
// TODO(v20): remove repoSlug in favour of repoData
|
||||
repoSlug?: RepoSlug;
|
||||
repoData?: GithubRepoData;
|
||||
// TODO(v20): Evaluate if there is a cleaner way to configure this when breaking changes are allowed
|
||||
// null if version plans are being used to generate the changelog
|
||||
conventionalCommitsConfig: NxReleaseConfig['conventionalCommits'] | null;
|
||||
@ -101,6 +105,7 @@ const defaultChangelogRenderer: ChangelogRenderer = async ({
|
||||
dependencyBumps,
|
||||
repoSlug,
|
||||
conventionalCommitsConfig,
|
||||
repoData,
|
||||
}): Promise<string> => {
|
||||
const markdownLines: string[] = [];
|
||||
|
||||
@ -148,7 +153,7 @@ const defaultChangelogRenderer: ChangelogRenderer = async ({
|
||||
change,
|
||||
changelogRenderOptions,
|
||||
isVersionPlans,
|
||||
repoSlug
|
||||
repoData
|
||||
);
|
||||
breakingChanges.push(line);
|
||||
relevantChanges.splice(i, 1);
|
||||
@ -222,7 +227,7 @@ const defaultChangelogRenderer: ChangelogRenderer = async ({
|
||||
change,
|
||||
changelogRenderOptions,
|
||||
isVersionPlans,
|
||||
repoSlug
|
||||
repoData
|
||||
);
|
||||
markdownLines.push(line);
|
||||
if (change.isBreaking) {
|
||||
@ -295,7 +300,7 @@ const defaultChangelogRenderer: ChangelogRenderer = async ({
|
||||
change,
|
||||
changelogRenderOptions,
|
||||
isVersionPlans,
|
||||
repoSlug
|
||||
repoData
|
||||
);
|
||||
markdownLines.push(line + '\n');
|
||||
if (change.isBreaking) {
|
||||
@ -350,7 +355,7 @@ const defaultChangelogRenderer: ChangelogRenderer = async ({
|
||||
}
|
||||
|
||||
// Try to map authors to github usernames
|
||||
if (repoSlug && changelogRenderOptions.mapAuthorsToGitHubUsernames) {
|
||||
if (repoData && changelogRenderOptions.mapAuthorsToGitHubUsernames) {
|
||||
await Promise.all(
|
||||
[..._authors.keys()].map(async (authorName) => {
|
||||
const meta = _authors.get(authorName);
|
||||
@ -455,7 +460,7 @@ function formatChange(
|
||||
change: ChangelogChange,
|
||||
changelogRenderOptions: DefaultChangelogRenderOptions,
|
||||
isVersionPlans: boolean,
|
||||
repoSlug?: RepoSlug
|
||||
repoData?: GithubRepoData
|
||||
): string {
|
||||
let description = change.description;
|
||||
let extraLines = [];
|
||||
@ -480,8 +485,8 @@ function formatChange(
|
||||
(!isVersionPlans && change.isBreaking ? '⚠️ ' : '') +
|
||||
(!isVersionPlans && change.scope ? `**${change.scope.trim()}:** ` : '') +
|
||||
description;
|
||||
if (repoSlug && changelogRenderOptions.commitReferences) {
|
||||
changeLine += formatReferences(change.githubReferences, repoSlug);
|
||||
if (repoData && changelogRenderOptions.commitReferences) {
|
||||
changeLine += formatReferences(change.githubReferences, repoData);
|
||||
}
|
||||
if (extraLinesStr) {
|
||||
changeLine += '\n\n' + extraLinesStr;
|
||||
|
||||
@ -691,6 +691,9 @@
|
||||
{
|
||||
"type": "boolean",
|
||||
"enum": [false]
|
||||
},
|
||||
{
|
||||
"$ref": "#/definitions/CreateReleaseProviderConfiguration"
|
||||
}
|
||||
]
|
||||
},
|
||||
@ -724,6 +727,24 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"CreateReleaseProviderConfiguration": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"provider": {
|
||||
"type": "string",
|
||||
"enum": ["github-enterprise-server"]
|
||||
},
|
||||
"hostname": {
|
||||
"type": "string",
|
||||
"description": "The hostname of the VCS provider instance, e.g. github.example.com"
|
||||
},
|
||||
"apiBaseUrl": {
|
||||
"type": "string",
|
||||
"description": "The base URL for the relevant VCS provider API. If not set, this will default to `https://${hostname}/api/v3`"
|
||||
}
|
||||
},
|
||||
"required": ["provider", "hostname"]
|
||||
},
|
||||
"NxReleaseVersionPlansConfiguration": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
||||
@ -32,6 +32,7 @@ import { ChangelogOptions } from './command-object';
|
||||
import {
|
||||
NxReleaseConfig,
|
||||
createNxReleaseConfig,
|
||||
defaultCreateReleaseProvider,
|
||||
handleNxReleaseConfigError,
|
||||
} from './config/config';
|
||||
import { deepMergeJson } from './config/deep-merge-json';
|
||||
@ -58,7 +59,7 @@ import {
|
||||
parseCommits,
|
||||
parseGitCommit,
|
||||
} from './utils/git';
|
||||
import { createOrUpdateGithubRelease, getGitHubRepoSlug } from './utils/github';
|
||||
import { createOrUpdateGithubRelease, getGitHubRepoData } from './utils/github';
|
||||
import { launchEditor } from './utils/launch-editor';
|
||||
import { parseChangelogMarkdown } from './utils/markdown';
|
||||
import { printAndFlushChanges } from './utils/print-changes';
|
||||
@ -411,6 +412,9 @@ export function createAPI(overrideReleaseConfig: NxReleaseConfiguration) {
|
||||
output.logSingleLine(`Creating GitHub Release`);
|
||||
|
||||
await createOrUpdateGithubRelease(
|
||||
nxReleaseConfig.changelog.workspaceChangelog
|
||||
? nxReleaseConfig.changelog.workspaceChangelog.createRelease
|
||||
: defaultCreateReleaseProvider,
|
||||
workspaceChangelog.releaseVersion,
|
||||
workspaceChangelog.contents,
|
||||
latestCommit,
|
||||
@ -644,6 +648,9 @@ export function createAPI(overrideReleaseConfig: NxReleaseConfiguration) {
|
||||
output.logSingleLine(`Creating GitHub Release`);
|
||||
|
||||
await createOrUpdateGithubRelease(
|
||||
releaseGroup.changelog
|
||||
? releaseGroup.changelog.createRelease
|
||||
: defaultCreateReleaseProvider,
|
||||
projectChangelog.releaseVersion,
|
||||
projectChangelog.contents,
|
||||
latestCommit,
|
||||
@ -797,6 +804,9 @@ export function createAPI(overrideReleaseConfig: NxReleaseConfiguration) {
|
||||
output.logSingleLine(`Creating GitHub Release`);
|
||||
|
||||
await createOrUpdateGithubRelease(
|
||||
releaseGroup.changelog
|
||||
? releaseGroup.changelog.createRelease
|
||||
: defaultCreateReleaseProvider,
|
||||
projectChangelog.releaseVersion,
|
||||
projectChangelog.contents,
|
||||
latestCommit,
|
||||
@ -1110,7 +1120,7 @@ async function generateChangelogForWorkspace({
|
||||
});
|
||||
}
|
||||
|
||||
const githubRepoSlug = getGitHubRepoSlug(gitRemote);
|
||||
const githubRepoData = getGitHubRepoData(gitRemote, config.createRelease);
|
||||
|
||||
let contents = await changelogRenderer({
|
||||
projectGraph,
|
||||
@ -1118,7 +1128,8 @@ async function generateChangelogForWorkspace({
|
||||
commits,
|
||||
releaseVersion: releaseVersion.rawVersion,
|
||||
project: null,
|
||||
repoSlug: githubRepoSlug,
|
||||
repoSlug: githubRepoData?.slug,
|
||||
repoData: githubRepoData,
|
||||
entryWhenNoChanges: config.entryWhenNoChanges,
|
||||
changelogRenderOptions: config.renderOptions,
|
||||
conventionalCommitsConfig: nxReleaseConfig.conventionalCommits,
|
||||
@ -1250,10 +1261,7 @@ async function generateChangelogForProjects({
|
||||
});
|
||||
}
|
||||
|
||||
const githubRepoSlug =
|
||||
config.createRelease === 'github'
|
||||
? getGitHubRepoSlug(gitRemote)
|
||||
: undefined;
|
||||
const githubRepoData = getGitHubRepoData(gitRemote, config.createRelease);
|
||||
|
||||
let contents = await changelogRenderer({
|
||||
projectGraph,
|
||||
@ -1261,7 +1269,8 @@ async function generateChangelogForProjects({
|
||||
commits,
|
||||
releaseVersion: releaseVersion.rawVersion,
|
||||
project: project.name,
|
||||
repoSlug: githubRepoSlug,
|
||||
repoSlug: githubRepoData?.slug,
|
||||
repoData: githubRepoData,
|
||||
entryWhenNoChanges:
|
||||
typeof config.entryWhenNoChanges === 'string'
|
||||
? interpolate(config.entryWhenNoChanges, {
|
||||
@ -1409,7 +1418,7 @@ export function shouldCreateGitHubRelease(
|
||||
return createReleaseArg === 'github';
|
||||
}
|
||||
|
||||
return (changelogConfig || {}).createRelease === 'github';
|
||||
return (changelogConfig || {}).createRelease !== false;
|
||||
}
|
||||
|
||||
async function promptForGitHubRelease(): Promise<boolean> {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -12,7 +12,11 @@
|
||||
* and easy to consume config object for all the `nx release` command implementations.
|
||||
*/
|
||||
import { join, relative } from 'node:path';
|
||||
import { NxJsonConfiguration } from '../../../config/nx-json';
|
||||
import { URL } from 'node:url';
|
||||
import {
|
||||
NxJsonConfiguration,
|
||||
NxReleaseChangelogConfiguration,
|
||||
} from '../../../config/nx-json';
|
||||
import { ProjectFileMap, ProjectGraph } from '../../../config/project-graph';
|
||||
import { readJsonFile } from '../../../utils/fileutils';
|
||||
import { findMatchingProjects } from '../../../utils/find-matching-projects';
|
||||
@ -41,15 +45,6 @@ type RemoveTrueFromProperties<T, K extends keyof T> = {
|
||||
type RemoveTrueFromPropertiesOnEach<T, K extends keyof T[keyof T]> = {
|
||||
[U in keyof T]: RemoveTrueFromProperties<T[U], K>;
|
||||
};
|
||||
|
||||
type RemoveFalseFromType<T> = T extends false ? never : T;
|
||||
type RemoveFalseFromProperties<T, K extends keyof T> = {
|
||||
[P in keyof T]: P extends K ? RemoveFalseFromType<T[P]> : T[P];
|
||||
};
|
||||
type RemoveFalseFromPropertiesOnEach<T, K extends keyof T[keyof T]> = {
|
||||
[U in keyof T]: RemoveFalseFromProperties<T[U], K>;
|
||||
};
|
||||
|
||||
type RemoveBooleanFromType<T> = T extends boolean ? never : T;
|
||||
type RemoveBooleanFromProperties<T, K extends keyof T> = {
|
||||
[P in keyof T]: P extends K ? RemoveBooleanFromType<T[P]> : T[P];
|
||||
@ -111,7 +106,11 @@ export interface CreateNxReleaseConfigError {
|
||||
| 'RELEASE_GROUP_RELEASE_TAG_PATTERN_VERSION_PLACEHOLDER_MISSING_OR_EXCESSIVE'
|
||||
| 'PROJECT_MATCHES_MULTIPLE_GROUPS'
|
||||
| 'CONVENTIONAL_COMMITS_SHORTHAND_MIXED_WITH_OVERLAPPING_GENERATOR_OPTIONS'
|
||||
| 'GLOBAL_GIT_CONFIG_MIXED_WITH_GRANULAR_GIT_CONFIG';
|
||||
| 'GLOBAL_GIT_CONFIG_MIXED_WITH_GRANULAR_GIT_CONFIG'
|
||||
| 'CANNOT_RESOLVE_CHANGELOG_RENDERER'
|
||||
| 'INVALID_CHANGELOG_CREATE_RELEASE_PROVIDER'
|
||||
| 'INVALID_CHANGELOG_CREATE_RELEASE_HOSTNAME'
|
||||
| 'INVALID_CHANGELOG_CREATE_RELEASE_API_BASE_URL';
|
||||
data: Record<string, string | string[]>;
|
||||
}
|
||||
|
||||
@ -566,7 +565,16 @@ export async function createNxReleaseConfig(
|
||||
releaseGroups[releaseGroupName] = finalReleaseGroup;
|
||||
}
|
||||
|
||||
ensureChangelogRenderersAreResolvable(releaseGroups, rootChangelogConfig);
|
||||
const configError = validateChangelogConfig(
|
||||
releaseGroups,
|
||||
rootChangelogConfig
|
||||
);
|
||||
if (configError) {
|
||||
return {
|
||||
error: configError,
|
||||
nxReleaseConfig: null,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
error: null,
|
||||
@ -766,6 +774,52 @@ export async function handleNxReleaseConfigError(
|
||||
});
|
||||
}
|
||||
break;
|
||||
case 'CANNOT_RESOLVE_CHANGELOG_RENDERER': {
|
||||
const nxJsonMessage = await resolveNxJsonConfigErrorMessage(['release']);
|
||||
output.error({
|
||||
title: `There was an error when resolving the configured changelog renderer at path: ${error.data.workspaceRelativePath}`,
|
||||
bodyLines: [nxJsonMessage],
|
||||
});
|
||||
}
|
||||
case 'INVALID_CHANGELOG_CREATE_RELEASE_PROVIDER':
|
||||
{
|
||||
const nxJsonMessage = await resolveNxJsonConfigErrorMessage([
|
||||
'release',
|
||||
]);
|
||||
output.error({
|
||||
title: `Your "changelog.createRelease" config specifies an unsupported provider "${
|
||||
error.data.provider
|
||||
}". The supported providers are ${(
|
||||
error.data.supportedProviders as string[]
|
||||
)
|
||||
.map((p) => `"${p}"`)
|
||||
.join(', ')}`,
|
||||
bodyLines: [nxJsonMessage],
|
||||
});
|
||||
}
|
||||
break;
|
||||
case 'INVALID_CHANGELOG_CREATE_RELEASE_HOSTNAME':
|
||||
{
|
||||
const nxJsonMessage = await resolveNxJsonConfigErrorMessage([
|
||||
'release',
|
||||
]);
|
||||
output.error({
|
||||
title: `Your "changelog.createRelease" config specifies an invalid hostname "${error.data.hostname}". Please ensure you provide a valid hostname value, such as "example.com"`,
|
||||
bodyLines: [nxJsonMessage],
|
||||
});
|
||||
}
|
||||
break;
|
||||
case 'INVALID_CHANGELOG_CREATE_RELEASE_API_BASE_URL':
|
||||
{
|
||||
const nxJsonMessage = await resolveNxJsonConfigErrorMessage([
|
||||
'release',
|
||||
]);
|
||||
output.error({
|
||||
title: `Your "changelog.createRelease" config specifies an invalid apiBaseUrl "${error.data.apiBaseUrl}". Please ensure you provide a valid URL value, such as "https://example.com"`,
|
||||
bodyLines: [nxJsonMessage],
|
||||
});
|
||||
}
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unhandled error code: ${error.code}`);
|
||||
}
|
||||
@ -950,10 +1004,16 @@ function isProjectPublic(
|
||||
}
|
||||
}
|
||||
|
||||
function ensureChangelogRenderersAreResolvable(
|
||||
/**
|
||||
* We need to ensure that changelog renderers are resolvable up front so that we do not end up erroring after performing
|
||||
* actions later, and we also make sure that any configured createRelease options are valid.
|
||||
*
|
||||
* For the createRelease config, we also set a default apiBaseUrl if applicable.
|
||||
*/
|
||||
function validateChangelogConfig(
|
||||
releaseGroups: NxReleaseConfig['groups'],
|
||||
rootChangelogConfig: NxReleaseConfig['changelog']
|
||||
) {
|
||||
): CreateNxReleaseConfigError | null {
|
||||
/**
|
||||
* If any form of changelog config is enabled, ensure that any provided changelog renderers are resolvable
|
||||
* up front so that we do not end up erroring only after the versioning step has been completed.
|
||||
@ -962,42 +1022,148 @@ function ensureChangelogRenderersAreResolvable(
|
||||
|
||||
if (
|
||||
rootChangelogConfig.workspaceChangelog &&
|
||||
typeof rootChangelogConfig.workspaceChangelog !== 'boolean' &&
|
||||
rootChangelogConfig.workspaceChangelog.renderer?.length
|
||||
typeof rootChangelogConfig.workspaceChangelog !== 'boolean'
|
||||
) {
|
||||
uniqueRendererPaths.add(rootChangelogConfig.workspaceChangelog.renderer);
|
||||
if (rootChangelogConfig.workspaceChangelog.renderer?.length) {
|
||||
uniqueRendererPaths.add(rootChangelogConfig.workspaceChangelog.renderer);
|
||||
}
|
||||
const createReleaseError = validateCreateReleaseConfig(
|
||||
rootChangelogConfig.workspaceChangelog
|
||||
);
|
||||
if (createReleaseError) {
|
||||
return createReleaseError;
|
||||
}
|
||||
}
|
||||
if (
|
||||
rootChangelogConfig.projectChangelogs &&
|
||||
typeof rootChangelogConfig.projectChangelogs !== 'boolean' &&
|
||||
rootChangelogConfig.projectChangelogs.renderer?.length
|
||||
typeof rootChangelogConfig.projectChangelogs !== 'boolean'
|
||||
) {
|
||||
uniqueRendererPaths.add(rootChangelogConfig.projectChangelogs.renderer);
|
||||
if (rootChangelogConfig.projectChangelogs.renderer?.length) {
|
||||
uniqueRendererPaths.add(rootChangelogConfig.projectChangelogs.renderer);
|
||||
}
|
||||
const createReleaseError = validateCreateReleaseConfig(
|
||||
rootChangelogConfig.projectChangelogs
|
||||
);
|
||||
if (createReleaseError) {
|
||||
return createReleaseError;
|
||||
}
|
||||
}
|
||||
|
||||
for (const group of Object.values(releaseGroups)) {
|
||||
if (
|
||||
group.changelog &&
|
||||
typeof group.changelog !== 'boolean' &&
|
||||
group.changelog.renderer?.length
|
||||
) {
|
||||
uniqueRendererPaths.add(group.changelog.renderer);
|
||||
if (group.changelog && typeof group.changelog !== 'boolean') {
|
||||
if (group.changelog.renderer?.length) {
|
||||
uniqueRendererPaths.add(group.changelog.renderer);
|
||||
}
|
||||
const createReleaseError = validateCreateReleaseConfig(group.changelog);
|
||||
if (createReleaseError) {
|
||||
return createReleaseError;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!uniqueRendererPaths.size) {
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
|
||||
for (const rendererPath of uniqueRendererPaths) {
|
||||
try {
|
||||
resolveChangelogRenderer(rendererPath);
|
||||
} catch (e) {
|
||||
const workspaceRelativePath = relative(workspaceRoot, rendererPath);
|
||||
output.error({
|
||||
title: `There was an error when resolving the configured changelog renderer at path: ${workspaceRelativePath}`,
|
||||
});
|
||||
throw e;
|
||||
} catch {
|
||||
return {
|
||||
code: 'CANNOT_RESOLVE_CHANGELOG_RENDERER',
|
||||
data: {
|
||||
workspaceRelativePath: relative(workspaceRoot, rendererPath),
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
const supportedCreateReleaseProviders = [
|
||||
{
|
||||
name: 'github-enterprise-server',
|
||||
defaultApiBaseUrl: 'https://__hostname__/api/v3',
|
||||
},
|
||||
];
|
||||
|
||||
// User opts into the default by specifying the string value 'github'
|
||||
export const defaultCreateReleaseProvider = {
|
||||
provider: 'github',
|
||||
hostname: 'github.com',
|
||||
apiBaseUrl: 'https://api.github.com',
|
||||
} as any;
|
||||
|
||||
function validateCreateReleaseConfig(
|
||||
changelogConfig: NxReleaseChangelogConfiguration
|
||||
): CreateNxReleaseConfigError | null {
|
||||
const createRelease = changelogConfig.createRelease;
|
||||
// Disabled: valid
|
||||
if (!createRelease) {
|
||||
return null;
|
||||
}
|
||||
// GitHub shorthand, expand to full object form, mark as valid
|
||||
if (createRelease === 'github') {
|
||||
changelogConfig.createRelease = defaultCreateReleaseProvider;
|
||||
return null;
|
||||
}
|
||||
// Object config, ensure that properties are valid
|
||||
const supportedProvider = supportedCreateReleaseProviders.find(
|
||||
(p) => p.name === createRelease.provider
|
||||
);
|
||||
if (!supportedProvider) {
|
||||
return {
|
||||
code: 'INVALID_CHANGELOG_CREATE_RELEASE_PROVIDER',
|
||||
data: {
|
||||
provider: createRelease.provider,
|
||||
supportedProviders: supportedCreateReleaseProviders.map((p) => p.name),
|
||||
},
|
||||
};
|
||||
}
|
||||
if (!isValidHostname(createRelease.hostname)) {
|
||||
return {
|
||||
code: 'INVALID_CHANGELOG_CREATE_RELEASE_HOSTNAME',
|
||||
data: {
|
||||
hostname: createRelease.hostname,
|
||||
},
|
||||
};
|
||||
}
|
||||
// user provided a custom apiBaseUrl, ensure it is valid (accounting for empty string case)
|
||||
if (
|
||||
createRelease.apiBaseUrl ||
|
||||
typeof createRelease.apiBaseUrl === 'string'
|
||||
) {
|
||||
if (!isValidUrl(createRelease.apiBaseUrl)) {
|
||||
return {
|
||||
code: 'INVALID_CHANGELOG_CREATE_RELEASE_API_BASE_URL',
|
||||
data: {
|
||||
apiBaseUrl: createRelease.apiBaseUrl,
|
||||
},
|
||||
};
|
||||
}
|
||||
} else {
|
||||
// Set default apiBaseUrl when not provided by the user
|
||||
createRelease.apiBaseUrl = supportedProvider.defaultApiBaseUrl.replace(
|
||||
'__hostname__',
|
||||
createRelease.hostname
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function isValidHostname(hostname) {
|
||||
// Regular expression to match a valid hostname
|
||||
const hostnameRegex =
|
||||
/^(?!:\/\/)(?=.{1,255}$)(?!.*\.$)(?!.*?\.\.)(?!.*?-$)(?!^-)([a-zA-Z0-9-]{1,63}\.?)+[a-zA-Z]{2,}$/;
|
||||
return hostnameRegex.test(hostname);
|
||||
}
|
||||
|
||||
function isValidUrl(str: string): boolean {
|
||||
try {
|
||||
new URL(str);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@ -252,6 +252,9 @@ export function createAPI(overrideReleaseConfig: NxReleaseConfiguration) {
|
||||
|
||||
latestCommit = await getCommitHash('HEAD');
|
||||
await createOrUpdateGithubRelease(
|
||||
nxReleaseConfig.changelog.workspaceChangelog
|
||||
? nxReleaseConfig.changelog.workspaceChangelog.createRelease
|
||||
: false,
|
||||
changelogResult.workspaceChangelog.releaseVersion,
|
||||
changelogResult.workspaceChangelog.contents,
|
||||
latestCommit,
|
||||
@ -297,6 +300,9 @@ export function createAPI(overrideReleaseConfig: NxReleaseConfiguration) {
|
||||
}
|
||||
|
||||
await createOrUpdateGithubRelease(
|
||||
releaseGroup.changelog
|
||||
? releaseGroup.changelog.createRelease
|
||||
: false,
|
||||
changelog.releaseVersion,
|
||||
changelog.contents,
|
||||
latestCommit,
|
||||
|
||||
@ -8,8 +8,10 @@ import { prompt } from 'enquirer';
|
||||
import { execSync } from 'node:child_process';
|
||||
import { existsSync, promises as fsp } from 'node:fs';
|
||||
import { homedir } from 'node:os';
|
||||
import { NxReleaseChangelogConfiguration } from '../../../config/nx-json';
|
||||
import { output } from '../../../utils/output';
|
||||
import { joinPathFragments } from '../../../utils/path';
|
||||
import { defaultCreateReleaseProvider } from '../config/config';
|
||||
import { Reference } from './git';
|
||||
import { printDiff } from './print-changes';
|
||||
import { ReleaseVersion, noDiffInChangelogMessage } from './shared';
|
||||
@ -20,12 +22,14 @@ const axios = _axios as any as (typeof _axios)['default'];
|
||||
|
||||
export type RepoSlug = `${string}/${string}`;
|
||||
|
||||
export interface GithubRequestConfig {
|
||||
interface GithubRequestConfig {
|
||||
repo: string;
|
||||
hostname: string;
|
||||
apiBaseUrl: string;
|
||||
token: string | null;
|
||||
}
|
||||
|
||||
export interface GithubRelease {
|
||||
interface GithubRelease {
|
||||
id?: string;
|
||||
tag_name: string;
|
||||
target_commitish?: string;
|
||||
@ -35,19 +39,46 @@ export interface GithubRelease {
|
||||
prerelease?: boolean;
|
||||
}
|
||||
|
||||
export function getGitHubRepoSlug(remoteName = 'origin'): RepoSlug {
|
||||
export interface GithubRepoData {
|
||||
hostname: string;
|
||||
slug: RepoSlug;
|
||||
apiBaseUrl: string;
|
||||
}
|
||||
|
||||
export function getGitHubRepoData(
|
||||
remoteName = 'origin',
|
||||
createReleaseConfig: NxReleaseChangelogConfiguration['createRelease']
|
||||
): GithubRepoData | null {
|
||||
try {
|
||||
const remoteUrl = execSync(`git remote get-url ${remoteName}`, {
|
||||
encoding: 'utf8',
|
||||
stdio: 'pipe',
|
||||
}).trim();
|
||||
|
||||
// Use the default provider (github.com) if custom one is not specified or releases are disabled
|
||||
let hostname = defaultCreateReleaseProvider.hostname;
|
||||
let apiBaseUrl = defaultCreateReleaseProvider.apiBaseUrl;
|
||||
if (
|
||||
createReleaseConfig !== false &&
|
||||
typeof createReleaseConfig !== 'string'
|
||||
) {
|
||||
hostname = createReleaseConfig.hostname;
|
||||
apiBaseUrl = createReleaseConfig.apiBaseUrl;
|
||||
}
|
||||
|
||||
// Extract the 'user/repo' part from the URL
|
||||
const regex = /github\.com[/:]([\w-]+\/[\w-]+)/;
|
||||
const escapedHostname = hostname.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
const regexString = `${escapedHostname}[/:]([\\w.-]+/[\\w.-]+)(\\.git)?`;
|
||||
const regex = new RegExp(regexString);
|
||||
const match = remoteUrl.match(regex);
|
||||
|
||||
if (match && match[1]) {
|
||||
return match[1] as RepoSlug;
|
||||
return {
|
||||
hostname,
|
||||
apiBaseUrl,
|
||||
// Ensure any trailing .git is stripped
|
||||
slug: match[1].replace(/\.git$/, '') as RepoSlug,
|
||||
};
|
||||
} else {
|
||||
throw new Error(
|
||||
`Could not extract "user/repo" data from the resolved remote URL: ${remoteUrl}`
|
||||
@ -59,13 +90,14 @@ export function getGitHubRepoSlug(remoteName = 'origin'): RepoSlug {
|
||||
}
|
||||
|
||||
export async function createOrUpdateGithubRelease(
|
||||
createReleaseConfig: NxReleaseChangelogConfiguration['createRelease'],
|
||||
releaseVersion: ReleaseVersion,
|
||||
changelogContents: string,
|
||||
latestCommit: string,
|
||||
{ dryRun }: { dryRun: boolean }
|
||||
): Promise<void> {
|
||||
const githubRepoSlug = getGitHubRepoSlug();
|
||||
if (!githubRepoSlug) {
|
||||
const githubRepoData = getGitHubRepoData(undefined, createReleaseConfig);
|
||||
if (!githubRepoData) {
|
||||
output.error({
|
||||
title: `Unable to create a GitHub release because the GitHub repo slug could not be determined.`,
|
||||
bodyLines: [
|
||||
@ -75,9 +107,11 @@ export async function createOrUpdateGithubRelease(
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const token = await resolveGithubToken();
|
||||
const token = await resolveGithubToken(githubRepoData.hostname);
|
||||
const githubRequestConfig: GithubRequestConfig = {
|
||||
repo: githubRepoSlug,
|
||||
repo: githubRepoData.slug,
|
||||
hostname: githubRepoData.hostname,
|
||||
apiBaseUrl: githubRepoData.apiBaseUrl,
|
||||
token,
|
||||
};
|
||||
|
||||
@ -106,7 +140,7 @@ export async function createOrUpdateGithubRelease(
|
||||
}
|
||||
}
|
||||
|
||||
const logTitle = `https://github.com/${githubRepoSlug}/releases/tag/${releaseVersion.gitTag}`;
|
||||
const logTitle = `https://${githubRepoData.hostname}/${githubRepoData.slug}/releases/tag/${releaseVersion.gitTag}`;
|
||||
if (existingGithubReleaseForVersion) {
|
||||
console.error(
|
||||
`${chalk.white('UPDATE')} ${logTitle}${
|
||||
@ -304,7 +338,7 @@ async function syncGithubRelease(
|
||||
}
|
||||
}
|
||||
|
||||
export async function resolveGithubToken(): Promise<string | null> {
|
||||
async function resolveGithubToken(hostname: string): Promise<string | null> {
|
||||
// Try and resolve from the environment
|
||||
const tokenFromEnv = process.env.GITHUB_TOKEN || process.env.GH_TOKEN;
|
||||
if (tokenFromEnv) {
|
||||
@ -320,15 +354,15 @@ export async function resolveGithubToken(): Promise<string | null> {
|
||||
const yamlContents = await fsp.readFile(ghCLIPath, 'utf8');
|
||||
const { load } = require('@zkochan/js-yaml');
|
||||
const ghCLIConfig = load(yamlContents);
|
||||
if (ghCLIConfig['github.com']) {
|
||||
if (ghCLIConfig[hostname]) {
|
||||
// Web based session (the token is already embedded in the config)
|
||||
if (ghCLIConfig['github.com'].oauth_token) {
|
||||
return ghCLIConfig['github.com'].oauth_token;
|
||||
if (ghCLIConfig[hostname].oauth_token) {
|
||||
return ghCLIConfig[hostname].oauth_token;
|
||||
}
|
||||
// SSH based session (we need to dynamically resolve a token using the CLI)
|
||||
if (
|
||||
ghCLIConfig['github.com'].user &&
|
||||
ghCLIConfig['github.com'].git_protocol === 'ssh'
|
||||
ghCLIConfig[hostname].user &&
|
||||
ghCLIConfig[hostname].git_protocol === 'ssh'
|
||||
) {
|
||||
return execSync(`gh auth token`, {
|
||||
encoding: 'utf8',
|
||||
@ -337,6 +371,11 @@ export async function resolveGithubToken(): Promise<string | null> {
|
||||
}
|
||||
}
|
||||
}
|
||||
if (hostname !== 'github.com') {
|
||||
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.`
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@ -359,7 +398,7 @@ async function makeGithubRequest(
|
||||
return (
|
||||
await axios<any, any>(url, {
|
||||
...opts,
|
||||
baseURL: 'https://api.github.com',
|
||||
baseURL: config.apiBaseUrl,
|
||||
headers: {
|
||||
...(opts.headers as any),
|
||||
Authorization: config.token ? `Bearer ${config.token}` : undefined,
|
||||
@ -395,11 +434,18 @@ async function updateGithubRelease(
|
||||
|
||||
function githubNewReleaseURL(
|
||||
config: GithubRequestConfig,
|
||||
release: { version: string; body: string }
|
||||
release: GithubReleaseOptions
|
||||
) {
|
||||
return `https://github.com/${config.repo}/releases/new?tag=${
|
||||
// Parameters taken from https://github.com/isaacs/github/issues/1410#issuecomment-442240267
|
||||
let url = `https://${config.hostname}/${config.repo}/releases/new?tag=${
|
||||
release.version
|
||||
}&title=${release.version}&body=${encodeURIComponent(release.body)}`;
|
||||
}&title=${release.version}&body=${encodeURIComponent(release.body)}&target=${
|
||||
release.commit
|
||||
}`;
|
||||
if (release.prerelease) {
|
||||
url += '&prerelease=true';
|
||||
}
|
||||
return url;
|
||||
}
|
||||
|
||||
type RepoProvider = 'github';
|
||||
@ -411,27 +457,30 @@ const providerToRefSpec: Record<
|
||||
github: { 'pull-request': 'pull', hash: 'commit', issue: 'issues' },
|
||||
};
|
||||
|
||||
function formatReference(ref: Reference, repoSlug: `${string}/${string}`) {
|
||||
function formatReference(ref: Reference, repoData: GithubRepoData) {
|
||||
const refSpec = providerToRefSpec['github'];
|
||||
return `[${ref.value}](https://github.com/${repoSlug}/${
|
||||
return `[${ref.value}](https://${repoData.hostname}/${repoData.slug}/${
|
||||
refSpec[ref.type]
|
||||
}/${ref.value.replace(/^#/, '')})`;
|
||||
}
|
||||
|
||||
export function formatReferences(references: Reference[], repoSlug: RepoSlug) {
|
||||
export function formatReferences(
|
||||
references: Reference[],
|
||||
repoData: GithubRepoData
|
||||
) {
|
||||
const pr = references.filter((ref) => ref.type === 'pull-request');
|
||||
const issue = references.filter((ref) => ref.type === 'issue');
|
||||
if (pr.length > 0 || issue.length > 0) {
|
||||
return (
|
||||
' (' +
|
||||
[...pr, ...issue]
|
||||
.map((ref) => formatReference(ref, repoSlug))
|
||||
.map((ref) => formatReference(ref, repoData))
|
||||
.join(', ') +
|
||||
')'
|
||||
);
|
||||
}
|
||||
if (references.length > 0) {
|
||||
return ' (' + formatReference(references[0], repoSlug) + ')';
|
||||
return ' (' + formatReference(references[0], repoData) + ')';
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
@ -79,7 +79,17 @@ export interface NxReleaseChangelogConfiguration {
|
||||
* NOTE: if createRelease is set on a group of projects, it will cause the default releaseTagPattern of
|
||||
* "{projectName}@{version}" to be used for those projects, even when versioning everything together.
|
||||
*/
|
||||
createRelease?: 'github' | false;
|
||||
createRelease?:
|
||||
| false
|
||||
| 'github'
|
||||
| {
|
||||
provider: 'github-enterprise-server';
|
||||
hostname: string;
|
||||
/**
|
||||
* If not set, this will default to `https://${hostname}/api/v3`
|
||||
*/
|
||||
apiBaseUrl?: string;
|
||||
};
|
||||
/**
|
||||
* This can either be set to a string value that will be written to the changelog file(s)
|
||||
* at the workspace root and/or within project directories, or set to `false` to specify
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user