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"