Skip to content

📦 WP-142 — ci: DCO sign-off required on pull requests

Engineering log · Phase 13 — Contributor admission, augmentation guard and keypoint parity, v0.6.0

🔗 Depends on WP-141 · Needed by none

⬅️ WP-141 · Roadmap · WP-143 ➡️

WP-142 — a sign-off is a representation, not an identity proof

scripts/lint/check_dco.py and a dco job in lint.yml. Every non-merge commit in a pull request must carry a well-formed Signed-off-by: trailer naming a person and an address that reaches them. Merges are excluded because their message is generated by whoever pressed the button rather than written by the contributor, so there is no representation in it to check.

The design decision worth defending is what the check refuses to do. It does not require the sign-off address to match the commit author's, which is the first thing a stricter implementation reaches for. A DCO sign-off is a representation by the signer: co-authored work legitimately carries several, an author committing under an employer address may sign under a personal one, and a maintainer applying a contributor's patch signs their own line beside the original. Author-matching rejects all three and catches nobody willing to type a false name, so the check enforces the shape of the statement and leaves its truth where the DCO itself puts it. A test asserts the non-matching case passes, so the decision is pinned rather than merely intended.

Malformed is reported as malformed, never as missing. A second pattern matches lines that mean to be a sign-off — a missing address, a bare address with no name, a domain without a dot, Signed off by, a trailer without angle brackets — so the report distinguishes a one-character fix from a contributor searching for a trailer they believe they already wrote. Five near-miss shapes are parametrized, and the refusal text names the flag that fixes it.

Pull requests only, and this repository is the reason. Its own history predates the requirement — running the check against main would refuse every existing commit rather than any submitted one, which is a gate that fails at everything and therefore protects nothing. Verified rather than assumed: run over HEAD~1..HEAD it refuses WP-141's own commit, which is the correct answer and exactly why the job is conditioned on pull_request.

Two smaller things. The job installs nothing, the checker being standard-library only, so layer one costs a container start rather than a dependency resolve. And the two SHAs reach the command through the environment rather than the command string: they are interpolated from event data, and a value reaching a shell directly is how that becomes an injection rather than a range.