git-commit-guard 0.21.0__tar.gz → 0.22.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (27) hide show
  1. {git_commit_guard-0.21.0 → git_commit_guard-0.22.1}/.github/workflows/lint-commits.yml +1 -1
  2. git_commit_guard-0.22.1/.github/workflows/lint-md.yml +68 -0
  3. {git_commit_guard-0.21.0 → git_commit_guard-0.22.1}/.github/workflows/release.yml +0 -3
  4. {git_commit_guard-0.21.0 → git_commit_guard-0.22.1}/PKG-INFO +22 -13
  5. {git_commit_guard-0.21.0 → git_commit_guard-0.22.1}/README.md +21 -12
  6. {git_commit_guard-0.21.0 → git_commit_guard-0.22.1}/action.yml +10 -2
  7. {git_commit_guard-0.21.0 → git_commit_guard-0.22.1}/docs/index.html +17 -9
  8. {git_commit_guard-0.21.0 → git_commit_guard-0.22.1}/src/git_commit_guard/__init__.py +35 -2
  9. {git_commit_guard-0.21.0 → git_commit_guard-0.22.1}/tests/test_git_commit_guard.py +95 -2
  10. git_commit_guard-0.21.0/.github/workflows/lint-md.yml +0 -26
  11. {git_commit_guard-0.21.0 → git_commit_guard-0.22.1}/.editorconfig +0 -0
  12. {git_commit_guard-0.21.0 → git_commit_guard-0.22.1}/.github/workflows/coverage-baseline.yml +0 -0
  13. {git_commit_guard-0.21.0 → git_commit_guard-0.22.1}/.github/workflows/coverage-comment.yml +0 -0
  14. {git_commit_guard-0.21.0 → git_commit_guard-0.22.1}/.github/workflows/lint-python.yml +0 -0
  15. {git_commit_guard-0.21.0 → git_commit_guard-0.22.1}/.github/workflows/lint-workflows.yml +0 -0
  16. {git_commit_guard-0.21.0 → git_commit_guard-0.22.1}/.github/workflows/test.yml +0 -0
  17. {git_commit_guard-0.21.0 → git_commit_guard-0.22.1}/.gitignore +0 -0
  18. {git_commit_guard-0.21.0 → git_commit_guard-0.22.1}/.markdownlint.json +0 -0
  19. {git_commit_guard-0.21.0 → git_commit_guard-0.22.1}/.pre-commit-hooks.yaml +0 -0
  20. {git_commit_guard-0.21.0 → git_commit_guard-0.22.1}/.python-version +0 -0
  21. {git_commit_guard-0.21.0 → git_commit_guard-0.22.1}/LICENSE +0 -0
  22. {git_commit_guard-0.21.0 → git_commit_guard-0.22.1}/cliff.toml +0 -0
  23. {git_commit_guard-0.21.0 → git_commit_guard-0.22.1}/docs/commit-guard-icon.svg +0 -0
  24. {git_commit_guard-0.21.0 → git_commit_guard-0.22.1}/pyproject.toml +0 -0
  25. {git_commit_guard-0.21.0 → git_commit_guard-0.22.1}/ruff.toml +0 -0
  26. {git_commit_guard-0.21.0 → git_commit_guard-0.22.1}/tests/__init__.py +0 -0
  27. {git_commit_guard-0.21.0 → git_commit_guard-0.22.1}/uv.lock +0 -0
@@ -22,6 +22,6 @@ jobs:
22
22
  key: nltk-averaged-perceptron-tagger-punkt
23
23
  - name: Lint commits
24
24
  # yamllint disable-line rule:line-length
25
- uses: benner/commit-guard@87889d2f3a1fb68bfa55dbbab28c96def415341e # v0.20.1
25
+ uses: benner/commit-guard@e438d8c4c287a1575e0cda352222fbbe71a88231 # v0.22.0
26
26
  with:
27
27
  range: origin/${{ github.base_ref }}..HEAD
