mirror of
https://github.com/jdalrymple/gitbeaker.git
synced 2026-01-25 16:04:01 +00:00
312 lines
8.8 KiB
JavaScript
312 lines
8.8 KiB
JavaScript
#!/usr/bin/env node
|
|
|
|
import { execSync } from 'child_process';
|
|
import { writeFileSync, existsSync, mkdirSync } from 'fs';
|
|
|
|
const isCanary = process.argv[2] === 'canary';
|
|
const releaseType = isCanary ? 'canary' : 'production';
|
|
const emoji = isCanary ? '🐤' : '🚀';
|
|
|
|
const labelToChangeType = {
|
|
breaking: 'major',
|
|
'type:feature': 'minor',
|
|
'type:bug': 'minor',
|
|
'type:hot fix': 'minor',
|
|
'type:technical debt': 'patch',
|
|
'type:security': 'patch',
|
|
'type:dependencies': 'patch',
|
|
'type:types': 'patch',
|
|
'type:testing': null,
|
|
'type:documentation': null,
|
|
'release:canary': 'patch',
|
|
};
|
|
|
|
function logStep(message) {
|
|
console.log(`${emoji} ${message}`);
|
|
}
|
|
|
|
function execCommand(command, description) {
|
|
logStep(description);
|
|
try {
|
|
execSync(command, { stdio: 'inherit' });
|
|
return true;
|
|
} catch (error) {
|
|
console.error(`❌ Failed: ${description}`);
|
|
console.error(error.message);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
function getPackageNames() {
|
|
try {
|
|
const output = execSync('yarn workspaces list --json', { encoding: 'utf8' });
|
|
const workspaces = output
|
|
.trim()
|
|
.split('\n')
|
|
.map((line) => JSON.parse(line));
|
|
|
|
return workspaces.filter((ws) => ws.location !== '.' && ws.name).map((ws) => ws.name);
|
|
} catch (error) {
|
|
console.warn('Could not get workspace packages:', error.message);
|
|
return [];
|
|
}
|
|
}
|
|
|
|
function generateChangesetYaml(packageNames, changeType) {
|
|
if (packageNames.length === 0) return '';
|
|
return packageNames.map((name) => `"${name}": ${changeType}`).join('\n');
|
|
}
|
|
|
|
function extractPublishedPackages(publishOutput, isCanary) {
|
|
const publishLines = publishOutput.split('\n');
|
|
|
|
return publishLines
|
|
.filter((line) => {
|
|
return line.includes('@') && (isCanary ? line.includes('canary') : !line.includes('canary'));
|
|
})
|
|
.map((line) => {
|
|
// Extract package@version from changeset output lines
|
|
const regex = isCanary ? /(@[^@]+@[\d\.-]+canary[\d-]+)/ : /(@[^@\s]+@[\d\.\-\w]+)/;
|
|
const match = line.match(regex);
|
|
return match ? match[1] : null;
|
|
})
|
|
.filter(Boolean);
|
|
}
|
|
|
|
function getRepoInfo() {
|
|
const repoUrl = process.env.CIRCLE_REPOSITORY_URL || 'https://github.com/jdalrymple/gitbeaker';
|
|
|
|
// Extract owner/repo from URL
|
|
const match = repoUrl.match(/github\.com[/:]([\w-]+)\/([\w-]+)/);
|
|
if (!match) {
|
|
throw new Error(`Could not parse repository URL: ${repoUrl}`);
|
|
}
|
|
|
|
return {
|
|
owner: match[1],
|
|
repo: match[2],
|
|
};
|
|
}
|
|
|
|
async function githubApiRequest(endpoint, options = {}) {
|
|
const { owner, repo } = getRepoInfo();
|
|
const url = `https://api.github.com/repos/${owner}/${repo}${endpoint}`;
|
|
|
|
const response = await fetch(url, {
|
|
headers: {
|
|
Authorization: `Bearer ${process.env.GITHUB_TOKEN}`,
|
|
Accept: 'application/vnd.github.v3+json',
|
|
'User-Agent': 'gitbeaker-api-client',
|
|
'Content-Type': 'application/json',
|
|
...options.headers,
|
|
},
|
|
...options,
|
|
});
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`GitHub API error: ${response.status} ${response.statusText}`);
|
|
}
|
|
|
|
return response.json();
|
|
}
|
|
|
|
async function generateChangesetFromPR(prNumber, labels, prTitle) {
|
|
if (!prNumber) {
|
|
logStep('No PR number provided, skipping changeset generation');
|
|
return null;
|
|
}
|
|
|
|
logStep(`Generating changeset for PR #${prNumber} with labels: ${labels.join(', ')}`);
|
|
|
|
// Find change type
|
|
const changeType = labels
|
|
.map((label) => labelToChangeType[label])
|
|
.filter(Boolean)
|
|
.sort(
|
|
(a, b) => ['major', 'minor', 'patch'].indexOf(a) - ['major', 'minor', 'patch'].indexOf(b),
|
|
)[0];
|
|
|
|
if (!changeType) {
|
|
logStep('No labels found that trigger a release, skipping changeset generation');
|
|
return null;
|
|
}
|
|
|
|
logStep(`Determined change type: ${changeType}`);
|
|
|
|
// Get package names using yarn workspaces
|
|
const packageNames = getPackageNames();
|
|
if (packageNames.length === 0) {
|
|
console.warn('No packages found in yarn workspaces');
|
|
return null;
|
|
}
|
|
|
|
logStep(`Found packages: ${packageNames.join(', ')}`);
|
|
|
|
// Create changeset directory if it doesn't exist
|
|
if (!existsSync('.changeset')) {
|
|
mkdirSync('.changeset');
|
|
}
|
|
|
|
const changesetContent = `---
|
|
${generateChangesetYaml(packageNames, changeType)}
|
|
---
|
|
|
|
${prTitle}`;
|
|
|
|
const filename = `.changeset/pr-${prNumber}-${Date.now()}.md`;
|
|
writeFileSync(filename, changesetContent);
|
|
|
|
logStep(`Generated changeset: ${filename}`);
|
|
return filename;
|
|
}
|
|
|
|
async function release() {
|
|
logStep(`Starting ${releaseType} release`);
|
|
|
|
const prNumber = process.env.PR_NUMBER;
|
|
|
|
if (!prNumber) {
|
|
logStep('No PR number found - skipping release');
|
|
return;
|
|
}
|
|
|
|
// Get PR data
|
|
const prData = await githubApiRequest(`/pulls/${prNumber}`);
|
|
const labels = prData.labels.map((label) => label.name);
|
|
|
|
if (isCanary && !labels.includes('release:canary')) {
|
|
logStep('No canary label present - skipping canary release');
|
|
return;
|
|
}
|
|
|
|
// Generate changesets (direct function call, no subprocess)
|
|
logStep('Generating changeset from PR labels');
|
|
try {
|
|
const changesetFile = await generateChangesetFromPR(prNumber, labels, prData.title);
|
|
if (!changesetFile) {
|
|
logStep(`No changeset generated - skipping ${releaseType} release`);
|
|
return;
|
|
}
|
|
} catch (error) {
|
|
console.error(`❌ Failed to generate changeset: ${error.message}`);
|
|
process.exit(1);
|
|
}
|
|
|
|
// Version packages
|
|
const versionCommand = isCanary
|
|
? 'yarn changeset version --snapshot canary'
|
|
: 'yarn changeset version';
|
|
|
|
if (!execCommand(versionCommand, `Creating ${releaseType} versions`)) {
|
|
process.exit(1);
|
|
}
|
|
|
|
// Update contributors (production only)
|
|
if (!isCanary) {
|
|
execCommand('yarn all-contributors-cli generate', 'Updating contributors (non-blocking)');
|
|
}
|
|
|
|
// Publish packages
|
|
const publishCommand = isCanary
|
|
? 'yarn changeset publish --tag canary --no-git-tag'
|
|
: 'yarn changeset publish';
|
|
|
|
let publishedPackages = [];
|
|
|
|
try {
|
|
// Capture publish output to extract version info
|
|
logStep(`Publishing ${releaseType} packages`);
|
|
|
|
const publishOutput = execSync(publishCommand, { stdio: 'pipe', encoding: 'utf8' });
|
|
|
|
logStep(publishOutput); // Show the output to user
|
|
|
|
publishedPackages = extractPublishedPackages(publishOutput, isCanary);
|
|
} catch (error) {
|
|
console.error(`❌ Failed to parse published packages: ${error.message}`);
|
|
process.exit(1);
|
|
}
|
|
|
|
// Post PR comment for releases
|
|
if (prNumber && publishedPackages.length > 0) {
|
|
try {
|
|
const releaseTitle = isCanary ? 'Canary Release Published' : 'Production Release Published';
|
|
const releaseDescription = isCanary ? 'canary versions' : 'new versions';
|
|
const installNote = isCanary
|
|
? 'Note: Canary releases are temporary and may be unstable. Use for testing purposes only.'
|
|
: 'Note: These are production releases available on the `latest` tag.';
|
|
|
|
logStep(`Posting ${releaseType} release comment to PR`);
|
|
|
|
const releaseLinks = publishedPackages
|
|
.map((pkgVersion) => {
|
|
// Handle scoped packages like @gitbeaker/cli@1.0.0
|
|
const lastAtIndex = pkgVersion.lastIndexOf('@');
|
|
const packageName = pkgVersion.substring(0, lastAtIndex);
|
|
const version = pkgVersion.substring(lastAtIndex + 1);
|
|
return `- [\`${packageName}@${version}\`](https://www.npmjs.com/package/${packageName}/v/${version})`;
|
|
})
|
|
.join('\n');
|
|
|
|
const comment = `${emoji} **${releaseTitle}** ${emoji}
|
|
|
|
The following packages have been published with ${releaseDescription}:
|
|
|
|
${releaseLinks}
|
|
|
|
${installNote}`;
|
|
|
|
const commentType = isCanary ? 'canary' : 'production';
|
|
|
|
await githubApiRequest('/actions/workflows/post-release-comment.yml/dispatches', {
|
|
method: 'POST',
|
|
body: JSON.stringify({
|
|
ref: 'main',
|
|
inputs: {
|
|
pr_number: prNumber.toString(),
|
|
comment_body: comment,
|
|
comment_type: commentType,
|
|
},
|
|
}),
|
|
});
|
|
|
|
logStep(`Successfully triggered ${releaseType} release comment workflow`);
|
|
} catch (error) {
|
|
console.warn('Failed to post PR comment:', error.message);
|
|
}
|
|
}
|
|
|
|
// Commit and push (production only)
|
|
if (!isCanary) {
|
|
const hasChanges = execCommand('git status --porcelain', { encoding: 'utf8' }).trim();
|
|
|
|
if (hasChanges) {
|
|
if (!execCommand('git add .', 'Staging changes')) process.exit(1);
|
|
|
|
if (
|
|
!execCommand(
|
|
'git commit -m "Version packages and update contributors"',
|
|
'Committing changes',
|
|
)
|
|
)
|
|
process.exit(1);
|
|
|
|
if (!execCommand('git push', 'Pushing changes')) process.exit(1);
|
|
|
|
logStep('Successfully committed and pushed version changes');
|
|
}
|
|
}
|
|
|
|
logStep(
|
|
`✅ ${releaseType.charAt(0).toUpperCase() + releaseType.slice(1)} release completed successfully!`,
|
|
);
|
|
}
|
|
|
|
release().catch((error) => {
|
|
console.error(
|
|
`❌ ${releaseType.charAt(0).toUpperCase() + releaseType.slice(1)} release failed:`,
|
|
error,
|
|
);
|
|
process.exit(1);
|
|
});
|