From d394db2f532aac080da7ab5d2b1036464dc68c49 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=BC=ED=98=81?= Date: Sun, 30 Aug 2026 13:14:10 +0900 Subject: [PATCH] =?UTF-8?q?ci:=20=EB=A6=B4=EB=A6=AC=EC=8A=A4=20PR=20?= =?UTF-8?q?=EC=9D=84=20path-filter=20300=20=EA=B2=BD=EA=B3=84=EC=97=90?= =?UTF-8?q?=EC=84=9C=20=EB=A9=B4=EC=A0=9C=ED=95=9C=EB=8B=A4=20(#1559)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `Guard server-side path filter boundary` 가 변경 파일 300개 초과를 fail-closed 하는데, 릴리스 PR(develop → main)은 본질적으로 그 경계를 넘는다. #1558 이 1070파일로 막혔고, `Repository Quality` 는 `required-checks (main)` 의 required 체크라 릴리스가 실제로 멈췄다. 「PR 을 분리하세요」 라는 이 가드의 처방이 유일하게 성립하지 않는 PR 이다 — 쪼개면 그건 릴리스가 아니다. 가드의 목적에 비춰도 릴리스 PR 은 대상이 아니다. 막으려는 것은 «paths 필터를 우회한 의존성 변경이 검토 없이 들어오는 것» 인데, 릴리스 PR 의 모든 변경은 이미 develop base PR 로 한 번씩 들어오면서 300 이하에서 필터와 dependency-review 를 통과했다. 실제로 #1558 에서도 `Review dependency changes` 와 `Generate PR dependency graph` 는 통과했다. - 면제 조건은 `base == main && head == develop` 두 개를 모두 요구한다. **크기가 아니라 「릴리스 PR 인가」로 가른다** — 파일 수 상한을 올리는 식으로 열면 가드 자체가 무의미해진다. - 면제될 때는 실패시키지 않되 `::notice::` 로 근거를 남긴다. - 그 밖의 300 초과는 종전대로 `exit 1`. 정책 테스트를 두 축으로 잠갔다(`supply-chain-policy.test.mjs`). - 기존 「fail closed」 테스트에 «초과 메시지 뒤에 exit 1 이 있다» 단언을 더했다. - 신규 「only the release pull request is exempt」 가 base/head 조건 두 개와 notice 를 단언하고, 면제 분기 안에 `exit` 가 없음을 확인한다. 역검증 — 면제 조건을 크기 기준(`CHANGED_FILES -gt 0`)으로 바꾸면 신규 테스트가 실패한다 (20건 중 1건 fail). 조건을 느슨하게 여는 변경이 테스트로 막힌다. --- .github/scripts/supply-chain-policy.test.mjs | 28 ++++++++++++++++++++ .github/workflows/repository-quality.yml | 19 +++++++++++-- 2 files changed, 45 insertions(+), 2 deletions(-) diff --git a/.github/scripts/supply-chain-policy.test.mjs b/.github/scripts/supply-chain-policy.test.mjs index 5c21310188..0d56f7dfdd 100644 --- a/.github/scripts/supply-chain-policy.test.mjs +++ b/.github/scripts/supply-chain-policy.test.mjs @@ -415,6 +415,34 @@ test('pull requests beyond the conservative path-filter boundary fail closed', a assert.match(source, /github\.event\.pull_request\.changed_files/); assert.match(source, /MAX_PATH_FILTER_FILES:\s*300/); assert.match(source, /CHANGED_FILES > MAX_PATH_FILTER_FILES/); + // 경계를 넘은 PR 은 여전히 exit 1 로 닫힌다 — 면제는 아래 테스트가 잠그는 릴리스 PR 하나뿐이다. + assert.match(source, /paths 안전 경계 \$\{MAX_PATH_FILTER_FILES\}개를 초과했습니다[^]*?exit 1/); +}); + +test('only the release pull request is exempt from the path-filter boundary', async () => { + const source = await readFile( + new URL('../workflows/repository-quality.yml', import.meta.url), + 'utf8', + ); + + // 면제 조건은 «릴리스 PR 인가» 다 — base main + head develop 둘 다 요구한다. + // 크기를 기준으로 열면(예: 파일 수 상한 상향) 가드 자체가 무의미해진다. + assert.match(source, /base_ref="\$\(jq -r '\.base\.ref' "\$pull_request_file"\)"/); + assert.match(source, /head_ref="\$\(jq -r '\.head\.ref' "\$pull_request_file"\)"/); + assert.match( + source, + /if \[ "\$base_ref" = "main" \] && \[ "\$head_ref" = "develop" \]; then/, + ); + + // 면제될 때는 실패시키지 않되 근거를 로그에 남긴다. + assert.match(source, /::notice::릴리스 PR\(develop → main\)이라 paths 경계/); + + // 면제 분기 안에는 exit 가 없어야 하고, 그 밖의 초과는 여전히 실패한다. + const exemption = source.match( + /if \[ "\$base_ref" = "main" \] && \[ "\$head_ref" = "develop" \]; then([^]*?)else/, + ); + assert.ok(exemption, 'release exemption branch not found'); + assert.doesNotMatch(exemption[1], /exit\s+1/); }); test('dependency review blocks high severity changes without enforcing a license allowlist', async () => { diff --git a/.github/workflows/repository-quality.yml b/.github/workflows/repository-quality.yml index 67f2677de7..609a5e70d8 100644 --- a/.github/workflows/repository-quality.yml +++ b/.github/workflows/repository-quality.yml @@ -132,8 +132,23 @@ jobs: fi if (( CHANGED_FILES > MAX_PATH_FILTER_FILES )); then - echo "::error::PR 변경 파일 ${CHANGED_FILES}개가 paths 안전 경계 ${MAX_PATH_FILTER_FILES}개를 초과했습니다. PR을 분리하세요." - exit 1 + # 릴리스 PR(develop → main)만 면제한다. 이 가드가 막으려는 것은 «paths 필터를 + # 우회한 의존성 변경이 검토 없이 들어오는 것» 인데, 릴리스 PR 의 모든 변경은 + # 이미 develop base PR 로 한 번씩 들어오면서 300 이하에서 필터와 + # dependency-review 를 통과했다. 릴리스는 새 코드를 만들지 않고 검증된 develop + # 을 main 으로 옮기는 머지다. 그리고 「PR 을 분리하세요」 가 유일하게 성립하지 + # 않는 PR 이기도 하다 — 쪼개면 그건 릴리스가 아니다 (#1559). + # + # 크기가 아니라 «릴리스 PR 인가» 로 가른다. 크기를 기준으로 열면 가드가 + # 무의미해진다. + base_ref="$(jq -r '.base.ref' "$pull_request_file")" + head_ref="$(jq -r '.head.ref' "$pull_request_file")" + if [ "$base_ref" = "main" ] && [ "$head_ref" = "develop" ]; then + echo "::notice::릴리스 PR(develop → main)이라 paths 경계 ${MAX_PATH_FILTER_FILES}개 검사를 건너뜁니다 (변경 ${CHANGED_FILES}개). 구성 변경은 각 develop base PR 에서 이미 필터를 거쳤습니다." + else + echo "::error::PR 변경 파일 ${CHANGED_FILES}개가 paths 안전 경계 ${MAX_PATH_FILTER_FILES}개를 초과했습니다. PR을 분리하세요." + exit 1 + fi fi printf 'count=%s\n' "$CHANGED_FILES" >> "$GITHUB_OUTPUT"