@@ -0,0 +1,68 @@
1
+ ---
2
+ name: Lint Markdown
3
+ on: # yamllint disable-line rule:truthy
4
+ pull_request:
5
+ permissions:
6
+ contents: read
7
+ jobs:
8
+ lint-md:
9
+ runs-on: ubuntu-latest
10
+ permissions:
11
+ contents: read
12
+ pull-requests: write
13
+ steps:
14
+ - name: Checkout code
15
+ # yamllint disable-line rule:line-length
16
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
17
+ with:
18
+ persist-credentials: false
19
+ - name: Lint Markdown files with reviewdog
20
+ # yamllint disable-line rule:line-length
21
+ uses: reviewdog/action-markdownlint@3667398db9118d7e78f7a63d10e26ce454ba5f58 # v0.26.2
22
+ with:
23
+ github_token: ${{ secrets.GITHUB_TOKEN }}
24
+ reporter: github-pr-review
25
+ level: info
26
+ fail_level: any
27
+ lint-md-rumdl:
28
+ runs-on: ubuntu-latest
29
+ permissions:
30
+ contents: read
31
+ pull-requests: write
32
+ steps:
33
+ - name: Checkout code
34
+ # yamllint disable-line rule:line-length
35
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
36
+ with:
37
+ persist-credentials: false
38
+ - name: Set up uv
39
+ # yamllint disable-line rule:line-length
40
+ uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7.6.0
41
+ with:
42
+ enable-cache: false
43
+ - name: Set up reviewdog
44
+ # yamllint disable-line rule:line-length
45
+ uses: reviewdog/action-setup@d8a7baabd7f3e8544ee4dbde3ee41d0011c3a93f # ratchet:reviewdog/action-setup@v1
46
+ - name: Lint Markdown files with rumdl via reviewdog
47
+ env:
48
+ REVIEWDOG_GITHUB_API_TOKEN: ${{ secrets.GITHUB_TOKEN }}
49
+ run: |-
50
+ uvx rumdl check . --output-format json-lines > rumdl.jsonl || true
51
+ jq -c '
52
+ {
53
+ source: {name: "rumdl"},
54
+ severity: (.severity | ascii_upcase),
55
+ message: .message,
56
+ location: {
57
+ path: .file,
58
+ range: {start: {line: .line, column: .column}}
59
+ },
60
+ code: {value: .rule}
61
+ }
62
+ ' rumdl.jsonl \
63
+ | reviewdog \
64
+ -f=rdjsonl \
65
+ -name=rumdl \
66
+ -reporter=github-pr-review \
67
+ -filter-mode=added \
68
+ -fail-level=any
@@ -18,9 +18,6 @@ jobs:
18
18
  with:
19
19
  persist-credentials: false
20
20
  fetch-depth: 0
21
- - name: Set up Python
22
- # yamllint disable-line rule:line-length
23
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
24
21
  - name: Set up uv
25
22
  # yamllint disable-line rule:line-length
26
23
  uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7.6.0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: git-commit-guard
3
- Version: 0.21.0
3
+ Version: 0.22.1
4
4
  Summary: Opinionated conventional commit message linter with imperative mood detection
5
5
  Project-URL: Homepage, https://github.com/benner/commit-guard
6
6
  Project-URL: Repository, https://github.com/benner/commit-guard
@@ -47,7 +47,9 @@ Opinionated conventional commit message linter with imperative mood detection.
47
47
  $ commit-guard
48
48
  ✗ [subject] subject does not match 'type(scope): description': WIP
49
49
  ✗ [signed-off] missing 'Signed-off-by' trailer — use 'git commit -s'
50
- ✗ [signature] commit is not signed (GPG/SSH)
50
+ ✗ [signature] signature could not be verified — commit may be
51
+ unsigned, or signed with a key not uploaded as a
52
+ Signing key on https://github.com/settings/keys
51
53
  ```
52
54
 
53
55
  ## Installation
@@ -249,8 +251,10 @@ The `signature` check verifies the commit without any local keyring setup:
249
251
  `{username}@users.noreply.github.com`) — no API call needed.
250
252
  3. If neither of the above resolves a username, fall back to searching GitHub
251
253
  by the commit author's email.
252
- 4. Fetch the resolved user's public keys from `github.com/{username}.gpg` and
253
- `github.com/{username}.keys`.
254
+ 4. Fetch the resolved user's public keys from `github.com/{username}.gpg`
255
+ (GPG) and the `/users/{username}/ssh_signing_keys` API (SSH keys tagged
256
+ with the **Signing key** role). Auth-only SSH keys are deliberately not
257
+ accepted — this mirrors GitHub's "Verified" badge semantics.
254
258
  5. Try GPG verification: import the fetched key into a temporary keyring and
255
259
  run `git verify-commit`.
256
260
  6. Try SSH verification: write a temporary `allowed_signers` file and run
@@ -261,7 +265,9 @@ If the author cannot be resolved via either method, or the GitHub API is
261
265
  unreachable, the check fails with a clear error.
262
266
 
263
267
  For private repositories, set `GITHUB_TOKEN` or `GH_TOKEN` so the Commits API
264
- can authenticate.
268
+ can authenticate. The official GitHub Action wires the workflow's automatic
269
+ token via the `github-token` input, so no manual `env:` is required; override
270
+ with a PAT only for cross-repo lookups.
265
271
 
266
272
  ### Configuration file
267
273
 
@@ -310,7 +316,7 @@ COMMIT_GUARD_GIT_TIMEOUT=30 commit-guard --range origin/main..HEAD
310
316
  In GitHub Actions, set it at the step or job level:
311
317
 
312
318
  ```yaml
