-
Notifications
You must be signed in to change notification settings - Fork 0
[chore] #7 - 슬랙 승인 & 머지 알람 #11
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,119 +1,118 @@ | ||
| name: Auto Label | ||
|
|
||
| on: | ||
| issues: | ||
| types: [opened, edited] | ||
| pull_request_target: | ||
| types: [opened, edited, reopened, synchronize] | ||
|
|
||
| permissions: | ||
| issues: write | ||
|
|
||
| jobs: | ||
| label: | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| - uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b | ||
| with: | ||
| script: | | ||
| const isIssue = context.eventName === 'issues'; | ||
| const item = isIssue | ||
| ? context.payload.issue | ||
| : context.payload.pull_request; | ||
|
|
||
| const title = item.title.toLowerCase(); | ||
| const body = (item.body || '').toLowerCase(); | ||
| const author = item.user.login; | ||
|
|
||
| const labels = new Set(); | ||
|
|
||
| const titleRules = [ | ||
| { | ||
| label: '✨ feat', | ||
| patterns: [/\[feat\]/, /(?:^|[^a-z0-9])feat:/] | ||
| }, | ||
| { | ||
| label: '🛠️ fix', | ||
| patterns: [/\[fix\]/, /(?:^|[^a-z0-9])fix:/] | ||
| }, | ||
| { | ||
| label: '🚨 hotfix', | ||
| patterns: [/\[hotfix\]/, /(?:^|[^a-z0-9])hotfix:/] | ||
| }, | ||
| { | ||
| label: '📃 docs', | ||
| patterns: [/\[docs\]/, /(?:^|[^a-z0-9])docs:/] | ||
| }, | ||
| { | ||
| label: '🔁 refactor', | ||
| patterns: [/\[refactor\]/, /(?:^|[^a-z0-9])refactor:/] | ||
| }, | ||
| { | ||
| label: '📝 chore', | ||
| patterns: [/\[chore\]/, /(?:^|[^a-z0-9])chore:/] | ||
| }, | ||
| { | ||
| label: '🧪 test', | ||
| patterns: [/\[test\]/, /(?:^|[^a-z0-9])test:/] | ||
| }, | ||
| { | ||
| label: '✏️ style', | ||
| patterns: [/\[style\]/, /(?:^|[^a-z0-9])style:/] | ||
| }, | ||
| { | ||
| label: '✒️ comment', | ||
| patterns: [/\[comment\]/, /(?:^|[^a-z0-9])comment:/] | ||
| }, | ||
| { | ||
| label: '📄 rename', | ||
| patterns: [/\[rename\]/, /(?:^|[^a-z0-9])rename:/] | ||
| }, | ||
| { | ||
| label: '♻️ remove', | ||
| patterns: [/\[remove\]/, /(?:^|[^a-z0-9])remove:/] | ||
| } | ||
| ]; | ||
|
|
||
| for (const rule of titleRules) { | ||
| if (rule.patterns.some(pattern => pattern.test(title))) { | ||
| labels.add(rule.label); | ||
| } | ||
| } | ||
|
|
||
| const authorLabels = { | ||
| 'Jy000n': '🌸 자윤', | ||
| 'aneykrap': '🌵 예나', | ||
| 'laura-jung': '🍀️ 윤아' | ||
| }; | ||
|
|
||
| const managedLabels = new Set([ | ||
| ...titleRules.map(rule => rule.label), | ||
| ...Object.values(authorLabels) | ||
| ]); | ||
|
|
||
| if (authorLabels[author]) { | ||
| labels.add(authorLabels[author]); | ||
| } | ||
|
|
||
| const currentLabels = (item.labels || []).map(label => label.name || label); | ||
| const labelsToRemove = currentLabels.filter(label => managedLabels.has(label)); | ||
|
|
||
| await Promise.all(labelsToRemove.map(label => github.rest.issues.removeLabel({ | ||
| owner: context.repo.owner, | ||
| repo: context.repo.repo, | ||
| issue_number: item.number, | ||
| name: label | ||
| }).catch(error => { | ||
| if (error.status !== 404) { | ||
| throw error; | ||
| } | ||
| }))); | ||
|
|
||
| if (labels.size > 0) { | ||
| await github.rest.issues.addLabels({ | ||
| owner: context.repo.owner, | ||
| repo: context.repo.repo, | ||
| issue_number: item.number, | ||
| labels: [...labels] | ||
| }); | ||
| } | ||
| #name: Auto Label | ||
| # | ||
| #on: | ||
| # issues: | ||
| # types: [opened, edited] | ||
| # pull_request_target: | ||
| # types: [opened, edited, reopened, synchronize] | ||
| # | ||
| #permissions: | ||
| # issues: write | ||
| # | ||
| #jobs: | ||
| # label: | ||
| # runs-on: ubuntu-latest | ||
| # steps: | ||
| # - uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b | ||
| # with: | ||
| # script: | | ||
| # const isIssue = context.eventName === 'issues'; | ||
| # const item = isIssue | ||
| # ? context.payload.issue | ||
| # : context.payload.pull_request; | ||
| # | ||
| # const title = item.title.toLowerCase(); | ||
| # const body = (item.body || '').toLowerCase(); | ||
| # const author = item.user.login; | ||
| # | ||
| # const labels = new Set(); | ||
| # | ||
| # const titleRules = [ | ||
| # { | ||
| # label: '✨ feat', | ||
| # patterns: [/\[feat\]/, /(?:^|[^a-z0-9])feat:/] | ||
| # }, | ||
| # { | ||
| # label: '🛠️ fix', | ||
| # patterns: [/\[fix\]/, /(?:^|[^a-z0-9])fix:/] | ||
| # }, | ||
| # { | ||
| # label: '🚨 hotfix', | ||
| # patterns: [/\[hotfix\]/, /(?:^|[^a-z0-9])hotfix:/] | ||
| # }, | ||
| # { | ||
| # label: '📃 docs', | ||
| # patterns: [/\[docs\]/, /(?:^|[^a-z0-9])docs:/] | ||
| # }, | ||
| # { | ||
| # label: '🔁 refactor', | ||
| # patterns: [/\[refactor\]/, /(?:^|[^a-z0-9])refactor:/] | ||
| # }, | ||
| # { | ||
| # label: '📝 chore', | ||
| # patterns: [/\[chore\]/, /(?:^|[^a-z0-9])chore:/] | ||
| # }, | ||
| # { | ||
| # label: '🧪 test', | ||
| # patterns: [/\[test\]/, /(?:^|[^a-z0-9])test:/] | ||
| # }, | ||
| # { | ||
| # label: '✏️ style', | ||
| # patterns: [/\[style\]/, /(?:^|[^a-z0-9])style:/] | ||
| # }, | ||
| # { | ||
| # label: '✒️ comment', | ||
| # patterns: [/\[comment\]/, /(?:^|[^a-z0-9])comment:/] | ||
| # }, | ||
| # { | ||
| # label: '📄 rename', | ||
| # patterns: [/\[rename\]/, /(?:^|[^a-z0-9])rename:/] | ||
| # }, | ||
| # { | ||
| # label: '♻️ remove', | ||
| # patterns: [/\[remove\]/, /(?:^|[^a-z0-9])remove:/] | ||
| # } | ||
| # ]; | ||
| # | ||
| # for (const rule of titleRules) { | ||
| # if (rule.patterns.some(pattern => pattern.test(title))) { | ||
| # labels.add(rule.label); | ||
| # } | ||
| # } | ||
| # | ||
| # const authorLabels = { | ||
| # 'Jy000n': '🌸 자윤', | ||
| # 'aneykrap': '🌵 예나', | ||
| # 'laura-jung': '🍀️ 윤아' | ||
| # }; | ||
| # | ||
| # const managedLabels = new Set([ | ||
| # ...titleRules.map(rule => rule.label), | ||
| # ...Object.values(authorLabels) | ||
| # ]); | ||
| # | ||
| # if (authorLabels[author]) { | ||
| # labels.add(authorLabels[author]); | ||
| # } | ||
| # | ||
| # const currentLabels = (item.labels || []).map(label => label.name || label); | ||
| # const labelsToRemove = currentLabels.filter(label => managedLabels.has(label)); | ||
| # | ||
| # await Promise.all(labelsToRemove.map(label => github.rest.issues.removeLabel({ | ||
| # owner: context.repo.owner, | ||
| # repo: context.repo.repo, | ||
| # issue_number: item.number, | ||
| # name: label | ||
| # }).catch(error => { | ||
| # if (error.status !== 404) { | ||
| # throw error; | ||
| # } | ||
| # }))); | ||
| # | ||
| # if (labels.size > 0) { | ||
| # await github.rest.issues.addLabels({ | ||
| # owner: context.repo.owner, | ||
| # repo: context.repo.repo, | ||
| # issue_number: item.number, | ||
| # labels: [...labels] | ||
| # }); | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. p4) 현재의 경우에서는 2명의 approval을 받고나면 marked label 처리가 되어 이후 다시 커밋이 진행되면 메세지가 오지 않을 위험이 있는 것 같습니다. 물론 어푸 이후에 커밋을 변경하는 경우가 거의 없어서 지금의 경우로도 충분히 괜찮을 것 같은데요! 추후에는 label 처리방식이 아닌 커밋 SHA를 기준으로 처리하는 방식으로 리팩토링 해보면 어떨까하는 생각입니다.
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 넹 요거 고민했던 부분인데 approval 이후 커밋이 변경되는 경우가 많지 않아서 일단 지금처럼 해놨습니당 추후에는 커밋SHA를 기준으로 리팩토링해보겠습니다. 좋은 의견 감사해용! |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,154 @@ | ||
| name: PR 승인 완료 Slack 알림 | ||
| on: | ||
| pull_request_review: | ||
| types: [submitted, dismissed] | ||
| pull_request: | ||
| types: [synchronize,ready_for_review] | ||
|
|
||
| permissions: | ||
| pull-requests: write | ||
| issues: write | ||
|
|
||
| env: | ||
| REQUIRED_APPROVALS: "2" | ||
| NOTIFIED_LABEL: "slack-approval-notified" | ||
|
|
||
| concurrency: | ||
| group: pr-approval-notify-${{ github.event.pull_request.number }} | ||
| cancel-in-progress: false | ||
|
|
||
| jobs: | ||
| notify: | ||
| if: github.event.pull_request.state == 'open' && github.event.pull_request.draft == false | ||
| runs-on: ubuntu-latest | ||
|
|
||
| steps: | ||
| - name: 승인 상태 확인 및 Slack 알림 전송 | ||
| uses: actions/github-script@v8 | ||
| env: | ||
| SLACK_PR_WEBHOOK_URL: ${{ secrets.SLACK_PR_WEBHOOK_URL }} | ||
| SLACK_USER_MAP: ${{ secrets.SLACK_USER_MAP }} | ||
| with: | ||
| script: | | ||
| const { owner, repo } = context.repo; | ||
| const pr = context.payload.pull_request; | ||
|
|
||
| const requiredApprovals = Number(process.env.REQUIRED_APPROVALS); | ||
| const markerLabel = process.env.NOTIFIED_LABEL; | ||
| const webhookUrl = process.env.SLACK_PR_WEBHOOK_URL; | ||
| const rawUserMap = process.env.SLACK_USER_MAP; | ||
|
|
||
| if (!webhookUrl) { | ||
| throw new Error("Repository Secret 'SLACK_PR_WEBHOOK_URL'이 없습니다."); | ||
| } | ||
|
|
||
| if (!rawUserMap) { | ||
| throw new Error("Repository Secret 'SLACK_USER_MAP'이 없습니다."); | ||
| } | ||
|
|
||
| const reviews = await github.paginate( | ||
| github.rest.pulls.listReviews, | ||
| { | ||
| owner, | ||
| repo, | ||
| pull_number: pr.number, | ||
| per_page: 100 | ||
| } | ||
| ); | ||
|
|
||
| const latestOpinionByReviewer = new Map(); | ||
|
|
||
| for (const review of reviews) { | ||
| const reviewerLogin = review.user?.login; | ||
|
|
||
| if (!reviewerLogin || reviewerLogin === pr.user.login) { | ||
| continue; | ||
| } | ||
|
|
||
| if ( | ||
| ["APPROVED", "CHANGES_REQUESTED", "DISMISSED"] | ||
| .includes(review.state) | ||
| ) { | ||
| latestOpinionByReviewer.set(reviewerLogin, review.state); | ||
| } | ||
| } | ||
|
|
||
| const approvers = [...latestOpinionByReviewer.entries()] | ||
| .filter(([, state]) => state === "APPROVED") | ||
| .map(([login]) => login); | ||
|
|
||
| const labels = await github.paginate( | ||
| github.rest.issues.listLabelsOnIssue, | ||
| { | ||
| owner, | ||
| repo, | ||
| issue_number: pr.number, | ||
| per_page: 100 | ||
| } | ||
| ); | ||
|
|
||
| const alreadyNotified = labels.some( | ||
| (label) => label.name === markerLabel | ||
| ); | ||
|
|
||
| const approvalsCompleted = | ||
| approvers.length >= requiredApprovals; | ||
|
|
||
| if (!approvalsCompleted) { | ||
| if (alreadyNotified) { | ||
| await github.rest.issues.removeLabel({ | ||
| owner, | ||
| repo, | ||
| issue_number: pr.number, | ||
| name: markerLabel | ||
| }); | ||
| } | ||
|
|
||
| return; | ||
| } | ||
|
|
||
| if (alreadyNotified) { | ||
| return; | ||
| } | ||
|
|
||
| let slackUserMap; | ||
|
|
||
| try { | ||
| slackUserMap = JSON.parse(rawUserMap); | ||
| } catch { | ||
| throw new Error( | ||
| "SLACK_USER_MAP은 올바른 JSON 형식이어야 합니다." | ||
| ); | ||
| } | ||
|
|
||
| const slackMemberId = slackUserMap[pr.user.login]; | ||
|
|
||
| if (!slackMemberId) { | ||
| throw new Error( | ||
| `SLACK_USER_MAP에 '${pr.user.login}'의 Slack 멤버 ID가 없습니다.` | ||
| ); | ||
| } | ||
|
|
||
| const text = [ | ||
| `<@${slackMemberId}> 리뷰 승인 완료 🧞`, | ||
| `<${pr.html_url}|PR 보러가기>` | ||
| ].join("\n"); | ||
|
|
||
| const slackResponse = await fetch(webhookUrl, { | ||
| method: "POST", | ||
| headers: { | ||
| "Content-Type": "application/json; charset=utf-8" | ||
| }, | ||
| body: JSON.stringify({ text }) | ||
| }); | ||
|
|
||
| if (!slackResponse.ok) { | ||
| throw new Error("Slack 알림 전송에 실패했습니다."); | ||
| } | ||
|
|
||
| await github.rest.issues.addLabels({ | ||
| owner, | ||
| repo, | ||
| issue_number: pr.number, | ||
| labels: [markerLabel] | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
주석 처리로 비활성화하지 말고 워크플로우 파일 자체를 제거하세요.
지금 상태는 “비활성화된 워크플로우”가 아니라
.github/workflows/아래에 남아 있는 빈 워크플로우라서actionlint가 이미workflow is empty로 실패하고 있습니다. 비활성화가 목적이면 이 파일을 삭제하거나, 보관이 필요하면.github/workflows/밖으로 옮기거나 확장자를 바꿔서 Actions가 스캔하지 않게 해야 합니다.🧰 Tools
🪛 actionlint (1.7.12)
[error] 1-1: workflow is empty
(syntax-check)
🤖 Prompt for AI Agents
Source: Linters/SAST tools