ci/github-script: convert more to TypeScript

This commit is contained in:
Michael Daniels
2026-09-27 14:10:41 -04:00
parent c4ab5fe26e
commit 00954dbca6
12 changed files with 111 additions and 41 deletions

View File

@@ -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,

View File

@@ -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,

View File

@@ -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)

View File

@@ -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()

View File

@@ -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'

View File

@@ -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

View File

@@ -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<typeof import('@actions/github/lib/utils').GitHub>
type Context = typeof import('@actions/github').context
type Core = typeof import('@actions/core')
type Team = Awaited<
ReturnType<GitHub['rest']['repos']['listTeams']>
>['data'][number]
type User = Awaited<
ReturnType<GitHub['rest']['teams']['listMembersInOrg']>
>['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`

View File

@@ -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<typeof import('@actions/github/lib/utils').GitHub>
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<BranchClassification, 'branch' | 'order' | 'version'>) {
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)

View File

@@ -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
}

View File

@@ -48,7 +48,7 @@ program
.argument('<pr>', '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('<repo>', '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 })
})

View File

@@ -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<string, BranchType[]> = {
master: ['development', 'primary'],
@@ -19,7 +19,7 @@ const typeConfig: Record<string, BranchType[]> = {
}
// "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<string, number> = {
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

View File

@@ -1,8 +1,28 @@
// @ts-nocheck
import Bottleneck from 'bottleneck'
export default async ({ github, core, maxConcurrent = 1 }, callback) => {
const stats = {
type GitHub = InstanceType<typeof import('@actions/github/lib/utils').GitHub>
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<void>,
) => {
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<ReturnType<typeof github.rest.rateLimit.get>>
try {
response = await github.rest.rateLimit.get()
} catch (err) {