Skip to content

Commit 7f4ead1

Browse files
committed
feat: add message-ai-attribution to check commits for AI attribution only
Each commit is checked with commit-check --message-ai-attribution: the AI rules (CC013 to CC017) without the format rules. It takes the place of message, which defaults to true, so it needs no other input.
1 parent 0264f70 commit 7f4ead1

4 files changed

Lines changed: 82 additions & 5 deletions

File tree

‎README.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ runners need a few tools — see [Good to know](#good-to-know).
8080
| `author-name` | `false` | Check each commit's author name |
8181
| `author-email` | `false` | Check each commit's author email |
8282
| `pr-title` | `false` | Check the pull request title against Conventional Commits — the one that matters for squash merges. Pull request events only |
83+
| `message-ai-attribution` | `false` | Check every commit against the AI rules of `cchk.toml` (`ai_attribution`) only, author included, without the format rules. Takes the place of `message` |
8384
| `pr-body-ai-attribution` | `false` | Check the pull request description against the AI rules of `cchk.toml` (`ai_attribution`) only. Pull request events only |
8485
| `ai-label` | `false` | Label the pull request when an AI tool wrote any of it (a commit, the description, or an agent's app that opened it): `true` for `ai-assisted`, or the label's name. Labels are only added, never removed. Needs `pull-requests: write`; skipped on fork pull requests under `pull_request` |
8586
| `pr-comments` | `false` | Post the report as a pull request comment, edited in place on later runs. Needs `pull-requests: write`; skipped on fork pull requests |

‎action.yml‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,10 @@ inputs:
3737
description: check pull request title following conventional commits
3838
required: false
3939
default: false
40+
message-ai-attribution:
41+
description: check each commit against the AI attribution rules (ai_attribution in cchk.toml) only, without the format rules; overrides message
42+
required: false
43+
default: false
4044
pr-body-ai-attribution:
4145
description: check the pull request description against the AI attribution rules (ai_attribution in cchk.toml)
4246
required: false
@@ -118,6 +122,7 @@ runs:
118122
JOB_SUMMARY: ${{ inputs.job-summary }}
119123
PR_COMMENTS: ${{ inputs.pr-comments }}
120124
PR_TITLE: ${{ inputs.pr-title }}
125+
MESSAGE_AI_ATTRIBUTION: ${{ inputs.message-ai-attribution }}
121126
PR_BODY_AI_ATTRIBUTION: ${{ inputs.pr-body-ai-attribution }}
122127
AI_LABEL: ${{ inputs.ai-label }}
123128
GITHUB_TOKEN: ${{ github.token }}

‎main.py‎

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,20 @@ def _reconfigure_io() -> None:
8888
PR_COMMENTS_ENABLED = env_flag("PR_COMMENTS")
8989
PR_TITLE_ENABLED = env_flag("PR_TITLE")
9090
PR_BODY_AI_ATTRIBUTION_ENABLED = env_flag("PR_BODY_AI_ATTRIBUTION")
91+
MESSAGE_AI_ATTRIBUTION_ENABLED = env_flag("MESSAGE_AI_ATTRIBUTION")
92+
93+
94+
def message_check_args() -> list[str]:
95+
"""The CLI flag each commit message is checked with.
96+
97+
``message-ai-attribution: true`` narrows the check to the AI rules (CC013
98+
to CC017), whatever ``message`` says: ``message`` defaults to true, so
99+
asking for the AI rules alone must not need it turned off as well.
100+
"""
101+
if MESSAGE_AI_ATTRIBUTION_ENABLED:
102+
return ["--message-ai-attribution"]
103+
return ["--message"]
104+
91105

92106
#: The label ``ai-label: true`` puts on a pull request that an AI tool wrote.
93107
DEFAULT_AI_LABEL = "ai-assisted"
@@ -255,6 +269,7 @@ def log_env_vars():
255269
"PR_COMMENTS",
256270
"PR_TITLE",
257271
"PR_BODY_AI_ATTRIBUTION",
272+
"MESSAGE_AI_ATTRIBUTION",
258273
"AI_LABEL",
259274
):
260275
value = os.getenv(name, "false")
@@ -790,7 +805,7 @@ def run_pr_message_checks(pr_commits: list[Commit]) -> list[ScopeResult]:
790805
names = ("GIT_AUTHOR_NAME", "GIT_AUTHOR_EMAIL")
791806
env = {key: value for key, value in zip(names, author) if value} or None
792807
results.append(
793-
check_scope(label, ["--message"], input_text=msg, sha=sha, env=env)
808+
check_scope(label, message_check_args(), input_text=msg, sha=sha, env=env)
794809
)
795810
return results
796811

@@ -817,7 +832,7 @@ def run_other_checks(args: list[str], rev: str | None = None) -> list[ScopeResul
817832
def build_check_args() -> list[str]:
818833
"""Map enabled validation switches to commit-check CLI arguments."""
819834
flags = [
820-
("--message", MESSAGE_ENABLED),
835+
("--message", MESSAGE_ENABLED or MESSAGE_AI_ATTRIBUTION_ENABLED),
821836
("--branch", BRANCH_ENABLED),
822837
("--author-name", AUTHOR_NAME_ENABLED),
823838
("--author-email", AUTHOR_EMAIL_ENABLED),
@@ -833,7 +848,8 @@ def run_commit_check() -> tuple[int, list[ScopeResult]]:
833848
2. PR description, AI rules only (when ``pr-body-ai-attribution: true`` and in a PR
834849
event; with only ``ai-label`` set it is read for the label, and no
835850
scope is reported)
836-
3. Individual PR commit messages (when ``message: true`` and in a PR event)
851+
3. Individual PR commit messages (when ``message: true`` and in a PR event;
852+
AI rules only with ``message-ai-attribution: true``)
837853
4. All remaining checks (branch, author name/email, etc.)
838854
839855
Outside of a PR event all enabled checks are handed to the CLI at once.
@@ -861,7 +877,7 @@ def run_commit_check() -> tuple[int, list[ScopeResult]]:
861877
PR_BODY_AI_TOOLS[:] = scope.ai_tools
862878

863879
# ---- 3. Commit message checks -----------------------------------------
864-
if MESSAGE_ENABLED:
880+
if MESSAGE_ENABLED or MESSAGE_AI_ATTRIBUTION_ENABLED:
865881
pr_commits = get_pr_commit_messages()
866882
if pr_commits:
867883
# In PR context: check each commit individually to avoid
@@ -879,7 +895,9 @@ def run_commit_check() -> tuple[int, list[ScopeResult]]:
879895
# ---- 4. Remaining checks (branch, author, etc.) -----------------------
880896
# Outside a PR, check the HEAD commit message directly.
881897
if "--message" in args:
882-
results.append(check_scope("Commit message", ["--message"], sha=head_sha()))
898+
results.append(
899+
check_scope("Commit message", message_check_args(), sha=head_sha())
900+
)
883901
args = [a for a in args if a != "--message"]
884902
rev = None
885903
if is_pr_event() and any(flag in AUTHOR_FLAGS for flag in args):

‎main_test.py‎

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,7 @@ def test_every_input_is_logged_as_a_debug_command(self):
154154
"::debug::PR_COMMENTS=false",
155155
"::debug::PR_TITLE=false",
156156
"::debug::PR_BODY_AI_ATTRIBUTION=false",
157+
"::debug::MESSAGE_AI_ATTRIBUTION=false",
157158
"::debug::AI_LABEL=false",
158159
],
159160
)
@@ -421,6 +422,21 @@ def test_a_commit_in_the_clone_carries_its_own_author(self):
421422
self.assertEqual(env["GIT_AUTHOR_NAME"], "Renovate Bot")
422423
self.assertEqual(env["GIT_AUTHOR_EMAIL"], "bot@renovateapp.com")
423424

425+
def test_message_ai_attribution_checks_only_the_ai_rules(self):
426+
"""The commit still goes in with its author, so CC017 judges it."""
427+
self.local_author.return_value = ("Jane Dev", "jane@example.com")
428+
mock_result = MagicMock(returncode=0, stdout=json_output(make_check("message")))
429+
with (
430+
patch("main.MESSAGE_AI_ATTRIBUTION_ENABLED", True),
431+
patch("main.subprocess.run", return_value=mock_result) as mock_run,
432+
):
433+
main.run_pr_message_checks([(SHA_A, "WIP")])
434+
self.assertEqual(
435+
mock_run.call_args[0][0],
436+
["commit-check", "--format", "json", "--message-ai-attribution"],
437+
)
438+
self.assertEqual(mock_run.call_args[1]["env"]["GIT_AUTHOR_NAME"], "Jane Dev")
439+
424440
def test_a_blank_author_field_is_left_to_git(self):
425441
self.local_author.return_value = ("Renovate Bot", "")
426442
mock_result = MagicMock(returncode=0, stdout=json_output(make_check("message")))
@@ -960,6 +976,43 @@ def test_non_pr_message_check_uses_commit_message_scope(self):
960976
# HEAD is the commit that was checked, so the scope names it too.
961977
mock_scope.assert_called_once_with("Commit message", ["--message"], sha=SHA_A)
962978

979+
def test_message_ai_attribution_alone_checks_each_commit(self):
980+
"""With message: false the commits are still checked, for AI only."""
981+
with (
982+
patch("main.MESSAGE_ENABLED", False),
983+
patch("main.MESSAGE_AI_ATTRIBUTION_ENABLED", True),
984+
patch("main.BRANCH_ENABLED", False),
985+
patch("main.AUTHOR_NAME_ENABLED", False),
986+
patch("main.AUTHOR_EMAIL_ENABLED", False),
987+
patch("main.get_pr_commit_messages", return_value=[(SHA_A, "WIP")]),
988+
patch("main.run_pr_message_checks", return_value=[pass_scope()]) as mock_pr,
989+
patch("main.run_other_checks", return_value=[]) as mock_other,
990+
):
991+
rc, _results = main.run_commit_check()
992+
self.assertEqual(rc, 0)
993+
mock_pr.assert_called_once_with([(SHA_A, "WIP")])
994+
mock_other.assert_called_once_with([], rev=None)
995+
996+
def test_message_ai_attribution_narrows_the_head_check(self):
997+
"""Outside a PR, HEAD is checked for AI only, even with message: true."""
998+
with (
999+
patch("main.MESSAGE_ENABLED", True),
1000+
patch("main.MESSAGE_AI_ATTRIBUTION_ENABLED", True),
1001+
patch("main.BRANCH_ENABLED", False),
1002+
patch("main.AUTHOR_NAME_ENABLED", False),
1003+
patch("main.AUTHOR_EMAIL_ENABLED", False),
1004+
patch("main.get_pr_commit_messages", return_value=[]),
1005+
patch("main.head_sha", return_value=SHA_A),
1006+
patch(
1007+
"main.check_scope", return_value=pass_scope("Commit message")
1008+
) as mock_scope,
1009+
patch("main.run_other_checks", return_value=[]),
1010+
):
1011+
main.run_commit_check()
1012+
mock_scope.assert_called_once_with(
1013+
"Commit message", ["--message-ai-attribution"], sha=SHA_A
1014+
)
1015+
9631016
def test_message_flag_removed_before_other_checks_in_pr(self):
9641017
captured_args = []
9651018

0 commit comments

Comments
 (0)