| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133 |
- 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@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.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 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} #${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('<!-- 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,
- });
- 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 2-hour window`);
- }
|