@@ -2885,40 +2885,68 @@ export function isOwnReviewThreadAuthor(login: string | null | undefined): boole
28852885/** The deterministic linked-issue facts the hard-rule evaluator needs (labels / assignees / open-state). */
28862886export type LinkedIssueFactsResult = { number : number ; labels : string [ ] ; assignees : string [ ] ; state : string ; authorLogin : string | null } ;
28872887
2888+ /** Tri-state outcome of fetching one linked issue's facts (#2136). `not_found` is a CONFIRMED 404 seen with a
2889+ * genuine, repo-scoped token — GitHub told an authenticated caller this issue number does not exist. `fetch_error`
2890+ * is everything else that prevented a read (network, 5xx, rate-limit, malformed body, or a 404 seen with only
2891+ * the public/anonymous token, which GitHub also returns for a real-but-inaccessible private issue) — a genuine
2892+ * outage or an unproven access gap, not confirmed evidence about the issue itself. Callers that treat an
2893+ * ALL-not_found result as significant (the linked-issue hard rule) must never extend that same treatment to
2894+ * fetch_error, or a GitHub outage would spuriously look like a fabricated reference. */
2895+ export type LinkedIssueFactsFetch =
2896+ | { status : "found" ; facts : LinkedIssueFactsResult }
2897+ | { status : "not_found" }
2898+ | { status : "fetch_error" } ;
2899+
28882900/**
2889- * FETCH the facts for one linked issue via the REST issues endpoint. FAIL-OPEN: any fetch/parse error returns
2890- * undefined so the caller skips that issue — a deterministic auto-close must NEVER fire (or be blocked) on a
2891- * transient fetch failure. Uses the same authenticated REST client + public-token 404-fallback as the other
2892- * live fetches. (Note: GitHub's issues endpoint also returns pull requests, which carry a `pull_request` field;
2893- * a PR number passed here would simply fail the rules — we only treat real issues' labels/assignees.)
2901+ * FETCH the facts for one linked issue via the REST issues endpoint. Distinguishes a CONFIRMED-nonexistent
2902+ * issue (404) from a transient fetch failure (#2136) — a deterministic auto-close must never fire on a
2903+ * transient failure, but a fabricated issue number is real, verifiable information the hard-rule evaluator
2904+ * needs. Uses the same authenticated REST client + public-token 404-fallback as the other live fetches. (Note:
2905+ * GitHub's issues endpoint also returns pull requests, which carry a `pull_request` field; a PR number passed
2906+ * here would simply fail the rules — we only treat real issues' labels/assignees.)
2907+ *
2908+ * GitHub returns 404 for BOTH a genuinely nonexistent issue and a real-but-inaccessible one (private repo, no
2909+ * grant) — it deliberately doesn't distinguish the two, to avoid leaking a private repo's existence to a caller
2910+ * without access. So a 404 is only trustworthy as CONFIRMED absence when `token` is a genuine, repo-scoped
2911+ * credential; the public/anonymous fallback token proves nothing about access. Without that, treat the 404 as
2912+ * `fetch_error` (fails open) rather than risk closing a PR over a real linked issue our token just can't see.
28942913 */
28952914export async function fetchLinkedIssueFacts (
28962915 env : Env ,
28972916 repoFullName : string ,
28982917 issueNumber : number ,
28992918 token : string | undefined ,
29002919 admissionKey ?: GitHubRateLimitAdmissionKey ,
2901- ) : Promise < LinkedIssueFactsResult | undefined > {
2902- const result = await githubJsonWithHeaders < {
2903- number ?: number ;
2904- state ?: string | null ;
2905- labels ?: Array < { name ?: string | null } | string | null > | null ;
2906- assignees ?: Array < { login ?: string | null } | null > | null ;
2907- user ?: { login ?: string | null } | null ;
2908- } > ( env , repoFullName , `/issues/${ issueNumber } ` , token , githubRateLimitOptions ( admissionKey ) ) . catch ( ( ) => undefined ) ;
2909- if ( ! result ) return undefined ;
2920+ ) : Promise < LinkedIssueFactsFetch > {
2921+ let result ;
2922+ try {
2923+ result = await githubJsonWithHeaders < {
2924+ number ?: number ;
2925+ state ?: string | null ;
2926+ labels ?: Array < { name ?: string | null } | string | null > | null ;
2927+ assignees ?: Array < { login ?: string | null } | null > | null ;
2928+ user ?: { login ?: string | null } | null ;
2929+ } > ( env , repoFullName , `/issues/${ issueNumber } ` , token , githubRateLimitOptions ( admissionKey ) ) ;
2930+ } catch ( error ) {
2931+ if ( ! ( error instanceof GitHubApiError ) || error . statusCode !== 404 ) return { status : "fetch_error" } ;
2932+ const hasProvenAccess = Boolean ( token ) && token !== env . GITHUB_PUBLIC_TOKEN ;
2933+ return { status : hasProvenAccess ? "not_found" : "fetch_error" } ;
2934+ }
29102935 const data = result . data ;
29112936 const labels = ( data . labels ?? [ ] ) . flatMap ( ( label ) => {
29122937 if ( typeof label === "string" ) return label . length > 0 ? [ label ] : [ ] ;
29132938 return label ?. name ? [ label . name ] : [ ] ;
29142939 } ) ;
29152940 const assignees = ( data . assignees ?? [ ] ) . flatMap ( ( assignee ) => ( assignee ?. login ? [ assignee . login ] : [ ] ) ) ;
29162941 return {
2917- number : data . number ?? issueNumber ,
2918- labels,
2919- assignees,
2920- state : String ( data . state ?? "open" ) . toLowerCase ( ) ,
2921- authorLogin : data . user ?. login ?? null ,
2942+ status : "found" ,
2943+ facts : {
2944+ number : data . number ?? issueNumber ,
2945+ labels,
2946+ assignees,
2947+ state : String ( data . state ?? "open" ) . toLowerCase ( ) ,
2948+ authorLogin : data . user ?. login ?? null ,
2949+ } ,
29222950 } ;
29232951}
29242952
0 commit comments