diff --git a/PUBLISH.md b/PUBLISH.md index 8fa6decb..df96297d 100644 --- a/PUBLISH.md +++ b/PUBLISH.md @@ -4,7 +4,7 @@ This document outlines the complete process for publishing new versions of Deskt ## 🚀 Automated Release (Recommended) -We now have an automated release script that handles the entire process! +We now have an automated release script that handles the entire process with **automatic state tracking and resume capability**! ```bash # Patch release (0.2.16 → 0.2.17) - Bug fixes, small improvements @@ -18,9 +18,31 @@ npm run release:major # Test without publishing npm run release:dry + +# Clear saved state and start fresh +node scripts/publish-release.cjs --clear-state ``` -**See [scripts/README-RELEASE.md](scripts/README-RELEASE.md) for full documentation of the automated release process.** +### ✨ Smart State Tracking + +The script automatically tracks completed steps and **resumes from failures**: + +1. **Automatic Resume**: If any step fails, just run the script again - it will skip completed steps and continue from where it failed +2. **No Manual Flags**: No need to remember which `--skip-*` flags to use +3. **Clear State**: Use `--clear-state` to reset and start from the beginning +4. **Transparent**: Shows which steps were already completed when resuming + +**Example workflow:** +```bash +# Start release - tests fail +npm run release +# ❌ Step 2/6 failed: Tests failed + +# Fix the tests, then just run again +npm run release +# ✓ Step 1/6: Version bump already completed +# ✓ Step 2/6: Running tests... (continues from here) +``` The script automatically handles: - ✅ Version bumping @@ -30,6 +52,7 @@ The script automatically handles: - ✅ NPM publishing - ✅ MCP Registry publishing - ✅ Publication verification +- ✨ **State tracking and automatic resume** --- diff --git a/scripts/publish-release.cjs b/scripts/publish-release.cjs index e0d6e50f..8556c961 100755 --- a/scripts/publish-release.cjs +++ b/scripts/publish-release.cjs @@ -1,7 +1,7 @@ #!/usr/bin/env node /** - * Desktop Commander - Complete Release Publishing Script + * Desktop Commander - Complete Release Publishing Script with State Tracking * * This script handles the entire release process: * 1. Version bump @@ -10,12 +10,17 @@ * 4. Publish to NPM * 5. Publish to MCP Registry * 6. Verify publications + * + * Features automatic resume from failed steps and state tracking. */ const { execSync } = require('child_process'); const fs = require('fs'); const path = require('path'); +// State file path +const STATE_FILE = path.join(process.cwd(), '.release-state.json'); + // Colors for output const colors = { reset: '\x1b[0m', @@ -23,6 +28,7 @@ const colors = { green: '\x1b[32m', yellow: '\x1b[33m', blue: '\x1b[34m', + cyan: '\x1b[36m', }; // Helper functions for colored output @@ -42,6 +48,45 @@ function printWarning(message) { console.log(`${colors.yellow}⚠${colors.reset} ${message}`); } +function printInfo(message) { + console.log(`${colors.cyan}ℹ${colors.reset} ${message}`); +} + +// State management functions +function loadState() { + if (fs.existsSync(STATE_FILE)) { + try { + return JSON.parse(fs.readFileSync(STATE_FILE, 'utf8')); + } catch (error) { + printWarning('Could not parse state file, starting fresh'); + return null; + } + } + return null; +} + +function saveState(state) { + fs.writeFileSync(STATE_FILE, JSON.stringify(state, null, 2)); +} +function clearState() { + if (fs.existsSync(STATE_FILE)) { + fs.unlinkSync(STATE_FILE); + printSuccess('Release state cleared'); + } else { + printInfo('No release state to clear'); + } +} + +function markStepComplete(state, step) { + state.completedSteps.push(step); + state.lastStep = step; + saveState(state); +} + +function isStepComplete(state, step) { + return state && state.completedSteps.includes(step); +} + // Execute command with error handling function exec(command, options = {}) { try { @@ -71,6 +116,7 @@ function parseArgs() { skipTests: false, dryRun: false, help: false, + clearState: false, skipBump: false, skipBuild: false, skipMcpb: false, @@ -112,6 +158,9 @@ function parseArgs() { options.skipGit = true; options.skipNpm = true; break; + case '--clear-state': + options.clearState = true; + break; case '--dry-run': options.dryRun = true; break; @@ -121,7 +170,7 @@ function parseArgs() { break; default: printError(`Unknown option: ${arg}`); - console.log("Run 'node scripts/publish-release.js --help' for usage information."); + console.log("Run 'node scripts/publish-release.cjs --help' for usage information."); process.exit(1); } } @@ -134,17 +183,22 @@ function showHelp() { console.log('Usage: node scripts/publish-release.cjs [OPTIONS]'); console.log(''); console.log('Options:'); - console.log(' --minor Bump minor version (default: patch)'); - console.log(' --major Bump major version (default: patch)'); - console.log(' --skip-tests Skip running tests'); - console.log(' --skip-bump Skip version bumping'); - console.log(' --skip-build Skip building (if tests also skipped)'); - console.log(' --skip-mcpb Skip building MCPB bundle'); - console.log(' --skip-git Skip git commit and tag'); - console.log(' --skip-npm Skip NPM publishing'); - console.log(' --mcp-only Only publish to MCP Registry (skip all other steps)'); - console.log(' --dry-run Simulate the release without publishing'); - console.log(' --help, -h Show this help message'); + console.log(' --minor Bump minor version (default: patch)'); + console.log(' --major Bump major version (default: patch)'); + console.log(' --skip-tests Skip running tests'); + console.log(' --skip-bump Skip version bumping'); + console.log(' --skip-build Skip building (if tests also skipped)'); + console.log(' --skip-mcpb Skip building MCPB bundle'); + console.log(' --skip-git Skip git commit and tag'); + console.log(' --skip-npm Skip NPM publishing'); + console.log(' --mcp-only Only publish to MCP Registry (skip all other steps)'); + console.log(' --clear-state Clear release state and start fresh'); + console.log(' --dry-run Simulate the release without publishing'); + console.log(' --help, -h Show this help message'); + console.log(''); + console.log('State Management:'); + console.log(' The script automatically tracks completed steps and resumes from failures.'); + console.log(' Use --clear-state to reset and start from the beginning.'); console.log(''); console.log('Examples:'); console.log(' node scripts/publish-release.cjs # Patch release (0.2.16 -> 0.2.17)'); @@ -152,6 +206,7 @@ function showHelp() { console.log(' node scripts/publish-release.cjs --major # Major release (0.2.16 -> 1.0.0)'); console.log(' node scripts/publish-release.cjs --dry-run # Test without publishing'); console.log(' node scripts/publish-release.cjs --mcp-only # Only publish to MCP Registry'); + console.log(' node scripts/publish-release.cjs --clear-state # Reset state and start over'); } // Main release function @@ -163,6 +218,12 @@ async function publishRelease() { return; } + // Handle clear state command + if (options.clearState) { + clearState(); + return; + } + // Check if we're in the right directory const packageJsonPath = path.join(process.cwd(), 'package.json'); if (!fs.existsSync(packageJsonPath)) { @@ -170,17 +231,52 @@ async function publishRelease() { process.exit(1); } + // Load or create state + let state = loadState(); + const isResume = state !== null; + + if (!state) { + state = { + startTime: new Date().toISOString(), + completedSteps: [], + lastStep: null, + version: null, + bumpType: options.bumpType, + }; + } + console.log(''); console.log('╔══════════════════════════════════════════════════════════╗'); console.log('║ Desktop Commander Release Publisher ║'); console.log('╚══════════════════════════════════════════════════════════╝'); console.log(''); + // Show resume information + if (isResume) { + console.log(`${colors.cyan}╔══════════════════════════════════════════════════════════╗${colors.reset}`); + console.log(`${colors.cyan}║ 📋 RESUMING FROM PREVIOUS RUN ║${colors.reset}`); + console.log(`${colors.cyan}╚══════════════════════════════════════════════════════════╝${colors.reset}`); + console.log(''); + printInfo(`Started: ${state.startTime}`); + printInfo(`Last completed step: ${state.lastStep || 'none'}`); + printInfo(`Completed steps: ${state.completedSteps.join(', ') || 'none'}`); + if (state.version) { + printInfo(`Target version: ${state.version}`); + } + console.log(''); + printWarning('Will skip already completed steps automatically'); + printInfo('Use --clear-state to start fresh'); + console.log(''); + } + // Get current version const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')); const currentVersion = packageJson.version; printStep(`Current version: ${currentVersion}`); - printStep(`Bump type: ${options.bumpType}`); + + if (!isResume) { + printStep(`Bump type: ${options.bumpType}`); + } if (options.dryRun) { printWarning('DRY RUN MODE - No changes will be published'); @@ -188,10 +284,11 @@ async function publishRelease() { } try { - let newVersion = currentVersion; + let newVersion = state.version || currentVersion; // Step 1: Bump version - if (!options.skipBump) { + const shouldSkipBump = options.skipBump || isStepComplete(state, 'bump'); + if (!shouldSkipBump) { printStep('Step 1/6: Bumping version...'); const bumpCommand = options.bumpType === 'minor' ? 'npm run bump:minor' : options.bumpType === 'major' ? 'npm run bump:major' : @@ -200,40 +297,53 @@ async function publishRelease() { const newPackageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')); newVersion = newPackageJson.version; + state.version = newVersion; + markStepComplete(state, 'bump'); printSuccess(`Version bumped: ${currentVersion} → ${newVersion}`); - console.log(''); + } else if (isStepComplete(state, 'bump')) { + printInfo('Step 1/6: Version bump already completed ✓'); } else { - printWarning('Step 1/6: Version bump skipped'); - console.log(''); + printWarning('Step 1/6: Version bump skipped (manual override)'); } console.log(''); - // Step 2: Run tests (unless skipped) - tests also build the project - if (!options.skipTests && !options.skipBuild) { - printStep('Step 2/6: Running tests (includes build)...'); - exec('npm test'); - printSuccess('All tests passed'); - } else if (!options.skipBuild) { - printWarning('Step 2/6: Tests skipped - building project...'); - exec('npm run build'); - printSuccess('Project built successfully'); + // Step 2: Run tests or build + const shouldSkipBuild = options.skipBuild || isStepComplete(state, 'build'); + if (!shouldSkipBuild) { + if (!options.skipTests) { + printStep('Step 2/6: Running tests (includes build)...'); + exec('npm test'); + printSuccess('All tests passed'); + } else { + printStep('Step 2/6: Building project...'); + exec('npm run build'); + printSuccess('Project built successfully'); + } + markStepComplete(state, 'build'); + } else if (isStepComplete(state, 'build')) { + printInfo('Step 2/6: Build already completed ✓'); } else { - printWarning('Step 2/6: Tests and build skipped'); + printWarning('Step 2/6: Build skipped (manual override)'); } console.log(''); // Step 3: Build MCPB bundle - if (!options.skipMcpb) { + const shouldSkipMcpb = options.skipMcpb || isStepComplete(state, 'mcpb'); + if (!shouldSkipMcpb) { printStep('Step 3/6: Building MCPB bundle...'); exec('npm run build:mcpb'); + markStepComplete(state, 'mcpb'); printSuccess('MCPB bundle created'); + } else if (isStepComplete(state, 'mcpb')) { + printInfo('Step 3/6: MCPB bundle already created ✓'); } else { - printWarning('Step 3/6: MCPB bundle build skipped'); + printWarning('Step 3/6: MCPB bundle build skipped (manual override)'); } console.log(''); // Step 4: Commit and tag - if (!options.skipGit) { + const shouldSkipGit = options.skipGit || isStepComplete(state, 'git'); + if (!shouldSkipGit) { printStep('Step 4/6: Creating git commit and tag...'); // Check if there are changes to commit @@ -271,19 +381,25 @@ Automated release commit with version bump from ${currentVersion} to ${newVersio exec(`git push origin ${tagName}`); printSuccess(`Tag ${tagName} created and pushed`); } + + markStepComplete(state, 'git'); + } else if (isStepComplete(state, 'git')) { + printInfo('Step 4/6: Git commit and tag already completed ✓'); } else { - printWarning('Step 4/6: Git commit and tag skipped'); + printWarning('Step 4/6: Git commit and tag skipped (manual override)'); } console.log(''); // Step 5: Publish to NPM - if (!options.skipNpm) { + const shouldSkipNpm = options.skipNpm || isStepComplete(state, 'npm'); + if (!shouldSkipNpm) { printStep('Step 5/6: Publishing to NPM...'); // Check NPM authentication const npmUser = execSilent('npm whoami', { ignoreError: true }).trim(); if (!npmUser) { printError('Not logged into NPM. Please run "npm login" first.'); + printError('After logging in, run the script again to resume from this step.'); process.exit(1); } printSuccess(`NPM user: ${npmUser}`); @@ -293,10 +409,11 @@ Automated release commit with version bump from ${currentVersion} to ${newVersio printWarning('Skipping NPM publish (dry run)'); } else { exec('npm publish'); + markStepComplete(state, 'npm'); printSuccess('Published to NPM'); // Verify NPM publication - await new Promise(resolve => setTimeout(resolve, 3000)); // Wait 3 seconds + await new Promise(resolve => setTimeout(resolve, 3000)); const npmVersion = execSilent('npm view @wonderwhy-er/desktop-commander version', { ignoreError: true }).trim(); if (npmVersion === newVersion) { printSuccess(`NPM publication verified: v${npmVersion}`); @@ -304,58 +421,73 @@ Automated release commit with version bump from ${currentVersion} to ${newVersio printWarning(`NPM version mismatch: expected ${newVersion}, got ${npmVersion} (may take a moment to propagate)`); } } + } else if (isStepComplete(state, 'npm')) { + printInfo('Step 5/6: NPM publish already completed ✓'); } else { - printWarning('Step 5/6: NPM publish skipped'); + printWarning('Step 5/6: NPM publish skipped (manual override)'); } console.log(''); // Step 6: Publish to MCP Registry - printStep('Step 6/6: Publishing to MCP Registry...'); - - // Check if mcp-publisher is installed - const hasMcpPublisher = execSilent('which mcp-publisher', { ignoreError: true }); - if (!hasMcpPublisher) { - printError('mcp-publisher not found. Install it with: brew install mcp-publisher'); - printError('Or check your PATH if already installed.'); - process.exit(1); - } + const shouldSkipMcp = isStepComplete(state, 'mcp'); + if (!shouldSkipMcp) { + printStep('Step 6/6: Publishing to MCP Registry...'); + + // Check if mcp-publisher is installed + const hasMcpPublisher = execSilent('which mcp-publisher', { ignoreError: true }); + if (!hasMcpPublisher) { + printError('mcp-publisher not found. Install it with: brew install mcp-publisher'); + printError('Or check your PATH if already installed.'); + printError('After installing, run the script again to resume from this step.'); + process.exit(1); + } - if (options.dryRun) { - printWarning('Would publish to MCP Registry: mcp-publisher publish'); - printWarning('Skipping MCP Registry publish (dry run)'); - } else { - try { - exec('mcp-publisher publish'); - printSuccess('Published to MCP Registry'); - - // Verify MCP Registry publication - await new Promise(resolve => setTimeout(resolve, 3000)); // Wait 3 seconds + if (options.dryRun) { + printWarning('Would publish to MCP Registry: mcp-publisher publish'); + printWarning('Skipping MCP Registry publish (dry run)'); + } else { try { - const mcpResponse = execSilent('curl -s "https://registry.modelcontextprotocol.io/v0/servers?search=io.github.wonderwhy-er/desktop-commander"'); - const mcpData = JSON.parse(mcpResponse); - const mcpVersion = mcpData.servers?.[0]?.version || 'unknown'; + exec('mcp-publisher publish'); + markStepComplete(state, 'mcp'); + printSuccess('Published to MCP Registry'); - if (mcpVersion === newVersion) { - printSuccess(`MCP Registry publication verified: v${mcpVersion}`); - } else { - printWarning(`MCP Registry version: ${mcpVersion} (expected ${newVersion}, may take a moment to propagate)`); + // Verify MCP Registry publication + await new Promise(resolve => setTimeout(resolve, 3000)); + try { + const mcpResponse = execSilent('curl -s "https://registry.modelcontextprotocol.io/v0/servers?search=io.github.wonderwhy-er/desktop-commander"'); + const mcpData = JSON.parse(mcpResponse); + const mcpVersion = mcpData.servers?.[0]?.version || 'unknown'; + + if (mcpVersion === newVersion) { + printSuccess(`MCP Registry publication verified: v${mcpVersion}`); + } else { + printWarning(`MCP Registry version: ${mcpVersion} (expected ${newVersion}, may take a moment to propagate)`); + } + } catch (error) { + printWarning('Could not verify MCP Registry publication'); } } catch (error) { - printWarning('Could not verify MCP Registry publication'); - } - } catch (error) { - printError('MCP Registry publish failed!'); - if (error.message.includes('401') || error.message.includes('expired')) { - printError('Authentication token expired. Please run: mcp-publisher login github'); - } else if (error.message.includes('422')) { - printError('Validation error in server.json. Check the error message above for details.'); + printError('MCP Registry publish failed!'); + if (error.message.includes('401') || error.message.includes('expired')) { + printError('Authentication token expired. Please run: mcp-publisher login github'); + printError('After logging in, run the script again to resume from this step.'); + } else if (error.message.includes('422')) { + printError('Validation error in server.json. Check the error message above for details.'); + printError('After fixing the issue, run the script again to resume from this step.'); + } + throw error; } - throw error; } + } else { + printInfo('Step 6/6: MCP Registry publish already completed ✓'); } console.log(''); + // All steps complete - clear state + clearState(); + // Success summary + const tagName = `v${newVersion}`; console.log('╔══════════════════════════════════════════════════════════╗'); console.log('║ 🎉 Release Complete! 🎉 ║'); console.log('╚══════════════════════════════════════════════════════════╝'); @@ -380,8 +512,12 @@ Automated release commit with version bump from ${currentVersion} to ${newVersio } catch (error) { console.log(''); - printError('Release failed!'); + printError('Release failed at step: ' + (state.lastStep || 'startup')); printError(error.message); + console.log(''); + printInfo('State has been saved. Simply run the script again to resume from where it failed.'); + printInfo('Use --clear-state to start over from the beginning.'); + console.log(''); process.exit(1); } }