Skip to content

Commit 7c3cc58

Browse files
silehtclaude
andauthored
fix(ci): auto-detect --tests-target-branch on Buildkite, CircleCI, Jenkins (#1326)
`mergify ci junit-process` and `junit-upload` previously resolved `--tests-target-branch` from a hard-coded list of GitHub-Actions env vars (`GITHUB_BASE_REF`, `GITHUB_HEAD_REF`, `GITHUB_REF_NAME`, `GITHUB_REF`). On any other CI provider the option came back empty and click aborted with `Missing option '--tests-target-branch' / '-ttb'`, even though the equivalent information is exposed by every supported provider — `BUILDKITE_PULL_REQUEST_BASE_BRANCH` / `BUILDKITE_BRANCH` on Buildkite, `CIRCLE_BRANCH` on CircleCI, `CHANGE_TARGET` / `GIT_BRANCH` on Jenkins. Mirror the `--repository` pattern: a new `detector.get_tests_target_branch` helper dispatches on `get_ci_provider()` and returns the right env var per provider. Both options now use `default=detector.get_tests_target_branch` in place of the GitHub-only `envvar=[...]` list. The existing `_process_tests_target_branch` callback still strips `refs/heads/` from whatever value resolves, so behaviour on GitHub Actions is unchanged. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent f6a87c8 commit 7c3cc58

5 files changed

Lines changed: 167 additions & 3 deletions

File tree

mergify_cli/ci/cli.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,7 @@ def ci(ctx: click.Context) -> None:
130130
"-ttb",
131131
help="The branch used to check if failing tests can be ignored with Mergify's Quarantine.",
132132
required=True,
133-
envvar=["GITHUB_BASE_REF", "GITHUB_HEAD_REF", "GITHUB_REF_NAME", "GITHUB_REF"],
133+
default=detector.get_tests_target_branch,
134134
callback=_process_tests_target_branch,
135135
)
136136
@click.option(
@@ -221,7 +221,7 @@ async def junit_upload(
221221
"-ttb",
222222
help="The branch used to check if failing tests can be ignored with Mergify's Quarantine.",
223223
required=True,
224-
envvar=["GITHUB_BASE_REF", "GITHUB_HEAD_REF", "GITHUB_REF_NAME", "GITHUB_REF"],
224+
default=detector.get_tests_target_branch,
225225
callback=_process_tests_target_branch,
226226
)
227227
@click.option(

mergify_cli/ci/detector.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,27 @@ def get_github_repository() -> str | None:
238238
return None
239239

240240

241+
def get_tests_target_branch() -> str | None:
242+
match get_ci_provider():
243+
case "github_actions":
244+
return (
245+
os.getenv("GITHUB_BASE_REF")
246+
or os.getenv("GITHUB_HEAD_REF")
247+
or os.getenv("GITHUB_REF_NAME")
248+
or os.getenv("GITHUB_REF")
249+
)
250+
case "buildkite":
251+
return os.getenv("BUILDKITE_PULL_REQUEST_BASE_BRANCH") or os.getenv(
252+
"BUILDKITE_BRANCH",
253+
)
254+
case "circleci":
255+
return os.getenv("CIRCLE_BRANCH")
256+
case "jenkins":
257+
return os.getenv("CHANGE_TARGET") or get_jenkins_head_ref_name()
258+
case _:
259+
return None
260+
261+
241262
MERGIFY_CONFIG_PATHS = (
242263
".mergify.yml",
243264
".mergify/config.yml",

mergify_cli/tests/ci/test_cli.py

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,76 @@ def test_tests_target_branch_environment_variable_processing(
226226
assert call_kwargs["tests_target_branch"] == expected_branch
227227

228228

229+
@pytest.mark.parametrize(
230+
("env", "expected_branch"),
231+
[
232+
pytest.param(
233+
{
234+
"BUILDKITE": "true",
235+
"MERGIFY_API_URL": "https://api.mergify.com",
236+
"MERGIFY_TOKEN": "abc",
237+
"BUILDKITE_REPO": "git@github.com:user/repo.git",
238+
"BUILDKITE_PULL_REQUEST_BASE_BRANCH": "main",
239+
"BUILDKITE_BRANCH": "feature-branch",
240+
},
241+
"main",
242+
id="BUILDKITE_PULL_REQUEST_BASE_BRANCH takes precedence",
243+
),
244+
pytest.param(
245+
{
246+
"BUILDKITE": "true",
247+
"MERGIFY_API_URL": "https://api.mergify.com",
248+
"MERGIFY_TOKEN": "abc",
249+
"BUILDKITE_REPO": "git@github.com:user/repo.git",
250+
"BUILDKITE_BRANCH": "feature-branch",
251+
},
252+
"feature-branch",
253+
id="BUILDKITE_BRANCH fallback when no PR base branch",
254+
),
255+
],
256+
)
257+
def test_tests_target_branch_buildkite_environment_variable_processing(
258+
env: dict[str, str],
259+
expected_branch: str,
260+
monkeypatch: pytest.MonkeyPatch,
261+
) -> None:
262+
"""Test that --tests-target-branch is auto-detected from Buildkite env vars."""
263+
for key in [
264+
"GITHUB_ACTIONS",
265+
"GITHUB_REF",
266+
"GITHUB_REF_NAME",
267+
"GITHUB_HEAD_REF",
268+
"GITHUB_BASE_REF",
269+
"JENKINS_URL",
270+
"CIRCLECI",
271+
"BUILDKITE",
272+
"BUILDKITE_PULL_REQUEST_BASE_BRANCH",
273+
"BUILDKITE_BRANCH",
274+
]:
275+
monkeypatch.delenv(key, raising=False)
276+
277+
for key, value in env.items():
278+
monkeypatch.setenv(key, value)
279+
280+
runner = testing.CliRunner()
281+
282+
with mock.patch.object(
283+
junit_processing_cli,
284+
"process_junit_files",
285+
mock.AsyncMock(),
286+
) as mocked_process_junit_files:
287+
result = runner.invoke(
288+
ci_cli.junit_process,
289+
[str(REPORT_XML)],
290+
)
291+
292+
assert result.exit_code == 0, result.stdout
293+
294+
assert mocked_process_junit_files.call_count == 1
295+
call_kwargs = mocked_process_junit_files.call_args.kwargs
296+
assert call_kwargs["tests_target_branch"] == expected_branch
297+
298+
229299
def test_process_tests_target_branch_callback() -> None:
230300
"""Test the _process_tests_target_branch callback function directly."""
231301
context_mock = mock.MagicMock(spec=click.Context)

mergify_cli/tests/ci/test_detector.py

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -381,3 +381,76 @@ def test_get_github_pull_request_number_buildkite_not_pr(
381381
) -> None:
382382
monkeypatch.setenv("BUILDKITE_PULL_REQUEST", "false")
383383
assert detector.get_github_pull_request_number() is None
384+
385+
386+
@pytest.mark.usefixtures("_buildkite_env")
387+
def test_get_tests_target_branch_buildkite_pr(
388+
monkeypatch: pytest.MonkeyPatch,
389+
) -> None:
390+
monkeypatch.setenv("BUILDKITE_PULL_REQUEST_BASE_BRANCH", "main")
391+
monkeypatch.setenv("BUILDKITE_BRANCH", "feature-branch")
392+
assert detector.get_tests_target_branch() == "main"
393+
394+
395+
@pytest.mark.usefixtures("_buildkite_env")
396+
def test_get_tests_target_branch_buildkite_push(
397+
monkeypatch: pytest.MonkeyPatch,
398+
) -> None:
399+
monkeypatch.delenv("BUILDKITE_PULL_REQUEST_BASE_BRANCH", raising=False)
400+
monkeypatch.setenv("BUILDKITE_BRANCH", "feature-branch")
401+
assert detector.get_tests_target_branch() == "feature-branch"
402+
403+
404+
@pytest.mark.usefixtures("_buildkite_env")
405+
def test_get_tests_target_branch_buildkite_unset() -> None:
406+
assert detector.get_tests_target_branch() is None
407+
408+
409+
@pytest.fixture
410+
def _clear_ci_provider_env(monkeypatch: pytest.MonkeyPatch) -> None:
411+
"""Clear all CI-provider toggle env vars so the test picks the one it sets."""
412+
for env in ("GITHUB_ACTIONS", "CIRCLECI", "JENKINS_URL", "BUILDKITE"):
413+
monkeypatch.delenv(env, raising=False)
414+
415+
416+
@pytest.mark.usefixtures("_clear_ci_provider_env")
417+
def test_get_tests_target_branch_github_actions_base_ref_precedence(
418+
monkeypatch: pytest.MonkeyPatch,
419+
) -> None:
420+
monkeypatch.setenv("GITHUB_ACTIONS", "true")
421+
monkeypatch.setenv("GITHUB_BASE_REF", "main")
422+
monkeypatch.setenv("GITHUB_HEAD_REF", "feature-branch")
423+
monkeypatch.setenv("GITHUB_REF", "refs/heads/feature-branch")
424+
assert detector.get_tests_target_branch() == "main"
425+
426+
427+
@pytest.mark.usefixtures("_clear_ci_provider_env")
428+
def test_get_tests_target_branch_circleci(monkeypatch: pytest.MonkeyPatch) -> None:
429+
monkeypatch.setenv("CIRCLECI", "true")
430+
monkeypatch.setenv("CIRCLE_BRANCH", "feature-branch")
431+
assert detector.get_tests_target_branch() == "feature-branch"
432+
433+
434+
@pytest.mark.usefixtures("_clear_ci_provider_env")
435+
def test_get_tests_target_branch_jenkins_change_target(
436+
monkeypatch: pytest.MonkeyPatch,
437+
) -> None:
438+
monkeypatch.setenv("JENKINS_URL", "http://jenkins.example.com")
439+
monkeypatch.setenv("CHANGE_TARGET", "main")
440+
monkeypatch.setenv("GIT_BRANCH", "origin/feature-branch")
441+
assert detector.get_tests_target_branch() == "main"
442+
443+
444+
@pytest.mark.usefixtures("_clear_ci_provider_env")
445+
def test_get_tests_target_branch_jenkins_git_branch_fallback(
446+
monkeypatch: pytest.MonkeyPatch,
447+
) -> None:
448+
monkeypatch.setenv("JENKINS_URL", "http://jenkins.example.com")
449+
monkeypatch.delenv("CHANGE_TARGET", raising=False)
450+
monkeypatch.setenv("GIT_BRANCH", "origin/feature-branch")
451+
assert detector.get_tests_target_branch() == "feature-branch"
452+
453+
454+
@pytest.mark.usefixtures("_clear_ci_provider_env")
455+
def test_get_tests_target_branch_no_provider() -> None:
456+
assert detector.get_tests_target_branch() is None

skills/mergify-ci/SKILL.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ mergify ci junit-process \
3737
**Key options:**
3838
- `--token` / `-t` (env: `MERGIFY_TOKEN`) -- CI Insights application key
3939
- `--repository` / `-r` -- Repository full name (auto-detected in GitHub Actions)
40-
- `--tests-target-branch` / `-ttb` -- Branch used for quarantine evaluation (auto-detected from `GITHUB_BASE_REF`, `GITHUB_HEAD_REF`, `GITHUB_REF_NAME`, `GITHUB_REF`)
40+
- `--tests-target-branch` / `-ttb` -- Branch used for quarantine evaluation. Auto-detected per CI provider: GitHub Actions (`GITHUB_BASE_REF``GITHUB_HEAD_REF``GITHUB_REF_NAME``GITHUB_REF`), Buildkite (`BUILDKITE_PULL_REQUEST_BASE_BRANCH``BUILDKITE_BRANCH`), CircleCI (`CIRCLE_BRANCH`), Jenkins (`CHANGE_TARGET``GIT_BRANCH`).
4141
- `--api-url` / `-u` (env: `MERGIFY_API_URL`) -- Mergify API URL (default: `https://api.mergify.com`)
4242
- `--test-framework` -- Test framework name (optional metadata)
4343
- `--test-language` -- Test language (optional metadata)

0 commit comments

Comments
 (0)