From bc41e861a4fb79d5a1991806c6f7794ee0401b47 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=BC=ED=98=81?= Date: Mon, 24 Aug 2026 23:37:20 +0900 Subject: [PATCH] =?UTF-8?q?fix(ci):=20=EB=A6=B4=EB=A6=AC=EC=8A=A4=20?= =?UTF-8?q?=EB=B2=94=EC=9C=84=20=EC=82=B0=EC=B6=9C=EC=9D=B4=20=EB=B9=84?= =?UTF-8?q?=EB=A9=B4=20=EA=B8=B0=EC=A1=B4=20=ED=8F=AC=ED=95=A8=20=EC=9D=B4?= =?UTF-8?q?=EC=8A=88=20=EB=AA=A9=EB=A1=9D=EC=9D=84=20=EC=A7=80=EC=9A=B0?= =?UTF-8?q?=EC=A7=80=20=EC=95=8A=EB=8A=94=EB=8B=A4=20[base:=20develop]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #1074 로 넣은 render-release-scope 는 산출된 이슈 목록이 비어도 `## 포함 이슈` 섹션을 덮어썼다. 누적 PR 이 0건이 되는 경우가 실제로 있다. - 이미 배포된 릴리스 PR 번호로 workflow_dispatch 를 돌릴 때 (alreadyDistributed=true 면 pendingPullRequests 가 0건) - 닫힌 릴리스 PR 을 reopen 했는데 그 사이 배포가 나간 경우 이때 사람이 확인한 릴리스 노트 입력이 "연결된 이슈 없음" 으로 덮인다. render-distribution-release-notes.sh 가 빈 섹션에서 중단시키므로 잘못된 노트가 나가지는 않지만, 목록이 사라지는 것 자체가 손실이다. 산출값이 있을 때만 덮어쓰고 비면 이미 적힌 목록을 남긴다. `## QA 포인트` 가 쓰던 "비어 있을 때만 채운다" 규칙과 같은 개념이라 writeSection 의 onlyWhenEmpty 로 합쳐 두 섹션이 같은 경로를 타게 했다. 섹션 자체가 없으면 종전대로 자리를 만든다. 실측 — 이미 배포된 #1025 로 수집(alreadyDistributed=true, 누적 PR 0건)해 현재 develop 판과 비교했다. 수정 전은 포함 이슈 28건이 "연결된 이슈 없음" 으로 덮이고, 수정 후는 그대로 남는다. --- .github/scripts/render-release-scope.mjs | 23 +++++++++-------- .github/scripts/render-release-scope.test.mjs | 25 +++++++++++++++++++ 2 files changed, 38 insertions(+), 10 deletions(-) diff --git a/.github/scripts/render-release-scope.mjs b/.github/scripts/render-release-scope.mjs index f0ed45e858..e404699a2e 100644 --- a/.github/scripts/render-release-scope.mjs +++ b/.github/scripts/render-release-scope.mjs @@ -100,19 +100,25 @@ function sectionHasContent(lines, section) { } /** - * `## 포함 이슈` 는 항상 산출값으로 덮어쓰고, `## QA 포인트` 는 비어 있을 때만 초안을 채운다. - * 사람이 다시 쓴 QA 문구와 나머지 섹션은 건드리지 않는다. + * 산출된 목록이 있으면 `## 포함 이슈` 를 덮어쓰고, 비었으면 이미 적힌 목록을 남긴다 — + * 이미 배포된 릴리스 PR 을 다시 산출할 때 누적 PR 이 0건이라, 덮어쓰면 사람이 확인한 + * 릴리스 노트 입력을 지우게 된다. + * `## QA 포인트` 는 항상 비어 있을 때만 초안을 채워 사람이 쓴 문장을 덮지 않는다. + * 섹션 자체가 없으면 둘 다 자리를 만든다 — 비어 있으면 배포가 중단되기 때문이다. */ -export function applyReleaseScopeToBody(body, { issueSection, qaSection }) { +export function applyReleaseScopeToBody(body, { issueSection, qaSection, overwriteIssues = true }) { let lines = String(body ?? "").split(/\r?\n/); - const replaceSection = (heading, content) => { + const writeSection = (heading, content, { onlyWhenEmpty }) => { const section = findSection(lines, heading); if (!section) { const tail = lines.length > 0 && lines.at(-1).trim() === "" ? [] : [""]; lines = [...lines, ...tail, `## ${heading}`, "", ...content.split("\n"), ""]; return; } + if (onlyWhenEmpty && sectionHasContent(lines, section)) { + return; + } lines = [ ...lines.slice(0, section.start + 1), "", @@ -122,12 +128,8 @@ export function applyReleaseScopeToBody(body, { issueSection, qaSection }) { ]; }; - replaceSection(ISSUE_HEADING, issueSection); - - const qa = findSection(lines, QA_HEADING); - if (!qa || !sectionHasContent(lines, qa)) { - replaceSection(QA_HEADING, qaSection); - } + writeSection(ISSUE_HEADING, issueSection, { onlyWhenEmpty: !overwriteIssues }); + writeSection(QA_HEADING, qaSection, { onlyWhenEmpty: true }); return lines.join("\n").replace(/\n{3,}/g, "\n\n").trimEnd() + "\n"; } @@ -144,6 +146,7 @@ async function main() { body: applyReleaseScopeToBody(context.releasePullRequest?.body, { issueSection: renderIssueSection(scope.includedIssues), qaSection: renderQaDraftSection(scope), + overwriteIssues: scope.includedIssues.length > 0, }), }; await fs.mkdir(path.dirname(outputPath), { recursive: true }); diff --git a/.github/scripts/render-release-scope.test.mjs b/.github/scripts/render-release-scope.test.mjs index a38518e634..4a2961b7be 100644 --- a/.github/scripts/render-release-scope.test.mjs +++ b/.github/scripts/render-release-scope.test.mjs @@ -129,3 +129,28 @@ test("leaves no heading inside the rendered sections", () => { [], ); }); + +test("keeps an issue list already in the body when nothing new was collected", () => { + const body = ["## 포함 이슈", "", "- #506", "- #618", "", "## QA 포인트", "", "- 사람이 쓴 문장"].join("\n"); + + const updated = applyReleaseScopeToBody(body, { + issueSection: renderIssueSection([]), + qaSection: "- 자동 초안", + overwriteIssues: false, + }); + + assert.match(updated, /- #506\n- #618/); + assert.doesNotMatch(updated, /연결된 이슈 없음/); +}); + +test("still fills an empty issue section when nothing was collected", () => { + const body = ["## 포함 이슈", "", "## QA 포인트", "", "- 사람이 쓴 문장"].join("\n"); + + const updated = applyReleaseScopeToBody(body, { + issueSection: renderIssueSection([]), + qaSection: "- 자동 초안", + overwriteIssues: false, + }); + + assert.match(updated, /## 포함 이슈\n\n- 연결된 이슈 없음/); +});