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
18 changes: 9 additions & 9 deletions mergify_cli/ci/detector.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
from __future__ import annotations

import json
import os
import pathlib
import re
Expand Down Expand Up @@ -84,11 +83,13 @@ def get_head_ref_name() -> str | None:

def get_github_actions_head_sha() -> str | None:
if os.getenv("GITHUB_EVENT_NAME") == "pull_request":
# NOTE(leo): we want the head sha of pull request
event_raw_path = os.getenv("GITHUB_EVENT_PATH")
if event_raw_path and ((event_path := pathlib.Path(event_raw_path)).is_file()):
event = json.loads(event_path.read_bytes())
return str(event["pull_request"]["head"]["sha"])
try:
_, event = utils.get_github_event()
except utils.GitHubEventNotFoundError:
pass
else:
if event.pull_request and event.pull_request.head:
return event.pull_request.head.sha
return os.getenv("GITHUB_SHA")


Expand Down Expand Up @@ -193,10 +194,9 @@ def get_github_pull_request_number() -> int | None:
_, event = utils.get_github_event()
except utils.GitHubEventNotFoundError:
return None
pr = event.get("pull_request")
if not isinstance(pr, dict):
if event.pull_request is None:
return None
return typing.cast("int", pr["number"])
return event.pull_request.number

case _:
return None
Expand Down
136 changes: 54 additions & 82 deletions mergify_cli/ci/git_refs/detector.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@
from mergify_cli.ci.scopes import exceptions


if typing.TYPE_CHECKING:
from mergify_cli.ci import github_event


GITHUB_ACTIONS_BASE_OUTPUT_NAME = "base"
GITHUB_ACTIONS_HEAD_OUTPUT_NAME = "head"

Expand All @@ -18,55 +22,6 @@ class BaseNotFoundError(exceptions.ScopesError):
pass


def _detect_base_from_merge_queue_payload(ev: dict[str, typing.Any]) -> str | None:
content = queue_metadata.extract_from_event(ev)
if content:
return content["checking_base_sha"]
return None


def _detect_head_from_event(ev: dict[str, typing.Any]) -> str | None:
pr = ev.get("pull_request")
if isinstance(pr, dict):
sha = pr.get("head", {}).get("sha")
if isinstance(sha, str) and sha:
return sha

return None


def _detect_base_from_event(ev: dict[str, typing.Any]) -> str | None:
pr = ev.get("pull_request")
if isinstance(pr, dict):
sha = pr.get("base", {}).get("sha")
if isinstance(sha, str) and sha:
return sha
return None


def _detect_default_branch_from_event(ev: dict[str, typing.Any]) -> str | None:
repo = ev.get("repository")
if isinstance(repo, dict):
sha = repo.get("default_branch")
if isinstance(sha, str) and sha:
return sha
return None


def _detect_head_from_push_event(ev: dict[str, typing.Any]) -> str | None:
sha = ev.get("after")
if isinstance(sha, str) and sha:
return sha
return None


def _detect_base_from_push_event(ev: dict[str, typing.Any]) -> str | None:
sha = ev.get("before")
if isinstance(sha, str) and sha:
return sha
return None


