tools/mail-patch/

Capability: contract:change-request

Kind: implementation

Vendor: email

Organization: ASF

[PATCH]-mail change-request adapter — the backend that lets a project driving code review over its developer mailing list use the pr-management-* skills. It implements the tools/change-request/ contract for the oldest review mechanism in open source: a patch posted to dev@ as a [PATCH] thread, discussed in replies, and committed to trunk by a committer.

There is no forge behind this backend at all. The “pull request” is a mail thread; the “diff” is the [PATCH] body or attachment; the “review” is the reply chain. This adapter composes two contracts the framework already ships:

  • Reads flow through contract:mail-archive (tools/ponymail/ at the ASF) — thread search, thread fetch, participant extraction.
  • Review replies are drafted through contract:mail-create — the adapter never sends mail autonomously; post_review and reject produce a drafted reply the maintainer sends by hand.

Its land verb, like jira-patch, delegates the actual commit to the project’s contract:source-control adapter (svn patch + svn commit) and then drafts an “applied in rNNNNN” reply on the thread.

Prerequisites

  • Runtime: Bash — doc-only adapter; composes the mail-archive, mail-create, and source-control adapters, no local package.
  • CLIs: the mail-archive backend (PonyMail MCP at the ASF), the mail-create backend (Gmail draft API), and svn for the delegated land.
  • Credentials / auth: anonymous read for public dev@ archives (PonyMail); an authenticated mail-create session to compose replies (drafts only — never sends); and ASF committer credentials for the SVN commit that land delegates to tools/asf-svn/. As with jira-patch, the commit runs under the landing committer’s identity and credits the patch author in the commit message — see Attribution.
  • Network: the mail archive host (lists.apache.org) and svn.apache.org for the delegated land.

What this maps

The mail-patch backend resolves the change-request as: a [PATCH] thread on the developer list is one change proposal. The thread’s opaque archive id is the proposal id; the [PATCH] body/attachment is the diff; the reply chain is the review discussion. A follow-up [PATCH v2] thread is a superseded-then-new proposal.

Operations

Each change-request verb resolves onto the mail surfaces as follows.

Verb mail-patch resolution
list_open(filter) mail-archive.list_recent_threads(dev-list, since) filtered to [PATCH]-subject threads that have no terminal “applied”/“rejected” reply. One proposal_summary per open patch thread.
get(id) mail-archive.fetch_thread_by_url(id); the diff is extracted from the first message’s [PATCH] body or .patch attachment. base is the trunk from config; commits is []; mergeable is unknown until an svn patch --dry-run.
get_discussion(id) The thread’s reply chain, normalised to {author, date, body, kind}. A reply containing the configured approval token (LGTM / +1) maps to kind: approval.
post_review(id, verdict, body) Drafts a threaded reply via contract:mail-create (in-reply-to the thread), never sends. The verdict shapes the reply’s opening line (+1, needs work); the maintainer reviews and sends.
land(id, strategy) Delegates to contract:source-control. Extracts the patch via get, calls the source-control adapter’s apply + commit (svn patch <file> then svn commit — see tools/asf-svn/source-control.md), then drafts an “applied in rNNNNN, thanks!” reply on the thread. strategy is advisory — SVN lands a patch as one commit (squash).
reject(id, reason) Drafts a threaded reply carrying reason. No commit — the absence of a land is the rejection. Nothing is transitioned; the thread simply closes socially.
status(id) checks: none (a mail thread has no CI), mergeable from an svn patch --dry-run. Skills degrade the checks gate to advisory and fall back to a human-judgement prompt (the contract’s status graceful-degradation path — this is the maximally-degraded backend).

Attribution

The delegated land credits the patch author in the SVN commit message (Patch by <From: header>.) while committing under the landing committer’s identity — the same convention ASF committers follow when applying a mailed patch by hand. The adapter reads the author from the [PATCH] message’s From: header; it never impersonates the author’s identity. This answers #669’s “patch-author vs. committer attribution” open question for the mail-patch backend.

Diff identity across backends

A [PATCH] body has no forge-assigned id, so the adapter derives a stable proposal id from the thread’s archive permalink (the mail-archive thread hash). Re-posted [PATCH v2] threads get a new id and mark the prior thread superseded — this is the mail-patch answer to #669’s “diff identity normalization across backends” open question: identity is the thread, not the diff bytes.

Configuration

Declared under the change-request block in projects/<project>/project.md:

change_request:
  backend: mail-patch
  land_via: source-control        # land delegates to the VCS adapter
  review_channel: mailing-list
  default_strategy: squash        # SVN applies a patch as one commit
  mail_patch:
    dev_list: dev@<project>.apache.org
    patch_subject_prefix: "[PATCH]"
    approval_token: "LGTM"        # reply token that counts as an approval
    trunk_url: https://svn.apache.org/repos/asf/<project>/trunk
  • dev_list — the list list_open scans and replies draft against.
  • patch_subject_prefix — the subject marker that identifies a patch thread (some projects use [PATCH], some [PROPOSAL]).
  • approval_token — the reply string get_discussion reads as a kind: approval.
  • trunk_url — the SVN trunk the delegated land applies to.

Backend-specific keys live under change_request.mail_patch.*; the generic keys are the contract’s.

Cross-references

Suggest a change