Saltar al contenido

Automatizar flujos de desarrollo con herramientas CLI propias

Cómo identificar tareas repetitivas y crear herramientas CLI que las automaticen: análisis, patrones de diseño de scripts y su rendimiento compuesto.

6 min de lectura
Ventana de terminal mostrando scripts de automatización personalizados para desarrolladores en acción

La regla de las tres horas para la automatización

Si haces algo manualmente más de tres veces, automatízalo. Si el proceso manual toma más de cinco minutos y lo haces semanalmente, la automatización se pagará sola en un mes. Estos no son umbrales precisos: son heurísticas para vencer la inercia que mantiene a los desarrolladores haciendo tareas repetitivas a mano.

La ironía de la ingeniería de software es que construimos automatización para todos los demás mientras copiamos manualmente variables de entorno entre terminales, escribimos a mano comandos de seed de bases de datos y tecleamos el mismo conjuro de Git por decimoquinta vez esta semana.

Esta guía cubre cómo identificar oportunidades de automatización, diseñar herramientas CLI que perduren y construir un kit personal de automatización que se acumule con el tiempo.

Identificar candidatos para automatizar

No toda tarea repetitiva merece ser automatizada. El punto óptimo son las tareas que son frecuentes, propensas a errores y tienen pasos bien definidos.

tstypescript
interface AutomationCandidate {
  task: string;
  frequencyPerWeek: number;
  minutesPerExecution: number;
  errorRate: string;
  automationEffort: string;
  weeklyTimeSaved: number;
  paybackWeeks: number;
}
 
function evaluateCandidates(
  candidates: AutomationCandidate[]
): AutomationCandidate[] {
  return candidates
    .map((c) => {
      const automationHours =
        c.automationEffort === "low"
          ? 1
          : c.automationEffort === "medium"
            ? 4
            : 12;
      const weeklySavedHours =
        (c.frequencyPerWeek * c.minutesPerExecution) / 60;
      const payback = automationHours / weeklySavedHours;
 
      return {
        ...c,
        weeklyTimeSaved: weeklySavedHours * 60,
        paybackWeeks: Math.ceil(payback),
      };
    })
    .sort((a, b) => a.paybackWeeks - b.paybackWeeks);
}
 
const candidates: AutomationCandidate[] = [
  {
    task: "Set up new feature branch with ticket reference",
    frequencyPerWeek: 8,
    minutesPerExecution: 3,
    errorRate: "low",
    automationEffort: "low",
    weeklyTimeSaved: 0,
    paybackWeeks: 0,
  },
  {
    task: "Seed local database with test data",
    frequencyPerWeek: 5,
    minutesPerExecution: 8,
    errorRate: "medium",
    automationEffort: "medium",
    weeklyTimeSaved: 0,
    paybackWeeks: 0,
  },
  {
    task: "Generate API client from OpenAPI spec",
    frequencyPerWeek: 2,
    minutesPerExecution: 15,
    errorRate: "high",
    automationEffort: "medium",
    weeklyTimeSaved: 0,
    paybackWeeks: 0,
  },
];

El cálculo de amortización es deliberadamente simple. Analizar en exceso el ROI de una automatización es en sí mismo una forma de procrastinación. Si la amortización es menor a cuatro semanas y la tarea te molesta, construye la herramienta.

Construir tu primer script de automatización

Empieza con scripts de shell envueltos en una interfaz consistente. No necesitas ningún framework: solo un archivo en el directorio scripts/ de tu proyecto con un nombre claro y salida de ayuda.

tstypescript
#!/usr/bin/env node
// scripts/new-feature.ts
import { execSync } from "child_process";
 
const args = process.argv.slice(2);
 
function printHelp(): void {
  console.log(`
Usage: ./scripts/new-feature.ts <ticket-id> [description]
 
Creates a new feature branch from latest main with:
- Branch name: feature/<ticket-id>-<description>
- Commits an empty .feature file with ticket metadata
 
Examples:
  ./scripts/new-feature.ts PROJ-123 add-payment-flow
  ./scripts/new-feature.ts PROJ-456 refactor-auth
`);
}
 
if (args.length < 1 || args[0] === "--help") {
  printHelp();
  process.exit(args[0] === "--help" ? 0 : 1);
}
 
const ticketId = args[0];
const description = args[1] || "feature";
const branchName = `feature/${ticketId}-${description}`.toLowerCase();
 
function run(cmd: string): string {
  return execSync(cmd, { encoding: "utf-8" }).trim();
}
 
