| name: compliance-close |
|
|
| on: |
| schedule: |
| |
| - 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@f28e40c7f34bde8b3046d885e986cb6290c5673b |
| 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; |
| 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: 'dev', |
| }); |
| 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} |
| 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('<!-- issue-compliance -->')); |
| if (!complianceComment) continue; |
|
|
| const commentAge = now - new Date(complianceComment.created_at).getTime(); |
| if (commentAge < twoHours) { |
| core.info(`${kind} |
| 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, |
| }); |
|
|
| 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} |
| } |
|
|