Something went wrong. Try again.
opencode v2 mirror
Something went wrong. Try again.
YAML
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361name: pr-standards
on: pull_request_target: types: [opened, edited, synchronize]
jobs: check-standards: runs-on: ubuntu-latest permissions: contents: read pull-requests: write steps: - name: Check PR standards uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0 with: script: | const pr = context.payload.pull_request; const login = pr.user.login;
// Skip PRs older than Feb 18, 2026 at 6PM EST (Feb 19, 2026 00:00 UTC) const cutoff = new Date('2026-02-19T00:00:00Z'); const prCreated = new Date(pr.created_at); if (prCreated < cutoff) { console.log(`Skipping: PR #${pr.number} was created before cutoff (${prCreated.toISOString()})`); return; }
// 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) { 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) { // Label wasn't present, ignore } }
async function comment(marker, body) { const markerText = `<!-- pr-standards:${marker} -->`; 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(markerText)); if (existing) return; await github.rest.issues.createComment({ owner: context.repo.owner, repo: context.repo.repo, issue_number: pr.number, body: markerText + '\n' + body }); }
// Step 1: Check title format // Matches: feat:, feat(scope):, feat (scope):, etc. const titlePattern = /^(feat|fix|docs|chore|refactor|test)\s*(\([a-zA-Z0-9-]+\))?\s*:/; const hasValidTitle = titlePattern.test(title);
if (!hasValidTitle) { await addLabel('needs:title'); await comment('title', `Hey! Your PR title \`${title}\` doesn't follow conventional commit format.
Please update it to start with one of: - \`feat:\` or \`feat(scope):\` new feature - \`fix:\` or \`fix(scope):\` bug fix - \`docs:\` or \`docs(scope):\` documentation changes - \`chore:\` or \`chore(scope):\` maintenance tasks - \`refactor:\` or \`refactor(scope):\` code refactoring - \`test:\` or \`test(scope):\` adding or updating tests
Where \`scope\` is the package name (e.g., \`app\`, \`desktop\`, \`opencode\`).
See [CONTRIBUTING.md](../blob/dev/CONTRIBUTING.md#pr-titles) for details.`); return; }
await removeLabel('needs:title');
// Step 2: Check for linked issue (skip for docs/refactor/feat PRs) const skipIssueCheck = /^(docs|refactor|feat)\s*(\([a-zA-Z0-9-]+\))?\s*:/.test(title); if (skipIssueCheck) { await removeLabel('needs:issue'); console.log('Skipping issue check for docs/refactor/feat PR'); return; } const query = ` query($owner: String!, $repo: String!, $number: Int!) { repository(owner: $owner, name: $repo) { pullRequest(number: $number) { closingIssuesReferences(first: 1) { totalCount } } } } `;
const result = await github.graphql(query, { owner: context.repo.owner, repo: context.repo.repo, number: pr.number });
const linkedIssues = result.repository.pullRequest.closingIssuesReferences.totalCount;
// GitHub only populates closingIssuesReferences when a PR targets the repository's // default branch (dev). PRs targeting other branches like v2 always return totalCount 0. // Fall back to checking the PR description for closing keywords (e.g. Closes #123). const body = pr.body || ''; const issueMatch = body.match(/### Issue for this PR\s*\n([\s\S]*?)(?=###|$)/); const issueContent = issueMatch ? issueMatch[1].trim() : body; const hasBodyIssueRef = /(closes|fixes|resolves)\s+#\d+/i.test(issueContent) || /#\d+/.test(issueContent); const hasLinkedIssue = linkedIssues > 0 || hasBodyIssueRef;
if (!hasLinkedIssue) { await addLabel('needs:issue'); await comment('issue', `Thanks for your contribution!
This PR doesn't have a linked issue. All PRs must reference an existing issue.
Please: 1. Open an issue describing the bug/feature (if one doesn't exist) 2. Add \`Fixes #<number>\` or \`Closes #<number>\` to this PR description
See [CONTRIBUTING.md](../blob/dev/CONTRIBUTING.md#issue-first-policy) for details.`); return; }
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@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0 with: script: | const pr = context.payload.pull_request; const login = pr.user.login;
// Skip PRs older than Feb 18, 2026 at 6PM EST (Feb 19, 2026 00:00 UTC) const cutoff = new Date('2026-02-19T00:00:00Z'); const prCreated = new Date(pr.created_at); if (prCreated < cutoff) { console.log(`Skipping: PR #${pr.number} was created before cutoff (${prCreated.toISOString()})`); return; }
// 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 isDocsRefactorOrFeat = /^(docs|refactor|feat)\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/feat) if (!isDocsRefactorOrFeat && 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`); }