@kinvolk/headlamp-plugin
Version:
The needed infrastructure for building Headlamp plugins.
253 lines (216 loc) ⢠9.66 kB
text/typescript
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);
}
}