Common Patterns & Best Practices
Overview
This section covers common patterns, best practices, and design approaches for building robust automation with Xec.
Pattern Categories
Execution Patterns
- Error Handling - Robust error management
- Parallel Execution - Concurrent command execution
- Async Patterns - Async/await usage
Target Patterns
- Multi-Target Execution - Running one step across multiple hosts
- Handling Target Failures - Reading failures honestly and stopping a rollout early
- Multi-Stage Pipelines - Stages that depend on each other
Data Patterns
- Stream Processing - Real-time data handling
- File Operations - Cross-environment file management
- Secret Management - Secure credential handling
Workflow Patterns
- Chaining Commands - Method chaining patterns
- Error Handling - Try/catch patterns
- Task Automation - Building automation workflows
Integration Patterns
- CI/CD Integration - Pipeline automation
- Database Operations - Database automation
- Container Orchestration - Docker/K8s patterns
- Deployment Automation - Deployment patterns
Quick Examples
Error Handling Pattern
// Robust error handling with retries
async function deployWithRetry(target: string, maxRetries = 3) {
for (let i = 0; i < maxRetries; i++) {
try {
const result = await $.ssh(target)`
cd /app &&
git pull &&
npm install &&
npm run build &&
pm2 restart app
`.timeout(300000);
console.log(`✅ Deployed successfully to ${target}`);
return result;
} catch (error) {
console.log(`⚠️ Attempt ${i + 1} failed: ${error.message}`);
if (i === maxRetries - 1) {
throw new Error(`Failed to deploy after ${maxRetries} attempts`);
}
// Wait before retry
await new Promise(resolve => setTimeout(resolve, 5000));
}
}
}
Parallel Execution Pattern
// Execute commands across multiple targets in parallel
async function healthCheckAll(targets: string[]) {
const results = await Promise.allSettled(
targets.map(async target => {
const result = await $.ssh(target)`
echo "=== System Health ===" &&
uptime &&
df -h / &&
free -m &&
systemctl status app
`.nothrow();
return {
target,
healthy: result.exitCode === 0,
output: result.stdout,
error: result.stderr
};
})
);
// Process results
const summary = results.map((result, i) => {
if (result.status === 'fulfilled') {
return result.value;
} else {
return {
target: targets[i],
healthy: false,
error: result.reason
};
}
});
return summary;
}
Pipeline Pattern
// Build a data processing pipeline
async function processLogs(logFile: string) {
const lines = await $`
cat ${logFile} |
grep ERROR |
awk '{print $1, $2, $NF}' |
sort |
uniq -c |
sort -rn |
head -20
`.lines();
return lines.map(line => {
const [count, date, time, error] = line.trim().split(/\s+/);
return { count: parseInt(count), date, time, error };
});
}
State Management Pattern
// Maintain state across execution
class DeploymentManager {
private state: Map<string, any> = new Map();
async deploy(environment: string) {
// Save initial state
this.state.set('start_time', Date.now());
this.state.set('environment', environment);
try {
// Pre-deployment checks
await this.runChecks();
this.state.set('checks_passed', true);
// Backup current version
const backup = await this.createBackup();
this.state.set('backup_id', backup.id);
// Deploy new version
await this.deployCode();
this.state.set('deployed', true);
// Verify deployment
await this.verify();
this.state.set('verified', true);
// Cleanup
await this.cleanup();
return {
success: true,
duration: Date.now() - this.state.get('start_time'),
backup: this.state.get('backup_id')
};
} catch (error) {
// Rollback on failure
if (this.state.get('deployed') && !this.state.get('verified')) {
await this.rollback(this.state.get('backup_id'));
}
throw error;
}
}
}
Stream Processing Pattern
// Process streaming data in real-time
async function monitorLogs(service: string) {
const proc = $.ssh('log-server')`
journalctl -u ${service} -f --output=json
`;
for await (const line of proc) {
try {
const entry = JSON.parse(line);
// Process based on severity
if (entry.PRIORITY <= 3) { // ERROR or worse
await notifyOncall({
service,
message: entry.MESSAGE,
timestamp: entry.__REALTIME_TIMESTAMP
});
}
// Store in metrics
await metrics.record({
service,
level: entry.PRIORITY,
timestamp: Date.now()
});
} catch (error) {
console.error('Failed to process log entry:', error);
}
}
}
Best Practices
1. Error Handling
Always handle errors appropriately:
// ❌ Bad - No error handling
await $`deploy.sh`;
// ✅ Good - Proper error handling
try {
await $`deploy.sh`;
} catch (error) {
console.error('Deployment failed:', error.message);
await $`rollback.sh`;
throw error;
}
// ✅ Better - Using nothrow pattern
const result = await $`deploy.sh`.nothrow();
if (result.exitCode !== 0) {
console.error('Deployment failed:', result.stderr);
await $`rollback.sh`;
}
2. Resource Management
Always clean up resources:
// ✅ Good - Proper cleanup
const conn = await pool.acquire();
try {
await conn.exec('command');
} finally {
await pool.release(conn);
}
3. Timeout Configuration
Set appropriate timeouts:
// ✅ Good - Timeout for long operations
await $`backup.sh`.timeout(600000); // 10 minutes
// ✅ Better - Configurable timeout
const timeout = config.get('backup.timeout', 600000);
await $`backup.sh`.timeout(timeout);
4. Logging and Monitoring
Add comprehensive logging:
// ✅ Good - Detailed logging
console.log(`Starting deployment to ${environment}`);
const startTime = Date.now();
try {
const result = await deploy(environment);
console.log(`✅ Deployment successful in ${Date.now() - startTime}ms`);
await metrics.recordSuccess('deployment', Date.now() - startTime);
} catch (error) {
console.error(`❌ Deployment failed: ${error.message}`);
await metrics.recordFailure('deployment', error);
throw error;
}
5. Configuration Management
Externalize configuration:
// ❌ Bad - Hardcoded values
await $.ssh('prod-server')`deploy`;
// ✅ Good - Configuration-driven
const target = config.get('deployment.target');
await $.ssh(target)`deploy`;
Anti-Patterns to Avoid
1. Ignoring Exit Codes
// ❌ Bad - Ignoring failures
await $`test.sh`;
await $`deploy.sh`; // Deploys even if tests fail
// ✅ Good - Check exit codes
await $`test.sh`;
if (result.exitCode === 0) {
await $`deploy.sh`;
}
2. No Timeout on Long Operations
// ❌ Bad - No timeout
await $`long-running-task`;
// ✅ Good - Set timeout
await $`long-running-task`.timeout(300000);
3. Mixing Concerns
// ❌ Bad - Mixed responsibilities
async function deployAndMonitor() {
// Deployment logic
// Monitoring logic
// Notification logic
}
// ✅ Good - Separated concerns
async function deploy() { /* ... */ }
async function monitor() { /* ... */ }
async function notify() { /* ... */ }
Performance Patterns
Connection Reuse
// Reuse SSH connections
const engine = new ExecutionEngine({
connectionPool: {
max: 10,
idleTimeoutMillis: 30000
}
});
// Connections are automatically reused
for (const command of commands) {
await engine.ssh('server').execute(command);
}
Batch Operations
// Batch multiple operations
async function batchUpdate(servers: string[], commands: string[]) {
const script = commands.join(' && ');
return Promise.all(
servers.map(server =>
$.ssh(server)`${script}`.nothrow()
)
);
}
Security Patterns
Secret Management
// ✅ Good - Use environment variables
const apiKey = process.env.API_KEY;
await $`curl -H "Authorization: Bearer ${apiKey}" https://api.example.com`;
// ✅ Better - Use secret management
const secrets = await loadSecrets();
await $`deploy`.env({ API_KEY: secrets.apiKey });
Input Validation
// Always validate external input
function validateTarget(target: string): boolean {
const pattern = /^[a-zA-Z0-9.-]+$/;
return pattern.test(target);
}
async function executeOn(target: string, command: string) {
if (!validateTarget(target)) {
throw new Error('Invalid target format');
}
await $.ssh(target)`${command}`;
}
Testing Patterns
Unit Testing Commands
import { describe, it, expect, vi } from 'vitest';
describe('deployment', () => {
it('should deploy successfully', async () => {
const mockExec = vi.fn().mockResolvedValue({
stdout: 'Success',
exitCode: 0
});
const result = await deploy('staging', { exec: mockExec });
expect(result.success).toBe(true);
expect(mockExec).toHaveBeenCalledWith('deploy.sh');
});
});
Integration Testing
import { TestContainer } from '@xec-sh/testing';
describe('integration', () => {
let container: TestContainer;
beforeAll(async () => {
container = new TestContainer();
await container.start();
});
afterAll(async () => {
await container.stop();
});
it('should execute in container', async () => {
const result = await $.docker({ container: container.id })`echo test`;
expect(result.stdout).toBe('test\n');
});
});
Related Documentation
- Error Handling - Detailed error handling patterns
- Parallel Execution - Concurrent execution strategies
- Stream Processing - Real-time data processing
- API Reference - Core API documentation
- Examples - Practical examples