Skip to content
Closed
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
62 changes: 62 additions & 0 deletions mergify_cli/config/cli.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
from __future__ import annotations

import asyncio
import pathlib
import re

import click
import httpx
from rich.markdown import Markdown
from rich.markup import escape
import yaml

from mergify_cli import console
from mergify_cli import utils
from mergify_cli.ci.detector import MERGIFY_CONFIG_PATHS
from mergify_cli.ci.detector import get_mergify_config_path
from mergify_cli.config import validate as config_validate
Expand Down Expand Up @@ -84,3 +88,61 @@ def validate(ctx: click.Context) -> None:
)

raise SystemExit(1)


_PR_URL_RE = re.compile(
r"https?://[^/]+/(?P<owner>[^/]+)/(?P<repo>[^/]+)/pull/(?P<number>\d+)$",
)


def _parse_pr_url(url: str) -> tuple[str, int]:
m = _PR_URL_RE.match(url)
if not m:
msg = f"Invalid pull request URL: {url}"
raise click.ClickException(msg)
return f"{m.group('owner')}/{m.group('repo')}", int(m.group("number"))


@config.command(
help="Simulate Mergify actions on a pull request using the local configuration",
)
@click.argument("pull_request_url")
@click.option(
"--token",
"-t",
help="Mergify or GitHub token",
envvar=["MERGIFY_TOKEN", "GITHUB_TOKEN"],
required=True,
default=lambda: asyncio.run(utils.get_default_token()),
)
@click.option(
"--api-url",
"-u",
help="URL of the Mergify API",
envvar="MERGIFY_API_URL",
default="https://api.mergify.com",
show_default=True,
)
@click.pass_context
@utils.run_with_asyncio
async def simulate(
ctx: click.Context,
*,
pull_request_url: str,
token: str,
api_url: str,
) -> None:
repository, pull_number = _parse_pr_url(pull_request_url)
config_path = _resolve_config_path(ctx.obj["config_file"])
mergify_yml = config_validate.read_raw(config_path)

async with utils.get_mergify_http_client(api_url, token) as client:
result = await config_validate.simulate_pr(
client,
repository,
pull_number,
mergify_yml,
)

console.print(f"[bold]{escape(result.title)}[/]")
console.print(Markdown(result.summary))
20 changes: 20 additions & 0 deletions mergify_cli/config/validate.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,3 +66,23 @@ def validate_config(
path = ".".join(str(p) for p in error.absolute_path) or "(root)"
errors.append(ValidationError(path=path, message=error.message))
return ValidationResult(errors=errors)


@dataclasses.dataclass
class SimulatorResult:
title: str
summary: str


async def simulate_pr(
client: httpx.AsyncClient,
repository: str,
pull_number: int,
mergify_yml: str,
) -> SimulatorResult:
response = await client.post(
f"/v1/repos/{repository}/pulls/{pull_number}/simulator",
json={"mergify_yml": mergify_yml},
)
data = response.json()
return SimulatorResult(title=data["title"], summary=data["summary"])
79 changes: 79 additions & 0 deletions mergify_cli/tests/config/test_validate.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
}

_SCHEMA_URL = "https://docs.mergify.com/mergify-configuration-schema.json"
_PR_URL = "https://github.com/owner/repo/pull/42"


def _write_config(tmp_path: pathlib.Path, content: str) -> str:
Expand Down Expand Up @@ -157,3 +158,81 @@ def test_empty_config(tmp_path: pathlib.Path) -> None:
)
assert result.exit_code == 0, result.output
assert "is valid" in result.output


def test_simulate_pr(tmp_path: pathlib.Path) -> None:
config_path = _write_config(tmp_path, "pull_request_rules: []\n")

with respx.mock(base_url="https://api.mergify.com") as rsp:
rsp.post("/v1/repos/owner/repo/pulls/42/simulator").mock(
return_value=Response(
200,
json={
"title": "The configuration is valid",
"summary": "No actions will be triggered",
},
),
)

result = CliRunner().invoke(
config,
[
"--config-file",
config_path,
"simulate",
_PR_URL,
"--token",
"test-token",
],
)
assert result.exit_code == 0, result.output
assert "The configuration is valid" in result.output
assert "No actions will be triggered" in result.output


def test_simulate_invalid_pr_url() -> None:
result = CliRunner().invoke(
config,
["simulate", "not-a-url", "--token", "test-token"],
)
assert result.exit_code != 0
assert "Invalid pull request URL" in result.output


def test_simulate_api_failure(tmp_path: pathlib.Path) -> None:
config_path = _write_config(tmp_path, "pull_request_rules: []\n")

with respx.mock(base_url="https://api.mergify.com") as rsp:
rsp.post("/v1/repos/owner/repo/pulls/42/simulator").mock(
return_value=Response(500),
)

result = CliRunner().invoke(
config,
[
"--config-file",
config_path,
"simulate",
_PR_URL,
"--token",
"test-token",
],
)
assert result.exit_code != 0
assert "Traceback" not in result.output


def test_simulate_config_not_found() -> None:
result = CliRunner().invoke(
config,
[
"--config-file",
"/nonexistent/.mergify.yml",
"simulate",
_PR_URL,
"--token",
"test-token",
],
)
assert result.exit_code != 0
assert "not found" in result.output.lower()
Loading