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
25 changes: 20 additions & 5 deletions docs/features/django-fork.rst
Original file line number Diff line number Diff line change
Expand Up @@ -213,15 +213,23 @@ maps an ``owner/name`` GitHub repo to a target that is **either**:
fork branch is exercised — e.g. the backend's ``main`` pins ``mongodb-6.0.x``.
Only ``test-python*`` workflows that actually declare a ``workflow_dispatch``
trigger are dispatched; any that don't (push/schedule/pull_request only) are
reported as ``skipped (no workflow_dispatch trigger)`` instead of failing.
reported as ``skipped (no workflow_dispatch trigger)`` instead of failing, or
- an **object** ``{pr = <n>, evergreen = true}`` — re-runs PR ``<n>``'s workflow
runs (exactly as the integer form) **and** re-triggers that PR's Evergreen
patch by commenting ``evergreen retry`` on it. This is needed because
Evergreen's PR patch, like the GitHub Actions workflows, checks out the fork
branch at a pinned ref, so a rebased fork branch does not re-run Evergreen on
its own. Set ``evergreen = false`` (or use the bare integer) to re-run only
GitHub Actions.

.. code-block:: toml

[repo.groups.django.ci_rerun.django]
"mongodb-6.0.x" = {"mongodb/django-mongodb-backend" = "main"} # dispatch, no PR
"mongodb-6.1.x" = {"mongodb/django-mongodb-backend" = 422} # re-run PR #422
"mongodb-6.1.x" = {"mongodb/django-mongodb-backend" = 422} # re-run PR #422 (Actions only)
"mongodb-5.2.x" = {"mongodb/django-mongodb-backend" = 562}
"mongodb-6.2.x" = {"mongodb/django-mongodb-backend" = 535}
# Re-run Actions AND re-trigger Evergreen on PR #535:
"mongodb-6.2.x" = {"mongodb/django-mongodb-backend" = {pr = 535, evergreen = true}}

.. code-block:: text

Expand All @@ -233,12 +241,19 @@ maps an ``owner/name`` GitHub repo to a target that is **either**:
test-python-replica.yml — skipped (no workflow_dispatch trigger)
♻️ mongodb-6.1.x → re-running CI on mongodb/django-mongodb-backend#422...
#422 ✓ queued (4 workflow run(s))
♻️ mongodb-6.2.x → re-running CI on mongodb/django-mongodb-backend#535...
#535 ✓ queued (4 workflow run(s))
♻️ mongodb-6.2.x → retrying Evergreen on mongodb/django-mongodb-backend#535...
#535 ✓ commented 'evergreen retry'

This uses the ``gh`` CLI (GitHub CLI), so ``gh`` must be installed and
authenticated. It is best-effort: a missing ``gh``, an unconfigured ``ci_rerun``
mapping, or a GitHub API error is reported as a warning and never fails the
sync. Pass ``--no-ci`` to skip the re-run, and note it is skipped automatically
for ``--dry-run`` and when no branch actually synced.
sync. The Evergreen retry uses the same ``gh`` CLI (it posts an ``evergreen
retry`` PR comment, which Evergreen watches for), so no Evergreen token or CLI
is required. Pass ``--no-ci`` to skip all of the above (Actions re-runs and
Evergreen retries alike), and note it is skipped automatically for ``--dry-run``
and when no branch actually synced.

.. note::

