Merge pull request #35370 from github/repo-sync

Repo sync
This commit is contained in:
docs-bot 2024-11-20 10:36:27 -08:00 коммит произвёл GitHub
Родитель 5e50aa665c 1ad5033572
Коммит ef684f4d98
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: B5690EEEBB952194
17 изменённых файлов: 91 добавлений и 95 удалений

Просмотреть файл

@ -33,7 +33,7 @@ runs:
- name: Run script
if: ${{ inputs.restore-only == '' }}
shell: bash
run: node src/archives/scripts/warmup-remotejson.js
run: npm run warmup-remotejson
- name: Cache .remotejson-cache (save)
if: ${{ inputs.restore-only == '' }}

Просмотреть файл

@ -7,7 +7,7 @@ name: Purge old workflow runs
on:
workflow_dispatch:
schedule:
- cron: '20 */2 * * *' # Run every 2 hours at 20 minutes past the hour
- cron: '20 * * * *' # Run every hour at 20 minutes past the hour
permissions:
contents: write

Просмотреть файл

@ -88,7 +88,7 @@
"update-internal-links": "tsx src/links/scripts/update-internal-links.ts",
"validate-asset-images": "tsx src/assets/scripts/validate-asset-images.ts",
"validate-github-github-docs-urls": "tsx src/links/scripts/validate-github-github-docs-urls/index.ts",
"warmup-remotejson": "node src/archives/scripts/warmup-remotejson.js"
"warmup-remotejson": "tsx src/archives/scripts/warmup-remotejson.ts"
},
"lint-staged": {
"*.{js,mjs,ts,tsx}": "eslint --cache --fix",

9
src/archives/lib/is-archived-version.d.ts поставляемый
Просмотреть файл

@ -1,9 +0,0 @@
import type { ExtendedRequest } from '@/types'
type IsArchivedInfo = {
isArchived?: boolean
requestedVersion?: string
}
export declare function isArchivedVersion(req: ExtendedRequest): IsArchivedInfo
export declare function isArchivedVersionByPath(pathToCheck: string): IsArchivedInfo

Просмотреть файл

@ -1,14 +1,20 @@
import patterns from '#src/frame/lib/patterns.js'
import { deprecated } from '#src/versions/lib/enterprise-server-releases.js'
import patterns from '@/frame/lib/patterns.js'
import { deprecated } from '@/versions/lib/enterprise-server-releases.js'
import type { ExtendedRequest } from '@/types'
export function isArchivedVersion(req) {
type IsArchivedInfo = {
isArchived?: boolean
requestedVersion?: string
}
export function isArchivedVersion(req: ExtendedRequest): IsArchivedInfo {
// if this is an assets path, use the referrer
// if this is a docs path, use the req.path
const pathToCheck = patterns.assetPaths.test(req.path) ? req.get('referrer') : req.path
return isArchivedVersionByPath(pathToCheck || '')
}
export function isArchivedVersionByPath(pathToCheck) {
export function isArchivedVersionByPath(pathToCheck: string): IsArchivedInfo {
// ignore paths that don't have an enterprise version number
if (
!(
@ -22,10 +28,10 @@ export function isArchivedVersionByPath(pathToCheck) {
// extract enterprise version from path, e.g. 2.16
const requestedVersion = pathToCheck.includes('enterprise-server@')
? pathToCheck.match(patterns.getEnterpriseServerNumber)?.[1]
: pathToCheck.match(patterns.getEnterpriseVersionNumber)[1]
: pathToCheck.match(patterns.getEnterpriseVersionNumber)?.[1]
// bail if the request version is not deprecated
if (!deprecated.includes(requestedVersion)) {
if (!requestedVersion || !deprecated.includes(requestedVersion)) {
return {}
}

Просмотреть файл

@ -1,8 +1,8 @@
import path from 'path'
import { supported, latest } from '#src/versions/lib/enterprise-server-releases.js'
import patterns from '#src/frame/lib/patterns.js'
import nonEnterpriseDefaultVersion from '#src/versions/lib/non-enterprise-default-version.js'
import { allVersions } from '#src/versions/lib/all-versions.js'
import { supported, latest } from '@/versions/lib/enterprise-server-releases.js'
import patterns from '@/frame/lib/patterns.js'
import nonEnterpriseDefaultVersion from '@/versions/lib/non-enterprise-default-version.js'
import { allVersions } from '@/versions/lib/all-versions.js'
const latestNewVersion = `enterprise-server@${latest}`
const oldVersions = ['dotcom'].concat(supported)
const newVersions = Object.keys(allVersions)
@ -18,7 +18,7 @@ const newVersions = Object.keys(allVersions)
// return an old version like 2.21.
// Fall back to latest GHES version if one can't be found,
// for example, if the new version is private-instances@latest.
export function getOldVersionFromNewVersion(newVersion) {
export function getOldVersionFromNewVersion(newVersion: string) {
return newVersion === nonEnterpriseDefaultVersion
? 'dotcom'
: oldVersions.find((oldVersion) => newVersion.includes(oldVersion)) || latest
@ -27,7 +27,7 @@ export function getOldVersionFromNewVersion(newVersion) {
// Given an old version like 2.21,
// return a new version like enterprise-server@2.21.
// Fall back to latest GHES version if one can't be found.
export function getNewVersionFromOldVersion(oldVersion) {
export function getNewVersionFromOldVersion(oldVersion: string) {
return oldVersion === 'dotcom'
? nonEnterpriseDefaultVersion
: newVersions.find((newVersion) => newVersion.includes(oldVersion)) || latestNewVersion
@ -35,7 +35,7 @@ export function getNewVersionFromOldVersion(oldVersion) {
// Given an old path like /enterprise/2.21/user/github/category/article,
// return an old version like 2.21.
export function getOldVersionFromOldPath(oldPath) {
export function getOldVersionFromOldPath(oldPath: string) {
// We should never be calling this function on a path that starts with a new version,
// so we can assume the path either uses the old /enterprise format or it's dotcom.
if (!patterns.enterprise.test(oldPath)) return 'dotcom'
@ -46,7 +46,7 @@ export function getOldVersionFromOldPath(oldPath) {
// Given an old path like /en/enterprise/2.21/user/github/category/article,
// return a new path like /en/enterprise-server@2.21/github/category/article.
export function getNewVersionedPath(oldPath, languageCode = '') {
export function getNewVersionedPath(oldPath: string, languageCode = '') {
// It's possible a new version has been injected into an old path
// via syntax like: /en/enterprise/{{ currentVersion }}/admin/category/article
// which could resolve to /en/enterprise/private-instances@latest/admin/category/article,
@ -58,7 +58,7 @@ export function getNewVersionedPath(oldPath, languageCode = '') {
// If no new version was found, assume path contains an old version, like 2.21
if (!newVersion) {
const oldVersion = getOldVersionFromOldPath(oldPath, languageCode)
const oldVersion = getOldVersionFromOldPath(oldPath)
newVersion = getNewVersionFromOldVersion(oldVersion)
}

Просмотреть файл

@ -2,7 +2,7 @@ import got from 'got'
import type { Response, NextFunction } from 'express'
import patterns from '@/frame/lib/patterns.js'
import { isArchivedVersion } from '@/archives/lib/is-archived-version.js'
import { isArchivedVersion } from '@/archives/lib/is-archived-version'
import {
setFastlySurrogateKey,
SURROGATE_ENUMS,

Просмотреть файл

@ -10,7 +10,7 @@ import {
} from '@/versions/lib/enterprise-server-releases.js'
import patterns from '@/frame/lib/patterns.js'
import versionSatisfiesRange from '@/versions/lib/version-satisfies-range.js'
import { isArchivedVersion } from '@/archives/lib/is-archived-version.js'
import { isArchivedVersion } from '@/archives/lib/is-archived-version'
import {
setFastlySurrogateKey,
SURROGATE_ENUMS,

Просмотреть файл

@ -19,12 +19,11 @@
// [end-readme]
import { program } from 'commander'
import semver from 'semver'
import semver, { SemVer } from 'semver'
import getRemoteJSON from '#src/frame/lib/get-remote-json.js'
import getRemoteJSON from '@/frame/lib/get-remote-json.js'
import {
deprecated,
firstReleaseStoredInBlobStorage,
lastVersionWithoutArchivedRedirectsFile,
} from '#src/versions/lib/enterprise-server-releases.js'
@ -36,18 +35,14 @@ program
main()
function version2url(version) {
const inBlobStorage = semver.gte(
semver.coerce(version).raw,
semver.coerce(firstReleaseStoredInBlobStorage).raw,
)
function version2url(version: string | SemVer) {
return `https://github.github.com/docs-ghes-${version}/redirects.json`
}
function withArchivedRedirectsFile(version) {
function withArchivedRedirectsFile(version: string | SemVer) {
return semver.eq(
semver.coerce(version).raw,
semver.coerce(lastVersionWithoutArchivedRedirectsFile).raw,
semver.coerce(version)?.raw || '',
semver.coerce(lastVersionWithoutArchivedRedirectsFile)?.raw || '',
)
}

Просмотреть файл

@ -1,8 +1,8 @@
import { describe, expect, test, vi } from 'vitest'
import enterpriseServerReleases from '#src/versions/lib/enterprise-server-releases.js'
import { get, getDOM } from '#src/tests/helpers/e2etest.js'
import { SURROGATE_ENUMS } from '#src/frame/middleware/set-fastly-surrogate-key.js'
import enterpriseServerReleases from '@/versions/lib/enterprise-server-releases.js'
import { get, getDOM } from '@/tests/helpers/e2etest-ts'
import { SURROGATE_ENUMS } from '@/frame/middleware/set-fastly-surrogate-key.js'
describe('enterprise deprecation', () => {
vi.setConfig({ testTimeout: 60 * 1000 })
@ -45,8 +45,8 @@ describe('enterprise deprecation', () => {
test('handles requests for deprecated Enterprise pages ( >=2.13 )', async () => {
expect(enterpriseServerReleases.deprecated.includes('2.13')).toBe(true)
const $ = await getDOM('/en/enterprise/2.13/user/articles/about-branches')
expect($.res.statusCode).toBe(200)
const { $, res } = await getDOM('/en/enterprise/2.13/user/articles/about-branches')
expect(res.statusCode).toBe(200)
expect($('h1').first().text()).toBe('About branches')
})
@ -60,27 +60,27 @@ describe('enterprise deprecation', () => {
test('handles requests for deprecated Enterprise pages ( <2.13 )', async () => {
expect(enterpriseServerReleases.deprecated.includes('2.12')).toBe(true)
const $ = await getDOM('/enterprise/2.12/user/articles/about-branches')
expect($.res.statusCode).toBe(200)
const { $, res } = await getDOM('/enterprise/2.12/user/articles/about-branches')
expect(res.statusCode).toBe(200)
expect($('h2').text()).toBe('About branches')
})
test('handles requests for deprecated Enterprise version 11.10.340', async () => {
expect(enterpriseServerReleases.deprecated.includes('11.10.340')).toBe(true)
const $ = await getDOM('/enterprise/11.10.340/admin/articles/adding-teams')
expect($.res.statusCode).toBe(200)
const { $, res } = await getDOM('/enterprise/11.10.340/admin/articles/adding-teams')
expect(res.statusCode).toBe(200)
expect($('h2').text()).toBe('Adding teams')
})
test('has working admin guide links ( <2.13 )', async () => {
const guidesPath = '/enterprise/2.12/admin'
let $ = await getDOM(`${guidesPath}/guides`)
const firstLink = $('[class="guide-section"]').children('a').attr('href')
const { $: $1 } = await getDOM(`${guidesPath}/guides`)
const firstLink = $1('[class="guide-section"]').children('a').attr('href')
$ = await getDOM(`${guidesPath}/${firstLink}`)
expect($.res.statusCode).toBe(200)
const { $: $2, res } = await getDOM(`${guidesPath}/${firstLink}`)
expect(res.statusCode).toBe(200)
// this test assumes the Installation guide is the first link on the guides page
expect($('h2').text()).toBe('Installing and configuring GitHub Enterprise')
expect($2('h2').text()).toBe('Installing and configuring GitHub Enterprise')
})
})
@ -139,22 +139,22 @@ describe('recently deprecated redirects', () => {
describe('deprecation banner', () => {
test('renders a deprecation warning banner on oldest supported Enterprise version', async () => {
const $ = await getDOM(`/en/enterprise/${enterpriseServerReleases.oldestSupported}`)
const { $ } = await getDOM(`/en/enterprise/${enterpriseServerReleases.oldestSupported}`)
expect($('[data-testid=deprecation-banner]').length).toBe(1)
})
test('does not render a deprecation warning banner on other Enterprise versions', async () => {
const $ = await getDOM(`/en/enterprise/${enterpriseServerReleases.latest}`)
const { $ } = await getDOM(`/en/enterprise/${enterpriseServerReleases.latest}`)
expect($('[data-testid=deprecation-banner]').length).toBe(0)
})
test('deprecation warning banner includes a date', async () => {
const $ = await getDOM(`/en/enterprise/${enterpriseServerReleases.oldestSupported}`)
const { $ } = await getDOM(`/en/enterprise/${enterpriseServerReleases.oldestSupported}`)
expect($('[data-testid=deprecation-banner] b').text().endsWith('discontinued on .')).toBe(false)
})
test('deprecation warning banner includes the right text depending on the date', async () => {
const $ = await getDOM(`/en/enterprise/${enterpriseServerReleases.oldestSupported}`)
const { $ } = await getDOM(`/en/enterprise/${enterpriseServerReleases.oldestSupported}`)
const expectedString = enterpriseServerReleases.isOldestReleaseDeprecated
? 'was discontinued'
: 'will be discontinued'
@ -164,24 +164,28 @@ describe('deprecation banner', () => {
describe('does not render survey prompt or contribution button', () => {
test('does not render survey prompt', async () => {
let $ = await getDOM(`/en/enterprise/${enterpriseServerReleases.latest}/github`)
expect($('[data-testid="survey-form"]').length).toBeGreaterThan(0)
$ = await getDOM(`/en/enterprise/${enterpriseServerReleases.oldestSupported}/github`)
const { $: $1 } = await getDOM(`/en/enterprise/${enterpriseServerReleases.latest}/github`)
expect($1('[data-testid="survey-form"]').length).toBeGreaterThan(0)
const { $: $2 } = await getDOM(
`/en/enterprise/${enterpriseServerReleases.oldestSupported}/github`,
)
if (enterpriseServerReleases.isOldestReleaseDeprecated) {
expect($('[data-testid="survey-form"]').length).toBe(0)
expect($2('[data-testid="survey-form"]').length).toBe(0)
} else {
expect($('[data-testid="survey-form"]').length).toBeGreaterThan(0)
expect($2('[data-testid="survey-form"]').length).toBeGreaterThan(0)
}
})
test('does not render contribution button', async () => {
let $ = await getDOM(`/en/enterprise/${enterpriseServerReleases.latest}/github`)
expect($('.contribution').length).toBeGreaterThan(0)
$ = await getDOM(`/en/enterprise/${enterpriseServerReleases.oldestSupported}/github`)
const { $: $1 } = await getDOM(`/en/enterprise/${enterpriseServerReleases.latest}/github`)
expect($1('.contribution').length).toBeGreaterThan(0)
const { $: $2 } = await getDOM(
`/en/enterprise/${enterpriseServerReleases.oldestSupported}/github`,
)
if (enterpriseServerReleases.isOldestReleaseDeprecated) {
expect($('.contribution').length).toBe(0)
expect($2('.contribution').length).toBe(0)
} else {
expect($('[data-testid=survey-form]').length).toBeGreaterThan(0)
expect($2('[data-testid=survey-form]').length).toBeGreaterThan(0)
}
})
})

Просмотреть файл

@ -5,7 +5,7 @@ import stripAnsi from 'strip-ansi'
import { visit } from 'unist-util-visit'
import { distance } from 'fastest-levenshtein'
import { getPathWithoutLanguage, getVersionStringFromPath } from '#src/frame/lib/path-utils.js'
import { getNewVersionedPath } from '#src/archives/lib/old-versions-utils.js'
import { getNewVersionedPath } from '#src/archives/lib/old-versions-utils.ts'
import patterns from '#src/frame/lib/patterns.js'
import { deprecated, latest } from '#src/versions/lib/enterprise-server-releases.js'
import nonEnterpriseDefaultVersion from '#src/versions/lib/non-enterprise-default-version.js'

Просмотреть файл

@ -1,6 +1,6 @@
import type { NextFunction, Request, Response } from 'express'
import helmet from 'helmet'
import { isArchivedVersion } from '@/archives/lib/is-archived-version.js'
import { isArchivedVersion } from '@/archives/lib/is-archived-version'
import versionSatisfiesRange from '@/versions/lib/version-satisfies-range.js'
import { languagePrefixPathRegex } from '@/languages/lib/languages.js'

Просмотреть файл

@ -14,7 +14,7 @@ import shortVersions from '@/versions/middleware/short-versions.js'
import contextualize from '@/frame/middleware/context/context'
import features from '@/versions/middleware/features.js'
import getRedirect from '@/redirects/lib/get-redirect.js'
import { isArchivedVersionByPath } from '@/archives/lib/is-archived-version.js'
import { isArchivedVersionByPath } from '@/archives/lib/is-archived-version'
import { readCompressedJsonFile } from '@/frame/lib/read-json-file.js'
const router = express.Router()

Просмотреть файл

@ -32,7 +32,7 @@ describeIfElasticsearchURL('search rendering page', () => {
// To see why this will work,
// see src/search/tests/fixtures/search-indexes/github-docs-dotcom-en-records.json
// which clearly has a record with the title "Foo"
const $ = await getDOM('/en/search?query=foo')
const { $ } = await getDOM('/en/search?query=foo')
expect($('h1').text()).toMatch(/\d+ Search results for "foo"/)
// Note it testid being 'search-result', not 'search-results'
@ -62,7 +62,7 @@ describeIfElasticsearchURL('search rendering page', () => {
})
test('debug search', async () => {
const $ = await getDOM('/en/search?query=foo&debug=1')
const { $ } = await getDOM('/en/search?query=foo&debug=1')
expect($('h1').text()).toMatch(/\d+ Search results for "foo"/)
// Note it testid being 'search-result', not 'search-results'
@ -74,21 +74,21 @@ describeIfElasticsearchURL('search rendering page', () => {
})
test('no query', async () => {
const $ = await getDOM('/en/search')
const { $ } = await getDOM('/en/search')
const results = $('[data-testid="search-result"]')
expect(results.length).toBe(0)
expect($('[data-testid="search-results"]').text()).toMatch('Enter a search term')
})
test('empty query', async () => {
const $ = await getDOM('/en/search?query=')
const { $ } = await getDOM('/en/search?query=')
const results = $('[data-testid="search-result"]')
expect(results.length).toBe(0)
expect($('[data-testid="search-results"]').text()).toMatch('Enter a search term')
})
test('find nothing', async () => {
const $ = await getDOM('/en/search?query=xojixjoiwejhfoiuwehjfioweufhj')
const { $ } = await getDOM('/en/search?query=xojixjoiwejhfoiuwehjfioweufhj')
const results = $('[data-testid="search-result"]')
expect(results.length).toBe(0)
expect($('[data-testid="search-results"]').text()).toMatch('0 Search results')
@ -100,7 +100,7 @@ describeIfElasticsearchURL('search rendering page', () => {
})
test('links per version in pathname', async () => {
const $ = await getDOM('/en/enterprise-cloud@latest/search?query=foo')
const { $ } = await getDOM('/en/enterprise-cloud@latest/search?query=foo')
expect($('[data-testid="search-results"]').text()).toMatch('Exclusively for GHEC')
// Note it testid being 'search-result', not 'search-results'
const results = $('[data-testid="search-result"]')
@ -115,14 +115,14 @@ describeIfElasticsearchURL('search rendering page', () => {
})
test('invalid parameters (page)', async () => {
const $ = await getDOM('/en/search?query=foo&page=999')
const { $ } = await getDOM('/en/search?query=foo&page=999')
expect($('[data-testid="search-results"]').text()).toMatch('Not a valid value (999)')
const results = $('[data-testid="search-result"]')
expect(results.length).toBe(0)
})
test('invalid parameters (size)', async () => {
const $ = await getDOM('/en/search?query=foo&size=888')
const { $ } = await getDOM('/en/search?query=foo&size=888')
expect($('[data-testid="search-results"]').text()).toMatch('Not a valid value (888)')
const results = $('[data-testid="search-result"]')
expect(results.length).toBe(0)
@ -134,14 +134,14 @@ describeIfElasticsearchURL('search rendering page', () => {
})
test('more than one search query', async () => {
const $ = await getDOM('/en/search?query=foo&query=bar')
const { $ } = await getDOM('/en/search?query=foo&query=bar')
expect($('[data-testid="search-results"]').text()).toMatch('Cannot have multiple values')
const results = $('[data-testid="search-result"]')
expect(results.length).toBe(0)
})
test("search with 'toplevel' query string", async () => {
const $ = await getDOM('/en/search?query=foo&toplevel=Baring')
const { $ } = await getDOM('/en/search?query=foo&toplevel=Baring')
expect($('h1').text()).toMatch(/\d+ Search results for "foo"/)
// Note it testid being 'search-result', not 'search-results'

Просмотреть файл

@ -5,7 +5,7 @@ describe('search results page', () => {
vi.setConfig({ testTimeout: 60 * 1000 })
test('says something if no query is provided', async (): Promise<void> => {
const $ = await getDOM('/en/search')
const { $ } = await getDOM('/en/search')
const $container = $('[data-testid="search-results"]')
expect($container.text()).toMatch(/Enter a search term/)
// Default is the frontmatter title of the content/search/index.md
@ -14,7 +14,7 @@ describe('search results page', () => {
test('says something if query is empty', async (): Promise<void> => {
const queryParams = new URLSearchParams({ query: ' ' }).toString()
const $ = await getDOM(`/en/search?${queryParams}`)
const { $ } = await getDOM(`/en/search?${queryParams}`)
const $container = $('[data-testid="search-results"]')
expect($container.text()).toMatch(/Enter a search term/)
})
@ -22,7 +22,7 @@ describe('search results page', () => {
test('mentions search term in h1', async (): Promise<void> => {
const searchTerm = 'peterbe'
const queryParams = new URLSearchParams({ query: searchTerm }).toString()
const $ = await getDOM(`/en/search?${queryParams}`)
const { $ } = await getDOM(`/en/search?${queryParams}`)
const $container = $('[data-testid="search-results"]')
const h1Text: string = $container.find('h1').text()

Просмотреть файл

@ -122,8 +122,8 @@ export async function getDOMCached(
): Promise<cheerio.Root> {
const key = `${route}::${JSON.stringify(options)}`
if (!getDOMCache.has(key)) {
const dom = await getDOM(route, options)
getDOMCache.set(key, dom)
const { $ } = await getDOM(route, options)
getDOMCache.set(key, $)
}
// The non-null assertion is safe here because we've just set the key if it didn't exist
return getDOMCache.get(key)!
@ -136,7 +136,10 @@ export async function getDOMCached(
* @param options - Options for fetching the DOM.
* @returns A promise that resolves to the loaded DOM object.
*/
export async function getDOM(route: string, options: GetDOMOptions = {}): Promise<cheerio.Root> {
export async function getDOM(
route: string,
options: GetDOMOptions = {},
): Promise<{ $: cheerio.Root; res: Response }> {
const { headers, allow500s = false, allow404 = false, retries = 0 } = options
const res = await get(route, { followRedirects: true, headers, retries })
@ -150,10 +153,7 @@ export async function getDOM(route: string, options: GetDOMOptions = {}): Promis
const $ = cheerio.load(res.body || '', { xmlMode: true })
// Extend the Cheerio instance with the response object
;($ as any).res = { ...res }
return $
return { $, res }
}
/**

Просмотреть файл

@ -18,7 +18,7 @@
* For every run found, it deletes its logs and its run.
*
* The total number of deletions is limited by the `MAX_DELETIONS`
* environment variable. The default is 100.
* environment variable. The default is 2000.
* */
import fs from 'fs'
@ -29,7 +29,7 @@ import { getOctokit } from '@actions/github'
main()
async function main() {
const DRY_RUN = Boolean(JSON.parse(process.env.DRY_RUN || 'false'))
const MAX_DELETIONS = parseInt(JSON.parse(process.env.MAX_DELETIONS || '100'))
const MAX_DELETIONS = parseInt(JSON.parse(process.env.MAX_DELETIONS || '2000'))
const MIN_AGE_DAYS = parseInt(process.env.MIN_AGE_DAYS || '90', 10)
const [owner, repo] = process.env.GITHUB_REPOSITORY.split('/')
@ -134,11 +134,11 @@ async function deleteWorkflowRuns(
owner,
repo,
workflow,
{ dryRun = false, minAgeDays = 100, maxDeletions = 1000 },
{ dryRun = false, minAgeDays = 90, maxDeletions = 2000 },
) {
// https://docs.github.com/en/search-github/getting-started-with-searching-on-github/understanding-the-search-syntax#query-for-dates
const minCreated = new Date(Date.now() - minAgeDays * 24 * 60 * 60 * 1000)
const minCreatedSearch = `<=${minCreated.toISOString().split('T')[0]}`
const minCreatedSearch = `<${minCreated.toISOString().split('T')[0]}`
// Delete is 10, but max is 100. But if we're only going to delete,
// 30, use 30. And if we're only going to delete 5, use the default
// per_page value of 10.