313
- - uses: benner/commit-guard@v0.21.0
319
+ - uses: benner/commit-guard@v0.22.1
314
320
  env:
315
321
  COMMIT_GUARD_GIT_TIMEOUT: 30
316
322
  with:
@@ -394,7 +400,7 @@ steps:
394
400
  - uses: actions/checkout@v4
395
401
  with:
396
402
  fetch-depth: 0
397
- - uses: benner/commit-guard@v0.21.0
403
+ - uses: benner/commit-guard@v0.22.1
398
404
  ```
399
405
 
400
406
  Check all commits in a pull request:
@@ -410,7 +416,7 @@ jobs:
410
416
  - uses: actions/checkout@v4
411
417
  with:
412
418
  fetch-depth: 0
413
- - uses: benner/commit-guard@v0.21.0
419
+ - uses: benner/commit-guard@v0.22.1
414
420
  with:
415
421
  range: ${{ env.PR_BASE }}..${{ env.PR_HEAD }}
416
422
  ```
@@ -418,7 +424,7 @@ jobs:
418
424
  Check a specific commit SHA (mirrors the positional CLI argument):
419
425
 
420
426
  ```yaml
421
- - uses: benner/commit-guard@v0.21.0
427
+ - uses: benner/commit-guard@v0.22.1
422
428
  with:
423
429
  rev: ${{ github.sha }}
424
430
  ```
@@ -436,7 +442,7 @@ jobs:
436
442
  - uses: actions/checkout@v4
437
443
  with:
438
444
  fetch-depth: 0
439
- - uses: benner/commit-guard@v0.21.0
445
+ - uses: benner/commit-guard@v0.22.1
440
446
  with:
441
447
  range: ${{ env.PR_BASE }}..${{ env.PR_HEAD }}
442
448
  disable: signed-off,signature
@@ -456,7 +462,7 @@ jobs:
456
462
  When `output-file` is set the action exposes the path as an output:
457
463
 
458
464
  ```yaml
459
- - uses: benner/commit-guard@v0.21.0
465
+ - uses: benner/commit-guard@v0.22.1
460
466
  id: cg
461
467
  with:
462
468
  range: ${{ env.PR_BASE }}..${{ env.PR_HEAD }}
@@ -472,7 +478,7 @@ Add to your `.pre-commit-config.yaml`:
472
478
  ---
473
479
  repos:
474
480
  - repo: https://github.com/benner/commit-guard
475
- rev: v0.21.0
481
+ rev: v0.22.1
476
482
  hooks:
477
483
  - id: commit-guard
478
484
  - id: commit-guard-signature
@@ -497,11 +503,14 @@ To selectively enable or disable checks, pass `args`:
497
503
 
498
504
  ## Imperative mood detection
499
505
 
500
- commit-guard combines two strategies to detect non-imperative descriptions:
506
+ commit-guard combines three strategies to detect non-imperative descriptions:
501
507
 
502
508
  1. nltk POS tagging — flags words tagged as past tense (`VBD`),
503
509
  gerund (`VBG`), third person (`VBZ`), etc.
504
510
  2. WordNet morphology as a fallback for words the tagger misclassifies.
511
+ 3. Hyphenated verb prefixes — accepts `re-enable`, `auto-detect`,
512
+ `pre-process`, `co-locate`, `under-mine` and similar
513
+ `<prefix>-<verb>` compounds the POS tagger misclassifies.
505
514
 
506
515
  This catches common mistakes like `added logging` or `fixes bug` while
507
516
  keeping false positives low.
@@ -26,7 +26,9 @@ Opinionated conventional commit message linter with imperative mood detection.
26
26
  $ commit-guard
27
27
  ✗ [subject] subject does not match 'type(scope): description': WIP
28
28
  ✗ [signed-off] missing 'Signed-off-by' trailer — use 'git commit -s'
29
- ✗ [signature] commit is not signed (GPG/SSH)
29
+ ✗ [signature] signature could not be verified — commit may be
30
+ unsigned, or signed with a key not uploaded as a
31
+ Signing key on https://github.com/settings/keys
30
32
  ```
