From 94d9bb23f60c5a7ab30311d54880dd377ef862fa Mon Sep 17 00:00:00 2001
From: Jaehoon You <158752+teslamint@users.noreply.github.com>
Date: Tue, 1 Sep 2026 11:55:29 +0900
Subject: [PATCH] fix(ci): reject fixture identities before merging
Protect the default branch with PR-only merges, signed commits, and a CI range check for fixture author or committer metadata.
Constraint: fixture repositories intentionally use fixture identities
Rejected: signed commits alone | valid signing keys can sign incorrect metadata
Confidence: high
Scope-risk: narrow
Directive: enable the required status rule only after this workflow runs on main
Tested: bash scripts/validate.sh
Not-tested: GitHub Actions execution on a pull request
Assisted-by: GPT-5.6-terra with Oh My Pi
---
.github/workflows/commit-identity.yml | 24 +++++
scripts/check-commit-identity.sh | 53 +++++++++++
scripts/git-identity-invariant.sh | 33 +++++++
scripts/test-commit-identity-policy.sh | 105 +++++++++++++++++++++
scripts/test-git-identity-isolation.sh | 109 ++++++++++++++++++++++
scripts/test-plan-consumer-portability.sh | 5 +
scripts/validate.sh | 11 +++
7 files changed, 340 insertions(+)
create mode 100644 .github/workflows/commit-identity.yml
create mode 100755 scripts/check-commit-identity.sh
create mode 100644 scripts/git-identity-invariant.sh
create mode 100755 scripts/test-commit-identity-policy.sh
create mode 100755 scripts/test-git-identity-isolation.sh
diff --git a/.github/workflows/commit-identity.yml b/.github/workflows/commit-identity.yml
new file mode 100644
index 0000000..04a3eb0
--- /dev/null
+++ b/.github/workflows/commit-identity.yml
@@ -0,0 +1,24 @@
+name: Commit identity
+
+on:
+ pull_request:
+ branches: [main]
+ push:
+ branches: [main]
+
+permissions:
+ contents: read
+
+jobs:
+ reject-fixture-identity:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+ persist-credentials: false
+ - name: Reject fixture commit identities
+ env:
+ BASE_SHA: ${{ github.event.pull_request.base.sha || github.event.before }}
+ HEAD_SHA: ${{ github.event.pull_request.head.sha || github.sha }}
+ run: bash scripts/check-commit-identity.sh "$BASE_SHA" "$HEAD_SHA"
diff --git a/scripts/check-commit-identity.sh b/scripts/check-commit-identity.sh
new file mode 100755
index 0000000..8625f32
--- /dev/null
+++ b/scripts/check-commit-identity.sh
@@ -0,0 +1,53 @@
+#!/usr/bin/env bash
+# Reject fixture identities from a verified commit range.
+set -uo pipefail
+
+BASE="${1:?usage: check-commit-identity.sh
}"
+HEAD="${2:?usage: check-commit-identity.sh }"
+
+if ! git rev-parse --verify --quiet "$BASE^{commit}" >/dev/null || \
+ ! git rev-parse --verify --quiet "$HEAD^{commit}" >/dev/null; then
+ echo "FAIL: commit identity policy requires two commits" >&2
+ exit 2
+fi
+
+RANGE_BASE="$(git merge-base "$BASE" "$HEAD")" || {
+ echo "FAIL: commit identity policy cannot derive a merge base" >&2
+ exit 2
+}
+
+is_fixture_name() {
+ case "$1" in
+ [Ff][Ii][Xx][Tt][Uu][Rr][Ee]) return 0 ;;
+ *) return 1 ;;
+ esac
+}
+
+is_fixture_email() {
+ case "$1" in
+ [Ff][Ii][Xx][Tt][Uu][Rr][Ee]@[Ee][Xx][Aa][Mm][Pp][Ll][Ee].[Ii][Nn][Vv][Aa][Ll][Ii][Dd]) return 0 ;;
+ *) return 1 ;;
+ esac
+}
+
+FAILED=0
+while IFS= read -r -d '' sha &&
+ IFS= read -r -d '' author_name &&
+ IFS= read -r -d '' author_email &&
+ IFS= read -r -d '' committer_name &&
+ IFS= read -r -d '' committer_email; do
+ if is_fixture_name "$author_name" || is_fixture_email "$author_email"; then
+ echo "FAIL: fixture identity in author for commit $sha" >&2
+ FAILED=1
+ fi
+ if is_fixture_name "$committer_name" || is_fixture_email "$committer_email"; then
+ echo "FAIL: fixture identity in committer for commit $sha" >&2
+ FAILED=1
+ fi
+done < <(git log --format='%H%x00%an%x00%ae%x00%cn%x00%ce%x00' "$RANGE_BASE..$HEAD")
+
+if [[ $FAILED -ne 0 ]]; then
+ exit 1
+fi
+
+echo "ok: commit identities accepted"
diff --git a/scripts/git-identity-invariant.sh b/scripts/git-identity-invariant.sh
new file mode 100644
index 0000000..3690b70
--- /dev/null
+++ b/scripts/git-identity-invariant.sh
@@ -0,0 +1,33 @@
+# Shared Git identity boundary for validation scripts.
+# A non-Git copied fixture returns an empty baseline and is intentionally skipped.
+
+git_identity_baseline() {
+ local repo="$1" repo_root worktree_root config status
+
+ repo_root="$(cd "$repo" && pwd -P)" || return 0
+ [[ "$(git -C "$repo_root" rev-parse --is-inside-work-tree 2>/dev/null)" == "true" ]] || return 0
+ worktree_root="$(git -C "$repo_root" rev-parse --show-toplevel 2>/dev/null)" || return 0
+ worktree_root="$(cd "$worktree_root" && pwd -P)" || return 0
+ [[ "$worktree_root" == "$repo_root" ]] || return 0
+
+ config="$(git -C "$repo_root" config --local --get-regexp '^user\.(name|email)$' 2>/dev/null)"; status=$?
+ [[ $status -eq 0 ]] && printf '%s\n' "$config"
+ [[ $status -eq 0 || $status -eq 1 ]] && return 0
+ return "$status"
+}
+
+git_identity_unchanged() {
+ local repo="$1" baseline="$2" observed
+
+ if ! observed="$(git_identity_baseline "$repo")"; then
+ printf '%s\n' '[git-identity] could not read local user.name/user.email' >&2
+ return 1
+ fi
+
+ if [[ "$observed" == "$baseline" ]]; then
+ return 0
+ fi
+
+ printf '%s\n' '[git-identity] local user.name/user.email changed during validation' >&2
+ return 1
+}
diff --git a/scripts/test-commit-identity-policy.sh b/scripts/test-commit-identity-policy.sh
new file mode 100755
index 0000000..c05980e
--- /dev/null
+++ b/scripts/test-commit-identity-policy.sh
@@ -0,0 +1,105 @@
+#!/usr/bin/env bash
+# Fixture tests for the protected-branch commit identity policy.
+set -uo pipefail
+
+ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
+POLICY="$ROOT/scripts/check-commit-identity.sh"
+FAIL_COUNT=0
+
+setup_repo() {
+ local dir
+ dir="$(mktemp -d)" || return 1
+ git -C "$dir" init -q || { rm -rf "$dir"; return 1; }
+ git -C "$dir" config user.name "Canonical Fixture" || { rm -rf "$dir"; return 1; }
+ git -C "$dir" config user.email "canonical@example.invalid" || { rm -rf "$dir"; return 1; }
+ git -C "$dir" config commit.gpgsign false || { rm -rf "$dir"; return 1; }
+ printf 'base\n' >"$dir/file"
+ git -C "$dir" add file && git -C "$dir" commit -qm base || { rm -rf "$dir"; return 1; }
+ printf '%s\n' "$dir"
+}
+
+assert_contains() {
+ local haystack="$1" needle="$2" label="$3"
+ if [[ "$haystack" == *"$needle"* ]]; then
+ return 0
+ fi
+ echo " assertion failed ($label): expected output to contain: $needle"
+ return 1
+}
+
+run_case() {
+ local name="$1"
+ shift
+ echo "Case $name:"
+ if "$@"; then
+ echo " pass"
+ else
+ echo " FAIL"
+ FAIL_COUNT=$((FAIL_COUNT + 1))
+ fi
+}
+
+case_normal_identity_accepted() {
+ local dir base out code
+ dir="$(setup_repo)" || return 1
+ base="$(git -C "$dir" rev-parse HEAD)"
+ printf 'normal\n' >>"$dir/file"
+ git -C "$dir" add file && git -C "$dir" commit -qm normal || { rm -rf "$dir"; return 1; }
+ out="$(cd "$dir" && "$POLICY" "$base" HEAD 2>&1)"; code=$?
+ rm -rf "$dir"
+ [[ $code -eq 0 ]] && assert_contains "$out" "commit identities accepted" "normal identity"
+}
+
+case_fixture_author_rejected() {
+ local dir base out code
+ dir="$(setup_repo)" || return 1
+ base="$(git -C "$dir" rev-parse HEAD)"
+ git -C "$dir" config user.name fixture
+ git -C "$dir" config user.email fixture@example.invalid
+ printf 'fixture author\n' >>"$dir/file"
+ git -C "$dir" add file && git -C "$dir" commit -qm fixture || { rm -rf "$dir"; return 1; }
+ out="$(cd "$dir" && "$POLICY" "$base" HEAD 2>&1)"; code=$?
+ rm -rf "$dir"
+ [[ $code -ne 0 ]] && assert_contains "$out" "fixture identity" "fixture author diagnostic"
+}
+
+case_fixture_committer_rejected() {
+ local dir base out code
+ dir="$(setup_repo)" || return 1
+ base="$(git -C "$dir" rev-parse HEAD)"
+ printf 'fixture committer\n' >>"$dir/file"
+ git -C "$dir" add file || { rm -rf "$dir"; return 1; }
+ GIT_COMMITTER_NAME=fixture GIT_COMMITTER_EMAIL=fixture@example.invalid git -C "$dir" commit -qm committer || { rm -rf "$dir"; return 1; }
+ out="$(cd "$dir" && "$POLICY" "$base" HEAD 2>&1)"; code=$?
+ rm -rf "$dir"
+ [[ $code -ne 0 ]] && assert_contains "$out" "fixture identity" "fixture committer diagnostic"
+}
+
+case_diverged_branch_accepted() {
+ local dir base branch out code
+ dir="$(setup_repo)" || return 1
+ branch="$(git -C "$dir" symbolic-ref --short HEAD)"
+ git -C "$dir" checkout -qb feature || { rm -rf "$dir"; return 1; }
+ printf 'feature\n' >>"$dir/file"
+ git -C "$dir" add file && git -C "$dir" commit -qm feature || { rm -rf "$dir"; return 1; }
+ git -C "$dir" checkout -q "$branch" || { rm -rf "$dir"; return 1; }
+ printf 'main\n' >>"$dir/file"
+ git -C "$dir" add file && git -C "$dir" commit -qm main || { rm -rf "$dir"; return 1; }
+ base="$(git -C "$dir" rev-parse HEAD)"
+ out="$(cd "$dir" && "$POLICY" "$base" feature 2>&1)"; code=$?
+ rm -rf "$dir"
+ [[ $code -eq 0 ]] && assert_contains "$out" "commit identities accepted" "diverged branch"
+}
+
+run_case normal-identity-accepted case_normal_identity_accepted
+run_case fixture-author-rejected case_fixture_author_rejected
+run_case fixture-committer-rejected case_fixture_committer_rejected
+run_case diverged-branch-accepted case_diverged_branch_accepted
+
+if [[ $FAIL_COUNT -eq 0 ]]; then
+ echo "ALL COMMIT IDENTITY POLICY CHECKS PASSED"
+ exit 0
+fi
+
+echo "$FAIL_COUNT commit identity policy check(s) failed"
+exit 1
diff --git a/scripts/test-git-identity-isolation.sh b/scripts/test-git-identity-isolation.sh
new file mode 100755
index 0000000..272829c
--- /dev/null
+++ b/scripts/test-git-identity-isolation.sh
@@ -0,0 +1,109 @@
+#!/usr/bin/env bash
+# Unit tests for the Git identity boundary used by validation scripts.
+set -uo pipefail
+
+ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
+FAIL_COUNT=0
+
+setup_repo() {
+ local dir
+ dir="$(mktemp -d 2>&1)" || { echo " harness error: mktemp -d failed: $dir" >&2; return 1; }
+ git -C "$dir" init -q || { rm -rf "$dir"; return 1; }
+ git -C "$dir" config user.name "Canonical Fixture" || { rm -rf "$dir"; return 1; }
+ git -C "$dir" config user.email "canonical@example.invalid" || { rm -rf "$dir"; return 1; }
+ printf '%s\n' "$dir"
+}
+
+assert_contains() {
+ local haystack="$1" needle="$2" label="$3"
+ if [[ "$haystack" == *"$needle"* ]]; then
+ return 0
+ fi
+ echo " assertion failed ($label): expected output to contain: $needle"
+ return 1
+}
+
+assert_equal() {
+ local actual="$1" expected="$2" label="$3"
+ if [[ "$actual" == "$expected" ]]; then
+ return 0
+ fi
+ echo " assertion failed ($label): expected '$expected', got '$actual'"
+ return 1
+}
+
+run_case() {
+ local name="$1"
+ shift
+ echo "Case $name:"
+ if "$@"; then
+ echo " pass"
+ else
+ echo " FAIL"
+ FAIL_COUNT=$((FAIL_COUNT + 1))
+ fi
+}
+
+case_identity_preserved() {
+ local dir before after
+ dir="$(setup_repo)" || return 1
+ before="$(git_identity_baseline "$dir")"
+ after="$(git_identity_baseline "$dir")"
+ rm -rf "$dir"
+ [[ "$after" == "$before" ]]
+}
+
+case_identity_mutation_detected() {
+ local dir before out code
+ dir="$(setup_repo)" || return 1
+ before="$(git_identity_baseline "$dir")"
+ git -C "$dir" config user.name fixture
+ out="$(git_identity_unchanged "$dir" "$before" 2>&1)"; code=$?
+ rm -rf "$dir"
+ [[ $code -ne 0 ]] && assert_contains "$out" "[git-identity] local user.name/user.email changed during validation" "mutation diagnostic"
+}
+
+case_email_mutation_detected() {
+ local dir before out code
+ dir="$(setup_repo)" || return 1
+ before="$(git_identity_baseline "$dir")"
+ git -C "$dir" config user.email fixture@example.invalid
+ out="$(git_identity_unchanged "$dir" "$before" 2>&1)"; code=$?
+ rm -rf "$dir"
+ [[ $code -ne 0 ]] && assert_contains "$out" "[git-identity] local user.name/user.email changed during validation" "email mutation diagnostic"
+}
+
+case_bare_repository_skipped() {
+ local dir baseline
+ dir="$(mktemp -d)" || return 1
+ git -C "$dir" init --bare -q || { rm -rf "$dir"; return 1; }
+ baseline="$(git_identity_baseline "$dir")"
+ rm -rf "$dir"
+ assert_equal "$baseline" "" "bare repository skipped"
+}
+
+case_metadata_less_child_skipped() {
+ local dir baseline
+ dir="$(setup_repo)" || return 1
+ mkdir "$dir/fixture"
+ baseline="$(git_identity_baseline "$dir/fixture")"
+ rm -rf "$dir"
+ assert_equal "$baseline" "" "metadata-less child skipped"
+}
+
+# shellcheck source=scripts/git-identity-invariant.sh
+source "$ROOT/scripts/git-identity-invariant.sh"
+
+run_case identity-preserved case_identity_preserved
+run_case identity-mutation-detected case_identity_mutation_detected
+run_case bare-repository-skipped case_bare_repository_skipped
+run_case metadata-less-child-skipped case_metadata_less_child_skipped
+run_case email-mutation-detected case_email_mutation_detected
+
+if [[ $FAIL_COUNT -eq 0 ]]; then
+ echo "ALL GIT IDENTITY ISOLATION CHECKS PASSED"
+ exit 0
+fi
+
+echo "$FAIL_COUNT Git identity isolation check(s) failed"
+exit 1
diff --git a/scripts/test-plan-consumer-portability.sh b/scripts/test-plan-consumer-portability.sh
index 0f2b1d5..8493992 100755
--- a/scripts/test-plan-consumer-portability.sh
+++ b/scripts/test-plan-consumer-portability.sh
@@ -6,6 +6,8 @@
set -uo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
+source "$ROOT/scripts/git-identity-invariant.sh"
+IDENTITY_BEFORE="$(git_identity_baseline "$ROOT")"
PASS_COUNT=0
FAIL_COUNT=0
@@ -2764,6 +2766,9 @@ else
fail "phase consumers derive one portable artifact root from the exact progress path"
fi
rm -rf "$fixture" "$consumer_repo"
+if ! git_identity_unchanged "$ROOT" "$IDENTITY_BEFORE"; then
+ fail "local user.name/user.email unchanged"
+fi
echo "Summary: $PASS_COUNT passed, $FAIL_COUNT failed"
if [ "$FAIL_COUNT" -ne 0 ]; then
diff --git a/scripts/validate.sh b/scripts/validate.sh
index 52680d4..d453143 100755
--- a/scripts/validate.sh
+++ b/scripts/validate.sh
@@ -4,6 +4,8 @@
set -uo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
+source "$ROOT/scripts/git-identity-invariant.sh"
+IDENTITY_BEFORE="$(git_identity_baseline "$ROOT")"
FAIL=0
fail() { echo "FAIL: $1"; FAIL=1; }
@@ -1266,6 +1268,15 @@ fi
if ! bash "$ROOT/scripts/test-release-loop-conformance.sh" static; then
fail "release-loop static conformance failed"
fi
+if ! bash "$ROOT/scripts/test-git-identity-isolation.sh"; then
+ FAIL=1
+fi
+if ! bash "$ROOT/scripts/test-commit-identity-policy.sh"; then
+ FAIL=1
+fi
+if ! git_identity_unchanged "$ROOT" "$IDENTITY_BEFORE"; then
+ FAIL=1
+fi
echo
if [ "$FAIL" -eq 0 ]; then