Ctrl K
ComunidadGuías…

utils/statusChecks.ts

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

Archivo original ↗Preguntar a KiBot
import type { RestEndpointMethodTypes } from "@octokit/rest";
import type { ToolContext } from "../mcp/server.ts";
import { primaryRepoState } from "../toolState.ts";
import { log } from "./cli.ts";
import { countOutstandingPullfrogThreads } from "./outstandingThreads.ts";
import {
  APPROVAL_CHECK_NAME,
  createTerminalRunStatusCheck,
  finalizeRunStatusCheck,
  GITHUB_ACTIONS_APP_SLUG,
  parseCheckRunId,
  RUN_STATUS_CHECK_NAME,
} from "./runStatusCheck.ts";

/**
 * post the `pullfrog` (run lifecycle) and `pullfrog-approval` (review verdict)
 * commit-status check-runs.
 *
 *   - `Pullfrog` is on by default (`Repo.statusChecks`). the server already created it
 *     `in_progress` at dispatch, so the work here is a PATCH to its terminal conclusion —
 *     see `runStatusCheck.ts` for why a second create would leave two contradictory rows.
 *     the terminal-create fallback covers a payload with no `checkRun` (older server
 *     build mid-rolling-deploy, or a workflow driven outside Pullfrog's dispatch path).
 *   - `pullfrog-approval` stays opt-in (`Repo.approvalCheck`, default off) and terminal-only:
 *     it asserts a review verdict, which only exists once a run produces one. anchored
 *     to the exact reviewed sha so a mid-run push leaves the new head unapproved until
 *     a follow-up re-review reports. a re-review that submits no review carries the
 *     prior verdict forward instead (see `carriedApproval`).
 *
 * best-effort throughout: a check-post failure (transient 5xx, closed PR, revoked
 * permission) must never flip the run's own outcome. the `workflow_run.completed` webhook
 * and both stuck-run reaper sweeps close out a check this function fails to finalize.
 */
export async function reportStatusChecks(
  ctx: ToolContext,
  params: { runSucceeded: boolean }
): Promise<void> {
  const event = ctx.payload.event;
  const pullNumber = event.issue_number;
  if (event.is_pr !== true || typeof pullNumber !== "number") return;
  // the check-run id is the authority, not the setting: if the server seeded a check, it
  // MUST be finalized even when this workflow opted out via `status_checks: disabled`.
  // the server never parses workflow YAML (it only sees `Repo.statusChecks`), so the
  // opt-out cannot prevent the seed — and a seeded check left `in_progress` because the
  // action declined to touch it is strictly worse than the check the user didn't want.
  const checkRunId = parseCheckRunId(ctx.payload.checkRun);
  if (checkRunId === undefined && !ctx.payload.runStatusCheck && !ctx.payload.approvalCheck) return;

  const conclusion = params.runSucceeded ? "success" : "failure";
  const detailsUrl = ctx.runId
    ? `https://github.com/${ctx.repo.owner}/${ctx.repo.name}/actions/runs/${ctx.runId}`
    : undefined;
  const approval = ctx.toolState.approval;
  // a silent run has no progress comment, so a final report that is not a review — a
  // re-review's "no new review was warranted" — otherwise reaches nobody.
  const note = event.silent && !approval?.url ? ctx.toolState.lastProgressBody : undefined;

  if (checkRunId !== undefined) {
    await finalizeRunStatusCheck({
      octokit: ctx.octokit,
      owner: ctx.repo.owner,
      repo: ctx.repo.name,
      checkRunId,
      conclusion,
      detailsUrl,
      reviewUrl: approval?.url,
      note,
    })
      .then(() => log.info(`» finalized ${RUN_STATUS_CHECK_NAME} check (${conclusion})`))
      .catch((err) => log.debug(`status checks: ${RUN_STATUS_CHECK_NAME} finalize failed: ${err}`));
  }

  // a re-review that submits no review records no verdict of its own.
  const carriesApproval = !approval && ctx.toolState.selectedMode === "IncrementalReview";
  // everything below needs a head sha, which costs an API call — skip it when there is
  // nothing left to post.
  const needsApprovalCheck =
    ctx.payload.approvalCheck && params.runSucceeded && (approval !== undefined || carriesApproval);
  const needsFallbackRunCheck = ctx.payload.runStatusCheck && checkRunId === undefined;
  if (!needsApprovalCheck && !needsFallbackRunCheck) return;

  let headSha: string;
  try {
    const pr = await ctx.octokit.rest.pulls.get({
      owner: ctx.repo.owner,
      repo: ctx.repo.name,
      pull_number: pullNumber,
    });
    headSha = pr.data.head.sha;
  } catch (err) {
    log.debug(`status checks: failed to resolve PR #${pullNumber} head sha: ${err}`);
    return;
  }

  if (needsFallbackRunCheck) {
    await createTerminalRunStatusCheck({
      octokit: ctx.octokit,
      owner: ctx.repo.owner,
      repo: ctx.repo.name,
      headSha: primaryRepoState(ctx.toolState).checkoutSha ?? headSha,
      conclusion,
      detailsUrl,
      reviewUrl: approval?.url,
      note,
    })
      .then(() => log.info(`» posted ${RUN_STATUS_CHECK_NAME} check (${conclusion})`))
      .catch((err) => log.debug(`status checks: ${RUN_STATUS_CHECK_NAME} post failed: ${err}`));
  }

  // only assert an approval verdict when the run cleanly completed. the verdict is
  // recorded before create_pull_request_review actually submits, so on a failed/crashed
  // run the review may not have landed — leave pullfrog-approval absent (the next run
  // resolves it) rather than post a stale verdict.
  if (!needsApprovalCheck) return;

  const verdict = approval
    ? { wouldApprove: approval.wouldApprove, sha: approval.sha ?? headSha, carriedLine: "" }
    : await carriedApproval(ctx, pullNumber).catch((err) => {
        log.debug(`status checks: ${APPROVAL_CHECK_NAME} carry-forward failed: ${err}`);
        return undefined;
      });
  if (!verdict) return;

  const createParams: RestEndpointMethodTypes["checks"]["create"]["parameters"] = {
    owner: ctx.repo.owner,
    repo: ctx.repo.name,
    name: APPROVAL_CHECK_NAME,
    head_sha: verdict.sha,
    status: "completed",
    conclusion: verdict.wouldApprove ? "success" : "failure",
    output: {
      title: verdict.wouldApprove ? "Pullfrog would approve" : "Pullfrog would not approve",
      summary:
        (verdict.wouldApprove
          ? "Pullfrog has no outstanding review feedback on this PR."
          : "Pullfrog has outstanding review feedback or requested changes on this PR.") +
        verdict.carriedLine,
    },
  };
  if (detailsUrl) createParams.details_url = detailsUrl;
  await ctx.octokit.rest.checks
    .create(createParams)
    .then(() => log.info(`» posted ${APPROVAL_CHECK_NAME} check`))
    .catch((err) => log.debug(`status checks: ${APPROVAL_CHECK_NAME} post failed: ${err}`));
}

