Ctrl K
ComunidadGuías…

scripts/reset-test-env.mjs

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

Archivo original ↗Preguntar a KiBot
#!/usr/bin/env node
/**
 * Exact-resource cleanup for disposable Chickpea live acceptance runs.
 *
 * Dry-run is mandatory first. This script never discovers cleanup targets by
 * name or prefix: every target comes from an append-only run ledger. Slack UI
 * resources and Cloudflare/GitHub resources without a supported exact CLI
 * operation remain explicit manual actions and are not marked verified here.
 */
import { spawnSync } from 'node:child_process';
import { resolve } from 'node:path';
import { fileURLToPath } from 'node:url';

import { cleanupPlan, recordCleanupVerified } from './live-test-resource-ledger.mjs';
import { authorizeEnvironmentCleanupPlan } from './lib/environment-preflight.mjs';

const REPO_ROOT = resolve(fileURLToPath(new URL('..', import.meta.url)));

export function renderCleanupPlan(targets) {
  if (!Array.isArray(targets) || targets.length === 0) return 'No pending owned resources.';
  return targets.map((target, index) => {
    const operation = target.action === 'archive' ? 'ARCHIVE' : 'DELETE';
    return `${index + 1}. ${operation} ${target.provider}/${target.kind} id=${target.id}`;
  }).join('\n');
}

export function automaticCleanupCommand(_target, _options = {}) {
  // Wrangler Worker and D1 deletion, GitHub repository deletion, and the
  // currently supported container commands all address a mutable name. None
  // exposes a provider-side immutable-version/etag precondition, so a local
  // readback can never close the final read/delete race. Keep these resources
  // explicit manual actions until the provider offers a genuine conditional
  // deletion primitive.
  return null;
}

export function resolveD1NameByExactId(target, rawList) {
  const parsed = typeof rawList === 'string' ? JSON.parse(rawList) : rawList;
  if (!Array.isArray(parsed)) throw new Error('Wrangler D1 inventory was not an array.');
  const matches = parsed.filter((row) => row && row.uuid === target.id && typeof row.name === 'string');
  if (matches.length !== 1) {
    throw new Error(`D1 exact ID ${target.id} resolved to ${matches.length} resources; refusing cleanup.`);
  }
  return matches[0].name;
}

export function isManualCleanupTarget(target) {
  return automaticCleanupCommand(target) === null;
}

