Skip to main content

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

Target Patterns

Data Patterns

Workflow Patterns

Integration 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');
});
});