try {
  // Ensure clean working directory
  const status = run("git status --porcelain");
  if (status) {
    console.error("Error: Working directory is not clean. Commit or stash changes first.");
    process.exit(1);
  }
 
  // Update main and create branch
  console.log("Updating main branch...");
  run("git checkout main");
  run("git pull origin main");
 
  console.log(`Creating branch: ${branchName}`);
  run(`git checkout -b ${branchName}`);
 
  console.log(`\nBranch '${branchName}' created and checked out.`);
  console.log(`Ticket: ${ticketId}`);
} catch (error) {
  console.error("Failed:", (error as Error).message);
  process.exit(1);
}

El script valida las entradas, verifica las precondiciones y proporciona mensajes de error claros. Estas tres cualidades determinan si un script se usa una vez y se olvida o se convierte en parte del flujo de trabajo diario del equipo.

Arquitectura de scripts componibles

A medida que tu kit de automatización crece, los scripts individuales deberían componerse en flujos de trabajo más grandes. Una biblioteca compartida de funciones de utilidad evita la duplicación.

tstypescript
// scripts/lib/git.ts
import { execSync } from "child_process";
 
export function getCurrentBranch(): string {
  return execSync("git branch --show-current", {
    encoding: "utf-8",
  }).trim();
}
 
export function isClean(): boolean {
  const status = execSync("git status --porcelain", {
    encoding: "utf-8",
  }).trim();
  return status === "";
}
 
export function getLastTag(): string | null {
  try {
    return execSync("git describe --tags --abbrev=0", {
      encoding: "utf-8",
    }).trim();
  } catch {
    return null;
  }
}
 
export function getCommitsSince(ref: string): string[] {
  return execSync(`git log ${ref}..HEAD --oneline`, {
    encoding: "utf-8",
  })
    .trim()
    .split("\n")
    .filter(Boolean);
}
tstypescript
// scripts/lib/env.ts
import { readFileSync, existsSync } from "fs";
import path from "path";
 
export function loadEnvFile(envPath: string): Record<string, string> {
  if (!existsSync(envPath)) {
    throw new Error(`Environment file not found: ${envPath}`);
  }
 
  const content = readFileSync(envPath, "utf-8");
  const vars: Record<string, string> = {};
 
  for (const line of content.split("\n")) {
    const trimmed = line.trim();
    if (!trimmed || trimmed.startsWith("#")) continue;
 
    const eqIndex = trimmed.indexOf("=");
    if (eqIndex === -1) continue;
 
    const key = trimmed.slice(0, eqIndex).trim();
    let value = trimmed.slice(eqIndex + 1).trim();
 
    // Remove surrounding quotes
    if (
      (value.startsWith('"') && value.endsWith('"')) ||
      (value.startsWith("'") && value.endsWith("'"))
    ) {
      value = value.slice(1, -1);
    }
 
    vars[key] = value;
  }
 
  return vars;
}
 
export function requireEnvVars(vars: string[]): void {
  const missing = vars.filter((v) => !process.env[v]);
  if (missing.length > 0) {
    throw new Error(
      `Missing required environment variables: ${missing.join(", ")}`
    );
  }
}
tstypescript
// scripts/lib/prompt.ts
import readline from "readline";
 
export async function confirm(message: string): Promise<boolean> {
  const rl = readline.createInterface({
    input: process.stdin,
    output: process.stdout,
  });
 
  return new Promise((resolve) => {
    rl.question(`${message} (y/N): `, (answer) => {
      rl.close();
      resolve(answer.toLowerCase() === "y");
    });
  });
}
 
export async function select(
  message: string,
  options: string[]
): Promise<string> {
  const rl = readline.createInterface({
    input: process.stdin,
    output: process.stdout,
  });
 
  console.log(message);
  options.forEach((opt, i) => console.log(`  ${i + 1}. ${opt}`));
 
  return new Promise((resolve) => {
    rl.question("Choice: ", (answer) => {
      rl.close();
      const idx = parseInt(answer, 10) - 1;
      resolve(options[idx] || options[0]);
    });
  });
}

Estas utilidades —operaciones de Git, gestión de entorno, prompts interactivos— forman los bloques de construcción de cualquier script de automatización. Importa lo que necesites; ignora lo que no.

Automatización del seed de bases de datos

El seed de bases de datos es uno de los objetivos de automatización de mayor valor. Se hace con frecuencia, varía según el contexto y los errores causan problemas de desarrollo en cascada.

tstypescript
#!/usr/bin/env node
// scripts/seed.ts
import { confirm, select } from "./lib/prompt";
 
interface SeedProfile {
  name: string;
  description: string;
  users: number;
  projects: number;
  includeEdgeCases: boolean;
}
 
const profiles: SeedProfile[] = [
  {
    name: "minimal",
    description: "1 admin, 2 users, 1 project — fast startup",
    users: 3,
    projects: 1,
    includeEdgeCases: false,
  },
  {
    name: "development",
    description: "10 users, 5 projects, realistic data distribution",
    users: 10,
    projects: 5,
    includeEdgeCases: false,
  },
  {
    name: "stress-test",
    description: "1000 users, 50 projects, includes edge cases",
    users: 1000,
    projects: 50,
    includeEdgeCases: true,
  },
];
 
