Ctrl K
ComunidadGuías…

commands/gha.ts

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

Archivo original ↗Preguntar a KiBot
import { spawn } from "node:child_process";
import { dirname } from "node:path";
import * as core from "@actions/core";
import arg from "arg";
import { main } from "../main.ts";
import { runOAuthWriteback } from "../utils/oauthWriteback.ts";
import { maskSecret, saveSecretState, setSecretOutput } from "../utils/secretCommands.ts";
import { removeAbandonedTempDirectory } from "../utils/setup.ts";
import { acquireInstallationToken, revokeInstallationToken } from "../utils/token.ts";

// GitHub Actions runs the action entry point with the node24 binary specified
// in action.yml, but doesn't add that binary's directory to PATH. Without this,
// spawned processes (pnpm, npm, etc.) resolve to the runner's default node (v20).
process.env.PATH = `${dirname(process.execPath)}:${process.env.PATH}`;

const STATE_TOKEN = "token";

/** `runs.using: node24`; the runtime reaches node 22+ APIs (`Promise.withResolvers`, `Promise.try`) */
const MIN_NODE_MAJOR = 24;

/**
 * a Volta/asdf/mise `npx` shim answers `--version`, so the bootstrap's PATH walk accepts it,
 * then runs the CLI on its own default node. on node 20 the run worked until teardown threw
 * on `Promise.withResolvers` with every installation token still live (#1565). so re-run
 * under the bootstrap's own node; an older bootstrap that does not pass it fails up front.
 */
async function rerunOnBootstrapNode(args: string[]): Promise<void> {
  const node = process.env.PULLFROG_NODE;
  const found = `the runner's npx launched Pullfrog on Node ${process.versions.node} (${process.execPath}), which is below Node ${MIN_NODE_MAJOR}`;
  // equal to ours means the bootstrap itself ran on it, and re-running would loop
  if (!node || node === process.execPath) {
    // the post step must never fail a finished run (#815)
    if (args.includes("--post")) core.warning(found);
    else
      core.setFailed(
        `${found}. bump the Pullfrog action ref, or make the runner's npx use Node ${MIN_NODE_MAJOR}.`
      );
    return;
  }
  core.info(`» ${found}; re-running under ${node}`);
  const child = spawn(node, process.argv.slice(1), { stdio: "inherit" });
  for (const signal of ["SIGINT", "SIGTERM"] as const) {
    process.on(signal, () => child.kill(signal));
  }
  process.exitCode = await new Promise<number>((resolve) =>
    child.on("exit", (code) => resolve(code ?? 1))
  );
}

interface GhaCliParams {
  args: string[];
  prog: string;
  showHelp?: boolean;
}

async function runMain(): Promise<void> {
  try {
    const result = await main();
    if (!result.success) {
      throw new Error(result.error || "agent execution failed");
    }
  } catch (error) {
    const errorMessage = error instanceof Error ? error.message : "unknown error occurred";
    core.setFailed(`action failed: ${errorMessage}`);
  }
}

async function tokenMain(): Promise<void> {
  const reposInput = core.getInput("repos");
  const additionalRepos = reposInput
    ? reposInput
        .split(",")
        .map((r) => r.trim())
        .filter(Boolean)
    : [];

  const token = await acquireInstallationToken({ repos: additionalRepos });

  maskSecret(token);
  saveSecretState(STATE_TOKEN, token);
  setSecretOutput("token", token);

  const scope = additionalRepos.length
    ? `current repo + ${additionalRepos.join(", ")}`
    : "current repo only";
  core.info(`» installation token acquired (${scope})`);
}

async function tokenPost(): Promise<void> {
  const token = core.getState(STATE_TOKEN);
  if (!token) {
    core.debug("no token found in state, skipping revocation");
    return;
  }
  await revokeInstallationToken(token);
  core.info("» installation token revoked");
}