31
33
 
32
34
  ## Installation
@@ -228,8 +230,10 @@ The `signature` check verifies the commit without any local keyring setup:
228
230
  `{username}@users.noreply.github.com`) — no API call needed.
229
231
  3. If neither of the above resolves a username, fall back to searching GitHub
230
232
  by the commit author's email.
231
- 4. Fetch the resolved user's public keys from `github.com/{username}.gpg` and
232
- `github.com/{username}.keys`.
233
+ 4. Fetch the resolved user's public keys from `github.com/{username}.gpg`
234
+ (GPG) and the `/users/{username}/ssh_signing_keys` API (SSH keys tagged
235
+ with the **Signing key** role). Auth-only SSH keys are deliberately not
236
+ accepted — this mirrors GitHub's "Verified" badge semantics.
233
237
  5. Try GPG verification: import the fetched key into a temporary keyring and
234
238
  run `git verify-commit`.
235
239
  6. Try SSH verification: write a temporary `allowed_signers` file and run
@@ -240,7 +244,9 @@ If the author cannot be resolved via either method, or the GitHub API is
240
244
  unreachable, the check fails with a clear error.
241
245
 
242
246
  For private repositories, set `GITHUB_TOKEN` or `GH_TOKEN` so the Commits API
243
- can authenticate.
247
+ can authenticate. The official GitHub Action wires the workflow's automatic
248
+ token via the `github-token` input, so no manual `env:` is required; override
249
+ with a PAT only for cross-repo lookups.
244
250
 
245
251
  ### Configuration file
246
252
 
@@ -289,7 +295,7 @@ COMMIT_GUARD_GIT_TIMEOUT=30 commit-guard --range origin/main..HEAD
289
295
  In GitHub Actions, set it at the step or job level:
290
296
 
291
297
  ```yaml
292
- - uses: benner/commit-guard@v0.21.0
298
+ - uses: benner/commit-guard@v0.22.1
293
299
  env:
294
300
  COMMIT_GUARD_GIT_TIMEOUT: 30
295
301
  with:
@@ -373,7 +379,7 @@ steps:
373
379
  - uses: actions/checkout@v4
374
380
  with:
375
381
  fetch-depth: 0
376
- - uses: benner/commit-guard@v0.21.0
382
+ - uses: benner/commit-guard@v0.22.1
377
383
  ```
378
384
 
379
385
  Check all commits in a pull request:
@@ -389,7 +395,7 @@ jobs:
389
395
  - uses: actions/checkout@v4
390
396
  with:
391
397
  fetch-depth: 0
392
- - uses: benner/commit-guard@v0.21.0
398
+ - uses: benner/commit-guard@v0.22.1
393
399
  with:
394
400
  range: ${{ env.PR_BASE }}..${{ env.PR_HEAD }}
395
401
  ```
@@ -397,7 +403,7 @@ jobs:
397
403
  Check a specific commit SHA (mirrors the positional CLI argument):
398
404
 
399
405
  ```yaml
400
- - uses: benner/commit-guard@v0.21.0
406
+ - uses: benner/commit-guard@v0.22.1
401
407
  with:
402
408
  rev: ${{ github.sha }}
403
409
  ```
@@ -415,7 +421,7 @@ jobs:
415
421
  - uses: actions/checkout@v4
416
422
  with:
417
423
  fetch-depth: 0
418
- - uses: benner/commit-guard@v0.21.0
424
+ - uses: benner/commit-guard@v0.22.1
419
425
  with:
420
426
  range: ${{ env.PR_BASE }}..${{ env.PR_HEAD }}
421
427
  disable: signed-off,signature
@@ -435,7 +441,7 @@ jobs:
435
441
  When `output-file` is set the action exposes the path as an output:
436
442
 
437
443
  ```yaml
438
- - uses: benner/commit-guard@v0.21.0
444
+ - uses: benner/commit-guard@v0.22.1
439
445
  id: cg
440
446
  with:
441
447
  range: ${{ env.PR_BASE }}..${{ env.PR_HEAD }}
@@ -451,7 +457,7 @@ Add to your `.pre-commit-config.yaml`:
451
457
  ---
452
458
  repos:
453
459
  - repo: https://github.com/benner/commit-guard
454
- rev: v0.21.0
460
+ rev: v0.22.1
455
461
  hooks:
