From c0fcaef25f8de5e4d6b371f6e12b8306d7d88421 Mon Sep 17 00:00:00 2001 From: emil Date: Wed, 1 Jul 2026 06:19:34 +0000 Subject: [PATCH] DEVX-106: feat: detect badge commits as automated CI commits --- src/devx/ci/detect_release_commit.py | 22 ++++++++++- src/devx/translations.json | 8 ++++ tests/unit/test_detect_release_commit.py | 50 +++++++++++++++++++++++- 3 files changed, 76 insertions(+), 4 deletions(-) diff --git a/src/devx/ci/detect_release_commit.py b/src/devx/ci/detect_release_commit.py index 0c76587..092cbf8 100644 --- a/src/devx/ci/detect_release_commit.py +++ b/src/devx/ci/detect_release_commit.py @@ -1,7 +1,10 @@ #!/usr/bin/env python3 -"""Detect whether the latest git commit is a release commit. +"""Detect whether the latest git commit is an automated CI commit. Release commits have the format ``release: vX.Y.Z``. +Badge commits have the format ``chore: update badge URLs ... [skip ci]``. +Both are generated by CI and should skip post-merge jobs. + This script writes ``is-release=true`` or ``is-release=false`` to ``$GITHUB_OUTPUT`` for use in CI workflow conditionals. @@ -21,6 +24,7 @@ from devx.ci._shared import write_github_output from devx.i18n import _ RELEASE_RE = re.compile(r"^release: v\d+\.\d+\.\d+") +BADGE_RE = re.compile(r"^chore: update badge URLs.*\[skip ci\]") def get_commit_message() -> str: @@ -41,15 +45,29 @@ def is_release_commit(message: str) -> bool: return bool(RELEASE_RE.match(message)) +def is_badge_commit(message: str) -> bool: + """Check if a commit message matches the badge commit format.""" + return bool(BADGE_RE.match(message)) + + +def is_automated_commit(message: str) -> bool: + """Check if a commit is an automated CI commit (release or badge).""" + return is_release_commit(message) or is_badge_commit(message) + + @click.command() def main() -> None: - """Detect if the latest commit is a release commit and set GITHUB_OUTPUT.""" + """Detect if the latest commit is an automated CI commit and set GITHUB_OUTPUT.""" msg = get_commit_message() click.echo(_("Commit message: {msg}", msg=msg)) is_release = is_release_commit(msg) + is_automated = is_automated_commit(msg) write_github_output("is-release", "true" if is_release else "false") + write_github_output("is-automated", "true" if is_automated else "false") if is_release: click.echo(_("Release commit — skipping all post-merge jobs.")) + elif is_automated: + click.echo(_("Automated CI commit (badge) — skipping post-merge jobs.")) else: click.echo(_("Regular merge commit — running all post-merge jobs.")) diff --git a/src/devx/translations.json b/src/devx/translations.json index 8b72e57..8be2c42 100644 --- a/src/devx/translations.json +++ b/src/devx/translations.json @@ -2127,6 +2127,14 @@ "ru": "Release commit — skipping all post-merge jobs.", "zh": "Release commit — skipping all post-merge jobs." }, + "Automated CI commit (badge) — skipping post-merge jobs.": { + "bg": "Automated CI commit (badge) — skipping post-merge jobs.", + "de": "Automated CI commit (badge) — skipping post-merge jobs.", + "en": "Automated CI commit (badge) — skipping post-merge jobs.", + "pl": "Automated CI commit (badge) — skipping post-merge jobs.", + "ru": "Automated CI commit (badge) — skipping post-merge jobs.", + "zh": "Automated CI commit (badge) — skipping post-merge jobs." + }, "Release creation failed: {error}": { "bg": "Release creation failed: {error}", "de": "Release creation failed: {error}", diff --git a/tests/unit/test_detect_release_commit.py b/tests/unit/test_detect_release_commit.py index a24bee7..f0ac8d1 100644 --- a/tests/unit/test_detect_release_commit.py +++ b/tests/unit/test_detect_release_commit.py @@ -38,6 +38,31 @@ class TestIsReleaseCommit: assert detect_release_commit.is_release_commit("") is False +class TestIsBadgeCommit: + def test_badge_commit(self) -> None: + assert detect_release_commit.is_badge_commit("chore: update badge URLs to commit abc123 [skip ci]") is True + + def test_regular_chore(self) -> None: + assert detect_release_commit.is_badge_commit("chore: cleanup deps") is False + + def test_empty(self) -> None: + assert detect_release_commit.is_badge_commit("") is False + + +class TestIsAutomatedCommit: + def test_release_is_automated(self) -> None: + assert detect_release_commit.is_automated_commit("release: v1.0.0 [skip ci]") is True + + def test_badge_is_automated(self) -> None: + assert detect_release_commit.is_automated_commit("chore: update badge URLs to commit abc123 [skip ci]") is True + + def test_regular_is_not_automated(self) -> None: + assert detect_release_commit.is_automated_commit("OBL-INFRA-363: fix: something") is False + + def test_empty(self) -> None: + assert detect_release_commit.is_automated_commit("") is False + + class TestWriteGithubOutput: def test_write(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: gh_file = tmp_path / "output.txt" @@ -62,7 +87,26 @@ class TestMain: assert result.exit_code == 0 assert "Release commit" in result.output with open(gh_file) as f: - assert "is-release=true" in f.read() + content = f.read() + assert "is-release=true" in content + assert "is-automated=true" in content + + def test_badge_commit(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: + gh_file = tmp_path / "output.txt" + monkeypatch.setenv("GITHUB_OUTPUT", str(gh_file)) + with patch.object( + detect_release_commit, + "get_commit_message", + return_value="chore: update badge URLs to commit abc123 [skip ci]", + ): + runner = CliRunner() + result = runner.invoke(detect_release_commit.main, []) + assert result.exit_code == 0 + assert "Automated CI commit" in result.output + with open(gh_file) as f: + content = f.read() + assert "is-release=false" in content + assert "is-automated=true" in content def test_regular_commit(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: gh_file = tmp_path / "output.txt" @@ -73,4 +117,6 @@ class TestMain: assert result.exit_code == 0 assert "Regular merge commit" in result.output with open(gh_file) as f: - assert "is-release=false" in f.read() + content = f.read() + assert "is-release=false" in content + assert "is-automated=false" in content