Files
wehub-resource-sync 4b6817381b
CI (OpenClaw E2E) / openclaw test (push) Has been cancelled
CI / coverage-report (push) Has been cancelled
CI / test-kubernetes (push) Has been cancelled
CI / should-run-thorough (push) Has been cancelled
CI / test-thorough (cloudwatch-demo) (push) Has been cancelled
CI / test-thorough (flink-ecs) (push) Has been cancelled
CI / test-thorough (upstream-lambda) (push) Has been cancelled
CI / test-thorough (prefect-ecs-fargate) (push) Has been cancelled
Release / build-binaries (zip, opensre.exe, onefile, windows-latest, windows-x64) (push) Has been cancelled
Benchmark image — build + push to ECR (any adapter) / build + push (push) Has been cancelled
CI / quality (ubuntu-latest) (push) Has been cancelled
CI / test (tools-runtime) (push) Has been cancelled
CI / test (e2e-general) (push) Has been cancelled
CI / test (cli-runtime) (push) Has been cancelled
CI / test (e2e-provider-and-openclaw) (push) Has been cancelled
CI / test (integrations-and-misc) (push) Has been cancelled
Release / verify (push) Has been cancelled
Release / build-python-dist (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, macos-15-intel, darwin-x64) (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, macos-latest, darwin-arm64) (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04, linux-x64) (push) Has been cancelled
Release / publish-release (push) Has been cancelled
Release / publish-main-release (push) Has been cancelled
Interactive Shell Live (PR + post-merge) / turn-checks (no-LLM) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Interactive Shell Live (PR + post-merge) / turn-live shard ${{ matrix.shard_index }} (push) Has been cancelled
Release / prepare (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04-arm, linux-arm64) (push) Has been cancelled
Synthetic Deterministic Tests / Synthetic offline (deterministic) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:10:45 +08:00

182 lines
7.4 KiB
Python

