chore: update repo policy to check tool co-authors
This commit is contained in:
parent
f4ac3b0c12
commit
93e8258270
5 changed files with 42 additions and 31 deletions
|
|
@ -2,9 +2,10 @@
|
||||||
"version": 1,
|
"version": 1,
|
||||||
"salt": "40f076a2ae183c348be3e11a8b58ebac",
|
"salt": "40f076a2ae183c348be3e11a8b58ebac",
|
||||||
"digests": [],
|
"digests": [],
|
||||||
"allowedCoAuthors": [
|
"toolCoAuthors": [
|
||||||
"ayo@ayco.io",
|
"*@anthropic.com",
|
||||||
"ramon.aycojr@gmail.com",
|
"*@openai.com",
|
||||||
"*@users.noreply.github.com"
|
"*@cursor.com",
|
||||||
|
"*@devin.ai"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -126,7 +126,7 @@ App-owned modules in `modules/`: `UserService` (`user/user.js`) derives a non-cr
|
||||||
|
|
||||||
- **Code style is enforced by ESLint Stylistic**, not Prettier: 2-space indent, single quotes, **no semicolons**, no trailing commas, spaces inside `{ braces }` but not `[brackets]`. Run `pnpm lint:fix` before committing. Both `**/*.js` and `**/*.css` are linted (CSS via `@eslint/css`).
|
- **Code style is enforced by ESLint Stylistic**, not Prettier: 2-space indent, single quotes, **no semicolons**, no trailing commas, spaces inside `{ braces }` but not `[brackets]`. Run `pnpm lint:fix` before committing. Both `**/*.js` and `**/*.css` are linted (CSS via `@eslint/css`).
|
||||||
- `modules/` uses ES classes; `scripts/` uses plain functions. Match the surrounding style of the file you edit.
|
- `modules/` uses ES classes; `scripts/` uses plain functions. Match the surrounding style of the file you edit.
|
||||||
- **Content policy.** Commit messages, branch names, PR text, and contributed lines are checked by `scripts/check-content.mjs` (hooks + the `Checks` workflow) against `.repo-policy.json`. Write commit messages in plain project voice; no tool-attribution trailers or footers, no `Co-Authored-By:` line for anyone outside the policy's `allowedCoAuthors`, no session links.
|
- **Content policy.** Commit messages, branch names, PR text, and contributed lines are checked by `scripts/check-content.mjs` (hooks + the `Checks` workflow) against `.repo-policy.json`. Write commit messages in plain project voice; no tool-attribution trailers or footers, no `Co-Authored-By:` line for a non-human contributor (the policy's `toolCoAuthors` list — human co-authors are always fine), no session links.
|
||||||
- The same scanner matches text against a maintainer-managed reserved-terms list. Findings report a location and a masked preview, never the term. If one flags your change, reword it or ask a maintainer — don't edit `.repo-policy.json`.
|
- The same scanner matches text against a maintainer-managed reserved-terms list. Findings report a location and a masked preview, never the term. If one flags your change, reword it or ask a maintainer — don't edit `.repo-policy.json`.
|
||||||
- **Source stays JS + JSDoc (`// @ts-check`)** — no TypeScript. Nothing is published from this repo, so there is no type-generation step here; the `.d.ts` files shipped by `@cozy-games/*` are generated in the cozy-games repo.
|
- **Source stays JS + JSDoc (`// @ts-check`)** — no TypeScript. Nothing is published from this repo, so there is no type-generation step here; the `.d.ts` files shipped by `@cozy-games/*` are generated in the cozy-games repo.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -112,9 +112,10 @@ For anything large, open an issue to discuss the approach first.
|
||||||
## Commit & PR hygiene
|
## Commit & PR hygiene
|
||||||
|
|
||||||
Write commit messages in plain project voice — what changed and why. Keep them
|
Write commit messages in plain project voice — what changed and why. Keep them
|
||||||
free of tool-attribution trailers or footers (including `Co-Authored-By:` lines
|
free of AI-tool attribution trailers or footers (including `Co-Authored-By:`
|
||||||
for anyone who isn't a human contributor) and of session links; the same goes for
|
lines crediting a coding assistant) and of session links; the same goes for PR
|
||||||
PR titles and bodies.
|
titles and bodies. Co-authoring with another *person* is always fine — use any
|
||||||
|
address you like, there is no contributor allowlist.
|
||||||
|
|
||||||
CI also checks your changes against a maintainer-managed reserved-terms list. If a
|
CI also checks your changes against a maintainer-managed reserved-terms list. If a
|
||||||
check flags your change, reword it — or ask a maintainer if it isn't clear why.
|
check flags your change, reword it — or ask a maintainer if it isn't clear why.
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,8 @@ const ALLOW_MARKER = 'content-policy: allow-next-line'
|
||||||
* @property {number} version
|
* @property {number} version
|
||||||
* @property {string} salt - HMAC key for the digest list.
|
* @property {string} salt - HMAC key for the digest list.
|
||||||
* @property {string[]} digests - Reserved-term digests, maintainer-managed.
|
* @property {string[]} digests - Reserved-term digests, maintainer-managed.
|
||||||
* @property {string[]} allowedCoAuthors - Emails; a leading `*` matches a suffix.
|
* @property {string[]} toolCoAuthors - Non-human co-author addresses to reject;
|
||||||
|
* a leading `*` matches a suffix. Every other address is a human, and passes.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -131,14 +132,16 @@ const CO_AUTHOR_PATTERN = /^\s*co-authored-by:\s*(.+)$/i
|
||||||
const EMAIL_PATTERN = /<([^>]+)>/
|
const EMAIL_PATTERN = /<([^>]+)>/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* Is this co-author address a tool rather than a person? Humans are the default:
|
||||||
|
* only addresses matching the policy's tool list are rejected.
|
||||||
* @param {string} email
|
* @param {string} email
|
||||||
* @param {string[]} [allowed] - Exact emails, or `*@domain` suffix patterns.
|
* @param {string[]} [tools] - Exact emails, or `*@domain` suffix patterns.
|
||||||
* @returns {boolean}
|
* @returns {boolean}
|
||||||
*/
|
*/
|
||||||
export function isAllowedCoAuthor(email, allowed = []) {
|
export function isToolCoAuthor(email, tools = []) {
|
||||||
const value = String(email).trim().toLowerCase()
|
const value = String(email).trim().toLowerCase()
|
||||||
if (!value) return false
|
if (!value) return false
|
||||||
return allowed.some((entry) => {
|
return tools.some((entry) => {
|
||||||
const pattern = String(entry).trim().toLowerCase()
|
const pattern = String(entry).trim().toLowerCase()
|
||||||
return pattern.startsWith('*') ? value.endsWith(pattern.slice(1)) : value === pattern
|
return pattern.startsWith('*') ? value.endsWith(pattern.slice(1)) : value === pattern
|
||||||
})
|
})
|
||||||
|
|
@ -171,8 +174,8 @@ export function checkStructural(line, policy) {
|
||||||
const trailer = CO_AUTHOR_PATTERN.exec(line)
|
const trailer = CO_AUTHOR_PATTERN.exec(line)
|
||||||
if (trailer) {
|
if (trailer) {
|
||||||
const email = (EMAIL_PATTERN.exec(trailer[1]) || [])[1] || ''
|
const email = (EMAIL_PATTERN.exec(trailer[1]) || [])[1] || ''
|
||||||
if (!isAllowedCoAuthor(email, policy.allowedCoAuthors)) {
|
if (isToolCoAuthor(email, policy.toolCoAuthors)) {
|
||||||
findings.push({ rule: 'unlisted-co-author', detail: preview(line) })
|
findings.push({ rule: 'tool-co-author', detail: preview(line) })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return findings
|
return findings
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ import {
|
||||||
digestCandidate,
|
digestCandidate,
|
||||||
maskTerm,
|
maskTerm,
|
||||||
matchReserved,
|
matchReserved,
|
||||||
isAllowedCoAuthor,
|
isToolCoAuthor,
|
||||||
checkStructural,
|
checkStructural,
|
||||||
scanText,
|
scanText,
|
||||||
isAllowedByMarker,
|
isAllowedByMarker,
|
||||||
|
|
@ -116,36 +116,42 @@ describe('maskTerm', () => {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('isAllowedCoAuthor', () => {
|
describe('isToolCoAuthor', () => {
|
||||||
it('accepts the repo\'s listed humans, case-insensitively', () => {
|
it('matches a tool address by domain, case-insensitively', () => {
|
||||||
expect(isAllowedCoAuthor('ayo@ayco.io', policy.allowedCoAuthors)).toBe(true)
|
expect(isToolCoAuthor('noreply@anthropic.com', policy.toolCoAuthors)).toBe(true)
|
||||||
expect(isAllowedCoAuthor('Ramon.AycoJr@gmail.com', policy.allowedCoAuthors)).toBe(true)
|
expect(isToolCoAuthor('NoReply@OpenAI.com', policy.toolCoAuthors)).toBe(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('accepts a `*` suffix pattern', () => {
|
it('leaves dependabot alone — it is honest automation, not AI attribution', () => {
|
||||||
expect(isAllowedCoAuthor('1234+someone@users.noreply.github.com', policy.allowedCoAuthors)).toBe(true)
|
expect(isToolCoAuthor('49699333+dependabot[bot]@users.noreply.github.com', policy.toolCoAuthors)).toBe(false)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('rejects anyone else, and an empty address', () => {
|
it('treats every other address as a human — contributors are not allowlisted', () => {
|
||||||
expect(isAllowedCoAuthor('someone@example.com', policy.allowedCoAuthors)).toBe(false)
|
expect(isToolCoAuthor('jane@example.com', policy.toolCoAuthors)).toBe(false)
|
||||||
expect(isAllowedCoAuthor('ayo@ayco.io.example.com', policy.allowedCoAuthors)).toBe(false)
|
expect(isToolCoAuthor('ayo@ayco.io', policy.toolCoAuthors)).toBe(false)
|
||||||
expect(isAllowedCoAuthor('', policy.allowedCoAuthors)).toBe(false)
|
expect(isToolCoAuthor('1234+someone@users.noreply.github.com', policy.toolCoAuthors)).toBe(false)
|
||||||
|
expect(isToolCoAuthor('', policy.toolCoAuthors)).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('does not match a lookalike domain', () => {
|
||||||
|
expect(isToolCoAuthor('someone@anthropic.com.example.com', policy.toolCoAuthors)).toBe(false)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('checkStructural: co-author trailers', () => {
|
describe('checkStructural: co-author trailers', () => {
|
||||||
it('allows a listed co-author', () => {
|
it('allows an outside human contributor using any address', () => {
|
||||||
|
expect(checkStructural('Co-authored-by: Jane Dev <jane@example.com>', policy)).toEqual([])
|
||||||
expect(checkStructural('Co-authored-by: Ayo <ayo@ayco.io>', policy)).toEqual([])
|
expect(checkStructural('Co-authored-by: Ayo <ayo@ayco.io>', policy)).toEqual([])
|
||||||
})
|
})
|
||||||
|
|
||||||
it('flags an unlisted one, whatever the casing', () => {
|
it('flags a tool trailer, whatever the casing', () => {
|
||||||
const findings = checkStructural('Co-Authored-By: Some Tool <tool@example.com>', policy)
|
const findings = checkStructural('Co-Authored-By: Some Model <noreply@anthropic.com>', policy)
|
||||||
expect(findings).toHaveLength(1)
|
expect(findings).toHaveLength(1)
|
||||||
expect(findings[0].rule).toBe('unlisted-co-author')
|
expect(findings[0].rule).toBe('tool-co-author')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('flags a trailer with no address at all', () => {
|
it('allows a trailer with no address — a human typo, not a tool', () => {
|
||||||
expect(checkStructural('Co-authored-by: Some Tool', policy)).toHaveLength(1)
|
expect(checkStructural('Co-authored-by: Jane Dev', policy)).toEqual([])
|
||||||
})
|
})
|
||||||
|
|
||||||
it('ignores prose that merely mentions co-authors', () => {
|
it('ignores prose that merely mentions co-authors', () => {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue