Skill Promotion Workflow - Atomic promotion from staging to production
Manages atomic promotion of skills from staging directory to production with validation, SLA enforcement, and optional auto-deployment.
The skill promotion workflow automates the process of moving generated skills from the staging directory to production, ensuring:
.claude/skills/
├── staging/ # Skills pending promotion (temporary)
│ ├── auth-v2/
│ │ ├── SKILL.md
│ │ ├── execute.sh
│ │ └── test.sh
│ └── logging-v3/
│ └── ...
└── [production]/ # Production skills (after promotion)
├── authentication/
├── logging/
└── ...
# List all staged skills
./scripts/promote-staged-skills.sh --list
# Check for stale skills (>48h)
./scripts/promote-staged-skills.sh --check-stale
# Promote a specific skill (with confirmation prompt)
./scripts/promote-staged-skills.sh .claude/skills/staging/auth-v2
# Auto-promote if validation passes (no prompt)
./scripts/promote-staged-skills.sh .claude/skills/staging/auth-v2 --auto
# Promote with git commit and deployment
./scripts/promote-staged-skills.sh .claude/skills/staging/auth-v2 --auto --git-commit --deploy
# Force promotion (skip validation - admin only)
./scripts/promote-staged-skills.sh .claude/skills/staging/auth-v2 --force --auto
import { SkillPromotionService } from './src/services/skill-promotion';
import { DatabaseService } from './src/lib/database-service';
const dbService = new DatabaseService({ type: 'sqlite', path: './data/cfn.db' });
const promotionService = new SkillPromotionService(dbService);
// Promote a skill
const result = await promotionService.promoteSkill(
'.claude/skills/staging/auth-v2',
{
autoDeploy: true,
gitCommit: true,
notify: true,
promotedBy: 'admin@example.com'
}
);
if (result.success) {
console.log(`Skill promoted: ${result.skillName}`);
console.log(`Production path: ${result.productionPath}`);
console.log(`Deployment ID: ${result.deploymentId}`);
} else {
console.error(`Promotion failed: ${result.error}`);
}
// List staged skills
const stagedSkills = await promotionService.listStagedSkills();
console.log(`${stagedSkills.length} skills in staging`);
// Check for stale skills (>48h)
const staleSkills = await promotionService.checkStaleness();
if (staleSkills.length > 0) {
console.log(`WARNING: ${staleSkills.length} stale skills detected`);
for (const skill of staleSkills) {
console.log(` - ${skill.name} (${skill.ageHours}h, ${skill.slaBreachHours}h over SLA)`);
}
}
The promotion validator performs the following checks:
SKILL.md exists and is readableexecute.sh exists and is executabletest.sh, README.md (warnings if missing)name, description, versionauthor, tags, dependenciestest.sh exists, it must be executableSkills in staging for >48 hours trigger SLA breach alerts:
./scripts/promote-staged-skills.sh --check-stale
# Run daily at 9am to check for stale skills
0 9 * * * cd /path/to/project && ./scripts/promote-staged-skills.sh --check-stale
import { PromotionSLAEnforcer } from './src/jobs/promotion-sla-enforcer';
const enforcer = new PromotionSLAEnforcer(dbService, {
autoPromote: true, // Auto-promote stale skills
notifyStale: true, // Send notifications
});
await enforcer.enforceSLA();
Environment variables:
# SLA threshold (hours)
CFN_PROMOTION_SLA_THRESHOLD=48
# Auto-promote stale skills
CFN_PROMOTION_AUTO_PROMOTE=true
# Enable git commits
CFN_PROMOTION_GIT_COMMIT=true
# Enable auto-deployment
CFN_PROMOTION_AUTO_DEPLOY=true
-- Skills currently in staging
SELECT name, created_at,
ROUND((julianday('now') - julianday(created_at)) * 24, 1) as age_hours
FROM staged_skills
WHERE promoted = 0
ORDER BY created_at ASC;
-- Promotion success rate (last 30 days)
SELECT
COUNT(*) as total_promotions,
SUM(CASE WHEN success = 1 THEN 1 ELSE 0 END) as successful,
ROUND(100.0 * SUM(success) / COUNT(*), 2) as success_rate
FROM skill_promotions
WHERE promoted_at > datetime('now', '-30 days');
-- SLA breaches (skills >48h in staging)
SELECT COUNT(*) as sla_breaches
FROM staged_skills
WHERE promoted = 0
AND created_at < datetime('now', '-48 hours');
After promotion, skills can be automatically deployed:
const result = await promotionService.promoteSkill(stagingPath, {
autoDeploy: true // Triggers SkillDeploymentPipeline
});
file-operations.ts: Atomic moves, file lockinglogging.ts: Structured loggingerrors.ts: Error handlingGenerated skills are placed in staging directory:
// Skill generator outputs to staging
const stagingPath = await skillGenerator.generate({
output: '.claude/skills/staging/new-skill'
});
// Later: promote to production
await promotionService.promoteSkill(stagingPath);
--overwrite or resolve manually--force (admin only) or fix testsIf promotion fails mid-operation:
# If promotion succeeded but deployment failed
rm -rf .claude/skills/production-skill
mv .claude/skills/staging/backup-skill .claude/skills/staging/production-skill
Solution: Add execute.sh to staged skill
Solution: Run chmod +x execute.sh
Solution: Fix tests or use --force to skip
Solution: Use --overwrite flag or choose different name
Solution: Ensure git repo is initialized and configured
.claude/skills/cfn-deployment/SKILL.mddocs/IMPLEMENTATION_UTILITIES.mddocs/SKILL_GENERATION.md (future)docs/SKILL_PROMOTION_WORKFLOW.md