nx
Version:
272 lines (271 loc) • 11.8 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.GitLabRemoteReleaseClient = exports.defaultCreateReleaseProvider = void 0;
const chalk = require("chalk");
const enquirer_1 = require("enquirer");
const node_child_process_1 = require("node:child_process");
const output_1 = require("../../../../utils/output");
const remote_release_client_1 = require("./remote-release-client");
exports.defaultCreateReleaseProvider = {
provider: 'gitlab',
hostname: 'gitlab.com',
apiBaseUrl: 'https://gitlab.com/api/v4',
};
class GitLabRemoteReleaseClient extends remote_release_client_1.RemoteReleaseClient {
constructor() {
super(...arguments);
this.remoteReleaseProviderName = 'GitLab';
}
/**
* Get GitLab 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 || hostname;
apiBaseUrl = createReleaseConfig.apiBaseUrl || apiBaseUrl;
}
// Extract the project path from the URL
const escapedHostname = hostname.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const regexString = `${escapedHostname}[/:]([\\w.-]+/[\\w.-]+(?:/[\\w.-]+)*)(\\.git)?`;
const regex = new RegExp(regexString);
const match = remoteUrl.match(regex);
if (match && match[1]) {
// Remove trailing .git if present
const slug = match[1].replace(/\.git$/, '');
// Encode the project path for use in API URLs
const projectId = encodeURIComponent(slug);
return {
hostname,
apiBaseUrl,
slug,
projectId,
};
}
else {
throw new Error(`Could not extract project path data from the resolved remote URL: ${remoteUrl}`);
}
}
catch (err) {
if (process.env.NX_VERBOSE_LOGGING === 'true') {
console.error(err);
}
return null;
}
}
/**
* Resolve a GitLab token from various environment variables
*/
static async resolveTokenData(hostname) {
// Try and resolve from the environment
const tokenFromEnv = process.env.GITLAB_TOKEN || process.env.GL_TOKEN;
if (tokenFromEnv) {
return { token: tokenFromEnv, headerName: 'PRIVATE-TOKEN' };
}
// Try and resolve from a CI environment
if (process.env.CI_JOB_TOKEN) {
return { token: process.env.CI_JOB_TOKEN, headerName: 'JOB-TOKEN' };
}
if (hostname !== 'gitlab.com') {
console.log(`Warning: It was not possible to automatically resolve a GitLab token from your environment for hostname ${hostname}. If you set the GITLAB_TOKEN or GL_TOKEN environment variable (or you are in GitLab CI where CI_JOB_TOKEN is set automatically), that will be used for GitLab API requests.`);
}
return null;
}
createPostGitTask(releaseVersion, changelogContents, dryRun) {
return async (latestCommit) => {
output_1.output.logSingleLine(`Creating GitLab Release`);
await this.createOrUpdateRelease(releaseVersion, changelogContents, latestCommit, { dryRun });
};
}
// Not implemented for GitLab yet, the changelog renderer should not call this method
async applyUsernameToAuthors() {
throw new Error('applyUsernameToAuthors is not implemented for GitLab yet');
}
async getReleaseByTag(tag) {
const gitlabRepoData = this.getRequiredRemoteRepoData();
return await this.makeRequest(`/projects/${gitlabRepoData.projectId}/releases/${encodeURIComponent(tag)}`);
}
async createRelease(remoteRelease) {
const gitlabRepoData = this.getRequiredRemoteRepoData();
return await this.makeRequest(`/projects/${gitlabRepoData.projectId}/releases`, {
method: 'POST',
data: remoteRelease,
});
}
async updateRelease(_id, remoteRelease) {
const gitlabRepoData = this.getRequiredRemoteRepoData();
return await this.makeRequest(`/projects/${gitlabRepoData.projectId}/releases/${encodeURIComponent(remoteRelease.tag_name)}`, {
method: 'PUT',
data: remoteRelease,
});
}
/**
* Generate a URL for manual release creation on GitLab. Sadly, unlike GitHub, GitLab does not
* seem to respect query string parameters for setting the UI form fields, so the user has to
* start from scratch.
*/
getManualRemoteReleaseURL(_remoteReleaseOptions) {
const gitlabRepoData = this.getRequiredRemoteRepoData();
return `https://${gitlabRepoData.hostname}/${gitlabRepoData.slug}/-/releases/new`;
}
handleAuthError() {
output_1.output.error({
title: `Unable to resolve data via the GitLab API.`,
bodyLines: [
'- Set the `GITLAB_TOKEN` or `GL_TOKEN` environment variable to a valid GitLab token with `api` scope',
'- If running in GitLab CI, the automatically provisioned CI_JOB_TOKEN can also be used',
],
});
}
logReleaseAction(existingRelease, gitTag, dryRun) {
const gitlabRepoData = this.getRequiredRemoteRepoData();
const logTitle = `https://${gitlabRepoData.hostname}/${gitlabRepoData.slug}/-/releases/${encodeURIComponent(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) {
output_1.output.error({
title: `A GitLab API Error occurred when creating/updating the release`,
bodyLines: [
`GitLab 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 GitLab, please report this on https://github.com/nrwl/nx (NOTE: make sure to redact your GitLab token from the error message!)`);
}
}
const shouldContinueInGitLab = await this.promptForContinueInGitLab();
if (!shouldContinueInGitLab) {
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 promptForContinueInGitLab() {
try {
const reply = await (0, enquirer_1.prompt)([
{
name: 'open',
message: 'Do you want to create the release manually in your browser?',
type: 'autocomplete',
choices: [
{
name: 'Yes',
hint: 'It will open the GitLab release page 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., MRs, issues)
*/
formatReferences(references) {
const gitlabRepoData = this.getRequiredRemoteRepoData();
const providerToRefSpec = {
gitlab: {
'pull-request': 'merge_requests',
hash: 'commit',
issue: 'issues',
},
};
const refSpec = providerToRefSpec.gitlab;
const formatSingleReference = (ref) => {
return `https://${gitlabRepoData.hostname}/${gitlabRepoData.slug}/-/${refSpec[ref.type]}/${ref.value.replace(/^[#!]/, '')}`;
};
const mr = references.filter((ref) => ref.type === 'pull-request');
const issue = references.filter((ref) => ref.type === 'issue');
if (mr.length > 0 || issue.length > 0) {
return (' (' +
[...mr, ...issue].map((ref) => formatSingleReference(ref)).join(', ') +
')');
}
if (references.length > 0) {
return ' (' + formatSingleReference(references[0]) + ')';
}
return '';
}
async syncRelease(remoteReleaseOptions, existingRelease) {
const gitlabReleaseData = {
tag_name: remoteReleaseOptions.version,
name: remoteReleaseOptions.version,
description: remoteReleaseOptions.body,
prerelease: remoteReleaseOptions.prerelease,
ref: remoteReleaseOptions.commit,
released_at: new Date().toISOString(),
assets: { links: [] },
milestones: [],
};
try {
const newGlRelease = await (existingRelease
? this.updateRelease(existingRelease.id, gitlabReleaseData)
: this.createRelease(gitlabReleaseData));
const gitlabRepoData = this.getRequiredRemoteRepoData();
return {
status: existingRelease ? 'updated' : 'created',
id: newGlRelease.tag_name,
url: `https://${gitlabRepoData.hostname}/${gitlabRepoData.slug}/-/tags/${encodeURIComponent(remoteReleaseOptions.version)}`,
};
}
catch (error) {
return {
status: 'manual',
error,
url: this.getManualRemoteReleaseURL(remoteReleaseOptions),
requestData: gitlabReleaseData,
};
}
}
getRequiredRemoteRepoData() {
const gitlabRepoData = this.getRemoteRepoData();
if (!gitlabRepoData) {
throw new Error(`No remote repo data could be resolved for the current workspace`);
}
return gitlabRepoData;
}
}
exports.GitLabRemoteReleaseClient = GitLabRemoteReleaseClient;