From 08adb26da9f36b2748a40b665b4ceb11b35b284e Mon Sep 17 00:00:00 2001 From: TomasPalsson Date: Tue, 29 Sep 2026 18:21:47 +0000 Subject: [PATCH 1/2] flow next: find a PR pushed under a different public branch name --- plugins/flow/bin/lib/router.js | 23 +++++++++++++------- plugins/flow/scripts/tests/test_next.sh | 29 +++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 8 deletions(-) diff --git a/plugins/flow/bin/lib/router.js b/plugins/flow/bin/lib/router.js index ee385ba..fe4f968 100644 --- a/plugins/flow/bin/lib/router.js +++ b/plugins/flow/bin/lib/router.js @@ -338,16 +338,23 @@ function passCommand(root) { // prNumber(root, branch) -> "41" | null. gh is optional: no gh, no network, // or any failure all mean "no PR", which routes to 11 (open one) not 12. +// Stealth pushes HEAD:, often from a fork, so `pr view ` +// misses; fall back to a head-name lookup under the branch's upstream name. function prNumber(root, branch) { if (!branch || branch === 'HEAD') return null; - const r = spawnSync('gh', ['pr', 'view', branch, '--json', 'number', '-q', '.number'], { - cwd: root, - encoding: 'utf8', - timeout: 15000, - }); - if (r.status !== 0) return null; - const n = (r.stdout || '').trim(); - return /^\d+$/.test(n) ? n : null; + const ghNumber = (args) => { + const r = spawnSync('gh', args, { cwd: root, encoding: 'utf8', timeout: 15000 }); + if (r.status !== 0) return null; + const n = (r.stdout || '').trim(); + return /^\d+$/.test(n) ? n : null; + }; + const direct = ghNumber(['pr', 'view', branch, '--json', 'number', '-q', '.number']); + if (direct) return direct; + const merge = git(root, ['config', `branch.${branch}.merge`]).out; + const name = merge.replace(/^refs\/heads\//, ''); + if (!name || name === merge || name === branch) return null; + return ghNumber(['pr', 'list', '--head', name, '--author', '@me', '--state', 'all', + '--json', 'number', '-q', '.[0].number']); } // ───────────────────────────────────────────────────────────────────────────── diff --git a/plugins/flow/scripts/tests/test_next.sh b/plugins/flow/scripts/tests/test_next.sh index 1603faa..2d9d721 100644 --- a/plugins/flow/scripts/tests/test_next.sh +++ b/plugins/flow/scripts/tests/test_next.sh @@ -584,6 +584,35 @@ Verified: 2026-09-08 by user" \ rm -rf "$home" "$proj" } +t_next_row12_pr_found_by_upstream_name() { + # stealth pushes HEAD:, so `gh pr view ` finds + # nothing; the PR is only findable by the upstream branch name + local home proj base sha + home=$(tmp_dir) + proj=$(tmp_repo) + base=$(git -C "$proj" rev-parse --short HEAD) + nx_feature "$proj" 001-x + sha=$(nx_commit "$proj" a.py) + nx_tasks "$proj/.specs/001-x/TASKS.md" "$base" "Approved: 2026-09-08 by user +Verified: 2026-09-08 by user" \ + "- [x] T001 a — files: a.py — verify: \`true\` — done: $sha" \ + '' '## Gates' "- [x] G001 clean — verify: \`true\` — done: $sha" + printf 'gates green\n' >"$proj/.specs/001-x/PASS-$sha.md" + git -C "$proj" checkout -qb flow/x + printf '#!/bin/sh\ncase "$*" in\n"pr list --head feat/x "*) echo 77 ;;\n*) exit 1 ;;\nesac\n' >"$home/gh" + chmod +x "$home/gh" + run_cmd bash -c 'cd "$1" || exit 1; export HOME="$2"; export PATH="$2:$PATH"; shift 2; exec "$@"' \ + _ "$proj" "$home" node "$NX_CLI_PATH" next --json + assert_contains "$OUT" '"state": "shippable"' "no upstream configured: the old no-PR answer holds" + git -C "$proj" config branch.flow/x.remote fork + git -C "$proj" config branch.flow/x.merge refs/heads/feat/x + run_cmd bash -c 'cd "$1" || exit 1; export HOME="$2"; export PATH="$2:$PATH"; shift 2; exec "$@"' \ + _ "$proj" "$home" node "$NX_CLI_PATH" next --json + assert_contains "$OUT" '"state": "shipped"' "a PR under the upstream branch name is found" + assert_contains "$OUT" 'PR #77' "and its number is reported" + rm -rf "$home" "$proj" +} + t_next_row13_idle() { local home proj home=$(tmp_dir) From 765346b6902f17e2bd89ea33c6d51787d88b4983 Mon Sep 17 00:00:00 2001 From: TomasPalsson Date: Tue, 29 Sep 2026 18:24:16 +0000 Subject: [PATCH 2/2] flow next: match the fallback PR by head-repo owner, not author --- plugins/flow/bin/lib/router.js | 8 ++++++-- plugins/flow/scripts/tests/test_next.sh | 7 ++++++- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/plugins/flow/bin/lib/router.js b/plugins/flow/bin/lib/router.js index fe4f968..cae970a 100644 --- a/plugins/flow/bin/lib/router.js +++ b/plugins/flow/bin/lib/router.js @@ -353,8 +353,12 @@ function prNumber(root, branch) { const merge = git(root, ['config', `branch.${branch}.merge`]).out; const name = merge.replace(/^refs\/heads\//, ''); if (!name || name === merge || name === branch) return null; - return ghNumber(['pr', 'list', '--head', name, '--author', '@me', '--state', 'all', - '--json', 'number', '-q', '.[0].number']); + const remote = git(root, ['config', `branch.${branch}.remote`]).out; + const url = remote ? git(root, ['config', `remote.${remote}.url`]).out : ''; + const m = url.match(/^(?:[\w.-]+@[^:/]+:|[a-z+]+:\/\/(?:[^@/]+@)?[^/]+\/)([A-Za-z0-9-]+)\/[^/]+?(?:\.git)?\/?$/i); + if (!m) return null; + return ghNumber(['pr', 'list', '--head', name, '--state', 'all', '--json', 'number,headRepositoryOwner', + '-q', `[.[] | select(.headRepositoryOwner.login == "${m[1]}")][0].number`]); } // ───────────────────────────────────────────────────────────────────────────── diff --git a/plugins/flow/scripts/tests/test_next.sh b/plugins/flow/scripts/tests/test_next.sh index 2d9d721..59d1aa5 100644 --- a/plugins/flow/scripts/tests/test_next.sh +++ b/plugins/flow/scripts/tests/test_next.sh @@ -599,13 +599,18 @@ Verified: 2026-09-08 by user" \ '' '## Gates' "- [x] G001 clean — verify: \`true\` — done: $sha" printf 'gates green\n' >"$proj/.specs/001-x/PASS-$sha.md" git -C "$proj" checkout -qb flow/x - printf '#!/bin/sh\ncase "$*" in\n"pr list --head feat/x "*) echo 77 ;;\n*) exit 1 ;;\nesac\n' >"$home/gh" + printf '#!/bin/sh\ncase "$*" in\n"pr list --head feat/x "*someone*) echo 77 ;;\n*) exit 1 ;;\nesac\n' >"$home/gh" chmod +x "$home/gh" run_cmd bash -c 'cd "$1" || exit 1; export HOME="$2"; export PATH="$2:$PATH"; shift 2; exec "$@"' \ _ "$proj" "$home" node "$NX_CLI_PATH" next --json assert_contains "$OUT" '"state": "shippable"' "no upstream configured: the old no-PR answer holds" git -C "$proj" config branch.flow/x.remote fork git -C "$proj" config branch.flow/x.merge refs/heads/feat/x + git -C "$proj" config remote.fork.url /somewhere/local/path + run_cmd bash -c 'cd "$1" || exit 1; export HOME="$2"; export PATH="$2:$PATH"; shift 2; exec "$@"' \ + _ "$proj" "$home" node "$NX_CLI_PATH" next --json + assert_contains "$OUT" '"state": "shippable"' "a remote url with no parseable owner is not guessed at" + git -C "$proj" config remote.fork.url git@github.com:someone/repo.git run_cmd bash -c 'cd "$1" || exit 1; export HOME="$2"; export PATH="$2:$PATH"; shift 2; exec "$@"' \ _ "$proj" "$home" node "$NX_CLI_PATH" next --json assert_contains "$OUT" '"state": "shipped"' "a PR under the upstream branch name is found"