Expand Down
52 changes: 48 additions & 4 deletions src/dbx_python_cli/commands/sync.py
Original file line number Diff line number Diff line change
Expand Up @@ -271,10 +271,13 @@ def _rerun_downstream_ci(config, group_name, repo_name, synced_branches, verbose
"""Re-run downstream CI for the branches that synced, per the ci_rerun mapping.

Each fork branch maps to a downstream target that is either a **PR number**
(re-run that PR's workflow runs) or a **git ref** (dispatch the repo's
(re-run that PR's workflow runs), a **git ref** (dispatch the repo's
``test-python*`` workflows on that backend branch via ``workflow_dispatch`` —
no PR needed). Only branches that actually rebased are processed; a branch
that failed or was skipped triggers nothing.
no PR needed), or a PR number flagged for **Evergreen** (additionally comment
``evergreen retry`` to re-trigger the PR's Evergreen patch, since a rebased
fork branch pinned by the PR does not re-trigger Evergreen on its own). Only
branches that actually rebased are processed; a branch that failed or was
skipped triggers nothing.

Best-effort: this never fails the sync. A missing ``gh`` CLI, an unconfigured
``ci_rerun`` mapping, or a GitHub API error is reported as a warning and
Expand Down Expand Up @@ -303,6 +306,11 @@ def _rerun_downstream_ci(config, group_name, repo_name, synced_branches, verbose
if key not in seen:
seen.add(key)
actions.append((branch, target, "ref", ref))
for number in spec["evergreen_prs"]:
key = (target, "evergreen", number)
if key not in seen:
seen.add(key)
actions.append((branch, target, "evergreen", number))

if not actions:
return
Expand All @@ -326,6 +334,8 @@ def _gh_json(args):
for branch, target, kind, value in actions:
if kind == "pr":
_rerun_pr_ci(target, value, branch, verbose, _gh_json)
elif kind == "evergreen":
_retry_evergreen(target, value, branch, verbose)
else:
_dispatch_workflows(target, value, branch, verbose, _gh_json)

Expand Down Expand Up @@ -391,6 +401,40 @@ def _rerun_pr_ci(target, number, branch, verbose, _gh_json):
)


def _retry_evergreen(target, number, branch, verbose):
"""Re-trigger a PR's Evergreen patch by commenting ``evergreen retry``.

Evergreen's PR patch checks out the backend PR at a pinned fork ref, so a
rebased (force-pushed) fork branch does not re-run Evergreen on its own.
Commenting ``evergreen retry`` on the PR aborts any existing patch for the
head commit and starts a fresh one. Best-effort: never fails the sync.
"""
typer.echo(f"♻️ {branch} → retrying Evergreen on {target}#{number}...")
try:
subprocess.run(
[
"gh",
"pr",
"comment",
str(number),
"--repo",
target,
"--body",
"evergreen retry",
],
check=True,
capture_output=not verbose,
text=True,
)
typer.echo(f" #{number} ✓ commented 'evergreen retry'")
except subprocess.CalledProcessError as e:
typer.echo(
f" #{number} ⚠️ could not comment 'evergreen retry': "
f"{e.stderr if not verbose else ''}",
err=True,
)


def _dispatch_workflows(target, ref, branch, verbose, _gh_json):
"""Dispatch the target repo's ``test-python*`` workflows on a backend ref.

Expand Down Expand Up @@ -525,7 +569,7 @@ def sync_callback(
no_ci: bool = typer.Option(
False,
"--no-ci",
help="Skip re-running downstream CI after --all-branches (see ci_rerun config)",
help="Skip re-running downstream CI (GitHub Actions + Evergreen retry) after --all-branches (see ci_rerun config)",
),
):
"""Sync repository with upstream by fetching, rebasing, and pushing.
Expand Down
27 changes: 21 additions & 6 deletions src/dbx_python_cli/utils/repo.py
Original file line number Diff line number Diff line change
Expand Up @@ -454,15 +454,18 @@ def get_ci_rerun_targets(config, group_name, repo_name, branch):

Configure in ``[repo.groups.<group>.ci_rerun.<synced-repo>]`` keyed by fork
branch; each value maps ``owner/name`` GitHub repo to a target that is either
an integer **PR number** (re-run that PR's workflow runs) or a string **git
ref** (dispatch the repo's ``test-python*`` workflows on that backend branch).
A list may mix both forms:
an integer **PR number** (re-run that PR's workflow runs), a string **git
ref** (dispatch the repo's ``test-python*`` workflows on that backend branch),
or an **object** ``{pr = <n>, evergreen = true}`` to also re-trigger the PR's
Evergreen patch (by commenting ``evergreen retry``). A list may mix all forms:

.. code-block:: toml

[repo.groups.django.ci_rerun.django]
"mongodb-6.0.x" = {"mongodb/django-mongodb-backend" = "main"}
"mongodb-6.1.x" = {"mongodb/django-mongodb-backend" = 422}
# Re-run GitHub Actions *and* re-trigger Evergreen on PR 422:
"mongodb-6.2.x" = {"mongodb/django-mongodb-backend" = {pr = 422, evergreen = true}}

Args:
config: Configuration dictionary
Expand All @@ -471,8 +474,9 @@ def get_ci_rerun_targets(config, group_name, repo_name, branch):
branch: The fork branch that just synced (e.g., 'mongodb-6.1.x')

Returns:
dict[str, dict]: ``owner/name`` -> ``{"prs": [int, ...], "refs": [str, ...]}``
(empty dict if unconfigured)
dict[str, dict]: ``owner/name`` -> ``{"prs": [int, ...], "refs": [str, ...],
"evergreen_prs": [int, ...]}`` (empty dict if unconfigured). ``evergreen_prs``
is the subset of ``prs`` that also want an ``evergreen retry`` comment.
"""
groups = get_repo_groups(config)
if group_name not in groups:
Expand All @@ -485,6 +489,7 @@ def get_ci_rerun_targets(config, group_name, repo_name, branch):
items = value if isinstance(value, list) else [value]
prs = []
refs = []
evergreen_prs = []
for item in items:
# bool is an int subclass; exclude it so `true`/`false` never parse as a PR.
if isinstance(item, bool):
Expand All @@ -493,7 +498,17 @@ def get_ci_rerun_targets(config, group_name, repo_name, branch):
prs.append(item)
elif isinstance(item, str):
refs.append(item)
result[target] = {"prs": prs, "refs": refs}
elif isinstance(item, dict):
# Object form: {pr = <n>, evergreen = <bool>}. The PR still gets
# its GitHub Actions runs re-queued (as a bare int would); the
# evergreen flag additionally re-triggers the Evergreen patch.
pr = item.get("pr")
if isinstance(pr, bool) or not isinstance(pr, int):
continue
prs.append(pr)
if item.get("evergreen"):
evergreen_prs.append(pr)
result[target] = {"prs": prs, "refs": refs, "evergreen_prs": evergreen_prs}
return result


Expand Down
87 changes: 87 additions & 0 deletions tests/test_repo.py
Original file line number Diff line number Diff line change
Expand Up @@ -1662,6 +1662,93 @@ def mock_run_side_effect(*args, **kwargs):
)