export function readLiveCleanupResource(resource, {
  runCommand = run,
  providerContext = [],
  now = () => Date.now(),
} = {}) {
  const context = validateProviderContext(providerContext);
  if (!resource || typeof resource !== 'object'
    || typeof resource.target !== 'string'
    || typeof resource.provider !== 'string'
    || typeof resource.kind !== 'string'
    || typeof resource.id !== 'string'
    || typeof resource.immutableId !== 'string'
    || typeof resource.creationIntentDigest !== 'string') {
    throw new Error('Invalid provider cleanup readback request.');
  }
  let immutableId;
  let creationIntentDigest;
  if (resource.provider === 'cloudflare'
    && ['worker', 'capture_worker'].includes(resource.kind)) {
    const deployments = checkedJson(runCommand({
      command: 'node_modules/.bin/wrangler',
      args: ['deployments', 'status', '--json', '--name', resource.id, ...context],
    }), 'Unable to read Worker deployment identity before cleanup.');
    const active = Array.isArray(deployments?.versions)
      ? deployments.versions.filter((version) => Number(version?.percentage) > 0)
      : [];
    if (active.length !== 1 || active[0]?.percentage !== 100
      || typeof active[0]?.version_id !== 'string') {
      throw new Error('Worker does not have one exact 100% serving version; refusing cleanup.');
    }
    immutableId = active[0].version_id;
    const view = checkedJson(runCommand({
      command: 'node_modules/.bin/wrangler',
      args: ['versions', 'view', immutableId, '--json', '--name', resource.id, ...context],
    }), 'Unable to read Worker creation authority before cleanup.');
    const bindings = Array.isArray(view?.resources?.bindings) ? view.resources.bindings : [];
    const stamps = bindings.filter((binding) => binding?.name === 'CHICKPEA_RESOURCE_CREATION_INTENT_DIGEST')
      .map((binding) => binding?.text ?? binding?.value)
      .filter((value) => typeof value === 'string');
    if (stamps.length !== 1) throw new Error('Worker creation authority is missing or ambiguous.');
    [creationIntentDigest] = stamps;
  } else if (resource.provider === 'cloudflare'
    && ['d1', 'capture_store'].includes(resource.kind)) {
    immutableId = resource.id;
    const statement = checkedJson(runCommand({
      command: 'node_modules/.bin/wrangler',
      args: [
        'd1', 'execute', resource.id, '--remote', '--json', '--command',
        "SELECT value FROM chickpea_resource_authority WHERE key = 'creation_intent_digest'",
        ...context,
      ],
    }), 'Unable to read D1 creation authority before cleanup.');
    const results = Array.isArray(statement) ? statement : [];
    const rows = results.flatMap((entry) => Array.isArray(entry?.results) ? entry.results : []);
    if (rows.length !== 1 || typeof rows[0]?.value !== 'string') {
      throw new Error('D1 creation authority is missing or ambiguous.');
    }
    creationIntentDigest = rows[0].value;
  } else if (resource.provider === 'github' && resource.kind === 'generated_repository') {
    if (!/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/u.test(resource.id)) {
      throw new Error('Invalid exact GitHub repository coordinate.');
    }
    const repository = checkedJson(runCommand({
      command: 'gh', args: ['api', `repos/${resource.id}`],
    }), 'Unable to read GitHub repository identity before cleanup.');
    const stamp = checkedJson(runCommand({
      command: 'gh',
      args: ['api', `repos/${resource.id}/actions/variables/CHICKPEA_RESOURCE_CREATION_INTENT_DIGEST`],
    }), 'Unable to read GitHub repository creation authority before cleanup.');
    immutableId = repository?.node_id;
    creationIntentDigest = stamp?.value;
  } else {
    throw new Error(`No exact immutable provider readback for ${resource.provider}/${resource.kind}.`);
  }
  if (immutableId !== resource.immutableId
    || creationIntentDigest !== resource.creationIntentDigest) {
    throw new Error('Provider resource identity changed; refusing cleanup.');
  }
  return Object.freeze({
    target: resource.target,
    provider: resource.provider,
    kind: resource.kind,
    id: resource.id,
    immutableId,
    creationIntentDigest,
    observedAt: new Date(now()).toISOString(),
  });
}

export function executeCleanup({
  ledgerPath,
  apply = false,
  target,
  receiptPaths = [],
  environmentOptions = {},
  authorizeCleanup = authorizeEnvironmentCleanupPlan,
  runCommand = run,
  stdout = process.stdout,
}) {
  const targets = cleanupPlan(ledgerPath);
  stdout.write(`\nChickpea disposable cleanup${apply ? '' : ' (dry run)'}\n`);
  stdout.write(`${renderCleanupPlan(targets)}\n`);
  if (!apply || targets.length === 0) return { targets, completed: [], manual: targets.filter(isManualCleanupTarget) };

  const providerContext = validateProviderContext(environmentOptions.providerContext ?? []);

  // Validate the entire exact-ID plan before inventorying or deleting even
  // one resource. A single missing, forged, mixed-target, or protected receipt
  // leaves every provider untouched.
  const readProviderResource = environmentOptions.readProviderResource
    ?? ((resource) => readLiveCleanupResource(resource, { runCommand, providerContext }));
  const authorizedTargets = authorizeCleanup(targets, {
    ...environmentOptions,
    target,
    receiptPaths,
    readProviderResource,
  });
  if (!Array.isArray(authorizedTargets) || authorizedTargets.length !== targets.length
    || authorizedTargets.some((authorized, index) =>
      authorized?.provider !== targets[index]?.provider
      || authorized?.kind !== targets[index]?.kind
      || authorized?.id !== targets[index]?.id
      || typeof authorized?.immutableId !== 'string'
      || typeof authorized?.creationIntentDigest !== 'string'
    )) {
    throw new Error('Cleanup authorization returned an invalid immutable plan.');
  }
  if (environmentOptions.beforeCleanupCommandBoundary) {
    environmentOptions.beforeCleanupCommandBoundary();
  }
  // Re-read the entire immutable plan immediately at the provider command
  // boundary. A renamed/recreated resource prevents every delete, including
  // earlier otherwise-valid items in the same plan.
  for (const authorized of authorizedTargets) {
    const boundary = readProviderResource(authorized);
    if (!boundary || boundary.target !== target
      || boundary.provider !== authorized.provider
      || boundary.kind !== authorized.kind
      || boundary.id !== authorized.id
      || boundary.immutableId !== authorized.immutableId
      || boundary.creationIntentDigest !== authorized.creationIntentDigest) {
      throw new Error('Provider resource identity changed at cleanup command boundary; refusing deletion.');
    }
  }
  if (environmentOptions.afterCleanupBoundaryRead) {
    environmentOptions.afterCleanupBoundaryRead();
  }

  return { targets, completed: [], manual: authorizedTargets };
}

function checkedJson(result, message) {
  if (!result || result.status !== 0 || typeof result.stdout !== 'string') throw new Error(message);
  try { return JSON.parse(result.stdout); } catch { throw new Error(message); }
}

function validateProviderContext(input) {
  if (!Array.isArray(input) || input.length % 2 !== 0 || input.length > 4) {
    throw new Error('Invalid Cloudflare provider context.');
  }
  const seen = new Set();
  for (let index = 0; index < input.length; index += 2) {
    const flag = input[index];
    const value = input[index + 1];
    if (!['--profile', '--env'].includes(flag) || seen.has(flag)
      || typeof value !== 'string' || !/^[A-Za-z0-9._-]{1,128}$/u.test(value)) {
      throw new Error('Invalid Cloudflare provider context.');
    }
    seen.add(flag);
  }
  return Object.freeze([...input]);
}

function run({ command, args, input }) {
  return spawnSync(command, args, {
    cwd: REPO_ROOT,
    ...(input ? { input } : {}),
    encoding: 'utf8',
    stdio: ['pipe', 'pipe', 'pipe'],
  });
}

function argument(args, flag) {
  const index = args.indexOf(flag);
  return index >= 0 ? args[index + 1] : undefined;
}

function argumentsFor(args, flag) {
  return args.flatMap((value, index) => value === flag && args[index + 1] ? [args[index + 1]] : []);
}

async function cli() {
  const args = process.argv.slice(2);
  const ledgerPath = argument(args, '--ledger');
  if (!ledgerPath) throw new Error('Pass --ledger <path>. Cleanup targets never come from command-line names.');
  const apply = args.includes('--apply');
  const target = argument(args, '--target');
  const receiptPaths = argumentsFor(args, '--receipt');
  if (apply && !target) throw new Error('Pass --target <amber|cobalt|violet> for receipt-backed cleanup.');
  const root = argument(args, '--root');
  const worktreePath = argument(args, '--worktree');
  const profile = argument(args, '--profile');
  const environment = argument(args, '--env');
  const result = executeCleanup({
    ledgerPath, apply, target, receiptPaths,
    environmentOptions: {
      ...(root ? { root } : {}),
      ...(worktreePath ? { worktreePath } : {}),
      ...((profile || environment) ? {
        providerContext: [
          ...(profile ? ['--profile', profile] : []),
          ...(environment ? ['--env', environment] : []),
        ],
      } : {}),
    },
  });
  if (!apply) {
    console.log('\nDry run only. Review every exact ID, then re-run with --apply.');
  }
  if (result.manual.length > 0) {
    console.log('\nManual exact-ID cleanup still required:');
    console.log(renderCleanupPlan(result.manual));
  }
  if (result.completed.length > 0) {
    console.log('\nCommands completed but are NOT yet ledger-verified. Re-inventory each exact ID, then record cleanup_verified.');
  }
}

if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
  cli().catch((error) => { console.error(error instanceof Error ? error.message : String(error)); process.exitCode = 1; });
}

export { recordCleanupVerified };