function printGhaUsage(params: { stream: typeof console.log; prog: string }): void {
  params.stream(`usage: ${params.prog} gha [subcommand]\n`);
  params.stream("run the github action runtime flow.");
  params.stream("");
  params.stream("subcommands:");
  params.stream("  token        acquire a github app installation token");
  params.stream("");
  params.stream("options:");
  params.stream("  -h, --help   show help");
  params.stream("  --post       run the post-step cleanup (post-step usage only)");
}

function printGhaTokenUsage(params: { stream: typeof console.log; prog: string }): void {
  params.stream(`usage: ${params.prog} gha token [--post]\n`);
  params.stream("acquire a github app installation token, or revoke it in the post step.");
  params.stream("");
  params.stream("options:");
  params.stream("  -h, --help   show help");
  params.stream("  --post       revoke the previously-acquired token (post-step usage only)");
}

function parseGhaArgs(args: string[]) {
  return arg(
    {
      "--help": Boolean,
      "--post": Boolean,
      "-h": "--help",
    },
    {
      argv: args,
      stopAtPositional: true,
    }
  );
}

function parseGhaTokenArgs(args: string[]) {
  return arg(
    {
      "--help": Boolean,
      "--post": Boolean,
      "-h": "--help",
    },
    {
      argv: args,
    }
  );
}

export async function runCli(params: GhaCliParams): Promise<void> {
  if (params.showHelp) {
    printGhaUsage({ stream: console.log, prog: params.prog });
    return;
  }

  let parsed: ReturnType<typeof parseGhaArgs>;
  try {
    parsed = parseGhaArgs(params.args);
  } catch (error) {
    const message = error instanceof Error ? error.message : String(error);
    console.error(`${message}\n`);
    printGhaUsage({ stream: console.error, prog: params.prog });
    process.exit(1);
  }

  if (parsed["--help"]) {
    printGhaUsage({ stream: console.log, prog: params.prog });
    return;
  }

  const positional = parsed._;
  const subcommand = positional[0];

  if (!subcommand) {
    await run(parsed["--post"] ? ["gha", "--post"] : ["gha"]);
    return;
  }

  if (subcommand !== "token") {
    console.error(`unknown gha subcommand: ${subcommand}\n`);
    printGhaUsage({ stream: console.error, prog: params.prog });
    process.exit(1);
  }

  // gha token [--post]
  let tokenParsed: ReturnType<typeof parseGhaTokenArgs>;
  try {
    tokenParsed = parseGhaTokenArgs(positional.slice(1));
  } catch (error) {
    const message = error instanceof Error ? error.message : String(error);
    console.error(`${message}\n`);
    printGhaTokenUsage({ stream: console.error, prog: params.prog });
    process.exit(1);
  }

  if (tokenParsed["--help"]) {
    printGhaTokenUsage({ stream: console.log, prog: params.prog });
    return;
  }

  if (tokenParsed._.length > 0) {
    console.error(`unexpected positional arguments for gha token: ${tokenParsed._.join(" ")}\n`);
    printGhaTokenUsage({ stream: console.error, prog: params.prog });
    process.exit(1);
  }

  const normalizedArgs = ["gha", "token"];
  if (tokenParsed["--post"]) {
    normalizedArgs.push("--post");
  }
  await run(normalizedArgs);
}

export async function run(args: string[]) {
  if (Number(process.versions.node.split(".")[0]) < MIN_NODE_MAJOR) {
    await rerunOnBootstrapNode(args);
    return;
  }
  // the cleanup step runs after the agent has already exited, so it must never
  // reach `setFailed` below — a red X there flips a finished, successful run to
  // `failure` with nothing actionable in the comment. that is #815's shape.
  if (args.includes("--post") && !args.includes("token")) {
    await runOAuthWriteback().catch((error) => core.warning(`oauth post-hook: ${error}`));
    removeAbandonedTempDirectory();
    return;
  }

  try {
    if (args.includes("token")) {
      if (args.includes("--post")) {
        await tokenPost();
      } else {
        await tokenMain();
      }
    } else {
      await runMain();
    }
  } catch (error) {
    const message = error instanceof Error ? error.message : String(error);
    core.setFailed(message);
  }
}