summaryrefslogtreecommitdiffhomepage
path: root/.github/workflows
diff options
context:
space:
mode:
authorRyan Vogel <[email protected]>2026-02-09 18:15:06 -0500
committerGitHub <[email protected]>2026-02-09 18:15:06 -0500
commit3118cab2d823920c507d82fa3e5120ddda951e12 (patch)
treefe60a0c300f0f3ecfbd8b4c8f17babbe2424fb0e /.github/workflows
parent31f893f8cb7cbec11ae743b4ead806c201a396b7 (diff)
downloadopencode-3118cab2d823920c507d82fa3e5120ddda951e12.tar.gz
opencode-3118cab2d823920c507d82fa3e5120ddda951e12.zip
feat: integrate vouch & stricter issue trust management system (#12640)
Diffstat (limited to '.github/workflows')
-rw-r--r--.github/workflows/compliance-close.yml86
-rw-r--r--.github/workflows/duplicate-issues.yml82
-rw-r--r--.github/workflows/vouch-check-issue.yml96
-rw-r--r--.github/workflows/vouch-check-pr.yml93
-rw-r--r--.github/workflows/vouch-manage-by-issue.yml27
5 files changed, 370 insertions, 14 deletions
diff --git a/.github/workflows/compliance-close.yml b/.github/workflows/compliance-close.yml
new file mode 100644
index 000000000..5b424d0ad
--- /dev/null
+++ b/.github/workflows/compliance-close.yml
@@ -0,0 +1,86 @@
+name: compliance-close
+
+on:
+ schedule:
+ # Run every 30 minutes to check for expired compliance windows
+ - cron: "*/30 * * * *"
+ workflow_dispatch:
+
+permissions:
+ contents: read
+ issues: write
+ pull-requests: write
+
+jobs:
+ close-non-compliant:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Close non-compliant issues and PRs after 2 hours
+ uses: actions/github-script@v7
+ with:
+ script: |
+ const { data: items } = await github.rest.issues.listForRepo({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ labels: 'needs:compliance',
+ state: 'open',
+ per_page: 100,
+ });
+
+ if (items.length === 0) {
+ core.info('No open issues/PRs with needs:compliance label');
+ return;
+ }
+
+ const now = Date.now();
+ const twoHours = 2 * 60 * 60 * 1000;
+
+ for (const item of items) {
+ const isPR = !!item.pull_request;
+ const kind = isPR ? 'PR' : 'issue';
+
+ const { data: comments } = await github.rest.issues.listComments({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ issue_number: item.number,
+ });
+
+ const complianceComment = comments.find(c => c.body.includes('<!-- issue-compliance -->'));
+ if (!complianceComment) continue;
+
+ const commentAge = now - new Date(complianceComment.created_at).getTime();
+ if (commentAge < twoHours) {
+ core.info(`${kind} #${item.number} still within 2-hour window (${Math.round(commentAge / 60000)}m elapsed)`);
+ continue;
+ }
+
+ const closeMessage = isPR
+ ? 'This pull request has been automatically closed because it was not updated to meet our [contributing guidelines](../blob/dev/CONTRIBUTING.md) within the 2-hour window.\n\nFeel free to open a new pull request that follows our guidelines.'
+ : 'This issue has been automatically closed because it was not updated to meet our [contributing guidelines](../blob/dev/CONTRIBUTING.md) within the 2-hour window.\n\nFeel free to open a new issue that follows our issue templates.';
+
+ await github.rest.issues.createComment({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ issue_number: item.number,
+ body: closeMessage,
+ });
+
+ if (isPR) {
+ await github.rest.pulls.update({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ pull_number: item.number,
+ state: 'closed',
+ });
+ } else {
+ await github.rest.issues.update({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ issue_number: item.number,
+ state: 'closed',
+ state_reason: 'not_planned',
+ });
+ }
+
+ core.info(`Closed non-compliant ${kind} #${item.number} after 2-hour window`);
+ }
diff --git a/.github/workflows/duplicate-issues.yml b/.github/workflows/duplicate-issues.yml
index cbe8df517..87e655fe4 100644
--- a/.github/workflows/duplicate-issues.yml
+++ b/.github/workflows/duplicate-issues.yml
@@ -21,7 +21,7 @@ jobs:
- name: Install opencode
run: curl -fsSL https://opencode.ai/install | bash
- - name: Check for duplicate issues
+ - name: Check duplicates and compliance
env:
OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -34,30 +34,84 @@ jobs:
"webfetch": "deny"
}
run: |
- opencode run -m opencode/claude-haiku-4-5 "A new issue has been created:'
+ opencode run -m opencode/claude-haiku-4-5 "A new issue has been created:
- Issue number:
- ${{ github.event.issue.number }}
+ Issue number: ${{ github.event.issue.number }}
- Lookup this issue and search through existing issues (excluding #${{ github.event.issue.number }}) in this repository to find any potential duplicates of this new issue.
+ Lookup this issue with gh issue view ${{ github.event.issue.number }}.
+
+ You have TWO tasks. Perform both, then post a SINGLE comment (if needed).
+
+ ---
+
+ TASK 1: CONTRIBUTING GUIDELINES COMPLIANCE CHECK
+
+ Check whether the issue 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.
+
+ ---
+
+ TASK 2: DUPLICATE CHECK
+
+ Search through existing issues (excluding #${{ github.event.issue.number }}) to find potential duplicates.
Consider:
1. Similar titles or descriptions
2. Same error messages or symptoms
3. Related functionality or components
4. Similar feature requests
- If you find any potential duplicates, please comment on the new issue with:
- - A brief explanation of why it might be a duplicate
- - Links to the potentially duplicate issues
- - A suggestion to check those issues first
+ Additionally, if the issue mentions keybinds, keyboard shortcuts, or key bindings, note the pinned keybinds issue #4997.
+
+ ---
+
+ POSTING YOUR COMMENT:
+
+ Based on your findings, post a SINGLE comment on issue #${{ github.event.issue.number }}. Build the comment as follows:
+
+ If the issue is NOT compliant, start the comment with:
+ <!-- issue-compliance -->
+ Then explain what needs to be fixed and that they have 2 hours to edit the issue before it is automatically closed. Also add the label needs:compliance to the issue using: gh issue edit ${{ github.event.issue.number }} --add-label needs:compliance
+
+ If duplicates were found, include a section about potential duplicates with links.
+
+ If the issue mentions keybinds/keyboard shortcuts, include a note about #4997.
+
+ If the issue IS compliant AND no duplicates were found AND no keybind reference, do NOT comment at all.
Use this format for the comment:
- 'This issue might be a duplicate of existing issues. Please check:
+
+ [If not compliant:]
+ <!-- issue-compliance -->
+ This issue doesn't fully meet our [contributing guidelines](../blob/dev/CONTRIBUTING.md).
+
+ **What needs to be fixed:**
+ - [specific reasons]
+
+ Please edit this issue to address the above within **2 hours**, or it will be automatically closed.
+
+ [If duplicates found, add:]
+ ---
+ This issue might be a duplicate of existing issues. Please check:
- #[issue_number]: [brief description of similarity]
- Feel free to ignore if none of these address your specific case.'
+ [If keybind-related, add:]
+ For keybind-related issues, please also check our pinned keybinds documentation: #4997
- Additionally, if the issue mentions keybinds, keyboard shortcuts, or key bindings, please add a comment mentioning the pinned keybinds issue #4997:
- 'For keybind-related issues, please also check our pinned keybinds documentation: #4997'
+ [End with if not compliant:]
+ If you believe this was flagged incorrectly, please let a maintainer know.
- If no clear duplicates are found, do not comment."
+ Remember: post at most ONE comment combining all findings. If everything is fine, post nothing."
diff --git a/.github/workflows/vouch-check-issue.yml b/.github/workflows/vouch-check-issue.yml
new file mode 100644
index 000000000..94569f473
--- /dev/null
+++ b/.github/workflows/vouch-check-issue.yml
@@ -0,0 +1,96 @@
+name: vouch-check-issue
+
+on:
+ issues:
+ types: [opened]
+
+permissions:
+ contents: read
+ issues: write
+
+jobs:
+ check:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check if issue author is denounced
+ uses: actions/github-script@v7
+ with:
+ script: |
+ const author = context.payload.issue.user.login;
+ const issueNumber = context.payload.issue.number;
+
+ // Skip bots
+ if (author.endsWith('[bot]')) {
+ core.info(`Skipping bot: ${author}`);
+ return;
+ }
+
+ // Read the VOUCHED.td file via API (no checkout needed)
+ let content;
+ try {
+ const response = await github.rest.repos.getContent({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ path: '.github/VOUCHED.td',
+ });
+ content = Buffer.from(response.data.content, 'base64').toString('utf-8');
+ } catch (error) {
+ if (error.status === 404) {
+ core.info('No .github/VOUCHED.td file found, skipping check.');
+ return;
+ }
+ throw error;
+ }
+
+ // Parse the .td file for denounced users
+ const denounced = new Map();
+ for (const line of content.split('\n')) {
+ const trimmed = line.trim();
+ if (!trimmed || trimmed.startsWith('#')) continue;
+ if (!trimmed.startsWith('-')) continue;
+
+ const rest = trimmed.slice(1).trim();
+ if (!rest) continue;
+ const spaceIdx = rest.indexOf(' ');
+ const handle = spaceIdx === -1 ? rest : rest.slice(0, spaceIdx);
+ const reason = spaceIdx === -1 ? null : rest.slice(spaceIdx + 1).trim();
+
+ // Handle platform:username or bare username
+ // Only match bare usernames or github: prefix (skip other platforms)
+ const colonIdx = handle.indexOf(':');
+ if (colonIdx !== -1) {
+ const platform = handle.slice(0, colonIdx).toLowerCase();
+ if (platform !== 'github') continue;
+ }
+ const username = colonIdx === -1 ? handle : handle.slice(colonIdx + 1);
+ if (!username) continue;
+
+ denounced.set(username.toLowerCase(), reason);
+ }
+
+ // Check if the author is denounced
+ const reason = denounced.get(author.toLowerCase());
+ if (reason === undefined) {
+ core.info(`User ${author} is not denounced. Allowing issue.`);
+ return;
+ }
+
+ // Author is denounced — close the issue
+ const body = 'This issue has been automatically closed.';
+
+ await github.rest.issues.createComment({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ issue_number: issueNumber,
+ body,
+ });
+
+ await github.rest.issues.update({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ issue_number: issueNumber,
+ state: 'closed',
+ state_reason: 'not_planned',
+ });
+
+ core.info(`Closed issue #${issueNumber} from denounced user ${author}`);
diff --git a/.github/workflows/vouch-check-pr.yml b/.github/workflows/vouch-check-pr.yml
new file mode 100644
index 000000000..470b8e0a5
--- /dev/null
+++ b/.github/workflows/vouch-check-pr.yml
@@ -0,0 +1,93 @@
+name: vouch-check-pr
+
+on:
+ pull_request_target:
+ types: [opened]
+
+permissions:
+ contents: read
+ pull-requests: write
+
+jobs:
+ check:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check if PR author is denounced
+ uses: actions/github-script@v7
+ with:
+ script: |
+ const author = context.payload.pull_request.user.login;
+ const prNumber = context.payload.pull_request.number;
+
+ // Skip bots
+ if (author.endsWith('[bot]')) {
+ core.info(`Skipping bot: ${author}`);
+ return;
+ }
+
+ // Read the VOUCHED.td file via API (no checkout needed)
+ let content;
+ try {
+ const response = await github.rest.repos.getContent({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ path: '.github/VOUCHED.td',
+ });
+ content = Buffer.from(response.data.content, 'base64').toString('utf-8');
+ } catch (error) {
+ if (error.status === 404) {
+ core.info('No .github/VOUCHED.td file found, skipping check.');
+ return;
+ }
+ throw error;
+ }
+
+ // Parse the .td file for denounced users
+ const denounced = new Map();
+ for (const line of content.split('\n')) {
+ const trimmed = line.trim();
+ if (!trimmed || trimmed.startsWith('#')) continue;
+ if (!trimmed.startsWith('-')) continue;
+
+ const rest = trimmed.slice(1).trim();
+ if (!rest) continue;
+ const spaceIdx = rest.indexOf(' ');
+ const handle = spaceIdx === -1 ? rest : rest.slice(0, spaceIdx);
+ const reason = spaceIdx === -1 ? null : rest.slice(spaceIdx + 1).trim();
+
+ // Handle platform:username or bare username
+ // Only match bare usernames or github: prefix (skip other platforms)
+ const colonIdx = handle.indexOf(':');
+ if (colonIdx !== -1) {
+ const platform = handle.slice(0, colonIdx).toLowerCase();
+ if (platform !== 'github') continue;
+ }
+ const username = colonIdx === -1 ? handle : handle.slice(colonIdx + 1);
+ if (!username) continue;
+
+ denounced.set(username.toLowerCase(), reason);
+ }
+
+ // Check if the author is denounced
+ const reason = denounced.get(author.toLowerCase());
+ if (reason === undefined) {
+ core.info(`User ${author} is not denounced. Allowing PR.`);
+ return;
+ }
+
+ // Author is denounced — close the PR
+ await github.rest.issues.createComment({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ issue_number: prNumber,
+ body: 'This pull request has been automatically closed.',
+ });
+
+ await github.rest.pulls.update({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ pull_number: prNumber,
+ state: 'closed',
+ });
+
+ core.info(`Closed PR #${prNumber} from denounced user ${author}`);
diff --git a/.github/workflows/vouch-manage-by-issue.yml b/.github/workflows/vouch-manage-by-issue.yml
new file mode 100644
index 000000000..863a8a846
--- /dev/null
+++ b/.github/workflows/vouch-manage-by-issue.yml
@@ -0,0 +1,27 @@
+name: vouch-manage-by-issue
+
+on:
+ issue_comment:
+ types: [created]
+
+concurrency:
+ group: vouch-manage
+ cancel-in-progress: false
+
+permissions:
+ contents: write
+ issues: write
+ pull-requests: read
+
+jobs:
+ manage:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+
+ - uses: mitchellh/vouch/action/manage-by-issue@main
+ with:
+ issue-id: ${{ github.event.issue.number }}
+ comment-id: ${{ github.event.comment.id }}
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}