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
4 changes: 2 additions & 2 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ jobs:
python -m pip install --no-deps -e .
python -m pip install \
"PyGithub==2.4.0" "PyJWT[crypto]==2.13.0" \
"aiohttp==3.14.1" "ipython==8.10.0" \
"aiohttp==3.14.3" "ipython==8.10.0" \
"langchain-core==1.3.3" "langchain-mistralai==1.0.0" \
"langchain-openai==1.1.14" "langchain-text-splitters==1.1.2" \
"nest-asyncio==1.6.0" "numpy==1.26.4" "pydantic==2.10.0" \
Expand Down Expand Up @@ -129,7 +129,7 @@ jobs:
assert callable(Repository.get_ci_status_with_status)
assert callable(Repository.read_text_file_bounded)
assert BoundedTextReadResult.__name__ == "BoundedTextReadResult"
assert llama_github.__version__ == "0.4.5"
assert llama_github.__version__ == "0.4.6"
PY

dependency-audit:
Expand Down
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,16 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [0.4.6] - 2026-08-13

### Changed
- Requested GitHub's latest check-run view, retained stable run/app/suite identities, and reconciled obsolete attempts without merging equal check names from distinct suites or apps
- Added page counts and honest truncation/error metadata to bounded status and check-run retrieval while retaining already observed evidence when a later page or CI surface fails

### Fixed
- Removed the synthetic space after unified diff addition/deletion markers so new-file column-zero content and real indentation remain distinguishable from tool decoration
- Raised the `aiohttp` security floor to `3.14.3`

## [0.4.5] - 2026-07-20

### Added
Expand Down
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ pip install llama-github

Current maintained runtime target: Python `3.10` through `3.14`.

Version `0.4.5` keeps the existing high-level API while allowing retrieval-only
Version `0.4.6` keeps the existing high-level API while allowing retrieval-only
integrations to import GitHub and diff helpers without loading the ML/RAG dependency
stack. Built-in Jina models are pinned to immutable revisions; custom remote-code
models should provide their own immutable revision. Pull-request retrieval now keeps
Expand All @@ -30,6 +30,9 @@ opt into typed, 2 MiB-capped
lockfile or CI-config reads without weakening the generic file policy. Memory-sensitive
PR consumers can also opt into per-file and cumulative source limits; oversized source
falls back to GitHub's changed-file patch without changing the default retrieval API.
Exact-head CI snapshots expose stable check-run identity, latest-attempt semantics,
bounded page completeness, and retained evidence from partial failures. Synthetic
new-file/deletion diffs use standard unified markers without inserting source spaces.

## Usage

Expand Down
152 changes: 124 additions & 28 deletions llama_github/data_retrieval/github_entities.py
Original file line number Diff line number Diff line change
Expand Up @@ -410,18 +410,37 @@ def _bounded_github_call(
fetch: Callable[[], Any],
max_items: int,
) -> RetrievalResult:
"""Preserve items already fetched when a paginated PyGithub call fails."""
"""Preserve bounded items and page truth when a GitHub call fails."""
if max_items <= 0:
raise ValueError("max_items must be positive")

values = []
pages_fetched = 0
try:
iterator = iter(fetch())
for _ in range(max_items + 1):
try:
values.append(next(iterator))
except StopIteration:
break
result = fetch()
get_page = getattr(result, "get_page", None)
if callable(get_page):
page_index = 0
while len(values) <= max_items:
page = list(get_page(page_index))
pages_fetched += 1
if not page:
break
remaining = max_items + 1 - len(values)
values.extend(page[:remaining])
if len(values) > max_items:
break
page_index += 1
else:
# Plain lists and legacy iterators have no page boundary to
# expose. Treat the single bounded traversal as one page.
pages_fetched = 1
iterator = iter(result)
for _ in range(max_items + 1):
try:
values.append(next(iterator))
except StopIteration:
break
except Exception as exc:
retained = values[:max_items]
return RetrievalResult(
Expand All @@ -431,7 +450,10 @@ def _bounded_github_call(
if retained
else RetrievalOutcome.ERROR
),
truncated=bool(retained),
pages_fetched=pages_fetched,
# A transport failure makes the result incomplete, but it
# does not prove the configured item bound was exceeded.
truncated=len(values) > max_items,
status_code=getattr(exc, "status", None),
error_type=type(exc).__name__,
)
Expand All @@ -445,9 +467,96 @@ def _bounded_github_call(
if truncated
else (RetrievalOutcome.OK if retained else RetrievalOutcome.NO_HIT)
),
pages_fetched=pages_fetched,
truncated=truncated,
)

@staticmethod
def _ci_scalar(value: Any) -> Any:
"""Return only stable JSON scalars from optional PyGithub fields."""
return (
value
if isinstance(value, (str, int)) and not isinstance(value, bool)
else None
)

