nx
Version:
325 lines (324 loc) • 13.6 kB
JavaScript
Object.defineProperty(exports, "__esModule", { value: true });
exports.getGitHubRepoData = getGitHubRepoData;
exports.createOrUpdateGithubRelease = createOrUpdateGithubRelease;
exports.getGithubReleaseByTag = getGithubReleaseByTag;
exports.formatReferences = formatReferences;
const chalk = require("chalk");
const enquirer_1 = require("enquirer");
const node_child_process_1 = require("node:child_process");
const node_fs_1 = require("node:fs");
const node_os_1 = require("node:os");
const output_1 = require("../../../utils/output");
const path_1 = require("../../../utils/path");
const config_1 = require("../config/config");
const print_changes_1 = require("./print-changes");
const shared_1 = require("./shared");
// axios types and values don't seem to match
const _axios = require("axios");
const axios = _axios;
function getGitHubRepoData(remoteName = 'origin', createReleaseConfig) {
try {
const remoteUrl = (0, node_child_process_1.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 = config_1.defaultCreateReleaseProvider.hostname;
let apiBaseUrl = config_1.defaultCreateReleaseProvider.apiBaseUrl;
if (createReleaseConfig !== false &&
typeof createReleaseConfig !== 'string') {
hostname = createReleaseConfig.hostname;
apiBaseUrl = createReleaseConfig.apiBaseUrl;
}
// Extract the 'user/repo' part from the URL
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 {
hostname,
apiBaseUrl,
// Ensure any trailing .git is stripped
slug: match[1].replace(/\.git$/, ''),
};
}
else {
throw new Error(`Could not extract "user/repo" data from the resolved remote URL: ${remoteUrl}`);
}
}
catch (error) {
return null;
}
}
async function createOrUpdateGithubRelease(createReleaseConfig, releaseVersion, changelogContents, latestCommit, { dryRun }) {
const githubRepoData = getGitHubRepoData(undefined, createReleaseConfig);
if (!githubRepoData) {
output_1.output.error({
title: `Unable to create a GitHub release because the GitHub repo slug could not be determined.`,
bodyLines: [
`Please ensure you have a valid GitHub remote configured. You can run \`git remote -v\` to list your current remotes.`,
],
});
process.exit(1);
}
const token = await resolveGithubToken(githubRepoData.hostname);
const githubRequestConfig = {
repo: githubRepoData.slug,
hostname: githubRepoData.hostname,
apiBaseUrl: githubRepoData.apiBaseUrl,
token,
};
let existingGithubReleaseForVersion;
try {
existingGithubReleaseForVersion = await getGithubReleaseByTag(githubRequestConfig, releaseVersion.gitTag);
}
catch (err) {
if (err.response?.status === 401) {
output_1.output.error({
title: `Unable to resolve data via the GitHub API. You can use any of the following options to resolve this:`,
bodyLines: [
'- Set the `GITHUB_TOKEN` or `GH_TOKEN` environment variable to a valid GitHub token with `repo` scope',
'- Have an active session via the official gh CLI tool (https://cli.github.com) in your current terminal',
],
});
process.exit(1);
}
if (err.response?.status === 404) {
// No existing release found, this is fine
}
else {
// Rethrow unknown errors for now
throw err;
}
}
const logTitle = `https://${githubRepoData.hostname}/${githubRepoData.slug}/releases/tag/${releaseVersion.gitTag}`;
if (existingGithubReleaseForVersion) {
console.error(`${chalk.white('UPDATE')} ${logTitle}${dryRun ? chalk.keyword('orange')(' [dry-run]') : ''}`);
}
else {
console.error(`${chalk.green('CREATE')} ${logTitle}${dryRun ? chalk.keyword('orange')(' [dry-run]') : ''}`);
}
console.log('');
(0, print_changes_1.printDiff)(existingGithubReleaseForVersion ? existingGithubReleaseForVersion.body : '', changelogContents, 3, shared_1.noDiffInChangelogMessage);
if (!dryRun) {
await createOrUpdateGithubReleaseInternal(githubRequestConfig, {
version: releaseVersion.gitTag,
prerelease: releaseVersion.isPrerelease,
body: changelogContents,
commit: latestCommit,
}, existingGithubReleaseForVersion);
}
}
async function createOrUpdateGithubReleaseInternal(githubRequestConfig, release, existingGithubReleaseForVersion) {
const result = await syncGithubRelease(githubRequestConfig, release, existingGithubReleaseForVersion);
/**
* If something went wrong POSTing to Github we can still pre-populate the web form on github.com
* to allow the user to manually complete the release if they so choose.
*/
if (result.status === 'manual') {
if (result.error) {
process.exitCode = 1;
if (result.error.response?.data) {
// There's a nicely formatted error from GitHub we can display to the user
output_1.output.error({
title: `A GitHub API Error occurred when creating/updating the release`,
bodyLines: [
`GitHub Error: ${JSON.stringify(result.error.response.data)}`,
`---`,
`Request Data:`,
`Repo: ${githubRequestConfig.repo}`,
`Token: ${githubRequestConfig.token}`,
`Body: ${JSON.stringify(result.requestData)}`,
],
});
}
else {
console.log(result.error);
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!)`);
}
}
const shouldContinueInGitHub = await promptForContinueInGitHub();
if (!shouldContinueInGitHub) {
return;
}
const open = require('open');
await open(result.url)
.then(() => {
console.info(`\nFollow up in the browser to manually create the release:\n\n` +
chalk.underline(chalk.cyan(result.url)) +
`\n`);
})
.catch(() => {
console.info(`Open this link to manually create a release: \n` +
chalk.underline(chalk.cyan(result.url)) +
'\n');
});
}
/**
* If something went wrong POSTing to Github we can still pre-populate the web form on github.com
* to allow the user to manually complete the release.
*/
if (result.status === 'manual') {
if (result.error) {
console.error(result.error);
process.exitCode = 1;
}
const open = require('open');
await open(result.url)
.then(() => {
console.info(`Follow up in the browser to manually create the release.`);
})
.catch(() => {
console.info(`Open this link to manually create a release: \n` +
chalk.underline(chalk.cyan(result.url)) +
'\n');
});
}
}
async function promptForContinueInGitHub() {
try {
const reply = await (0, enquirer_1.prompt)([
{
name: 'open',
message: 'Do you want to finish creating the release manually in your browser?',
type: 'autocomplete',
choices: [
{
name: 'Yes',
hint: 'It will pre-populate the form for you',
},
{
name: 'No',
},
],
initial: 0,
},
]);
return reply.open === 'Yes';
}
catch (e) {
// Handle the case where the user exits the prompt with ctrl+c
process.exit(1);
}
}
async function syncGithubRelease(githubRequestConfig, release, existingGithubReleaseForVersion) {
const ghRelease = {
tag_name: release.version,
name: release.version,
body: release.body,
prerelease: release.prerelease,
// legacy specifies that the latest release should be determined based on the release creation date and higher semantic version.
make_latest: 'legacy',
};
try {
const newGhRelease = await (existingGithubReleaseForVersion
? updateGithubRelease(githubRequestConfig, existingGithubReleaseForVersion.id, ghRelease)
: createGithubRelease(githubRequestConfig, {
...ghRelease,
target_commitish: release.commit,
}));
return {
status: existingGithubReleaseForVersion ? 'updated' : 'created',
id: newGhRelease.id,
url: newGhRelease.html_url,
};
}
catch (error) {
return {
status: 'manual',
error,
url: githubNewReleaseURL(githubRequestConfig, release),
requestData: ghRelease,
};
}
}
async function resolveGithubToken(hostname) {
// Try and resolve from the environment
const tokenFromEnv = process.env.GITHUB_TOKEN || process.env.GH_TOKEN;
if (tokenFromEnv) {
return tokenFromEnv;
}
// Try and resolve from gh CLI installation
const ghCLIPath = (0, path_1.joinPathFragments)(process.env.XDG_CONFIG_HOME || (0, path_1.joinPathFragments)((0, node_os_1.homedir)(), '.config'), 'gh', 'hosts.yml');
if ((0, node_fs_1.existsSync)(ghCLIPath)) {
const yamlContents = await node_fs_1.promises.readFile(ghCLIPath, 'utf8');
const { load } = require('@zkochan/js-yaml');
const ghCLIConfig = load(yamlContents);
if (ghCLIConfig[hostname]) {
// Web based session (the token is already embedded in the config)
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[hostname].user &&
ghCLIConfig[hostname].git_protocol === 'ssh') {
return (0, node_child_process_1.execSync)(`gh auth token`, {
encoding: 'utf8',
stdio: 'pipe',
windowsHide: false,
}).trim();
}
}
}
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;
}
async function getGithubReleaseByTag(config, tag) {
return await makeGithubRequest(config, `/repos/${config.repo}/releases/tags/${tag}`, {});
}
async function makeGithubRequest(config, url, opts = {}) {
return (await axios(url, {
...opts,
baseURL: config.apiBaseUrl,
headers: {
...opts.headers,
Authorization: config.token ? `Bearer ${config.token}` : undefined,
},
})).data;
}
async function createGithubRelease(config, body) {
return await makeGithubRequest(config, `/repos/${config.repo}/releases`, {
method: 'POST',
data: body,
});
}
async function updateGithubRelease(config, id, body) {
return await makeGithubRequest(config, `/repos/${config.repo}/releases/${id}`, {
method: 'PATCH',
data: body,
});
}
function githubNewReleaseURL(config, release) {
// 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)}&target=${release.commit}`;
if (release.prerelease) {
url += '&prerelease=true';
}
return url;
}
const providerToRefSpec = {
github: { 'pull-request': 'pull', hash: 'commit', issue: 'issues' },
};
function formatReference(ref, repoData) {
const refSpec = providerToRefSpec['github'];
return `[${ref.value}](https://${repoData.hostname}/${repoData.slug}/${refSpec[ref.type]}/${ref.value.replace(/^#/, '')})`;
}
function formatReferences(references, repoData) {
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, repoData))
.join(', ') +
')');
}
if (references.length > 0) {
return ' (' + formatReference(references[0], repoData) + ')';
}
return '';
}
;