fix(tangled_pr): POST the htmx /pulls/new form, not /pulls/

/pulls/ is 405. The appview's PR form is htmx: it only processes the create when
it sees HX-Request, otherwise it re-renders the page — a 200 that silently creates
nothing. Success comes back as HX-Redirect (…/pulls/<n>), not a 3xx Location, so
the old redirect check never fired. Also send source=branch + the title/body Dirty
flags the form expects, and point the auth-failure hint at the current cookie path.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UWTdUq2bsic7JZGqJp3nD6
This commit is contained in:
2026-08-01 16:36:09 +00:00
co-authored by Claude
parent 5a993aacba
commit 26d93c1eda
+25 -15
View File
@@ -49,17 +49,26 @@ def main():
a = ap.parse_args()
cookie = load_cookie(a.cookie_file)
url = f"{BASE}/{a.owner}/{a.repo}/pulls/"
form = {"targetBranch": a.target, "sourceBranch": a.source}
# Tangled's PR form is htmx: it POSTs to /pulls/new (NOT /pulls/, which is 405) and only processes
# the create when it sees the HX-Request header — otherwise it just re-renders the page (a 200 that
# creates nothing). Success is signalled by an HX-Redirect header pointing at the new pull.
new_url = f"{BASE}/{a.owner}/{a.repo}/pulls/new"
form = {
"source": "branch", # branch-compare mode (each PR targets the branch below it)
"targetBranch": a.target,
"sourceBranch": a.source,
"title": a.title, "titleDirty": "true",
"body": a.body, "bodyDirty": "true",
}
if a.fork: form["fork"] = a.fork
if a.title: form["title"] = a.title
if a.body: form["body"] = a.body
data = urllib.parse.urlencode(form).encode()
req = urllib.request.Request(url, data=data, method="POST", headers={
req = urllib.request.Request(new_url, data=data, method="POST", headers={
"Cookie": cookie,
"Content-Type": "application/x-www-form-urlencoded",
"User-Agent": "tangled-pr-bot",
"Referer": f"{BASE}/{a.owner}/{a.repo}/pulls/new",
"HX-Request": "true",
"HX-Current-URL": new_url,
"Referer": new_url,
})
class NoRedirect(urllib.request.HTTPRedirectHandler):
@@ -68,17 +77,18 @@ def main():
opener = urllib.request.build_opener(NoRedirect)
try:
r = opener.open(req, timeout=90)
code, loc, body = r.getcode(), r.headers.get("Location", ""), r.read(3000).decode("utf-8", "replace")
hdrs, code, body = r.headers, r.getcode(), r.read(3000).decode("utf-8", "replace")
except urllib.error.HTTPError as e:
code, loc, body = e.code, e.headers.get("Location", ""), e.read(3000).decode("utf-8", "replace")
print(f"HTTP {code}" + (f" -> {loc}" if loc else ""))
if code in (301, 302, 303, 307, 308) and "/pulls/" in loc and "/new" not in loc:
print("OK: pull created ->", (BASE + loc) if loc.startswith("/") else loc)
hdrs, code, body = e.headers, e.code, e.read(3000).decode("utf-8", "replace")
# htmx success is signalled by HX-Redirect (…/pulls/<n>), not a normal 3xx Location.
target = hdrs.get("HX-Redirect", "") or hdrs.get("HX-Location", "") or hdrs.get("Location", "")
print(f"HTTP {code}" + (f" -> {target}" if target else ""))
if target and "/pulls/" in target and "/new" not in target:
print("OK: pull created ->", (BASE + target) if target.startswith("/") else target)
return
if code in (301, 302, 303) and ("/login" in loc or "oauth" in loc):
sys.exit("AUTH FAILED: session cookie expired/invalid — redo the one-time browser login "
"and update tools/.tangled-session")
if ("/login" in target or "oauth" in target.lower()):
sys.exit("AUTH FAILED: session cookie expired/invalid — refresh engine/.tangled-session "
"(scripts/get-tangled-cookie.py)")
# otherwise surface whatever the page said (a Notice, etc.)
snippet = " ".join(body.split())[:600]
print(" no clear success redirect — response snippet:")