commit b704bccc5ff5d56bb52c846c1066e9cff6724654 Author: Zhongwei Li Date: Sat Nov 29 17:59:24 2025 +0800 Initial commit diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json new file mode 100644 index 0000000..59dce19 --- /dev/null +++ b/.claude-plugin/plugin.json @@ -0,0 +1,15 @@ +{ + "name": "deployment", + "description": "v1.0.0 - Safe deployment automation with CICD integration. Manages branch-based deployments to dev/uat/prod environments with safety checks and build validation.", + "version": "1.0.0", + "author": { + "name": "AutomateWith.Us", + "email": "team@automatewith.us" + }, + "commands": [ + "./commands" + ], + "hooks": [ + "./hooks" + ] +} \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..fc08a38 --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# deployment + +v1.0.0 - Safe deployment automation with CICD integration. Manages branch-based deployments to dev/uat/prod environments with safety checks and build validation. diff --git a/commands/deploy.md b/commands/deploy.md new file mode 100644 index 0000000..de31a3d --- /dev/null +++ b/commands/deploy.md @@ -0,0 +1,244 @@ +You are managing a deployment system. The user wants to deploy to an environment. + +## Task: Deploy Application + +Parse the environment from arguments: $ARGUMENTS +Expected format: /deploy [environment] +Valid environments: dev, uat, prod + +### Step 1: Parse and Validate Environment + +Extract the environment name from $ARGUMENTS. + +If no environment provided, show error and STOP: +``` +❌ Error: No environment specified + +Usage: /deploy [environment] + +Available environments: + • dev - Development environment (deploy_dev branch) + • uat - User Acceptance Testing (deploy_uat branch) + • prod - Production environment (deploy_prod branch) + +Example: /deploy dev +``` + +### Step 2: Load Configuration + +Run CLI to get configuration: + +```bash +node deployment/cli/deploy-cli.js config +``` + +If configuration not found (error in output), show error and STOP: +``` +❌ Error: Deployment not configured + +💡 Run /deploy:init to set up deployment +``` + +Parse the JSON output and extract: +- mainBranch +- buildCommand +- environments.{env} configuration + +If the requested environment doesn't exist in config, show available environments and STOP: +``` +❌ Error: Unknown environment "{env}" + +Available environments: {list from config} + +💡 Edit .claude/deployment.config.json to add custom environments +``` + +### Step 3: Run Pre-Deployment Validation + +Run CLI validation for the target environment: + +```bash +node deployment/cli/deploy-cli.js validate --check-git --env {environment} +``` + +Parse the JSON output. + +If `success: false`, show all errors and STOP: +``` +🚫 Deployment blocked by safety checks: + +{for each error:} +❌ {error.message} + 💡 Fix: {error.fix} + +Please resolve these issues before deploying. +``` + +If warnings exist (success: true but warnings present), show warnings but continue: +``` +⚠️ Warnings detected: +{for each warning:} + • {warning.message} + 💡 {warning.fix} + +Continuing with deployment... +``` + +### Step 4: Determine Source Branch + +Based on environment configuration: +- If environment has `sourceBranch`: Use that branch +- If environment has `sourceEnvironment`: Use that environment's deployment branch + +Example: +- dev: source is "main" (sourceBranch) +- uat: source is "deploy_dev" (from sourceEnvironment: "dev") +- prod: source is "deploy_uat" (from sourceEnvironment: "uat") + +Store the source branch and deployment branch for later steps. + +### Step 5: Run Build Validation + +Show progress: +``` +🔨 Running build validation... + Command: {buildCommand} +``` + +Execute the build command: + +```bash +{buildCommand} +``` + +Monitor the output. + +**If build succeeds:** +``` +✓ Build completed successfully +``` +Proceed to Step 6. + +**If build fails:** + +Show the build errors: +``` +❌ Build failed with errors: + +{build_error_output} + +What would you like to do? +``` + +Use AskUserQuestion: +```json +{ + "questions": [{ + "question": "Build failed. How should we proceed?", + "header": "Action", + "multiSelect": false, + "options": [ + {"label": "Show me the errors, I'll fix them", "description": "Stop deployment, let me fix manually"}, + {"label": "Try to auto-fix", "description": "Let Claude attempt to fix the errors"}, + {"label": "Cancel deployment", "description": "Stop the deployment process"} + ] + }] +} +``` + +- If "Show me" or "Cancel": STOP with guidance +- If "Try to auto-fix": Attempt to fix, then re-run build + - If second build fails: STOP and ask user to fix manually + +### Step 6: Checkout and Update Source Branch + +Ensure we're on the correct source branch and it's up-to-date: + +```bash +git fetch origin && git checkout {source_branch} && git pull origin {source_branch} +``` + +Verify the branch is clean and up-to-date. + +### Step 7: Merge to Deployment Branch + +Get the deployment branch from config: `environments.{env}.branch` + +```bash +git checkout {deployment_branch} && git pull origin {deployment_branch} && git merge {source_branch} --no-ff -m "Deploy {source_branch} to {environment} environment" +``` + +**If merge conflicts occur:** + +``` +❌ Merge conflict detected + +Conflicting files: +{list files from git status} + +You need to resolve these conflicts manually: +1. The merge is in progress with conflicts +2. Resolve conflicts in the files listed above +3. Run: git add . && git commit +4. Then retry: /deploy {environment} + +Aborting deployment. +``` + +Run: `git merge --abort` +STOP execution. + +**If merge succeeds:** +``` +✓ Merged {source_branch} → {deployment_branch} +``` + +### Step 8: Push to Trigger Deployment + +Push the deployment branch to trigger Netlify auto-deploy: + +```bash +git push origin {deployment_branch} +``` + +If push fails, show error: +``` +❌ Push failed + +{error output} + +💡 Check your remote connection and permissions +``` + +STOP execution. + +### Step 9: Display Success Message + +Show deployment confirmation: + +``` +✓ Deployment initiated successfully +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +🚀 Environment: {environment} +📦 Branch: {deployment_branch} +🔗 Source: {source_branch} + +📊 Netlify will now build and deploy automatically + Check your Netlify dashboard for deployment status + +💡 Next steps: + {if dev} → After testing, deploy to UAT: /deploy uat + {if uat} → After approval, deploy to prod: /deploy prod + {if prod} → Monitor production for any issues + +🔍 To check status: Visit your Netlify dashboard +``` + +--- + +**IMPORTANT:** +- Always validate before executing +- Show clear progress updates +- Handle errors gracefully with recovery options +- Enforce environment progression (dev → uat → prod) +- Never skip safety checks diff --git a/commands/init.md b/commands/init.md new file mode 100644 index 0000000..ddfb912 --- /dev/null +++ b/commands/init.md @@ -0,0 +1,176 @@ +You are managing a deployment configuration system. The user wants to initialize deployment settings for their project. + +## Task: Initialize Deployment Configuration + +This command sets up deployment configuration for the project. This is a one-time setup. + +### Step 1: Check for Existing Configuration + +Run the CLI to check if configuration already exists: + +```bash +node deployment/cli/deploy-cli.js config 2>&1 +``` + +If the output contains "Configuration not found", proceed to Step 2. + +If configuration exists, show this error and STOP: +``` +❌ Error: Deployment configuration already exists +📁 Location: .claude/deployment.config.json + +💡 To view current config: Run node deployment/cli/deploy-cli.js config +💡 To modify: Edit .claude/deployment.config.json directly +``` + +### Step 2: Gather Configuration Details + +Ask the user the following questions using AskUserQuestion tool: + +```json +{ + "questions": [ + { + "question": "What is your main development branch?", + "header": "Main Branch", + "multiSelect": false, + "options": [ + {"label": "main", "description": "Default branch named 'main'"}, + {"label": "master", "description": "Legacy default branch 'master'"}, + {"label": "develop", "description": "Use 'develop' as main branch"} + ] + }, + { + "question": "What command should run to build your project?", + "header": "Build Command", + "multiSelect": false, + "options": [ + {"label": "npm run build", "description": "Node.js project with npm"}, + {"label": "yarn build", "description": "Node.js project with yarn"}, + {"label": "pnpm build", "description": "Node.js project with pnpm"}, + {"label": "make build", "description": "Project with Makefile"} + ] + } + ] +} +``` + +Store the user's answers for Step 3. + +### Step 3: Create Configuration + +Use the CLI to initialize the configuration with user's choices: + +```bash +node deployment/cli/deploy-cli.js init --main-branch {user_main_branch} --build-command "{user_build_command}" +``` + +**Expected output**: JSON with success: true + +If the command fails, show error and STOP: +``` +❌ Error: Failed to initialize configuration +{error_message} + +💡 Check that .claude/ directory is writable +``` + +### Step 4: Verify Configuration Created + +Read the created configuration to verify: + +```bash +node deployment/cli/deploy-cli.js config +``` + +Parse the JSON output and extract the environments. + +### Step 5: Display Configuration Summary + +Show the user what was configured: + +``` +✓ Deployment configuration initialized +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +📁 Location: .claude/deployment.config.json + +📋 Configuration: + Main Branch: {main_branch} + Build Command: {build_command} + +🌍 Environments configured: + • dev (deploy_dev) ← {main_branch} + • uat (deploy_uat) ← deploy_dev + • prod (deploy_prod) ← deploy_uat + +🔒 Safety Features: + ✓ Uncommitted files check + ✓ Branch validation + ✓ Clean build requirement + +💡 Next Steps: + 1. Review config: Edit .claude/deployment.config.json if needed + 2. Create deployment branches (see below) + 3. Deploy to dev: /deploy dev +``` + +### Step 6: Offer to Create Deployment Branches + +Ask the user: + +``` +The following deployment branches need to exist in your repository: + - deploy_dev + - deploy_uat + - deploy_prod + +Would you like me to create these branches now? +``` + +Use AskUserQuestion: +```json +{ + "questions": [{ + "question": "Create deployment branches?", + "header": "Setup", + "multiSelect": false, + "options": [ + {"label": "Yes", "description": "Create all deployment branches from main"}, + {"label": "No", "description": "I'll create them manually later"} + ] + }] +} +``` + +If user selects "Yes": +```bash +git checkout {main_branch} && git pull origin {main_branch} && git checkout -b deploy_dev && git push -u origin deploy_dev && git checkout -b deploy_uat && git push -u origin deploy_uat && git checkout -b deploy_prod && git push -u origin deploy_prod && git checkout {main_branch} +``` + +Show success message: +``` +✓ Deployment branches created successfully + • deploy_dev + • deploy_uat + • deploy_prod + +All branches have been pushed to origin. +You're ready to deploy! +``` + +If user selects "No": +``` +💡 Remember to create these branches manually: + git checkout {main_branch} + git checkout -b deploy_dev && git push -u origin deploy_dev + git checkout -b deploy_uat && git push -u origin deploy_uat + git checkout -b deploy_prod && git push -u origin deploy_prod +``` + +--- + +**IMPORTANT:** +- Use CLI for all config operations (plan mode support) +- Validate user input before proceeding +- Provide clear next steps +- Make it interactive and user-friendly diff --git a/hooks/pre-deploy-check.js b/hooks/pre-deploy-check.js new file mode 100755 index 0000000..db3227d --- /dev/null +++ b/hooks/pre-deploy-check.js @@ -0,0 +1,312 @@ +#!/usr/bin/env node + +const fs = require('fs'); +const path = require('path'); +const { execSync } = require('child_process'); + +/** + * PreToolUse Hook: Pre-deployment Safety Checks + * + * Validates deployment operations before execution: + * - Detects deployment-related commands + * - Checks for uncommitted files + * - Validates current branch + * - Blocks execution if unsafe + */ + +async function main() { + try { + // Read input from stdin + const input = fs.readFileSync(0, 'utf8').trim(); + + if (!input) { + process.exit(0); // No input, exit silently + } + + const eventData = JSON.parse(input); + + // Extract tool input + const toolInput = eventData.tool_input || {}; + const cwd = eventData.cwd || process.cwd(); + + // Change to correct directory + process.chdir(cwd); + + // Check if this is a deployment-related command + if (!isDeploymentCommand(toolInput)) { + process.exit(0); // Not a deployment, allow execution + } + + // Run safety checks + const issues = await runSafetyChecks(toolInput); + + if (issues.length === 0) { + process.exit(0); // All checks passed, allow execution + } + + // Checks failed - block execution + const output = { + hookSpecificOutput: { + blockExecution: true, + additionalContext: formatIssues(issues, toolInput) + } + }; + + console.log(JSON.stringify(output)); + process.exit(0); + + } catch (error) { + // Silent failure - never block Claude Code + // Log error to stderr for debugging (optional) + process.exit(0); + } +} + +/** + * Detect if command is deployment-related + */ +function isDeploymentCommand(toolInput) { + const command = toolInput.command || ''; + const description = toolInput.description || ''; + + // Check for deployment-related keywords + const deploymentKeywords = [ + 'deploy_dev', + 'deploy_uat', + 'deploy_prod', + '/deploy', + 'deployment' + ]; + + const commandLower = command.toLowerCase(); + const descriptionLower = description.toLowerCase(); + + // Check if command or description contains deployment keywords + for (const keyword of deploymentKeywords) { + if (commandLower.includes(keyword) || descriptionLower.includes(keyword)) { + return true; + } + } + + // Check for git operations on deployment branches + if (command.includes('git merge') || command.includes('git push')) { + // Check if targeting a deployment branch + const deployBranchPattern = /deploy_(dev|uat|prod)/; + if (deployBranchPattern.test(command)) { + return true; + } + } + + return false; +} + +/** + * Extract environment from command if present + */ +function extractEnvironment(toolInput) { + const command = toolInput.command || ''; + const description = toolInput.description || ''; + + const envPattern = /(dev|uat|prod)/i; + + // Try to find environment in command + const commandMatch = command.match(envPattern); + if (commandMatch) { + return commandMatch[1].toLowerCase(); + } + + // Try to find in description + const descMatch = description.match(envPattern); + if (descMatch) { + return descMatch[1].toLowerCase(); + } + + return null; +} + +/** + * Run all safety checks + * Returns array of issues found + */ +async function runSafetyChecks(toolInput) { + const issues = []; + + // Load configuration + const configPath = path.join(process.cwd(), '.claude/deployment.config.json'); + + if (!fs.existsSync(configPath)) { + // No config = not a configured deployment project + // Don't block, user might be doing manual git operations + return []; + } + + let config; + try { + config = JSON.parse(fs.readFileSync(configPath, 'utf8')); + } catch (error) { + // Malformed config - warn but don't block + return [{ + type: 'warning', + message: 'Deployment config malformed', + fix: 'Check .claude/deployment.config.json syntax' + }]; + } + + // Extract environment from command + const environment = extractEnvironment(toolInput); + + // Check 1: Uncommitted files + if (config.safeguards?.checkUncommittedFiles) { + const uncommitted = checkUncommittedFiles(); + if (uncommitted.count > 0) { + issues.push({ + type: 'error', + message: `${uncommitted.count} uncommitted file(s) detected`, + details: uncommitted.files.slice(0, 5), // Show first 5 files + fix: 'Commit or stash changes: git add . && git commit -m "..."', + count: uncommitted.count + }); + } + } + + // Check 2: Branch validation + if (config.safeguards?.checkBranch && environment) { + const branchIssue = checkBranchValidity(config, environment); + if (branchIssue) { + issues.push(branchIssue); + } + } + + // Check 3: Environment exists + if (environment && !config.environments[environment]) { + issues.push({ + type: 'error', + message: `Unknown environment: ${environment}`, + fix: `Valid environments: ${Object.keys(config.environments).join(', ')}` + }); + } + + return issues; +} + +/** + * Check for uncommitted files + */ +function checkUncommittedFiles() { + try { + const output = execSync('git status --porcelain', { + encoding: 'utf8', + stdio: ['pipe', 'pipe', 'pipe'] + }).trim(); + + if (!output) { + return { count: 0, files: [] }; + } + + const files = output.split('\n').map(line => { + const status = line.substring(0, 2); + const file = line.substring(3); + return { status, file }; + }); + + return { count: files.length, files }; + + } catch (error) { + // Not a git repo or git command failed + return { count: 0, files: [] }; + } +} + +/** + * Check if on correct branch for deployment + */ +function checkBranchValidity(config, environment) { + try { + const currentBranch = execSync('git rev-parse --abbrev-ref HEAD', { + encoding: 'utf8', + stdio: ['pipe', 'pipe', 'pipe'] + }).trim(); + + const envConfig = config.environments[environment]; + if (!envConfig) { + return null; // Environment doesn't exist + } + + // Determine expected source branch + let expectedBranch; + if (envConfig.sourceBranch) { + expectedBranch = envConfig.sourceBranch; + } else if (envConfig.sourceEnvironment) { + const sourceEnv = config.environments[envConfig.sourceEnvironment]; + expectedBranch = sourceEnv?.branch; + } + + if (!expectedBranch) { + return null; // Can't determine expected branch + } + + if (currentBranch !== expectedBranch) { + return { + type: 'warning', + message: `On branch "${currentBranch}", expected "${expectedBranch}"`, + fix: `Switch to correct branch: git checkout ${expectedBranch}` + }; + } + + return null; // Branch is correct + + } catch (error) { + return null; // Git command failed, don't block + } +} + +/** + * Format issues into user-friendly message + */ +function formatIssues(issues, toolInput) { + const errors = issues.filter(i => i.type === 'error'); + const warnings = issues.filter(i => i.type === 'warning'); + + let message = '🚫 Deployment blocked by safety checks:\n\n'; + + // Show errors + if (errors.length > 0) { + message += '**Errors** (must fix):\n'; + for (const error of errors) { + message += `\n❌ ${error.message}\n`; + + if (error.details && error.details.length > 0) { + message += ' Files:\n'; + for (const detail of error.details) { + message += ` - ${detail.file} (${detail.status})\n`; + } + if (error.count && error.details.length < error.count) { + message += ` ... and ${error.count - error.details.length} more\n`; + } + } + + if (error.fix) { + message += ` 💡 Fix: ${error.fix}\n`; + } + } + } + + // Show warnings + if (warnings.length > 0) { + message += '\n**Warnings** (recommended to fix):\n'; + for (const warning of warnings) { + message += `\n⚠️ ${warning.message}\n`; + if (warning.fix) { + message += ` 💡 Suggestion: ${warning.fix}\n`; + } + } + } + + message += '\n---\n\n'; + message += 'Fix the issues above before deploying.\n'; + message += 'Once fixed, retry the deployment command.\n'; + + return message; +} + +main(); diff --git a/plugin.lock.json b/plugin.lock.json new file mode 100644 index 0000000..520109f --- /dev/null +++ b/plugin.lock.json @@ -0,0 +1,53 @@ +{ + "$schema": "internal://schemas/plugin.lock.v1.json", + "pluginId": "gh:awudevelop/claude-plugins:deployment", + "normalized": { + "repo": null, + "ref": "refs/tags/v20251128.0", + "commit": "824a49ec5abf11b0d811fda9c8861515015582ba", + "treeHash": "d715bf800868f8068f8a160d47f09a378e934fae7a013e7f41d5aa4dc6855b50", + "generatedAt": "2025-11-28T10:14:05.577412Z", + "toolVersion": "publish_plugins.py@0.2.0" + }, + "origin": { + "remote": "git@github.com:zhongweili/42plugin-data.git", + "branch": "master", + "commit": "aa1497ed0949fd50e99e70d6324a29c5b34f9390", + "repoRoot": "/Users/zhongweili/projects/openmind/42plugin-data" + }, + "manifest": { + "name": "deployment", + "description": "v1.0.0 - Safe deployment automation with CICD integration. Manages branch-based deployments to dev/uat/prod environments with safety checks and build validation.", + "version": "1.0.0" + }, + "content": { + "files": [ + { + "path": "README.md", + "sha256": "8150f9ee2d32f94c14c5ca28064341b078e6c021b10a88952ab1169a41be5385" + }, + { + "path": "hooks/pre-deploy-check.js", + "sha256": "a716ecb7bd8d4bc154f1fe9565bafaf48b83a70c71f5fc7bea0c42b23ff54c58" + }, + { + "path": ".claude-plugin/plugin.json", + "sha256": "d1e6223a482459eb57e397cdd73dc4e72aebe884cd8977630f3c75e91b42cc85" + }, + { + "path": "commands/init.md", + "sha256": "0b9b55305d20b47f46bcbcd83f9d50a60df8f1d8cd7b22cf9b77b933ab3f108f" + }, + { + "path": "commands/deploy.md", + "sha256": "e504114e294c4d4600d753f940d1e8f7bd9e3f7ec515c7828b978355d5d1f52a" + } + ], + "dirSha256": "d715bf800868f8068f8a160d47f09a378e934fae7a013e7f41d5aa4dc6855b50" + }, + "security": { + "scannedAt": null, + "scannerVersion": null, + "flags": [] + } +} \ No newline at end of file