Skip to content
Merged
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
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ Opinionated conventional commit message linter with imperative mood detection.
verb, verified via nltk POS tagging — not a hand-coded regex of "bad"
words.
* **Signature verification without a local keyring.** Resolves the commit
author via the GitHub API and verifies GPG/SSH against their published
committer via the GitHub API and verifies GPG/SSH against their published
`.gpg`/`.keys` — no per-runner key management.
* **Strict by default.** Subject format, body, trailers, `Signed-off-by`,
and signature all enforced out of the box; opt out with `--disable`.
Expand Down Expand Up @@ -221,15 +221,15 @@ independently of `--enable`/`--disable`.
The `signature` check verifies the commit without any local keyring setup:

1. If the repo has a GitHub remote, call the Commits API
(`GET /repos/{owner}/{repo}/commits/{sha}`) to resolve the author's GitHub
(`GET /repos/{owner}/{repo}/commits/{sha}`) to resolve the committer's GitHub
username — this works for corporate emails, noreply addresses, or any email
not listed publicly on a GitHub profile.
2. If the Commits API is unavailable (no GitHub remote, commit not yet pushed,
or API error), parse the username directly from a GitHub noreply address
(`{id}+{username}@users.noreply.github.com` or
`{username}@users.noreply.github.com`) — no API call needed.
3. If neither of the above resolves a username, fall back to searching GitHub
by the commit author's email.
by the commit committer's email.
4. Fetch the resolved user's public keys from `github.com/{username}.gpg`
(GPG) and the `/users/{username}/ssh_signing_keys` API (SSH keys tagged
with the **Signing key** role). Auth-only SSH keys are deliberately not
Expand All @@ -240,7 +240,7 @@ The `signature` check verifies the commit without any local keyring setup:
`git verify-commit` with the SSH allowed-signers config.
7. If any key verifies, the check passes. If none do, it fails.

If the author cannot be resolved via either method, or the GitHub API is
If the committer cannot be resolved via either method, or the GitHub API is
unreachable, the check fails with a clear error.

For private repositories, set `GITHUB_TOKEN` or `GH_TOKEN` so the Commits API
Expand Down
8 changes: 4 additions & 4 deletions docs/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -315,7 +315,7 @@ <h2>Why commit-guard? <a href="#why" class="anchor">#</a></h2>
</li>
<li>
<strong>Signature verification without a local keyring.</strong>
Resolves the commit author via the GitHub API and verifies GPG/SSH
Resolves the commit committer via the GitHub API and verifies GPG/SSH
against their published <code>.gpg</code>/<code>.keys</code> — no
per-runner key management.
</li>
Expand Down Expand Up @@ -456,15 +456,15 @@ <h3>Signature verification</h3>
<ol>
<li>If the repo has a GitHub remote, call the Commits API
(<code>GET /repos/{owner}/{repo}/commits/{sha}</code>) to resolve
the author's GitHub username — works for corporate emails, noreply
the committer's GitHub username — works for corporate emails, noreply
addresses, or any email not listed publicly on a GitHub profile.</li>
<li>If the Commits API is unavailable (no GitHub remote, commit not
yet pushed, or API error), parse the username directly from a
GitHub noreply address
(<code>{id}+{username}@users.noreply.github.com</code>) — no API
call needed.</li>
<li>If neither of the above resolves a username, fall back to
searching GitHub by the commit author's email.</li>
searching GitHub by the commit committer's email.</li>
<li>Fetch the resolved user's public keys from
<code>github.com/{username}.gpg</code> (GPG) and
<code>/users/{username}/ssh_signing_keys</code> (SSH keys tagged
Expand All @@ -476,7 +476,7 @@ <h3>Signature verification</h3>
<li>Pass if any key verifies; fail if none do.</li>
</ol>
<p>
If the author cannot be resolved via either method, or the GitHub API
If the committer cannot be resolved via either method, or the GitHub API
is unreachable, the check fails with a clear error. For private
repositories, set <code>GITHUB_TOKEN</code> or <code>GH_TOKEN</code>
so the Commits API can authenticate. The official GitHub Action wires
Expand Down
22 changes: 11 additions & 11 deletions src/git_commit_guard/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -295,9 +295,9 @@ def check_required_trailers(message, required, result):
result.error(f"missing required trailer: {trailer}")


def _get_author_email(rev):
def _get_committer_email(rev):
return subprocess.check_output( # noqa: S603
["git", "log", "-1", "--format=%ae", rev], # noqa: S607
["git", "log", "-1", "--format=%ce", rev], # noqa: S607
text=True,
stderr=subprocess.PIPE,
timeout=_git_timeout(),
Expand All @@ -320,7 +320,7 @@ def _get_github_remote_info():
return match.group("owner"), match.group("repo")


def _fetch_github_commit_author(owner, repo, sha):
def _fetch_github_commit_committer(owner, repo, sha):
url = f"https://api.github.com/repos/{owner}/{repo}/commits/{sha}"
headers = {"Accept": "application/vnd.github+json"}
token = os.environ.get("GITHUB_TOKEN") or os.environ.get("GH_TOKEN")
Expand All @@ -329,8 +329,8 @@ def _fetch_github_commit_author(owner, repo, sha):
req = urllib.request.Request(url, headers=headers) # noqa: S310 Audit URL open for permitted schemes
with urllib.request.urlopen(req, timeout=_git_timeout()) as resp: # noqa: S310 Audit URL open for permitted schemes
data = json.loads(resp.read())
author = data.get("author")
return author["login"] if author else None
committer = data.get("committer")
return committer["login"] if committer else None


def _parse_noreply_username(email):
Expand Down Expand Up @@ -435,7 +435,7 @@ def _resolve_github_username(rev, email):
if remote:
owner, repo = remote
try:
username = _fetch_github_commit_author(owner, repo, rev)
username = _fetch_github_commit_committer(owner, repo, rev)
except urllib.error.HTTPError as e:
if e.code == HTTPStatus.NOT_FOUND:
commits_api_404 = True
Expand All @@ -450,24 +450,24 @@ def _resolve_github_username(rev, email):
return username, commits_api_404


def _author_not_found_message(commits_api_404):
def _committer_not_found_message(commits_api_404):
had_token = bool(os.environ.get("GITHUB_TOKEN") or os.environ.get("GH_TOKEN"))
if commits_api_404 and not had_token:
return (
"commit author not found on GitHub — if the repo is private, "
"committer not found on GitHub — if the repo is private, "
"set GITHUB_TOKEN in the workflow step "
"(env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }})"
)
return "commit author not found on GitHub — cannot verify signature"
return "committer not found on GitHub — cannot verify signature"


def check_signature(rev, result):
try:
email = _get_author_email(rev)
email = _get_committer_email(rev)
username, commits_api_404 = _resolve_github_username(rev, email)
if username is None:
result.error(
_author_not_found_message(commits_api_404),
_committer_not_found_message(commits_api_404),
check=Check.SIGNATURE,
)
return
Expand Down
Loading