ReferencesSource = typing.Literal[
"manual",
"merge_queue",
Expand All @@ -92,46 +47,63 @@ def maybe_write_to_github_outputs(self) -> None:
fh.write(f"{GITHUB_ACTIONS_HEAD_OUTPUT_NAME}={self.head}\n")


def _detect_from_pull_request_event(
ev: github_event.GitHubEvent,
) -> References | None:
head = "HEAD"
if ev.pull_request and ev.pull_request.head:
head = ev.pull_request.head.sha

# 0) merge-queue PR override
content = queue_metadata.extract_from_event(ev)
if content:
return References(content["checking_base_sha"], head, "merge_queue")

# 1) standard event payload
if ev.pull_request and ev.pull_request.base:
return References(ev.pull_request.base.sha, head, "github_event_pull_request")

# 2) repository default branch fallback
if ev.repository and ev.repository.default_branch:
return References(
ev.repository.default_branch,
head,
"github_event_pull_request",
)

return None


def _detect_from_push_event(ev: github_event.GitHubEvent) -> References | None:
head_sha = ev.after or "HEAD"
if ev.before:
return References(ev.before, head_sha, "github_event_push")

if ev.repository and ev.repository.default_branch:
return References(ev.repository.default_branch, "HEAD", "github_event_push")

return None


def detect() -> References:
try:
event_name, event = utils.get_github_event()
except utils.GitHubEventNotFoundError:
# fallback to last commit
return References("HEAD^", "HEAD", "fallback_last_commit")

if event_name in queue_metadata.PULL_REQUEST_EVENTS:
result = _detect_from_pull_request_event(event)
if result:
return result

elif event_name == "push":
result = _detect_from_push_event(event)
if result:
return result

else:
if event_name in queue_metadata.PULL_REQUEST_EVENTS:
head = _detect_head_from_event(event) or "HEAD"
# 0) merge-queue PR override
mq_sha = _detect_base_from_merge_queue_payload(event)
if mq_sha:
return References(mq_sha, head, "merge_queue")

# 1) standard event payload
event_sha = _detect_base_from_event(event)
if event_sha:
return References(event_sha, head, "github_event_pull_request")

# 2) standard event payload
event_sha = _detect_default_branch_from_event(event)
if event_sha:
return References(
event_sha,
head,
"github_event_pull_request",
)

elif event_name == "push":
head_sha = _detect_head_from_push_event(event) or "HEAD"
base_sha = _detect_base_from_push_event(event)
if base_sha:
return References(base_sha, head_sha, "github_event_push")

event_sha = _detect_default_branch_from_event(event)
if event_sha:
return References(event_sha, "HEAD", "github_event_push")

else:
return References(None, "HEAD", "github_event_other")
return References(None, "HEAD", "github_event_other")

msg = "Could not detect base SHA. Provide GITHUB_EVENT_NAME / GITHUB_EVENT_PATH."
raise BaseNotFoundError(msg)
36 changes: 36 additions & 0 deletions mergify_cli/ci/github_event.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
from __future__ import annotations

import pydantic


class GitRef(pydantic.BaseModel):
model_config = pydantic.ConfigDict(extra="ignore")

sha: str
ref: str | None = None


class PullRequest(pydantic.BaseModel):
model_config = pydantic.ConfigDict(extra="ignore")

number: int
title: str | None = None
body: str | None = None
base: GitRef | None = None
head: GitRef | None = None


class Repository(pydantic.BaseModel):
model_config = pydantic.ConfigDict(extra="ignore")

default_branch: str | None = None


class GitHubEvent(pydantic.BaseModel):
model_config = pydantic.ConfigDict(extra="ignore")

pull_request: PullRequest | None = None
repository: Repository | None = None
# push events
before: str | None = None
after: str | None = None
21 changes: 11 additions & 10 deletions mergify_cli/ci/queue/metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@
from mergify_cli import utils


if typing.TYPE_CHECKING:
from mergify_cli.ci import github_event


class MergeQueuePullRequest(typing.TypedDict):
number: int

Expand Down Expand Up @@ -46,23 +50,20 @@ def _yaml_docs_from_fenced_blocks(body: str) -> MergeQueueMetadata | None:
return None


def extract_from_event(ev: dict[str, typing.Any]) -> MergeQueueMetadata | None:
pr = ev.get("pull_request")
if not isinstance(pr, dict):
return None
title = pr.get("title") or ""
if not isinstance(title, str):
def extract_from_event(ev: github_event.GitHubEvent) -> MergeQueueMetadata | None:
if ev.pull_request is None:
return None
if not title.startswith("merge queue: "):
if not ev.pull_request.title or not ev.pull_request.title.startswith(
"merge queue: ",
):
return None
body = pr.get("body")
if not body:
if not ev.pull_request.body:
click.echo(
"WARNING: MQ pull request without body, skipping metadata extraction",
err=True,
)
return None
ref = _yaml_docs_from_fenced_blocks(body)
ref = _yaml_docs_from_fenced_blocks(ev.pull_request.body)
if ref is None:
click.echo(
"WARNING: MQ pull request body without Mergify metadata, skipping metadata extraction",
Expand Down
31 changes: 31 additions & 0 deletions mergify_cli/tests/ci/git_refs/test_git_refs_detector.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ def test_detect_base_from_pull_request_event_path(
) -> None:
event_data = {
"pull_request": {
"number": 1,
"base": {"sha": "abc123"},
"head": {"sha": "xyz987"},
},
Expand All @@ -109,6 +110,7 @@ def test_detect_base_merge_queue_override(
) -> None:
event_data = {
"pull_request": {
"number": 1,
"title": "merge queue: embarking #1 together",
"body": "```yaml\nchecking_base_sha: xyz789\n```",
"base": {"sha": "abc123"},
Expand Down Expand Up @@ -143,6 +145,35 @@ def test_detect_base_no_info(
detector.detect()


def test_detect_no_github_event(
monkeypatch: pytest.MonkeyPatch,
) -> None:
monkeypatch.delenv("GITHUB_EVENT_NAME", raising=False)
monkeypatch.delenv("GITHUB_EVENT_PATH", raising=False)

result = detector.detect()

assert result == detector.References("HEAD^", "HEAD", "fallback_last_commit")


def test_detect_push_event_no_info(
tmp_path: pathlib.Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
event_data: dict[str, str] = {}
event_file = tmp_path / "event.json"
event_file.write_text(json.dumps(event_data))

monkeypatch.setenv("GITHUB_EVENT_NAME", "push")
monkeypatch.setenv("GITHUB_EVENT_PATH", str(event_file))

with pytest.raises(
detector.BaseNotFoundError,
match="Could not detect base SHA",
):
detector.detect()


def test_detect_unhandled_event(
monkeypatch: pytest.MonkeyPatch,
tmp_path: pathlib.Path,
Expand Down
46 changes: 46 additions & 0 deletions mergify_cli/tests/ci/push_event.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
{
"ref": "refs/heads/main",
"before": "773db6b5c5f77d0c70c75e6dacef1684cb03495f",
"after": "10068d193546082d802676bb310a570d0898e061",
"created": false,
"deleted": false,
"forced": false,
"pusher": {
"name": "mergify[bot]",
"email": "37929162+mergify[bot]@users.noreply.github.com"
},
"repository": {
"id": 368096773,
"node_id": "MDEwOlJlcG9zaXRvcnkzNjgwOTY3NzM=",
"name": "mergify-cli",
"full_name": "Mergifyio/mergify-cli",
"private": false,
"owner": {
"login": "Mergifyio",
"id": 37838584,
"node_id": "MDEyOk9yZ2FuaXphdGlvbjM3ODM4NTg0",
"type": "Organization",
"site_admin": false
},
"html_url": "https://github.com/Mergifyio/mergify-cli",
"description": "Mergify CLI tool",
"fork": false,
"url": "https://api.github.com/repos/Mergifyio/mergify-cli",
"default_branch": "main",
"visibility": "public"
},
"head_commit": {
"id": "10068d193546082d802676bb310a570d0898e061",
"message": "chore(deps): update dependency uv to v0.10.6",
"timestamp": "2026-02-25T07:34:33Z",
"author": {
"name": "renovate[bot]",
"email": "29139614+renovate[bot]@users.noreply.github.com"
}
},
"sender": {
"login": "mergify[bot]",
"id": 37929162,
"type": "Bot"
}
}
Loading