gitbeaker/scripts/release.mjs
2026-01-17 18:17:12 -05:00

233 lines
6.4 KiB
JavaScript

#!/usr/bin/env node
/**
* Unified release script
*/
import { execSync } from 'child_process';
import { writeFileSync, existsSync, mkdirSync } from 'fs';
import { fetchPRData } from './github-api.mjs';
const isCanary = process.argv[2] === 'canary';
const releaseType = isCanary ? 'canary' : 'production';
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,
};
function logStep(message) {
const emoji = isCanary ? '🐤' : '🚀';
logStep(`${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 = execCommand('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');
}
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
if (!execCommand('.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 fetchPRData(prNumber) {
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}`);
}
const [, owner, repo] = match;
const response = await fetch(`https://api.github.com/repos/${owner}/${repo}/pulls/${prNumber}`, {
headers: {
Authorization: `Bearer ${process.env.GITHUB_TOKEN}`,
Accept: 'application/vnd.github.v3+json',
'User-Agent': 'gitbeaker-api-client',
},
});
if (!response.ok) {
throw new Error(`GitHub API error: ${response.status} ${response.statusText}`);
}
return response.json();
}
async function release() {
logStep(`Starting ${releaseType} release`);
// Generate changeset from PR labels
// Adds limitation to only release from PRs for now
let prNumber = process.env.PR_NUMBER;
if (!prNumber) {
logStep('No PR number found - skipping release');
return;
}
// Get PR data
const prData = await fetchPRData(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);
}
// Check if there are any changesets to process
try {
execCommand('yarn changeset version', { stdio: 'pipe' });
} catch (error) {
// changeset status exits with non-zero when no changesets found
logStep(`No changesets found - skipping ${releaseType} release`);
return;
}
logStep(`Changesets found - proceeding with ${releaseType} release`);
// 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';
if (!execCommand(publishCommand, `Publishing ${releaseType} packages`)) {
process.exit(1);
}
// 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);
});