@@ -71,10 +71,10 @@ jobs:
7171 repo: context.repo.repo,
7272 issue_number: pr.number
7373 });
74-
74+
7575 const existing = comments.find(c => c.body.includes(markerText));
7676 if (existing) return;
77-
77+
7878 await github.rest.issues.createComment({
7979 owner: context.repo.owner,
8080 repo: context.repo.repo,
8383 });
8484 }
8585
86+ async function removeComment(marker) {
87+ const markerText = `<!-- pr-standards:${marker} -->`;
88+ const { data: comments } = await github.rest.issues.listComments({
89+ owner: context.repo.owner,
90+ repo: context.repo.repo,
91+ issue_number: pr.number
92+ });
93+ const existing = comments.find(c => c.body.includes(markerText));
94+ if (!existing) return;
95+ await github.rest.issues.deleteComment({
96+ owner: context.repo.owner,
97+ repo: context.repo.repo,
98+ comment_id: existing.id
99+ });
100+ }
101+
102+ await removeLabel('needs:issue');
103+ await removeComment('issue');
104+
86105 // Step 1: Check title format
87106 // Matches: feat:, feat(scope):, feat (scope):, etc.
88- const titlePattern = /^(feat|fix|docs|chore|refactor|test)\s*(\([a-zA-Z0-9-]+\))?\s*:/;
107+ const titlePattern = /^(feat|fix|docs|chore|refactor|test|ci )\s*(\([a-zA-Z0-9-]+\))?\s*:/;
89108 const hasValidTitle = titlePattern.test(title);
90109
91110 if (!hasValidTitle) {
99118 - \`chore:\` or \`chore(scope):\` maintenance tasks
100119 - \`refactor:\` or \`refactor(scope):\` code refactoring
101120 - \`test:\` or \`test(scope):\` adding or updating tests
121+ - \`ci:\` or \`ci(scope):\` CI and automation changes
102122
103123 Where \`scope\` is the package name (e.g., \`app\`, \`desktop\`, \`opencode\`).
104124
@@ -107,245 +127,5 @@ jobs:
107127 }
108128
109129 await removeLabel('needs:title');
110-
111- // Step 2: Check for linked issue (skip for docs/refactor/feat PRs)
112- const skipIssueCheck = /^(docs|refactor|feat)\s*(\([a-zA-Z0-9-]+\))?\s*:/.test(title);
113- if (skipIssueCheck) {
114- await removeLabel('needs:issue');
115- console.log('Skipping issue check for docs/refactor/feat PR');
116- return;
117- }
118- const query = `
119- query($owner: String!, $repo: String!, $number: Int!) {
120- repository(owner: $owner, name: $repo) {
121- pullRequest(number: $number) {
122- closingIssuesReferences(first: 1) {
123- totalCount
124- }
125- }
126- }
127- }
128- `;
129-
130- const result = await github.graphql(query, {
131- owner: context.repo.owner,
132- repo: context.repo.repo,
133- number: pr.number
134- });
135-
136- const linkedIssues = result.repository.pullRequest.closingIssuesReferences.totalCount;
137-
138- if (linkedIssues === 0) {
139- await addLabel('needs:issue');
140- await comment('issue', `Thanks for your contribution!
141-
142- This PR doesn't have a linked issue. All PRs must reference an existing issue.
143-
144- Please:
145- 1. Open an issue describing the bug/feature (if one doesn't exist)
146- 2. Add \`Fixes #<number>\` or \`Closes #<number>\` to this PR description
147-
148- See [CONTRIBUTING.md](../blob/dev/CONTRIBUTING.md#issue-first-policy) for details.`);
149- return;
150- }
151-
152- await removeLabel('needs:issue');
153130 console.log('PR meets all standards');
154131
155- check-compliance :
156- runs-on : ubuntu-latest
157- permissions :
158- contents : read
159- pull-requests : write
160- steps :
161- - name : Check PR template compliance
162- uses : actions/github-script@v7
163- with :
164- script : |
165- const pr = context.payload.pull_request;
166- const login = pr.user.login;
167-
168- // Skip PRs older than Feb 18, 2026 at 6PM EST (Feb 19, 2026 00:00 UTC)
169- const cutoff = new Date('2026-02-19T00:00:00Z');
170- const prCreated = new Date(pr.created_at);
171- if (prCreated < cutoff) {
172- console.log(`Skipping: PR #${pr.number} was created before cutoff (${prCreated.toISOString()})`);
173- return;
174- }
175-
176- // Check if author is a team member or bot
177- if (login === 'opencode-agent[bot]') return;
178- const { data: file } = await github.rest.repos.getContent({
179- owner: context.repo.owner,
180- repo: context.repo.repo,
181- path: '.github/TEAM_MEMBERS',
182- ref: 'dev'
183- });
184- const members = Buffer.from(file.content, 'base64').toString().split('\n').map(l => l.trim()).filter(Boolean);
185- if (members.includes(login)) {
186- console.log(`Skipping: ${login} is a team member`);
187- return;
188- }
189-
190- const body = pr.body || '';
191- const title = pr.title;
192- const isDocsRefactorOrFeat = /^(docs|refactor|feat)\s*(\([a-zA-Z0-9-]+\))?\s*:/.test(title);
193-
194- const issues = [];
195-
196- // Check: template sections exist
197- const hasWhatSection = /### What does this PR do\?/.test(body);
198- const hasTypeSection = /### Type of change/.test(body);
199- const hasVerifySection = /### How did you verify your code works\?/.test(body);
200- const hasChecklistSection = /### Checklist/.test(body);
201- const hasIssueSection = /### Issue for this PR/.test(body);
202-
203- if (!hasWhatSection || !hasTypeSection || !hasVerifySection || !hasChecklistSection || !hasIssueSection) {
204- issues.push('PR description is missing required template sections. Please use the [PR template](../blob/dev/.github/pull_request_template.md).');
205- }
206-
207- // Check: "What does this PR do?" has real content (not just placeholder text)
208- if (hasWhatSection) {
209- const whatMatch = body.match(/### What does this PR do\?\s*\n([\s\S]*?)(?=###|$)/);
210- const whatContent = whatMatch ? whatMatch[1].trim() : '';
211- const placeholder = 'Please provide a description of the issue';
212- const onlyPlaceholder = whatContent.includes(placeholder) && whatContent.replace(placeholder, '').replace(/[*\s]/g, '').length < 20;
213- if (!whatContent || onlyPlaceholder) {
214- issues.push('"What does this PR do?" section is empty or only contains placeholder text. Please describe your changes.');
215- }
216- }
217-
218- // Check: at least one "Type of change" checkbox is checked
219- if (hasTypeSection) {
220- const typeMatch = body.match(/### Type of change\s*\n([\s\S]*?)(?=###|$)/);
221- const typeContent = typeMatch ? typeMatch[1] : '';
222- const hasCheckedBox = /- \[x\]/i.test(typeContent);
223- if (!hasCheckedBox) {
224- issues.push('No "Type of change" checkbox is checked. Please select at least one.');
225- }
226- }
227-
228- // Check: issue reference (skip for docs/refactor/feat)
229- if (!isDocsRefactorOrFeat && hasIssueSection) {
230- const issueMatch = body.match(/### Issue for this PR\s*\n([\s\S]*?)(?=###|$)/);
231- const issueContent = issueMatch ? issueMatch[1].trim() : '';
232- const hasIssueRef = /(closes|fixes|resolves)\s+#\d+/i.test(issueContent) || /#\d+/.test(issueContent);
233- if (!hasIssueRef) {
234- issues.push('No issue referenced. Please add `Closes #<number>` linking to the relevant issue.');
235- }
236- }
237-
238- // Check: "How did you verify" has content
239- if (hasVerifySection) {
240- const verifyMatch = body.match(/### How did you verify your code works\?\s*\n([\s\S]*?)(?=###|$)/);
241- const verifyContent = verifyMatch ? verifyMatch[1].trim() : '';
242- if (!verifyContent) {
243- issues.push('"How did you verify your code works?" section is empty. Please explain how you tested.');
244- }
245- }
246-
247- // Check: checklist boxes are checked
248- if (hasChecklistSection) {
249- const checklistMatch = body.match(/### Checklist\s*\n([\s\S]*?)(?=###|$)/);
250- const checklistContent = checklistMatch ? checklistMatch[1] : '';
251- const unchecked = (checklistContent.match(/- \[ \]/g) || []).length;
252- const checked = (checklistContent.match(/- \[x\]/gi) || []).length;
253- if (checked < 2) {
254- issues.push('Not all checklist items are checked. Please confirm you have tested locally and have not included unrelated changes.');
255- }
256- }
257-
258- // Helper functions
259- async function addLabel(label) {
260- await github.rest.issues.addLabels({
261- owner: context.repo.owner,
262- repo: context.repo.repo,
263- issue_number: pr.number,
264- labels: [label]
265- });
266- }
267-
268- async function removeLabel(label) {
269- try {
270- await github.rest.issues.removeLabel({
271- owner: context.repo.owner,
272- repo: context.repo.repo,
273- issue_number: pr.number,
274- name: label
275- });
276- } catch (e) {}
277- }
278-
279- const hasComplianceLabel = pr.labels.some(l => l.name === 'needs:compliance');
280-
281- if (issues.length > 0) {
282- // Non-compliant
283- if (!hasComplianceLabel) {
284- await addLabel('needs:compliance');
285- }
286-
287- const marker = '<!-- issue-compliance -->';
288- const { data: comments } = await github.rest.issues.listComments({
289- owner: context.repo.owner,
290- repo: context.repo.repo,
291- issue_number: pr.number
292- });
293- const existing = comments.find(c => c.body.includes(marker));
294-
295- const body_text = `${marker}
296- This PR doesn't fully meet our [contributing guidelines](../blob/dev/CONTRIBUTING.md) and [PR template](../blob/dev/.github/pull_request_template.md).
297-
298- **What needs to be fixed:**
299- ${issues.map(i => `- ${i}`).join('\n')}
300-
301- Please edit this PR description to address the above within **2 hours**, or it will be automatically closed.
302-
303- If you believe this was flagged incorrectly, please let a maintainer know.`;
304-
305- if (existing) {
306- await github.rest.issues.updateComment({
307- owner: context.repo.owner,
308- repo: context.repo.repo,
309- comment_id: existing.id,
310- body: body_text
311- });
312- } else {
313- await github.rest.issues.createComment({
314- owner: context.repo.owner,
315- repo: context.repo.repo,
316- issue_number: pr.number,
317- body: body_text
318- });
319- }
320-
321- console.log(`PR #${pr.number} is non-compliant: ${issues.join(', ')}`);
322- } else if (hasComplianceLabel) {
323- // Was non-compliant, now fixed
324- await removeLabel('needs:compliance');
325-
326- const { data: comments } = await github.rest.issues.listComments({
327- owner: context.repo.owner,
328- repo: context.repo.repo,
329- issue_number: pr.number
330- });
331- const marker = '<!-- issue-compliance -->';
332- const existing = comments.find(c => c.body.includes(marker));
333- if (existing) {
334- await github.rest.issues.deleteComment({
335- owner: context.repo.owner,
336- repo: context.repo.repo,
337- comment_id: existing.id
338- });
339- }
340-
341- await github.rest.issues.createComment({
342- owner: context.repo.owner,
343- repo: context.repo.repo,
344- issue_number: pr.number,
345- body: 'Thanks for updating your PR! It now meets our contributing guidelines. :+1:'
346- });
347-
348- console.log(`PR #${pr.number} is now compliant, label removed`);
349- } else {
350- console.log(`PR #${pr.number} is compliant`);
351- }
0 commit comments