compliance-close.yml 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133
  1. name: compliance-close
  2. on:
  3. schedule:
  4. # Run every 30 minutes to check for expired compliance windows
  5. - cron: "*/30 * * * *"
  6. workflow_dispatch:
  7. permissions:
  8. contents: read
  9. issues: write
  10. pull-requests: write
  11. jobs:
  12. close-non-compliant:
  13. runs-on: ubuntu-latest
  14. steps:
  15. - name: Close non-compliant issues and PRs after 2 hours
  16. uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0
  17. with:
  18. script: |
  19. const { data: items } = await github.rest.issues.listForRepo({
  20. owner: context.repo.owner,
  21. repo: context.repo.repo,
  22. labels: 'needs:compliance',
  23. state: 'open',
  24. per_page: 100,
  25. });
  26. if (items.length === 0) {
  27. core.info('No open issues/PRs with needs:compliance label');
  28. return;
  29. }
  30. const now = Date.now();
  31. const twoHours = 2 * 60 * 60 * 1000;
  32. const orgMemberAssociations = new Set(['OWNER', 'MEMBER']);
  33. const agentLogin = 'opencode-agent[bot]';
  34. const { data: file } = await github.rest.repos.getContent({
  35. owner: context.repo.owner,
  36. repo: context.repo.repo,
  37. path: '.github/TEAM_MEMBERS',
  38. ref: 'dev',
  39. });
  40. const teamMembers = new Set(
  41. Buffer.from(file.content, 'base64')
  42. .toString()
  43. .split('\n')
  44. .map((line) => line.trim().toLowerCase())
  45. .filter(Boolean)
  46. );
  47. function isExempt(item) {
  48. const login = item.user?.login?.toLowerCase();
  49. return (
  50. login === agentLogin ||
  51. orgMemberAssociations.has(item.author_association) ||
  52. (login && teamMembers.has(login))
  53. );
  54. }
  55. for (const item of items) {
  56. const isPR = !!item.pull_request;
  57. const kind = isPR ? 'PR' : 'issue';
  58. const login = item.user?.login;
  59. if (isExempt(item)) {
  60. core.info(`Skipping ${kind} #${item.number}; author ${login || 'unknown'} is exempt`);
  61. try {
  62. await github.rest.issues.removeLabel({
  63. owner: context.repo.owner,
  64. repo: context.repo.repo,
  65. issue_number: item.number,
  66. name: 'needs:compliance',
  67. });
  68. } catch (e) {}
  69. continue;
  70. }
  71. const { data: comments } = await github.rest.issues.listComments({
  72. owner: context.repo.owner,
  73. repo: context.repo.repo,
  74. issue_number: item.number,
  75. });
  76. const complianceComment = comments.find(c => c.body.includes('<!-- issue-compliance -->'));
  77. if (!complianceComment) continue;
  78. const commentAge = now - new Date(complianceComment.created_at).getTime();
  79. if (commentAge < twoHours) {
  80. core.info(`${kind} #${item.number} still within 2-hour window (${Math.round(commentAge / 60000)}m elapsed)`);
  81. continue;
  82. }
  83. const closeMessage = isPR
  84. ? '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.'
  85. : '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.';
  86. await github.rest.issues.createComment({
  87. owner: context.repo.owner,
  88. repo: context.repo.repo,
  89. issue_number: item.number,
  90. body: closeMessage,
  91. });
  92. try {
  93. await github.rest.issues.removeLabel({
  94. owner: context.repo.owner,
  95. repo: context.repo.repo,
  96. issue_number: item.number,
  97. name: 'needs:compliance',
  98. });
  99. } catch (e) {}
  100. if (isPR) {
  101. await github.rest.pulls.update({
  102. owner: context.repo.owner,
  103. repo: context.repo.repo,
  104. pull_number: item.number,
  105. state: 'closed',
  106. });
  107. } else {
  108. await github.rest.issues.update({
  109. owner: context.repo.owner,
  110. repo: context.repo.repo,
  111. issue_number: item.number,
  112. state: 'closed',
  113. state_reason: 'not_planned',
  114. });
  115. }
  116. core.info(`Closed non-compliant ${kind} #${item.number} after 2-hour window`);
  117. }