Skip to content

Commit 7e070fe

Browse files
authored
chore: develop -> master ff-only 머지 자동화 액션 추가 (#738)
* chore: develop -> master ff-only 머지 자동화 액션 추가 * fix: 승인 시점 이후 푸시된 커밋 관련 검증 추기 * fix: CI 상태 또한 고려하도록
1 parent 88d603e commit 7e070fe

1 file changed

Lines changed: 134 additions & 0 deletions

File tree

.github/workflows/ff-merge.yml

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
name: FF-Only Merge to Master
2+
3+
on:
4+
pull_request_review:
5+
types: [submitted]
6+
check_suite:
7+
types: [completed]
8+
9+
jobs:
10+
ff-merge:
11+
if: |
12+
github.event_name == 'pull_request_review' ||
13+
(github.event_name == 'check_suite' && github.event.check_suite.conclusion == 'success')
14+
runs-on: ubuntu-latest
15+
16+
steps:
17+
- name: PR 조회 및 조건 검증
18+
id: validate
19+
uses: actions/github-script@v7
20+
with:
21+
script: |
22+
let prNumber, headSha;
23+
24+
if (context.eventName === 'pull_request_review') {
25+
const pr = context.payload.pull_request;
26+
if (pr.base.ref !== 'master' || pr.head.ref !== 'develop' || pr.state !== 'open') {
27+
core.setOutput('ready', 'false');
28+
return;
29+
}
30+
if (context.payload.review.state !== 'approved') {
31+
core.setOutput('ready', 'false');
32+
return;
33+
}
34+
prNumber = pr.number;
35+
headSha = pr.head.sha;
36+
} else {
37+
const { data: prs } = await github.rest.pulls.list({
38+
owner: context.repo.owner,
39+
repo: context.repo.repo,
40+
state: 'open',
41+
base: 'master',
42+
head: `${context.repo.owner}:develop`,
43+
});
44+
45+
if (prs.length === 0) {
46+
core.setOutput('ready', 'false');
47+
return;
48+
}
49+
50+
prNumber = prs[0].number;
51+
headSha = prs[0].head.sha;
52+
53+
if (context.payload.check_suite.head_sha !== headSha) {
54+
core.setOutput('ready', 'false');
55+
return;
56+
}
57+
}
58+
59+
// 승인 상태 확인
60+
const { data: reviews } = await github.rest.pulls.listReviews({
61+
owner: context.repo.owner,
62+
repo: context.repo.repo,
63+
pull_number: prNumber,
64+
});
65+
66+
const latest = {};
67+
for (const r of reviews) {
68+
if (r.state !== 'COMMENTED') {
69+
latest[r.user.login] = r.state;
70+
}
71+
}
72+
73+
const values = Object.values(latest);
74+
const approved = values.filter(s => s === 'APPROVED').length >= 1;
75+
const blocked = values.some(s => s === 'CHANGES_REQUESTED');
76+
77+
if (!approved || blocked) {
78+
core.setOutput('ready', 'false');
79+
return;
80+
}
81+
82+
// CI 상태 확인
83+
const { data: { check_runs } } = await github.rest.checks.listForRef({
84+
owner: context.repo.owner,
85+
repo: context.repo.repo,
86+
ref: headSha,
87+
per_page: 100,
88+
});
89+
90+
const ciRuns = check_runs.filter(r => r.name !== context.workflow);
91+
const allPassed = ciRuns.length > 0 && ciRuns.every(r =>
92+
r.status === 'completed' &&
93+
['success', 'skipped', 'neutral'].includes(r.conclusion)
94+
);
95+
96+
if (!allPassed) {
97+
core.setOutput('ready', 'false');
98+
return;
99+
}
100+
101+
core.setOutput('ready', 'true');
102+
core.setOutput('head_sha', headSha);
103+
104+
- name: Checkout
105+
if: steps.validate.outputs.ready == 'true'
106+
uses: actions/checkout@v4
107+
with:
108+
token: ${{ secrets.PAT }}
109+
fetch-depth: 0
110+
111+
- name: Git 설정
112+
if: steps.validate.outputs.ready == 'true'
113+
run: |
114+
git config user.name "github-actions[bot]"
115+
git config user.email "github-actions[bot]@users.noreply.github.com"
116+
117+
- name: develop 변경 여부 검증
118+
if: steps.validate.outputs.ready == 'true'
119+
run: |
120+
APPROVED_SHA="${{ steps.validate.outputs.head_sha }}"
121+
CURRENT_SHA=$(git rev-parse origin/develop)
122+
if [ "$APPROVED_SHA" != "$CURRENT_SHA" ]; then
123+
echo "develop이 승인 이후 변경되었습니다."
124+
echo " 승인된 SHA: $APPROVED_SHA"
125+
echo " 현재 SHA: $CURRENT_SHA"
126+
exit 1
127+
fi
128+
129+
- name: FF-Only merge develop → master
130+
if: steps.validate.outputs.ready == 'true'
131+
run: |
132+
git checkout master
133+
git merge --ff-only ${{ steps.validate.outputs.head_sha }}
134+
git push origin master

0 commit comments

Comments
 (0)