456
462
  - id: commit-guard
457
463
  - id: commit-guard-signature
@@ -476,11 +482,14 @@ To selectively enable or disable checks, pass `args`:
476
482
 
477
483
  ## Imperative mood detection
478
484
 
479
- commit-guard combines two strategies to detect non-imperative descriptions:
485
+ commit-guard combines three strategies to detect non-imperative descriptions:
480
486
 
481
487
  1. nltk POS tagging — flags words tagged as past tense (`VBD`),
482
488
  gerund (`VBG`), third person (`VBZ`), etc.
483
489
  2. WordNet morphology as a fallback for words the tagger misclassifies.
490
+ 3. Hyphenated verb prefixes — accepts `re-enable`, `auto-detect`,
491
+ `pre-process`, `co-locate`, `under-mine` and similar
492
+ `<prefix>-<verb>` compounds the POS tagger misclassifies.
484
493
 
485
494
  This catches common mistakes like `added logging` or `fixes bug` while
486
495
  keeping false positives low.
@@ -39,7 +39,8 @@ inputs:
39
39
  required: false
40
40
  default: 'false'
41
41
  no-trailing-chars:
42
- description: Forbidden trailing characters, comma-separated (default '.', '!', '?', ' ')
42
+ description: Forbidden trailing characters, comma-separated (default '.', '!',
43
+ '?', ' ')
43
44
  required: false
44
45
  allow-empty:
45
46
  description: Exit 0 when --range yields no commits
@@ -58,9 +59,15 @@ inputs:
58
59
  output-file:
59
60
  description: Write JSONL results to this file path (text still goes to stdout)
60
61
  required: false
62
+ github-token:
63
+ description: GitHub token for Commits API access (signature check). Defaults to
64
+ the workflow's automatic token; override with a PAT for cross-repo lookups.
65
+ required: false
66
+ default: ${{ github.token }}
61
67
  outputs:
62
68
  output-file:
63
- description: Path to the JSONL output file (set only when output-file input is provided)
69
+ description: Path to the JSONL output file (set only when output-file input is
70
+ provided)
64
71
  value: ${{ steps.run.outputs.output-file }}
65
72
  runs:
66
73
  using: composite
@@ -76,6 +83,7 @@ runs:
76
83
  - name: Run commit-guard
77
84
  id: run
78
85
  env:
86
+ GITHUB_TOKEN: ${{ inputs.github-token }}
79
87
  CG_REV: ${{ inputs.rev }}
80
88
  CG_RANGE: ${{ inputs.range }}
81
89
  CG_ENABLE: ${{ inputs.enable }}
@@ -294,7 +294,9 @@
294
294
  <pre><code id="hero-code">$ commit-guard
295
295
  <span class="c-red">✗</span> <span class="c-dim">[subject]</span> subject does not match 'type(scope): description': WIP
296
296
  <span class="c-red">✗</span> <span class="c-dim">[signed-off]</span> missing 'Signed-off-by' trailer — use 'git commit -s'
297
- <span class="c-red">✗</span> <span class="c-dim">[signature]</span> commit is not signed (GPG/SSH)</code></pre>
297
+ <span class="c-red">✗</span> <span class="c-dim">[signature]</span> signature could not be verified — commit may be
298
+ unsigned, or signed with a key not uploaded as a
299
+ Signing key on https://github.com/settings/keys</code></pre>
298
300
  </div>
299
301
  <div class="hero-dots">
300
302
  <button class="hero-dot" data-i="0"></button>
@@ -464,8 +466,11 @@ require-subject-pattern = "[A-Z]+-[0-9]+"</code></pre>
464
466
  <li>If neither of the above resolves a username, fall back to
465
467
  searching GitHub by the commit author's email.</li>
466
468
  <li>Fetch the resolved user's public keys from
467
- <code>github.com/{username}.gpg</code> and
468
- <code>github.com/{username}.keys</code>.</li>
469
+ <code>github.com/{username}.gpg</code> (GPG) and
470
+ <code>/users/{username}/ssh_signing_keys</code> (SSH keys tagged
471
+ with the <strong>Signing key</strong> role). Auth-only SSH keys
472
+ are deliberately not accepted — this mirrors GitHub's
473
+ &ldquo;Verified&rdquo; badge semantics.</li>
469
474
  <li>Try GPG verification using a temporary keyring.</li>
470
475
  <li>Try SSH verification using a temporary <code>allowed_signers</code> file.</li>
471
476
  <li>Pass if any key verifies; fail if none do.</li>