async function main(): Promise<void> {
  const profileName = await select(
    "Select seed profile:",
    profiles.map((p) => `${p.name} — ${p.description}`)
  );
 
  const profile = profiles.find((p) =>
    profileName.startsWith(p.name)
  );
 
  if (!profile) {
    console.error("Invalid profile selected");
    process.exit(1);
  }
 
  const shouldReset = await confirm(
    "Reset database before seeding?"
  );
 
  if (shouldReset) {
    console.log("Resetting database...");
    // Reset logic here
  }
 
  console.log(`Seeding with profile: ${profile.name}`);
  console.log(`  Users: ${profile.users}`);
  console.log(`  Projects: ${profile.projects}`);
 
  // Seed execution here
  console.log("Seeding complete.");
}
 
main().catch(console.error);

El índice de automatización: seguimiento de tu kit de herramientas

Mantén un README en tu directorio de scripts que documente qué hace cada herramienta, cuándo usarla y quién la mantiene:

tstypescript
// scripts/README.ts — Generate automation index from script metadata
import { readdirSync, readFileSync } from "fs";
import path from "path";
 
interface ScriptMeta {
  name: string;
  description: string;
  usage: string;
  author: string;
  lastUpdated: string;
}
 
function extractMeta(filepath: string): ScriptMeta | null {
  const content = readFileSync(filepath, "utf-8");
  const lines = content.split("\n").slice(0, 20);
 
  const descLine = lines.find((l) => l.includes("@description"));
  const usageLine = lines.find((l) => l.includes("@usage"));
  const authorLine = lines.find((l) => l.includes("@author"));
 
  if (!descLine) return null;
 
  return {
    name: path.basename(filepath),
    description: descLine.replace(/.*@description\s*/, "").trim(),
    usage: usageLine?.replace(/.*@usage\s*/, "").trim() || "See --help",
    author: authorLine?.replace(/.*@author\s*/, "").trim() || "team",
    lastUpdated: "",
  };
}
 
function generateIndex(scriptsDir: string): string {
  const files = readdirSync(scriptsDir).filter(
    (f) => f.endsWith(".ts") || f.endsWith(".mjs")
  );
 
  const metas = files
    .map((f) => extractMeta(path.join(scriptsDir, f)))
    .filter(Boolean) as ScriptMeta[];
 
  let index = "# Developer Scripts\n\n";
  index += "| Script | Description | Usage |\n";
  index += "|--------|-------------|-------|\n";
 
  for (const meta of metas) {
    index += `| \`${meta.name}\` | ${meta.description} | \`${meta.usage}\` |\n`;
  }
 
  return index;
}

Un kit de herramientas que se puede descubrir se usa. Uno que no se puede descubrir lo reconstruye desde cero el siguiente desarrollador que se tope con el mismo problema.

Integrar los scripts con package.json

jsonjson
{
  "scripts": {
    "new-feature": "tsx scripts/new-feature.ts",
    "seed": "tsx scripts/seed.ts",
    "seed:minimal": "tsx scripts/seed.ts --profile minimal --no-prompt",
    "db:reset": "tsx scripts/db-reset.ts",
    "release": "tsx scripts/release.ts",
    "check:deps": "tsx scripts/check-deps.ts",
    "scripts:index": "tsx scripts/generate-index.ts"
  }
}

Exponer los scripts a través de package.json los hace detectables mediante el autocompletado con tab de npm run y los documenta junto a los comandos estándar del proyecto. Es más probable que se encuentre y se use npm run seed que tsx scripts/seed.ts.

Conclusiones clave

La automatización es la actividad de mayor apalancamiento en la ingeniería de software. Cada minuto invertido en construir un script confiable se multiplica con cada ejecución futura. Los rendimientos compuestos son enormes: un script que ahorra cinco minutos al día ahorra más de 20 horas al año, y eso es solo para una persona del equipo.

Empieza en pequeño: elige la tarea que más te molesta, automatízala de la forma más simple posible y ponla donde el equipo pueda encontrarla. Construye una biblioteca compartida de utilidades para operaciones de Git, gestión de entorno y prompts interactivos. Documenta tus scripts en un índice central.

La mejor herramienta de desarrollo no es la más sofisticada: es la más confiable. Un script que funciona siempre, maneja los errores con claridad y tarda diez segundos en ejecutarse se usará a diario. Un framework de CLI precioso que toma tres días construir, quizá no.

Wilfredo Rujel

Wilfredo Rujel

Ingeniero de Software Full Stack

Compartir esta publicaciónX