Skip to content

Commit 4ef09cd

Browse files
committed
ci: CI/CD 파이프라인 재구성
- pr-checks: lint/prettier/build/test + 이슈 참조 가드 - release: 승인 게이트(Environment), beta/stable prerelease 분기, GitHub App 단기 토큰 push(SHA 핀), Chrome 스토어 게시 기반(비활성) - discord-notify: 이슈/PR/CI 실패 알림 - issue-dedup: GitHub Models 기반 유사 이슈 자동 탐지 - manifest 베타 채널 이름 분기, CLAUDE.md 갱신
1 parent fab5962 commit 4ef09cd

7 files changed

Lines changed: 607 additions & 156 deletions

File tree

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
name: Discord 알림
2+
3+
# 릴리즈 알림은 release.yaml 안에서 직접 보낸다.
4+
# 이 워크플로는 이슈/PR/CI 실패 이벤트를 담당한다.
5+
on:
6+
issues:
7+
types: [opened]
8+
pull_request:
9+
types: [opened, closed]
10+
workflow_run:
11+
workflows: ['PR 검사']
12+
types: [completed]
13+
14+
permissions:
15+
contents: read
16+
17+
jobs:
18+
notify:
19+
runs-on: ubuntu-latest
20+
if: ${{ vars.DISCORD_ENABLED != 'false' }}
21+
steps:
22+
- name: Discord 전송
23+
env:
24+
DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_WEBHOOK_URL }}
25+
uses: actions/github-script@v7
26+
with:
27+
script: |
28+
const webhook = process.env.DISCORD_WEBHOOK_URL;
29+
if (!webhook) {
30+
core.info('DISCORD_WEBHOOK_URL 미설정 — 건너뜀');
31+
return;
32+
}
33+
34+
const event = context.eventName;
35+
let embed;
36+
37+
if (event === 'issues') {
38+
const i = context.payload.issue;
39+
embed = {
40+
title: `🐛 새 이슈 #${i.number}: ${i.title}`,
41+
url: i.html_url,
42+
description: `작성자: ${i.user.login}`,
43+
color: 0x5865f2, // 파랑
44+
};
45+
} else if (event === 'pull_request') {
46+
const pr = context.payload.pull_request;
47+
const merged = pr.merged === true;
48+
const action = context.payload.action;
49+
if (action === 'closed' && !merged) return; // 머지 없이 닫힌 PR은 무시
50+
embed = {
51+
title: merged
52+
? `🟣 PR 머지됨 #${pr.number}: ${pr.title}`
53+
: `🟢 PR 열림 #${pr.number}: ${pr.title}`,
54+
url: pr.html_url,
55+
description: `작성자: ${pr.user.login}`,
56+
color: merged ? 0x8957e5 : 0x57f287, // 보라/초록
57+
};
58+
} else if (event === 'workflow_run') {
59+
const run = context.payload.workflow_run;
60+
if (run.conclusion !== 'failure') return; // 실패만 알림
61+
embed = {
62+
title: `❌ CI 실패: ${run.name}`,
63+
url: run.html_url,
64+
description: `브랜치: ${run.head_branch}`,
65+
color: 0xed4245, // 빨강
66+
};
67+
}
68+
69+
if (!embed) return;
70+
const res = await fetch(webhook, {
71+
method: 'POST',
72+
headers: { 'Content-Type': 'application/json' },
73+
body: JSON.stringify({ embeds: [embed] }),
74+
});
75+
if (!res.ok) core.warning(`Discord 알림 실패: ${res.status}`);