@@ -474,7 +479,10 @@ require-subject-pattern = "[A-Z]+-[0-9]+"</code></pre>
474
479
  If the author cannot be resolved via either method, or the GitHub API
475
480
  is unreachable, the check fails with a clear error. For private
476
481
  repositories, set <code>GITHUB_TOKEN</code> or <code>GH_TOKEN</code>
477
- so the Commits API can authenticate. Disable the
482
+ so the Commits API can authenticate. The official GitHub Action wires
483
+ the workflow's automatic token via the <code>github-token</code>
484
+ input, so no manual <code>env:</code> is required; override with a
485
+ PAT only for cross-repo lookups. Disable the
478
486
  <code>signature</code> check if GitHub API access is unavailable:
479
487
  </p>
480
488
  <pre><code class="language-bash">commit-guard --disable signature</code></pre>
@@ -558,13 +566,13 @@ require-subject-pattern = "[A-Z]+-[0-9]+"</code></pre>
558
566
  - uses: actions/checkout@v4
559
567
  with:
560
568
  fetch-depth: 0
561
- - uses: benner/commit-guard@v0.21.0
569
+ - uses: benner/commit-guard@v0.22.1
562
570
  with:
563
571
  range: ${{ env.PR_BASE }}..${{ env.PR_HEAD }}
564
572
  disable: signed-off,signature</code></pre>
565
573
 
566
574
  <p>Check a specific commit SHA:</p>
567
- <pre><code class="language-yaml"> - uses: benner/commit-guard@v0.21.0
575
+ <pre><code class="language-yaml"> - uses: benner/commit-guard@v0.22.1
568
576
  with:
569
577
  rev: ${{ github.sha }}</code></pre>
570
578
 
@@ -583,7 +591,7 @@ require-subject-pattern = "[A-Z]+-[0-9]+"</code></pre>
583
591
  When <code>output-file</code> is set the action exposes the path as
584
592
  a step output, making JSONL results available to subsequent steps:
585
593
  </p>
586
- <pre><code class="language-yaml"> - uses: benner/commit-guard@v0.21.0
594
+ <pre><code class="language-yaml"> - uses: benner/commit-guard@v0.22.1
587
595
  id: cg
588
596
  with:
589
597
  range: ${{ env.PR_BASE }}..${{ env.PR_HEAD }}
@@ -596,7 +604,7 @@ require-subject-pattern = "[A-Z]+-[0-9]+"</code></pre>
596
604
  <p>Add to <code>.pre-commit-config.yaml</code>:</p>
597
605
  <pre><code class="language-yaml">repos:
598
606
  - repo: https://github.com/benner/commit-guard
599
- rev: v0.21.0
607
+ rev: v0.22.1
600
608
  hooks:
601
609
  - id: commit-guard
602
610
  - id: commit-guard-signature</code></pre>
@@ -618,7 +626,7 @@ require-subject-pattern = "[A-Z]+-[0-9]+"</code></pre>
618
626
  ["$ commit-guard\n ", null],
619
627
  ["✗", "c-red"], [" [subject]", "c-dim"], [" subject does not match 'type(scope): description': WIP\n ", null],
620
628
  ["✗", "c-red"], [" [signed-off]", "c-dim"], [" missing 'Signed-off-by' trailer — use 'git commit -s'\n ", null],
621
- ["✗", "c-red"], [" [signature]", "c-dim"], [" commit is not signed (GPG/SSH)", null],
629
+ ["✗", "c-red"], [" [signature]", "c-dim"], [" signature could not be verified — commit may be\n unsigned, or signed with a key not uploaded as a\n Signing key on https://github.com/settings/keys", null],
622
630
  ],
