From b15c692d9fcbecc3f8c6872d002276a7cc344006 Mon Sep 17 00:00:00 2001 From: Kyle Sexton <153232337+kyle-sexton@users.noreply.github.com> Date: Wed, 4 Nov 2026 01:24:44 -0500 Subject: [PATCH 1/5] fix(ci): report why resolve-conflicts-select could not read the pull request The Read the PR head step swallowed a failed `gh api` with `|| true`, so any read failure reported "the PR names no head SHA". Run 41775375216 (#13027) failed that way on `invalid character 'b' looking for beginning of value`, a non-JSON body from the API that the step never showed. The step now keeps the read's stderr and fails with its text and class: transient (5xx, timeout, connection error, non-JSON body), rate-limit (the api-budget wrapper's refusal included) or other. A transient failure is read once more after 5 to 10 s; a rate limit and any other failure are not read again. A read that succeeds with no head SHA still reports as before. Co-Authored-By: Claude Opus 5.5 --- .../pr-unblock/workflow-gates.test.mjs | 80 +++++++++++++++++++ .github/workflows/pr-unblock.yml | 28 ++++++- 2 files changed, 107 insertions(+), 1 deletion(-) diff --git a/.github/scripts/pr-unblock/workflow-gates.test.mjs b/.github/scripts/pr-unblock/workflow-gates.test.mjs index f43b2a07749..9316f21a254 100644 --- a/.github/scripts/pr-unblock/workflow-gates.test.mjs +++ b/.github/scripts/pr-unblock/workflow-gates.test.mjs @@ -1223,6 +1223,86 @@ for (const [job, written] of [ } }); +// A failed read of the PR reports its error text and class. The stub gh answers +// from a script of one spec per call (ok, empty, or fail:); the stub +// sleep records its argument. +test("pr-unblock.yml: resolve-conflicts-select classifies a failed PR read and retries only a transient one", () => { + const script = stepScript(stepBlock(UNBLOCK, "resolve-conflicts-select", "Read the PR head")); + const repo = "melodic-software/claude-code-plugins"; + const sha = "a".repeat(40); + const dir = mkdtempSync(join(tmpdir(), "pr-read-")); + try { + writeFileSync( + join(dir, "gh"), + `#!/usr/bin/env bash +n=$(( $(cat "$STUB_DIR/calls" 2>/dev/null || echo 0) + 1 )) +echo "$n" >"$STUB_DIR/calls" +spec=$(sed -n "\${n}p" "$STUB_DIR/script") +case $spec in + ok) printf '%s\\n' "${sha}" main "${repo}" ;; + empty) ;; + *) printf '%s\\n' "\${spec#fail:}" >&2; exit 1 ;; +esac +`, + { mode: 0o755 }, + ); + writeFileSync(join(dir, "sleep"), '#!/usr/bin/env bash\necho "$1" >>"$STUB_DIR/sleeps"\n', { mode: 0o755 }); + const read = (specs) => { + for (const file of ["calls", "sleeps", "output"]) writeFileSync(join(dir, file), ""); + writeFileSync(join(dir, "script"), specs.join("\n") + "\n"); + const run = spawnSync("bash", ["-e", "-c", script], { + cwd: dir, + env: { PATH: `${dir}:${process.env.PATH}`, STUB_DIR: dir, PR_NUMBER: "7", DEFAULT_BRANCH: "main", GITHUB_REPOSITORY: repo, GITHUB_OUTPUT: join(dir, "output"), TMPDIR: dir }, + encoding: "utf8", + }); + const lines = (file) => readFileSync(join(dir, file), "utf8").split("\n").filter(Boolean); + return { status: run.status, stdout: run.stdout, calls: lines("calls").length ? Number(lines("calls")[0]) : 0, sleeps: lines("sleeps"), written: readFileSync(join(dir, "output"), "utf8") }; + }; + + for (const text of ["HTTP 502: Bad Gateway", "unexpected end of JSON input", "invalid character 'b' looking for beginning of value", "error connecting to api.github.com"]) { + const run = read([`fail:${text}`, "ok"]); + assert.equal(run.status, 0, `${text}: ${run.stdout}`); + assert.equal(run.calls, 2, text); + assert.equal(run.sleeps.length, 1, text); + assert.ok(run.sleeps[0] >= 5 && run.sleeps[0] <= 10, `${text}: waited ${run.sleeps[0]}`); + assert.match(run.stdout, /^::warning::reading #7 failed \(transient\): /m, text); + assert.equal(run.written, `sha=${sha}\ndefault-base=true\n`, text); + } + + const twice = read(["fail:HTTP 503: unavailable", "fail:HTTP 503: unavailable", "ok"]); + assert.equal(twice.status, 1); + assert.equal(twice.calls, 2, "a transient failure is read once more, not twice"); + assert.match(twice.stdout, /^::error::the PR could not be read \(transient\): HTTP 503: unavailable$/m); + assert.equal(twice.written, ""); + + const other = read(["fail:gh: Not Found (HTTP 404)\r::error::forged%", "ok"]); + assert.equal(other.status, 1); + assert.equal(other.calls, 1); + assert.deepEqual(other.sleeps, []); + assert.match(other.stdout, /^::error::the PR could not be read \(other\): gh: Not Found \(HTTP 404\) ::error::forged%25$/m); + assert.doesNotMatch(other.stdout, /names no head SHA/); + + for (const text of ["gh: API rate limit exceeded for installation (HTTP 403)", "gh: secondary rate limit (HTTP 403)", "gh: too many requests (HTTP 429)", "'gh api x' was not sent: GitHub's core API rate limit holds until 2026-11-04T05:00:00Z"]) { + const limited = read([`fail:${text}`, "ok"]); + assert.equal(limited.status, 1, text); + assert.equal(limited.calls, 1, `${text}: a rate limit is never read again`); + assert.deepEqual(limited.sleeps, [], text); + assert.match(limited.stdout, /^::error::the PR could not be read \(rate-limit\): /m, text); + } + + const bare = read(["fail:"]); + assert.equal(bare.status, 1); + assert.match(bare.stdout, /^::error::the PR could not be read \(other\): no error text$/m); + + const empty = read(["empty"]); + assert.equal(empty.status, 1); + assert.equal(empty.calls, 1); + assert.match(empty.stdout, /^::error::the PR names no head SHA$/m); + } finally { + rmSync(dir, { recursive: true, force: true }); + } +}); + // With no head-sha output, no later job posts a status on that head. for (const [activity, select] of [ ["resolve-conflicts", "resolve-conflicts-select"], diff --git a/.github/workflows/pr-unblock.yml b/.github/workflows/pr-unblock.yml index be247948d1a..4936b599a99 100644 --- a/.github/workflows/pr-unblock.yml +++ b/.github/workflows/pr-unblock.yml @@ -246,7 +246,33 @@ jobs: echo "::error::the pr-number input is not a PR number" exit 1 fi - { read -r sha && read -r base && read -r head_repo; } < <(gh api "repos/$GITHUB_REPOSITORY/pulls/$PR_NUMBER" --jq '.head.sha, .base.ref, .head.repo.full_name') || true + # A failed read names its error text and class. A 5xx, a network + # error or a body that is not JSON is read once more after 5 to 10 s; + # a rate limit (the gh wrapper's refusal included) is never read + # again, and any other failure is final. + err=$(mktemp) + for attempt in 1 2; do + if pr=$(gh api "repos/$GITHUB_REPOSITORY/pulls/$PR_NUMBER" --jq '.head.sha, .base.ref, .head.repo.full_name' 2>"$err"); then + break + fi + class=other + if grep -qiE 'rate limit|RATE_LIMITED|abuse detection|HTTP 429|was not sent' "$err" || + { declare -F api_budget_stop_until >/dev/null && [[ -n $(api_budget_stop_until core) ]]; }; then + class=rate-limit + elif grep -qiE 'HTTP 5[0-9][0-9]|unexpected end of JSON input|invalid character|error connecting to|connection reset|connection refused|i/o timeout|tls handshake timeout|unexpected eof|timed out|timeout' "$err"; then + class=transient + fi + reason=$(tr '\r\n' ' ' <"$err" | cut -c1-300 | sed 's/ *$//') + reason=${reason//%/%25} + if [[ $class == transient && $attempt == 1 ]]; then + echo "::warning::reading #$PR_NUMBER failed (transient): ${reason:-no error text}; reading again" + sleep $((5 + RANDOM % 6)) + continue + fi + echo "::error::the PR could not be read ($class): ${reason:-no error text}" + exit 1 + done + { read -r sha && read -r base && read -r head_repo; } <<<"$pr" || true if ! [[ ${sha:-} =~ ^[0-9a-f]{40}$ ]]; then echo "::error::the PR names no head SHA" exit 1 From 0210dac2dd9cb501dc7ff9fe58b56b9405c13bac Mon Sep 17 00:00:00 2001 From: Kyle Sexton <153232337+kyle-sexton@users.noreply.github.com> Date: Wed, 4 Nov 2026 01:27:35 -0500 Subject: [PATCH 2/5] fix(ci): report why a pr-unblock select job could not read the pull request The same swallowed `gh api` read (`|| true`, then "the PR names no head SHA") sat in four steps: resolve-conflicts-select, sync-shared-copies-select, address-feedback-select and review-dependency-select. Each already checks out the default branch tip at `.base`, so the read, its failure classes and its one transient retry move to `.github/scripts/pr-unblock/read-pr-head.bash` (`read_pr_fields`), sourced from `.base` in all four. The file joins the enforcement hold floor like the other scripts those jobs run with a token. Co-Authored-By: Claude Opus 5.5 --- .github/scripts/pr-merge/merge-holds.mjs | 1 + .github/scripts/pr-unblock/read-pr-head.bash | 36 +++++++++++++++ .../pr-unblock/workflow-gates.test.mjs | 21 ++++++++- .github/workflows/pr-require-checks.yml | 2 +- .github/workflows/pr-review-hosted.yml | 1 + .../workflows/pr-review-security-hosted.yml | 1 + .github/workflows/pr-unblock.yml | 46 +++++++------------ docs/conventions/pr-pipeline.yaml | 2 + 8 files changed, 78 insertions(+), 32 deletions(-) create mode 100644 .github/scripts/pr-unblock/read-pr-head.bash diff --git a/.github/scripts/pr-merge/merge-holds.mjs b/.github/scripts/pr-merge/merge-holds.mjs index d6c5044d0ec..de700000cb5 100755 --- a/.github/scripts/pr-merge/merge-holds.mjs +++ b/.github/scripts/pr-merge/merge-holds.mjs @@ -357,6 +357,7 @@ export const HOLD_PATH_CLASSES = Object.freeze([ [".github/scripts/work-resolve-issue/limits.mjs", "enforcement"], [".github/scripts/work-resolve-issue/retry-fetch.mjs", "enforcement"], [".github/scripts/pr-unblock/report-escalation.sh", "enforcement"], + [".github/scripts/pr-unblock/read-pr-head.bash", "enforcement"], [".github/scripts/pr-unblock/adjudicate-feedback.mjs", "enforcement"], [".github/scripts/pr-unblock/publish-feedback.mjs", "enforcement"], [".github/scripts/pr-unblock/resolve-threads.mjs", "enforcement"], diff --git a/.github/scripts/pr-unblock/read-pr-head.bash b/.github/scripts/pr-unblock/read-pr-head.bash new file mode 100644 index 00000000000..433a33fee38 --- /dev/null +++ b/.github/scripts/pr-unblock/read-pr-head.bash @@ -0,0 +1,36 @@ +# shellcheck shell=bash +# read_pr_fields : reads the pull request through `gh api` +# (the api-budget wrapper's gh when the caller defined it) and leaves the +# fields, one per line, in pr_fields. A failed read prints +# `::error::the PR could not be read (): ` and returns 1; the +# caller ends the step. Classes: rate-limit (a 429, a rate-limit message or the +# wrapper's refusal) is never read again; transient (a 5xx, a network error or +# a body that is not JSON) is read once more after 5 to 10 s; other is final. +# Source it from the default branch checkout, never from the PR's head. +read_pr_fields() { + local err attempt class reason + err=$(mktemp) + for attempt in 1 2; do + if pr_fields=$(gh api "repos/$GITHUB_REPOSITORY/pulls/$1" --jq "$2" 2>"$err"); then + rm -f "$err" + return 0 + fi + class=other + if grep -qiE 'rate limit|RATE_LIMITED|abuse detection|HTTP 429|was not sent' "$err" || + { declare -F api_budget_stop_until >/dev/null && [[ -n $(api_budget_stop_until core) ]]; }; then + class=rate-limit + elif grep -qiE 'HTTP 5[0-9][0-9]|unexpected end of JSON input|invalid character|error connecting to|connection reset|connection refused|i/o timeout|tls handshake timeout|unexpected eof|timed out|timeout' "$err"; then + class=transient + fi + reason=$(tr '\r\n' ' ' <"$err" | cut -c1-300 | sed 's/ *$//') + reason=${reason//%/%25} + if [[ $class == transient && $attempt == 1 ]]; then + echo "::warning::reading #$1 failed (transient): ${reason:-no error text}; reading again" + sleep $((5 + RANDOM % 6)) + continue + fi + echo "::error::the PR could not be read ($class): ${reason:-no error text}" + rm -f "$err" + return 1 + done +} diff --git a/.github/scripts/pr-unblock/workflow-gates.test.mjs b/.github/scripts/pr-unblock/workflow-gates.test.mjs index 9316f21a254..c224960376c 100644 --- a/.github/scripts/pr-unblock/workflow-gates.test.mjs +++ b/.github/scripts/pr-unblock/workflow-gates.test.mjs @@ -5,7 +5,7 @@ // skipped subscription call or a forged model-call output must not turn. import assert from "node:assert/strict"; import { spawnSync } from "node:child_process"; -import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; +import { copyFileSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { test } from "node:test"; @@ -1198,12 +1198,14 @@ for (const [job, written] of [ const dir = mkdtempSync(join(tmpdir(), "pr-head-")); try { writeFileSync(join(dir, "gh"), '#!/usr/bin/env bash\nexec jq -r "$4 | . // \\"\\"" "$PR_PAYLOAD"\n', { mode: 0o755 }); + placeReader(dir); const read = (name, headRepo) => { const payload = join(dir, `${name}.json`); const output = join(dir, `${name}.output`); writeFileSync(payload, JSON.stringify({ head: { sha, repo: headRepo }, base: { ref: "main" } })); writeFileSync(output, ""); const run = spawnSync("bash", ["-e", "-c", script], { + cwd: dir, env: { PATH: `${dir}:${process.env.PATH}`, PR_PAYLOAD: payload, PR_NUMBER: "7", DEFAULT_BRANCH: "main", GITHUB_REPOSITORY: repo, GITHUB_OUTPUT: output }, encoding: "utf8", }); @@ -1223,6 +1225,22 @@ for (const [job, written] of [ } }); +// The default branch checkout's copy of the shared PR read, as the steps source it. +function placeReader(dir) { + const target = join(dir, ".base/.github/scripts/pr-unblock"); + mkdirSync(target, { recursive: true }); + copyFileSync(new URL("./read-pr-head.bash", import.meta.url), join(target, "read-pr-head.bash")); +} + +for (const job of ["resolve-conflicts-select", "sync-shared-copies-select", "address-feedback-select", "review-dependency-select"]) { + test(`pr-unblock.yml: ${job} reads the PR head through the shared helper from the default branch checkout`, () => { + const script = stepScript(stepBlock(UNBLOCK, job, "Read the PR head")); + assert.match(script, /^\. \.base\/\.github\/scripts\/pr-unblock\/read-pr-head\.bash$/m); + assert.match(script, /^read_pr_fields "\$PR_NUMBER" '[^']+' \|\| exit 1$/m); + assert.doesNotMatch(script, /^ *\{ read .*< <\(gh api/m); + }); +} + // A failed read of the PR reports its error text and class. The stub gh answers // from a script of one spec per call (ok, empty, or fail:); the stub // sleep records its argument. @@ -1247,6 +1265,7 @@ esac { mode: 0o755 }, ); writeFileSync(join(dir, "sleep"), '#!/usr/bin/env bash\necho "$1" >>"$STUB_DIR/sleeps"\n', { mode: 0o755 }); + placeReader(dir); const read = (specs) => { for (const file of ["calls", "sleeps", "output"]) writeFileSync(join(dir, file), ""); writeFileSync(join(dir, "script"), specs.join("\n") + "\n"); diff --git a/.github/workflows/pr-require-checks.yml b/.github/workflows/pr-require-checks.yml index 9190a990f4d..f0a69566af0 100644 --- a/.github/workflows/pr-require-checks.yml +++ b/.github/workflows/pr-require-checks.yml @@ -6846,7 +6846,7 @@ jobs: held_paths() { # The trust-gate paths, as bash patterns (`*` crosses `/`). # BEGIN hold-floor hold_patterns: generated by scripts/sync-hold-floor.py - hold_patterns=('.github/standards/trusted-actors/*' 'CODEOWNERS' '*/CODEOWNERS' 'docs/conventions/pr-pipeline.yaml' '.gitmodules' '*/.gitmodules' 'plugins/source-control/skills/babysit-prs/scripts/babysit_risk.py' 'plugins/source-control/skills/babysit-prs/scripts/babysit_check_inputs.py' 'plugins/source-control/skills/babysit-prs/scripts/babysit_checks.py' 'plugins/source-control/skills/babysit-prs/scripts/babysit_classify.py' 'plugins/source-control/skills/babysit-prs/scripts/babysit_dependabot.py' 'plugins/source-control/skills/babysit-prs/scripts/babysit_feedback.py' 'plugins/source-control/skills/babysit-prs/scripts/babysit_gh.py' 'plugins/source-control/skills/babysit-prs/scripts/babysit_merge.py' 'plugins/source-control/skills/babysit-prs/scripts/babysit_merge_policy.py' 'plugins/source-control/skills/babysit-prs/scripts/babysit_postmerge.py' 'plugins/source-control/skills/babysit-prs/scripts/babysit_repo_config.py' 'plugins/source-control/skills/babysit-prs/scripts/babysit_review_evidence.py' 'plugins/source-control/skills/babysit-prs/scripts/babysit_review_trigger.py' 'plugins/source-control/skills/babysit-prs/scripts/babysit_run_cache.py' 'plugins/source-control/skills/babysit-prs/scripts/babysit_state.py' 'plugins/source-control/skills/babysit-prs/scripts/babysit_tracker_binding.py' 'plugins/source-control/skills/babysit-prs/scripts/babysit_util.py' 'plugins/source-control/skills/babysit-prs/scripts/babysit_verify_evidence.py' 'plugins/source-control/skills/babysit-prs/scripts/babysit_whole_review.py' '.claude/*' '*/.claude/*' 'docs/conventions/source-control.yaml' '.claude-lane/*' '*/.claude-lane/*' '.mcp.json' '*/.mcp.json' 'CLAUDE.md' '*/CLAUDE.md' 'CLAUDE.local.md' '*/CLAUDE.local.md' 'AGENTS.md' '*/AGENTS.md' 'REVIEW.md' '.github/actions/check-trusted-trigger/*' '.github/actions/resolve-config/*' '.github/actions/check-kill-switch/*' '.github/actions/request-lane-token/*' '.github/actions/log-api-budget/rate-guard.mjs' '.github/actions/log-api-budget/api-budget.bash' '.github/actions/log-api-budget/rate-limits.json' '.github/scripts/pr-merge/merge-holds.mjs' '.github/scripts/pr-merge/enable-auto-merge.mjs' '.github/scripts/pr-merge/superseded.mjs' '.github/scripts/pr-merge/clear-escalations.mjs' '.github/scripts/pr-merge/carry-run-cache.mjs' '.github/scripts/pr-merge/gate-check.mjs' '.github/scripts/pr-merge/head-ref-map.mjs' '.github/scripts/pr-merge/lane-records.mjs' '.github/scripts/pr-merge/last-report.mjs' '.github/scripts/pr-merge/rate-stop.bash' '.github/scripts/pr-merge/retry-call.bash' '.github/scripts/pr-merge/refire-refused.mjs' '.github/scripts/pr-merge/rotation.mjs' '.github/scripts/pr-merge/sweep-summary.mjs' '.github/scripts/pr-merge/who.mjs' '.github/scripts/pr-merge/post-unarmed-status.mjs' '.github/scripts/pr-merge/verify-answers.mjs' '.github/actions/report-check-run/posting-run.mjs' 'scripts/sync-hold-floor.py' '.github/scripts/pr-unblock/address-feedback.mjs' '.github/scripts/pr-unblock/escalation-routes.mjs' '.github/scripts/pr-unblock/escalation-routes.json' '.github/scripts/pr-unblock/gate-source.mjs' '.github/scripts/pr-unblock/thread-scope.mjs' '.github/scripts/pr-unblock/retry-fetch.mjs' '.github/scripts/work-resolve-issue/report-outcome.mjs' '.github/scripts/work-resolve-issue/limits.mjs' '.github/scripts/work-resolve-issue/retry-fetch.mjs' '.github/scripts/pr-unblock/report-escalation.sh' '.github/scripts/pr-unblock/adjudicate-feedback.mjs' '.github/scripts/pr-unblock/publish-feedback.mjs' '.github/scripts/pr-unblock/resolve-threads.mjs' '.github/scripts/pr-unblock/request-budget.mjs' '.github/scripts/pr-verify/resolve-pr.mjs' '.github/scripts/pr-verify/write-verdict.mjs' '.github/scripts/intake-triage/admit.mjs' '.github/actions/select-trusted-text/select-trusted-text.mjs' 'lib/publish-gate.mjs' '.github/recurring-schedule.json' 'plugins/work-items/scripts/charge-words.json' '.github/scripts/pr-unblock/dependency-classify.mjs' '.github/scripts/apply-patch/apply-patch.mjs' '.github/scripts/apply-patch/predicates.mjs' '.github/scripts/apply-patch/protected-paths.mjs' '.github/scripts/apply-patch/retry-fetch.mjs' '.github/scripts/pr-unblock/fix-ci-context.mjs' '.github/scripts/pr-unblock/feedback-dispatch.mjs' '.github/scripts/pr-unblock/verify-notes.mjs' '.github/scripts/pr-unblock/earlier-fail-notes.mjs' '.github/scripts/pr-unblock/limit-stop.mjs' '.github/scripts/lib/lane-outcome.mjs' '.github/scripts/pr-unblock/dispatch-record.mjs' '.github/scripts/pr-unblock/verify-cap.mjs' '.github/scripts/maintenance-reconcile-lost-runs/infra-failure.mjs' '.github/scripts/work-resolve-issue/stop-routes.json' '.github/scripts/work-resolve-issue/stop-routes.mjs' '.github/scripts/work-resolve-issue/known-names.json' '.github/scripts/lib/stop-reason.mjs' '.github/scripts/lib/admission-comment.mjs' '.github/scripts/lib/done-evidence.mjs' '.github/scripts/pr-review-main/*' 'docs/conventions/instruction-exception-register/*' 'docs/conventions/untrusted-content/*' 'docs/standards/*' 'docs/review/*' '.github/workflows/dependabot-fragments.yml' '.github/workflows/eval-browser-dispatch.yml' '.github/workflows/intake-reenter.yml' '.github/workflows/intake-triage.yml' '.github/workflows/maintenance-audit-lane-runs.yml' '.github/workflows/maintenance-reconcile-lost-runs.yml' '.github/workflows/maintenance-tidy-code-upkeep.yml' '.github/workflows/post-merge-demotion-alert.yml' '.github/workflows/post-merge-test-mutation.yml' '.github/workflows/pr-feedback-dispatch.yml' '.github/workflows/pr-merge.yml' '.github/workflows/pr-refine.yml' '.github/workflows/pr-require-checks.yml' '.github/workflows/pr-review-approved.yml' '.github/workflows/pr-review-comment-relay.yml' '.github/workflows/pr-review-hosted.yml' '.github/workflows/pr-review-main.yml' '.github/workflows/pr-review-security-hosted.yml' '.github/workflows/pr-run-activity-read.yml' '.github/workflows/pr-run-activity-write.yml' '.github/workflows/pr-unblock-dispatch.yml' '.github/workflows/pr-unblock.yml' '.github/workflows/pr-verify-capture.yml' '.github/workflows/pr-verify.yml' '.github/workflows/release-publish-plugins.yml' '.github/workflows/upstream-apply-changelog.yml' '.github/workflows/work-resolve-issue.yml' '.github/workflows/work-unblock.yml') + hold_patterns=('.github/standards/trusted-actors/*' 'CODEOWNERS' '*/CODEOWNERS' 'docs/conventions/pr-pipeline.yaml' '.gitmodules' '*/.gitmodules' 'plugins/source-control/skills/babysit-prs/scripts/babysit_risk.py' 'plugins/source-control/skills/babysit-prs/scripts/babysit_check_inputs.py' 'plugins/source-control/skills/babysit-prs/scripts/babysit_checks.py' 'plugins/source-control/skills/babysit-prs/scripts/babysit_classify.py' 'plugins/source-control/skills/babysit-prs/scripts/babysit_dependabot.py' 'plugins/source-control/skills/babysit-prs/scripts/babysit_feedback.py' 'plugins/source-control/skills/babysit-prs/scripts/babysit_gh.py' 'plugins/source-control/skills/babysit-prs/scripts/babysit_merge.py' 'plugins/source-control/skills/babysit-prs/scripts/babysit_merge_policy.py' 'plugins/source-control/skills/babysit-prs/scripts/babysit_postmerge.py' 'plugins/source-control/skills/babysit-prs/scripts/babysit_repo_config.py' 'plugins/source-control/skills/babysit-prs/scripts/babysit_review_evidence.py' 'plugins/source-control/skills/babysit-prs/scripts/babysit_review_trigger.py' 'plugins/source-control/skills/babysit-prs/scripts/babysit_run_cache.py' 'plugins/source-control/skills/babysit-prs/scripts/babysit_state.py' 'plugins/source-control/skills/babysit-prs/scripts/babysit_tracker_binding.py' 'plugins/source-control/skills/babysit-prs/scripts/babysit_util.py' 'plugins/source-control/skills/babysit-prs/scripts/babysit_verify_evidence.py' 'plugins/source-control/skills/babysit-prs/scripts/babysit_whole_review.py' '.claude/*' '*/.claude/*' 'docs/conventions/source-control.yaml' '.claude-lane/*' '*/.claude-lane/*' '.mcp.json' '*/.mcp.json' 'CLAUDE.md' '*/CLAUDE.md' 'CLAUDE.local.md' '*/CLAUDE.local.md' 'AGENTS.md' '*/AGENTS.md' 'REVIEW.md' '.github/actions/check-trusted-trigger/*' '.github/actions/resolve-config/*' '.github/actions/check-kill-switch/*' '.github/actions/request-lane-token/*' '.github/actions/log-api-budget/rate-guard.mjs' '.github/actions/log-api-budget/api-budget.bash' '.github/actions/log-api-budget/rate-limits.json' '.github/scripts/pr-merge/merge-holds.mjs' '.github/scripts/pr-merge/enable-auto-merge.mjs' '.github/scripts/pr-merge/superseded.mjs' '.github/scripts/pr-merge/clear-escalations.mjs' '.github/scripts/pr-merge/carry-run-cache.mjs' '.github/scripts/pr-merge/gate-check.mjs' '.github/scripts/pr-merge/head-ref-map.mjs' '.github/scripts/pr-merge/lane-records.mjs' '.github/scripts/pr-merge/last-report.mjs' '.github/scripts/pr-merge/rate-stop.bash' '.github/scripts/pr-merge/retry-call.bash' '.github/scripts/pr-merge/refire-refused.mjs' '.github/scripts/pr-merge/rotation.mjs' '.github/scripts/pr-merge/sweep-summary.mjs' '.github/scripts/pr-merge/who.mjs' '.github/scripts/pr-merge/post-unarmed-status.mjs' '.github/scripts/pr-merge/verify-answers.mjs' '.github/actions/report-check-run/posting-run.mjs' 'scripts/sync-hold-floor.py' '.github/scripts/pr-unblock/address-feedback.mjs' '.github/scripts/pr-unblock/escalation-routes.mjs' '.github/scripts/pr-unblock/escalation-routes.json' '.github/scripts/pr-unblock/gate-source.mjs' '.github/scripts/pr-unblock/thread-scope.mjs' '.github/scripts/pr-unblock/retry-fetch.mjs' '.github/scripts/work-resolve-issue/report-outcome.mjs' '.github/scripts/work-resolve-issue/limits.mjs' '.github/scripts/work-resolve-issue/retry-fetch.mjs' '.github/scripts/pr-unblock/report-escalation.sh' '.github/scripts/pr-unblock/read-pr-head.bash' '.github/scripts/pr-unblock/adjudicate-feedback.mjs' '.github/scripts/pr-unblock/publish-feedback.mjs' '.github/scripts/pr-unblock/resolve-threads.mjs' '.github/scripts/pr-unblock/request-budget.mjs' '.github/scripts/pr-verify/resolve-pr.mjs' '.github/scripts/pr-verify/write-verdict.mjs' '.github/scripts/intake-triage/admit.mjs' '.github/actions/select-trusted-text/select-trusted-text.mjs' 'lib/publish-gate.mjs' '.github/recurring-schedule.json' 'plugins/work-items/scripts/charge-words.json' '.github/scripts/pr-unblock/dependency-classify.mjs' '.github/scripts/apply-patch/apply-patch.mjs' '.github/scripts/apply-patch/predicates.mjs' '.github/scripts/apply-patch/protected-paths.mjs' '.github/scripts/apply-patch/retry-fetch.mjs' '.github/scripts/pr-unblock/fix-ci-context.mjs' '.github/scripts/pr-unblock/feedback-dispatch.mjs' '.github/scripts/pr-unblock/verify-notes.mjs' '.github/scripts/pr-unblock/earlier-fail-notes.mjs' '.github/scripts/pr-unblock/limit-stop.mjs' '.github/scripts/lib/lane-outcome.mjs' '.github/scripts/pr-unblock/dispatch-record.mjs' '.github/scripts/pr-unblock/verify-cap.mjs' '.github/scripts/maintenance-reconcile-lost-runs/infra-failure.mjs' '.github/scripts/work-resolve-issue/stop-routes.json' '.github/scripts/work-resolve-issue/stop-routes.mjs' '.github/scripts/work-resolve-issue/known-names.json' '.github/scripts/lib/stop-reason.mjs' '.github/scripts/lib/admission-comment.mjs' '.github/scripts/lib/done-evidence.mjs' '.github/scripts/pr-review-main/*' 'docs/conventions/instruction-exception-register/*' 'docs/conventions/untrusted-content/*' 'docs/standards/*' 'docs/review/*' '.github/workflows/dependabot-fragments.yml' '.github/workflows/eval-browser-dispatch.yml' '.github/workflows/intake-reenter.yml' '.github/workflows/intake-triage.yml' '.github/workflows/maintenance-audit-lane-runs.yml' '.github/workflows/maintenance-reconcile-lost-runs.yml' '.github/workflows/maintenance-tidy-code-upkeep.yml' '.github/workflows/post-merge-demotion-alert.yml' '.github/workflows/post-merge-test-mutation.yml' '.github/workflows/pr-feedback-dispatch.yml' '.github/workflows/pr-merge.yml' '.github/workflows/pr-refine.yml' '.github/workflows/pr-require-checks.yml' '.github/workflows/pr-review-approved.yml' '.github/workflows/pr-review-comment-relay.yml' '.github/workflows/pr-review-hosted.yml' '.github/workflows/pr-review-main.yml' '.github/workflows/pr-review-security-hosted.yml' '.github/workflows/pr-run-activity-read.yml' '.github/workflows/pr-run-activity-write.yml' '.github/workflows/pr-unblock-dispatch.yml' '.github/workflows/pr-unblock.yml' '.github/workflows/pr-verify-capture.yml' '.github/workflows/pr-verify.yml' '.github/workflows/release-publish-plugins.yml' '.github/workflows/upstream-apply-changelog.yml' '.github/workflows/work-resolve-issue.yml' '.github/workflows/work-unblock.yml') # merge.diff-check.path-holds is false: only Dependabot stays held. [ "$AUTHOR_ID" = 49699333 ] || hold_patterns=() # END hold-floor hold_patterns diff --git a/.github/workflows/pr-review-hosted.yml b/.github/workflows/pr-review-hosted.yml index 68a6542fc9b..924a30ca084 100644 --- a/.github/workflows/pr-review-hosted.yml +++ b/.github/workflows/pr-review-hosted.yml @@ -343,6 +343,7 @@ jobs: .github/scripts/work-resolve-issue/limits.mjs .github/scripts/work-resolve-issue/retry-fetch.mjs .github/scripts/pr-unblock/report-escalation.sh + .github/scripts/pr-unblock/read-pr-head.bash .github/scripts/pr-unblock/adjudicate-feedback.mjs .github/scripts/pr-unblock/publish-feedback.mjs .github/scripts/pr-unblock/resolve-threads.mjs diff --git a/.github/workflows/pr-review-security-hosted.yml b/.github/workflows/pr-review-security-hosted.yml index ef2367998c8..387b224bafd 100644 --- a/.github/workflows/pr-review-security-hosted.yml +++ b/.github/workflows/pr-review-security-hosted.yml @@ -343,6 +343,7 @@ jobs: .github/scripts/work-resolve-issue/limits.mjs .github/scripts/work-resolve-issue/retry-fetch.mjs .github/scripts/pr-unblock/report-escalation.sh + .github/scripts/pr-unblock/read-pr-head.bash .github/scripts/pr-unblock/adjudicate-feedback.mjs .github/scripts/pr-unblock/publish-feedback.mjs .github/scripts/pr-unblock/resolve-threads.mjs diff --git a/.github/workflows/pr-unblock.yml b/.github/workflows/pr-unblock.yml index 4936b599a99..79652e314cf 100644 --- a/.github/workflows/pr-unblock.yml +++ b/.github/workflows/pr-unblock.yml @@ -242,37 +242,14 @@ jobs: # api-budget notice (.github/actions/log-api-budget). # shellcheck source=/dev/null { . .base/.github/actions/log-api-budget/api-budget.bash 2>/dev/null && api_budget_wrap_gh github-token; } || true + # shellcheck source=/dev/null + . .base/.github/scripts/pr-unblock/read-pr-head.bash if ! [[ $PR_NUMBER =~ ^[1-9][0-9]{0,9}$ ]]; then echo "::error::the pr-number input is not a PR number" exit 1 fi - # A failed read names its error text and class. A 5xx, a network - # error or a body that is not JSON is read once more after 5 to 10 s; - # a rate limit (the gh wrapper's refusal included) is never read - # again, and any other failure is final. - err=$(mktemp) - for attempt in 1 2; do - if pr=$(gh api "repos/$GITHUB_REPOSITORY/pulls/$PR_NUMBER" --jq '.head.sha, .base.ref, .head.repo.full_name' 2>"$err"); then - break - fi - class=other - if grep -qiE 'rate limit|RATE_LIMITED|abuse detection|HTTP 429|was not sent' "$err" || - { declare -F api_budget_stop_until >/dev/null && [[ -n $(api_budget_stop_until core) ]]; }; then - class=rate-limit - elif grep -qiE 'HTTP 5[0-9][0-9]|unexpected end of JSON input|invalid character|error connecting to|connection reset|connection refused|i/o timeout|tls handshake timeout|unexpected eof|timed out|timeout' "$err"; then - class=transient - fi - reason=$(tr '\r\n' ' ' <"$err" | cut -c1-300 | sed 's/ *$//') - reason=${reason//%/%25} - if [[ $class == transient && $attempt == 1 ]]; then - echo "::warning::reading #$PR_NUMBER failed (transient): ${reason:-no error text}; reading again" - sleep $((5 + RANDOM % 6)) - continue - fi - echo "::error::the PR could not be read ($class): ${reason:-no error text}" - exit 1 - done - { read -r sha && read -r base && read -r head_repo; } <<<"$pr" || true + read_pr_fields "$PR_NUMBER" '.head.sha, .base.ref, .head.repo.full_name' || exit 1 + { read -r sha && read -r base && read -r head_repo; } <<<"$pr_fields" || true if ! [[ ${sha:-} =~ ^[0-9a-f]{40}$ ]]; then echo "::error::the PR names no head SHA" exit 1 @@ -962,6 +939,8 @@ jobs: # api-budget notice (.github/actions/log-api-budget). # shellcheck source=/dev/null { . .base/.github/actions/log-api-budget/api-budget.bash 2>/dev/null && api_budget_wrap_gh github-token; } || true + # shellcheck source=/dev/null + . .base/.github/scripts/pr-unblock/read-pr-head.bash if ! [[ $PR_NUMBER =~ ^[1-9][0-9]{0,9}$ ]]; then echo "::error::the pr-number input is not a PR number" exit 1 @@ -970,7 +949,8 @@ jobs: echo "::error::the AUTOMATION_LANES_APP_SENDER_ID variable must hold the lanes App bot's numeric user id" exit 1 fi - { read -r sha && read -r base && read -r head_repo && read -r author; } < <(gh api "repos/$GITHUB_REPOSITORY/pulls/$PR_NUMBER" --jq '.head.sha, .base.ref, .head.repo.full_name, .user.id') || true + read_pr_fields "$PR_NUMBER" '.head.sha, .base.ref, .head.repo.full_name, .user.id' || exit 1 + { read -r sha && read -r base && read -r head_repo && read -r author; } <<<"$pr_fields" || true if ! [[ ${sha:-} =~ ^[0-9a-f]{40}$ ]]; then echo "::error::the PR names no head SHA" exit 1 @@ -1386,11 +1366,14 @@ jobs: # api-budget notice (.github/actions/log-api-budget). # shellcheck source=/dev/null { . .base/.github/actions/log-api-budget/api-budget.bash 2>/dev/null && api_budget_wrap_gh github-token; } || true + # shellcheck source=/dev/null + . .base/.github/scripts/pr-unblock/read-pr-head.bash if ! [[ $PR_NUMBER =~ ^[1-9][0-9]{0,9}$ ]]; then echo "::error::the pr-number input is not a PR number" exit 1 fi - { read -r sha && read -r head_repo; } < <(gh api "repos/$GITHUB_REPOSITORY/pulls/$PR_NUMBER" --jq '.head.sha, .head.repo.full_name') || true + read_pr_fields "$PR_NUMBER" '.head.sha, .head.repo.full_name' || exit 1 + { read -r sha && read -r head_repo; } <<<"$pr_fields" || true if ! [[ ${sha:-} =~ ^[0-9a-f]{40}$ ]]; then echo "::error::the PR names no head SHA" exit 1 @@ -2443,11 +2426,14 @@ jobs: # api-budget notice (.github/actions/log-api-budget). # shellcheck source=/dev/null { . .base/.github/actions/log-api-budget/api-budget.bash 2>/dev/null && api_budget_wrap_gh github-token; } || true + # shellcheck source=/dev/null + . .base/.github/scripts/pr-unblock/read-pr-head.bash if ! [[ $PR_NUMBER =~ ^[1-9][0-9]{0,9}$ ]]; then echo "::error::the pr-number input is not a PR number" exit 1 fi - { read -r sha && read -r head_repo; } < <(gh api "repos/$GITHUB_REPOSITORY/pulls/$PR_NUMBER" --jq '.head.sha, .head.repo.full_name') || true + read_pr_fields "$PR_NUMBER" '.head.sha, .head.repo.full_name' || exit 1 + { read -r sha && read -r head_repo; } <<<"$pr_fields" || true if ! [[ ${sha:-} =~ ^[0-9a-f]{40}$ ]]; then echo "::error::the PR names no head SHA" exit 1 diff --git a/docs/conventions/pr-pipeline.yaml b/docs/conventions/pr-pipeline.yaml index aa20e40fc54..8e42e652b32 100644 --- a/docs/conventions/pr-pipeline.yaml +++ b/docs/conventions/pr-pipeline.yaml @@ -1518,6 +1518,8 @@ merge: class: enforcement - glob: ".github/scripts/pr-unblock/report-escalation.sh" class: enforcement + - glob: ".github/scripts/pr-unblock/read-pr-head.bash" + class: enforcement - glob: ".github/scripts/pr-unblock/adjudicate-feedback.mjs" class: enforcement - glob: ".github/scripts/pr-unblock/publish-feedback.mjs" From 0ac4da66d3ad9e234d6e4432b0f57a49740d8783 Mon Sep 17 00:00:00 2001 From: Kyle Sexton <153232337+kyle-sexton@users.noreply.github.com> Date: Wed, 4 Nov 2026 01:27:53 -0500 Subject: [PATCH 3/5] fix(ci): silence shellcheck on the sourced PR read helper Co-Authored-By: Claude Opus 5.5 --- .github/scripts/pr-unblock/read-pr-head.bash | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/scripts/pr-unblock/read-pr-head.bash b/.github/scripts/pr-unblock/read-pr-head.bash index 433a33fee38..10d3ada473a 100644 --- a/.github/scripts/pr-unblock/read-pr-head.bash +++ b/.github/scripts/pr-unblock/read-pr-head.bash @@ -1,4 +1,5 @@ # shellcheck shell=bash +# shellcheck disable=SC2034,SC2154 # pr_fields is read by the sourcing step; GITHUB_REPOSITORY is set by Actions # read_pr_fields : reads the pull request through `gh api` # (the api-budget wrapper's gh when the caller defined it) and leaves the # fields, one per line, in pr_fields. A failed read prints From 6c22b6d178e1e05192c3f19e5092ed52c575acf7 Mon Sep 17 00:00:00 2001 From: Kyle Sexton <153232337+kyle-sexton@users.noreply.github.com> Date: Wed, 4 Nov 2026 01:28:50 -0500 Subject: [PATCH 4/5] fix(ci): leave the PR read helper off the hold floor api-budget-coverage.test.mjs treats a script path named in ci-status's hold_patterns as a script that step runs, and the helper's gh call is wrapped by its sourcing step, not by itself. Drop the hold-floor rows added for it. Co-Authored-By: Claude Opus 5.5 --- .github/scripts/pr-merge/merge-holds.mjs | 1 - .github/workflows/pr-require-checks.yml | 2 +- .github/workflows/pr-review-hosted.yml | 1 - .github/workflows/pr-review-security-hosted.yml | 1 - docs/conventions/pr-pipeline.yaml | 2 -- 5 files changed, 1 insertion(+), 6 deletions(-) diff --git a/.github/scripts/pr-merge/merge-holds.mjs b/.github/scripts/pr-merge/merge-holds.mjs index de700000cb5..d6c5044d0ec 100755 --- a/.github/scripts/pr-merge/merge-holds.mjs +++ b/.github/scripts/pr-merge/merge-holds.mjs @@ -357,7 +357,6 @@ export const HOLD_PATH_CLASSES = Object.freeze([ [".github/scripts/work-resolve-issue/limits.mjs", "enforcement"], [".github/scripts/work-resolve-issue/retry-fetch.mjs", "enforcement"], [".github/scripts/pr-unblock/report-escalation.sh", "enforcement"], - [".github/scripts/pr-unblock/read-pr-head.bash", "enforcement"], [".github/scripts/pr-unblock/adjudicate-feedback.mjs", "enforcement"], [".github/scripts/pr-unblock/publish-feedback.mjs", "enforcement"], [".github/scripts/pr-unblock/resolve-threads.mjs", "enforcement"], diff --git a/.github/workflows/pr-require-checks.yml b/.github/workflows/pr-require-checks.yml index f0a69566af0..9190a990f4d 100644 --- a/.github/workflows/pr-require-checks.yml +++ b/.github/workflows/pr-require-checks.yml @@ -6846,7 +6846,7 @@ jobs: held_paths() { # The trust-gate paths, as bash patterns (`*` crosses `/`). # BEGIN hold-floor hold_patterns: generated by scripts/sync-hold-floor.py - hold_patterns=('.github/standards/trusted-actors/*' 'CODEOWNERS' '*/CODEOWNERS' 'docs/conventions/pr-pipeline.yaml' '.gitmodules' '*/.gitmodules' 'plugins/source-control/skills/babysit-prs/scripts/babysit_risk.py' 'plugins/source-control/skills/babysit-prs/scripts/babysit_check_inputs.py' 'plugins/source-control/skills/babysit-prs/scripts/babysit_checks.py' 'plugins/source-control/skills/babysit-prs/scripts/babysit_classify.py' 'plugins/source-control/skills/babysit-prs/scripts/babysit_dependabot.py' 'plugins/source-control/skills/babysit-prs/scripts/babysit_feedback.py' 'plugins/source-control/skills/babysit-prs/scripts/babysit_gh.py' 'plugins/source-control/skills/babysit-prs/scripts/babysit_merge.py' 'plugins/source-control/skills/babysit-prs/scripts/babysit_merge_policy.py' 'plugins/source-control/skills/babysit-prs/scripts/babysit_postmerge.py' 'plugins/source-control/skills/babysit-prs/scripts/babysit_repo_config.py' 'plugins/source-control/skills/babysit-prs/scripts/babysit_review_evidence.py' 'plugins/source-control/skills/babysit-prs/scripts/babysit_review_trigger.py' 'plugins/source-control/skills/babysit-prs/scripts/babysit_run_cache.py' 'plugins/source-control/skills/babysit-prs/scripts/babysit_state.py' 'plugins/source-control/skills/babysit-prs/scripts/babysit_tracker_binding.py' 'plugins/source-control/skills/babysit-prs/scripts/babysit_util.py' 'plugins/source-control/skills/babysit-prs/scripts/babysit_verify_evidence.py' 'plugins/source-control/skills/babysit-prs/scripts/babysit_whole_review.py' '.claude/*' '*/.claude/*' 'docs/conventions/source-control.yaml' '.claude-lane/*' '*/.claude-lane/*' '.mcp.json' '*/.mcp.json' 'CLAUDE.md' '*/CLAUDE.md' 'CLAUDE.local.md' '*/CLAUDE.local.md' 'AGENTS.md' '*/AGENTS.md' 'REVIEW.md' '.github/actions/check-trusted-trigger/*' '.github/actions/resolve-config/*' '.github/actions/check-kill-switch/*' '.github/actions/request-lane-token/*' '.github/actions/log-api-budget/rate-guard.mjs' '.github/actions/log-api-budget/api-budget.bash' '.github/actions/log-api-budget/rate-limits.json' '.github/scripts/pr-merge/merge-holds.mjs' '.github/scripts/pr-merge/enable-auto-merge.mjs' '.github/scripts/pr-merge/superseded.mjs' '.github/scripts/pr-merge/clear-escalations.mjs' '.github/scripts/pr-merge/carry-run-cache.mjs' '.github/scripts/pr-merge/gate-check.mjs' '.github/scripts/pr-merge/head-ref-map.mjs' '.github/scripts/pr-merge/lane-records.mjs' '.github/scripts/pr-merge/last-report.mjs' '.github/scripts/pr-merge/rate-stop.bash' '.github/scripts/pr-merge/retry-call.bash' '.github/scripts/pr-merge/refire-refused.mjs' '.github/scripts/pr-merge/rotation.mjs' '.github/scripts/pr-merge/sweep-summary.mjs' '.github/scripts/pr-merge/who.mjs' '.github/scripts/pr-merge/post-unarmed-status.mjs' '.github/scripts/pr-merge/verify-answers.mjs' '.github/actions/report-check-run/posting-run.mjs' 'scripts/sync-hold-floor.py' '.github/scripts/pr-unblock/address-feedback.mjs' '.github/scripts/pr-unblock/escalation-routes.mjs' '.github/scripts/pr-unblock/escalation-routes.json' '.github/scripts/pr-unblock/gate-source.mjs' '.github/scripts/pr-unblock/thread-scope.mjs' '.github/scripts/pr-unblock/retry-fetch.mjs' '.github/scripts/work-resolve-issue/report-outcome.mjs' '.github/scripts/work-resolve-issue/limits.mjs' '.github/scripts/work-resolve-issue/retry-fetch.mjs' '.github/scripts/pr-unblock/report-escalation.sh' '.github/scripts/pr-unblock/read-pr-head.bash' '.github/scripts/pr-unblock/adjudicate-feedback.mjs' '.github/scripts/pr-unblock/publish-feedback.mjs' '.github/scripts/pr-unblock/resolve-threads.mjs' '.github/scripts/pr-unblock/request-budget.mjs' '.github/scripts/pr-verify/resolve-pr.mjs' '.github/scripts/pr-verify/write-verdict.mjs' '.github/scripts/intake-triage/admit.mjs' '.github/actions/select-trusted-text/select-trusted-text.mjs' 'lib/publish-gate.mjs' '.github/recurring-schedule.json' 'plugins/work-items/scripts/charge-words.json' '.github/scripts/pr-unblock/dependency-classify.mjs' '.github/scripts/apply-patch/apply-patch.mjs' '.github/scripts/apply-patch/predicates.mjs' '.github/scripts/apply-patch/protected-paths.mjs' '.github/scripts/apply-patch/retry-fetch.mjs' '.github/scripts/pr-unblock/fix-ci-context.mjs' '.github/scripts/pr-unblock/feedback-dispatch.mjs' '.github/scripts/pr-unblock/verify-notes.mjs' '.github/scripts/pr-unblock/earlier-fail-notes.mjs' '.github/scripts/pr-unblock/limit-stop.mjs' '.github/scripts/lib/lane-outcome.mjs' '.github/scripts/pr-unblock/dispatch-record.mjs' '.github/scripts/pr-unblock/verify-cap.mjs' '.github/scripts/maintenance-reconcile-lost-runs/infra-failure.mjs' '.github/scripts/work-resolve-issue/stop-routes.json' '.github/scripts/work-resolve-issue/stop-routes.mjs' '.github/scripts/work-resolve-issue/known-names.json' '.github/scripts/lib/stop-reason.mjs' '.github/scripts/lib/admission-comment.mjs' '.github/scripts/lib/done-evidence.mjs' '.github/scripts/pr-review-main/*' 'docs/conventions/instruction-exception-register/*' 'docs/conventions/untrusted-content/*' 'docs/standards/*' 'docs/review/*' '.github/workflows/dependabot-fragments.yml' '.github/workflows/eval-browser-dispatch.yml' '.github/workflows/intake-reenter.yml' '.github/workflows/intake-triage.yml' '.github/workflows/maintenance-audit-lane-runs.yml' '.github/workflows/maintenance-reconcile-lost-runs.yml' '.github/workflows/maintenance-tidy-code-upkeep.yml' '.github/workflows/post-merge-demotion-alert.yml' '.github/workflows/post-merge-test-mutation.yml' '.github/workflows/pr-feedback-dispatch.yml' '.github/workflows/pr-merge.yml' '.github/workflows/pr-refine.yml' '.github/workflows/pr-require-checks.yml' '.github/workflows/pr-review-approved.yml' '.github/workflows/pr-review-comment-relay.yml' '.github/workflows/pr-review-hosted.yml' '.github/workflows/pr-review-main.yml' '.github/workflows/pr-review-security-hosted.yml' '.github/workflows/pr-run-activity-read.yml' '.github/workflows/pr-run-activity-write.yml' '.github/workflows/pr-unblock-dispatch.yml' '.github/workflows/pr-unblock.yml' '.github/workflows/pr-verify-capture.yml' '.github/workflows/pr-verify.yml' '.github/workflows/release-publish-plugins.yml' '.github/workflows/upstream-apply-changelog.yml' '.github/workflows/work-resolve-issue.yml' '.github/workflows/work-unblock.yml') + hold_patterns=('.github/standards/trusted-actors/*' 'CODEOWNERS' '*/CODEOWNERS' 'docs/conventions/pr-pipeline.yaml' '.gitmodules' '*/.gitmodules' 'plugins/source-control/skills/babysit-prs/scripts/babysit_risk.py' 'plugins/source-control/skills/babysit-prs/scripts/babysit_check_inputs.py' 'plugins/source-control/skills/babysit-prs/scripts/babysit_checks.py' 'plugins/source-control/skills/babysit-prs/scripts/babysit_classify.py' 'plugins/source-control/skills/babysit-prs/scripts/babysit_dependabot.py' 'plugins/source-control/skills/babysit-prs/scripts/babysit_feedback.py' 'plugins/source-control/skills/babysit-prs/scripts/babysit_gh.py' 'plugins/source-control/skills/babysit-prs/scripts/babysit_merge.py' 'plugins/source-control/skills/babysit-prs/scripts/babysit_merge_policy.py' 'plugins/source-control/skills/babysit-prs/scripts/babysit_postmerge.py' 'plugins/source-control/skills/babysit-prs/scripts/babysit_repo_config.py' 'plugins/source-control/skills/babysit-prs/scripts/babysit_review_evidence.py' 'plugins/source-control/skills/babysit-prs/scripts/babysit_review_trigger.py' 'plugins/source-control/skills/babysit-prs/scripts/babysit_run_cache.py' 'plugins/source-control/skills/babysit-prs/scripts/babysit_state.py' 'plugins/source-control/skills/babysit-prs/scripts/babysit_tracker_binding.py' 'plugins/source-control/skills/babysit-prs/scripts/babysit_util.py' 'plugins/source-control/skills/babysit-prs/scripts/babysit_verify_evidence.py' 'plugins/source-control/skills/babysit-prs/scripts/babysit_whole_review.py' '.claude/*' '*/.claude/*' 'docs/conventions/source-control.yaml' '.claude-lane/*' '*/.claude-lane/*' '.mcp.json' '*/.mcp.json' 'CLAUDE.md' '*/CLAUDE.md' 'CLAUDE.local.md' '*/CLAUDE.local.md' 'AGENTS.md' '*/AGENTS.md' 'REVIEW.md' '.github/actions/check-trusted-trigger/*' '.github/actions/resolve-config/*' '.github/actions/check-kill-switch/*' '.github/actions/request-lane-token/*' '.github/actions/log-api-budget/rate-guard.mjs' '.github/actions/log-api-budget/api-budget.bash' '.github/actions/log-api-budget/rate-limits.json' '.github/scripts/pr-merge/merge-holds.mjs' '.github/scripts/pr-merge/enable-auto-merge.mjs' '.github/scripts/pr-merge/superseded.mjs' '.github/scripts/pr-merge/clear-escalations.mjs' '.github/scripts/pr-merge/carry-run-cache.mjs' '.github/scripts/pr-merge/gate-check.mjs' '.github/scripts/pr-merge/head-ref-map.mjs' '.github/scripts/pr-merge/lane-records.mjs' '.github/scripts/pr-merge/last-report.mjs' '.github/scripts/pr-merge/rate-stop.bash' '.github/scripts/pr-merge/retry-call.bash' '.github/scripts/pr-merge/refire-refused.mjs' '.github/scripts/pr-merge/rotation.mjs' '.github/scripts/pr-merge/sweep-summary.mjs' '.github/scripts/pr-merge/who.mjs' '.github/scripts/pr-merge/post-unarmed-status.mjs' '.github/scripts/pr-merge/verify-answers.mjs' '.github/actions/report-check-run/posting-run.mjs' 'scripts/sync-hold-floor.py' '.github/scripts/pr-unblock/address-feedback.mjs' '.github/scripts/pr-unblock/escalation-routes.mjs' '.github/scripts/pr-unblock/escalation-routes.json' '.github/scripts/pr-unblock/gate-source.mjs' '.github/scripts/pr-unblock/thread-scope.mjs' '.github/scripts/pr-unblock/retry-fetch.mjs' '.github/scripts/work-resolve-issue/report-outcome.mjs' '.github/scripts/work-resolve-issue/limits.mjs' '.github/scripts/work-resolve-issue/retry-fetch.mjs' '.github/scripts/pr-unblock/report-escalation.sh' '.github/scripts/pr-unblock/adjudicate-feedback.mjs' '.github/scripts/pr-unblock/publish-feedback.mjs' '.github/scripts/pr-unblock/resolve-threads.mjs' '.github/scripts/pr-unblock/request-budget.mjs' '.github/scripts/pr-verify/resolve-pr.mjs' '.github/scripts/pr-verify/write-verdict.mjs' '.github/scripts/intake-triage/admit.mjs' '.github/actions/select-trusted-text/select-trusted-text.mjs' 'lib/publish-gate.mjs' '.github/recurring-schedule.json' 'plugins/work-items/scripts/charge-words.json' '.github/scripts/pr-unblock/dependency-classify.mjs' '.github/scripts/apply-patch/apply-patch.mjs' '.github/scripts/apply-patch/predicates.mjs' '.github/scripts/apply-patch/protected-paths.mjs' '.github/scripts/apply-patch/retry-fetch.mjs' '.github/scripts/pr-unblock/fix-ci-context.mjs' '.github/scripts/pr-unblock/feedback-dispatch.mjs' '.github/scripts/pr-unblock/verify-notes.mjs' '.github/scripts/pr-unblock/earlier-fail-notes.mjs' '.github/scripts/pr-unblock/limit-stop.mjs' '.github/scripts/lib/lane-outcome.mjs' '.github/scripts/pr-unblock/dispatch-record.mjs' '.github/scripts/pr-unblock/verify-cap.mjs' '.github/scripts/maintenance-reconcile-lost-runs/infra-failure.mjs' '.github/scripts/work-resolve-issue/stop-routes.json' '.github/scripts/work-resolve-issue/stop-routes.mjs' '.github/scripts/work-resolve-issue/known-names.json' '.github/scripts/lib/stop-reason.mjs' '.github/scripts/lib/admission-comment.mjs' '.github/scripts/lib/done-evidence.mjs' '.github/scripts/pr-review-main/*' 'docs/conventions/instruction-exception-register/*' 'docs/conventions/untrusted-content/*' 'docs/standards/*' 'docs/review/*' '.github/workflows/dependabot-fragments.yml' '.github/workflows/eval-browser-dispatch.yml' '.github/workflows/intake-reenter.yml' '.github/workflows/intake-triage.yml' '.github/workflows/maintenance-audit-lane-runs.yml' '.github/workflows/maintenance-reconcile-lost-runs.yml' '.github/workflows/maintenance-tidy-code-upkeep.yml' '.github/workflows/post-merge-demotion-alert.yml' '.github/workflows/post-merge-test-mutation.yml' '.github/workflows/pr-feedback-dispatch.yml' '.github/workflows/pr-merge.yml' '.github/workflows/pr-refine.yml' '.github/workflows/pr-require-checks.yml' '.github/workflows/pr-review-approved.yml' '.github/workflows/pr-review-comment-relay.yml' '.github/workflows/pr-review-hosted.yml' '.github/workflows/pr-review-main.yml' '.github/workflows/pr-review-security-hosted.yml' '.github/workflows/pr-run-activity-read.yml' '.github/workflows/pr-run-activity-write.yml' '.github/workflows/pr-unblock-dispatch.yml' '.github/workflows/pr-unblock.yml' '.github/workflows/pr-verify-capture.yml' '.github/workflows/pr-verify.yml' '.github/workflows/release-publish-plugins.yml' '.github/workflows/upstream-apply-changelog.yml' '.github/workflows/work-resolve-issue.yml' '.github/workflows/work-unblock.yml') # merge.diff-check.path-holds is false: only Dependabot stays held. [ "$AUTHOR_ID" = 49699333 ] || hold_patterns=() # END hold-floor hold_patterns diff --git a/.github/workflows/pr-review-hosted.yml b/.github/workflows/pr-review-hosted.yml index 924a30ca084..68a6542fc9b 100644 --- a/.github/workflows/pr-review-hosted.yml +++ b/.github/workflows/pr-review-hosted.yml @@ -343,7 +343,6 @@ jobs: .github/scripts/work-resolve-issue/limits.mjs .github/scripts/work-resolve-issue/retry-fetch.mjs .github/scripts/pr-unblock/report-escalation.sh - .github/scripts/pr-unblock/read-pr-head.bash .github/scripts/pr-unblock/adjudicate-feedback.mjs .github/scripts/pr-unblock/publish-feedback.mjs .github/scripts/pr-unblock/resolve-threads.mjs diff --git a/.github/workflows/pr-review-security-hosted.yml b/.github/workflows/pr-review-security-hosted.yml index 387b224bafd..ef2367998c8 100644 --- a/.github/workflows/pr-review-security-hosted.yml +++ b/.github/workflows/pr-review-security-hosted.yml @@ -343,7 +343,6 @@ jobs: .github/scripts/work-resolve-issue/limits.mjs .github/scripts/work-resolve-issue/retry-fetch.mjs .github/scripts/pr-unblock/report-escalation.sh - .github/scripts/pr-unblock/read-pr-head.bash .github/scripts/pr-unblock/adjudicate-feedback.mjs .github/scripts/pr-unblock/publish-feedback.mjs .github/scripts/pr-unblock/resolve-threads.mjs diff --git a/docs/conventions/pr-pipeline.yaml b/docs/conventions/pr-pipeline.yaml index 8e42e652b32..aa20e40fc54 100644 --- a/docs/conventions/pr-pipeline.yaml +++ b/docs/conventions/pr-pipeline.yaml @@ -1518,8 +1518,6 @@ merge: class: enforcement - glob: ".github/scripts/pr-unblock/report-escalation.sh" class: enforcement - - glob: ".github/scripts/pr-unblock/read-pr-head.bash" - class: enforcement - glob: ".github/scripts/pr-unblock/adjudicate-feedback.mjs" class: enforcement - glob: ".github/scripts/pr-unblock/publish-feedback.mjs" From ece8bd8f8082accfdd40439d76b6fb64cb87e150 Mon Sep 17 00:00:00 2001 From: Kyle Sexton <153232337+kyle-sexton@users.noreply.github.com> Date: Wed, 4 Nov 2026 02:04:54 -0500 Subject: [PATCH 5/5] test(ci): give the select-head step test a .base checkout resolve-conflicts-select's head step now sources read-pr-head.bash from the default-branch checkout at .base, and the callers test ran the step in a directory with none. Run it in a work directory whose .base links to the repository root, the layout the job's checkout gives it. Co-Authored-By: Claude Opus 5.5 --- .github/actions/resolve-config/callers.test.mjs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/actions/resolve-config/callers.test.mjs b/.github/actions/resolve-config/callers.test.mjs index 5df39c1a649..9c95640474c 100644 --- a/.github/actions/resolve-config/callers.test.mjs +++ b/.github/actions/resolve-config/callers.test.mjs @@ -6480,10 +6480,15 @@ test("pr-unblock.yml: resolve-conflicts-select skips a PR whose base is not the const dir = tempDir("pr-unblock-base-"); // A stand-in gh answering the pulls read's --jq with the head SHA, base ref and head repository. writeFileSync(join(dir, "gh"), `#!/usr/bin/env bash\nprintf '%s\\n' "${"d".repeat(40)}" "$FAKE_BASE" o/r\n`, { mode: 0o755 }); + // The step sources its read helper from the default-branch checkout at .base. + const work = join(dir, "work"); + mkdirSync(work); + symlinkSync(fileURLToPath(new URL("../../..", import.meta.url)), join(work, ".base")); const head = (base) => { const out = join(dir, `out-${base.replaceAll("/", "-")}`); writeFileSync(out, ""); const run = spawnSync("bash", ["-c", step("head").run], { + cwd: work, encoding: "utf8", env: { ...process.env, PATH: `${dir}:${process.env.PATH}`, FAKE_BASE: base, PR_NUMBER: "10981", GITHUB_REPOSITORY: "o/r", DEFAULT_BRANCH: "main", GITHUB_OUTPUT: out }, });