chore: add compliance checks for issues and PRs with recheck on edit (#14170)
This commit is contained in:
64
.github/workflows/duplicate-issues.yml
vendored
64
.github/workflows/duplicate-issues.yml
vendored
@@ -2,10 +2,11 @@ name: duplicate-issues
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [opened]
|
||||
types: [opened, edited]
|
||||
|
||||
jobs:
|
||||
check-duplicates:
|
||||
if: github.event.action == 'opened'
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -34,7 +35,7 @@ jobs:
|
||||
"webfetch": "deny"
|
||||
}
|
||||
run: |
|
||||
opencode run -m opencode/claude-haiku-4-5 "A new issue has been created:
|
||||
opencode run -m opencode/claude-sonnet-4-6 "A new issue has been created:
|
||||
|
||||
Issue number: ${{ github.event.issue.number }}
|
||||
|
||||
@@ -115,3 +116,62 @@ jobs:
|
||||
If you believe this was flagged incorrectly, please let a maintainer know.
|
||||
|
||||
Remember: post at most ONE comment combining all findings. If everything is fine, post nothing."
|
||||
|
||||
recheck-compliance:
|
||||
if: github.event.action == 'edited' && contains(github.event.issue.labels.*.name, 'needs:compliance')
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
permissions:
|
||||
contents: read
|
||||
issues: write
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
- uses: ./.github/actions/setup-bun
|
||||
|
||||
- name: Install opencode
|
||||
run: curl -fsSL https://opencode.ai/install | bash
|
||||
|
||||
- name: Recheck compliance
|
||||
env:
|
||||
OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
OPENCODE_PERMISSION: |
|
||||
{
|
||||
"bash": {
|
||||
"*": "deny",
|
||||
"gh issue*": "allow"
|
||||
},
|
||||
"webfetch": "deny"
|
||||
}
|
||||
run: |
|
||||
opencode run -m opencode/claude-sonnet-4-6 "Issue #${{ github.event.issue.number }} was previously flagged as non-compliant and has been edited.
|
||||
|
||||
Lookup this issue with gh issue view ${{ github.event.issue.number }}.
|
||||
|
||||
Re-check whether the issue now follows our contributing guidelines and issue templates.
|
||||
|
||||
This project has three issue templates that every issue MUST use one of:
|
||||
|
||||
1. Bug Report - requires a Description field with real content
|
||||
2. Feature Request - requires a verification checkbox and description, title should start with [FEATURE]:
|
||||
3. Question - requires the Question field with real content
|
||||
|
||||
Additionally check:
|
||||
- No AI-generated walls of text (long, AI-generated descriptions are not acceptable)
|
||||
- The issue has real content, not just template placeholder text left unchanged
|
||||
- Bug reports should include some context about how to reproduce
|
||||
- Feature requests should explain the problem or need
|
||||
- We want to push for having the user provide system description & information
|
||||
|
||||
Do NOT be nitpicky about optional fields. Only flag real problems like: no template used, required fields empty or placeholder text only, obviously AI-generated walls of text, or completely empty/nonsensical content.
|
||||
|
||||
If the issue is NOW compliant:
|
||||
1. Remove the needs:compliance label: gh issue edit ${{ github.event.issue.number }} --remove-label needs:compliance
|
||||
2. Find and delete the previous compliance comment (the one containing <!-- issue-compliance -->) using: gh api repos/${{ github.repository }}/issues/${{ github.event.issue.number }}/comments --jq '.[] | select(.body | contains(\"<!-- issue-compliance -->\")) | .id' then delete it with: gh api -X DELETE repos/${{ github.repository }}/issues/${{ github.event.issue.number }}/comments/{id}
|
||||
3. Post a short comment thanking them for updating the issue.
|
||||
|
||||
If the issue is STILL not compliant:
|
||||
Post a comment explaining what still needs to be fixed. Keep the needs:compliance label."
|
||||
|
||||
27
.github/workflows/pr-management.yml
vendored
27
.github/workflows/pr-management.yml
vendored
@@ -6,17 +6,6 @@ on:
|
||||
|
||||
jobs:
|
||||
check-duplicates:
|
||||
if: |
|
||||
github.event.pull_request.user.login != 'actions-user' &&
|
||||
github.event.pull_request.user.login != 'opencode' &&
|
||||
github.event.pull_request.user.login != 'rekram1-node' &&
|
||||
github.event.pull_request.user.login != 'thdxr' &&
|
||||
github.event.pull_request.user.login != 'kommander' &&
|
||||
github.event.pull_request.user.login != 'jayair' &&
|
||||
github.event.pull_request.user.login != 'fwang' &&
|
||||
github.event.pull_request.user.login != 'adamdotdevin' &&
|
||||
github.event.pull_request.user.login != 'iamdavidhill' &&
|
||||
github.event.pull_request.user.login != 'opencode-agent[bot]'
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -27,16 +16,31 @@ jobs:
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Check team membership
|
||||
id: team-check
|
||||
run: |
|
||||
LOGIN="${{ github.event.pull_request.user.login }}"
|
||||
if [ "$LOGIN" = "opencode-agent[bot]" ] || grep -qxF "$LOGIN" .github/TEAM_MEMBERS; then
|
||||
echo "is_team=true" >> "$GITHUB_OUTPUT"
|
||||
echo "Skipping: $LOGIN is a team member or bot"
|
||||
else
|
||||
echo "is_team=false" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
- name: Setup Bun
|
||||
if: steps.team-check.outputs.is_team != 'true'
|
||||
uses: ./.github/actions/setup-bun
|
||||
|
||||
- name: Install dependencies
|
||||
if: steps.team-check.outputs.is_team != 'true'
|
||||
run: bun install
|
||||
|
||||
- name: Install opencode
|
||||
if: steps.team-check.outputs.is_team != 'true'
|
||||
run: curl -fsSL https://opencode.ai/install | bash
|
||||
|
||||
- name: Build prompt
|
||||
if: steps.team-check.outputs.is_team != 'true'
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||
@@ -53,6 +57,7 @@ jobs:
|
||||
} > pr_info.txt
|
||||
|
||||
- name: Check for duplicate PRs
|
||||
if: steps.team-check.outputs.is_team != 'true'
|
||||
env:
|
||||
OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
218
.github/workflows/pr-standards.yml
vendored
218
.github/workflows/pr-standards.yml
vendored
@@ -6,19 +6,9 @@ on:
|
||||
|
||||
jobs:
|
||||
check-standards:
|
||||
if: |
|
||||
github.event.pull_request.user.login != 'actions-user' &&
|
||||
github.event.pull_request.user.login != 'opencode' &&
|
||||
github.event.pull_request.user.login != 'rekram1-node' &&
|
||||
github.event.pull_request.user.login != 'thdxr' &&
|
||||
github.event.pull_request.user.login != 'kommander' &&
|
||||
github.event.pull_request.user.login != 'jayair' &&
|
||||
github.event.pull_request.user.login != 'fwang' &&
|
||||
github.event.pull_request.user.login != 'adamdotdevin' &&
|
||||
github.event.pull_request.user.login != 'iamdavidhill' &&
|
||||
github.event.pull_request.user.login != 'opencode-agent[bot]'
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Check PR standards
|
||||
@@ -26,6 +16,22 @@ jobs:
|
||||
with:
|
||||
script: |
|
||||
const pr = context.payload.pull_request;
|
||||
const login = pr.user.login;
|
||||
|
||||
// Check if author is a team member or bot
|
||||
if (login === 'opencode-agent[bot]') return;
|
||||
const { data: file } = await github.rest.repos.getContent({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
path: '.github/TEAM_MEMBERS',
|
||||
ref: 'dev'
|
||||
});
|
||||
const members = Buffer.from(file.content, 'base64').toString().split('\n').map(l => l.trim()).filter(Boolean);
|
||||
if (members.includes(login)) {
|
||||
console.log(`Skipping: ${login} is a team member`);
|
||||
return;
|
||||
}
|
||||
|
||||
const title = pr.title;
|
||||
|
||||
async function addLabel(label) {
|
||||
@@ -137,3 +143,193 @@ jobs:
|
||||
|
||||
await removeLabel('needs:issue');
|
||||
console.log('PR meets all standards');
|
||||
|
||||
check-compliance:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Check PR template compliance
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const pr = context.payload.pull_request;
|
||||
const login = pr.user.login;
|
||||
|
||||
// Check if author is a team member or bot
|
||||
if (login === 'opencode-agent[bot]') return;
|
||||
const { data: file } = await github.rest.repos.getContent({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
path: '.github/TEAM_MEMBERS',
|
||||
ref: 'dev'
|
||||
});
|
||||
const members = Buffer.from(file.content, 'base64').toString().split('\n').map(l => l.trim()).filter(Boolean);
|
||||
if (members.includes(login)) {
|
||||
console.log(`Skipping: ${login} is a team member`);
|
||||
return;
|
||||
}
|
||||
|
||||
const body = pr.body || '';
|
||||
const title = pr.title;
|
||||
const isDocsOrRefactor = /^(docs|refactor)\s*(\([a-zA-Z0-9-]+\))?\s*:/.test(title);
|
||||
|
||||
const issues = [];
|
||||
|
||||
// Check: template sections exist
|
||||
const hasWhatSection = /### What does this PR do\?/.test(body);
|
||||
const hasTypeSection = /### Type of change/.test(body);
|
||||
const hasVerifySection = /### How did you verify your code works\?/.test(body);
|
||||
const hasChecklistSection = /### Checklist/.test(body);
|
||||
const hasIssueSection = /### Issue for this PR/.test(body);
|
||||
|
||||
if (!hasWhatSection || !hasTypeSection || !hasVerifySection || !hasChecklistSection || !hasIssueSection) {
|
||||
issues.push('PR description is missing required template sections. Please use the [PR template](../blob/dev/.github/pull_request_template.md).');
|
||||
}
|
||||
|
||||
// Check: "What does this PR do?" has real content (not just placeholder text)
|
||||
if (hasWhatSection) {
|
||||
const whatMatch = body.match(/### What does this PR do\?\s*\n([\s\S]*?)(?=###|$)/);
|
||||
const whatContent = whatMatch ? whatMatch[1].trim() : '';
|
||||
const placeholder = 'Please provide a description of the issue';
|
||||
const onlyPlaceholder = whatContent.includes(placeholder) && whatContent.replace(placeholder, '').replace(/[*\s]/g, '').length < 20;
|
||||
if (!whatContent || onlyPlaceholder) {
|
||||
issues.push('"What does this PR do?" section is empty or only contains placeholder text. Please describe your changes.');
|
||||
}
|
||||
}
|
||||
|
||||
// Check: at least one "Type of change" checkbox is checked
|
||||
if (hasTypeSection) {
|
||||
const typeMatch = body.match(/### Type of change\s*\n([\s\S]*?)(?=###|$)/);
|
||||
const typeContent = typeMatch ? typeMatch[1] : '';
|
||||
const hasCheckedBox = /- \[x\]/i.test(typeContent);
|
||||
if (!hasCheckedBox) {
|
||||
issues.push('No "Type of change" checkbox is checked. Please select at least one.');
|
||||
}
|
||||
}
|
||||
|
||||
// Check: issue reference (skip for docs/refactor)
|
||||
if (!isDocsOrRefactor && hasIssueSection) {
|
||||
const issueMatch = body.match(/### Issue for this PR\s*\n([\s\S]*?)(?=###|$)/);
|
||||
const issueContent = issueMatch ? issueMatch[1].trim() : '';
|
||||
const hasIssueRef = /(closes|fixes|resolves)\s+#\d+/i.test(issueContent) || /#\d+/.test(issueContent);
|
||||
if (!hasIssueRef) {
|
||||
issues.push('No issue referenced. Please add `Closes #<number>` linking to the relevant issue.');
|
||||
}
|
||||
}
|
||||
|
||||
// Check: "How did you verify" has content
|
||||
if (hasVerifySection) {
|
||||
const verifyMatch = body.match(/### How did you verify your code works\?\s*\n([\s\S]*?)(?=###|$)/);
|
||||
const verifyContent = verifyMatch ? verifyMatch[1].trim() : '';
|
||||
if (!verifyContent) {
|
||||
issues.push('"How did you verify your code works?" section is empty. Please explain how you tested.');
|
||||
}
|
||||
}
|
||||
|
||||
// Check: checklist boxes are checked
|
||||
if (hasChecklistSection) {
|
||||
const checklistMatch = body.match(/### Checklist\s*\n([\s\S]*?)(?=###|$)/);
|
||||
const checklistContent = checklistMatch ? checklistMatch[1] : '';
|
||||
const unchecked = (checklistContent.match(/- \[ \]/g) || []).length;
|
||||
const checked = (checklistContent.match(/- \[x\]/gi) || []).length;
|
||||
if (checked < 2) {
|
||||
issues.push('Not all checklist items are checked. Please confirm you have tested locally and have not included unrelated changes.');
|
||||
}
|
||||
}
|
||||
|
||||
// Helper functions
|
||||
async function addLabel(label) {
|
||||
await github.rest.issues.addLabels({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: pr.number,
|
||||
labels: [label]
|
||||
});
|
||||
}
|
||||
|
||||
async function removeLabel(label) {
|
||||
try {
|
||||
await github.rest.issues.removeLabel({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: pr.number,
|
||||
name: label
|
||||
});
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
const hasComplianceLabel = pr.labels.some(l => l.name === 'needs:compliance');
|
||||
|
||||
if (issues.length > 0) {
|
||||
// Non-compliant
|
||||
if (!hasComplianceLabel) {
|
||||
await addLabel('needs:compliance');
|
||||
}
|
||||
|
||||
const marker = '<!-- issue-compliance -->';
|
||||
const { data: comments } = await github.rest.issues.listComments({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: pr.number
|
||||
});
|
||||
const existing = comments.find(c => c.body.includes(marker));
|
||||
|
||||
const body_text = `${marker}
|
||||
This PR doesn't fully meet our [contributing guidelines](../blob/dev/CONTRIBUTING.md) and [PR template](../blob/dev/.github/pull_request_template.md).
|
||||
|
||||
**What needs to be fixed:**
|
||||
${issues.map(i => `- ${i}`).join('\n')}
|
||||
|
||||
Please edit this PR description to address the above within **2 hours**, or it will be automatically closed.
|
||||
|
||||
If you believe this was flagged incorrectly, please let a maintainer know.`;
|
||||
|
||||
if (existing) {
|
||||
await github.rest.issues.updateComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
comment_id: existing.id,
|
||||
body: body_text
|
||||
});
|
||||
} else {
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: pr.number,
|
||||
body: body_text
|
||||
});
|
||||
}
|
||||
|
||||
console.log(`PR #${pr.number} is non-compliant: ${issues.join(', ')}`);
|
||||
} else if (hasComplianceLabel) {
|
||||
// Was non-compliant, now fixed
|
||||
await removeLabel('needs:compliance');
|
||||
|
||||
const { data: comments } = await github.rest.issues.listComments({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: pr.number
|
||||
});
|
||||
const marker = '<!-- issue-compliance -->';
|
||||
const existing = comments.find(c => c.body.includes(marker));
|
||||
if (existing) {
|
||||
await github.rest.issues.deleteComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
comment_id: existing.id
|
||||
});
|
||||
}
|
||||
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: pr.number,
|
||||
body: 'Thanks for updating your PR! It now meets our contributing guidelines. :+1:'
|
||||
});
|
||||
|
||||
console.log(`PR #${pr.number} is now compliant, label removed`);
|
||||
} else {
|
||||
console.log(`PR #${pr.number} is compliant`);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user