UNPKG

bumpx-action

Version:

GitHub Action for bumpx version bumping tool.

442 lines (383 loc) 12.9 kB
import type { ActionInputs, InstallationSummary, PackageInstallResult } from './types' import * as os from 'node:os' import * as path from 'node:path' import * as process from 'node:process' import * as core from '@actions/core' import * as exec from '@actions/exec' import * as github from '@actions/github' import { ActionError, ActionErrorType } from './types' export * from './types' const DEFAULT_TIMEOUT = 600 // 10 minutes /** * Main function to run the GitHub Action */ export async function run(): Promise<void> { const startTime = Date.now() const summary: InstallationSummary = { totalPackages: 0, successfulInstalls: 0, failedInstalls: 0, results: [], totalTime: 0, bumpxInstalled: false, bunInstalled: false, pkgxInstalled: false, } try { // Get and validate inputs const inputs = getActionInputs() core.info('Starting bumpx Installer Action') if (inputs.verbose) { core.info(`Inputs: ${JSON.stringify(inputs, null, 2)}`) core.info(`Context: ${JSON.stringify(github.context, null, 2)}`) } // Set working directory if specified if (inputs.workingDirectory && inputs.workingDirectory !== '.') { process.chdir(inputs.workingDirectory) core.info(`Changed working directory to: ${inputs.workingDirectory}`) } // Setup environment variables if (inputs.envVars) { setupEnvironmentVariables(inputs.envVars) } // Setup Bun if requested if (inputs.installBun) { const bunResult = await setupBun() summary.bunInstalled = bunResult.success if (!bunResult.success) { throw new ActionError( `Bun installation failed: ${bunResult.error}`, ActionErrorType.BUN_INSTALLATION_FAILED, { error: bunResult.error }, ) } } // Install bumpx const bumpxResult = await installBumpx(inputs.bumpxVersion) summary.bumpxInstalled = bumpxResult.success if (!bumpxResult.success) { throw new ActionError( `bumpx installation failed: ${bumpxResult.error}`, ActionErrorType.BUMPX_INSTALLATION_FAILED, { error: bumpxResult.error, version: inputs.bumpxVersion }, ) } // Install pkgx if requested if (inputs.installPkgx) { const pkgxResult = await installPkgx(inputs.verbose) summary.pkgxInstalled = pkgxResult.success if (!pkgxResult.success) { core.warning(`pkgx installation failed: ${pkgxResult.error}`) } } // Install specified packages if (inputs.packages) { const packagesToInstall = inputs.packages.split(/\s+/).filter(Boolean) if (packagesToInstall.length > 0) { const installResults = await installPackages(packagesToInstall, inputs.timeout, inputs.verbose) summary.results = installResults summary.totalPackages = installResults.length summary.successfulInstalls = installResults.filter(r => r.success).length summary.failedInstalls = installResults.filter(r => !r.success).length } } // Calculate total time summary.totalTime = Date.now() - startTime // Set outputs setActionOutputs(summary, bumpxResult.version) // Log summary logInstallationSummary(summary) core.info(`bumpx installation completed successfully in ${summary.totalTime}ms`) } catch (error) { summary.totalTime = Date.now() - startTime handleActionError(error, summary) } } /** * Get and validate action inputs */ function getActionInputs(): ActionInputs { const inputs: ActionInputs = { packages: core.getInput('packages', { required: false }) || '', configPath: core.getInput('config-path', { required: false }) || 'bumpx.config.ts', bumpxVersion: core.getInput('bumpx-version', { required: false }) || 'latest', installBun: core.getBooleanInput('install-bun', { required: false }) ?? true, installPkgx: core.getBooleanInput('install-pkgx', { required: false }) ?? true, verbose: core.getBooleanInput('verbose', { required: false }) ?? false, skipDetection: core.getBooleanInput('skip-detection', { required: false }) ?? false, workingDirectory: core.getInput('working-directory', { required: false }) || '.', envVars: core.getInput('env-vars', { required: false }) || '', timeout: Number.parseInt(core.getInput('timeout', { required: false }) || String(DEFAULT_TIMEOUT)), cache: core.getBooleanInput('cache', { required: false }) ?? true, cacheKey: core.getInput('cache-key', { required: false }) || 'bumpx-packages', } // Validate inputs if (inputs.timeout <= 0 || inputs.timeout > 3600) { throw new ActionError( `Invalid timeout: ${inputs.timeout}. Must be between 1 and 3600 seconds.`, ActionErrorType.CONFIG_PARSING_FAILED, { timeout: inputs.timeout }, ) } return inputs } /** * Setup environment variables from JSON string */ function setupEnvironmentVariables(envVarsJson: string): void { try { const envVars = JSON.parse(envVarsJson) Object.entries(envVars).forEach(([key, value]) => { process.env[key] = String(value) core.info(`Set environment variable: ${key}`) }) } catch (error) { throw new ActionError( `Failed to parse env-vars JSON: ${error}`, ActionErrorType.CONFIG_PARSING_FAILED, { envVarsJson, error }, ) } } /** * Setup Bun in the environment */ async function setupBun(): Promise<PackageInstallResult> { const startTime = Date.now() core.info('Setting up Bun...') try { // Check if Bun is already installed const { exitCode } = await exec.getExecOutput('which', ['bun'], { ignoreReturnCode: true }) if (exitCode === 0) { core.info('Bun is already installed') const { stdout } = await exec.getExecOutput('bun', ['--version']) const version = stdout.trim() return { name: 'bun', success: true, installTime: Date.now() - startTime, version, } } core.info('Bun is not installed, installing now...') // Install Bun based on platform const platform = process.platform if (platform === 'darwin' || platform === 'linux') { // macOS or Linux await exec.exec('curl', ['-fsSL', 'https://bun.sh/install', '|', 'bash'], { env: { ...process.env, FORCE: '1' }, }) } else if (platform === 'win32') { // Windows await exec.exec('powershell', ['-Command', 'irm bun.sh/install.ps1 | iex']) } else { throw new ActionError( `Unsupported platform: ${platform}`, ActionErrorType.UNSUPPORTED_PLATFORM, { platform }, ) } // Add Bun to PATH const bunPath = path.join(os.homedir(), '.bun', 'bin') core.addPath(bunPath) // Verify installation const { stdout } = await exec.getExecOutput('bun', ['--version']) const version = stdout.trim() core.info(`Bun installation completed: ${version}`) return { name: 'bun', success: true, installTime: Date.now() - startTime, version, } } catch (error) { return { name: 'bun', success: false, installTime: Date.now() - startTime, error: error instanceof Error ? error.message : String(error), } } } /** * Install bumpx using Bun */ async function installBumpx(version: string): Promise<PackageInstallResult> { const startTime = Date.now() core.info(`Installing bumpx version: ${version}`) try { const packageSpec = version === 'latest' ? 'bumpx' : `bumpx@${version}` await exec.exec('bun', ['install', '-g', packageSpec]) // Verify installation const { stdout } = await exec.getExecOutput('bumpx', ['--version'], { ignoreReturnCode: true }) const installedVersion = stdout.trim() core.info(`bumpx installation completed: ${installedVersion}`) return { name: 'bumpx', success: true, installTime: Date.now() - startTime, version: installedVersion, } } catch (error) { return { name: 'bumpx', success: false, installTime: Date.now() - startTime, error: error instanceof Error ? error.message : String(error), } } } /** * Install pkgx using bumpx */ async function installPkgx(verbose: boolean): Promise<PackageInstallResult> { const startTime = Date.now() core.info('Installing pkgx...') try { const options = { env: { ...process.env, bumpx_VERBOSE: verbose ? 'true' : 'false', CONTEXT: JSON.stringify(github.context), }, } const args = ['pkgx'] if (verbose) { args.push('--verbose') } await exec.exec('bumpx', args, options) core.info('pkgx installation completed') return { name: 'pkgx', success: true, installTime: Date.now() - startTime, } } catch (error) { return { name: 'pkgx', success: false, installTime: Date.now() - startTime, error: error instanceof Error ? error.message : String(error), } } } /** * Install multiple packages */ async function installPackages(packages: string[], timeout: number, verbose: boolean): Promise<PackageInstallResult[]> { core.info(`Installing ${packages.length} packages: ${packages.join(', ')}`) const results: PackageInstallResult[] = [] for (const packageName of packages) { const result = await installSinglePackage(packageName, timeout, verbose) results.push(result) if (result.success) { core.info(`✓ ${packageName} installed successfully`) } else { core.warning(`✗ ${packageName} installation failed: ${result.error}`) } } return results } /** * Install a single package */ async function installSinglePackage(packageName: string, timeout: number, verbose: boolean): Promise<PackageInstallResult> { const startTime = Date.now() try { const options = { env: { ...process.env, bumpx_VERBOSE: verbose ? 'true' : 'false', CONTEXT: JSON.stringify(github.context), }, timeout: timeout * 1000, // Convert to milliseconds } const args = ['install'] if (verbose) { args.push('--verbose') } args.push(packageName) await exec.exec('bumpx', args, options) return { name: packageName, success: true, installTime: Date.now() - startTime, } } catch (error) { return { name: packageName, success: false, installTime: Date.now() - startTime, error: error instanceof Error ? error.message : String(error), } } } /** * Set action outputs */ function setActionOutputs(summary: InstallationSummary, bumpxVersion?: string): void { core.setOutput('success', 'true') core.setOutput('packages-installed', String(summary.successfulInstalls)) core.setOutput('installed-packages', JSON.stringify(summary.results.filter(r => r.success).map(r => r.name))) core.setOutput('summary', JSON.stringify(summary)) core.setOutput('bumpx-version', bumpxVersion || 'unknown') core.setOutput('bun-version', summary.results.find(r => r.name === 'bun')?.version || 'unknown') core.setOutput('pkgx-installed', String(summary.pkgxInstalled)) } /** * Log installation summary */ function logInstallationSummary(summary: InstallationSummary): void { core.info('='.repeat(50)) core.info('Installation Summary') core.info('='.repeat(50)) core.info(`Total packages: ${summary.totalPackages}`) core.info(`Successful installations: ${summary.successfulInstalls}`) core.info(`Failed installations: ${summary.failedInstalls}`) core.info(`Total time: ${summary.totalTime}ms`) core.info(`bumpx installed: ${summary.bumpxInstalled}`) core.info(`Bun installed: ${summary.bunInstalled}`) core.info(`pkgx installed: ${summary.pkgxInstalled}`) if (summary.failedInstalls > 0) { core.info('Failed installations:') summary.results.filter(r => !r.success).forEach((result) => { core.info(` - ${result.name}: ${result.error}`) }) } core.info('='.repeat(50)) } /** * Handle action errors */ function handleActionError(error: unknown, summary: InstallationSummary): void { const errorMessage = error instanceof Error ? error.message : String(error) if (error instanceof ActionError) { core.error(`Action failed: ${error.message}`) core.error(`Error type: ${error.type}`) if (error.details) { core.error(`Details: ${JSON.stringify(error.details, null, 2)}`) } } else { core.error(`Unexpected error: ${errorMessage}`) } // Set failure outputs core.setOutput('success', 'false') core.setOutput('summary', JSON.stringify(summary)) core.setFailed(errorMessage) } // Run the action if this is the main module if (require.main === module) { run().catch((error) => { core.setFailed(error instanceof Error ? error.message : String(error)) }) }