@@ -2,90 +2,109 @@ name: Auto Assign, Close or Merge PR
22
33on :
44 pull_request :
5- branches :
6- - main
7- types :
8- - opened
9- - reopened
5+ branches : [main]
6+ types : [opened, reopened]
107 pull_request_review :
11- types :
12- - submitted
8+ types : [submitted]
139
1410jobs :
15- manage-pr :
11+ assign-and-merge :
1612 runs-on : ubuntu-latest
1713 steps :
18- # 1️⃣ PR 열릴 때 랜덤 리뷰어 2명 지정
19- - name : Assign 2 random reviewers
20- id : assign
14+ - name : Assign random collaborators as reviewers & save to PR body
15+ if : github.event_name == 'pull_request'
2116 uses : actions/github-script@v7
2217 with :
18+ github-token : ${{ secrets.PERSONAL_ACCESS_TOKEN }}
2319 script : |
24- const org = context.repo.owner;
25- let members = [];
20+ // 현재 리포의 모든 collaborator 가져오기 (작성자 제외)
21+ let collaborators = [];
2622 let page = 1;
27-
28- // 조직 멤버 가져오기
29- while(true){
30- const { data } = await github.rest.orgs.listMembers({
31- org,
23+ while (true) {
24+ const { data } = await github.rest.repos.listCollaborators({
25+ owner: context.repo.owner,
26+ repo: context.repo.repo,
3227 per_page: 100,
3328 page
3429 });
35- if(data.length === 0) break;
36- members = members .concat(data.map(u => u.login));
30+ if (data.length === 0) break;
31+ collaborators = collaborators .concat(data.map(u => u.login));
3732 page++;
3833 }
39-
40- // PR 작성자 제외
4134 const author = context.payload.pull_request.user.login;
42- members = members .filter(u => u !== author);
35+ collaborators = collaborators .filter(u => u !== author);
4336
44- // 랜덤 2명 선택
45- const shuffled = members.sort(() => 0.5 - Math.random());
46- const reviewers = shuffled.slice(0, 2);
37+ // 랜덤 2명(또는 남은 만큼) 선정
38+ const reviewers = collaborators.sort(() => 0.5 - Math.random()).slice(0, 2);
4739
4840 // 리뷰어 지정
49- await github.rest.pulls.requestReviewers({
41+ if (reviewers.length > 0) {
42+ await github.rest.pulls.requestReviewers({
43+ owner: context.repo.owner,
44+ repo: context.repo.repo,
45+ pull_number: context.payload.pull_request.number,
46+ reviewers: reviewers
47+ });
48+ }
49+
50+ // PR 본문에 리뷰어 기록
51+ const body = context.payload.pull_request.body || "";
52+ const reviewersNote = `<!-- reviewers: ${reviewers.join(',')} -->`;
53+ await github.rest.pulls.update({
5054 owner: context.repo.owner,
5155 repo: context.repo.repo,
5256 pull_number: context.payload.pull_request.number,
53- reviewers: reviewers
57+ body: `${body}\n${reviewersNote}`
5458 });
5559
60+ console.log("Review candidate collaborators:", collaborators);
5661 console.log("Assigned reviewers:", reviewers);
57- return reviewers.join(',');
58- github-token : ${{ secrets.ORG_ACCESS_TOKEN }}
5962
60- # 2️⃣ 리뷰 제출 감지 후 Close / Merge 처리 (리뷰어 2명 모두 승인해야 merge )
61- - name : Check review and act
63+ - name : Auto merge if reviewers all approved (or none assigned )
64+ if : github.event_name == 'pull_request_review'
6265 uses : actions/github-script@v7
6366 with :
67+ github-token : ${{ secrets.PERSONAL_ACCESS_TOKEN }}
6468 script : |
6569 const pr_number = context.payload.pull_request.number;
70+ const { data: pr } = await github.rest.pulls.get({
71+ owner: context.repo.owner,
72+ repo: context.repo.repo,
73+ pull_number: pr_number
74+ });
75+ const body = pr.body || "";
76+ const reviewersMatch = body.match(/<!-- reviewers: ([^>]*)-->/);
77+ const reviewers = reviewersMatch && reviewersMatch[1].trim()
78+ ? reviewersMatch[1].split(',').map(r => r.trim()).filter(r => r)
79+ : [];
6680
67- // 여러 명이 할당되어 있으므로 배열로 처리
68- const reviewers = steps.assign.outputs.result.split(',');
81+ // 리뷰어가 없으면 자동 머지
82+ if (reviewers.length === 0) {
83+ await github.rest.pulls.merge({
84+ owner: context.repo.owner,
85+ repo: context.repo.repo,
86+ pull_number: pr_number
87+ });
88+ console.log("No reviewers assigned. PR auto merged.");
89+ return;
90+ }
6991
92+ // 각 리뷰어의 마지막 리뷰 상태 확인
7093 const { data: reviews } = await github.rest.pulls.listReviews({
7194 owner: context.repo.owner,
7295 repo: context.repo.repo,
7396 pull_number: pr_number
7497 });
7598
76- // 각 리뷰어의 마지막 리뷰 상태 추적
7799 const reviewStates = {};
78100 reviewers.forEach(reviewer => {
79- // 해당 리뷰어의 마지막 리뷰 찾기
80101 const userReviews = reviews.filter(r => r.user.login === reviewer);
81102 if (userReviews.length > 0) {
82103 reviewStates[reviewer] = userReviews[userReviews.length - 1].state;
83104 }
84105 });
85106
86- console.log("Review states:", reviewStates);
87-
88- // CHANGES_REQUESTED가 하나라도 있으면 PR Close
107+ // CHANGES_REQUESTED가 있으면 PR 반려(닫기)
89108 if (Object.values(reviewStates).includes("CHANGES_REQUESTED")) {
90109 await github.rest.issues.createComment({
91110 owner: context.repo.owner,
@@ -103,15 +122,14 @@ jobs:
103122 return;
104123 }
105124
106- // 2명 모두 APPROVED면 자동 Merge
107- if (reviewers.every(reviewer => reviewStates[reviewer ] === "APPROVED")) {
125+ // 모든 리뷰어 APPROVED면 자동 머지
126+ if (reviewers.length > 0 && reviewers. every(r => reviewStates[r ] === "APPROVED")) {
108127 await github.rest.pulls.merge({
109128 owner: context.repo.owner,
110129 repo: context.repo.repo,
111130 pull_number: pr_number
112131 });
113- console.log("PR auto merged.");
132+ console.log("All reviewers approved. PR auto merged.");
114133 } else {
115134 console.log("Waiting for all reviewers to approve.");
116135 }
117- github-token : ${{ secrets.ORG_ACCESS_TOKEN }}
0 commit comments