/**
 * the verdict for a re-review that submitted no review. recording none left a PR requiring
 * `pullfrog-approval` blocked on "Expected — Waiting for status to be reported" after every
 * base-branch merge. the verdict on `beforeSha` carries to the checked-out commit, and never
 * one from further back, which would approve changes no run reviewed. a rejection always
 * carries. an approval carries only when the agent declared this commit needs no review
 * (`no_review_needed`) and no Pullfrog finding is open: a run blocked mid-review also ends
 * on `report_progress`, so ending there proves nothing.
 */
async function carriedApproval(ctx: ToolContext, pullNumber: number) {
  const primary = primaryRepoState(ctx.toolState);
  if (!primary.beforeSha || !primary.checkoutSha) return undefined;
  // never shadow a verdict a concurrent review already posted on this commit.
  if ((await latestApprovalConclusion(ctx, primary.checkoutSha)) !== undefined) return undefined;
  const prior = await latestApprovalConclusion(ctx, primary.beforeSha);
  if (prior === undefined) return undefined;
  const from = primary.beforeSha.slice(0, 7);
  if (prior !== "success") {
    const carriedLine = `\n\nNo review covered this commit, so the verdict on ${from} carries forward.`;
    return { wouldApprove: false, sha: primary.checkoutSha, carriedLine };
  }
  if (ctx.toolState.noReviewNeededSha !== primary.checkoutSha) return undefined;
  return {
    wouldApprove: (await countOutstandingPullfrogThreads(ctx, pullNumber)) === 0,
    sha: primary.checkoutSha,
    carriedLine: `\n\nPullfrog found nothing to review in the commits since ${from}.`,
  };
}

/** the conclusion of the newest `pullfrog-approval` on `ref`, if one was posted. */
async function latestApprovalConclusion(ctx: ToolContext, ref: string) {
  const result = await ctx.octokit.rest.checks.listForRef({
    owner: ctx.repo.owner,
    repo: ctx.repo.name,
    ref,
    check_name: APPROVAL_CHECK_NAME,
    status: "completed",
  });
  // a workflow job of the same name posts as GitHub Actions — never a verdict Pullfrog issued.
  const latest = result.data.check_runs
    .filter((run) => run.app?.slug !== GITHUB_ACTIONS_APP_SLUG)
    .sort((a, b) => (b.completed_at ?? "").localeCompare(a.completed_at ?? ""))[0];
  return latest?.conclusion ?? undefined;
}