Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
237 changes: 118 additions & 119 deletions .github/workflows/auto-label.yml
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]
# });
Comment on lines +1 to +118

Copy link
Copy Markdown

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
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/auto-label.yml around lines 1 - 118, The auto-label
workflow is fully commented out, leaving an empty workflow definition in the
Actions scan path. Remove the workflow file entirely if it is meant to be
disabled, or move/rename it so it is no longer picked up by GitHub Actions; use
the auto-label workflow entry under .github/workflows/ and its job/script block
as the target for cleanup.

Source: Linters/SAST tools

154 changes: 154 additions & 0 deletions .github/workflows/pr-approval-notify.yml

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

p4) 현재의 경우에서는 2명의 approval을 받고나면 marked label 처리가 되어 이후 다시 커밋이 진행되면 메세지가 오지 않을 위험이 있는 것 같습니다. 물론 어푸 이후에 커밋을 변경하는 경우가 거의 없어서 지금의 경우로도 충분히 괜찮을 것 같은데요! 추후에는 label 처리방식이 아닌 커밋 SHA를 기준으로 처리하는 방식으로 리팩토링 해보면 어떨까하는 생각입니다.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The 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]
});
Loading
Loading