feat(release): support github enterprise server (#26482)

This commit is contained in:
James Henry 2024-09-20 16:15:15 +04:00 committed by GitHub
parent 0d37ef98da
commit 21d1696ef5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 1750 additions and 79 deletions

View File

@ -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;

View File

@ -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": {

View File

@ -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

View File

@ -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'
) {
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'
) {
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
) {
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;
}
}

View File

@ -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,

View File

@ -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 '';
}

View File

@ -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