UNPKG

@kinvolk/headlamp-plugin

Version:

The needed infrastructure for building Headlamp plugins.

253 lines (216 loc) • 9.66 kB
import chalk from 'chalk'; import path from 'path'; import inquirer from 'inquirer'; import { getPluginPath, getPluginInfo, findTarball } from '../utils/plugin.js'; import { sanitizeVersion, validateVersion } from '../utils/version.js'; import { getChangelogForPlugin, getVersionBumpCommit, validateCommitSha, isCommitPushedToRemote, createTagIfNotExists, pushTagIfNotOnRemote } from '../utils/git.js'; import { getRelease, getReleaseByName, createDraftRelease, uploadAssetToRelease, getOwnerAndRepo, publishRelease } from '../utils/github.js'; interface ReleaseOptions { tarball?: string; publish?: boolean; noTarball?: boolean; } export async function releasePlugin( pluginName: string, releaseVersion?: string, options: ReleaseOptions = {} ): Promise<void> { const pluginPath = getPluginPath(pluginName); const info = getPluginInfo(pluginPath); // Use version from package.json if not provided const version = releaseVersion ? sanitizeVersion(releaseVersion) : info.version; if (!validateVersion(version)) { console.error(chalk.red(`Error: "${version}" is not a valid semantic version`)); console.error(chalk.red('Please use format: X.Y.Z (e.g., 1.0.0)')); process.exit(1); } console.log(chalk.blue(`šŸš€ Starting release process for plugin "${pluginName}" version ${version}...\n`)); // If a version was provided, verify it matches package.json if (releaseVersion && info.version !== version) { console.error(chalk.red(`āŒ Version mismatch!`)); console.error(chalk.red(`Plugin package.json version: ${info.version}`)); console.error(chalk.red(`Release version: ${version}`)); console.error(chalk.red(`\nPlease update the plugin version first using:`)); console.error(chalk.red(`plugin-releaser bump ${pluginName} ${version}`)); process.exit(1); } // If no version was provided, we're using the package.json version if (!releaseVersion) { console.log(chalk.dim(`Using version from package.json: ${version}`)); } let tarballPath: string | undefined; // Only handle tarball if --no-tarball is not specified if (!options.noTarball) { if (options.tarball) { tarballPath = path.resolve(options.tarball); if (!require('fs').existsSync(tarballPath)) { console.error(chalk.red(`āŒ Specified tarball does not exist: ${tarballPath}`)); process.exit(1); } } else { const foundTarball = findTarball(pluginPath, pluginName, version); if (!foundTarball) { console.error(chalk.red(`āŒ Could not find tarball for ${pluginName} v${version}`)); console.error(chalk.red(`Please create a package first using:`)); console.error(chalk.red(`plugin-releaser package ${pluginName}`)); process.exit(1); } tarballPath = foundTarball; } } console.log(chalk.dim(`Plugin: ${info.name}`)); console.log(chalk.dim(`Version: ${version}`)); if (tarballPath) { console.log(chalk.dim(`Tarball: ${path.basename(tarballPath)}`)); } else { console.log(chalk.dim(`Tarball: Skipped (--no-tarball)`)); } console.log(''); try { const tagName = `${pluginName}-${version}`; console.log(chalk.blue('šŸ“ Generating changelog...')); const changelog = getChangelogForPlugin(pluginName); const releaseBody = `## New Changes\n${changelog}`; console.log(chalk.blue('šŸ” Checking for existing release...')); let existingRelease = await getRelease(tagName); // If no release found by tag, check for draft release by name if (!existingRelease) { const releaseName = `${pluginName} v${version}`; existingRelease = await getReleaseByName(releaseName); if (existingRelease && existingRelease.draft) { console.log(chalk.yellow(`āš ļø Found existing draft release: ${releaseName}`)); } else if (existingRelease) { console.log(chalk.yellow(`āš ļø Found existing published release: ${releaseName}`)); } } let releaseId: number; if (existingRelease) { console.log(chalk.yellow(`āš ļø Release for ${existingRelease.draft ? 'draft ' : ''}${tagName} already exists`)); releaseId = existingRelease.id; // Check if tarball is already uploaded (only if we have a tarball to upload) if (tarballPath) { const tarballName = path.basename(tarballPath); const existingAsset = existingRelease.assets.find(asset => asset.name === tarballName ); if (existingAsset) { console.log(chalk.yellow('āš ļø Tarball already uploaded to this release')); const { overwrite } = await inquirer.prompt([{ type: 'confirm', name: 'overwrite', message: 'Do you want to overwrite the existing asset?', default: false }]); if (!overwrite) { console.log(chalk.blue('ā„¹ļø Skipping asset upload')); // Continue with the release process without uploading a new tarball // Set tarballPath to undefined to skip upload tarballPath = undefined; } else { console.log(chalk.blue('šŸ—‘ļø Deleting existing asset...')); const { getOctokit } = require('../utils/github'); const octokit = getOctokit(); const { owner, repo } = getOwnerAndRepo(); await octokit.repos.deleteReleaseAsset({ owner, repo, asset_id: existingAsset.id }); console.log(chalk.green('āœ… Existing asset deleted')); } } } } else { console.log(chalk.blue('šŸ“ Creating draft release...')); // Find the commit that bumped the version for this plugin console.log(chalk.blue('šŸ” Finding version bump commit...')); const versionBumpCommit = getVersionBumpCommit(pluginName, version); let targetCommit: string | undefined; if (versionBumpCommit) { console.log(chalk.dim(`Found commit: ${versionBumpCommit}`)); // Validate that the commit exists locally if (validateCommitSha(versionBumpCommit)) { console.log(chalk.dim(`āœ… Commit exists locally: ${versionBumpCommit}`)); // Check if the commit exists on the remote (required for GitHub API) if (isCommitPushedToRemote(versionBumpCommit)) { targetCommit = versionBumpCommit; console.log(chalk.dim(`āœ… Commit exists on remote: ${versionBumpCommit}`)); } else { console.log(chalk.yellow(`āš ļø Commit ${versionBumpCommit} not pushed to remote yet, using current HEAD`)); console.log(chalk.dim('šŸ’” Tip: Push your commits first, then run the release command to use the exact version bump commit')); targetCommit = undefined; } } else { console.log(chalk.yellow(`āš ļø Commit ${versionBumpCommit} does not exist locally, using current HEAD`)); targetCommit = undefined; } } else { console.log(chalk.yellow('āš ļø Could not find version bump commit, using current HEAD')); targetCommit = undefined; } const newRelease = await createDraftRelease(pluginName, version, releaseBody, targetCommit); releaseId = newRelease.id; console.log(chalk.green(`āœ… Created draft release: ${newRelease.name}`)); } // Upload tarball only if we have one if (tarballPath) { console.log(chalk.blue('šŸ“¦ Uploading tarball to release...')); await uploadAssetToRelease(releaseId, tarballPath); if (existingRelease) { console.log(chalk.green('āœ… Updated existing release with new tarball!')); } else { console.log(chalk.green('āœ… Draft release created successfully with tarball!')); } } else { if (existingRelease) { console.log(chalk.green('āœ… Using existing release (no tarball attached)!')); } else { console.log(chalk.green('āœ… Draft release created successfully (no tarball attached)!')); } } // Handle publish logic if --publish option is provided if (options.publish) { const tagName = `${pluginName}-${version}`; console.log(chalk.blue('\nšŸ·ļø Managing tags and publishing release...')); // 1. Create tag if it doesn't exist (uses existing one if it exists) console.log(chalk.blue('šŸ” Checking for existing tag...')); createTagIfNotExists(pluginName, version); // 2. Push tag to remote if not already there pushTagIfNotOnRemote(tagName); // 3. Publish the release if (existingRelease && !existingRelease.draft) { console.log(chalk.yellow('āš ļø Release is already published, skipping publish step')); } else { console.log(chalk.blue('šŸ“¤ Publishing release...')); await publishRelease(releaseId); console.log(chalk.green('āœ… Release published successfully!')); } console.log(chalk.green('\nšŸŽ‰ Release process completed successfully!')); const { owner, repo } = getOwnerAndRepo(); console.log(chalk.dim(`View at: https://github.com/${owner}/${repo}/releases/tag/${tagName}`)); } else { console.log(chalk.dim('\nThe release is currently in draft mode.')); const { owner, repo } = getOwnerAndRepo(); console.log(chalk.dim(`Or visit: https://github.com/${owner}/${repo}/releases`)); } console.log(chalk.green(`\nšŸŽ‰ Release process completed successfully!`)); } catch (error) { console.error(chalk.red('āŒ Error during release process:')); console.error(error); process.exit(1); } }