diff --git a/.github/workflows/commit-lint.yml b/.github/workflows/commit-lint.yml index e960c12e3..18ba227f9 100644 --- a/.github/workflows/commit-lint.yml +++ b/.github/workflows/commit-lint.yml @@ -4,18 +4,24 @@ name: Commit Lint # Org-wide commit-message, PR-title, and branch-naming policy, promoted from # z-shell/zi's next-branch-only commit-lint.yml (z-shell/.github#464). Enforces # the AGENTS.md rules that were previously "author-enforced" only: the -# disallowed Co-authored-by trailer (organization-wide ban, decisions/0003), -# Conventional Commits (decisions/0003), and feature-/bug-/hotfix- -# branch naming (decisions/0008). +# disallowed bot/AI-agent Co-authored-by trailer -- a human co-author is +# allowed, only a bot/agent identity is banned (AGENTS.md) -- Conventional +# Commits (decisions/0003), and feature-/bug-/hotfix- branch +# naming (decisions/0008). on: workflow_call: inputs: disallowed-trailer-pattern: - description: Case-insensitive grep -E pattern for a banned commit trailer. + description: > + Case-insensitive grep -E pattern for a banned commit trailer. A + Co-authored-by trailer crediting a real human is allowed; only a + bot/AI-agent/automation identity is disallowed (AGENTS.md), so the + default matches known bot/agent trailer shapes rather than every + Co-authored-by line. Extend as new agent identities show up. required: false type: string - default: "^[[:space:]]*Co-authored-by:" + default: '^[[:space:]]*Co-authored-by:.*(\[bot\]|noreply@anthropic\.com|noreply@openai\.com|noreply@x\.ai|codex@openai\.com|copilot@github\.com|claude@anthropic\.com)' branch-pattern: description: > grep -E pattern a PR head branch must match. dependabot/*, @@ -54,7 +60,7 @@ jobs: while IFS= read -r sha; do if git show -s --format='%B' "$sha" | grep -qiE "$DISALLOWED_TRAILER_PATTERN"; then - echo "::error::Disallowed trailer found (${sha:0:7}): remove before merging. A squash merge without an explicit --subject/--body can also reintroduce it — see runbooks/branch-protection.md." + echo "::error::Bot/agent Co-authored-by trailer found (${sha:0:7}): remove before merging. A human co-author is fine; only bot/AI-agent identities are banned (AGENTS.md). A squash merge without an explicit --subject/--body can also reintroduce one — see runbooks/branch-protection.md." errors=$((errors + 1)) fi diff --git a/AGENTS.md b/AGENTS.md index 1d51dd780..f84c3fcdf 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -70,7 +70,7 @@ When working in z-shell repositories, optimize for: - **Plugin authoring:** read the canonical [Zsh Plugin Standard](https://wiki.zshell.dev/community/zsh_plugin_standard) for plugin creation, code changes, reviews, templates, and documentation. Official Zsh documentation remains authoritative for shell semantics; manager-specific profiles are optional. - **Canonical plugin manager:** `zi`. See `decisions/0002-zi-as-canonical-plugin-manager.md`. - **Commits and PR titles:** Conventional Commits. See `decisions/0003-conventional-commits.md`. -- **Commit trailers:** Never include a `Co-authored-by` trailer. CI-enforced on `z-shell/.github` via `commit-lint.yml`; org-wide rollout to other repositories is tracked in [z-shell/.github#464](https://github.com/z-shell/.github/issues/464), still author-enforced there until each repo adds the caller. +- **Commit trailers:** `Co-authored-by` crediting a real human — including the PR author crediting themselves — is fine. Never credit a bot, AI agent, or automation as a co-author. CI-enforced on `z-shell/.github` via `commit-lint.yml`; org-wide rollout to other repositories is tracked in [z-shell/.github#464](https://github.com/z-shell/.github/issues/464), still author-enforced there until each repo adds the caller. - **Branch selection:** Follow `decisions/0008-branching-model.md` and verify the live state of the owning repository; do not assume one universal default branch. - **Documentation placement:** keep long-form docs in the wiki when practical; keep repo-local docs focused on policy, workflow, and source-adjacent guidance. - **Workflow files:** follow the org workflow conventions and keep permissions explicit, actions pinned, and concurrency defined. diff --git a/decisions/0013-repository-settings-baseline.md b/decisions/0013-repository-settings-baseline.md index aa74de2d2..2f09d13a0 100644 --- a/decisions/0013-repository-settings-baseline.md +++ b/decisions/0013-repository-settings-baseline.md @@ -116,28 +116,32 @@ Rationale for the differences: - **Squash merge default message** is required uniformly, unlike the other rows, because it isn't a class-scaled risk — it's a plain repository API setting (`squash_merge_commit_title`/`squash_merge_commit_message`), not a - ruleset rule, and it fails the same way regardless of class. When - `squash_merge_commit_message` is `COMMIT_MESSAGES` (GitHub's default), - squashing a PR without an explicit `--body` aggregates every squashed - commit's full message — trailers included — into the merge commit, which - reliably reintroduces `Co-authored-by` even when no individual commit - carried one (`runbooks/branch-protection.md`'s "Squash-merge trailers" - section documents the same mechanism for `next` → `main` promotions). This - is not a theoretical risk: it hit the ADR-0016 acceptance PR - ([z-shell/.github#516](https://github.com/z-shell/.github/pull/516)) and, - within the same day, the PR that added org-wide commit-trailer CI - enforcement itself - ([z-shell/.github#517](https://github.com/z-shell/.github/pull/517)) — a - lint check on pre-merge commits structurally cannot catch this, since - GitHub synthesizes the trailer into a commit that doesn't exist until - merge time. Setting `squash_merge_commit_title=PR_TITLE` and - `squash_merge_commit_message=BLANK` (`gh api -X PATCH repos// ---field squash_merge_commit_title=PR_TITLE --field -squash_merge_commit_message=BLANK`) removes the default body entirely, so - there is nothing to synthesize a trailer from regardless of whether a - human remembers `--body`. Applied to `z-shell/.github` itself - 2026-08-16; auditing and applying it to the rest of the org is open - follow-up, not covered by this change. + ruleset rule, and it fails the same way regardless of class. `AGENTS.md` + bans a `Co-authored-by` trailer crediting a bot, AI agent, or automation; + a trailer crediting a real human (including the PR's own author) is + allowed. When `squash_merge_commit_message` is `COMMIT_MESSAGES` (GitHub's + default), squashing a PR without an explicit `--body` aggregates every + squashed commit's full message into the merge commit, which can carry a + bot/agent trailer forward from an individual commit + (`runbooks/branch-protection.md`'s "Squash-merge trailers" section + documents the same mechanism for `next` → `main` promotions). Setting + `squash_merge_commit_title=PR_TITLE` and `squash_merge_commit_message=BLANK` + (`gh api -X PATCH repos// --field +squash_merge_commit_title=PR_TITLE --field squash_merge_commit_message=BLANK`) + removes that aggregated body, reducing the risk. **It does not eliminate + it**: confirmed empirically on + [z-shell/.github#519](https://github.com/z-shell/.github/pull/519) itself, + merged with `BLANK` already active from a source commit with no trailer at + all, GitHub still appended a `Co-authored-by` trailer for the merger alone + in an otherwise-empty body — a separate, apparently independent mechanism. + That specific case is harmless (the merger crediting themselves is + allowed); the setting is still worth having because it closes the larger + aggregated-history exposure, not because it's a complete fix. `--subject`/ + `--body` passed explicitly remains the only confirmed way to fully control + the resulting message. Applied to `z-shell/.github` itself 2026-08-16; + auditing and applying it to the rest of the org is open follow-up + ([z-shell/.github#518](https://github.com/z-shell/.github/issues/518)), + not covered by this change. ### Expressed as rulesets, not classic protection diff --git a/runbooks/branch-protection.md b/runbooks/branch-protection.md index ab27df690..860879087 100644 --- a/runbooks/branch-protection.md +++ b/runbooks/branch-protection.md @@ -107,15 +107,28 @@ decision for any dependent pull request that must remain retargeted. ## Squash-merge trailers -When squash-merging a `next` → `main` promotion PR without an explicit -`--body`, GitHub synthesizes one by aggregating the squashed commits' -trailers — which reliably reintroduces `Co-authored-by` and `Signed-off-by` -trailers even when no individual commit you authored had one. Only -`Co-authored-by` is organization-disallowed (`AGENTS.md`); letting a -synthesized body reintroduce it violates that policy regardless of which -squashed commit it came from. Always pass both `--subject` and an explicit -one-line `--body` (e.g. `gh pr merge --squash --subject "..." --body "..."`) to suppress -the synthesized body. Verify with +`AGENTS.md` does not ban `Co-authored-by` trailers outright. A trailer +crediting a real human — including the PR author crediting themselves via a +GitHub-synthesized trailer — is fine. The ban is specifically on crediting a +**bot, AI agent, or automation** as a co-author (for example a coding +assistant's default identity, or a `[bot]` GitHub account); those must never +appear regardless of merge method. + +When squash-merging a PR without an explicit `--body`, GitHub synthesizes +one by aggregating the squashed commits' trailers, which can carry forward +a bot/agent `Co-authored-by` from an individual commit even when the PR +subject and description don't mention one. Separately — confirmed +empirically on `z-shell/.github#519` — GitHub appends a `Co-authored-by` +trailer for the merger even with the repository's +`squash_merge_commit_message` set to `BLANK` and an otherwise-empty default +body (`decisions/0013-repository-settings-baseline.md`); that setting +reduces the aggregation risk but does not eliminate this second, apparently +independent mechanism. Neither is a problem when the credited party is the +human author/merger. It is a problem if any commit in the PR carries a +bot/agent trailer, since squash synthesis can promote it into the merge +commit. Always pass both `--subject` and an explicit one-line `--body` +(e.g. `gh pr merge --squash --subject "..." --body "..."`) when that +risk exists, and verify with `gh api repos///commits/ --jq .commit.message` before considering the promotion done. diff --git a/runbooks/onboarding.md b/runbooks/onboarding.md index e38a7936d..910748520 100644 --- a/runbooks/onboarding.md +++ b/runbooks/onboarding.md @@ -46,13 +46,14 @@ Grant only what the role requires; record the grant: - Clone the owning repository directly. Separate multi-repository tooling is optional and outside this public runbook. - Configure commit signing: commits are signed (`gpg.format=ssh`); set a - `user.signingkey`. Never add a `Co-authored-by` trailer — this is org policy. - `z-shell/.github` enforces it in CI (`commit-lint.yml`, PRs into `main`); - most other repositories do not have the caller wired in yet + `user.signingkey`. A `Co-authored-by` trailer crediting a real human is + fine — never credit a bot, AI agent, or automation as a co-author; that is + org policy. `z-shell/.github` enforces it in CI (`commit-lint.yml`, PRs + into `main`); most other repositories do not have the caller wired in yet ([z-shell/.github#464](https://github.com/z-shell/.github/issues/464)), so it remains the author's responsibility there — including watching for a - squash merge silently reintroducing the trailer even when no individual - commit had one (`runbooks/branch-protection.md`). + squash merge silently promoting a bot/agent trailer from an individual + commit into the merge commit (`runbooks/branch-protection.md`). - Follow Conventional Commits and the branch model for the repo's class (ADR-0008).