diff --git a/docs/features/django-fork.rst b/docs/features/django-fork.rst index 29ba5a3..9a05c16 100644 --- a/docs/features/django-fork.rst +++ b/docs/features/django-fork.rst @@ -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 = , evergreen = true}`` — re-runs PR ````'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 @@ -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:: diff --git a/src/dbx_python_cli/commands/sync.py b/src/dbx_python_cli/commands/sync.py index c1452c2..2525120 100644 --- a/src/dbx_python_cli/commands/sync.py +++ b/src/dbx_python_cli/commands/sync.py @@ -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 @@ -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 @@ -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) @@ -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. @@ -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. diff --git a/src/dbx_python_cli/utils/repo.py b/src/dbx_python_cli/utils/repo.py index d23b8f3..df4cac9 100644 --- a/src/dbx_python_cli/utils/repo.py +++ b/src/dbx_python_cli/utils/repo.py @@ -454,15 +454,18 @@ def get_ci_rerun_targets(config, group_name, repo_name, branch): Configure in ``[repo.groups..ci_rerun.]`` 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 = , 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 @@ -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: @@ -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): @@ -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 = , evergreen = }. 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 diff --git a/tests/test_repo.py b/tests/test_repo.py index 0ee14c1..dcf2e18 100644 --- a/tests/test_repo.py +++ b/tests/test_repo.py @@ -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"