From ce55151ca5cae940bbec6e2e506ca1e0456bf082 Mon Sep 17 00:00:00 2001 From: Diego Rodrigues de Sa e Souza Date: Mon, 7 Sep 2026 08:35:29 -0300 Subject: [PATCH] chore(ci): guard commit identity in pre-commit to stop author misattribution (#12772) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore(ci): guard commit identity in pre-commit to stop author misattribution Two windows of commits in this checkout were signed with the wrong identity, both caused by an identity override left behind by an automated session: 2026-08-13..26 (name "Xiangzhe" + @backryun's e-mail, 237 commits) and 2026-08-29..09-02 (name "Markus Hartung" + the maintainer's e-mail, 59 commits). The .mailmap repairs the record after the fact; this gate stops the next window. The gate is opt-in per machine via omniroute.expectedName / expectedEmail — with no config it exits 0, so contributors who clone the repo are never affected. It blocks three things: a committer that is not this machine's identity (which is what BOTH windows looked like — in August neither the name nor the e-mail was the maintainer's, so checking only their e-mail would have missed it), an author carrying the maintainer's e-mail under someone else's name, and any address listed in omniroute.legacyEmail. Crediting a contributor with `git commit --author="Name "` keeps working, since the rule targets the committer and the maintainer's own address. * test(ci): isolate the identity gate's test from the ambient git config The "stays inert when the machine has not opted in" case read the real global config, so on a machine that HAS opted in (omniroute.expectedEmail set — the maintainer's own boxes, where this gate matters most) the gate correctly refused a synthetic contributor identity and the test failed. It only passed on a clean CI runner. Neutralising GIT_CONFIG_GLOBAL/GIT_CONFIG_SYSTEM makes the opt-in state come solely from what the test injects, so the suite is deterministic on both an opted-in and a clean machine. --- .husky/pre-commit | 1 + scripts/check/check-git-identity.sh | 75 +++++++++++++ tests/unit/check-git-identity.test.ts | 145 ++++++++++++++++++++++++++ 3 files changed, 221 insertions(+) create mode 100755 scripts/check/check-git-identity.sh create mode 100644 tests/unit/check-git-identity.test.ts diff --git a/.husky/pre-commit b/.husky/pre-commit index ec14ffcd28..268c9b5f7d 100755 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -7,6 +7,7 @@ fi # Cheap, deterministic local gates (re-enabled). Slower checks (i18n drift, # openapi coverage/security-tiers, env-doc sync) run in CI to keep commits fast. +sh scripts/check/check-git-identity.sh npx lint-staged node scripts/check/check-docs-sync.mjs npm run check:any-budget:t11 diff --git a/scripts/check/check-git-identity.sh b/scripts/check/check-git-identity.sh new file mode 100755 index 0000000000..e0328ffb33 --- /dev/null +++ b/scripts/check/check-git-identity.sh @@ -0,0 +1,75 @@ +#!/usr/bin/env sh +# Guard de identidade de commit — previne misattribution de autoria. +# +# Contexto (ver .mailmap na raiz): este checkout já produziu DUAS janelas de +# commits com autoria trocada, ambas por um override de identidade deixado para +# trás por uma sessão automatizada: +# 1. 2026-08-13..26 — nome "Xiangzhe" + e-mail de @backryun (237 commits) +# 2. 2026-08-29..09-02 — nome "Markus Hartung" + e-mail do mantenedor (59 commits) +# +# Este gate NÃO impõe uma identidade única: contribuidores commitam normalmente +# com a sua, e creditar um contribuidor via `--author` continua funcionando. +# Ele bloqueia apenas as duas assinaturas do defeito: +# (a) um COMMITTER que não é a identidade desta máquina (pega ambas as janelas); +# (b) um AUTHOR com o e-mail do mantenedor sob o nome de outra pessoa; +# (c) um e-mail explicitamente aposentado (`omniroute.legacyEmail`). +# +# Ativação — opcional e por máquina; sem ela o gate é inerte: +# git config --global omniroute.expectedName "diegosouzapw" +# git config --global omniroute.expectedEmail "8016841+diegosouzapw@users.noreply.github.com" +# git config --global --add omniroute.legacyEmail "diegosouzapw@users.noreply.github.com" + +expected_name=$(git config --get omniroute.expectedName 2>/dev/null) +expected_email=$(git config --get omniroute.expectedEmail 2>/dev/null) +legacy_emails=$(git config --get-all omniroute.legacyEmail 2>/dev/null) + +# Sem configuração nesta máquina o gate não opina — contribuidores não são afetados. +[ -z "$expected_email" ] && exit 0 + +an=$(git var GIT_AUTHOR_IDENT 2>/dev/null | sed 's/ <.*//') +ae=$(git var GIT_AUTHOR_IDENT 2>/dev/null | sed 's/.*.*//') +cn=$(git var GIT_COMMITTER_IDENT 2>/dev/null | sed 's/ <.*//') +ce=$(git var GIT_COMMITTER_IDENT 2>/dev/null | sed 's/.*.*//') + +fail=0 + +# (a) O COMMITTER é quem executa o commit — nesta máquina, sempre o dono dela. +# Um override de identidade esquecido por uma sessão aparece exatamente aqui, +# e foi o que passou despercebido nas duas janelas: em agosto NEM o nome NEM +# o e-mail eram do mantenedor, então checar só o e-mail dele não bastaria. +if [ "$ce" != "$expected_email" ] || { [ -n "$expected_name" ] && [ "$cn" != "$expected_name" ]; }; then + echo "🛑 COMMITTER não é a identidade desta máquina: $cn <$ce>" >&2 + fail=1 +fi + +# (b) O AUTHOR pode ser um contribuidor (crédito via --author), mas nunca pode +# carregar o e-mail do mantenedor sob o nome de outra pessoa. +if [ -n "$expected_name" ] && [ "$ae" = "$expected_email" ] && [ "$an" != "$expected_name" ]; then + echo "🛑 AUTHOR combina o e-mail do mantenedor com outro nome: $an <$ae>" >&2 + fail=1 +fi + +# (c) e-mails aposentados que já causaram misattribution. +for legacy in $legacy_emails; do + if [ "$ae" = "$legacy" ]; then + echo "🛑 AUTHOR usa e-mail aposentado: $an <$ae>" >&2 + fail=1 + fi + if [ "$ce" = "$legacy" ]; then + echo "🛑 COMMITTER usa e-mail aposentado: $cn <$ce>" >&2 + fail=1 + fi +done + +[ "$fail" = "0" ] && exit 0 + +cat >&2 < + Corrija com: + git config --global user.name "$expected_name" + git config --global user.email "$expected_email" + Para creditar um contribuidor, use o E-MAIL DELE (nunca o seu): + git commit --author="Nome " +MSG +exit 1 diff --git a/tests/unit/check-git-identity.test.ts b/tests/unit/check-git-identity.test.ts new file mode 100644 index 0000000000..fb31a2b247 --- /dev/null +++ b/tests/unit/check-git-identity.test.ts @@ -0,0 +1,145 @@ +// Guards the commit-identity gate (scripts/check/check-git-identity.sh): a stale +// identity override left behind by an automated session must not be able to sign +// commits with the maintainer's e-mail under someone else's name. +// +// Two real incidents motivate this (see .mailmap at the repo root): +// 1. 2026-08-13..26 — name "Xiangzhe" + @backryun's e-mail (237 commits) +// 2. 2026-08-29..09-02 — name "Markus Hartung" + the maintainer's e-mail (59 commits) +import { test } from "node:test"; +import assert from "node:assert/strict"; +import { spawnSync } from "node:child_process"; +import { fileURLToPath } from "node:url"; + +const SCRIPT_PATH = fileURLToPath( + new URL("../../scripts/check/check-git-identity.sh", import.meta.url) +); + +const OWNER_NAME = "diegosouzapw"; +const OWNER_EMAIL = "8016841+diegosouzapw@users.noreply.github.com"; +const LEGACY_EMAIL = "diegosouzapw@users.noreply.github.com"; + +/** Runs the gate with a synthetic git identity. `configured` toggles the opt-in. */ +function runGate( + identity: { + authorName: string; + authorEmail: string; + committerName: string; + committerEmail: string; + }, + configured = true +) { + const env: Record = { + ...process.env, + // The gate reads its opt-in from git config, so the ambient global/system + // config has to be neutralised: on a machine that HAS opted in (the + // maintainer's own boxes) the "not opted in" case is otherwise impossible + // to simulate and the test fails there while passing on a clean CI runner. + GIT_CONFIG_GLOBAL: "/dev/null", + GIT_CONFIG_SYSTEM: "/dev/null", + GIT_AUTHOR_NAME: identity.authorName, + GIT_AUTHOR_EMAIL: identity.authorEmail, + GIT_COMMITTER_NAME: identity.committerName, + GIT_COMMITTER_EMAIL: identity.committerEmail, + }; + if (configured) { + // GIT_CONFIG_* is inherited by every child `git` call the script makes, + // unlike `git -c`, which would only apply to a single invocation. + Object.assign(env, { + GIT_CONFIG_COUNT: "3", + GIT_CONFIG_KEY_0: "omniroute.expectedName", + GIT_CONFIG_VALUE_0: OWNER_NAME, + GIT_CONFIG_KEY_1: "omniroute.expectedEmail", + GIT_CONFIG_VALUE_1: OWNER_EMAIL, + GIT_CONFIG_KEY_2: "omniroute.legacyEmail", + GIT_CONFIG_VALUE_2: LEGACY_EMAIL, + }); + } + const r = spawnSync("sh", [SCRIPT_PATH], { env, encoding: "utf8" }); + return { status: r.status, stderr: r.stderr ?? "" }; +} + +const owner = { + authorName: OWNER_NAME, + authorEmail: OWNER_EMAIL, + committerName: OWNER_NAME, + committerEmail: OWNER_EMAIL, +}; + +test("stays inert when the machine has not opted in", () => { + // A contributor who cloned the repo must never be blocked by the maintainer's gate. + const r = runGate( + { + authorName: "Some Contributor", + authorEmail: "someone@example.com", + committerName: "Some Contributor", + committerEmail: "someone@example.com", + }, + false + ); + assert.equal(r.status, 0); +}); + +test("accepts the maintainer's own identity", () => { + assert.equal(runGate(owner).status, 0); +}); + +test("rejects the maintainer's e-mail carrying another person's name", () => { + const r = runGate({ + authorName: "Markus Hartung", + authorEmail: OWNER_EMAIL, + committerName: "Markus Hartung", + committerEmail: OWNER_EMAIL, + }); + assert.equal(r.status, 1); + assert.match(r.stderr, /AUTHOR combina o e-mail do mantenedor/); + assert.match(r.stderr, /COMMITTER não é a identidade desta máquina/); +}); + +test("rejects the retired legacy e-mail — the 2026-08-29 window's exact signature", () => { + const r = runGate({ + authorName: "Markus Hartung", + authorEmail: LEGACY_EMAIL, + committerName: "Markus Hartung", + committerEmail: LEGACY_EMAIL, + }); + assert.equal(r.status, 1); + assert.match(r.stderr, /e-mail aposentado/); +}); + +test("allows crediting a contributor through their OWN e-mail", () => { + // `git commit --author="Name "` is the sanctioned credit path and + // must keep working — the gate targets the maintainer's e-mail, not the name. + const r = runGate({ + authorName: "Markus Hartung", + authorEmail: "mail@hartmark.se", + committerName: OWNER_NAME, + committerEmail: OWNER_EMAIL, + }); + assert.equal(r.status, 0); +}); + +test("rejects a committer that is not this machine's identity", () => { + // The committer is whoever RAN the commit, so on the maintainer's machine it is + // always them. A forgotten identity override surfaces here first. + const r = runGate({ + authorName: OWNER_NAME, + authorEmail: OWNER_EMAIL, + committerName: "Bob.Hou", + committerEmail: "houminxi@gmail.com", + }); + assert.equal(r.status, 1); + assert.match(r.stderr, /COMMITTER não é a identidade desta máquina/); +}); + +test("rejects the 2026-08-13 window: neither name nor e-mail is the maintainer's", () => { + // Name "Xiangzhe" (@xz-dev) + @backryun's e-mail. Checking only the maintainer's + // e-mail would MISS this window entirely — hence the committer-identity rule. + const r = runGate({ + authorName: "Xiangzhe", + authorEmail: "bakryun0718@proton.me", + committerName: "Xiangzhe", + committerEmail: "bakryun0718@proton.me", + }); + assert.equal(r.status, 1); + assert.match(r.stderr, /COMMITTER não é a identidade desta máquina/); +});