From 00954dbca63b8d69bf184d30b3b067812f0f6ef5 Mon Sep 17 00:00:00 2001 From: Michael Daniels Date: Sun, 27 Sep 2026 14:10:41 -0400 Subject: [PATCH] ci/github-script: convert more to TypeScript --- .github/workflows/pull-request-target.yml | 2 +- .github/workflows/teams.yml | 2 +- .github/workflows/test.yml | 6 +-- ci/github-script/bot.js | 2 +- ci/github-script/check-target-branch.ts | 4 +- ci/github-script/commits.ts | 2 +- .../{get-teams.js => get-teams.ts} | 54 +++++++++++++++---- ci/github-script/{prepare.js => prepare.ts} | 36 +++++++++---- ci/github-script/reviews.ts | 2 +- ci/github-script/run | 4 +- ci/github-script/supportedBranches.ts | 6 +-- .../{withRateLimit.js => withRateLimit.ts} | 32 ++++++++--- 12 files changed, 111 insertions(+), 41 deletions(-) rename ci/github-script/{get-teams.js => get-teams.ts} (65%) rename ci/github-script/{prepare.js => prepare.ts} (91%) rename ci/github-script/{withRateLimit.js => withRateLimit.ts} (78%) diff --git a/.github/workflows/pull-request-target.yml b/.github/workflows/pull-request-target.yml index 98d463cb588d..2201912028a6 100644 --- a/.github/workflows/pull-request-target.yml +++ b/.github/workflows/pull-request-target.yml @@ -64,7 +64,7 @@ jobs: # https://github.com/octokit/plugin-retry.js/blob/9a2443746c350b3beedec35cf26e197ea318a261/src/index.ts#L14 retry-exempt-status-codes: 400,401,403,404 script: | - const { default: prepare } = await import('${{ github.workspace }}/ci/github-script/prepare.js') + const { default: prepare } = await import('${{ github.workspace }}/ci/github-script/prepare.ts') await prepare({ github, context, diff --git a/.github/workflows/teams.yml b/.github/workflows/teams.yml index 5e8e20497b7f..26d259cf375b 100644 --- a/.github/workflows/teams.yml +++ b/.github/workflows/teams.yml @@ -46,7 +46,7 @@ jobs: with: github-token: ${{ steps.app-token.outputs.token }} script: | - const { default: getTeams } = await import('${{ github.workspace }}/ci/github-script/get-teams.js') + const { default: getTeams } = await import('${{ github.workspace }}/ci/github-script/get-teams.ts') await getTeams({ github, context, diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index f5a6c1bfcec4..ec3598fbf2ed 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -35,7 +35,7 @@ jobs: # https://github.com/octokit/plugin-retry.js/blob/9a2443746c350b3beedec35cf26e197ea318a261/src/index.ts#L14 retry-exempt-status-codes: 400,401,403,404 script: | - const { default: prepare } = await import('${{ github.workspace }}/ci/github-script/prepare.js') + const { default: prepare } = await import('${{ github.workspace }}/ci/github-script/prepare.ts') await prepare({ github, context, @@ -88,13 +88,13 @@ jobs: 'ci/github-script/merge.js', 'ci/github-script/package.json', 'ci/github-script/package-lock.json', - 'ci/github-script/prepare.js', + 'ci/github-script/prepare.ts', 'ci/github-script/reminders.ts', 'ci/github-script/reviewers.js', 'ci/github-script/reviews.ts', 'ci/github-script/supportedBranches.ts', 'ci/github-script/supportedSystems.ts', - 'ci/github-script/withRateLimit.js', + 'ci/github-script/withRateLimit.ts', 'ci/pinned.json', 'pkgs/top-level/release-supported-systems.json', ].includes(file) || file.startsWith('ci/github-script/reminders/'))) core.setOutput('pr', true) diff --git a/ci/github-script/bot.js b/ci/github-script/bot.js index 7000e5b79f92..dbe5238a1d50 100644 --- a/ci/github-script/bot.js +++ b/ci/github-script/bot.js @@ -5,7 +5,7 @@ import { DefaultArtifactClient } from '@actions/artifact' import { handleMerge } from './merge.js' import { handleReviewers } from './reviewers.js' import { classify } from './supportedBranches.ts' -import withRateLimit from './withRateLimit.js' +import withRateLimit from './withRateLimit.ts' export default async ({ github, context, core, dry }) => { const artifactClient = new DefaultArtifactClient() diff --git a/ci/github-script/check-target-branch.ts b/ci/github-script/check-target-branch.ts index 709256ba6518..3beb7c58dd03 100644 --- a/ci/github-script/check-target-branch.ts +++ b/ci/github-script/check-target-branch.ts @@ -9,9 +9,9 @@ import { import { dismissReviews, postReview } from './reviews.ts' import { split } from './supportedBranches.ts' -// TODO: should this be combined with the branch checks in prepare.js? +// TODO: should this be combined with the branch checks in prepare.ts? // They do seem quite similar, but this needs to run after eval, -// and prepare.js obviously doesn't. +// and prepare.ts obviously doesn't. const reviewKey = 'check-target-branch' diff --git a/ci/github-script/commits.ts b/ci/github-script/commits.ts index ef8098f95d14..479b0c19d054 100644 --- a/ci/github-script/commits.ts +++ b/ci/github-script/commits.ts @@ -4,7 +4,7 @@ import type { context as actionsContext } from '@actions/github' import type { GitHub } from '@actions/github/lib/utils' import { dismissReviews, postReview } from './reviews.ts' import { classify } from './supportedBranches.ts' -import withRateLimit from './withRateLimit.js' +import withRateLimit from './withRateLimit.ts' const dirname = import.meta.dirname diff --git a/ci/github-script/get-teams.js b/ci/github-script/get-teams.ts similarity index 65% rename from ci/github-script/get-teams.js rename to ci/github-script/get-teams.ts index 577dd0e81436..72daabbd765d 100644 --- a/ci/github-script/get-teams.js +++ b/ci/github-script/get-teams.ts @@ -1,6 +1,5 @@ -// @ts-nocheck import { writeFileSync } from 'node:fs' -import withRateLimit from './withRateLimit.js' +import withRateLimit from './withRateLimit.ts' const excludeTeams = [ /^voters.*$/, @@ -8,25 +7,58 @@ const excludeTeams = [ /^nixpkgs-committers$/, ] -export default async ({ github, context, core, outFile }) => { +type GitHub = InstanceType +type Context = typeof import('@actions/github').context +type Core = typeof import('@actions/core') +type Team = Awaited< + ReturnType +>['data'][number] +type User = Awaited< + ReturnType +>['data'][number] + +interface Result { + [slug: string]: { + description: string | null + id: number + maintainers: unknown + members: unknown + name: string + } +} + +export default async ({ + github, + context, + core, + outFile, +}: { + github: GitHub + context: Context + core: Core + outFile: string +}) => { const org = context.repo.owner - const result = {} + const result: Result = {} await withRateLimit({ github, core }, async () => { // Turn an Array of users into an Object, mapping user.login -> user.id - function makeUserSet(users) { + function makeUserSet(users: User[]) { // Sort in-place and build result by mutation users.sort((a, b) => (a.login > b.login ? 1 : -1)) - return users.reduce((acc, user) => { - acc[user.login] = user.id - return acc - }, {}) + return users.reduce( + (acc, user) => { + acc[user.login] = user.id + return acc + }, + {} as { [login: string]: number }, + ) } // Process a list of teams and append to the result variable - async function processTeams(teams) { + async function processTeams(teams: Team[]) { for (const team of teams) { core.notice(`Processing team ${team.slug}`) if (!excludeTeams.some((regex) => team.slug.match(regex))) { @@ -74,7 +106,7 @@ export default async ({ github, context, core, outFile }) => { .reduce((acc, key) => { acc[key] = result[key] return acc - }, {}) + }, {} as Result) const json = `${JSON.stringify(sorted, null, 2)}\n` diff --git a/ci/github-script/prepare.js b/ci/github-script/prepare.ts similarity index 91% rename from ci/github-script/prepare.js rename to ci/github-script/prepare.ts index 7a4fcc1f1fb1..be08c09ee525 100644 --- a/ci/github-script/prepare.js +++ b/ci/github-script/prepare.ts @@ -1,12 +1,26 @@ -// @ts-nocheck import { dismissReviews, postReview } from './reviews.ts' -import { classify } from './supportedBranches.ts' +import { type BranchClassification, classify } from './supportedBranches.ts' import supportedSystems from './supportedSystems.ts' const reviewKey = 'prepare' -export default async ({ github, context, core, dry }) => { - const pull_number = context.payload.pull_request.number +type GitHub = InstanceType +type Context = typeof import('@actions/github').context +type Core = typeof import('@actions/core') + +export default async ({ + github, + context, + core, + dry, +}: { + github: GitHub + context: Context + core: Core + dry: boolean +}) => { + const pull_number = context.payload.pull_request?.number + if (!pull_number) throw new Error('Not a pull request') for (const retryInterval of [5, 10, 20, 40, 80]) { core.info('Checking whether the pull request can be merged...') @@ -89,7 +103,11 @@ export default async ({ github, context, core, dry }) => { .filter(({ stable, type }) => type.includes('primary') && stable) .sort((a, b) => b.version.localeCompare(a.version)) - async function mergeBase({ branch, order, version }) { + async function mergeBase({ + branch, + order, + version, + }: Pick) { const { data } = await github.rest.repos.compareCommitsWithBasehead({ ...context.repo, basehead: `${branch}...${head.sha}`, @@ -152,7 +170,7 @@ export default async ({ github, context, core, dry }) => { ) .sort((a, b) => a.order - b.order) - const best = candidates.at(0) + const best = candidates[0] core.info('The base branches for this PR are:') core.info(`github: ${base.ref}`) @@ -188,9 +206,9 @@ export default async ({ github, context, core, dry }) => { } } - let mergedSha, targetSha + let mergedSha: string, targetSha: string - if (prInfo.mergeable) { + if (prInfo.mergeable && prInfo.merge_commit_sha != null) { core.info('The PR can be merged.') mergedSha = prInfo.merge_commit_sha @@ -224,7 +242,7 @@ export default async ({ github, context, core, dry }) => { const files = ( await github.paginate(github.rest.pulls.listFiles, { ...context.repo, - pull_number: context.payload.pull_request.number, + pull_number, per_page: 100, }) ).map((file) => file.filename) diff --git a/ci/github-script/reviews.ts b/ci/github-script/reviews.ts index be4168e0c549..d66352d76a25 100644 --- a/ci/github-script/reviews.ts +++ b/ci/github-script/reviews.ts @@ -175,7 +175,7 @@ interface PostReviewProps { core: typeof import('@actions/core') dry: boolean body: string - event: keyof typeof eventToState + event?: keyof typeof eventToState reviewKey: string } diff --git a/ci/github-script/run b/ci/github-script/run index 2920ae8b6f44..67af7cb1c676 100755 --- a/ci/github-script/run +++ b/ci/github-script/run @@ -48,7 +48,7 @@ program .argument('', 'Number of the Pull Request to check') .option('--no-dry', 'Make actual modifications') .action(async (owner, repo, pr, options) => { - const prepare = (await import('./prepare.js')).default + const prepare = (await import('./prepare.ts')).default await run(prepare, owner, repo, pr, options) }) @@ -90,7 +90,7 @@ program .argument('', 'Name of the GitHub repository to label (Example: nixpkgs)') .argument('[outFile]', 'Path to the output file (Example: github-teams.json). If not set, prints to stdout') .action(async (owner, repo, outFile, options) => { - const getTeams = (await import('./get-teams.js')).default + const getTeams = (await import('./get-teams.ts')).default await run(getTeams, owner, repo, undefined, { ...options, outFile }) }) diff --git a/ci/github-script/supportedBranches.ts b/ci/github-script/supportedBranches.ts index cb947691ca61..a29819a9ee3a 100755 --- a/ci/github-script/supportedBranches.ts +++ b/ci/github-script/supportedBranches.ts @@ -5,7 +5,7 @@ import { resolve } from 'node:path' import { fileURLToPath } from 'node:url' -type BranchType = 'channel' | 'development' | 'primary' | 'secondary' +type BranchType = 'channel' | 'development' | 'primary' | 'secondary' | 'wip' const typeConfig: Record = { master: ['development', 'primary'], @@ -19,7 +19,7 @@ const typeConfig: Record = { } // "order" ranks the development branches by how likely they are the intended base branch -// when they are an otherwise equally good fit according to ci/github-script/prepare.js. +// when they are an otherwise equally good fit according to ci/github-script/prepare.ts. const orderConfig: Record = { master: 0, release: 1, @@ -44,7 +44,7 @@ function split(branch: string) { return groups as unknown as SplitResult } -interface BranchClassification { +export interface BranchClassification { branch: string order: number stable: boolean diff --git a/ci/github-script/withRateLimit.js b/ci/github-script/withRateLimit.ts similarity index 78% rename from ci/github-script/withRateLimit.js rename to ci/github-script/withRateLimit.ts index 59fb088ca3d7..df3b62f6356d 100644 --- a/ci/github-script/withRateLimit.js +++ b/ci/github-script/withRateLimit.ts @@ -1,8 +1,28 @@ -// @ts-nocheck import Bottleneck from 'bottleneck' -export default async ({ github, core, maxConcurrent = 1 }, callback) => { - const stats = { +type GitHub = InstanceType +type Core = typeof import('@actions/core') + +export interface Stats { + issues: number + prs: number + requests: number + artifacts: number +} + +export default async ( + { + github, + core, + maxConcurrent = 1, + }: { + github: GitHub + core: Core + maxConcurrent?: number + }, + callback: (stats: Stats) => Promise, +) => { + const stats: Stats = { issues: 0, prs: 0, requests: 0, @@ -30,12 +50,12 @@ export default async ({ github, core, maxConcurrent = 1 }, callback) => { if (options.url.startsWith('/search/')) return request(options) stats.requests++ if (['POST', 'PUT', 'PATCH', 'DELETE'].includes(options.method)) - return writeLimits.schedule(request.bind(null, options)) - else return allLimits.schedule(request.bind(null, options)) + return writeLimits.schedule(async () => request(options)) + else return allLimits.schedule(async () => request(options)) }) async function updateReservoir() { - let response + let response: Awaited> try { response = await github.rest.rateLimit.get() } catch (err) {