From 920d634e835ede760831bee51baabb054924f041 Mon Sep 17 00:00:00 2001 From: morluto <76467478+morluto@users.noreply.github.com> Date: Tue, 3 Nov 2026 02:18:55 +0800 Subject: [PATCH] ci: exempt designated authors from readiness declaration --- .github/pull_request_template.md | 3 +- AGENTS.md | 4 +- CONTRIBUTING.md | 12 +++-- scripts/ci/pr-readiness.d.mts | 3 +- scripts/ci/pr-readiness.mjs | 14 +++++ tests/boundary/filesystem/prReadiness.test.ts | 54 +++++++++++++++++++ 6 files changed, 82 insertions(+), 8 deletions(-) diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 91063384cc..5420b790ad 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -106,7 +106,8 @@ limits, security/privacy/containment implications, and meaningful proof gaps. -- ## Author readiness - diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 55221d1261..744cb9361f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -93,7 +93,8 @@ Before requesting maintainer review, the human owner must: passing. Follow the [verification requirements](#development-feedback-and-pr-verification) for required checks and real-provider coverage. - Complete the **Author readiness** declaration in the PR template, explicitly - stating that the PR is ready for initial review. + stating that the PR is ready for initial review, unless the PR author is + exempt from the declaration as listed below. Keep the PR description concise and current. Explain the problem, observable outcome, approach, and useful evidence; link long logs or put them in optional @@ -114,9 +115,12 @@ validation current as work changes. AI agents must leave human declarations to the human owner and must not attest to a human review on their behalf. The `PR readiness` check reports pending for drafts and requires a named human -owner and all three template declarations for non-drafts. It checks that the -declaration is filled, not whether the code or evidence is correct. Existing PRs -can copy the **Author readiness** section from the current template. +owner and all three template declarations for non-drafts. PRs authored by +@morluto, @kaoru0822-kitauji, @N0zoM1z0, or @gensokyo-dev are exempt from +the declaration; their human ownership, review, and evidence responsibilities +above still apply. The check identifies authors by GitHub account ID. It checks +that the declaration is filled, not whether the code or evidence is correct. +Existing PRs can copy the **Author readiness** section from the current template. Open PRs must have distinct head commits because GitHub shares commit statuses. Maintainers enable merge enforcement after the workflow is merged: run diff --git a/scripts/ci/pr-readiness.d.mts b/scripts/ci/pr-readiness.d.mts index c42684eb1c..7997e9cfdd 100644 --- a/scripts/ci/pr-readiness.d.mts +++ b/scripts/ci/pr-readiness.d.mts @@ -3,6 +3,7 @@ export interface ReadinessPullRequest { readonly state: "open" | "closed"; readonly draft: boolean; readonly body: string | null; + readonly user: { readonly id: number } | null; readonly head: { readonly sha: string }; readonly base: { readonly ref: string }; } @@ -53,7 +54,7 @@ export interface ReadinessGitHub { >; } export function evaluateReadiness( - pull: Pick, + pull: Pick, template: string, ): ReadinessResult; export function runReadinessCheck(input: { diff --git a/scripts/ci/pr-readiness.mjs b/scripts/ci/pr-readiness.mjs index 3f53438156..1043af92ec 100644 --- a/scripts/ci/pr-readiness.mjs +++ b/scripts/ci/pr-readiness.mjs @@ -2,6 +2,14 @@ import { readFile } from "node:fs/promises"; const result = (state, description) => ({ state, description }); +// GitHub account IDs remain stable when a login changes. +const EXEMPT_AUTHOR_IDS = new Set([ + 76467478, // morluto + 208281322, // kaoru0822-kitauji + 161784452, // N0zoM1z0 + 342580529, // gensokyo-dev +]); + const OPEN_HEADS_QUERY = `query ReadinessOpenHeads($owner: String!, $name: String!, $after: String) { repository(owner: $owner, name: $name) { pullRequests(first: 100, after: $after, states: OPEN, orderBy: {field: CREATED_AT, direction: ASC}) { @@ -105,6 +113,11 @@ export function evaluateReadiness(pull, template) { "pending", "Draft PR: complete Author readiness before requesting review.", ); + if (EXEMPT_AUTHOR_IDS.has(pull.user?.id)) + return result( + "success", + "PR author is exempt from the readiness declaration.", + ); const expected = readinessLines(template).flatMap((line) => { const match = /^- \[ \] (.+)$/u.exec(line); return match ? [match[1]] : []; @@ -210,6 +223,7 @@ export async function runReadinessCheck({ github, context, core }) { latest.head.sha !== pull.head.sha || latest.body !== pull.body || latest.draft !== pull.draft || + latest.user?.id !== pull.user?.id || latest.base.ref !== pull.base.ref ) verdict = result( diff --git a/tests/boundary/filesystem/prReadiness.test.ts b/tests/boundary/filesystem/prReadiness.test.ts index 4d3e7273e2..8e136c9da4 100644 --- a/tests/boundary/filesystem/prReadiness.test.ts +++ b/tests/boundary/filesystem/prReadiness.test.ts @@ -30,6 +30,7 @@ const makePull = ( state: "open", draft: false, body: readyBody, + user: { id: 123456 }, head: { sha: "a".repeat(40) }, base: { ref: "main" }, ...changes, @@ -94,6 +95,30 @@ function fixture(changes: Partial = {}) { } describe("initial author declaration", () => { + it.each([76467478, 208281322, 161784452, 342580529])( + "exempts the configured author ID %s, but keeps drafts pending", + (id) => { + expect( + evaluateReadiness(makePull({ user: { id }, body: null }), template), + ).toMatchObject({ + state: "success", + description: expect.stringContaining("exempt"), + }); + expect( + evaluateReadiness( + makePull({ user: { id }, body: null, draft: true }), + template, + ).state, + ).toBe("pending"); + }, + ); + + it("does not exempt unknown or missing author identities", () => { + for (const user of [{ id: 123456 }, null]) + expect( + evaluateReadiness(makePull({ user, body: null }), template).state, + ).toBe("failure"); + }); it("accepts the filled current template and uppercase checkmarks", () => { expect(evaluateReadiness(makePull(), template).state).toBe("success"); expect( @@ -212,6 +237,35 @@ describe("initial author declaration", () => { }); describe("GitHub status boundary", () => { + it.each([76467478, 208281322, 161784452, 342580529])( + "publishes a labeled exemption for author ID %s after checking the head", + async (id) => { + const input = fixture({ user: { id }, body: null }); + expect(await runReadinessCheck(input)).toMatchObject({ + state: "success", + description: expect.stringContaining("exempt"), + }); + expect(input.paginate).toHaveBeenCalledOnce(); + expect(input.createCommitStatus).toHaveBeenLastCalledWith( + expect.objectContaining({ state: "success", sha: input.pull.head.sha }), + ); + }, + ); + it("does not let an exempt author lend success to another PR sharing the head", async () => { + const input = fixture({ user: { id: 76467478 }, body: null }); + input.paginate.mockResolvedValue([input.pull, makePull({ number: 43 })]); + expect((await runReadinessCheck(input))?.state).toBe("failure"); + }); + + it("does not publish an exemption if the author changes during the check", async () => { + const input = fixture({ user: { id: 76467478 }, body: null }); + input.get + .mockResolvedValueOnce({ data: input.pull }) + .mockResolvedValueOnce({ + data: makePull({ user: { id: 123456 }, body: null }), + }); + expect((await runReadinessCheck(input))?.state).toBe("pending"); + }); it("publishes failure on the actual PR head when declarations are unchecked", async () => { const input = fixture({ body: readyBody.replaceAll("[x]", "[ ]") }); expect((await runReadinessCheck(input))?.state).toBe("failure");