def _check_run_payload(self, check_run: Any) -> dict:
"""Project a check run with enough identity for honest reconciliation."""
app = getattr(check_run, "app", None)
check_suite = getattr(check_run, "check_suite", None)
started_at = getattr(check_run, "started_at", None)
completed_at = getattr(check_run, "completed_at", None)
return {
"id": self._ci_scalar(getattr(check_run, "id", None)),
"name": getattr(check_run, "name", None),
"status": getattr(check_run, "status", None),
"conclusion": getattr(check_run, "conclusion", None),
"started_at": self.to_isoformat(started_at) if started_at else None,
"completed_at": (
self.to_isoformat(completed_at) if completed_at else None
),
# Keep the historical field bound to the GitHub check-run page.
"details_url": self._ci_scalar(getattr(check_run, "html_url", None)),
"external_details_url": self._ci_scalar(
getattr(check_run, "details_url", None)
),
"external_id": self._ci_scalar(getattr(check_run, "external_id", None)),
"app_id": self._ci_scalar(getattr(app, "id", None)),
"app_slug": self._ci_scalar(getattr(app, "slug", None)),
"check_suite_id": self._ci_scalar(getattr(check_suite, "id", None)),
}

@staticmethod
def _check_run_identity(check_run: dict) -> Optional[tuple]:
"""Identify attempts within one suite without merging other workflows/apps."""
suite_id = check_run.get("check_suite_id")
if suite_id is None:
return None
return (
check_run.get("app_id") or check_run.get("app_slug"),
suite_id,
str(check_run.get("name") or "unknown"),
)

@staticmethod
def _check_run_recency(check_run: dict) -> tuple:
"""Order attempts so a newer in-progress run supersedes stale completion."""
def sequence(value: Any) -> int:
try:
return int(value)
except (TypeError, ValueError):
return -1

return (
str(check_run.get("started_at") or check_run.get("completed_at") or ""),
str(check_run.get("completed_at") or ""),
sequence(check_run.get("id")),
)

@classmethod
def _current_check_runs(cls, check_runs: list[dict]) -> list[dict]:
"""Keep the latest run per app/suite/name and preserve distinct identities."""
current: dict[tuple, dict] = {}
ordered: list[tuple[Optional[tuple], Optional[dict]]] = []
for check_run in check_runs:
identity = cls._check_run_identity(check_run)
if identity is None:
# Without a suite identity, collapsing equal names could hide
# distinct workflows. Preserve the evidence instead.
ordered.append((None, check_run))
continue
existing = current.get(identity)
if existing is None:
ordered.append((identity, None))
if existing is None or cls._check_run_recency(
check_run
) >= cls._check_run_recency(existing):
current[identity] = check_run
return [
check_run if identity is None else current[identity]
for identity, check_run in ordered
]

@staticmethod
def _aggregate_retrieval_outcome(
statuses_result: RetrievalResult,
Expand Down Expand Up @@ -504,7 +613,7 @@ def get_ci_status_with_status(
max_statuses,
)
check_runs_result = self._bounded_github_call(
head_commit.get_check_runs,
lambda: head_commit.get_check_runs(filter="latest"),
max_check_runs,
)
statuses = [
Expand All @@ -530,25 +639,12 @@ def get_ci_status_with_status(
):
current_statuses[context] = status
statuses = list(current_statuses.values())
check_runs = [
{
"name": check_run.name,
"status": check_run.status,
"conclusion": check_run.conclusion,
"started_at": (
self.to_isoformat(check_run.started_at)
if check_run.started_at
else None
),
"completed_at": (
self.to_isoformat(check_run.completed_at)
if check_run.completed_at
else None
),
"details_url": check_run.html_url,
}
for check_run in check_runs_result.items
]
check_runs = self._current_check_runs(
[
self._check_run_payload(check_run)
for check_run in check_runs_result.items
]
)
status_states = {
str(status.get("state") or "").strip().lower()
for status in statuses
Expand Down
4 changes: 2 additions & 2 deletions llama_github/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,10 +73,10 @@ def generate_custom_diff(base_content: str, head_content: str, context_lines: in
return "" # Both contents are None, no diff to generate
elif base_content is None:
# File is newly added
return "".join(f"+ {line}\n" for line in head_content.splitlines())
return "".join(f"+{line}\n" for line in head_content.splitlines())
elif head_content is None:
# File is deleted
return "".join(f"- {line}\n" for line in base_content.splitlines())
return "".join(f"-{line}\n" for line in base_content.splitlines())

# Use empty strings for None content to ensure difflib handles them correctly
# as file additions or deletions. This is more robust and aligns with difflib's expectations.
Expand Down
2 changes: 1 addition & 1 deletion llama_github/version.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = "0.4.5"
__version__ = "0.4.6"
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ classifiers = [
dependencies = [
"PyGithub>=2.4,<3",
"PyJWT[crypto]>=2.13,<3",
"aiohttp>=3.14.1,<4",
"aiohttp>=3.14.3,<4",
"ipython>=8.10,<9",
"langchain-core>=1.3.3,<2",
"langchain-mistralai>=1.0,<2",
Expand Down
Loading