From cf4426010e1e87be3564dc56fee3ad045b19892b Mon Sep 17 00:00:00 2001 From: Sol <49aa1f65411fd096d2e2ec144f1e7aa36fdc76d1b907cfdf7be000c66f9d3b8e@buzz.block.builderlab.xyz> Date: Wed, 9 Sep 2026 10:27:25 -0400 Subject: [PATCH 1/2] chore: validate DCO sign-offs before commit Signed-off-by: Sol <49aa1f65411fd096d2e2ec144f1e7aa36fdc76d1b907cfdf7be000c66f9d3b8e@buzz.block.builderlab.xyz> --- lefthook.yml | 5 +++ package.json | 2 +- scripts/dco-signoff.test.mjs | 55 ++++++++++++++++++++++++++++++++ scripts/validate-dco-signoff.mjs | 46 ++++++++++++++++++++++++++ 4 files changed, 107 insertions(+), 1 deletion(-) create mode 100644 scripts/dco-signoff.test.mjs create mode 100755 scripts/validate-dco-signoff.mjs diff --git a/lefthook.yml b/lefthook.yml index 272ce8636..c7d8a3b42 100644 --- a/lefthook.yml +++ b/lefthook.yml @@ -1,6 +1,11 @@ # Equivalent of the aaif-goose/goose Husky hooks, using lefthook. # See: https://github.com/evilmartians/lefthook +commit-msg: + commands: + dco-signoff: + run: node ./scripts/validate-dco-signoff.mjs "{1}" + pre-commit: commands: biome-format: diff --git a/package.json b/package.json index effad5dd2..411192d3e 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,7 @@ "format": "biome format --write .", "preview": "vite preview", "tauri": "tauri", - "test": "vitest run", + "test": "vitest run && node --test scripts/dco-signoff.test.mjs", "test:release-scripts": "vitest run --config vitest.release-scripts.config.ts", "test:watch": "vitest", "test:coverage": "vitest run --coverage", diff --git a/scripts/dco-signoff.test.mjs b/scripts/dco-signoff.test.mjs new file mode 100644 index 000000000..12815a187 --- /dev/null +++ b/scripts/dco-signoff.test.mjs @@ -0,0 +1,55 @@ +import assert from "node:assert/strict"; +import { execFileSync, spawnSync } from "node:child_process"; +import { mkdtempSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import test from "node:test"; + +const validator = new URL("./validate-dco-signoff.mjs", import.meta.url) + .pathname; + +function run( + message, + author = { name: "Ada Lovelace", email: "ada@example.com" }, +) { + const repo = mkdtempSync(join(tmpdir(), "berd-dco-")); + execFileSync("git", ["init", "--quiet", repo]); + execFileSync("git", ["-C", repo, "config", "user.name", author.name]); + execFileSync("git", ["-C", repo, "config", "user.email", author.email]); + const messagePath = join(repo, "COMMIT_EDITMSG"); + writeFileSync(messagePath, message); + return spawnSync("node", [validator, messagePath], { + cwd: repo, + encoding: "utf8", + }); +} + +test("accepts an author-matching sign-off", () => { + const result = run( + "Subject\n\nSigned-off-by: Ada Lovelace \n", + ); + assert.equal(result.status, 0, result.stderr); +}); + +test("rejects a missing sign-off with remediation", () => { + const result = run("Subject\n"); + assert.equal(result.status, 1); + assert.match(result.stderr, /Signed-off-by: Ada Lovelace /); + assert.match(result.stderr, /git commit --signoff/); +}); + +test("rejects a sign-off from someone other than the author", () => { + const result = run( + "Subject\n\nSigned-off-by: Grace Hopper \n", + ); + assert.equal(result.status, 1); + assert.match(result.stderr, /Signed-off-by: Ada Lovelace /); +}); + +test("honors the effective author identity", () => { + const result = run( + "Subject\n\nSigned-off-by: Zoë Agent \n", + { name: "Zoë Agent", email: "zoe@example.com" }, + ); + assert.equal(result.status, 0, result.stderr); +}); diff --git a/scripts/validate-dco-signoff.mjs b/scripts/validate-dco-signoff.mjs new file mode 100755 index 000000000..c26a6ba60 --- /dev/null +++ b/scripts/validate-dco-signoff.mjs @@ -0,0 +1,46 @@ +#!/usr/bin/env node + +import { execFileSync } from "node:child_process"; + +function git(...args) { + return execFileSync("git", args, { encoding: "utf8" }).trim(); +} + +function authorIdentity() { + const ident = git("var", "GIT_AUTHOR_IDENT"); + const match = ident.match(/^(.* <[^<>]+>) \d+ [+-]\d{4}$/); + if (!match) { + throw new Error(`could not parse Git author identity: ${ident}`); + } + return match[1]; +} + +function signedOffByIdentities(messagePath) { + const trailers = git("interpret-trailers", "--parse", "--", messagePath); + return trailers + .split("\n") + .map((line) => line.match(/^Signed-off-by:\s*(.+)$/i)?.[1].trim()) + .filter(Boolean); +} + +const messagePath = process.argv[2]; +if (!messagePath) { + console.error("usage: validate-dco-signoff.mjs "); + process.exit(2); +} + +try { + const author = authorIdentity(); + if (!signedOffByIdentities(messagePath).includes(author)) { + console.error( + `DCO sign-off required: add exactly "Signed-off-by: ${author}".`, + ); + console.error( + "Retry the commit with `git commit --signoff` or add that trailer in your commit editor.", + ); + process.exit(1); + } +} catch (error) { + console.error(`Unable to validate DCO sign-off: ${error.message}`); + process.exit(1); +} From 55f81acbb26043fc4a32940a29d4b7c5b235f649 Mon Sep 17 00:00:00 2001 From: Sol <49aa1f65411fd096d2e2ec144f1e7aa36fdc76d1b907cfdf7be000c66f9d3b8e@buzz.block.builderlab.xyz> Date: Wed, 9 Sep 2026 10:35:15 -0400 Subject: [PATCH 2/2] test: isolate DCO validator author identity Signed-off-by: Sol <49aa1f65411fd096d2e2ec144f1e7aa36fdc76d1b907cfdf7be000c66f9d3b8e@buzz.block.builderlab.xyz> --- scripts/dco-signoff.test.mjs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/scripts/dco-signoff.test.mjs b/scripts/dco-signoff.test.mjs index 12815a187..03bf0b3ba 100644 --- a/scripts/dco-signoff.test.mjs +++ b/scripts/dco-signoff.test.mjs @@ -3,10 +3,12 @@ import { execFileSync, spawnSync } from "node:child_process"; import { mkdtempSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; +import { fileURLToPath } from "node:url"; import test from "node:test"; -const validator = new URL("./validate-dco-signoff.mjs", import.meta.url) - .pathname; +const validator = fileURLToPath( + new URL("./validate-dco-signoff.mjs", import.meta.url), +); function run( message, @@ -21,6 +23,11 @@ function run( return spawnSync("node", [validator, messagePath], { cwd: repo, encoding: "utf8", + env: { + ...process.env, + GIT_AUTHOR_NAME: author.name, + GIT_AUTHOR_EMAIL: author.email, + }, }); }