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
JavaScript
/**
* 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