From 9972589953d283ef46c9b5d8eeb5d1b12db4e247 Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Wed, 8 Jul 2026 10:52:18 +0200 Subject: [PATCH 1/3] tools: prefilter commit queue metadata Run `git node metadata --readme` in the lightweight commit queue selector before starting the checkout-heavy landing job. The selector keeps `commit-queue` on PRs that are only waiting for approvals, TSC approval count, or wait time. Other metadata failures continue to the existing commit queue script so failure labels and comments are still produced by the current landing path. Fetch age-based, fast-track, and broader queue buckets before deduplicating the list so not-yet-ready PRs do not crowd out PRs that would otherwise have been selected. Signed-off-by: Filip Skokan --- .github/workflows/commit-queue.yml | 128 ++++++++++++++++++++--- doc/contributing/commit-queue.md | 160 +++++++++++++++++++---------- 2 files changed, 220 insertions(+), 68 deletions(-) diff --git a/.github/workflows/commit-queue.yml b/.github/workflows/commit-queue.yml index 7af712268711..6c6eb5bb751f 100644 --- a/.github/workflows/commit-queue.yml +++ b/.github/workflows/commit-queue.yml @@ -30,30 +30,128 @@ jobs: outputs: numbers: ${{ steps.get_mergeable_prs.outputs.numbers }} steps: - - name: Get Pull Requests - id: get_mergeable_prs + - name: Get Pull Request Candidates + id: get_candidate_prs run: | - prs=$(gh pr list \ + list_prs() { + gh pr list \ --repo "$GITHUB_REPOSITORY" \ --base "$GITHUB_REF_NAME" \ --label 'commit-queue' \ + "$@" \ --json 'number' \ - --search "created:<=$(date --date="2 days ago" +"%Y-%m-%dT%H:%M:%S%z") -label:blocked" \ -t '{{ range . }}{{ .number }} {{ end }}' \ - --limit 100) - fast_track_prs=$(gh pr list \ - --repo "$GITHUB_REPOSITORY" \ - --base "$GITHUB_REF_NAME" \ - --label 'commit-queue' \ + --limit 100 + } + aged_prs=$(list_prs \ + --search "created:<=$(date --date="2 days ago" +"%Y-%m-%dT%H:%M:%S%z") -label:blocked") + fast_track_prs=$(list_prs \ --label 'fast-track' \ - --search "-label:blocked" \ - --json 'number' \ - -t '{{ range . }}{{ .number }} {{ end }}' \ - --limit 100) - numbers=$(echo $prs' '$fast_track_prs | jq -r -s 'unique | join(" ")') - echo "numbers=$numbers" >> "$GITHUB_OUTPUT" + --search "-label:blocked") + queued_prs=$(list_prs \ + --search "-label:blocked") + candidates=$(printf '%s %s %s\n' "$aged_prs" "$fast_track_prs" "$queued_prs" | + jq -r -s 'reduce .[] as $pr ([]; if index($pr) then . else . + [$pr] end) | join(" ")') + echo "candidates=$candidates" >> "$GITHUB_OUTPUT" env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Install Node.js + if: steps.get_candidate_prs.outputs.candidates != '' + uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + with: + node-version: ${{ env.NODE_VERSION }} + - name: Install @node-core/utils + if: steps.get_candidate_prs.outputs.candidates != '' + run: npm install -g @node-core/utils + + - name: Filter Pull Requests + if: steps.get_candidate_prs.outputs.candidates != '' + id: get_mergeable_prs + run: | + fail_selector() { + echo "$1" + exit 1 + } + + repository="${GITHUB_REPOSITORY#*/}" + readme="${RUNNER_TEMP}/README.md" + readme_base64="${RUNNER_TEMP}/README.md.base64" + + ncu-config set branch "${GITHUB_REF_NAME}" + ncu-config set username "$USERNAME" + ncu-config set token "$GITHUB_TOKEN" + ncu-config set jenkins_token "$JENKINS_TOKEN" + ncu-config set repo "$repository" + ncu-config set owner "${GITHUB_REPOSITORY_OWNER}" + + if ! gh api "repos/${GITHUB_REPOSITORY}/contents/README.md?ref=${GITHUB_REF_NAME}" \ + --jq '.content' > "$readme_base64"; then + fail_selector "failed to download README.md" + fi + + if ! base64 -d < "$readme_base64" > "$readme"; then + fail_selector "failed to decode README.md" + fi + + numbers= + # shellcheck disable=SC2086 + for pr in $CANDIDATES; do + metadata="${RUNNER_TEMP}/metadata-${pr}.json" + output="${RUNNER_TEMP}/metadata-${pr}.txt" + if git node metadata "$pr" \ + --owner "$GITHUB_REPOSITORY_OWNER" \ + --repo "$repository" \ + --readme "$readme" \ + --json > "$metadata" 2> "$output"; then + metadata_status=0 + else + metadata_status=$? + fi + + if [ -s "$output" ]; then + cat "$output" + fi + + case "$metadata_status" in + 0|2[0-9]|4[0-9]) ;; + *) + fail_selector "git node metadata failed for pr ${pr} with exit code ${metadata_status}" + ;; + esac + + metadata_exit_code=$(jq -r '.exitCode' "$metadata") || + fail_selector "failed to parse metadata JSON for pr ${pr}" + if [ "$metadata_exit_code" != "$metadata_status" ]; then + fail_selector "metadata JSON exitCode mismatch for pr ${pr}" + fi + metadata_reason_codes=$(jq -r '.reasonCodes | join(", ")' "$metadata") || + fail_selector "failed to parse metadata reason codes for pr ${pr}" + + if [ "$metadata_status" -eq 0 ]; then + echo "pr ${pr} is ready for the commit queue" + numbers="$numbers $pr" + continue + fi + + if [ "$metadata_status" -ge 20 ] && [ "$metadata_status" -le 29 ]; then + echo "pr ${pr} skipped, not ready to land" + echo "reason codes: ${metadata_reason_codes}" + continue + fi + + echo "pr ${pr} will be handled by the commit queue" + echo "reason codes: ${metadata_reason_codes}" + numbers="$numbers $pr" + done + + numbers=$(echo "$numbers" | xargs) + echo "numbers=$numbers" >> "$GITHUB_OUTPUT" + env: + CANDIDATES: ${{ steps.get_candidate_prs.outputs.candidates }} + USERNAME: ${{ secrets.JENKINS_USER }} + GITHUB_TOKEN: ${{ secrets.GH_USER_TOKEN }} + JENKINS_TOKEN: ${{ secrets.JENKINS_TOKEN }} commitQueue: needs: get_mergeable_prs if: needs.get_mergeable_prs.outputs.numbers != '' diff --git a/doc/contributing/commit-queue.md b/doc/contributing/commit-queue.md index 6cedc833029e..a0b9be76caaf 100644 --- a/doc/contributing/commit-queue.md +++ b/doc/contributing/commit-queue.md @@ -1,36 +1,48 @@ # Commit queue -_tl;dr: You can land pull requests by adding the `commit-queue` label to it._ +_tl;dr: You can ask the queue to land pull requests by adding the +`commit-queue` label to them._ Commit Queue is a feature for the project which simplifies the landing process by automating it via GitHub Actions. With it, collaborators can -land pull requests by adding the `commit-queue` label to a PR. All -checks will run via `@node-core/utils`, and if the pull request is ready to -land, the Action will rebase it and push to `main`. +queue pull requests for landing by adding the `commit-queue` label to a PR. The +selector checks readiness with `@node-core/utils`. If the pull request is only +blocked on a deferrable condition, currently wait time, the queue leaves the +label in place and retries later. Other failures continue to the existing +landing and failure-reporting path. This document gives an overview of how the Commit Queue works, as well as implementation details, reasoning for design choices, and current limitations. ## Overview -From a high-level, the Commit Queue works as follow: - -1. Collaborators will add `commit-queue` label to pull requests ready to land -2. Every five minutes the queue will do the following for each mergeable pull request - with the label: - 1. Check if the PR also has a `request-ci` label (if it has, skip this PR +From a high-level, the Commit Queue works as follows: + +1. Collaborators will add `commit-queue` label to pull requests they want the + queue to land. The label can be added before the pull request has completed + its wait time, or before requested CI has finished. Required approvals must + already be in place. The commit queue does not request CI on its own. +2. On each scheduled run, the queue builds a candidate list from open pull + requests with the `commit-queue` label and without the `blocked` label. The + workflow uses a five-minute cron, but GitHub Actions scheduled workflows are + not guaranteed to run exactly every five minutes. For each candidate, the + queue will: + 1. Run a metadata-only readiness check that uses `@node-core/utils` without + checking out the repository + 2. If the metadata check exits with a deferrable readiness code, meaning + the PR is only blocked on wait time, keep the `commit-queue` label and + skip this PR until a later queue run + 3. Check if the PR also has a `request-ci` label (if it has, skip this PR since it's pending a CI run) - 2. Check if the last Jenkins CI is finished running (if it is not, skip this - PR) - 3. Remove the `commit-queue` label - 4. Run `git node land --oneCommitMax` - 5. If it fails: - 1. Abort `git node land` session - 2. Add `commit-queue-failed` label to the PR - 3. Leave a comment on the PR with the output from `git node land` - 4. Skip next steps, go to next PR in the queue - 6. If it succeeds: - 1. Push the changes to nodejs/node + 4. Check whether GitHub checks are still running (if they are, skip this PR) + 5. Remove the `commit-queue` label and run `git node land` + 6. If it fails: + 1. Add the `commit-queue-failed` label to the PR + 2. Leave a comment on the PR with the output from `git node land` + 3. Abort the `git node land` session. If the abort succeeds, continue to + the next PR; otherwise, stop the queue in an unknown state + 7. If it succeeds: + 1. Push or merge the changes into nodejs/node 2. Leave a comment on the PR with `Landed in ...` 3. Close the PR 4. Go to next PR in the queue @@ -51,7 +63,7 @@ of the commit queue: guidelines or be a valid [`fixup!`](https://git-scm.com/docs/git-commit#Documentation/git-commit.txt---fixupamendrewordltcommitgt) commit that will be correctly handled by the [`--autosquash`](https://git-scm.com/docs/git-rebase#Documentation/git-rebase.txt---autosquash) option -2. A CI must've ran and succeeded since the last change on the PR +2. A CI must have run and succeeded since the last change on the PR 3. A collaborator must have approved the PR since the last change 4. Only Jenkins CI and GitHub Actions are checked (V8 CI and CITGM are ignored) 5. The PR must target the `main` branch (PRs opened against other branches, such @@ -59,10 +71,15 @@ of the commit queue: ## Implementation -The [action](../../.github/workflows/commit-queue.yml) will run on scheduler -events every five minutes. Five minutes is the smallest number accepted by -the scheduler. The scheduler is not guaranteed to run every five minutes, it -might take longer between runs. +The [action](../../.github/workflows/commit-queue.yml) runs on scheduled events. +It uses a five-minute cron because that is the smallest interval accepted by +GitHub Actions. Scheduled workflows are not guaranteed to run exactly at that +cadence and might take longer between runs. + +The workflow also uses a concurrency group so only one commit queue run can be +active at a time. If a scheduled run starts while a previous run is still +running, GitHub Actions keeps at most one pending run for the same concurrency +group. A newer pending run replaces an older pending run. Using the scheduler is preferable over using pull\_request\_target for two reasons: @@ -76,41 +93,78 @@ reasons: commit, meaning we wouldn't be able to use it for already opened PRs without rebasing them first. -`@node-core/utils` is configured with a personal token and -a Jenkins token from -[@nodejs-github-bot](https://github.com/nodejs/github-bot). -`octokit/graphql-action` is used to fetch all pull requests with the -`commit-queue` label. The output is a JSON payload, so `jq` is used to turn -that into a list of PR ids we can pass as arguments to -[`commit-queue.sh`](../../tools/actions/commit-queue.sh). - -> The personal token only needs permission for public repositories and to read -> profiles, we can use the GITHUB\_TOKEN for write operations. Jenkins token is +`@node-core/utils` is configured with a personal token and a Jenkins token from +[@nodejs-github-bot](https://github.com/nodejs/github-bot). The workflow starts +with a small selector step that uses GitHub CLI to fetch pull requests with the +`commit-queue` label. It first fetches the same age-based and fast-track buckets +the queue used before accepting early queue requests, then fetches the broader +queue and de-duplicates the result. This keeps not-yet-ready PRs from crowding +out PRs that the previous query would have selected if GitHub paginates or caps +a query result. + +If there are candidate PRs, the selector installs `@node-core/utils`, downloads +the target branch's README without checking out the repository, and runs +`git node metadata --readme --json` for each candidate. This uses the same +`@node-core/utils` PR readiness checks as `git node land`, but does not clone, +fetch, or merge the PR. The selector consumes the structured metadata result +and its exit code instead of matching human-readable output: + +* exit code `0`: the PR is ready and is passed to + [`commit-queue.sh`](../../tools/actions/commit-queue.sh) +* exit codes `20`-`29`: the PR is not ready for a deferrable metadata reason, + currently wait time, so it keeps the `commit-queue` label and is retried + later +* exit codes `40`-`49`: the PR has a hard or mixed metadata readiness failure + and is passed to [`commit-queue.sh`](../../tools/actions/commit-queue.sh) + +The `20`-`29` exit code range is reserved by `@node-core/utils` for deferrable +metadata readiness states, and `40`-`49` is reserved for hard metadata failure +states. Unknown selector failures fail the workflow before starting the landing +job and leave PR labels unchanged so the queue can retry on a later scheduled +run. PRs passed through with exit code `40`-`49` continue through +`commit-queue.sh`. The script still applies its existing `request-ci` and +pending-check deferrals before removing the queue label and reporting a hard +failure. + +> The personal token needs permission for public repositories and to read +> profiles. It is used by `@node-core/utils` and by the landing job for +> checkout, label and comment updates, merging, and pushing. Jenkins token is > required to check CI status. `commit-queue.sh` receives the following positional arguments: 1. The repository owner 2. The repository name -3. The Action GITHUB\_TOKEN -4. Every positional argument starting at this one will be a pull request ID of +3. Every positional argument starting at this one will be a pull request ID of a pull request with commit-queue set. -The script will iterate over the pull requests. `ncu-ci` is used to check if -the last CI is still pending, and calls to the GitHub API are used to check if -the PR is waiting for CI to start (`request-ci` label). The PR is skipped if CI -is pending. No other CI validation is done here since `git node land` will fail -if the last CI failed. - -The script removes the `commit-queue` label. It then runs `git node land`, -forwarding stdout and stderr to a file. If any errors happen, -`git node land --abort` is run, and then a `commit-queue-failed` label is added -to the PR, as well as a comment with the output of `git node land`. - -If no errors happen during `git node land`, the script will use the -`GITHUB_TOKEN` to push the changes to `main`, and then will leave a -`Landed in ...` comment in the PR, and then will close it. Iteration continues -until all PRs have done the steps above. +The script will iterate over the pull requests. GitHub CLI is used to check if +the PR is waiting for CI to start (`request-ci` label) or still has pending +GitHub checks. The PR is skipped if CI is pending. No other CI validation is +done here since `git node land` will fail if the last CI failed. + +The script removes the `commit-queue` label, then runs `git node land`, +forwarding stdout and stderr to a file. PRs that are only blocked on wait time +should have already been filtered by the metadata check. If a hard readiness +failure appears between the selector job and `git node land`, the landing job +adds a `commit-queue-failed` label to the PR, leaves a comment with the output +of `git node land`, and then aborts the landing session. If the abort fails, +the queue stops instead of continuing in an unknown state. + +Fast-tracked PRs use the metadata check before the landing job. If the +fast-track request has not yet received enough collaborator thumbs-up, the queue +keeps the `commit-queue` label and retries until either the fast-track request +is approved or the PR becomes landable through the regular wait-time rules. The +commit queue does not create the fast-track request comment; that is handled +when the `fast-track` label is added. If that comment is missing, the queue +reports the failure instead of keeping the PR queued. + +If no errors happen during `git node land`, the script either pushes the direct +rebase landing to `main` or uses GitHub's squash merge API for single-commit and +fixup landings. It then leaves a `Landed in ...` comment in the PR. GitHub +closes PRs merged through the merge API automatically; for direct pushes, the +script closes the PR. Iteration continues until all PRs have done the steps +above. ## Reverting broken commits From 267548668a98a85db03ae32a6806e2bc2b840ba8 Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Fri, 7 Aug 2026 20:58:34 +0200 Subject: [PATCH 2/3] fixup! tools: prefilter commit queue metadata --- .github/workflows/commit-queue.yml | 82 ++++++++++++------------------ doc/contributing/commit-queue.md | 65 +++++++++++------------ 2 files changed, 65 insertions(+), 82 deletions(-) diff --git a/.github/workflows/commit-queue.yml b/.github/workflows/commit-queue.yml index 6c6eb5bb751f..4c2955b1e77b 100644 --- a/.github/workflows/commit-queue.yml +++ b/.github/workflows/commit-queue.yml @@ -22,13 +22,13 @@ permissions: contents: read jobs: - get_mergeable_prs: + get_candidate_prs: permissions: pull-requests: read if: github.repository == 'nodejs/node' runs-on: ubuntu-slim outputs: - numbers: ${{ steps.get_mergeable_prs.outputs.numbers }} + candidates: ${{ steps.get_candidate_prs.outputs.candidates }} steps: - name: Get Pull Request Candidates id: get_candidate_prs @@ -55,18 +55,38 @@ jobs: echo "candidates=$candidates" >> "$GITHUB_OUTPUT" env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - + commitQueue: + needs: get_candidate_prs + if: needs.get_candidate_prs.outputs.candidates != '' + runs-on: ubuntu-slim + steps: + # Install dependencies - name: Install Node.js - if: steps.get_candidate_prs.outputs.candidates != '' uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: node-version: ${{ env.NODE_VERSION }} - name: Install @node-core/utils - if: steps.get_candidate_prs.outputs.candidates != '' run: npm install -g @node-core/utils + - name: Set variables + run: | + echo "REPOSITORY=$(echo "$GITHUB_REPOSITORY" | cut -d/ -f2)" >> "$GITHUB_ENV" + + - name: Configure @node-core/utils + run: | + ncu-config set branch "${GITHUB_REF_NAME}" + ncu-config set upstream origin + ncu-config set username "$USERNAME" + ncu-config set token "$GITHUB_TOKEN" + ncu-config set jenkins_token "$JENKINS_TOKEN" + ncu-config set repo "${REPOSITORY}" + ncu-config set owner "${GITHUB_REPOSITORY_OWNER}" + env: + USERNAME: ${{ secrets.JENKINS_USER }} + GITHUB_TOKEN: ${{ secrets.GH_USER_TOKEN }} + JENKINS_TOKEN: ${{ secrets.JENKINS_TOKEN }} + - name: Filter Pull Requests - if: steps.get_candidate_prs.outputs.candidates != '' id: get_mergeable_prs run: | fail_selector() { @@ -74,17 +94,9 @@ jobs: exit 1 } - repository="${GITHUB_REPOSITORY#*/}" readme="${RUNNER_TEMP}/README.md" readme_base64="${RUNNER_TEMP}/README.md.base64" - ncu-config set branch "${GITHUB_REF_NAME}" - ncu-config set username "$USERNAME" - ncu-config set token "$GITHUB_TOKEN" - ncu-config set jenkins_token "$JENKINS_TOKEN" - ncu-config set repo "$repository" - ncu-config set owner "${GITHUB_REPOSITORY_OWNER}" - if ! gh api "repos/${GITHUB_REPOSITORY}/contents/README.md?ref=${GITHUB_REF_NAME}" \ --jq '.content' > "$readme_base64"; then fail_selector "failed to download README.md" @@ -101,7 +113,7 @@ jobs: output="${RUNNER_TEMP}/metadata-${pr}.txt" if git node metadata "$pr" \ --owner "$GITHUB_REPOSITORY_OWNER" \ - --repo "$repository" \ + --repo "$REPOSITORY" \ --readme "$readme" \ --json > "$metadata" 2> "$output"; then metadata_status=0 @@ -148,16 +160,11 @@ jobs: numbers=$(echo "$numbers" | xargs) echo "numbers=$numbers" >> "$GITHUB_OUTPUT" env: - CANDIDATES: ${{ steps.get_candidate_prs.outputs.candidates }} - USERNAME: ${{ secrets.JENKINS_USER }} + CANDIDATES: ${{ needs.get_candidate_prs.outputs.candidates }} GITHUB_TOKEN: ${{ secrets.GH_USER_TOKEN }} - JENKINS_TOKEN: ${{ secrets.JENKINS_TOKEN }} - commitQueue: - needs: get_mergeable_prs - if: needs.get_mergeable_prs.outputs.numbers != '' - runs-on: ubuntu-slim - steps: + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + if: steps.get_mergeable_prs.outputs.numbers != '' with: # A personal token is required because pushing with GITHUB_TOKEN will # prevent commits from running CI after they land. It needs @@ -165,33 +172,8 @@ jobs: # for push as well. token: ${{ secrets.GH_USER_TOKEN }} - # Install dependencies - - name: Install Node.js - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 - with: - node-version: ${{ env.NODE_VERSION }} - - name: Install @node-core/utils - run: npm install -g @node-core/utils - - - name: Set variables - run: | - echo "REPOSITORY=$(echo "$GITHUB_REPOSITORY" | cut -d/ -f2)" >> "$GITHUB_ENV" - - - name: Configure @node-core/utils - run: | - ncu-config set branch "${GITHUB_REF_NAME}" - ncu-config set upstream origin - ncu-config set username "$USERNAME" - ncu-config set token "$GITHUB_TOKEN" - ncu-config set jenkins_token "$JENKINS_TOKEN" - ncu-config set repo "${REPOSITORY}" - ncu-config set owner "${GITHUB_REPOSITORY_OWNER}" - env: - USERNAME: ${{ secrets.JENKINS_USER }} - GITHUB_TOKEN: ${{ secrets.GH_USER_TOKEN }} - JENKINS_TOKEN: ${{ secrets.JENKINS_TOKEN }} - - name: Start the Commit Queue - run: ./tools/actions/commit-queue.sh "${GITHUB_REPOSITORY_OWNER}" "${REPOSITORY}" ${{ needs.get_mergeable_prs.outputs.numbers }} + if: steps.get_mergeable_prs.outputs.numbers != '' + run: ./tools/actions/commit-queue.sh "${GITHUB_REPOSITORY_OWNER}" "${REPOSITORY}" ${{ steps.get_mergeable_prs.outputs.numbers }} env: GITHUB_TOKEN: ${{ secrets.GH_USER_TOKEN }} diff --git a/doc/contributing/commit-queue.md b/doc/contributing/commit-queue.md index a0b9be76caaf..835b1268e420 100644 --- a/doc/contributing/commit-queue.md +++ b/doc/contributing/commit-queue.md @@ -27,8 +27,8 @@ From a high-level, the Commit Queue works as follows: workflow uses a five-minute cron, but GitHub Actions scheduled workflows are not guaranteed to run exactly every five minutes. For each candidate, the queue will: - 1. Run a metadata-only readiness check that uses `@node-core/utils` without - checking out the repository + 1. In the landing job, install and configure `@node-core/utils`, then run a + metadata-only readiness check without checking out the repository 2. If the metadata check exits with a deferrable readiness code, meaning the PR is only blocked on wait time, keep the `commit-queue` label and skip this PR until a later queue run @@ -93,20 +93,20 @@ reasons: commit, meaning we wouldn't be able to use it for already opened PRs without rebasing them first. -`@node-core/utils` is configured with a personal token and a Jenkins token from -[@nodejs-github-bot](https://github.com/nodejs/github-bot). The workflow starts -with a small selector step that uses GitHub CLI to fetch pull requests with the -`commit-queue` label. It first fetches the same age-based and fast-track buckets -the queue used before accepting early queue requests, then fetches the broader -queue and de-duplicates the result. This keeps not-yet-ready PRs from crowding -out PRs that the previous query would have selected if GitHub paginates or caps -a query result. - -If there are candidate PRs, the selector installs `@node-core/utils`, downloads -the target branch's README without checking out the repository, and runs +The workflow starts with a small candidate job that uses GitHub CLI to fetch +pull requests with the `commit-queue` label. It first fetches the same +age-based and fast-track buckets the queue used before accepting early queue +requests, then fetches the broader queue and de-duplicates the result. This +keeps not-yet-ready PRs from crowding out PRs that the previous query would +have selected if GitHub paginates or caps a query result. + +If there are candidate PRs, the landing job installs and configures +`@node-core/utils` once with a personal token and a Jenkins token from +[@nodejs-github-bot](https://github.com/nodejs/github-bot). It then downloads +the target branch's README without checking out the repository and runs `git node metadata --readme --json` for each candidate. This uses the same `@node-core/utils` PR readiness checks as `git node land`, but does not clone, -fetch, or merge the PR. The selector consumes the structured metadata result +fetch, or merge the PR. The filter consumes the structured metadata result and its exit code instead of matching human-readable output: * exit code `0`: the PR is ready and is passed to @@ -119,12 +119,13 @@ and its exit code instead of matching human-readable output: The `20`-`29` exit code range is reserved by `@node-core/utils` for deferrable metadata readiness states, and `40`-`49` is reserved for hard metadata failure -states. Unknown selector failures fail the workflow before starting the landing -job and leave PR labels unchanged so the queue can retry on a later scheduled -run. PRs passed through with exit code `40`-`49` continue through -`commit-queue.sh`. The script still applies its existing `request-ci` and -pending-check deferrals before removing the queue label and reporting a hard -failure. +states. Unknown filter failures fail the workflow before starting the landing +script and leave PR labels unchanged so the queue can retry on a later +scheduled run. PRs passed through with exit code `40`-`49` continue through +`commit-queue.sh`. The workflow checks out the repository only when at least +one PR remains after filtering. The script still applies its existing +`request-ci` and pending-check deferrals before removing the queue label and +reporting a hard failure. > The personal token needs permission for public repositories and to read > profiles. It is used by `@node-core/utils` and by the landing job for @@ -146,18 +147,18 @@ done here since `git node land` will fail if the last CI failed. The script removes the `commit-queue` label, then runs `git node land`, forwarding stdout and stderr to a file. PRs that are only blocked on wait time should have already been filtered by the metadata check. If a hard readiness -failure appears between the selector job and `git node land`, the landing job -adds a `commit-queue-failed` label to the PR, leaves a comment with the output -of `git node land`, and then aborts the landing session. If the abort fails, -the queue stops instead of continuing in an unknown state. - -Fast-tracked PRs use the metadata check before the landing job. If the -fast-track request has not yet received enough collaborator thumbs-up, the queue -keeps the `commit-queue` label and retries until either the fast-track request -is approved or the PR becomes landable through the regular wait-time rules. The -commit queue does not create the fast-track request comment; that is handled -when the `fast-track` label is added. If that comment is missing, the queue -reports the failure instead of keeping the PR queued. +failure appears between the metadata filter and `git node land`, the landing +job adds a `commit-queue-failed` label to the PR, leaves a comment with the +output of `git node land`, and then aborts the landing session. If the abort +fails, the queue stops instead of continuing in an unknown state. + +Fast-tracked PRs use the metadata check before checkout and the landing script. +If the fast-track request has not yet received enough collaborator thumbs-up, +the queue keeps the `commit-queue` label and retries until either the +fast-track request is approved or the PR becomes landable through the regular +wait-time rules. The commit queue does not create the fast-track request +comment; that is handled when the `fast-track` label is added. If that comment +is missing, the queue reports the failure instead of keeping the PR queued. If no errors happen during `git node land`, the script either pushes the direct rebase landing to `main` or uses GitHub's squash merge API for single-commit and From b41f4c27049cdb176a5c9bb2ba56b3ec457c1446 Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Fri, 7 Aug 2026 22:05:27 +0200 Subject: [PATCH 3/3] fixup! tools: prefilter commit queue metadata --- .github/workflows/commit-queue.yml | 34 ++++++++++++------------------ doc/contributing/commit-queue.md | 2 +- 2 files changed, 14 insertions(+), 22 deletions(-) diff --git a/.github/workflows/commit-queue.yml b/.github/workflows/commit-queue.yml index 4c2955b1e77b..73c6e6bdee86 100644 --- a/.github/workflows/commit-queue.yml +++ b/.github/workflows/commit-queue.yml @@ -89,22 +89,8 @@ jobs: - name: Filter Pull Requests id: get_mergeable_prs run: | - fail_selector() { - echo "$1" - exit 1 - } - readme="${RUNNER_TEMP}/README.md" - readme_base64="${RUNNER_TEMP}/README.md.base64" - - if ! gh api "repos/${GITHUB_REPOSITORY}/contents/README.md?ref=${GITHUB_REF_NAME}" \ - --jq '.content' > "$readme_base64"; then - fail_selector "failed to download README.md" - fi - - if ! base64 -d < "$readme_base64" > "$readme"; then - fail_selector "failed to decode README.md" - fi + curl -fsSLo "$readme" "https://github.com/${GITHUB_REPOSITORY}/raw/${GITHUB_SHA}/README.md" numbers= # shellcheck disable=SC2086 @@ -128,17 +114,23 @@ jobs: case "$metadata_status" in 0|2[0-9]|4[0-9]) ;; *) - fail_selector "git node metadata failed for pr ${pr} with exit code ${metadata_status}" + echo "git node metadata failed for pr ${pr} with exit code ${metadata_status}" + exit 1 ;; esac - metadata_exit_code=$(jq -r '.exitCode' "$metadata") || - fail_selector "failed to parse metadata JSON for pr ${pr}" + metadata_exit_code=$(jq -r '.exitCode' "$metadata") || { + echo "failed to parse metadata JSON for pr ${pr}" + exit 1 + } if [ "$metadata_exit_code" != "$metadata_status" ]; then - fail_selector "metadata JSON exitCode mismatch for pr ${pr}" + echo "metadata JSON exitCode mismatch for pr ${pr}" + exit 1 fi - metadata_reason_codes=$(jq -r '.reasonCodes | join(", ")' "$metadata") || - fail_selector "failed to parse metadata reason codes for pr ${pr}" + metadata_reason_codes=$(jq -r '.reasonCodes | join(", ")' "$metadata") || { + echo "failed to parse metadata reason codes for pr ${pr}" + exit 1 + } if [ "$metadata_status" -eq 0 ]; then echo "pr ${pr} is ready for the commit queue" diff --git a/doc/contributing/commit-queue.md b/doc/contributing/commit-queue.md index 835b1268e420..08f01b32d4ee 100644 --- a/doc/contributing/commit-queue.md +++ b/doc/contributing/commit-queue.md @@ -103,7 +103,7 @@ have selected if GitHub paginates or caps a query result. If there are candidate PRs, the landing job installs and configures `@node-core/utils` once with a personal token and a Jenkins token from [@nodejs-github-bot](https://github.com/nodejs/github-bot). It then downloads -the target branch's README without checking out the repository and runs +the workflow commit's README without checking out the repository and runs `git node metadata --readme --json` for each candidate. This uses the same `@node-core/utils` PR readiness checks as `git node land`, but does not clone, fetch, or merge the PR. The filter consumes the structured metadata result