UNPKG

nx

Version:

The core Nx plugin contains the core functionality of Nx like the project graph, nx commands and task orchestration.

310 lines (309 loc) • 13.6 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.GithubRemoteReleaseClient = exports.defaultCreateReleaseProvider = void 0; 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 remote_release_client_1 = require("./remote-release-client"); // axios types and values don't seem to match const _axios = require("axios"); const axios = _axios; exports.defaultCreateReleaseProvider = { provider: 'github', hostname: 'github.com', apiBaseUrl: 'https://api.github.com', }; class GithubRemoteReleaseClient extends remote_release_client_1.RemoteReleaseClient { constructor() { super(...arguments); this.remoteReleaseProviderName = 'GitHub'; } /** * Get GitHub repository data from git remote */ static resolveRepoData(createReleaseConfig, remoteName = 'origin') { try { const remoteUrl = (0, node_child_process_1.execSync)(`git remote get-url ${remoteName}`, { encoding: 'utf8', stdio: 'pipe', }).trim(); // Use the default provider if custom one is not specified or releases are disabled let hostname = exports.defaultCreateReleaseProvider.hostname; let apiBaseUrl = exports.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; } } /** * Resolve a GitHub token from environment variables or gh CLI */ static async resolveTokenData(hostname) { // Try and resolve from the environment const tokenFromEnv = process.env.GITHUB_TOKEN || process.env.GH_TOKEN; if (tokenFromEnv) { return { token: tokenFromEnv, headerName: 'Authorization' }; } // 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') { const token = (0, node_child_process_1.execSync)(`gh auth token`, { encoding: 'utf8', stdio: 'pipe', windowsHide: false, }).trim(); return { token, headerName: 'Authorization' }; } } } 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; } createPostGitTask(releaseVersion, changelogContents, dryRun) { return async (latestCommit) => { output_1.output.logSingleLine(`Creating GitHub Release`); await this.createOrUpdateRelease(releaseVersion, changelogContents, latestCommit, { dryRun }); }; } async applyUsernameToAuthors(authors) { await Promise.all([...authors.keys()].map(async (authorName) => { const meta = authors.get(authorName); for (const email of meta.email) { if (email.endsWith('@users.noreply.github.com')) { const match = email.match(/^(\d+\+)?([^@]+)@users\.noreply\.github\.com$/); if (match && match[2]) { meta.username = match[2]; break; } } const { data } = await axios .get(`https://ungh.cc/users/find/${email}`) .catch(() => ({ data: { user: null } })); if (data?.user) { meta.username = data.user.username; break; } } })); } /** * Get a release by tag */ async getReleaseByTag(tag) { const githubRepoData = this.getRequiredRemoteRepoData(); return await this.makeRequest(`/repos/${githubRepoData.slug}/releases/tags/${tag}`); } /** * Create a new release */ async createRelease(remoteRelease) { const githubRepoData = this.getRequiredRemoteRepoData(); return await this.makeRequest(`/repos/${githubRepoData.slug}/releases`, { method: 'POST', data: remoteRelease, }); } async updateRelease(id, remoteRelease) { const githubRepoData = this.getRequiredRemoteRepoData(); return await this.makeRequest(`/repos/${githubRepoData.slug}/releases/${id}`, { method: 'PATCH', data: remoteRelease, }); } getManualRemoteReleaseURL(remoteReleaseOptions) { const githubRepoData = this.getRequiredRemoteRepoData(); // Parameters taken from https://github.com/isaacs/github/issues/1410#issuecomment-442240267 let url = `https://${githubRepoData.hostname}/${githubRepoData.slug}/releases/new?tag=${remoteReleaseOptions.version}&title=${remoteReleaseOptions.version}&body=${encodeURIComponent(remoteReleaseOptions.body)}&target=${remoteReleaseOptions.commit}`; if (remoteReleaseOptions.prerelease) { url += '&prerelease=true'; } return url; } handleAuthError() { 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', ], }); } logReleaseAction(existingRelease, gitTag, dryRun) { const githubRepoData = this.getRequiredRemoteRepoData(); const logTitle = `https://${githubRepoData.hostname}/${githubRepoData.slug}/releases/tag/${gitTag}`; if (existingRelease) { 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]') : ''}`); } } async handleError(error, result) { if (error) { process.exitCode = 1; if (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(error.response.data)}`, `---`, `Request Data:`, `Repo: ${this.getRemoteRepoData()?.slug}`, `Token Header Data: ${this.tokenHeader}`, `Body: ${JSON.stringify(result.requestData)}`, ], }); } else { console.log(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 this.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'); }); } async 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 { // Ensure the cursor is always restored before exiting process.stdout.write('\u001b[?25h'); // Handle the case where the user exits the prompt with ctrl+c process.exit(1); } } /** * Format references for the release (e.g., PRs, issues) */ formatReferences(references) { const githubRepoData = this.getRequiredRemoteRepoData(); const providerToRefSpec = { github: { 'pull-request': 'pull', hash: 'commit', issue: 'issues' }, }; const refSpec = providerToRefSpec.github; const formatSingleReference = (ref) => { return `[${ref.value}](https://${githubRepoData.hostname}/${githubRepoData.slug}/${refSpec[ref.type]}/${ref.value.replace(/^#/, '')})`; }; 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) => formatSingleReference(ref)).join(', ') + ')'); } if (references.length > 0) { return ' (' + formatSingleReference(references[0]) + ')'; } return ''; } async syncRelease(remoteReleaseOptions, existingRelease) { const githubReleaseData = { tag_name: remoteReleaseOptions.version, name: remoteReleaseOptions.version, body: remoteReleaseOptions.body, prerelease: remoteReleaseOptions.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 (existingRelease ? this.updateRelease(existingRelease.id, githubReleaseData) : this.createRelease({ ...githubReleaseData, target_commitish: remoteReleaseOptions.commit, })); return { status: existingRelease ? 'updated' : 'created', id: newGhRelease.id, url: newGhRelease.html_url, }; } catch (error) { return { status: 'manual', error, url: this.getManualRemoteReleaseURL(remoteReleaseOptions), requestData: githubReleaseData, }; } } getRequiredRemoteRepoData() { const githubRepoData = this.getRemoteRepoData(); if (!githubRepoData) { throw new Error(`No remote repo data could be resolved for the current workspace`); } return githubRepoData; } } exports.GithubRemoteReleaseClient = GithubRemoteReleaseClient;