623
631
  [
624
632
  ["$ commit-guard --range origin/main..HEAD\n", null],
@@ -34,6 +34,15 @@ TYPES = frozenset(
34
34
  )
35
35
 
36
36
  _NON_IMPERATIVE_SUFFIX_RE = re.compile(r"(?:ing|ed)$")
37
+ _VERB_FORMING_PREFIXES = frozenset(
38
+ {
39
+ "re",
40
+ "pre",
41
+ "auto",
42
+ "co",
43
+ "under",
44
+ }
45
+ )
37
46
  _TRAILER_RE = re.compile(r"^[\w-]+:\s+\S")
38
47
  _GITHUB_REMOTE_RE = re.compile(
39
48
  r"github\.com[:/](?P<owner>[^/]+)/(?P<repo>[^/\s]+?)(?:\.git)?$"
@@ -235,6 +244,13 @@ def check_imperative(desc, result):
235
244
  if tagged[1][1] != "VB":
236
245
  if wordnet.morphy(first, wordnet.VERB) == first:
237
246
  return
247
+ if "-" in first:
248
+ hyphen_prefix, hyphen_base = first.split("-", 1)
249
+ if (
250
+ hyphen_prefix in _VERB_FORMING_PREFIXES
251
+ and wordnet.morphy(hyphen_base, wordnet.VERB) == hyphen_base
252
+ ):
253
+ return
238
254
  result.error(
239
255
  f"expected imperative verb, got '{tagged[1][0]}' (POS={tagged[1][1]})",
240
256
  check=Check.IMPERATIVE,
@@ -336,9 +352,21 @@ def _fetch_url(url):
336
352
  return resp.read().decode()
337
353
 
338
354
 
355
+ def _fetch_github_signing_keys(username):
356
+ url = f"https://api.github.com/users/{username}/ssh_signing_keys"
357
+ headers = {"Accept": "application/vnd.github+json"}
358
+ token = os.environ.get("GITHUB_TOKEN") or os.environ.get("GH_TOKEN")
359
+ if token:
360
+ headers["Authorization"] = f"Bearer {token}"
361
+ req = urllib.request.Request(url, headers=headers) # noqa: S310 Audit URL open for permitted schemes
362
+ with urllib.request.urlopen(req, timeout=_git_timeout()) as resp: # noqa: S310 Audit URL open for permitted schemes
363
+ data = json.loads(resp.read())
364
+ return "\n".join(item["key"] for item in data)
365
+
366
+
339
367
  def _fetch_github_keys(username):
340
368
  gpg = _fetch_url(f"https://github.com/{username}.gpg")
341
- ssh = _fetch_url(f"https://github.com/{username}.keys")
369
+ ssh = _fetch_github_signing_keys(username)
342
370
  return gpg.strip(), ssh.strip()
343
371
 
344
372
 
@@ -450,7 +478,12 @@ def check_signature(rev, result):
450
478
  if _verify_ssh(rev, email, ssh_text):
451
479
  result.info("signature type: SSH", check=Check.SIGNATURE)
452
480
  return
453
- result.error("commit is not signed (GPG/SSH)", check=Check.SIGNATURE)
481
+ result.error(
482
+ "signature could not be verified — commit may be unsigned, "
483
+ "or signed with a key not uploaded as a Signing key on "
484
+ "https://github.com/settings/keys",
485
+ check=Check.SIGNATURE,
486
+ )
454
487
  except subprocess.TimeoutExpired:
455
488
  result.error(
456
489
  "git operation timed out — cannot verify signature",
@@ -17,6 +17,7 @@ from git_commit_guard import (
17
17
  _ensure_nltk_data,
18
18
  _fetch_github_commit_author,
19
19
  _fetch_github_keys,
20
+ _fetch_github_signing_keys,
20
21
  _fetch_github_username,
21
22
  _fetch_url,
22
23
  _get_author_email,
@@ -572,6 +573,39 @@ class TestCheckImperative:
572
573
  assert not r.ok
573
574
  assert "POS=NN" in r.errors[0][2]
574
575
 
576
+ def test_hyphen_re_prefix_verb_passes(self):
577
+ r = Result()
578
+ check_imperative("re-enable signature check", r)
579
+ assert r.ok
580
+
581
+ def test_hyphen_auto_prefix_verb_passes(self):
582
+ r = Result()
583
+ check_imperative("auto-detect format from header", r)
584
+ assert r.ok
585
+
586
+ def test_hyphen_pre_prefix_verb_passes(self):
587
+ r = Result()
588
+ check_imperative("pre-process input lines", r)
589
+ assert r.ok
590
+
591
+ def test_hyphen_unknown_prefix_fails(self):
592
+ # 'high' is not in the verb-forming prefix allowlist
593
+ r = Result()
594
+ check_imperative("high-level overview of foo", r)
595
+ assert not r.ok
596
+
597
+ def test_hyphen_non_verb_base_fails(self):
598
+ # 'in' is not a verb, so even though split shape matches, base check rejects
599
+ r = Result()
600
+ check_imperative("built-in helper function", r)
601
+ assert not r.ok
602
+
603
+ def test_hyphen_inflected_suffix_still_fails(self):
604
+ # Suffix check runs before hyphen escape — 're-running' caught by ing$
605
+ r = Result()
606
+ check_imperative("re-running the tests", r)
607
+ assert not r.ok
608
+
575
609
 
576
610
  class TestDownloadIfMissing:
577
611
  def test_skips_download_when_present(self):
@@ -735,10 +769,69 @@ class TestFetchGithubCommitAuthor:
735
769
  assert captured[0].get_header("Authorization") == "Bearer ghtoken"
736
770
 
737
771
 
772
+ class TestFetchGithubSigningKeys:
773
+ def _mock_response(self, data):
774
+ mock_resp = MagicMock()
775
+ mock_resp.__enter__ = lambda s: s
776
+ mock_resp.__exit__ = MagicMock(return_value=False)
777
+ mock_resp.read.return_value = json.dumps(data).encode()
778
+ return mock_resp
779
+
780
+ def test_returns_keys_joined_by_newline(self):
781
+ resp = self._mock_response(
782
+ [{"key": "ssh-ed25519 AAAA"}, {"key": "ssh-rsa BBBB"}]
783
+ )
784
+ with patch("git_commit_guard.urllib.request.urlopen", return_value=resp):
785
+ assert (
786
+ _fetch_github_signing_keys("testuser")
787
+ == "ssh-ed25519 AAAA\nssh-rsa BBBB"
788
+ )
789
+
790
+ def test_empty_list_returns_empty_string(self):
791
+ resp = self._mock_response([])
792
+ with patch("git_commit_guard.urllib.request.urlopen", return_value=resp):
793
+ assert _fetch_github_signing_keys("testuser") == ""
794
+
795
+ def test_github_token_sent_in_header(self):
796
+ resp = self._mock_response([])
797
+ captured = []
798
+
799
+ def mock_urlopen(req, **_):
800
+ captured.append(req)
801
+ return resp
802
+
803
+ with (
804
+ patch("git_commit_guard.urllib.request.urlopen", side_effect=mock_urlopen),
805
+ patch.dict("os.environ", {"GITHUB_TOKEN": "mytoken"}, clear=False),
806
+ ):
807
+ _fetch_github_signing_keys("testuser")
808
+ assert captured[0].get_header("Authorization") == "Bearer mytoken"
809
+
810
+ def test_gh_token_used_when_github_token_absent(self):
811
+ resp = self._mock_response([])
812
+ captured = []
813
+
814
+ def mock_urlopen(req, **_):
815
+ captured.append(req)
816
+ return resp
817
+
818
+ env = {k: v for k, v in os.environ.items() if k != "GITHUB_TOKEN"}
819
+ env["GH_TOKEN"] = "ghtoken" # noqa: S105 Possible hardcoded password assigned to: "GH_TOKEN"
820
+ with (
821
+ patch("git_commit_guard.urllib.request.urlopen", side_effect=mock_urlopen),
822
+ patch.dict("os.environ", env, clear=True),
823
+ ):
824
+ _fetch_github_signing_keys("testuser")
825
+ assert captured[0].get_header("Authorization") == "Bearer ghtoken"
826
+
827
+
738
828
  class TestFetchGithubKeys:
739
829
  def test_returns_gpg_and_ssh(self):
740
- with patch(
741
- "git_commit_guard._fetch_url", side_effect=["GPG KEY\n", "SSH KEY\n"]
830
+ with (
831
+ patch("git_commit_guard._fetch_url", return_value="GPG KEY\n"),
832
+ patch(
833
+ "git_commit_guard._fetch_github_signing_keys", return_value="SSH KEY\n"
834
+ ),
742
835
  ):
743
836
  gpg, ssh = _fetch_github_keys("testuser")
744
837
  assert gpg == "GPG KEY"
@@ -1,26 +0,0 @@
1
- ---
2
- name: Lint Markdown
3
- on: # yamllint disable-line rule:truthy
4
- pull_request:
5
- permissions:
6
- contents: read
7
- jobs:
8
- lint-md:
9
- runs-on: ubuntu-latest
10
- permissions:
11
- contents: read
12
- pull-requests: write
13
- steps:
14
- - name: Checkout code
15
- # yamllint disable-line rule:line-length
16
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
17
- with:
18
- persist-credentials: false
19
- - name: Lint Markdown files with reviewdog
20
- # yamllint disable-line rule:line-length
21
- uses: reviewdog/action-markdownlint@3667398db9118d7e78f7a63d10e26ce454ba5f58 # v0.26.2
22
- with:
23
- github_token: ${{ secrets.GITHUB_TOKEN }}
24
- reporter: github-pr-review
25
- level: info
26
- fail_level: any