diff --git a/.circleci/config.yml b/.circleci/config.yml index 2b1bde6e..33353b2b 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -469,17 +469,7 @@ jobs: command: | export PR_NUMBER=$(echo $CIRCLE_PULL_REQUEST | sed 's/.*\/pull\///') - if [[ -n "$PR_NUMBER" ]]; then - # Check if PR has release:canary label - if node scripts/check-release-label.mjs $PR_NUMBER; then - echo "Proceeding with canary release" - yarn release:canary - else - echo "Skipping canary release" - fi - else - echo "No PR found - skipping canary release" - fi + yarn release:canary release-production: executor: node-executor diff --git a/scripts/check-release-label.mjs b/scripts/check-release-label.mjs deleted file mode 100644 index a57e8c85..00000000 --- a/scripts/check-release-label.mjs +++ /dev/null @@ -1,39 +0,0 @@ -#!/usr/bin/env node -/** - * Check if PR has release:canary label - * Usage: - * node scripts/check-release-label.mjs - * node scripts/check-release-label.mjs # Uses environment variables - * Exit code: 0 if label found, 1 if not found - */ - -import { fetchPRData } from './github-api.mjs'; - -const prNumber = process.argv[2] || process.env.CIRCLE_PR_NUMBER || process.env.PR_NUMBER; - -if (!prNumber) { - console.error('No PR number provided'); - process.exit(1); -} - -async function checkReleaseLabel() { - try { - const prData = await fetchPRData(prNumber); - const labels = prData.labels.map(label => label.name); - - console.log(`PR #${prNumber} labels: ${labels.join(', ')}`); - - if (labels.includes('release:canary')) { - console.log('✅ Found release:canary label'); - process.exit(0); - } else { - console.log('❌ No release:canary label found'); - process.exit(1); - } - } catch (error) { - console.error('Error checking label:', error.message); - process.exit(1); - } -} - -checkReleaseLabel(); \ No newline at end of file diff --git a/scripts/generate-changesets-from-labels.mjs b/scripts/generate-changesets-from-labels.mjs deleted file mode 100644 index 4c91a98b..00000000 --- a/scripts/generate-changesets-from-labels.mjs +++ /dev/null @@ -1,106 +0,0 @@ -#!/usr/bin/env node -import { writeFileSync, existsSync, mkdirSync } from 'fs'; -import { execSync } from 'child_process'; -import { fetchPRData } from './github-api.mjs'; - -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 getPackageNames() { - try { - // Get all workspace packages using yarn - const output = execSync('yarn workspaces list --json', { encoding: 'utf8' }); - const workspaces = output - .trim() - .split('\n') - .map((line) => JSON.parse(line)); - - // Filter out the root workspace and get package names - const packageNames = workspaces - .filter((ws) => ws.location !== '.' && ws.name) // Skip root workspace - .map((ws) => ws.name); - - return packageNames; - } 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() { - const prNumber = process.env.PR_NUMBER; - - if (!prNumber) { - console.log('No PR number found in environment, skipping changeset generation'); - return; - } - - try { - // Fetch PR data from GitHub API - const prData = await fetchPRData(prNumber); - - // Get labels - const labels = prData.labels.map((label) => label.name); - console.log(`Found PR 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) { - console.log('No labels found that trigger a release, skipping changeset generation'); - - // TODO: Default to patch update instead - return; - } - - console.log(`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; - } - - console.log(`Found packages: ${packageNames.join(', ')}`); - - // Create changeset - if (!existsSync('.changeset')) mkdirSync('.changeset'); - - const changesetContent = `--- -${generateChangesetYaml(packageNames, changeType)} ---- - -${prData.title}`; - - const filename = `.changeset/pr-${prNumber}-${Date.now()}.md`; - writeFileSync(filename, changesetContent); - - console.log(`Generated changeset: ${filename}`); - } catch (error) { - console.error('Error generating changeset:', error.message); - } -} - -generateChangesetFromPR(); diff --git a/scripts/github-api.mjs b/scripts/github-api.mjs deleted file mode 100644 index 518c8e8a..00000000 --- a/scripts/github-api.mjs +++ /dev/null @@ -1,33 +0,0 @@ -/** - * Shared GitHub API utilities - */ - -/** - * Fetch PR data from GitHub API - * @param {string} prNumber - The PR number - * @returns {Promise} PR data from GitHub API - */ -export 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(); -} \ No newline at end of file diff --git a/scripts/release.mjs b/scripts/release.mjs index c9c5a73e..d937fd29 100644 --- a/scripts/release.mjs +++ b/scripts/release.mjs @@ -4,13 +4,28 @@ */ 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 ? '🐤' : '🚀'; - console.log(`${emoji} ${message}`); + logStep(`${emoji} ${message}`); } function execCommand(command, description) { @@ -25,6 +40,99 @@ function execCommand(command, description) { } } +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`); @@ -37,18 +145,31 @@ async function release() { return; } - if ( - !execCommand( - 'node scripts/generate-changesets-from-labels.mjs', - 'Generating changeset from PR labels', - ) - ) { + // 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 { - execSync('yarn changeset version', { stdio: 'pipe' }); + 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`); @@ -82,7 +203,7 @@ async function release() { // Commit and push (production only) if (!isCanary) { - const hasChanges = execSync('git status --porcelain', { encoding: 'utf8' }).trim(); + const hasChanges = execCommand('git status --porcelain', { encoding: 'utf8' }).trim(); if (hasChanges) { if (!execCommand('git add .', 'Staging changes')) process.exit(1); if (