Skip to content

Commit 045ab49

Browse files
authored
docs: add AI contribution policy and PR guardrail (#20225)
* docs: add AI contribution policy and PR guardrail * fix(ci): avoid sanitizing PR body as HTML * fix(ci): align AI declaration with PR guardrail * fix(ci): validate only visible PR declarations * fix(ci): close AI declaration validation gaps
1 parent d4cb4a2 commit 045ab49

6 files changed

Lines changed: 390 additions & 0 deletions

File tree

.github/PULL_REQUEST_TEMPLATE.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,3 +24,16 @@ Briefly describe what this PR aims to solve. Include background context that wil
2424
- [ ] Refactoring
2525
- [ ] Performance Improvement
2626
- [ ] Other (please describe):
27+
28+
## AI assistance
29+
30+
<!--
31+
See AI_POLICY.md. Agent-opened PRs are welcome; a responsible human on the author side must own the change.
32+
The responsible human is NOT the reviewer — it is the submitter-side owner who has read the diff,
33+
can explain each change, and will answer questions during review.
34+
Write "None" for AI usage if no AI was involved.
35+
-->
36+
37+
- AI usage: <!-- Describe the assistance and affected scope without product/model/provider names, or write "None" -->
38+
- Responsible human: <!-- @github-id — author-side owner: has read every line, can explain each change, answers questions during review, owns follow-up fixes -->
39+
- [ ] The responsible human has read every line of this diff and can explain each change
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
const removeHtmlComments = (input) => {
2+
let visible = "";
3+
let cursor = 0;
4+
5+
while (cursor < input.length) {
6+
const commentStart = input.indexOf("<!--", cursor);
7+
if (commentStart === -1) {
8+
visible += input.slice(cursor);
9+
break;
10+
}
11+
12+
visible += input.slice(cursor, commentStart);
13+
const commentEnd = input.indexOf("-->", commentStart + 4);
14+
if (commentEnd === -1) {
15+
// GitHub hides an unterminated comment through the end of the body.
16+
break;
17+
}
18+
cursor = commentEnd + 3;
19+
}
20+
21+
return visible;
22+
};
23+
24+
const removeFencedCodeBlocks = (input) => {
25+
const lines = input.split("\n");
26+
const visibleLines = [];
27+
let fence = null;
28+
29+
for (const line of lines) {
30+
const content = line.replace(/^ {0,3}/, "");
31+
32+
if (!fence) {
33+
const opener = content.match(/^(`{3,}|~{3,})/);
34+
if (opener) {
35+
fence = { marker: opener[1][0], length: opener[1].length };
36+
visibleLines.push("");
37+
} else {
38+
visibleLines.push(line);
39+
}
40+
continue;
41+
}
42+
43+
let markerLength = 0;
44+
while (content[markerLength] === fence.marker) {
45+
markerLength += 1;
46+
}
47+
if (
48+
markerLength >= fence.length &&
49+
content.slice(markerLength).trim() === ""
50+
) {
51+
fence = null;
52+
}
53+
visibleLines.push("");
54+
}
55+
56+
return visibleLines.join("\n");
57+
};
58+
59+
module.exports = async ({ github, context, core }) => {
60+
const body = context.payload.pull_request.body || "";
61+
const visibleBody = removeFencedCodeBlocks(removeHtmlComments(body));
62+
63+
const problems = [];
64+
const sectionMatch = visibleBody.match(
65+
/^##\s*AI assistance\s*\n([\s\S]*?)(?=\n##\s|$(?![\s\S]))/im,
66+
);
67+
68+
if (!sectionMatch) {
69+
problems.push("the `## AI assistance` section is missing");
70+
} else {
71+
const section = sectionMatch[1];
72+
73+
const usageLine = section.match(/^-[ \t]*AI usage:[ \t]*(.*)$/im);
74+
const usage = usageLine?.[1].trim();
75+
if (!usage) {
76+
problems.push(
77+
"`AI usage:` is not filled in (write `None` if no AI was used)",
78+
);
79+
}
80+
81+
const humanLine = section.match(/^-[ \t]*Responsible human:[ \t]*(.*)$/im);
82+
const human = humanLine?.[1].trim();
83+
const usernameMatch = human?.match(/^@([A-Za-z0-9-]+)$/);
84+
const username = usernameMatch?.[1];
85+
const usernameIsValid =
86+
username &&
87+
username.length <= 39 &&
88+
!username.startsWith("-") &&
89+
!username.endsWith("-") &&
90+
!username.includes("--") &&
91+
username.toLowerCase() !== "your-github-id";
92+
93+
if (!usernameIsValid) {
94+
problems.push(
95+
"`Responsible human:` must name a real GitHub user, e.g. `@octocat`",
96+
);
97+
} else {
98+
const prAuthor = context.payload.pull_request.user;
99+
const authorIsBot =
100+
prAuthor.type === "Bot" || prAuthor.login.toLowerCase().endsWith("[bot]");
101+
102+
if (
103+
!authorIsBot &&
104+
username.toLowerCase() !== prAuthor.login.toLowerCase()
105+
) {
106+
problems.push(
107+
`Responsible human must match the PR author \`@${prAuthor.login}\``,
108+
);
109+
}
110+
111+
try {
112+
const { data: account } = await github.rest.users.getByUsername({
113+
username,
114+
});
115+
if (account.type !== "User") {
116+
problems.push("`Responsible human:` must refer to a human account");
117+
}
118+
} catch (error) {
119+
if (error.status === 404) {
120+
problems.push(`Responsible human \`@${username}\` does not exist`);
121+
} else {
122+
throw error;
123+
}
124+
}
125+
}
126+
127+
const readBox = section.match(
128+
/^-\s*\[x\]\s+The responsible human has read every line/im,
129+
);
130+
if (!readBox) {
131+
problems.push(
132+
'the checkbox "The responsible human has read every line of this diff" is not checked',
133+
);
134+
}
135+
}
136+
137+
if (problems.length > 0) {
138+
core.setOutput("ai", "invalid");
139+
core.setOutput("problems", problems.map((p) => `- ${p}`).join("\n"));
140+
core.setFailed(`AI assistance section incomplete: ${problems.join("; ")}`);
141+
} else {
142+
core.setOutput("ai", "valid");
143+
}
144+
};

.github/workflows/pr.yml

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,3 +149,46 @@ jobs:
149149
body: |
150150
At least one type of change must be checked in the PR description.
151151
@${{ github.event.pull_request.user.login }} please update it 🙏.
152+
153+
ai_assistance:
154+
runs-on: ubuntu-latest
155+
steps:
156+
- uses: actions/checkout@v6
157+
- name: Check AI assistance section
158+
uses: actions/github-script@v9
159+
id: check
160+
with:
161+
script: |
162+
const script = require('./.github/scripts/check_pr_ai_assistance.js')
163+
await script({ github, context, core })
164+
- name: Delete Comment
165+
if: always() && steps.check.outputs.ai == 'valid'
166+
uses: everpcpc/comment-on-pr-action@v1
167+
with:
168+
token: ${{ github.token }}
169+
identifier: "pr-assistant-ai-assistance"
170+
delete: true
171+
- name: Comment on PR
172+
if: always() && steps.check.outputs.ai == 'invalid'
173+
uses: everpcpc/comment-on-pr-action@v1
174+
with:
175+
token: ${{ github.token }}
176+
identifier: "pr-assistant-ai-assistance"
177+
body: |
178+
The `## AI assistance` section is incomplete. Review is blocked until it is filled in.
179+
@${{ github.event.pull_request.user.login }} please update it 🙏.
180+
181+
${{ steps.check.outputs.problems }}
182+
183+
Required format (see [AI_POLICY.md](https://github.com/databendlabs/databend/blob/main/AI_POLICY.md)):
184+
185+
```
186+
## AI assistance
187+
188+
- AI usage: An AI coding agent drafted the patch; I reviewed and added logic tests (or "None")
189+
- Responsible human: @actual-github-id
190+
- [x] The responsible human has read every line of this diff and can explain each change
191+
```
192+
193+
The responsible human is the author-side owner — the person who has read the diff,
194+
can explain each change, and will answer questions during review.

AGENTS.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ This file is the top-level guide for repository-specific working rules. Start by
44

55
## Core Workflow
66
- Build context from the codebase first. Databend is a multi-crate Rust workspace, so understand the affected module boundaries before editing.
7+
- Agents may open PRs directly, but every PR must name a responsible human on the author side who has read the full diff, can explain the changes, and answers review questions. Fill in the "AI assistance" section of the PR template. See [`AI_POLICY.md`](AI_POLICY.md).
78
- For code issues inside the repository, default to best-effort root cause analysis. Do not stop at symptom-only fixes when the underlying cause can be found with reasonable investigation.
89
- When guidance, lower-level docs, and repository practice conflict or are ambiguous, identify the specific conflict that affects the current decision instead of assuming one source is automatically correct.
910
- Validate incrementally. Run the smallest relevant checks early, and scale verification to the parts that will remain in the branch and enter review.
@@ -26,6 +27,7 @@ This file is the top-level guide for repository-specific working rules. Start by
2627
- A clean full build of the workspace can take about 20 minutes. Prefer the smallest relevant build or test step first, then scale validation up before handoff.
2728

2829
## Detail Index
30+
- [`AI_POLICY.md`](AI_POLICY.md) for AI-assisted contribution rules: human accountability, declaration, and what gets PRs closed.
2931
- [`agents/repository-structure.md`](agents/repository-structure.md) for workspace layout and where code, tests, tooling, and fixtures live.
3032
- [`agents/development-commands.md`](agents/development-commands.md) for setup, build, run, test, format, and lint commands.
3133
- [`agents/coding-style.md`](agents/coding-style.md) for Rust, Python, shell, naming, error handling, and observability conventions.

0 commit comments

Comments
 (0)