From a1864729f6abfb3d6d37dd257c4e4b2221f1b911 Mon Sep 17 00:00:00 2001 From: Drewry Pope Date: Thu, 29 Oct 2026 00:54:56 -0500 Subject: [PATCH] ci: name the basic tier as the merge requirement; consolidate event bots into repo-sweeper Merge-requirement naming (Drew, 2026-10-29): - ci-policy.md: explicit statement -- Merge requirement: the basic-validation (PR) tier only. The full suite runs on main and is NOT a merge gate. - PR-tier job display names now carry the tier: basic typecheck (check), basic affected packages + basic unit (test). Workflow names test/check/nix-eval are unchanged because main-broken.yml references them via workflow_run. e2e () aggregator names are unchanged so branch-required check names stay stable. Event-bot consolidation (Drew, 2026-10-29; supersedes the Phase 1 per-item concurrency treatment for management bots): - Deleted: pr-management, duplicate-issues, triage, close-issues, close-prs, compliance-close. - Added repo-sweeper.yml: every 15 minutes, one live run (cancel-in-progress: true), all management duties in a single pass -- contributor labels and the 72h compliance closes (deterministic, ported), daily stale-issue (02:00 UTC) and low-reaction-PR (22:00 UTC) closers gated to their original windows, and capped LLM passes (new-issue compliance+duplicates then triage, compliance rechecks, duplicate-PR check) over a 20-minute lookback with the original prompts and exemptions. - Kept event-based: opencode (/oc//opencode mention bot) and review (/review) -- a human invokes them and waits. pr-standards stays: it is a validation workflow (PR standards/template compliance checks), not management. Refs developing-today/code#1780, developing-today/code#1781, developing-today/code#1782, developing-today/code#1599. --- .github/ci-policy.md | 40 ++- .github/workflows/check.yml | 2 +- .github/workflows/close-issues.yml | 30 -- .github/workflows/close-prs.yml | 55 ---- .github/workflows/compliance-close.yml | 139 -------- .github/workflows/duplicate-issues.yml | 235 -------------- .github/workflows/pr-management.yml | 107 ------- .github/workflows/repo-sweeper.yml | 420 +++++++++++++++++++++++++ .github/workflows/test.yml | 4 +- .github/workflows/triage.yml | 68 ---- 10 files changed, 458 insertions(+), 642 deletions(-) delete mode 100644 .github/workflows/close-issues.yml delete mode 100644 .github/workflows/close-prs.yml delete mode 100644 .github/workflows/compliance-close.yml delete mode 100644 .github/workflows/duplicate-issues.yml delete mode 100644 .github/workflows/pr-management.yml create mode 100644 .github/workflows/repo-sweeper.yml delete mode 100644 .github/workflows/triage.yml diff --git a/.github/ci-policy.md b/.github/ci-policy.md index a1025605f29e..63a66b0b2355 100644 --- a/.github/ci-policy.md +++ b/.github/ci-policy.md @@ -19,6 +19,28 @@ Highest class wins when a change spans classes. | 3 — elevated | lockfiles, `go.mod`/`go.sum` roots, `flake.nix`/`flake.lock`, build definitions, `.github/workflows/**`, shared/core libraries | class 2 + compile check of affected packages | | 4 — nix expressions | `**.nix` | class 1 + parse/eval of the changed files only (never full flake check on a PR) | +## Merge requirement + +**Merge requirement: the basic-validation (PR) tier only. The full +suite runs on main and is NOT a merge gate.** + +The basic tier's checks are named so the tier is recognizable in the +check list — they carry the `basic` prefix: + +- `basic typecheck` (workflow `check`) — typecheck + changed-file lint +- `basic affected packages` (workflow `test`) — affected-package detection +- `basic unit ()` (workflow `test`) — unit tests of affected packages +- `e2e ()` (workflow `test`) — the aggregator only on PRs: it + reports green when the e2e suite is tier-skipped, so the name stays + stable for branch rules; the suite itself is main-tier + +Workflow names (`test`, `check`, `nix-eval`) are deliberately NOT +renamed: the Phase 3 watcher (`main-broken.yml`) references them by +name in its `workflow_run` trigger. Job display names carry the tier +label instead. If branch rules ever require checks by name, require +the `basic`-prefixed names above — the full main-tier jobs (`build`, +`e2e` suites) must never become merge requirements. + ## How the classes map to this repo's workflows - `check` (typecheck + changed-file lint) — the PR tier's core; runs on @@ -58,11 +80,19 @@ concurrency: Publish/release workflows extend the `run_id` keying to tag pushes and manual dispatches, so a release is never cancelled mid-publish. -Issue/PR bot workflows (`duplicate-issues`, `triage`, `review`, -`opencode`, `pr-management`, `pr-standards`) keep per-issue/per-PR -groups with `queue: max` and `cancel-in-progress: false` so bursts -queue per item instead of replacing each other. Scheduled singleton -and deploy workflows keep `cancel-in-progress: false` deliberately. +Issue/PR management is NOT event-triggered: `repo-sweeper.yml` runs +every 15 minutes (`*/15 * * * *`, single live run, cancel-in-progress) +and performs all management duties in one pass — contributor labels, +issue compliance + duplicate checks, compliance rechecks, triage, the +72-hour compliance closes, and the daily stale-issue / low-reaction-PR +closers (gated to their original 02:00 / 22:00 UTC windows). It +replaced `pr-management`, `duplicate-issues`, `triage`, `close-issues`, +`close-prs` and `compliance-close`. Event-based only where a human +waits synchronously: `opencode` (`/oc`, `/opencode` mentions) and +`review` (`/review` command). `pr-standards` stays a +`pull_request_target` validation workflow (PR standards / template +compliance). Scheduled singleton and deploy workflows keep +`cancel-in-progress: false` deliberately. Never pair `queue: max` with a `cancel-in-progress` expression that can evaluate true for `issues`/`pull_request_target` events — GitHub rejects that combination and the workflow never runs. diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index 8a8f3acf340e..99b0871fa834 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -22,7 +22,7 @@ permissions: jobs: check: timeout-minutes: 15 - name: typecheck + name: basic typecheck # RUNNER-SWITCH: Blacksmith (commented out 2026-10-18): runs-on: blacksmith-4vcpu-ubuntu-2404 runs-on: ubuntu-24.04 steps: diff --git a/.github/workflows/close-issues.yml b/.github/workflows/close-issues.yml deleted file mode 100644 index 600b771b6271..000000000000 --- a/.github/workflows/close-issues.yml +++ /dev/null @@ -1,30 +0,0 @@ -name: close-issues - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: false - queue: max - -on: - schedule: - - cron: "0 2 * * *" # Daily at 2:00 AM - workflow_dispatch: - -jobs: - close: - timeout-minutes: 10 - runs-on: ubuntu-latest - permissions: - contents: read - issues: write - steps: - - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 - - - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 - with: - bun-version-file: package.json - - - name: Close stale issues - env: - GITHUB_TOKEN: ${{ github.token }} - run: bun script/github/close-issues.ts diff --git a/.github/workflows/close-prs.yml b/.github/workflows/close-prs.yml deleted file mode 100644 index 520f792e7e10..000000000000 --- a/.github/workflows/close-prs.yml +++ /dev/null @@ -1,55 +0,0 @@ -name: close-prs - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: false - queue: max - -on: - schedule: - - cron: "0 22 * * *" # Daily at 10:00 PM UTC - workflow_dispatch: - inputs: - dry-run: - description: "Log matching PRs without closing them" - type: boolean - default: true - max-close: - description: "Maximum matching PRs to close" - type: string - required: false - default: "50" - -jobs: - close: - runs-on: ubuntu-latest - timeout-minutes: 240 - permissions: - contents: read - issues: write - pull-requests: write - steps: - - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 - - - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 - with: - bun-version-file: package.json - - - name: Close old PRs without enough positive reactions - env: - GITHUB_TOKEN: ${{ github.token }} - run: | - max_close="${{ inputs['max-close'] }}" - if [ -z "$max_close" ]; then - max_close="50" - fi - - args=("--threshold" "2" "--age-months" "1" "--sleep-ms" "20000" "--max-close" "$max_close") - - if [ "${{ github.event_name }}" = "schedule" ]; then - args+=("--execute") - elif [ "${{ inputs['dry-run'] }}" = "false" ]; then - args+=("--execute") - fi - - bun script/github/close-prs.ts "${args[@]}" diff --git a/.github/workflows/compliance-close.yml b/.github/workflows/compliance-close.yml deleted file mode 100644 index c2ca9ac5fec5..000000000000 --- a/.github/workflows/compliance-close.yml +++ /dev/null @@ -1,139 +0,0 @@ -name: compliance-close - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: false - queue: max - -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: - timeout-minutes: 10 - runs-on: ubuntu-latest - steps: - - name: Close non-compliant issues and PRs after 72 hours - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 - 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 seventyTwoHours = 72 * 60 * 60 * 1000; - const orgMemberAssociations = new Set(['OWNER', 'MEMBER']); - const agentLogin = 'opencode-agent[bot]'; - const { data: file } = await github.rest.repos.getContent({ - owner: context.repo.owner, - repo: context.repo.repo, - path: '.github/TEAM_MEMBERS', - ref: 'main', - }); - const teamMembers = new Set( - Buffer.from(file.content, 'base64') - .toString() - .split('\n') - .map((line) => line.trim().toLowerCase()) - .filter(Boolean) - ); - - function isExempt(item) { - const login = item.user?.login?.toLowerCase(); - return ( - login === agentLogin || - orgMemberAssociations.has(item.author_association) || - (login && teamMembers.has(login)) - ); - } - - for (const item of items) { - const isPR = !!item.pull_request; - const kind = isPR ? 'PR' : 'issue'; - const login = item.user?.login; - - if (isExempt(item)) { - core.info(`Skipping ${kind} #${item.number}; author ${login || 'unknown'} is exempt`); - try { - await github.rest.issues.removeLabel({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: item.number, - name: 'needs:compliance', - }); - } catch (e) {} - continue; - } - - 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('')); - if (!complianceComment) continue; - - const commentAge = now - new Date(complianceComment.created_at).getTime(); - if (commentAge < seventyTwoHours) { - core.info(`${kind} #${item.number} still within 72-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/main/CONTRIBUTING.md) within the 72-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/main/CONTRIBUTING.md) within the 72-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, - }); - - try { - await github.rest.issues.removeLabel({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: item.number, - name: 'needs:compliance', - }); - } catch (e) {} - - 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 72-hour window`); - } diff --git a/.github/workflows/duplicate-issues.yml b/.github/workflows/duplicate-issues.yml deleted file mode 100644 index c17c8aa2f416..000000000000 --- a/.github/workflows/duplicate-issues.yml +++ /dev/null @@ -1,235 +0,0 @@ -name: duplicate-issues -# RUNNER-SWITCH (2026-10-18): runners in this workflow were switched from Blacksmith to -# GitHub-hosted at Drew's direction. To switch back, grep the repo for RUNNER-SWITCH and -# swap each commented Blacksmith line back in for the active line under it. To choose per -# run instead, use an expression on runs-on, e.g. ${{ vars.RUNNER_UBUNTU || 'ubuntu-24.04' }}, -# or gate individual steps with if: on the runner. - -concurrency: - group: ${{ github.workflow }}-${{ github.event.issue.number || github.event.pull_request.number || github.ref }} - cancel-in-progress: false - queue: max - -on: - issues: - types: [opened, edited] - -jobs: - check-duplicates: - timeout-minutes: 10 - if: github.event.action == 'opened' - # RUNNER-SWITCH: Blacksmith (commented out 2026-10-18): runs-on: blacksmith-4vcpu-ubuntu-2404 - runs-on: ubuntu-24.04 - permissions: - contents: read - issues: write - steps: - - name: Checkout repository - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 - with: - fetch-depth: 1 - - - name: Check exempt issue author - id: author - run: | - LOGIN="${{ github.event.issue.user.login }}" - ASSOCIATION="${{ github.event.issue.author_association }}" - - if [ "$LOGIN" = "opencode-agent[bot]" ] || - [ "$ASSOCIATION" = "OWNER" ] || - [ "$ASSOCIATION" = "MEMBER" ] || - grep -qxiF "$LOGIN" .github/TEAM_MEMBERS; then - echo "skip=true" >> "$GITHUB_OUTPUT" - echo "Skipping issue automation for exempt author: $LOGIN ($ASSOCIATION)" - else - echo "skip=false" >> "$GITHUB_OUTPUT" - fi - - - uses: ./.github/actions/setup-bun - if: steps.author.outputs.skip != 'true' - - - name: Install opencode - if: steps.author.outputs.skip != 'true' - run: curl -fsSL https://opencode.ai/install | bash - - - name: Check duplicates and compliance - if: steps.author.outputs.skip != 'true' - 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 "A new issue has been created: - - Issue number: ${{ github.event.issue.number }} - Issue author association: ${{ github.event.issue.author_association }} - - 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. - - If the issue author association is OWNER or MEMBER, skip this compliance check. Do not add the needs:compliance label for organization-owned issues. - - 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 - - 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 and the author association is not OWNER or MEMBER, start the comment with: - - Then explain what needs to be fixed and that they have 72 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: - - [If not compliant:] - - 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 **72 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] - - [If keybind-related, add:] - 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. - - Remember: post at most ONE comment combining all findings. If everything is fine, post nothing." - - recheck-compliance: - timeout-minutes: 10 - if: github.event.action == 'edited' && contains(github.event.issue.labels.*.name, 'needs:compliance') - # RUNNER-SWITCH: Blacksmith (commented out 2026-10-18): runs-on: blacksmith-4vcpu-ubuntu-2404 - runs-on: ubuntu-24.04 - permissions: - contents: read - issues: write - steps: - - name: Checkout repository - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 - with: - fetch-depth: 1 - - - name: Check exempt issue author - id: author - run: | - LOGIN="${{ github.event.issue.user.login }}" - ASSOCIATION="${{ github.event.issue.author_association }}" - - if [ "$LOGIN" = "opencode-agent[bot]" ] || - [ "$ASSOCIATION" = "OWNER" ] || - [ "$ASSOCIATION" = "MEMBER" ] || - grep -qxiF "$LOGIN" .github/TEAM_MEMBERS; then - echo "skip=true" >> "$GITHUB_OUTPUT" - echo "Skipping issue automation for exempt author: $LOGIN ($ASSOCIATION)" - else - echo "skip=false" >> "$GITHUB_OUTPUT" - fi - - - uses: ./.github/actions/setup-bun - if: steps.author.outputs.skip != 'true' - - - name: Install opencode - if: steps.author.outputs.skip != 'true' - run: curl -fsSL https://opencode.ai/install | bash - - - name: Recheck compliance - if: steps.author.outputs.skip != 'true' - 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. - Issue author association: ${{ github.event.issue.author_association }} - - Lookup this issue with gh issue view ${{ github.event.issue.number }}. - - If the issue author association is OWNER or MEMBER, remove the needs:compliance label if present, delete the previous compliance comment if present, and do not post a new comment. - - 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 ) using: gh api repos/${{ github.repository }}/issues/${{ github.event.issue.number }}/comments --jq '.[] | select(.body | contains(\"\")) | .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." diff --git a/.github/workflows/pr-management.yml b/.github/workflows/pr-management.yml deleted file mode 100644 index c9a937b77016..000000000000 --- a/.github/workflows/pr-management.yml +++ /dev/null @@ -1,107 +0,0 @@ -name: pr-management -# RUNNER-SWITCH (2026-10-18): runners in this workflow were switched from Blacksmith to -# GitHub-hosted at Drew's direction. To switch back, grep the repo for RUNNER-SWITCH and -# swap each commented Blacksmith line back in for the active line under it. To choose per -# run instead, use an expression on runs-on, e.g. ${{ vars.RUNNER_UBUNTU || 'ubuntu-24.04' }}, -# or gate individual steps with if: on the runner. - -concurrency: - group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} - cancel-in-progress: true - -on: - pull_request_target: - types: [opened] - -jobs: - check-duplicates: - timeout-minutes: 10 - # RUNNER-SWITCH: Blacksmith (commented out 2026-10-18): runs-on: blacksmith-4vcpu-ubuntu-2404 - runs-on: ubuntu-24.04 - permissions: - contents: read - pull-requests: write - steps: - - name: Checkout repository - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 - 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 }} - run: | - { - echo "Check for duplicate PRs related to this new PR:" - echo "" - echo "CURRENT_PR_NUMBER: $PR_NUMBER" - echo "" - echo "Title: $(gh pr view "$PR_NUMBER" --json title --jq .title)" - echo "" - echo "Description:" - gh pr view "$PR_NUMBER" --json body --jq .body - } > 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 }} - PR_NUMBER: ${{ github.event.pull_request.number }} - run: | - COMMENT=$(bun script/duplicate-pr.ts -f pr_info.txt "Check the attached file for PR details and search for duplicates") - - if [ "$COMMENT" != "No duplicate PRs found" ]; then - gh pr comment "$PR_NUMBER" --body "_The following comment was made by an LLM, it may be inaccurate:_ - - $COMMENT" - fi - - add-contributor-label: - timeout-minutes: 10 - runs-on: ubuntu-latest - permissions: - pull-requests: write - issues: write - steps: - - name: Add Contributor Label - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 - with: - script: | - const isPR = !!context.payload.pull_request; - const issueNumber = isPR ? context.payload.pull_request.number : context.payload.issue.number; - const authorAssociation = isPR ? context.payload.pull_request.author_association : context.payload.issue.author_association; - - if (authorAssociation === 'CONTRIBUTOR') { - await github.rest.issues.addLabels({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: issueNumber, - labels: ['contributor'] - }); - } diff --git a/.github/workflows/repo-sweeper.yml b/.github/workflows/repo-sweeper.yml new file mode 100644 index 000000000000..aabab99db92c --- /dev/null +++ b/.github/workflows/repo-sweeper.yml @@ -0,0 +1,420 @@ +name: repo-sweeper +# CI tier policy: .github/ci-policy.md (fast PR tier, full main tier). +# +# EVENT-BOT CONSOLIDATION (2026-10-29, Drew directive): this single +# scheduled sweeper replaces the event-triggered issue/PR management +# workflows. Deleted and absorbed: +# - pr-management.yml (duplicate-PR LLM comment, contributor label) +# - duplicate-issues.yml (issue compliance + duplicate LLM check, +# needs:compliance recheck on edit) +# - triage.yml (LLM triage of new issues) +# - close-issues.yml (daily stale-issue closer; was scheduled) +# - close-prs.yml (daily low-reaction PR closer; was scheduled) +# - compliance-close.yml (72h needs:compliance closer; was scheduled) +# +# Management work is not synchronous: nobody waits on a label, a +# duplicate note or a stale close the second an event fires, so it +# runs in one pass every 15 minutes instead of fanning out per event. +# Deterministic duties run first (fast); LLM duties run last, newest +# first, capped per pass, over a 20-minute lookback window (overlapping +# the 15-minute cadence so nothing falls between passes; LLM comment +# markers make repeat passes idempotent where the originals were +# single-shot). +# +# KEPT event-based (a human waits synchronously): opencode.yml (/oc and +# /opencode mention bot) and review.yml (/review command). pr-standards +# stays as a pull_request_target VALIDATION workflow (PR standards / +# template compliance checks) in the tier system. main-broken.yml +# (Phase 3 remediation chain) is workflow_run-triggered and unchanged. +# +# Trade-off, accepted: with cancel-in-progress: true a still-running +# sweep is superseded by the next one. Duties are ordered so the fast +# deterministic ones always complete, and anything an LLM pass misses +# is picked up by the next pass while it is inside the window; the +# daily-gated closers resume the next day (closed items leave the +# pool, so progress is never lost). + +on: + schedule: + - cron: "*/15 * * * *" + workflow_dispatch: + +# One live sweep at a time; a newer sweep supersedes a stale one. +concurrency: + group: repo-sweeper + cancel-in-progress: true + +permissions: + contents: read + issues: write + pull-requests: write + +jobs: + sweep: + name: repo sweep + timeout-minutes: 90 + runs-on: ubuntu-24.04 + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + steps: + - name: Checkout repository + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 + with: + fetch-depth: 1 + + - name: Setup Bun + uses: ./.github/actions/setup-bun + + # ---- Deterministic duties (absorbed: pr-management contributor + # ---- label; compliance-close 72h closer, ported verbatim) ---- + - name: Deterministic duties (contributor labels, compliance closes) + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + with: + script: | + const owner = context.repo.owner; + const repo = context.repo.repo; + + // Duty 1: contributor label for CONTRIBUTOR-association PRs. + const pulls = await github.paginate(github.rest.pulls.list, { + owner, repo, state: 'open', per_page: 100, + }); + for (const pr of pulls) { + if (pr.author_association !== 'CONTRIBUTOR') continue; + if (pr.labels.some((l) => l.name === 'contributor')) continue; + await github.rest.issues.addLabels({ + owner, repo, issue_number: pr.number, labels: ['contributor'], + }); + core.info(`Added contributor label to PR #${pr.number}`); + } + + // Duty 2: close needs:compliance items 72h after the + // compliance comment (exempt authors lose the label instead). + const { data: items } = await github.rest.issues.listForRepo({ + owner, 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 seventyTwoHours = 72 * 60 * 60 * 1000; + const orgMemberAssociations = new Set(['OWNER', 'MEMBER']); + const agentLogin = 'opencode-agent[bot]'; + const { data: file } = await github.rest.repos.getContent({ + owner, repo, path: '.github/TEAM_MEMBERS', ref: 'main', + }); + const teamMembers = new Set( + Buffer.from(file.content, 'base64').toString().split('\n') + .map((line) => line.trim().toLowerCase()).filter(Boolean) + ); + function isExempt(item) { + const login = item.user?.login?.toLowerCase(); + return ( + login === agentLogin || + orgMemberAssociations.has(item.author_association) || + (login && teamMembers.has(login)) + ); + } + for (const item of items) { + const isPR = !!item.pull_request; + const kind = isPR ? 'PR' : 'issue'; + const login = item.user?.login; + if (isExempt(item)) { + core.info(`Skipping ${kind} #${item.number}; author ${login || 'unknown'} is exempt`); + try { + await github.rest.issues.removeLabel({ + owner, repo, issue_number: item.number, name: 'needs:compliance', + }); + } catch (e) {} + continue; + } + const { data: comments } = await github.rest.issues.listComments({ + owner, repo, issue_number: item.number, + }); + const complianceComment = comments.find((c) => c.body.includes('')); + if (!complianceComment) continue; + const commentAge = now - new Date(complianceComment.created_at).getTime(); + if (commentAge < seventyTwoHours) { + core.info(`${kind} #${item.number} still within 72-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/main/CONTRIBUTING.md) within the 72-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/main/CONTRIBUTING.md) within the 72-hour window.\n\nFeel free to open a new issue that follows our issue templates.'; + await github.rest.issues.createComment({ + owner, repo, issue_number: item.number, body: closeMessage, + }); + try { + await github.rest.issues.removeLabel({ + owner, repo, issue_number: item.number, name: 'needs:compliance', + }); + } catch (e) {} + if (isPR) { + await github.rest.pulls.update({ + owner, repo, pull_number: item.number, state: 'closed', + }); + } else { + await github.rest.issues.update({ + owner, repo, issue_number: item.number, state: 'closed', state_reason: 'not_planned', + }); + } + core.info(`Closed non-compliant ${kind} #${item.number} after 72-hour window`); + } + + # ---- Daily closers (absorbed: close-issues at 02:00 UTC, + # ---- close-prs at 22:00 UTC; gated to their original hours) ---- + - name: Close stale issues (daily, 02:00 UTC window) + run: | + # Schedule events carry no timestamp input; gate on the clock. + HOUR=$(date -u +%H) + if [ "$GITHUB_EVENT_NAME" = "schedule" ] && [ "$HOUR" != "02" ]; then + echo "Not the 02:00 UTC window (hour=$HOUR); skipping stale-issue close" + exit 0 + fi + bun script/github/close-issues.ts + + - name: Close old PRs without enough positive reactions (daily, 22:00 UTC window) + run: | + HOUR=$(date -u +%H) + if [ "$GITHUB_EVENT_NAME" = "schedule" ] && [ "$HOUR" != "22" ]; then + echo "Not the 22:00 UTC window (hour=$HOUR); skipping old-PR close" + exit 0 + fi + args=("--threshold" "2" "--age-months" "1" "--sleep-ms" "20000" "--max-close" "50") + if [ "$GITHUB_EVENT_NAME" = "schedule" ] || [ "$GITHUB_EVENT_NAME" = "workflow_dispatch" ]; then + args+=("--execute") + fi + bun script/github/close-prs.ts "${args[@]}" + + # ---- LLM candidate discovery (20-minute lookback) ---- + - name: Discover LLM candidates + id: candidates + run: | + SINCE=$(date -u -d '20 minutes ago' +%s) + echo "since_epoch=$SINCE" >> "$GITHUB_OUTPUT" + # New issues: number, association, login (newest first) + gh issue list --state open --limit 100 \ + --json number,author,authorAssociation,createdAt \ + --jq "[.[] | select((.createdAt | fromdateiso8601) > $SINCE)] | sort_by(.createdAt) | reverse | .[] | \"\(.number) \(.authorAssociation) \(.author.login)\"" \ + > new_issues.txt || true + # Compliance rechecks: needs:compliance issues updated in window + gh issue list --state open --label needs:compliance --limit 100 \ + --json number,author,authorAssociation,updatedAt \ + --jq "[.[] | select((.updatedAt | fromdateiso8601) > $SINCE)] | sort_by(.updatedAt) | reverse | .[] | \"\(.number) \(.authorAssociation) \(.author.login)\"" \ + > recheck_issues.txt || true + # New PRs for the duplicate check + gh pr list --state open --limit 100 \ + --json number,author,authorAssociation,createdAt \ + --jq "[.[] | select((.createdAt | fromdateiso8601) > $SINCE)] | sort_by(.createdAt) | reverse | .[] | \"\(.number) \(.authorAssociation) \(.author.login)\"" \ + > new_prs.txt || true + echo "new issues: $(wc -l < new_issues.txt), rechecks: $(wc -l < recheck_issues.txt), new PRs: $(wc -l < new_prs.txt)" + TOTAL=$(( $(wc -l < new_issues.txt) + $(wc -l < recheck_issues.txt) + $(wc -l < new_prs.txt) )) + echo "total=$TOTAL" >> "$GITHUB_OUTPUT" + + - name: Install opencode + if: steps.candidates.outputs.total != '0' + run: curl -fsSL https://opencode.ai/install | bash + + # ---- LLM duty: new-issue compliance + duplicate check, then + # ---- triage (absorbed: duplicate-issues check-duplicates, + # ---- triage). Exempt authors skipped exactly as before. ---- + - name: New issues (compliance + duplicates, then triage) + if: steps.candidates.outputs.total != '0' + env: + OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }} + run: | + export PATH="$HOME/.opencode/bin:$PATH" + COUNT=0 + while read -r N ASSOC LOGIN; do + [ -n "$N" ] || continue + if [ "$COUNT" -ge 3 ]; then echo "per-pass cap reached"; break; fi + if [ "$LOGIN" = "opencode-agent[bot]" ] || [ "$ASSOC" = "OWNER" ] || [ "$ASSOC" = "MEMBER" ] || grep -qxiF "$LOGIN" .github/TEAM_MEMBERS; then + echo "Skipping exempt author $LOGIN for issue #$N"; continue + fi + COUNT=$((COUNT + 1)) + echo "=== issue #$N (compliance + duplicate check) ===" + OPENCODE_PERMISSION='{"bash": {"*": "deny", "gh issue*": "allow"}, "webfetch": "deny"}' \ + opencode run -m opencode/claude-sonnet-4-6 "A new issue has been created: + + Issue number: $N + Issue author association: $ASSOC + + Lookup this issue with gh issue view $N. + + 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. + + If the issue author association is OWNER or MEMBER, skip this compliance check. Do not add the needs:compliance label for organization-owned issues. + + 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 #$N) 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 + + 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 #$N. Build the comment as follows: + + If the issue is NOT compliant and the author association is not OWNER or MEMBER, start the comment with: + + Then explain what needs to be fixed and that they have 72 hours to edit the issue before it is automatically closed. Also add the label needs:compliance to the issue using: gh issue edit $N --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: + + [If not compliant:] + + 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 **72 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] + + [If keybind-related, add:] + 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. + + Remember: post at most ONE comment combining all findings. If everything is fine, post nothing." || echo "compliance/duplicate pass failed for #$N; next sweep retries while in window" + echo "=== issue #$N (triage) ===" + TITLE=$(gh issue view "$N" --json title --jq .title) + BODY=$(gh issue view "$N" --json body --jq .body) + ISSUE_NUMBER="$N" ISSUE_TITLE="$TITLE" ISSUE_BODY="$BODY" \ + opencode run --agent triage "The following issue was recently opened, triage it: + + Title: $TITLE + + $BODY" || echo "triage pass failed for #$N; next sweep retries while in window" + done < new_issues.txt + + # ---- LLM duty: compliance recheck (absorbed: + # ---- duplicate-issues recheck-compliance) ---- + - name: Compliance rechecks + if: steps.candidates.outputs.total != '0' + env: + OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }} + run: | + export PATH="$HOME/.opencode/bin:$PATH" + COUNT=0 + while read -r N ASSOC LOGIN; do + [ -n "$N" ] || continue + if [ "$COUNT" -ge 2 ]; then echo "per-pass cap reached"; break; fi + if [ "$LOGIN" = "opencode-agent[bot]" ] || [ "$ASSOC" = "OWNER" ] || [ "$ASSOC" = "MEMBER" ] || grep -qxiF "$LOGIN" .github/TEAM_MEMBERS; then + echo "Skipping exempt author $LOGIN for issue #$N"; continue + fi + COUNT=$((COUNT + 1)) + echo "=== issue #$N (compliance recheck) ===" + OPENCODE_PERMISSION='{"bash": {"*": "deny", "gh issue*": "allow", "gh api*": "allow"}, "webfetch": "deny"}' \ + opencode run -m opencode/claude-sonnet-4-6 "Issue #$N was previously flagged as non-compliant and has been edited. + Issue author association: $ASSOC + + Lookup this issue with gh issue view $N. + + If the issue author association is OWNER or MEMBER, remove the needs:compliance label if present, delete the previous compliance comment if present, and do not post a new comment. + + 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 $N --remove-label needs:compliance + 2. Find and delete the previous compliance comment (the one containing ) using the gh api comments list for issue $N, then delete it with gh api -X DELETE on that comment. + 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." || echo "recheck failed for #$N; next sweep retries while in window" + done < recheck_issues.txt + + # ---- LLM duty: duplicate PRs (absorbed: pr-management + # ---- check-duplicates; same script, same comment shape) ---- + - name: Duplicate PR check + if: steps.candidates.outputs.total != '0' + env: + OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }} + run: | + COUNT=0 + while read -r N ASSOC LOGIN; do + [ -n "$N" ] || continue + if [ "$COUNT" -ge 2 ]; then echo "per-pass cap reached"; break; fi + if [ "$LOGIN" = "opencode-agent[bot]" ] || grep -qxF "$LOGIN" .github/TEAM_MEMBERS; then + echo "Skipping team/bot PR #$N"; continue + fi + # Idempotency: the original fired once at opened; skip PRs + # that already carry the LLM duplicate comment. + if gh pr view "$N" --json comments --jq '.comments[].body' | grep -q "made by an LLM"; then + echo "PR #$N already has a duplicate-check comment; skipping"; continue + fi + COUNT=$((COUNT + 1)) + echo "=== PR #$N (duplicate check) ===" + { + echo "Check for duplicate PRs related to this new PR:" + echo "" + echo "CURRENT_PR_NUMBER: $N" + echo "" + echo "Title: $(gh pr view "$N" --json title --jq .title)" + echo "" + echo "Description:" + gh pr view "$N" --json body --jq .body + } > pr_info.txt + COMMENT=$(bun script/duplicate-pr.ts -f pr_info.txt "Check the attached file for PR details and search for duplicates") || { echo "duplicate-pr script failed for #$N"; continue; } + if [ "$COMMENT" != "No duplicate PRs found" ]; then + gh pr comment "$N" --body "_The following comment was made by an LLM, it may be inaccurate:_ + + $COMMENT" + fi + done < new_prs.txt diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 02182e908125..93b513627f89 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -34,7 +34,7 @@ env: jobs: affected: timeout-minutes: 15 - name: affected packages + name: basic affected packages # RUNNER-SWITCH: Blacksmith (commented out 2026-10-18): runs-on: blacksmith-4vcpu-ubuntu-2404 runs-on: ubuntu-24.04 outputs: @@ -73,7 +73,7 @@ jobs: unit: timeout-minutes: 20 - name: unit (${{ matrix.settings.name }}) + name: basic unit (${{ matrix.settings.name }}) strategy: fail-fast: false matrix: diff --git a/.github/workflows/triage.yml b/.github/workflows/triage.yml deleted file mode 100644 index 27e8d1a55f36..000000000000 --- a/.github/workflows/triage.yml +++ /dev/null @@ -1,68 +0,0 @@ -name: triage -# RUNNER-SWITCH (2026-10-18): runners in this workflow were switched from Blacksmith to -# GitHub-hosted at Drew's direction. To switch back, grep the repo for RUNNER-SWITCH and -# swap each commented Blacksmith line back in for the active line under it. To choose per -# run instead, use an expression on runs-on, e.g. ${{ vars.RUNNER_UBUNTU || 'ubuntu-24.04' }}, -# or gate individual steps with if: on the runner. - -concurrency: - group: ${{ github.workflow }}-${{ github.event.issue.number || github.event.pull_request.number || github.ref }} - cancel-in-progress: false - queue: max - -on: - issues: - types: [opened] - -jobs: - triage: - timeout-minutes: 60 - # RUNNER-SWITCH: Blacksmith (commented out 2026-10-18): runs-on: blacksmith-4vcpu-ubuntu-2404 - runs-on: ubuntu-24.04 - permissions: - contents: read - issues: write - steps: - - name: Checkout repository - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 - with: - fetch-depth: 1 - - - name: Check exempt issue author - id: author - run: | - LOGIN="${{ github.event.issue.user.login }}" - ASSOCIATION="${{ github.event.issue.author_association }}" - - if [ "$LOGIN" = "opencode-agent[bot]" ] || - [ "$ASSOCIATION" = "OWNER" ] || - [ "$ASSOCIATION" = "MEMBER" ] || - grep -qxiF "$LOGIN" .github/TEAM_MEMBERS; then - echo "skip=true" >> "$GITHUB_OUTPUT" - echo "Skipping issue automation for exempt author: $LOGIN ($ASSOCIATION)" - else - echo "skip=false" >> "$GITHUB_OUTPUT" - fi - - - name: Setup Bun - if: steps.author.outputs.skip != 'true' - uses: ./.github/actions/setup-bun - - - name: Install opencode - if: steps.author.outputs.skip != 'true' - run: curl -fsSL https://opencode.ai/install | bash - - - name: Triage issue - if: steps.author.outputs.skip != 'true' - env: - OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }} - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - ISSUE_NUMBER: ${{ github.event.issue.number }} - ISSUE_TITLE: ${{ github.event.issue.title }} - ISSUE_BODY: ${{ github.event.issue.body }} - run: | - opencode run --agent triage "The following issue was just opened, triage it: - - Title: $ISSUE_TITLE - - $ISSUE_BODY"