Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
bump: patch
---

### Changed

- On a lanes App merge-group run, ci-status lists main's tip once, with one unauthenticated `git ls-remote` of refs/heads/main and refs/tags/main, and passes it to the carry readers as `PR_MERGE_MAIN_TIP`. `onMain` then makes no `git/ref` read. It falls back to both REST reads when the listing failed or holds a tag named main. Each run is still compared with the tip.
- The merge-group hold-label step reads the queued pull request's labels in its existing GraphQL query in place of REST `issues/{n}/labels`. It fails closed on a cut or missing list.
1 change: 1 addition & 0 deletions .github/scripts/pr-merge/carry-run-cache.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@
// RUNNER_TEMP read: where the artifact is downloaded
// PR_MERGE_READS_PATH read: the job's shared GET answers
// (rotation.mjs sharedReads), if set
// PR_MERGE_MAIN_TIP read: main's tip (head-ref-map.mjs onMain), if set
// GITHUB_API_URL
import { appendFileSync, existsSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
import process from "node:process";
Expand Down
11 changes: 8 additions & 3 deletions .github/scripts/pr-merge/head-ref-map.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
// RUNNER_TEMP read: where the artifact is downloaded
// PR_MERGE_READS_PATH read: the job's shared GET answers
// (rotation.mjs sharedReads), if set
// PR_MERGE_MAIN_TIP read: main's tip from the job's ls-remote, if set
// GITHUB_API_URL
// HEAD_REFS_PATH the map read before the gate
// BABYSIT_RUN_CACHE write: this run's cache (babysit_run_cache.py)
Expand Down Expand Up @@ -86,17 +87,21 @@ function writeAtomic(path, text) {
// main's tip, read once per process by the first onMain call, and only while
// no tag is named main (a run on one reports head_branch main;
// check-trusted-trigger.mjs readDefaultBranchTip). A failed or refused read
// is remembered too, so every later run in the process is refused.
// is remembered too, so every later run in the process is refused. A job that
// listed refs/heads/main and refs/tags/main in one `git ls-remote` passes the
// tip as PR_MERGE_MAIN_TIP, set only when that listing held no tag named
// main, and then no process reads either ref; unset or not 40 hex, it is read.
let mainTip;
export function forgetMainTip() {
mainTip = undefined;
}

// Whether commit `sha` ran main's own code: that tip read held, and `sha` is
// main's tip or behind it (requireBehindTip). Any failed read is a no.
export async function onMain({ repo, actions, sha }) {
export async function onMain({ repo, actions, sha, env = process.env }) {
if (!SHA.test(sha ?? "")) return false;
mainTip ??= readDefaultBranchTip(actions, repo, "main", "off-main");
const listed = env.PR_MERGE_MAIN_TIP ?? "";
mainTip ??= SHA.test(listed) ? Promise.resolve(listed) : readDefaultBranchTip(actions, repo, "main", "off-main");
try {
await requireBehindTip(actions, repo, sha, await mainTip, "off-main");
return true;
Expand Down
23 changes: 23 additions & 0 deletions .github/scripts/pr-merge/head-ref-map.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -248,3 +248,26 @@ test("onMain reads the tag named main and main's tip once per process", async ()
// Still refused with a client that would answer 404: the refusal holds.
assert.equal(await onMain({ repo: REPO, actions, sha: sha(7) }), false);
});

// A tip the job listed with `git ls-remote` (PR_MERGE_MAIN_TIP) replaces both
// ref reads; each run is still compared with it, and a commit off main is
// still refused. A value that is not 40 hex is ignored and the refs are read.
test("onMain takes main's tip from PR_MERGE_MAIN_TIP without a ref read", async () => {
const calls = [];
const actions = async (method, url) => {
calls.push(url);
return mainCall(url, { offMain: [sha(9)] });
};
const env = { PR_MERGE_MAIN_TIP: "f".repeat(40) };
forgetMainTip();
assert.equal(await onMain({ repo: REPO, actions, sha: sha(7), env }), true);
assert.equal(await onMain({ repo: REPO, actions, sha: sha(9), env }), false);
assert.deepEqual(calls.filter((u) => u.includes("/git/ref/")), []);
assert.deepEqual(calls, [`/repos/${REPO}/compare/${sha(7)}...${"f".repeat(40)}`, `/repos/${REPO}/compare/${sha(9)}...${"f".repeat(40)}`]);
for (const bad of ["", "F".repeat(40), "f".repeat(39), `${"f".repeat(40)}\n`]) {
calls.length = 0;
forgetMainTip();
assert.equal(await onMain({ repo: REPO, actions, sha: sha(7), env: { PR_MERGE_MAIN_TIP: bad } }), true);
assert.equal(calls.filter((u) => u.includes("/git/ref/")).length, 2, JSON.stringify(bad));
}
});
1 change: 1 addition & 0 deletions .github/scripts/pr-merge/listing-carry.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
// RUNNER_TEMP where the artifact is downloaded
// PR_MERGE_READS_PATH the job's shared GET answers
// (rotation.mjs sharedReads), if set
// PR_MERGE_MAIN_TIP main's tip (head-ref-map.mjs onMain), if set
// BABYSIT_VERIFY_LISTING_CARRY the file to write
// GITHUB_API_URL
import { renameSync, rmSync, writeFileSync } from "node:fs";
Expand Down
85 changes: 59 additions & 26 deletions .github/workflows/pr-require-checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5740,11 +5740,13 @@ jobs:
# entry's label would eject an unlabeled pull request while the labeled
# one still merged. A label that lands after this step passed does not stop the merge;
# dequeue the pull request as well. An unparsable ref, an unreadable
# tracker config or an unreadable label list fails closed. For a pull
# tracker config or an unreadable or cut label list fails closed. The
# labels come live from one GraphQL read, which also answers the link.
# For a pull
# request an automation opened it then applies the trust hold to its
# linked issue (comment in the body). report-outcome's Hold: line covers
# the label at PR open and enable-auto-merge at arm time, so this holds a
# label added after either: one GraphQL read, a labels read for a Refs
# label added after either: that GraphQL read, a labels read for a Refs
# issue, and one timeline read per linked issue; an unreadable one, or
# the base's trusted-actor list unreadable, fails closed. Pinned by
# scripts/ci-hold-merge-group.test.sh, which runs this body.
Expand Down Expand Up @@ -5797,14 +5799,29 @@ jobs:
echo "::error::cannot read the human-gated role label from .work-item-tracker.json at $BASE_SHA with its merge-holds.mjs"
exit 1
}
for attempt in 1 2 3; do
if labels=$(gh_read "repos/$GITHUB_REPOSITORY/issues/$pr/labels?per_page=100" --paginate --jq '.[].name'); then
break
fi
[ "$attempt" -lt 3 ] || { echo "::error::could not read the labels of #$pr; re-run this run."; exit 1; }
sleep $((attempt * 5))
done
jq -Rsc 'split("\n") | map(select(length > 0))' <<<"$labels" >"$RUNNER_TEMP/queued-labels.json"
# read3 <gh api args>...: gh_read up to three times, 5 and 10 s apart.
read3() {
local attempt
for attempt in 1 2 3; do
gh_read "$@" && return 0
[ "$attempt" -lt 3 ] || return 1
sleep $((attempt * 5))
done
}
# One GraphQL read answers the pull request's live labels and, for
# the trust hold below, its author, body and closing issues. A label
# list GitHub cut (totalCount above the nodes) fails closed.
# shellcheck disable=SC2016 # GraphQL variables, not shell
link=$(read3 graphql -f query='query($o:String!,$r:String!,$n:Int!){repository(owner:$o,name:$r){pullRequest(number:$n){labels(first:100){totalCount nodes{name}} author{__typename ... on Bot{databaseId}} body closingIssuesReferences(first:10){totalCount nodes{number repository{nameWithOwner} labels(first:100){totalCount nodes{name}}}}}}}' \
-F o="${GITHUB_REPOSITORY%%/*}" -F r="${GITHUB_REPOSITORY#*/}" -F n="$pr") || {
echo "::error::could not read the labels and issue link of #$pr; re-run this run."
exit 1
}
jq -ec '.data.repository.pullRequest.labels | objects | select(.totalCount == (.nodes | length)) | [.nodes[].name]
| if all(.[]; type == "string") then . else error("a label name is not a string") end' <<<"$link" >"$RUNNER_TEMP/queued-labels.json" || {
echo "::error::the labels of #$pr could not all be read; a person merges it."
exit 1
}
if ! reasons=$(label_holds holds "$module" "$RUNNER_TEMP/queued-labels.json" "$pr") || [ -z "$reasons" ] ||
! failing=$(jq -r 'if type == "array" then .[] else error("not a list") end' <<<"$reasons"); then
echo "::error::merge-holds.mjs at $BASE_SHA could not judge the labels of #$pr; re-run this run."
Expand All @@ -5828,22 +5845,8 @@ jobs:
# must link exactly one issue here, as _linked_issue refuses
# otherwise, so a link dropped after arming fails; another
# automation's (Dependabot, standards-sync, a plugin release) that
# links none passes. A jq error fails closed.
# read3 <gh api args>...: gh_read up to three times, 5 and 10 s apart.
read3() {
local attempt
for attempt in 1 2 3; do
gh_read "$@" && return 0
[ "$attempt" -lt 3 ] || return 1
sleep $((attempt * 5))
done
}
# shellcheck disable=SC2016 # GraphQL variables, not shell
link=$(read3 graphql -f query='query($o:String!,$r:String!,$n:Int!){repository(owner:$o,name:$r){pullRequest(number:$n){author{__typename ... on Bot{databaseId}} body closingIssuesReferences(first:10){totalCount nodes{number repository{nameWithOwner} labels(first:100){totalCount nodes{name}}}}}}}' \
-F o="${GITHUB_REPOSITORY%%/*}" -F r="${GITHUB_REPOSITORY#*/}" -F n="$pr") || {
echo "::error::could not read the issue link of #$pr; re-run this run."
exit 1
}
# links none passes. A jq error fails closed. The link is the
# GraphQL answer the labels came from.
author=$(jq -er '.data.repository.pullRequest.author.__typename | strings' <<<"$link") || {
echo "::error::the author of #$pr could not be read; re-run this run."
exit 1
Expand Down Expand Up @@ -5969,6 +5972,34 @@ jobs:
echo "lanes=false" >>"$GITHUB_OUTPUT"
echo "#$pr is not the lanes App's; the carry steps are skipped."
fi
# MAIN'S TIP, LISTED ONCE FOR THE CARRY READERS. carry-run-cache.mjs,
# head-ref-map.mjs and listing-carry.mjs each run in their own process
# and take a run's carried reads only when its head commit is on main
# (head-ref-map.mjs onMain), which otherwise reads the tag named main (a
# 404 that no process shares) and main's tip over REST. One
# unauthenticated `git ls-remote` of this public repository, run outside
# any checkout, lists both refs; the tip is output only when the listing
# succeeded, names refs/heads/main once as 40 hex and holds no tag named
# main. Anything else outputs nothing, and onMain reads both refs as
# before. Each run is still compared with the tip.
- name: List main's tip for the carry readers
id: main_tip
if: ${{ !cancelled() && github.event_name == 'merge_group' && steps.queued_author.outputs.lanes != 'false' }}
continue-on-error: true
run: |
if ! refs=$(GIT_TERMINAL_PROMPT=0 timeout 60 git -C "$RUNNER_TEMP" ls-remote "https://github.com/$GITHUB_REPOSITORY" refs/heads/main refs/tags/main); then
echo "::notice::could not list main's refs; the carry readers read them over REST."
exit 0
fi
tip=$(awk '$2 == "refs/heads/main" { print $1 }' <<<"$refs")
if awk '$2 == "refs/tags/main" || $2 == "refs/tags/main^{}" { found = 1 } END { exit !found }' <<<"$refs"; then
echo "::notice::a tag is named main; the carry readers refuse every run."
elif [[ $tip =~ ^[0-9a-f]{40}$ ]]; then
echo "sha=$tip" >>"$GITHUB_OUTPUT"
echo "main's tip: $tip"
else
echo "::notice::main's tip was not listed as one commit SHA; the carry readers read it over REST."
fi
# THE MERGE QUEUE'S VERDICT READER STARTS FROM PR-MERGE'S CARRIED READS.
# The reads that never change (FAIL run records, FAIL head refs, run
# heads on main) that pr-merge's last scheduled or hand-started run kept,
Expand Down Expand Up @@ -5998,6 +6029,7 @@ jobs:
API_BUDGET_TOKEN: github-token
CARRY_TO: ${{ runner.temp }}/pr-merge-carried.json
PR_MERGE_READS_PATH: ${{ runner.temp }}/pr-merge-reads.json
PR_MERGE_MAIN_TIP: ${{ steps.main_tip.outputs.sha }}
run: node .base/.github/scripts/pr-merge/carry-run-cache.mjs read
- name: Start the run cache from them
id: verdict_reads_carry
Expand Down Expand Up @@ -6197,6 +6229,7 @@ jobs:
BABYSIT_VERIFY_HEAD_REFS: ${{ runner.temp }}/head-refs-in.json
HEAD_REFS_PATH: ${{ runner.temp }}/head-refs-in.json
PR_MERGE_READS_PATH: ${{ runner.temp }}/pr-merge-reads.json
PR_MERGE_MAIN_TIP: ${{ steps.main_tip.outputs.sha }}
BABYSIT_RUN_CACHE: ${{ runner.temp }}/babysit-run-cache.json
API_BUDGET_TOKEN: github-token
PR_NUMBER: ${{ github.event.pull_request.number }}
Expand Down
Loading
Loading