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
18 changes: 12 additions & 6 deletions .github/workflows/commit-lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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-<id>/bug-<id>/hotfix-<id>
# 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-<id>/bug-<id>/hotfix-<id> 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/*,
Expand Down Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
48 changes: 26 additions & 22 deletions decisions/0013-repository-settings-baseline.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/<org>/<repo>
--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/<org>/<repo> --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

Expand Down
31 changes: 22 additions & 9 deletions runbooks/branch-protection.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <n> --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 <n> --squash --subject "..." --body "..."`) when that
risk exists, and verify with
`gh api repos/<org>/<repo>/commits/<sha> --jq .commit.message` before
considering the promotion done.

Expand Down
11 changes: 6 additions & 5 deletions runbooks/onboarding.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).

Expand Down
Loading