Ctrl K
ComunidadGuías…

scripts/release-impact.mjs

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
#!/usr/bin/env node
// Classifies changed repository paths into release areas so CI and release QA
// run only the checks a change can affect. No dependencies. The rule table below
// is the single source of truth; the first matching rule wins. A path no rule
// matches maps to `root`, which runs everything.
import { execFileSync } from "node:child_process";
import { appendFileSync } from "node:fs";
import { pathToFileURL } from "node:url";

export const AREAS = ["root", "web", "docs", "installers", "wizard-ui", "sidecar", "siwc-auth",
  "vps", "local-docker", "local-model", "supergrok", "assistant", "hosting", "packaging", "ci"];

export const RULES = [
  // Repository tooling, release process and tests.
  [/^AGENTS\.md$/u, ["ci"]],
  [/^\.agents\/skills\/(ship|release-relmio-everywhere|release-qa|changelog)\//u, ["ci", "root"]],
  [/^\.agents\/skills\/(installer-reliability|maintain-relmio-installers)\//u, ["ci", "installers"]],
  [/^\.agents\//u, ["ci"]],
  [/^(\.github|scripts)\//u, ["ci", "root"]],
  [/^(package\.json|package-lock\.json|\.npmrc|\.gitattributes)$/u, ["ci", "root"]],
  [/^(\.gitignore|\.coderabbit\.yaml)$/u, ["ci"]],
  [/^test\/integration\/n8n-nodes\//u, ["ci", "sidecar"]],
  [/^(test|dev|qa)\//u, ["ci"]],

  // Documentation. LICENSE and NOTICE ship in the npm package.
  [/^(LICENSE|NOTICE)$/u, ["docs", "packaging"]],
  [/^[^/]+\.md$/u, ["docs"]],
  [/^npm\/README\.md$/u, ["docs"]],
  [/^(docs|design-system|tasks|\.impeccable|\.hallmark)\//u, ["docs"]],

  // Hosted website and the installers it serves.
  [/^web\/public\/install\./u, ["installers"]],
  [/^web\//u, ["web"]],
  [/^Start Wizard\.(bat|command)$/u, ["installers"]],
  [/^packaging\//u, ["packaging"]],

  // Local browser wizard. The UI kit, fonts and icons are copied into web/.
  [/^src\/ui\/(relmio-ui\.css|fonts\/|relmio-icon)/u, ["wizard-ui", "web"]],
  [/^src\/ui\/hosting/u, ["wizard-ui", "hosting"]],
  [/^src\/ui\/local-model-vps/u, ["wizard-ui", "local-model", "vps"]],
  [/^src\/ui\/supergrok-vps/u, ["wizard-ui", "supergrok", "vps"]],
  [/^src\/ui\/assistant/u, ["wizard-ui", "assistant"]],
  [/^src\/ui\/(siwc|oauth-popup)/u, ["wizard-ui", "siwc-auth"]],
  [/^src\/ui\//u, ["wizard-ui"]],
  [/^src\/services\/(ui-preferences|project-meta)\.js$/u, ["wizard-ui"]],

  // Entry points and shared validation reach every feature.
  [/^src\/(cli\.js|web\/server\.js)$/u, ["root"]],
  [/^src\/domain\/(safety|validation|provider-lifecycle)\.js$/u, ["root"]],
  [/^src\/legacy-vps-cli\.js$/u, ["vps"]],
  [/^src\/(browser\.js|services\/browser-handoff\.js)$/u, ["installers"]],
  [/^src\/infrastructure\/(local-process|process-identity)\.js$/u, ["local-docker", "installers"]],

  // Experimental Claude adapter and its wizard services. Reached from the main CLI and server, so they run every check.
  [/^src\/(claude\/|gateway\/claude-chat\.js$|templates\/claude-companion\/|domain\/local-n8n-claude\.js$|services\/(local-n8n-claude-installer|claude-login|vps-claude)\.js$)/u, ["root"]],

  // ChatGPT sidecar, its image files and SIWC sign-in.
  [/^src\/gateway\//u, ["sidecar"]],
  [/^src\/services\/(model-discovery|codex-images)\.mjs$/u, ["sidecar"]],
  [/^src\/(domain\/templates|domain\/vps-build-state|services\/installer)\.js$/u, ["vps", "sidecar"]],
  [/^src\/(domain\/local-n8n-sidecar|services\/local-n8n-sidecar-installer)\.js$/u, ["sidecar", "local-docker"]],
  [/^src\/domain\/codex-images\.js$/u, ["vps", "sidecar", "local-docker"]],
  [/^src\/services\/(siwc-|oauth\.js|codex-login\.js|local-chat-test\.js)/u, ["siwc-auth"]],
  [/^src\/domain\/local-endpoints\.js$/u, ["siwc-auth"]],
  [/^src\/services\/local-installer\.js$/u, ["local-docker", "siwc-auth", "supergrok"]],

  // Local model.
  [/^src\/(domain\/local-n8n-model|domain\/local-model-network|services\/local-n8n-model-installer)\.js$/u, ["local-model", "local-docker"]],
  [/^src\/services\/vps-local-model\.js$/u, ["local-model", "vps"]],
  [/^src\/local-model\//u, ["local-model"]],

  // SuperGrok.
  [/^src\/(domain\/local-n8n-supergrok|services\/local-n8n-supergrok-installer)\.js$/u, ["supergrok", "local-docker"]],
  [/^src\/services\/vps-supergrok\.js$/u, ["supergrok", "vps"]],
  [/^src\/(supergrok\/|services\/grok-login\.js)/u, ["supergrok"]],

  // n8n AI Assistant.
  [/^src\/(domain\/local-n8n-assistant|services\/local-n8n-assistant-installer)\.js$/u, ["assistant", "local-docker"]],
  [/^src\/(domain\/assistant|domain\/assistant-templates|services\/assistant-installer)\.js$/u, ["assistant", "vps"]],

  // Local Docker stack and dashboard.
  [/^src\/(domain\/local-n8n-stack|services\/local-n8n-stack-installer|services\/local-n8n-stack-credentials)\.js$/u, ["local-docker"]],
  [/^src\/templates\/local-n8n-stack\//u, ["local-docker"]],
  [/^src\/services\/(local-dashboard|local-dashboard-control|local-integration-lifecycle-lock)\.js$/u, ["local-docker"]],

  // Hosting profiles and VPS access.
  [/^src\/domain\/hosting-/u, ["hosting"]],
  [/^src\/(infrastructure\/ssh|services\/vps-operation-lock|services\/discovery|domain\/vps-host-report|domain\/vps-network)/u, ["vps"]],
];

export function areasForPath(path) {
  return RULES.find(([pattern]) => pattern.test(path))?.[1];
}

export function classifyPaths(paths) {
  const areas = new Set();
  const unknown = [];
  for (const path of paths) {
    const matched = areasForPath(path);
    if (matched) {
      matched.forEach((area) => areas.add(area));
    } else {
      areas.add("root");
      unknown.push(path);
    }
  }
  // An empty change list usually means a wrong base; run everything.
  if (paths.length === 0) areas.add("root");
  const sorted = [...areas].sort();
  return {
    areas: sorted,
    docsOnly: sorted.every((area) => area === "docs"),
    webOnly: sorted.every((area) => area === "web" || area === "docs"),
    unknown,
  };
}

export function runFlags({ areas, docsOnly, webOnly }) {
  const has = (...names) => names.some((name) => areas.includes(name));
  const runRoot = !(docsOnly || webOnly);
  return {
    run_root: runRoot,
    run_web: has("root", "web", "docs", "installers"),
    run_windows: runRoot,
    run_macos: runRoot,
    run_linux: runRoot,
    run_n8n_nodes: has("root", "sidecar", "siwc-auth", "assistant"),
  };
}

export function formatGithubOutput(result) {
  const lines = [
    `areas=${result.areas.join(",")}`,
    `docs_only=${result.docsOnly}`,
    `web_only=${result.webOnly}`,
    ...Object.entries(runFlags(result)).map(([name, value]) => `${name}=${value}`),
  ];
  return `${lines.join("\n")}\n`;
}

export function formatSummary(result, { base, head }) {
  const rows = Object.entries(runFlags(result)).map(([name, value]) => `| \`${name}\` | ${value} |`);
  const unknown = result.unknown.length
    ? `\nUnmapped paths (classified as \`root\`, so everything runs):\n\n${result.unknown.map((path) => `- \`${path}\``).join("\n")}\n`
    : "";
  return [
    "## Release impact",
    "",
    `Compared \`${base}...${head}\`. Areas: ${result.areas.map((area) => `\`${area}\``).join(", ")}.`,
    "",
    "| Flag | Value |",
    "| --- | --- |",
    `| \`docs_only\` | ${result.docsOnly} |`,
    `| \`web_only\` | ${result.webOnly} |`,
    ...rows,
    unknown,
  ].join("\n");
}

export function changedPaths(base, head, { cwd = process.cwd() } = {}) {
  const output = execFileSync("git", ["diff", "--name-only", "--no-renames", "-z", `${base}...${head}`], {
    cwd,
    encoding: "utf8",
    maxBuffer: 64 * 1024 * 1024,
  });
  return output.split("\0").filter(Boolean);
}

function parseArgs(argv) {
  const options = { head: "HEAD", format: "json" };
  for (let index = 0; index < argv.length; index += 2) {
    const [flag, value] = [argv[index], argv[index + 1]];
    if (!["--base", "--head", "--format"].includes(flag) || value === undefined) {
      throw new Error(`Unknown or incomplete option: ${flag}`);
    }
    options[flag.slice(2)] = value;
  }
  if (!options.base) throw new Error("--base <ref> is required");
  if (!["json", "github"].includes(options.format)) throw new Error("--format must be json or github");
  return options;
}

export function main(argv = process.argv.slice(2), env = process.env) {
  const { base, head, format } = parseArgs(argv);
  const paths = changedPaths(base, head);
  const result = classifyPaths(paths);
  if (format === "json") {
    process.stdout.write(`${JSON.stringify({ ...result, ...runFlags(result), paths }, null, 2)}\n`);
    return;
  }
  const output = formatGithubOutput(result);
  if (env.GITHUB_OUTPUT) appendFileSync(env.GITHUB_OUTPUT, output);
  else process.stdout.write(output);
  if (env.GITHUB_STEP_SUMMARY) appendFileSync(env.GITHUB_STEP_SUMMARY, `${formatSummary(result, { base, head })}\n`);
}

if (import.meta.url === pathToFileURL(process.argv[1] ?? "").href) {
  try {
    main();
  } catch (error) {
    process.stderr.write(`release-impact: ${error.message}\n`);
    process.exitCode = 1;
  }
}