github-cicd.ts•10.1 kB
import { SSHService } from '../services/ssh-service.js';
import { logger } from '../utils/logger.js';
import { DeployKey, GitHubAction } from '../types/index.js';
export interface GitHubCICDConfig {
repoUrl: string;
deployPath: string;
}
export interface GitHubCICDResult {
success: boolean;
message: string;
deployKey?: DeployKey;
actionSecret?: string;
workflowFile?: GitHubAction;
}
export class GitHubCICD {
constructor(private sshService: SSHService) {}
async setupCICD(config: GitHubCICDConfig): Promise<GitHubCICDResult> {
try {
logger.info('Setting up GitHub CI/CD', {
repoUrl: config.repoUrl,
deployPath: config.deployPath,
});
// Generate deploy key
const deployKey = await this.generateDeployKey();
if (!deployKey) {
return {
success: false,
message: 'Failed to generate deploy key',
};
}
// Setup deployment directory
const setupResult = await this.setupDeploymentDirectory(config.deployPath);
if (!setupResult.success) {
return setupResult;
}
// Generate action secret
const actionSecret = await this.generateActionSecret();
if (!actionSecret) {
return {
success: false,
message: 'Failed to generate action secret',
};
}
// Create workflow file
const workflowFile = this.generateWorkflowFile(config);
return {
success: true,
message: 'GitHub CI/CD setup completed successfully',
deployKey,
actionSecret,
workflowFile,
};
} catch (error) {
logger.error('GitHub CI/CD setup failed', { error, config });
return {
success: false,
message: `GitHub CI/CD setup failed: ${error instanceof Error ? error.message : 'Unknown error'}`,
};
}
}
private async generateDeployKey(): Promise<DeployKey | null> {
try {
// Generate SSH key pair
const keyPath = '/root/.ssh/deploy_key';
const generateResult = await this.sshService.executeCommand(
`ssh-keygen -t ed25519 -C "deploy-key-$(date +%s)" -f ${keyPath} -N ""`
);
if (!generateResult.success) {
logger.error('Failed to generate deploy key', { error: generateResult.stderr });
return null;
}
// Read public key
const publicKeyResult = await this.sshService.executeCommand(`cat ${keyPath}.pub`);
if (!publicKeyResult.success) {
logger.error('Failed to read public key', { error: publicKeyResult.stderr });
return null;
}
// Read private key
const privateKeyResult = await this.sshService.executeCommand(`cat ${keyPath}`);
if (!privateKeyResult.success) {
logger.error('Failed to read private key', { error: privateKeyResult.stderr });
return null;
}
// Get fingerprint
const fingerprintResult = await this.sshService.executeCommand(
`ssh-keygen -lf ${keyPath}.pub`
);
const fingerprint = fingerprintResult.success
? fingerprintResult.stdout.split(' ')[1] || 'Unknown'
: 'Unknown';
return {
publicKey: publicKeyResult.stdout.trim(),
privateKey: privateKeyResult.stdout.trim(),
fingerprint,
};
} catch (error) {
logger.error('Deploy key generation failed', { error });
return null;
}
}
private async setupDeploymentDirectory(
deployPath: string
): Promise<{ success: boolean; message: string }> {
try {
// Create deployment directory
const mkdirResult = await this.sshService.executeCommand(`mkdir -p ${deployPath}`);
if (!mkdirResult.success) {
return {
success: false,
message: `Failed to create deployment directory: ${mkdirResult.stderr}`,
};
}
// Create deployment user (optional, for better security)
await this.sshService.executeCommand(
'id -u deploy 2>/dev/null || useradd -m -s /bin/bash deploy'
);
// Set proper permissions
await this.sshService.executeCommand(
`chown -R deploy:deploy ${deployPath} 2>/dev/null || chown -R root:root ${deployPath}`
);
// Create deployment script
const deployScript = this.generateDeployScript(deployPath);
const scriptResult = await this.sshService.executeCommand(
`cat > ${deployPath}/deploy.sh << 'EOF'\n${deployScript}\nEOF`
);
if (!scriptResult.success) {
return {
success: false,
message: `Failed to create deployment script: ${scriptResult.stderr}`,
};
}
// Make script executable
await this.sshService.executeCommand(`chmod +x ${deployPath}/deploy.sh`);
return {
success: true,
message: 'Deployment directory setup completed',
};
} catch (error) {
return {
success: false,
message: `Deployment directory setup failed: ${error instanceof Error ? error.message : 'Unknown error'}`,
};
}
}
private generateDeployScript(deployPath: string): string {
return `#!/bin/bash
set -e
DEPLOY_PATH="${deployPath}"
REPO_PATH="$DEPLOY_PATH/repo"
BACKUP_PATH="$DEPLOY_PATH/backups"
echo "Starting deployment at $(date)"
# Create backup of current deployment
if [ -d "$REPO_PATH" ]; then
echo "Creating backup..."
mkdir -p "$BACKUP_PATH"
tar -czf "$BACKUP_PATH/backup-$(date +%Y%m%d_%H%M%S).tar.gz" -C "$REPO_PATH" . 2>/dev/null || true
# Keep only last 5 backups
cd "$BACKUP_PATH" && ls -t backup-*.tar.gz 2>/dev/null | tail -n +6 | xargs rm -f || true
fi
# Clone or pull repository
if [ -d "$REPO_PATH/.git" ]; then
echo "Updating repository..."
cd "$REPO_PATH"
git fetch origin
git reset --hard origin/main
else
echo "Cloning repository..."
rm -rf "$REPO_PATH"
git clone "\\$REPO_URL" "$REPO_PATH"
cd "$REPO_PATH"
fi
# Install dependencies and build (customize as needed)
if [ -f "package.json" ]; then
echo "Installing Node.js dependencies..."
npm ci --production
if [ -f "package.json" ] && grep -q '\\"build\\"' package.json; then
echo "Building application..."
npm run build
fi
fi
# Restart application (customize as needed)
if command -v pm2 >/dev/null 2>&1; then
echo "Restarting application with PM2..."
pm2 restart all || pm2 start ecosystem.config.js || echo "PM2 restart failed"
fi
# Restart Nginx if needed
if systemctl is-active --quiet nginx; then
echo "Reloading Nginx..."
systemctl reload nginx
fi
echo "Deployment completed successfully at $(date)"`;
}
private async generateActionSecret(): Promise<string | null> {
try {
// Generate a random secret for GitHub Actions
const secretResult = await this.sshService.executeCommand('openssl rand -base64 32');
if (!secretResult.success) {
logger.error('Failed to generate action secret', { error: secretResult.stderr });
return null;
}
return secretResult.stdout.trim();
} catch (error) {
logger.error('Action secret generation failed', { error });
return null;
}
}
private generateWorkflowFile(config: GitHubCICDConfig): GitHubAction {
const workflowContent = `name: Deploy to VPS
on:
push:
branches: [ main, master ]
workflow_dispatch:
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js (if applicable)
uses: actions/setup-node@v4
with:
node-version: '18'
cache: 'npm'
if: always()
- name: Install dependencies
run: |
if [ -f "package.json" ]; then
npm ci
fi
- name: Run tests
run: |
if [ -f "package.json" ] && grep -q '\\"test\\"' package.json; then
npm test
fi
- name: Build application
run: |
if [ -f "package.json" ] && grep -q '\\"build\\"' package.json; then
npm run build
fi
- name: Deploy to VPS
uses: appleboy/ssh-action@v1.0.0
with:
host: \${{ secrets.VPS_HOST }}
username: \${{ secrets.VPS_USERNAME }}
key: \${{ secrets.VPS_SSH_KEY }}
port: \${{ secrets.VPS_PORT }}
script: |
export REPO_URL="${config.repoUrl}"
cd ${config.deployPath}
./deploy.sh
- name: Notify deployment status
if: always()
run: |
if [ "\${{ job.status }}" = "success" ]; then
echo "✅ Deployment completed successfully"
else
echo "❌ Deployment failed"
fi`;
return {
name: 'deploy.yml',
content: workflowContent,
path: '.github/workflows/deploy.yml',
};
}
async getSetupInstructions(result: GitHubCICDResult): Promise<string> {
if (!result.success || !result.deployKey || !result.actionSecret || !result.workflowFile) {
return 'Setup failed. Please check the error messages.';
}
return `
🚀 GitHub CI/CD Setup Instructions
1. Add Deploy Key to GitHub Repository:
- Go to your repository on GitHub
- Navigate to Settings > Deploy keys
- Click "Add deploy key"
- Title: "VPS Deploy Key"
- Key: ${result.deployKey.publicKey}
- ✅ Check "Allow write access"
- Click "Add key"
2. Add Secrets to GitHub Repository:
- Go to your repository on GitHub
- Navigate to Settings > Secrets and variables > Actions
- Add the following secrets:
VPS_HOST: [Your VPS IP address]
VPS_USERNAME: root
VPS_SSH_KEY: ${result.deployKey.privateKey}
VPS_PORT: 22
3. Add Workflow File:
- Create file: ${result.workflowFile.path}
- Content:
${result.workflowFile.content}
4. Push to trigger deployment:
git add .
git commit -m "Add CI/CD workflow"
git push origin main
🔐 Deploy Key Fingerprint: ${result.deployKey.fingerprint}
Note: The deployment script is located at the deployment path and can be customized as needed.`;
}
}