UNPKG

claude-flow-novice

Version:

Claude Flow Novice - Advanced orchestration platform for multi-agent AI workflows with CFN Loop architecture Includes Local RuVector Accelerator and all CFN skills for complete functionality.

474 lines (473 loc) 18.7 kB
/** * Skill Deployment Pipeline (Refactored with Transaction Framework) * * Orchestrates atomic deployment of skills from APPROVED → DEPLOYED state. * Part of Task 3.2: Skill Deployment Transaction Integration * * Features: * - Atomic cross-database transactions via TransactionManager (PostgreSQL + SQLite) * - Distributed locking to prevent concurrent deployments * - Automatic validation before deployment * - Version conflict detection and resolution within transaction * - Content hash validation within transaction * - Rollback capability on failure (automatic via transaction) * - Comprehensive audit trail (atomically updated) * * @example * ```typescript * const pipeline = new SkillDeploymentPipeline(dbService, txManager, lockManager); * const result = await pipeline.deploySkill({ * skillPath: '.claude/skills/authentication', * deployedBy: 'admin@example.com' * }); * * if (!result.success) { * console.error('Deployment failed:', result.error); * // Transaction automatically rolled back * } * ``` */ import { StandardError, ErrorCode } from '../lib/errors.js'; import { createLogger } from '../lib/logging.js'; import { validateSkill, parseFrontmatter } from './skill-validator.js'; import { getNextVersion, versionExists } from './skill-versioning.js'; const logger = createLogger('skill-deployment'); /** * Skill Deployment Pipeline (Transaction-Aware) * * Handles atomic deployment of skills with validation, versioning, rollback, * and distributed locking to prevent concurrent modifications. */ export class SkillDeploymentPipeline { dbService; txManager; lockManager; constructor(dbService, txManager, lockManager){ this.dbService = dbService; this.txManager = txManager; this.lockManager = lockManager; } /** * Generate unique skill ID */ generateSkillId(skillName, version) { const timestamp = Date.now(); const sanitizedName = skillName.replace(/[^a-zA-Z0-9_-]/g, '-'); return `skill-${sanitizedName}-${version}-${timestamp}`; } /** * Create backup of current skill state (for rollback) */ async createBackup(skillPath) { // For now, we'll just return the original path // In production, this would copy to a backup location logger.info('Creating deployment backup', { skillPath }); return skillPath; } /** * Build lock resource for skill deployment */ buildLockResource(skillName) { return { database: 'skills', table: 'skills', key: skillName }; } /** * Record deployment attempt in audit trail (transaction-aware) * * NOTE: This must be called within a transaction context */ async recordDeploymentAudit(adapter, skillId, fromStatus, toStatus, version, success, deployedBy, errorMessage, metadata) { logger.info('Recording deployment audit', { skillId, fromStatus, toStatus, version, success }); try { const result = await adapter.raw(`INSERT INTO deployment_audit (skill_id, from_status, to_status, version, success, deployed_by, error_message, metadata) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, [ skillId, fromStatus, toStatus, version, success ? 1 : 0, deployedBy, errorMessage || null, metadata ? JSON.stringify(metadata) : null ]); const auditId = result.lastInsertId || 0; logger.info('Deployment audit recorded', { auditId, skillId }); return auditId; } catch (error) { logger.error('Failed to record deployment audit', error, { skillId }); // In transaction mode, we want to fail the transaction if audit fails throw error; } } /** * Deploy skill atomically across all databases with distributed locking * * Uses TransactionManager for atomic operations and DistributedLock * to prevent concurrent deployments of the same skill. * * @param request - Deployment request parameters * @returns Deployment result */ async deploySkill(request) { const { skillPath, deployedBy = 'system', explicitVersion, skipValidation = false } = request; logger.info('Starting skill deployment', { skillPath, deployedBy }); let lock = null; let tx = null; try { // Step 1: Validate skill (unless skipped) - BEFORE acquiring lock if (!skipValidation) { const validationResult = await validateSkill(this.dbService, skillPath); if (!validationResult.valid) { logger.warn('Skill validation failed', { skillPath, errorCount: validationResult.errors.length }); // Record validation failure (no transaction needed for failure records) try { const adapter = this.dbService.getAdapter('sqlite'); await this.recordDeploymentAudit(adapter, 'unknown', null, 'FAILED', 'unknown', false, deployedBy, `Validation failed: ${validationResult.errors.map((e)=>e.message).join('; ')}`, { validationErrors: validationResult.errors }); } catch (auditError) { logger.warn('Failed to record validation failure audit (non-blocking)', { error: auditError.message }); } return { success: false, error: 'Validation failed', validationResult }; } } // Step 2: Parse skill metadata const frontmatter = parseFrontmatter(skillPath); const skillName = frontmatter.name; // Step 3: Acquire distributed lock for this skill (prevents concurrent deployments) const lockResource = this.buildLockResource(skillName); logger.debug('Acquiring distributed lock', { skillName, lockResource }); lock = await this.lockManager.acquireLock({ resource: lockResource, timeout: 10000, ttl: 60000, correlationId: `deploy-${skillName}-${Date.now()}` }); logger.info('Distributed lock acquired', { lockId: lock.id, skillName }); // Step 4: Begin cross-database transaction // Note: Currently only using SQLite, but framework supports PostgreSQL too tx = await this.txManager.begin([ 'sqlite' ], { timeout: 30000, correlationId: lock.correlationId }); logger.info('Transaction began', { transactionId: tx.id, skillName }); // Step 5: Determine version within transaction (prevents version conflicts) let version; await tx.execute('sqlite', async (adapter)=>{ if (explicitVersion) { // Check if explicit version already exists const exists = await versionExists(this.dbService, skillName, explicitVersion); if (exists) { throw new StandardError(ErrorCode.DB_DUPLICATE_KEY, `Version ${explicitVersion} already exists for skill: ${skillName}`, { skillName, version: explicitVersion }); } version = explicitVersion; } else { // Auto-increment version (patch by default) version = await getNextVersion(this.dbService, skillName, 'patch'); } }); // Step 6: Generate skill ID const skillId = this.generateSkillId(skillName, version); // Step 7: Create backup for rollback const rollbackPath = await this.createBackup(skillPath); logger.info('Deploying skill within transaction', { skillId, skillName, version, transactionId: tx.id }); // Step 8: Execute atomic deployment operations let auditId = 0; await tx.execute('sqlite', async (adapter)=>{ // Insert into skills table await adapter.raw(`INSERT INTO skills (id, name, version, content_path, status, metadata) VALUES (?, ?, ?, ?, ?, ?)`, [ skillId, skillName, version, skillPath, 'DEPLOYED', JSON.stringify({ deployedBy, deployedAt: new Date().toISOString(), description: frontmatter.description || '', author: frontmatter.author || '', transactionId: tx.id, lockId: lock.id }) ]); // Record successful deployment in audit trail (within same transaction) auditId = await this.recordDeploymentAudit(adapter, skillId, 'APPROVED', 'DEPLOYED', version, true, deployedBy, undefined, { skillName, contentPath: skillPath, transactionId: tx.id, lockId: lock.id }); }); // Step 9: Commit transaction (atomic across all operations) await tx.commit(); logger.info('Transaction committed successfully', { transactionId: tx.id, skillId, skillName, version, auditId }); // Step 10: Release distributed lock await this.lockManager.releaseLock(lock.id); logger.info('Skill deployed successfully', { skillId, skillName, version, auditId, transactionId: tx.id, lockId: lock.id }); return { success: true, deploymentId: auditId, skillId, skillName, version, rollbackPath, deployedAt: new Date(), transactionId: tx.id, lockId: lock.id }; } catch (error) { logger.error('Deployment failed', error, { skillPath }); // Transaction automatically rolled back by TransactionManager on error if (tx) { logger.info('Transaction automatically rolled back', { transactionId: tx.id }); } const errorMessage = error instanceof StandardError ? error.message : `Deployment failed: ${error.message}`; return { success: false, error: errorMessage, transactionId: tx?.id, lockId: lock?.id }; } finally{ // Ensure lock is released even if transaction fails if (lock) { try { await this.lockManager.releaseLock(lock.id); logger.debug('Distributed lock released in finally block', { lockId: lock.id }); } catch (lockError) { logger.error('Failed to release lock in finally block', lockError, { lockId: lock.id }); } } } } /** * Rollback a deployment * * Uses TransactionManager for atomic rollback across all databases. * * @param deploymentId - Deployment audit ID to rollback * @returns True if rollback succeeded */ async rollbackDeployment(deploymentId) { logger.info('Starting deployment rollback', { deploymentId }); let lock = null; let tx = null; try { // Step 1: Get deployment details (outside transaction to avoid deadlock) const adapter = this.dbService.getAdapter('sqlite'); const auditResult = await adapter.raw('SELECT skill_id, version FROM deployment_audit WHERE id = ?', [ deploymentId ]); if (!auditResult || auditResult.length === 0) { throw new StandardError(ErrorCode.DB_NOT_FOUND, `Deployment audit not found: ${deploymentId}`, { deploymentId }); } const { skill_id: skillId, version } = auditResult[0]; // Extract skill name from skill ID const skillName = skillId.replace(/^skill-/, '').replace(/-\d+-\d+$/, ''); // Step 2: Acquire distributed lock for this skill const lockResource = this.buildLockResource(skillName); lock = await this.lockManager.acquireLock({ resource: lockResource, timeout: 10000, ttl: 60000, correlationId: `rollback-${deploymentId}-${Date.now()}` }); logger.info('Distributed lock acquired for rollback', { lockId: lock.id, deploymentId }); // Step 3: Begin rollback transaction tx = await this.txManager.begin([ 'sqlite' ], { timeout: 30000, correlationId: lock.correlationId }); logger.info('Rollback transaction began', { transactionId: tx.id, deploymentId }); // Step 4: Execute rollback operations within transaction await tx.execute('sqlite', async (adapter)=>{ // Delete from skills table await adapter.raw('DELETE FROM skills WHERE id = ?', [ skillId ]); // Record rollback in audit trail (within same transaction) await this.recordDeploymentAudit(adapter, skillId, 'DEPLOYED', 'ROLLED_BACK', version, true, 'system', 'Deployment rolled back', { originalDeploymentId: deploymentId, transactionId: tx.id, lockId: lock.id }); }); // Step 5: Commit rollback transaction await tx.commit(); logger.info('Rollback transaction committed', { transactionId: tx.id, deploymentId, skillId }); // Step 6: Release distributed lock await this.lockManager.releaseLock(lock.id); logger.info('Deployment rollback succeeded', { deploymentId, skillId }); return true; } catch (error) { logger.error('Deployment rollback failed', error, { deploymentId }); // Transaction automatically rolled back on error if (tx) { logger.info('Rollback transaction automatically rolled back', { transactionId: tx.id }); } return false; } finally{ // Ensure lock is released if (lock) { try { await this.lockManager.releaseLock(lock.id); logger.debug('Distributed lock released in rollback finally block', { lockId: lock.id }); } catch (lockError) { logger.error('Failed to release lock in rollback finally block', lockError, { lockId: lock.id }); } } } } /** * Get deployment history for a skill * * @param skillName - Name of the skill * @param limit - Maximum number of results * @returns Array of deployment audit records */ async getDeploymentHistory(skillName, limit = 10) { logger.debug('Fetching deployment history', { skillName, limit }); try { const adapter = this.dbService.getAdapter('sqlite'); const result = await adapter.raw(`SELECT da.* FROM deployment_audit da JOIN skills s ON da.skill_id = s.id WHERE s.name = ? ORDER BY da.deployed_at DESC LIMIT ?`, [ skillName, limit ]); return result || []; } catch (error) { logger.error('Failed to fetch deployment history', error, { skillName }); throw new StandardError(ErrorCode.DB_QUERY_FAILED, `Failed to fetch deployment history for skill: ${skillName}`, { skillName }, error); } } /** * Get all deployments with a specific status * * @param status - Deployment status to filter by * @param limit - Maximum number of results * @returns Array of deployment audit records */ async getDeploymentsByStatus(status, limit = 50) { logger.debug('Fetching deployments by status', { status, limit }); try { const adapter = this.dbService.getAdapter('sqlite'); const result = await adapter.raw(`SELECT * FROM deployment_audit WHERE to_status = ? ORDER BY deployed_at DESC LIMIT ?`, [ status, limit ]); return result || []; } catch (error) { logger.error('Failed to fetch deployments by status', error, { status }); throw new StandardError(ErrorCode.DB_QUERY_FAILED, `Failed to fetch deployments by status: ${status}`, { status }, error); } } } //# sourceMappingURL=skill-deployment.js.map