#!/usr/bin/env bun import { DEFAULT_GRACE_DAYS, FLAG_LABEL, duplicateTarget, githubApi, listAll, type Comment, type GitHubApi, type Issue, } from "./auto-close-duplicates"; declare const process: { readonly env: Readonly> }; export interface Verdict { readonly duplicate_of: number | null; readonly confidence: number; readonly evidence: string; } export interface FlagConfig { readonly repo: string; readonly issueNumber: number; readonly dryRun: boolean; } export type ParsedVerdict = | { readonly kind: "verdict"; readonly verdict: Verdict } | { readonly kind: "skip"; readonly reason: string }; export type FlagTarget = | { readonly kind: "target"; readonly original: number } | { readonly kind: "skip"; readonly reason: string }; export type FlagVerdict = | { readonly kind: "flagged"; readonly original: number; readonly body: string } | { readonly kind: "skip"; readonly reason: string }; export const MIN_CONFIDENCE = 0.95; export const NOTICE_MARKER_PREFIX = "`, lead, "", evidence, "", ask + warning].join("\n"); } export async function flagIssue(api: GitHubApi, config: FlagConfig, verdict: Verdict): Promise { const target = flagTarget(verdict, config.issueNumber); if (target.kind === "skip") { return target; } const issuePath = `/repos/${config.repo}/issues/${config.issueNumber}`; const comments = await listAll(api, `${issuePath}/comments`); if (comments.some((comment) => comment.body.includes(NOTICE_MARKER_PREFIX))) { return skip("already carries a duplicate notice"); } const prior = await api.request("GET", `/repos/${config.repo}/issues/${target.original}`); if (prior.pull_request !== undefined) { return skip(`#${target.original} is a pull request`); } const issue = await api.request("GET", issuePath); const body = noticeBody(issue, prior, verdict.evidence); if (!config.dryRun) { await api.request("POST", `${issuePath}/labels`, { labels: [FLAG_LABEL] }); await api.request("POST", `${issuePath}/comments`, { body }); } return { kind: "flagged", original: target.original, body }; } export function readConfig(env: Readonly>): FlagConfig & { readonly token: string } { const token = env.GITHUB_TOKEN; const repo = env.GITHUB_REPOSITORY; if (!token || !repo || !/^[\w.-]+\/[\w.-]+$/.test(repo)) { throw new Error("GITHUB_TOKEN and GITHUB_REPOSITORY (owner/repo) are required"); } const issueNumber = Number(env.ISSUE_NUMBER); if (!Number.isInteger(issueNumber) || issueNumber <= 0) { throw new Error(`ISSUE_NUMBER must be a positive integer, got "${env.ISSUE_NUMBER}"`); } return { token, repo, issueNumber, dryRun: env.DRY_RUN === "true" }; } function describe(config: FlagConfig, verdict: FlagVerdict): string { if (verdict.kind === "skip") { return `#${config.issueNumber}: skipped, ${verdict.reason}`; } if (config.dryRun) { return `#${config.issueNumber}: DRY RUN, set the DUPLICATE_CHECK_ENABLED repo variable to true to post this:\n\n${verdict.body}`; } return `#${config.issueNumber}: flagged as a possible duplicate of #${verdict.original}`; } if (import.meta.main) { const { token, ...config } = readConfig(process.env); const parsed = parseVerdict(process.env.VERDICT ?? ""); const verdict = parsed.kind === "skip" ? parsed : await flagIssue(githubApi(token), config, parsed.verdict); console.log(describe(config, verdict)); }