.github/workflows/issue-dedup.yml

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
name: 유사 이슈 탐지
2+
3+
# 새 이슈가 열리면 기존 open 이슈들과 비교해 중복 가능성이 있으면 댓글을 단다.
4+
# GitHub Models(무료, GITHUB_TOKEN)로 의미 기반 비교 — 별도 API 키 불필요.
5+
on:
6+
issues:
7+
types: [opened]
8+
9+
permissions:
10+
issues: write
11+
models: read
12+
contents: read
13+
14+
jobs:
15+
detect:
16+
runs-on: ubuntu-latest
17+
steps:
18+
- name: 유사 이슈 분석 및 댓글
19+
uses: actions/github-script@v7
20+
with:
21+
script: |
22+
const current = context.payload.issue;
23+
24+
// 1) 기존 open 이슈 수집 (현재 이슈/PR 제외, 최근 갱신순 최대 50개)
25+
const { data: issues } = await github.rest.issues.listForRepo({
26+
owner: context.repo.owner,
27+
repo: context.repo.repo,
28+
state: 'open',
29+
sort: 'updated',
30+
direction: 'desc',
31+
per_page: 50,
32+
});
33+
const candidates = issues
34+
.filter((i) => i.number !== current.number && !i.pull_request)
35+
.map((i) => ({
36+
number: i.number,
37+
title: i.title,
38+
body: (i.body || '').slice(0, 500),
39+
}));
40+
41+
if (candidates.length === 0) {
42+
core.info('비교할 기존 이슈 없음 — 종료');
43+
return;
44+
}
45+
46+
// 2) GitHub Models 호출
47+
const prompt = [
48+
'너는 GitHub 이슈 트리아지 도우미야.',
49+
'아래 "새 이슈"와 의미상 중복(같은 버그/같은 요청)일 가능성이 높은 "기존 이슈"의 번호만 골라줘.',
50+
'주제가 다르면 절대 고르지 마. 확실하지 않으면 비워둬.',
51+
'반드시 JSON만 출력: {"duplicates":[{"number":123,"reason":"한 줄 근거"}]}',
52+
'',
53+
`## 새 이슈\n제목: ${current.title}\n본문: ${(current.body || '').slice(0, 1500)}`,
54+
'',
55+
'## 기존 이슈 목록',
56+
JSON.stringify(candidates),
57+
].join('\n');
58+
59+
let duplicates = [];
60+
try {
61+
const res = await fetch('https://models.github.ai/inference/chat/completions', {
62+
method: 'POST',
63+
headers: {
64+
Authorization: `Bearer ${process.env.GITHUB_TOKEN}`,
65+
'Content-Type': 'application/json',
66+
},
67+
body: JSON.stringify({
68+
model: 'openai/gpt-4o-mini',
69+
temperature: 0,
70+
response_format: { type: 'json_object' },
71+
messages: [{ role: 'user', content: prompt }],
72+
}),
73+
});
74+
if (!res.ok) {
75+
core.warning(`GitHub Models 호출 실패: ${res.status} ${await res.text()}`);
76+
return;
77+
}
78+
const data = await res.json();
79+
const content = data.choices?.[0]?.message?.content || '{}';
80+
duplicates = (JSON.parse(content).duplicates || []).filter((d) =>
81+
candidates.some((c) => c.number === d.number)
82+
);
83+
} catch (e) {
84+
core.warning(`분석 중 오류: ${e.message}`);
85+
return;
86+
}
87+
88+
if (duplicates.length === 0) {
89+
core.info('유사 이슈 없음');
90+
return;
91+
}
92+
93+
// 3) 댓글 + 라벨
94+
const lines = duplicates.map((d) => `- #${d.number} — ${d.reason}`).join('\n');
95+
const body = [
96+
'🔁 **유사한 이슈가 있는 것 같아요.**',
97+
'아래 이슈와 중복이 아닌지 확인해주세요. 같은 내용이라면 이 이슈를 닫고 기존 이슈에 의견을 남겨주시면 됩니다.',
98+
'',
99+
lines,
100+
'',
101+
'_이 댓글은 자동 분석 결과이며 틀릴 수 있습니다._',
102+
].join('\n');
103+
104+
await github.rest.issues.createComment({
105+
owner: context.repo.owner,
106+
repo: context.repo.repo,
107+
issue_number: current.number,
108+
body,
109+
});
110+
try {
111+
await github.rest.issues.addLabels({
112+
owner: context.repo.owner,
113+
repo: context.repo.repo,
114+
issue_number: current.number,
115+
labels: ['👀 possible-duplicate'],
116+
});
117+
} catch (e) {
118+
core.info(`라벨 추가 건너뜀: ${e.message}`);
119+
}
120+
env:
121+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

.github/workflows/pr-checks.yml

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
name: PR 검사
2+
3+
on:
4+
pull_request:
5+
types: [opened, synchronize, reopened, edited]
6+
branches:
7+
- main
8+
9+
permissions:
10+
contents: read
11+
pull-requests: read
12+
13+
concurrency:
14+
group: pr-checks-${{ github.event.pull_request.number }}
15+
cancel-in-progress: true
16+
17+
jobs:
18+
# PR 본문에 연결된 이슈 참조가 있는지 검사 (이슈-먼저 정책 강제)
19+
issue-link:
20+
runs-on: ubuntu-latest
21+
steps:
22+
- name: 이슈 참조 확인
23+
uses: actions/github-script@v7
24+
with:
25+
script: |
26+
const body = context.payload.pull_request.body || '';
27+
// "#123", "Closes #123", "Fixes #45", "Resolves: #6" 등 + 전체 URL 형태 모두 허용
28+
const ref = /(close[sd]?|fix(e[sd])?|resolve[sd]?)?\s*:?\s*#\d+/i.test(body)
29+
|| /github\.com\/[^/]+\/[^/]+\/issues\/\d+/i.test(body);
30+
if (!ref) {
31+
core.setFailed(
32+
'PR 본문에 연결된 이슈가 없습니다. ' +
33+
'`Closes #이슈번호` 형식으로 관련 이슈를 참조해주세요. ' +
34+
'(모든 변경은 이슈가 먼저 등록되어 있어야 합니다)'
35+
);
36+
}
37+
38+
verify:
39+
runs-on: ubuntu-latest
40+
steps:
41+
- name: 리포지토리 체크아웃
42+
uses: actions/checkout@v4
43+
44+
- name: Node.js 설정
45+
uses: actions/setup-node@v4
46+
with:
47+
node-version: '20'
48+
cache: npm
49+
50+
- name: 의존성 설치
51+
run: npm ci
52+
53+
- name: 포맷 검사 (Prettier)
54+
run: npx prettier --check .
55+
56+
- name: 린트 (ESLint)
57+
run: npm run lint
58+
59+
- name: 타입 체크 + 빌드
60+
run: npm run build
61+
62+
- name: 테스트
63+
run: npm run test

0 commit comments

Comments
 (0)