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
12 changes: 7 additions & 5 deletions .github/scripts/label-conflicting-prs.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -219,13 +219,15 @@ async function hasConflictComment(api, repository, number) {
return (comments ?? []).some((comment) => (comment.body ?? "").includes(COMMENT_MARKER));
}

export async function applyPlan(api, repository, plan, { label, dryRun }) {
// `logger` 를 받는 이유: 테스트가 실제 동작처럼 보이는 줄을 CI 로그에 찍지 않게 하기 위해서다.
// 워크플로의 «Run script tests» 단계에 «#40 라벨 부착» 이 섞이면 무엇이 실제 조작인지 흐려진다.
export async function applyPlan(api, repository, plan, { label, dryRun, logger = console }) {
const failures = [];

for (const pullRequest of plan.toLabel) {
try {
if (dryRun) {
console.log(`[dry-run] #${pullRequest.number} 라벨 부착 + 코멘트`);
logger.log(`[dry-run] #${pullRequest.number} 라벨 부착 + 코멘트`);
continue;
}
await api(`/repos/${repository}/issues/${pullRequest.number}/labels`, {
Expand All @@ -239,7 +241,7 @@ export async function applyPlan(api, repository, plan, { label, dryRun }) {
body: { body: renderConflictComment(pullRequest) },
});
}
console.log(`#${pullRequest.number} 라벨 부착 (base ${pullRequest.baseRefName})`);
logger.log(`#${pullRequest.number} 라벨 부착 (base ${pullRequest.baseRefName})`);
} catch (error) {
failures.push(`#${pullRequest.number} 라벨 부착 실패: ${error.message}`);
}
Expand All @@ -248,14 +250,14 @@ export async function applyPlan(api, repository, plan, { label, dryRun }) {
for (const pullRequest of plan.toUnlabel) {
try {
if (dryRun) {
console.log(`[dry-run] #${pullRequest.number} 라벨 제거`);
logger.log(`[dry-run] #${pullRequest.number} 라벨 제거`);
continue;
}
await api(`/repos/${repository}/issues/${pullRequest.number}/labels/${encodeURIComponent(label)}`, {
method: "DELETE",
allowNotFound: true,
});
console.log(`#${pullRequest.number} 라벨 제거`);
logger.log(`#${pullRequest.number} 라벨 제거`);
} catch (error) {
failures.push(`#${pullRequest.number} 라벨 제거 실패: ${error.message}`);
}
Expand Down
29 changes: 24 additions & 5 deletions .github/scripts/label-conflicting-prs.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ import {
/**
* 호출을 기록하는 가짜 API. `responses` 로 특정 경로의 응답을 지정한다.
*/
/** 테스트 출력이 CI 로그에서 실제 조작처럼 보이지 않도록 삼킨다. */
const silent = { log() {} };

function fakeApi({ responses = {}, failOn = null } = {}) {
const calls = [];
const api = async (apiPath, options = {}) => {
Expand Down Expand Up @@ -195,7 +198,7 @@ test("라벨을 붙이고 안내 코멘트를 한 번 남긴다", async () => {
api,
"o/r",
{ toLabel: [{ number: 40, baseRefName: "develop" }], toUnlabel: [] },
{ label: DEFAULT_LABEL, dryRun: false },
{ label: DEFAULT_LABEL, dryRun: false, logger: silent },
);

assert.deepEqual(failures, []);
Expand All @@ -217,7 +220,7 @@ test("마커 코멘트가 이미 있으면 다시 달지 않는다", async () =>
api,
"o/r",
{ toLabel: [{ number: 41, baseRefName: "develop" }], toUnlabel: [] },
{ label: DEFAULT_LABEL, dryRun: false },
{ label: DEFAULT_LABEL, dryRun: false, logger: silent },
);

const posts = api.calls.filter((call) => call.method === "POST").map((call) => call.apiPath);
Expand All @@ -231,7 +234,7 @@ test("해소된 PR 에서 라벨만 지운다", async () => {
api,
"o/r",
{ toLabel: [], toUnlabel: [{ number: 42 }] },
{ label: DEFAULT_LABEL, dryRun: false },
{ label: DEFAULT_LABEL, dryRun: false, logger: silent },
);

assert.deepEqual(api.calls, [
Expand All @@ -246,7 +249,7 @@ test("dry run 은 아무것도 쓰지 않는다", async () => {
api,
"o/r",
{ toLabel: [{ number: 43, baseRefName: "develop" }], toUnlabel: [{ number: 44 }] },
{ label: DEFAULT_LABEL, dryRun: true },
{ label: DEFAULT_LABEL, dryRun: true, logger: silent },
);

assert.deepEqual(api.calls, []);
Expand All @@ -268,7 +271,7 @@ test("한 PR 이 실패해도 나머지를 처리하고 실패를 모아 돌려
],
toUnlabel: [],
},
{ label: DEFAULT_LABEL, dryRun: false },
{ label: DEFAULT_LABEL, dryRun: false, logger: silent },
);

assert.equal(failures.length, 1);
Expand All @@ -288,6 +291,22 @@ test("라벨이 없으면 만들고, 있으면 만들지 않는다", async () =>
assert.equal(existing.calls.filter((call) => call.method === "POST").length, 0);
});

test("진행 상황은 주입한 로거로 나간다", async () => {
// 기본값이 console 이면 테스트 출력이 CI 로그에 «#60 라벨 부착» 으로 섞여
// 실제 조작과 구분되지 않는다. 주입 지점이 사라지지 않게 고정한다.
const lines = [];
const api = fakeApi({ responses: { "/repos/o/r/issues/60/comments": [] } });

await applyPlan(
api,
"o/r",
{ toLabel: [{ number: 60, baseRefName: "develop" }], toUnlabel: [] },
{ label: DEFAULT_LABEL, dryRun: false, logger: { log: (line) => lines.push(line) } },
);

assert.deepEqual(lines, ["#60 라벨 부착 (base develop)"]);
});

test("요약에 건수와 PR 번호가 남는다", () => {
const plan = planLabelChanges({
pullRequests: [
Expand Down
22 changes: 8 additions & 14 deletions .github/workflows/conflict-label.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,26 +2,20 @@ name: Conflict Label

# 충돌 PR 은 `pull_request` 워크플로가 트리거되지 않는다 (#1028) — GitHub 이 base 와 head 를
# 합친 `refs/pull/<N>/merge` 를 만들지 못하기 때문이다. 그래서 이 워크플로는 `pull_request` 를
# 쓰지 않는다.
#
# **지금은 수동 실행 전용이다.** 도입 시점에 이미 충돌 PR 이 10건이라, 자동 트리거를 켜면 첫
# 실행에서 라벨과 안내 코멘트가 한꺼번에 쏟아진다. 먼저 `dry_run` 으로 실제 러너 환경에서
# 판정을 확인하고, 결과를 보고 아래 두 트리거를 여는 후속 PR 을 낸다.
#
# ```yaml
# push:
# branches: [develop] # 충돌을 만들어 내는 쪽 — develop 이 움직인 직후
# schedule:
# - cron: '23 * * * *' # 보조. 정각 부하를 피해 매시 23분
# ```
# 쓰지 않는다. 충돌을 만들어 내는 쪽(develop 이 움직이는 순간)과 주기 실행으로 상태를 따라간다.
on:
push:
branches: [develop]
schedule:
# 보조 경로. develop push 로 대부분 잡히지만, mergeable 이 끝내 UNKNOWN 이라 보류된 PR 은
# 다음 주기에 확정된다. 정각 부하를 피해 매시 23분.
- cron: '23 * * * *'
workflow_dispatch:
inputs:
dry_run:
description: "라벨·코멘트를 쓰지 않고 계획만 출력"
required: false
# 자동 트리거가 없는 동안의 기본값은 안전한 쪽이다. 실제로 쓰려면 끄고 실행한다.
default: true
default: false
type: boolean

# 실행이 겹치면 같은 PR 에 코멘트가 두 번 달릴 수 있다. 취소하지 않고 줄을 세운다.
Expand Down
Loading