"""Orchestrate shipping a Sentry fix: fresh branch -> commit -> push -> open PR.
Runs only after the coding agent has left a successful fix in the workspace
working tree. Sequences the safe git primitives in :mod:`integrations.git` and the
PR call in :mod:`pr`, and enforces the "never touch the base branch" guarantee: the
fix always lands on a namespaced ``opensre/sentry-fix-*`` branch, the base branch is
never committed to or pushed, and the PR is opened *into* the base branch.
Git primitives raise a neutral :class:`GitCommandError`; this module maps its
``kind`` onto the tool's :class:`FixIssueError` error surface.
"""
from __future__ import annotations
import re
from collections.abc import Mapping, Sequence
from dataclasses import dataclass
from integrations.coding_agent import CodingResult
from integrations.git import (
GitCommandError,
changed_paths,
checkout_branch,
commit_paths,
create_branch,
current_branch,
default_branch,
ensure_git_repo,
file_fingerprints,
push_branch,
short_head,
)
from integrations.github.client import resolve_github_token
from tools.cross_vendor.fix_sentry_issue.errors import ERR_NO_CHANGES, ERR_PR_FAILED, FixIssueError
from tools.cross_vendor.fix_sentry_issue.pr import PullRequest, open_pull_request
_BRANCH_PREFIX = "opensre/sentry-fix"
_SUBJECT_MAX = 72
@dataclass(frozen=True)
class ShipResult:
"""Outcome of shipping a fix as a pull request."""
branch_name: str
pr: PullRequest
def _slug(issue_id: str) -> str:
"""Filesystem/ref-safe slug for the issue id (falls back to 'issue')."""
cleaned = re.sub(r"[^A-Za-z0-9_-]+", "-", issue_id).strip("-")
return cleaned or "issue"
def build_branch_name(workspace: str, issue_id: str) -> str:
"""Namespaced, unique-ish branch name for this fix (never the base branch)."""
suffix = short_head(workspace) or "wip"
return f"{_BRANCH_PREFIX}-{_slug(issue_id)}-{suffix}"
def _commit_message(issue_id: str, sentry_url: str, summary: str) -> str:
subject_body = summary.strip().splitlines()[0] if summary.strip() else "apply proposed fix"
subject = f"fix: resolve Sentry issue {issue_id}{subject_body}"[:_SUBJECT_MAX]
lines = [subject, ""]
if summary.strip():
lines += [summary.strip(), ""]
lines.append(f"Generated by OpenSRE (fix_sentry_issue) from {sentry_url}.")
return "\n".join(lines)
def _pr_body(issue_id: str, sentry_url: str, result: CodingResult, changed: Sequence[str]) -> str:
lines = [
f"Automated fix proposed by OpenSRE for Sentry issue [{issue_id}]({sentry_url}).",
"",
"**Summary**",
result.summary.strip() or "_(no summary provided)_",
]
if changed:
lines += ["", "**Files changed**"]
lines += [f"- `{path}`" for path in changed]
footer = (
"> Generated by the OpenSRE `fix_sentry_issue` tool (automated coding agent). "
"Review before merging."
)
lines += ["", footer]
return "\n".join(lines)
def ship_fix(
workspace: str,
*,
issue_id: str,
sentry_url: str,
result: CodingResult,
github_token: str | None = None,
baseline: Mapping[str, str] | None = None,
) -> ShipResult:
"""Branch, commit, push, and open a PR for the fix in *workspace*.
Assumes the coding run already succeeded and left changes in the working tree.
*baseline* is a ``{path: content-hash}`` fingerprint of files already dirty
**before** the run (see the tool's pre-run snapshot). A dirty file is committed
only if it is new or its content changed since the baseline — so the agent's
edits are always shipped (even to a file that was already dirty), while
pre-existing WIP the agent left untouched is excluded. Raises
:class:`FixIssueError` (with a stable ``kind``) on any failure.
"""
# One credential for both the push and the PR call, so the push doesn't fall
# back to a stale cached git credential (the usual cause of a 403 on push).
token = resolve_github_token(github_token)
# Pre-branch phase: nothing has been created yet, so failures carry no branch.
try:
ensure_git_repo(workspace)
# ``result.changed_files`` and a plain worktree scan both report the whole
# tree vs HEAD, so neither isolates the agent's edits from pre-existing WIP.
# Compare content hashes against the pre-run baseline: keep files the agent
# created or actually modified.
pre_existing = dict(baseline or {})
current = changed_paths(workspace)
current_fingerprints = file_fingerprints(workspace, current)
changed = [
path
for path in current
if path not in pre_existing or current_fingerprints.get(path, "") != pre_existing[path]
]
if not changed:
reason = (
"the coding agent made no changes to the repository"
if not current
else "the coding agent left no new changes (only pre-existing work-in-progress)"
)
raise FixIssueError(
ERR_NO_CHANGES,
f"Nothing to commit — {reason}; nothing to open a PR for. "
"Try re-running or setting a stronger CODING_MODEL.",
)
base = default_branch(workspace, token=token or None)
if not base:
# Don't guess the base — a PR against the wrong branch is worse than a
# clear failure. This only happens when origin/HEAD is unset AND the
# remote is unreachable.
raise FixIssueError(
ERR_PR_FAILED,
"Could not determine the base branch to open the PR against "
"(origin/HEAD is not configured and the remote is unreachable). "
"Run `git remote set-head origin -a` in the workspace, or check connectivity.",
)
branch = build_branch_name(workspace, issue_id)
# Branch off the resolved base, never off whatever the workspace happens to
# have checked out (e.g. an unrelated local feature branch) — unless a prior
# attempt already left us on this exact fix branch (retry after a partial
# failure below), in which case re-branching would just fail as "exists".
if current_branch(workspace) != branch:
checkout_branch(workspace, base)
create_branch(workspace, branch, base_default=base)
except GitCommandError as exc:
raise FixIssueError(exc.kind, exc.message) from exc
# From here the workspace is on ``branch``. Any failure (commit, push, or PR)
# leaves the user's changes on that branch, so tag it onto the error for the
# caller to surface for manual recovery instead of reporting an empty branch.
try:
commit_paths(workspace, changed, _commit_message(issue_id, sentry_url, result.summary))
push_branch(workspace, branch, base_default=base, token=token or None)
pr = open_pull_request(
workspace,
head_branch=branch,
base_branch=base,
title=f"fix: resolve Sentry issue {issue_id}",
body=_pr_body(issue_id, sentry_url, result, changed),
github_token=token or None,
)
except GitCommandError as exc:
raise FixIssueError(exc.kind, exc.message, branch_name=branch) from exc
except FixIssueError as exc:
exc.branch_name = branch
raise
return ShipResult(branch_name=branch, pr=pr)