11import { zip } from 'es-toolkit' ;
22import assert from 'node:assert' ;
3+ import { env } from 'node:process' ;
34import { duration_fmt , removePrefix } from '../../util' ;
4- import { execFile } from '../../util_node' ;
55import type { Link , RelativePath } from '../see_also.data' ;
66import { FileCacheWithInit } from './caching.ts' ;
77
@@ -158,29 +158,41 @@ type PullState = { title: string } & (
158158) ;
159159
160160/**
161- * Query GitHub GraphQL API using GitHub CLI .
161+ * Query GitHub GraphQL API.
162162 * https://docs.github.com/en/graphql/reference/queries
163163 */
164- async function queryGitHub (
165- query : string ,
166- vars : Record < string , string | number > = { } ,
167- ) : Promise < any > {
164+ async function queryGitHub ( query : string ) : Promise < any > {
168165 const timeStart = Date . now ( ) ;
169166
170- const { stdout } = await execFile ( 'gh' , [
171- 'api' ,
172- 'graphql' ,
173- '--raw-field' ,
174- `query=${ query } ` ,
175- ...Object . entries ( vars ) . flatMap ( ( [ k , v ] ) => [ '--field' , `${ k } =${ v } ` ] ) ,
176- ] ) ;
167+ const token = env . GH_TOKEN ?? env . GITHUB_TOKEN ;
168+ if ( token === undefined ) {
169+ throw new Error (
170+ 'GitHub GraphQL API requires authentication, but no token is available. Please set $GITHUB_TOKEN (no scope required) to authenticate.' ,
171+ ) ;
172+ }
173+
174+ const response = await fetch ( 'https://api.github.com/graphql' , {
175+ method : 'POST' ,
176+ headers : {
177+ 'Content-Type' : 'application/json' ,
178+ Authorization : `Bearer ${ token } ` ,
179+ } ,
180+ body : JSON . stringify ( { query } ) ,
181+ } ) ;
182+ const result = await response . text ( ) ;
183+
184+ if ( ! response . ok ) {
185+ throw new Error (
186+ `GitHub API request failed: ${ response . status } ${ response . statusText } \n${ result } ` ,
187+ ) ;
188+ }
177189
178190 console . log (
179- `📩 Got ${ stdout . length } characters from GitHub API successfully in` ,
191+ `📩 Got ${ result . length } characters from GitHub API successfully in` ,
180192 `${ duration_fmt ( Date . now ( ) - timeStart ) } .` ,
181193 ) ;
182194
183- return JSON . parse ( stdout ) . data ;
195+ return JSON . parse ( result ) . data ;
184196}
185197
186198/**
@@ -252,7 +264,22 @@ async function fetchStates(meta_list: (IssueMeta | PullMeta)[]): Promise<void> {
252264
253265 const stateFlat : ( IssueState | PullState ) [ ] = Object . values ( data )
254266 . map ( ( items_of_a_repo ) => Object . values ( items_of_a_repo ) )
255- . flat ( ) ;
267+ . flat ( )
268+ . map ( ( { __typename : typename , ...state } , i ) => {
269+ // Check the types match
270+ const actualType =
271+ typename === 'Issue'
272+ ? 'issue'
273+ : typename === 'PullRequest'
274+ ? 'pull'
275+ : typename ;
276+ const mismatched = grouped [ i ] . filter ( ( meta ) => meta . type !== actualType ) ;
277+ assert (
278+ mismatched . length === 0 ,
279+ `the link type is wrong. Actual: ${ actualType } ; Found: ${ JSON . stringify ( mismatched ) } ` ,
280+ ) ;
281+ return state ;
282+ } ) ;
256283
257284 assert . strictEqual ( metaFlat . length , stateFlat . length ) ;
258285
@@ -266,6 +293,9 @@ async function fetchStates(meta_list: (IssueMeta | PullMeta)[]): Promise<void> {
266293 _GITHUB_STATES_CACHE . save ( ) ;
267294}
268295
296+ /** Warned metadata, saved to avoid repetitive warnings. */
297+ const _WARNED = new Set < SerializedMeta > ( ) ;
298+
269299/**
270300 * Try to populate titles via GitHub in place.
271301 *
@@ -294,22 +324,28 @@ async function tryResolveViaGitHub(targets: Link[]): Promise<void> {
294324 try {
295325 await fetchStates ( fetchPlan ) ;
296326 } catch ( e ) {
297- // @ts -ignore
298- if ( e . code === 'ENOENT' ) {
327+ if ( ! fetchPlan . every ( ( meta ) => _WARNED . has ( serialize ( meta ) ) ) ) {
299328 console . warn (
300- '[Warning] GitHub CLI is not available. Skip resolving titles via GitHub for `<SeeAlso>`.' ,
329+ '[Warning] GitHub API is not accessible. Skip resolving titles for the following links in <SeeAlso> via GitHub. (You can ignore this warning if you do not care those titles.)\n ' ,
330+ fetchPlan . map ( ( meta ) => `${ meta . repo } #${ meta . num } (${ meta . type } )` ) ,
331+ `\n Cause: ${ e } ` ,
301332 ) ;
302- return ;
333+
334+ for ( const meta of fetchPlan ) {
335+ _WARNED . add ( serialize ( meta ) ) ;
336+ }
303337 }
304- throw e ;
305338 }
306339 }
307340
308341 // 2. Use the cache to populate titles
309342
310343 for ( const [ link , meta ] of relevant ) {
311344 const state = cache . get ( serialize ( meta ) ) ;
312- assert ( state !== undefined ) ;
345+ if ( state === undefined ) {
346+ // Skip this one if its cache is not ready, but allow others to reuse their caches.
347+ continue ;
348+ }
313349
314350 const repoNum = `${ removePrefix ( meta . repo , 'typst/' ) } #${ meta . num } ` ;
315351
0 commit comments