Skip to content
Open
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
8 changes: 6 additions & 2 deletions ymir/agents/backport_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
I_AM_YMIR,
ZSTREAM_TARGET_LABEL,
format_jira_links_for_mr,
format_zstream_branch_note,
mr_description_footer,
)
from ymir.agents.log_agent import create_log_agent
Expand Down Expand Up @@ -665,6 +666,7 @@ async def fork_and_prepare_dist_git(state):
state.update_branch,
state.fork_url,
_,
state.zstream_branch_created,
) = await tasks.fork_and_prepare_dist_git(
jira_issue=state.jira_issue,
package=state.package,
Expand Down Expand Up @@ -930,7 +932,8 @@ async def evaluate_inherit_source(state):
f"{triage_details_text}"
f"{format_jira_links_for_mr(state.jira_issue)}\n"
f"{wrap_details('Backporting steps', state.backport_log[-1])}"
f"\n\n{mr_description_footer(state.package)}"
f"\n\n{format_zstream_branch_note(state.zstream_branch_created)}"
f"{mr_description_footer(state.package)}"
)
state.backport_result = BackportOutputSchema(
success=True,
Expand Down Expand Up @@ -1403,7 +1406,8 @@ async def commit_push_and_open_mr(state):
f"{triage_details_text}"
f"{format_jira_links_for_mr(state.jira_issue)}\n"
f"{wrap_details('Backporting steps', state.backport_log[-1])}"
f"\n\n{mr_description_footer(state.package)}"
f"\n\n{format_zstream_branch_note(state.zstream_branch_created)}"
f"{mr_description_footer(state.package)}"
)
(
state.merge_request_url,
Expand Down
6 changes: 6 additions & 0 deletions ymir/agents/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,12 @@ def strip_resolves_from_mr_text(text: str) -> str:
return "\n".join(result).strip("\n")


def format_zstream_branch_note(note: str | None) -> str:
if not note:
return ""
return f"> **Note:** {note}\n\n"


def mr_description_footer(package: str) -> str:
return (
"---\n" # noqa: S608
Expand Down
1 change: 1 addition & 0 deletions ymir/agents/mr_consolidation_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -510,6 +510,7 @@ async def fork_and_prepare_dist_git(state):
state.update_branch,
state.fork_url,
_,
_,
) = await tasks.fork_and_prepare_dist_git(
jira_issue=working_id,
package=package,
Expand Down
1 change: 1 addition & 0 deletions ymir/agents/package_update_steps.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ class PackageUpdateState(BaseModel):
log_result: LogOutputSchema | None = Field(default=None)
merge_request_url: str | None = Field(default=None)
merge_request_newly_created: bool = Field(default=False) # was the MR newly created?
zstream_branch_created: str | None = Field(default=None)


class PackageUpdateStep:
Expand Down
5 changes: 4 additions & 1 deletion ymir/agents/rebase_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
I_AM_YMIR,
ZSTREAM_TARGET_LABEL,
format_jira_links_for_mr,
format_zstream_branch_note,
mr_description_footer,
)
from ymir.agents.log_agent import create_log_agent
Expand Down Expand Up @@ -354,6 +355,7 @@ async def fork_and_prepare_dist_git(state):
state.update_branch,
state.fork_url,
state.fedora_clone,
state.zstream_branch_created,
) = await tasks.fork_and_prepare_dist_git(
jira_issue=state.jira_issue,
package=state.package,
Expand Down Expand Up @@ -525,7 +527,8 @@ async def commit_push_and_open_mr(state):
f"{format_jira_links_for_mr(all_issues)}\n"
f"{wrap_details('Rebase status', state.rebase_log[-1])}"
f"{consolidation_text}"
f"\n\n{mr_description_footer(state.package)}"
f"\n\n{format_zstream_branch_note(state.zstream_branch_created)}"
f"{mr_description_footer(state.package)}"
),
available_tools=gateway_tools,
commit_only=dry_run,
Expand Down
5 changes: 4 additions & 1 deletion ymir/agents/rebuild_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
I_AM_YMIR,
ZSTREAM_TARGET_LABEL,
format_jira_links_for_mr,
format_zstream_branch_note,
mr_description_footer,
)
from ymir.agents.log_agent import create_log_agent
Expand Down Expand Up @@ -105,6 +106,7 @@ async def fork_and_prepare_dist_git(state):
state.update_branch,
state.fork_url,
_,
state.zstream_branch_created,
) = await tasks.fork_and_prepare_dist_git(
jira_issue=state.jira_issue,
package=state.package,
Expand Down Expand Up @@ -259,7 +261,8 @@ async def commit_push_and_open_mr(state):
f"{side_tag_text}\n"
f"{triage_details_text}"
f"{consolidation_text}"
f"\n\n{mr_description_footer(state.package)}"
f"\n\n{format_zstream_branch_note(state.zstream_branch_created)}"
f"{mr_description_footer(state.package)}"
),
available_tools=gateway_tools,
commit_only=dry_run,
Expand Down
9 changes: 6 additions & 3 deletions ymir/agents/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -239,7 +239,7 @@ async def fork_and_prepare_dist_git(
agent_type: str,
with_fedora: bool = False,
dist_git_namespace: str | None = None,
) -> tuple[Path, str, str, Path | None]:
) -> tuple[Path, str, str, Path | None, str | None]:
if not jira_issue or Path(jira_issue).is_absolute() or ".." in jira_issue:
raise ValueError(f"Invalid jira_issue: {jira_issue}")
# Scoped by agent_type so different agent types processing the same
Expand All @@ -255,13 +255,16 @@ async def fork_and_prepare_dist_git(
local_clone = working_dir / package
# create_zstream_branch only applies to plain internal rhel-X.Y[.0] branches;
# modular stream-* branches already exist in the rhel project.
zstream_branch_created = None
if not is_cs_branch(dist_git_branch) and not is_modular_branch(dist_git_branch):
await run_tool(
result = await run_tool(
"create_zstream_branch",
package=package,
branch=dist_git_branch,
available_tools=available_tools,
)
if "already exists" not in result:
zstream_branch_created = result
if await is_older_zstream(dist_git_branch):
await run_tool(
"clone_repository",
Expand All @@ -286,7 +289,7 @@ async def fork_and_prepare_dist_git(
fedora_clone = working_dir / f"{package}-fedora"
if not await _clone_fedora_dist_git(package, fedora_clone):
fedora_clone = None
return local_clone, update_branch, fork_url, fedora_clone
return local_clone, update_branch, fork_url, fedora_clone, zstream_branch_created


async def find_leading_zstream_branch(dist_git_branch: str) -> str | None:
Expand Down
51 changes: 39 additions & 12 deletions ymir/tools/privileged/distgit.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,6 @@
"no route to host",
"broken pipe",
"ssh_exchange_identification",
"failed to push some refs",
)

_T = TypeVar("_T")
Expand All @@ -55,10 +54,11 @@ async def _retry_transient(
label: str,
max_retries: int = _TRANSIENT_MAX_RETRIES,
base_delay: int = _TRANSIENT_BASE_DELAY,
retry_on_empty: bool = False,
) -> _T:
for attempt in range(max_retries):
try:
return await fn()
result = await fn()
except Exception as e:
if attempt < max_retries - 1 and _is_transient_git_error(e):
backoff = random.uniform(0, base_delay * 2**attempt) # noqa: S311
Expand All @@ -69,6 +69,16 @@ async def _retry_transient(
await asyncio.sleep(backoff)
else:
raise
else:
if retry_on_empty and not result and attempt < max_retries - 1:
backoff = random.uniform(0, base_delay * 2**attempt) # noqa: S311
logger.warning(
f"{label} returned empty (attempt {attempt + 1}/{max_retries}); "
f"retrying in {backoff:.1f}s"
)
await asyncio.sleep(backoff)
else:
return result
raise AssertionError("unreachable")


Expand Down Expand Up @@ -215,6 +225,7 @@ async def _clone():

with tool_error_context("Failed to clone dist-git repo", package=package, clone_url=clone_url):
repo = await _retry_transient(_clone, f"clone {package} from dist-git")
branch_creation_details = None
if branch in [ref.name.split("/")[-1] for ref in repo.remotes.origin.refs]:
# Branch already exists in dist-git but not yet mirrored to GitLab.
# This happens when a previous push succeeded server-side but the SSH
Expand All @@ -230,31 +241,47 @@ async def _clone():
_, ref = await get_latest_z_pending_build(package, branch)
else:
_, ref = await get_latest_candidate_build(package, branch)
if source_branch := self._find_source_branch(repo, branch):
source_branch = self._find_source_branch(repo, branch)
if source_branch and source_branch.endswith("-main"):
ref = await self._find_latest_same_nvr_ref(
repo,
package,
ref,
source_branch,
)
branch_creation_details = f"from {source_branch} at {ref[:12]}"
else:
branch_creation_details = f"at {ref[:12]}"
with tool_error_context(
"Failed to push branch to dist-git", package=package, branch=branch, ref=ref
):
push_infos = await _retry_transient(
lambda: asyncio.to_thread(repo.remotes.origin.push, f"{ref}:refs/heads/{branch}"),
try:
await asyncio.to_thread(repo.commit, ref)
except Exception:
raise ToolError(
f"Commit {ref} (from latest Brew build) not found in dist-git clone of {package}"
) from None
await _retry_transient(
lambda: asyncio.to_thread(repo.git.push, "origin", f"{ref}:refs/heads/{branch}"),
f"push {branch} to dist-git",
)
if getattr(push_infos, "error", None):
logger.error("git push stderr: %s", sanitize_url(str(push_infos.error)))
for info in push_infos:
if info.flags & git.remote.PushInfo.ERROR:
logger.error("Push to dist-git rejected: %s", info.summary.strip())
raise ToolError("Push to dist-git was rejected")
if not await _retry_transient(
lambda: asyncio.to_thread(repo.git.ls_remote, "--heads", "origin", branch),
f"verify {branch} on dist-git",
retry_on_empty=True,
):
raise ToolError(
f"Push appeared to succeed but branch {branch} not found "
f"on dist-git — possible silent rejection by server ACL"
)
start_time = time.monotonic()
while time.monotonic() - start_time < SYNC_TIMEOUT:
try:
if await asyncio.to_thread(repo.git.ls_remote, gitlab_repo_url, branch, branches=True):
return StringToolOutput(result=f"Successfully created Z-Stream branch {branch}")
msg = f"Successfully created Z-Stream branch {branch}"
if branch_creation_details:
msg += f" ({branch_creation_details})"
return StringToolOutput(result=msg)
except git.exc.GitCommandError as e:
if not _is_transient_git_error(e):
logger.error(
Expand Down
Loading
Loading