2
0

pr-standards.yml 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139
  1. name: PR Standards
  2. on:
  3. pull_request_target:
  4. types: [opened, edited, synchronize]
  5. jobs:
  6. check-standards:
  7. if: |
  8. github.event.pull_request.user.login != 'actions-user' &&
  9. github.event.pull_request.user.login != 'opencode' &&
  10. github.event.pull_request.user.login != 'rekram1-node' &&
  11. github.event.pull_request.user.login != 'thdxr' &&
  12. github.event.pull_request.user.login != 'kommander' &&
  13. github.event.pull_request.user.login != 'jayair' &&
  14. github.event.pull_request.user.login != 'fwang' &&
  15. github.event.pull_request.user.login != 'adamdotdevin' &&
  16. github.event.pull_request.user.login != 'iamdavidhill' &&
  17. github.event.pull_request.user.login != 'opencode-agent[bot]'
  18. runs-on: ubuntu-latest
  19. permissions:
  20. pull-requests: write
  21. steps:
  22. - name: Check PR standards
  23. uses: actions/github-script@v7
  24. with:
  25. script: |
  26. const pr = context.payload.pull_request;
  27. const title = pr.title;
  28. async function addLabel(label) {
  29. await github.rest.issues.addLabels({
  30. owner: context.repo.owner,
  31. repo: context.repo.repo,
  32. issue_number: pr.number,
  33. labels: [label]
  34. });
  35. }
  36. async function removeLabel(label) {
  37. try {
  38. await github.rest.issues.removeLabel({
  39. owner: context.repo.owner,
  40. repo: context.repo.repo,
  41. issue_number: pr.number,
  42. name: label
  43. });
  44. } catch (e) {
  45. // Label wasn't present, ignore
  46. }
  47. }
  48. async function comment(marker, body) {
  49. const markerText = `<!-- pr-standards:${marker} -->`;
  50. const { data: comments } = await github.rest.issues.listComments({
  51. owner: context.repo.owner,
  52. repo: context.repo.repo,
  53. issue_number: pr.number
  54. });
  55. const existing = comments.find(c => c.body.includes(markerText));
  56. if (existing) return;
  57. await github.rest.issues.createComment({
  58. owner: context.repo.owner,
  59. repo: context.repo.repo,
  60. issue_number: pr.number,
  61. body: markerText + '\n' + body
  62. });
  63. }
  64. // Step 1: Check title format
  65. // Matches: feat:, feat(scope):, feat (scope):, etc.
  66. const titlePattern = /^(feat|fix|docs|chore|refactor|test)\s*(\([a-zA-Z0-9-]+\))?\s*:/;
  67. const hasValidTitle = titlePattern.test(title);
  68. if (!hasValidTitle) {
  69. await addLabel('needs:title');
  70. await comment('title', `Hey! Your PR title \`${title}\` doesn't follow conventional commit format.
  71. Please update it to start with one of:
  72. - \`feat:\` or \`feat(scope):\` new feature
  73. - \`fix:\` or \`fix(scope):\` bug fix
  74. - \`docs:\` or \`docs(scope):\` documentation changes
  75. - \`chore:\` or \`chore(scope):\` maintenance tasks
  76. - \`refactor:\` or \`refactor(scope):\` code refactoring
  77. - \`test:\` or \`test(scope):\` adding or updating tests
  78. Where \`scope\` is the package name (e.g., \`app\`, \`desktop\`, \`opencode\`).
  79. See [CONTRIBUTING.md](../blob/dev/CONTRIBUTING.md#pr-titles) for details.`);
  80. return;
  81. }
  82. await removeLabel('needs:title');
  83. // Step 2: Check for linked issue (skip for docs/refactor PRs)
  84. const skipIssueCheck = /^(docs|refactor)\s*(\([a-zA-Z0-9-]+\))?\s*:/.test(title);
  85. if (skipIssueCheck) {
  86. await removeLabel('needs:issue');
  87. console.log('Skipping issue check for docs/refactor PR');
  88. return;
  89. }
  90. const query = `
  91. query($owner: String!, $repo: String!, $number: Int!) {
  92. repository(owner: $owner, name: $repo) {
  93. pullRequest(number: $number) {
  94. closingIssuesReferences(first: 1) {
  95. totalCount
  96. }
  97. }
  98. }
  99. }
  100. `;
  101. const result = await github.graphql(query, {
  102. owner: context.repo.owner,
  103. repo: context.repo.repo,
  104. number: pr.number
  105. });
  106. const linkedIssues = result.repository.pullRequest.closingIssuesReferences.totalCount;
  107. if (linkedIssues === 0) {
  108. await addLabel('needs:issue');
  109. await comment('issue', `Thanks for your contribution!
  110. This PR doesn't have a linked issue. All PRs must reference an existing issue.
  111. Please:
  112. 1. Open an issue describing the bug/feature (if one doesn't exist)
  113. 2. Add \`Fixes #<number>\` or \`Closes #<number>\` to this PR description
  114. See [CONTRIBUTING.md](../blob/dev/CONTRIBUTING.md#issue-first-policy) for details.`);
  115. return;
  116. }
  117. await removeLabel('needs:issue');
  118. console.log('PR meets all standards');