def test_repo_sync_all_branches_retries_evergreen(tmp_path, temp_repos_dir):
"""An ``{pr, evergreen = true}`` target re-runs Actions AND comments 'evergreen retry'."""
config_path = tmp_path / ".config" / "dbx-python-cli" / "config.toml"
config_path.parent.mkdir(parents=True, exist_ok=True)
repos_dir_str = str(temp_repos_dir).replace("\\", "/")
config_content = f"""
[repo]
base_dir = "{repos_dir_str}"

[repo.groups.django]
repos = [
"git@github.com:mongodb-forks/django.git",
]

[repo.groups.django.upstream_branch]
django = {{"mongodb-6.0.x" = "stable/6.0.x"}}

[repo.groups.django.ci_rerun.django]
"mongodb-6.0.x" = {{"mongodb/django-mongodb-backend" = {{pr = 422, evergreen = true}}}}
"""
config_path.write_text(config_content)

repo_dir = temp_repos_dir / "django" / "django"
repo_dir.mkdir(parents=True)
(repo_dir / ".git").mkdir()

state = {"current": "main"}

with patch("dbx_python_cli.utils.repo.get_config_path") as mock_get_path:
with patch("shutil.which", return_value="/usr/bin/gh"):
with patch("dbx_python_cli.commands.sync.subprocess.run") as mock_run:
mock_get_path.return_value = config_path

def mock_run_side_effect(*args, **kwargs):
cmd = args[0]
if cmd[0] == "gh":
if "pr" in cmd and "view" in cmd:
return subprocess.CompletedProcess(
cmd, 0, stdout='{"headRefOid": "abc123"}', stderr=""
)
if "api" in cmd and "-X" not in cmd:
return subprocess.CompletedProcess(
cmd, 0, stdout="[555]", stderr=""
)
return subprocess.CompletedProcess(cmd, 0, stdout="", stderr="")
if "switch" in cmd:
state["current"] = cmd[-1]
return subprocess.CompletedProcess(cmd, 0, stdout="", stderr="")
if "branch" in cmd and "--show-current" in cmd:
return subprocess.CompletedProcess(
cmd, 0, stdout=f"{state['current']}\n", stderr=""
)
if "remote" in cmd and "add" not in cmd and "show" not in cmd:
return subprocess.CompletedProcess(
cmd, 0, stdout="origin\nupstream\n", stderr=""
)
return subprocess.CompletedProcess(cmd, 0, stdout="", stderr="")

mock_run.side_effect = mock_run_side_effect

result = runner.invoke(app, ["sync", "django", "--all-branches"])
assert result.exit_code == 0

gh_cmds = [
c[0][0] for c in mock_run.call_args_list if c[0][0][0] == "gh"
]

# The Actions rerun still happens (PR is also re-queued).
assert any(
"-X" in cmd and cmd[-1].endswith("/rerun") for cmd in gh_cmds
)

# The 'evergreen retry' comment is posted on the PR.
comment_calls = [
cmd for cmd in gh_cmds if "pr" in cmd and "comment" in cmd
]
assert comment_calls
assert "422" in comment_calls[0]
assert "evergreen retry" in comment_calls[0]
assert "mongodb/django-mongodb-backend" in comment_calls[0]

output = result.stdout + result.stderr
assert (
"retrying Evergreen on mongodb/django-mongodb-backend#422" in output
)


def test_repo_sync_all_branches_no_ci_skips_rerun(tmp_path, temp_repos_dir):
"""--no-ci suppresses the downstream CI re-run."""
config_path = tmp_path / ".config" / "dbx-python-cli" / "config.toml"
Expand Down
Loading