From c7eee1a7558d356b0ca16d6eac6f54dfeb6bd38b Mon Sep 17 00:00:00 2001 From: Michael Daniels Date: Mon, 2 Feb 2026 17:52:04 -0500 Subject: [PATCH] ci/github-script/lint-commits: support PRs with over 250 commits We want to be able to check python-updates. --- .github/workflows/lint.yml | 5 +- ci/github-script/lint-commits.js | 94 +++++++++++++++++++++++++++++--- 2 files changed, 88 insertions(+), 11 deletions(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 4d31b761dd3b..0941cedf98b5 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -136,10 +136,8 @@ jobs: steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: - persist-credentials: false + persist-credentials: true # Needed to run git fetch for large PRs. path: trusted - sparse-checkout: | - ci/github-script - name: Check commit messages uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 with: @@ -150,4 +148,5 @@ jobs: github, context, core, + repoPath: 'trusted', }) diff --git a/ci/github-script/lint-commits.js b/ci/github-script/lint-commits.js index 5237b41845e1..4382300360af 100644 --- a/ci/github-script/lint-commits.js +++ b/ci/github-script/lint-commits.js @@ -1,14 +1,37 @@ // @ts-check const { classify } = require('../supportedBranches.js') +const { promisify } = require('node:util') +const execFile = promisify(require('node:child_process').execFile) + +/** + * @param {{ + * args: string[] + * core: import('@actions/core'), + * quiet?: boolean, + * repoPath?: string, + * }} RunGitProps + */ +async function runGit({ args, repoPath, core, quiet }) { + if (repoPath) { + args = ['-C', repoPath, ...args] + } + + if (!quiet) { + core.info(`About to run \`git ${args.map((s) => `'${s}'`).join(' ')}\``) + } + + return await execFile('git', args) +} /** * @param {{ * github: InstanceType, - * context: import('@actions/github/lib/context').Context - * core: import('@actions/core') + * context: import('@actions/github/lib/context').Context, + * core: import('@actions/core'), + * repoPath?: string, * }} CheckCommitMessagesProps */ -async function checkCommitMessages({ github, context, core }) { +async function checkCommitMessages({ github, context, core, repoPath }) { // This check should only be run when we have the pull_request context. const pull_number = context.payload.pull_request?.number if (!pull_number) { @@ -42,15 +65,70 @@ async function checkCommitMessages({ github, context, core }) { return } - const commits = await github.paginate(github.rest.pulls.listCommits, { - ...context.repo, - pull_number, - }) + /** + * GitHub's API will return a maximum of 250 commits. + * We will use it if we can, but fall back to using git locally. + * This type is used to abstract over the differences between the two. + * @type {{ + * message: string, + * sha: string, + * }[]} + */ + let commits + + if (pr.commits < 250) { + commits = ( + await github.paginate(github.rest.pulls.listCommits, { + ...context.repo, + pull_number, + }) + ).map((commit) => ({ message: commit.commit.message, sha: commit.sha })) + } else { + await runGit({ + args: ['fetch', `--depth=1`, 'origin', pr.base.sha], + repoPath, + core, + }) + await runGit({ + args: ['fetch', `--depth=${pr.commits + 1}`, 'origin', pr.head.sha], + repoPath, + core, + }) + + const shas = ( + await runGit({ + args: [ + 'rev-list', + `--max-count=${pr.commits}`, + `${pr.base.sha}..${pr.head.sha}`, + ], + repoPath, + core, + }) + ).stdout + .split('\n') + .map((s) => s.trim()) + .filter(Boolean) + + commits = await Promise.all( + shas.map(async (sha) => ({ + sha, + message: ( + await runGit({ + args: ['log', '--format=%s', '-1', sha], + repoPath, + core, + quiet: true, + }) + ).stdout, + })), + ) + } const failures = new Set() for (const commit of commits) { - const message = commit.commit.message + const message = commit.message const firstLine = message.split('\n')[0] const logMsgStart = `Commit ${commit.sha}'s message's subject ("${firstLine}")`