fix(open_pr): reuse config for GitHub token; defer installation token lookup (#1227)

Calling get_github_token() without arguments always invoked LangGraph get_config internally,
which broke tests that only patch agent.middleware.open_pr.get_config and failed outside
runnable context.

Extend get_github_token with an optional runnable config mapping; the middleware passes
the config dict already resolved from get_config(). Request GitHub App installation tokens
only after detecting sandbox/repo changes worth publishing.

Fixes failing Agent unit tests in tests/test_open_pr_middleware.py.
This commit is contained in:
Johannes du Plessis 2026-04-30 17:48:15 -07:00 • committed by GitHub
parent b5ed2a6b8b
commit 6ef70823be
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 16 additions and 10 deletions

View file

@ -93,18 +93,13 @@ async def open_pr_if_needed(
pr_title = pr_payload.get("title", "feat: Open SWE PR")
pr_body = pr_payload.get("body", "Automated PR created by Open SWE agent.")
commit_message = pr_payload.get("commit_message", pr_title)
github_token = get_github_token()
github_token = get_github_token(config)
user_identity = await asyncio.to_thread(
resolve_triggering_user_identity, config, github_token
)
pr_body = add_pr_collaboration_note(pr_body, user_identity)
commit_message = add_user_coauthor_trailer(commit_message, user_identity)
installation_token = await get_github_app_installation_token()
if not installation_token:
logger.error("Failed to get GitHub App installation token for thread %s", thread_id)
return None
if not thread_id:
raise ValueError("No thread_id found in config")
@ -132,6 +127,11 @@ async def open_pr_if_needed(
logger.info("No changes detected, skipping PR creation")
return None
installation_token = await get_github_app_installation_token()
if not installation_token:
logger.error("Failed to get GitHub App installation token for thread %s", thread_id)
return None
logger.info("Changes detected, preparing PR for thread %s", thread_id)
metadata = config.get("metadata", {})

View file

@ -3,6 +3,7 @@
from __future__ import annotations
import logging
from collections.abc import Mapping
from typing import Any
from langgraph.config import get_config
@ -30,10 +31,15 @@ def _decrypt_github_token(encrypted_token: str | None) -> str | None:
return decrypt_token(encrypted_token)
def get_github_token() -> str | None:
"""Resolve a GitHub token from run metadata."""
config = get_config()
return _decrypt_github_token(_read_encrypted_github_token(config.get("metadata", {})))
def get_github_token(run_config: Mapping[str, Any] | None = None) -> str | None:
"""Resolve a GitHub token from run metadata.
Pass ``run_config`` when LangGraph runnable config is already available (e.g. after
``get_config()`` in callers). Omit to read from ``get_config()`` (required runnable
context).
"""
resolved = run_config if run_config is not None else get_config()
return _decrypt_github_token(_read_encrypted_github_token(resolved.get("metadata", {})))
async def get_github_token_from_thread(thread_id: str) -> tuple[str | None, str | None]: