diff --git a/.github/scripts/pr-gate-policy.test.mjs b/.github/scripts/pr-gate-policy.test.mjs index 51c898847..a83fb663c 100644 --- a/.github/scripts/pr-gate-policy.test.mjs +++ b/.github/scripts/pr-gate-policy.test.mjs @@ -4,10 +4,15 @@ import test from "node:test"; const workflowDirectory = new URL("../workflows/", import.meta.url); const ENTRY_WORKFLOW = "pr-validation.yml"; -const VALIDATION_WORKFLOWS = ["lint.yml", "unit-test.yml", "screenshot.yml", "repository-quality.yml"]; +const VALIDATION_WORKFLOWS = ["lint.yml", "unit-test.yml", "screenshot.yml", "repository-quality.yml", "dependency-review.yml"]; const HEAVY_VALIDATION_WORKFLOWS = ["lint.yml", "unit-test.yml", "screenshot.yml"]; +// repository-quality 의 분류 결과를 받아 범위를 줄이는 lane. 분류 실패는 전부 실행으로 닫힌다. +const IMPACT_SCOPED_WORKFLOWS = [...HEAVY_VALIDATION_WORKFLOWS, "dependency-review.yml"]; +// PR 번호로 PR 을 다시 조회하는 lane. dependency-review 는 이벤트의 PR 을 쓰므로 번호를 받지 않는다. +const PR_NUMBER_WORKFLOWS = ["lint.yml", "unit-test.yml", "screenshot.yml", "repository-quality.yml"]; // Repository ruleset 20911039 의 required context 와 함께 바꿔야 하는 외부 계약이다. const REQUIRED_VALIDATION_CONTEXTS = [ + "Dependency Review / Review dependency changes", "Repository Quality / Repository Quality", "Screenshot / Validate Compose Preview Screenshots", "Static Analysis / Check Code Quality (Ktlint)", @@ -130,7 +135,7 @@ test("token-authored commits preserve the pull request context on manual dispatc /pull_request_number: \$\{\{ fromJSON\(inputs\.pull_request_number \|\| '0'\) \|\| github\.event\.pull_request\.number \|\| 0 \}\}/g, ) ?? [] ).length, - VALIDATION_WORKFLOWS.length, + PR_NUMBER_WORKFLOWS.length, ); assert.equal( (entry.match(/pull_request_number: \$\{\{ inputs\.pull_request_number/g) ?? []).length, @@ -158,7 +163,7 @@ test("merge group validation falls back to the full suite without a pull request assert.equal( (entry.match(/\|\| github\.event\.pull_request\.number \|\| 0 \}\}/g) ?? []).length, - VALIDATION_WORKFLOWS.length, + PR_NUMBER_WORKFLOWS.length, ); for (const gate of ["Require linked Issue", "Require module owner", "Validate CI Test Plan", "Reject test-only production declarations"]) { assert.match( @@ -194,13 +199,19 @@ test("the entry point keeps no pull_request branch or path filter", async () => test("impact outputs scope each heavy lane and classification failure runs full validation", async () => { const entry = await readWorkflow(ENTRY_WORKFLOW); - assert.equal((entry.match(/^ {4}needs: repository-quality$/gm) ?? []).length, HEAVY_VALIDATION_WORKFLOWS.length); + assert.equal((entry.match(/^ {4}needs: repository-quality$/gm) ?? []).length, IMPACT_SCOPED_WORKFLOWS.length); assert.equal( (entry.match(/^ {4}if: \$\{\{ !cancelled\(\) \}\}$/gm) ?? []).length, - HEAVY_VALIDATION_WORKFLOWS.length, + IMPACT_SCOPED_WORKFLOWS.length, "quality failures must fan out to full validation without reviving a cancelled stale run", ); - for (const output of ["ktlint_required", "android_lint_required", "unit_test_required", "screenshot_required"]) { + for (const output of [ + "ktlint_required", + "android_lint_required", + "unit_test_required", + "screenshot_required", + "dependency_review_required", + ]) { assert.match( entry, new RegExp(`needs\\.repository-quality\\.result != 'success' \\|\\| needs\\.repository-quality\\.outputs\\.${output} != 'false'`), @@ -237,6 +248,38 @@ test("heavy reusable workflows default to full validation and preserve every req assert.match(lint, /^ {4}if: inputs\.run_android_lint$/m); }); +test("dependency review keeps one required context and skips only its check step", async () => { + // #2258: job 자체를 if 로 건너뛰면 분류 job 실패와 «검사 불필요» 가 같은 skipped 로 보인다. + // job 은 늘 돌고, 검사 스텝만 PR 이벤트 + 의존성 입력 변경일 때 실행한다. 그 밖의 이벤트에서 + // 의존성 입력이 바뀌었으면 merge group 만 건너뛰고 나머지는 실패한다. + const source = await readWorkflow("dependency-review.yml"); + const jobs = jobNames(source); + + assert.deepEqual(jobs, ["dependency-review"]); + assert.doesNotMatch(source, /^ {4}if:/m); + assert.match(source, /^ {6}run_review:\n(?: {8}.+\n)*? {8}default: true\n {8}type: boolean$/m); + assert.match( + source, + /- name: Fail on newly introduced high-severity vulnerabilities\n\s+if: \$\{\{ inputs\.run_review && github\.event_name == 'pull_request' \}\}\n\s+uses: actions\/dependency-review-action@/, + ); + assert.match(source, /- name: Skip unchanged dependency graph inputs\n\s+if: \$\{\{ !inputs\.run_review \}\}/); + assert.match( + source, + /- name: Skip merge group revalidation\n\s+if: \$\{\{ inputs\.run_review && github\.event_name == 'merge_group' \}\}/, + ); + // bridge dispatch 커밋은 PR 스냅샷이 없다. 건너뛰면 이전 head 의 실패를 검사 없는 초록이 덮는다. + const rejectStart = source.indexOf("- name: Reject dependency changes without a pull request snapshot\n"); + assert.ok(rejectStart >= 0, "dispatch rejection step is missing"); + const rejectEnd = source.indexOf("\n - name:", rejectStart + 1); + const rejectStep = source.slice(rejectStart, rejectEnd < 0 ? undefined : rejectEnd); + assert.ok( + rejectStep.includes( + "if: ${{ inputs.run_review && github.event_name != 'pull_request' && github.event_name != 'merge_group' }}\n", + ), + ); + assert.match(rejectStep, /^ {10}exit 1$/m); +}); + test("repository quality owns fail-closed paginated impact classification and PR gates", async () => { const repositoryQuality = await readWorkflow("repository-quality.yml"); const unitTest = await readWorkflow("unit-test.yml"); diff --git a/.github/scripts/resolve-pr-impact.mjs b/.github/scripts/resolve-pr-impact.mjs index e60dc1a82..9e0aebe15 100644 --- a/.github/scripts/resolve-pr-impact.mjs +++ b/.github/scripts/resolve-pr-impact.mjs @@ -25,6 +25,7 @@ const IMPACT_POLICY_PATHS = new Set([ ".github/workflows/unit-test.yml", ".github/workflows/screenshot.yml", ".github/workflows/codeql.yml", + ".github/workflows/dependency-review.yml", // 기대 실패 목록(xfail)은 검증 정책 그 자체다 — 목록·메커니즘 변경은 모든 lane 으로 // fail-closed 해, 잘못 지운 항목이 그 자리에서 red 를 만들게 한다. ".github/ci-expected-failures.json", @@ -35,6 +36,50 @@ const IMPACT_POLICY_PATHS = new Set([ // 아키텍처 가드 전용 순수 JVM 모듈. src/main 이 없어 프로덕션 소스 분기를 탈 수 없고, // kover 도 안 붙어 coverageModules 로도 안 잡힌다 — 소스셋 판정에서 따로 건져야 한다. const KONSIST_PROJECT_PATH = ":konsist"; +// Dependency Review 를 깨우는 의존성 그래프 입력. 예전엔 dependency-review.yml 의 paths +// 필터였는데, 그러면 소스만 고친 PR 에 check 가 생기지 않아 required 로 걸 수 없었다 (#2258). +// 같은 glob 문법을 그대로 두고 job 안 분류로 옮긴다. 목록은 supply-chain-policy.test.mjs 가 고정한다. +export const DEPENDENCY_GRAPH_INPUT_PATTERNS = Object.freeze([ + "**/*.gradle", + "**/*.gradle.kts", + "**/*.lockfile", + "**/build-logic/**", + "**/buildSrc/**", + "**/gradle.properties", + "**/gradlew", + "**/gradlew.bat", + "gradle/**", + ".github/workflows/dependency-review.yml", + ".github/workflows/dependency-submission.yml", + ".github/workflows/dependency-submission-trusted.yml", + ".github/workflows/dependency-submission-upload.yml", +]); + +function globToRegExp(pattern) { + let source = ""; + for (let index = 0; index < pattern.length; ) { + if (pattern.startsWith("**/", index)) { + source += "(?:.*/)?"; + index += 3; + } else if (pattern.startsWith("**", index)) { + source += ".*"; + index += 2; + } else if (pattern[index] === "*") { + source += "[^/]*"; + index += 1; + } else { + source += pattern[index].replace(/[.+?^${}()|[\]\\]/g, "\\$&"); + index += 1; + } + } + return new RegExp(`^${source}$`); +} + +const DEPENDENCY_GRAPH_INPUT_MATCHERS = DEPENDENCY_GRAPH_INPUT_PATTERNS.map(globToRegExp); + +export function isDependencyGraphInput(filePath) { + return DEPENDENCY_GRAPH_INPUT_MATCHERS.some((matcher) => matcher.test(filePath)); +} function normalizePath(value) { return String(value ?? "") @@ -172,6 +217,7 @@ export function resolvePrImpact(changedFiles, modules, dependencies) { let runNodeTests = false; let codeqlActions = false; let codeqlJavaKotlin = false; + let dependencyReview = false; let forceFull = false; let repositoryQualityFixtures = false; @@ -214,6 +260,9 @@ export function resolvePrImpact(changedFiles, modules, dependencies) { if (filePath.startsWith(".github/workflows/") || filePath.startsWith(".github/actions/")) { codeqlActions = true; } + if (isDependencyGraphInput(filePath)) { + dependencyReview = true; + } if (filePath === ".editorconfig") { globalKtlintChange = true; } @@ -286,6 +335,10 @@ export function resolvePrImpact(changedFiles, modules, dependencies) { runKonsist = true; compileAndroidTest = true; } + if (forceFull) { + // 분류기 자신이나 처음 보는 경로가 바뀌면 의존성 입력이 없다고 단정하지 않는다. + dependencyReview = true; + } const productionAffected = reverseDependencyClosure(productionSeeds, reverseDependencies); if (productionAffected.size > 0) { @@ -353,6 +406,7 @@ export function resolvePrImpact(changedFiles, modules, dependencies) { screenshotTasks, codeqlActions, codeqlJavaKotlin, + dependencyReview, repositoryQualityFull: forceFull, repositoryQualityFixtures, }; @@ -430,6 +484,7 @@ export function githubOutputLines(impact) { screenshot_tasks: impact.screenshotTasks.join(" "), codeql_actions: impact.codeqlActions, codeql_java_kotlin: impact.codeqlJavaKotlin, + dependency_review_required: impact.dependencyReview, repository_quality_full: impact.repositoryQualityFull, repository_quality_fixtures: impact.repositoryQualityFixtures, }; diff --git a/.github/scripts/resolve-pr-impact.test.mjs b/.github/scripts/resolve-pr-impact.test.mjs index eacafa372..96c98d6c7 100644 --- a/.github/scripts/resolve-pr-impact.test.mjs +++ b/.github/scripts/resolve-pr-impact.test.mjs @@ -219,3 +219,28 @@ test("GitHub outputs preserve empty scopes and booleans", () => { assert.match(output, /^unit_test_tasks=$/m); assert.match(output, /^codeql_actions=false$/m); }); + +test("dependency review runs only for dependency graph inputs and fails closed", () => { + // #2258: Dependency Review 는 required 라 매 PR 에 check 가 생긴다. 검사 자체는 의존성 + // 입력이 바뀐 PR 에서만 하고, 분류가 확신할 수 없는 변경은 검사 쪽으로 닫는다. + const sourceOnly = resolvePrImpact(["feature/home/presentation/src/main/kotlin/Home.kt"], modules, dependencies); + assert.equal(sourceOnly.dependencyReview, false); + assert.match(githubOutputLines(sourceOnly).join("\n"), /^dependency_review_required=false$/m); + + for (const changed of [ + ["gradle/libs.versions.toml"], + ["feature/home/presentation/build.gradle.kts"], + ["build-logic/convention/src/main/kotlin/Convention.kt"], + [".github/workflows/dependency-submission.yml"], + [".github/workflows/dependency-review.yml"], + [".github/workflows/pr-validation.yml"], + [".github/scripts/resolve-pr-impact.mjs"], + ["unexpected/runtime-config.toml"], + ]) { + const impact = resolvePrImpact(changed, modules, dependencies); + assert.equal(impact.dependencyReview, true, changed[0]); + assert.match(githubOutputLines(impact).join("\n"), /^dependency_review_required=true$/m); + } + + assert.equal(resolvePrImpact(["README.md", "docs/qa/plan.md"], modules, dependencies).dependencyReview, false); +}); diff --git a/.github/scripts/supply-chain-policy.test.mjs b/.github/scripts/supply-chain-policy.test.mjs index 787af146b..0ca50431f 100644 --- a/.github/scripts/supply-chain-policy.test.mjs +++ b/.github/scripts/supply-chain-policy.test.mjs @@ -5,7 +5,11 @@ import path from 'node:path'; import test from 'node:test'; import { fileURLToPath } from 'node:url'; -import { inspectModules } from './resolve-pr-impact.mjs'; +import { + DEPENDENCY_GRAPH_INPUT_PATTERNS, + inspectModules, + isDependencyGraphInput, +} from './resolve-pr-impact.mjs'; const workflowDirectory = new URL('../workflows/', import.meta.url); const repoRoot = path.resolve(fileURLToPath(new URL('../..', import.meta.url))); @@ -122,18 +126,6 @@ function checkoutStepBlocks(source) { return blocks; } -function eventPathFilters(source, eventName) { - const match = new RegExp(`^ ${eventName}:\\n paths:\\n((?: - '[^']+'\\n)+)`, 'm').exec( - source, - ); - assert.ok(match, `${eventName} must declare a paths filter`); - return match[1].trim().split('\n').map((line) => { - const path = /^- '([^']+)'$/.exec(line.trim()); - assert.ok(path, `invalid dependency path filter: ${line}`); - return path[1]; - }); -} - function workflowName(source) { const match = /^name:\s*(.+)$/m.exec(source); assert.ok(match, 'workflow must declare a top-level name'); @@ -488,7 +480,7 @@ test('manual dependency baseline is hard-wired to the checked-out main SHA', asy assert.doesNotMatch(manualJob, /inputs\./); }); -test('all pull request heads produce a baseline while dependency review keeps its path filter', async () => { +test('all pull request heads produce a baseline while dependency review classifies inputs in the job', async () => { const submission = await readFile( new URL('../workflows/dependency-submission.yml', import.meta.url), 'utf8', @@ -504,7 +496,12 @@ test('all pull request heads produce a baseline while dependency review keeps it assert.doesNotMatch(submission, /^\s+(?:paths(?:-ignore)?|branches(?:-ignore)?|types):/m); assert.doesNotMatch(submission, /^\s+if:/m); assert.doesNotMatch(submission, /pull_request_target:|workflow_dispatch:/); - assert.deepEqual(eventPathFilters(review, 'pull_request'), dependencyPathFilters); + // #2258: paths 필터는 소스만 고친 PR 에 check 를 만들지 않아 required 로 걸 수 없다. + // 같은 경로 목록을 repository-quality 의 분류로 옮기고, review 는 PR Validation 이 매번 부른다. + assert.match(review, /^on:\n workflow_call:\n/m); + assert.doesNotMatch(review, /^\s+(?:paths(?:-ignore)?|branches(?:-ignore)?):/m); + assert.doesNotMatch(review, /^ (?:pull_request|merge_group|push):/m); + assert.deepEqual([...DEPENDENCY_GRAPH_INPUT_PATTERNS], dependencyPathFilters); assert.doesNotMatch(submission, /Detect dependency graph input changes/); assert.doesNotMatch(review, /Detect dependency graph input changes/); }); @@ -607,6 +604,44 @@ test('only the release pull request is exempt from the path-filter boundary', as assert.doesNotMatch(exemption[1], /exit\s+1/); }); +test('dependency graph input classification matches the former paths filter semantics', () => { + for (const filePath of [ + 'build.gradle.kts', + 'settings.gradle.kts', + 'app/build.gradle.kts', + 'legacy/build.gradle', + 'gradle.properties', + 'feature/home/gradle.properties', + 'gradlew', + 'gradlew.bat', + 'gradle/libs.versions.toml', + 'gradle/wrapper/gradle-wrapper.properties', + 'build-logic/convention/src/main/kotlin/AndroidLibrary.kt', + 'tools/buildSrc/src/main/kotlin/Plugin.kt', + 'app/dependencies.lockfile', + '.github/workflows/dependency-review.yml', + '.github/workflows/dependency-submission.yml', + '.github/workflows/dependency-submission-trusted.yml', + '.github/workflows/dependency-submission-upload.yml', + ]) { + assert.equal(isDependencyGraphInput(filePath), true, filePath); + } + for (const filePath of [ + 'README.md', + 'docs/gradle.md', + 'app/src/main/kotlin/MainActivity.kt', + 'app/src/main/res/values/strings.xml', + 'feature/gradle/Screen.kt', + 'build-logic', + 'gradle', + 'gradlew.sh', + 'app/build.gradle.kts.orig', + '.github/workflows/pr-validation.yml', + ]) { + assert.equal(isDependencyGraphInput(filePath), false, filePath); + } +}); + test('dependency review blocks high severity changes without enforcing a license allowlist', async () => { const source = await readFile(new URL('../workflows/dependency-review.yml', import.meta.url), 'utf8'); diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml index d230020ef..5c314dd8b 100644 --- a/.github/workflows/dependency-review.yml +++ b/.github/workflows/dependency-review.yml @@ -1,25 +1,26 @@ name: Dependency Review +# PR 에서는 pr-validation.yml 이 매번 부른다 (#2258). 예전에는 의존성 파일 paths 필터로 +# 직접 pull_request 를 들었는데, 그러면 소스만 고친 PR 에 check 가 생기지 않아 ruleset 의 +# required context 로 걸 수 없었다. 이제 repository-quality 가 같은 경로 목록으로 변경을 +# 분류하고, 의존성 입력이 없으면 아래 job 이 검사 스텝만 건너뛴 채 성공한다. 분류가 +# 실패하면 호출자가 run_review 를 true 로 넘겨 검사 쪽으로 닫힌다. +# +# 검사는 pull_request 이벤트에서만 할 수 있다. dependency-submission.yml 이 PR 스냅샷을 +# pull_request 에서만 만들기 때문이다. +# - merge group 은 건너뛴다. 큐에 들어온 PR 은 각 head 에서 이 required check 를 이미 +# 통과했고, 큐 SHA 는 그 head 들과 develop 의 병합이다. +# - bridge dispatch(GITHUB_TOKEN 커밋 뒤 PR Validation 재호출)는 의존성 입력이 바뀐 PR 이면 +# 실패로 닫는다. 건너뛰면 이전 head 의 실패를 검사 없는 초록이 덮는다. PR 브랜치에 새로 +# push 하면 pull_request 실행이 스냅샷과 함께 다시 검사한다. on: - pull_request: - paths: - - '**/*.gradle' - - '**/*.gradle.kts' - - '**/*.lockfile' - - '**/build-logic/**' - - '**/buildSrc/**' - - '**/gradle.properties' - - '**/gradlew' - - '**/gradlew.bat' - - 'gradle/**' - - '.github/workflows/dependency-review.yml' - - '.github/workflows/dependency-submission.yml' - - '.github/workflows/dependency-submission-trusted.yml' - - '.github/workflows/dependency-submission-upload.yml' - -concurrency: - group: dependency-review-${{ github.event.pull_request.number }} - cancel-in-progress: true + workflow_call: + inputs: + run_review: + description: "의존성 그래프 입력이 바뀌어 검사가 필요한지 여부" + required: false + default: true + type: boolean permissions: contents: read @@ -30,11 +31,28 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 30 steps: + - name: Skip unchanged dependency graph inputs + if: ${{ !inputs.run_review }} + run: echo "::notice::의존성 그래프 입력이 바뀌지 않아 Dependency Review 를 건너뜁니다." + + - name: Skip merge group revalidation + if: ${{ inputs.run_review && github.event_name == 'merge_group' }} + run: echo "::notice::큐에 들어온 PR head 가 이미 Dependency Review 를 통과했으므로 merge group 에서는 건너뜁니다." + + - name: Reject dependency changes without a pull request snapshot + if: ${{ inputs.run_review && github.event_name != 'pull_request' && github.event_name != 'merge_group' }} + env: + EVENT_NAME: ${{ github.event_name }} + run: | + echo "::error::$EVENT_NAME 실행에는 비교할 PR 의존성 스냅샷이 없습니다. 의존성 입력이 바뀐 PR 은 브랜치에 새로 push 해 pull_request 실행으로 검사하세요." + exit 1 + # PR snapshot 은 두 workflow 를 거쳐 API 에 올라온다 (fork PR 을 checkout 하지 않기 위한 분리다). # 1. Generate PR Dependency Graph — PR context 에서 artifact 만 만든다. # 2. Submit PR Dependency Graph — trusted workflow_run 에서 artifact 를 제출한다. # 이 job 은 1번과 동시에 출발하므로 제출이 끝날 때까지 snapshot warning 을 재시도한다. - name: Fail on newly introduced high-severity vulnerabilities + if: ${{ inputs.run_review && github.event_name == 'pull_request' }} uses: actions/dependency-review-action@a1d282b36b6f3519aa1f3fc636f609c47dddb294 # v5.0.0 with: fail-on-scopes: runtime, development, unknown diff --git a/.github/workflows/pr-validation.yml b/.github/workflows/pr-validation.yml index e33f356dc..2f7b17551 100644 --- a/.github/workflows/pr-validation.yml +++ b/.github/workflows/pr-validation.yml @@ -1,7 +1,7 @@ name: PR Validation # PR 검증의 유일한 진입점이다. repository quality 가 변경 파일을 완전하게 분류한 뒤 -# static analysis · unit test · screenshot 을 reusable workflow 로 한 번씩 부른다. +# static analysis · unit test · screenshot · dependency review 를 reusable workflow 로 한 번씩 부른다. # 각 job check 를 ruleset 에서 직접 required 로 요구하므로 문서 전용 PR 도 같은 이름의 # check 를 만들되, 무거운 job 내부는 즉시 건너뛴다. 모든 검증 뒤 집계용 runner 는 없다. # required context 이름은 pr-gate-policy.test.mjs 에 고정해 workflow 이름 변경과 ruleset @@ -87,3 +87,11 @@ jobs: run_validation: ${{ needs.repository-quality.result != 'success' || needs.repository-quality.outputs.screenshot_required != 'false' }} gradle_tasks: ${{ needs.repository-quality.result == 'success' && needs.repository-quality.outputs.screenshot_tasks || ':core:ui:validateScreenshotTest :feature:home:presentation:validateScreenshotTest :feature:receiver:presentation:validateScreenshotTest :feature:onboarding:presentation:validateScreenshotTest :feature:afternote:presentation:validateScreenshotTest :feature:mindrecord:presentation:validateScreenshotTest :feature:setting:presentation:validateScreenshotTest :feature:timeletter:presentation:validateScreenshotTest' }} screenshot_modules: ${{ needs.repository-quality.result == 'success' && needs.repository-quality.outputs.screenshot_modules || ':core:ui :feature:home:presentation :feature:receiver:presentation :feature:onboarding:presentation :feature:afternote:presentation :feature:mindrecord:presentation :feature:setting:presentation :feature:timeletter:presentation' }} + + dependency-review: + name: Dependency Review + needs: repository-quality + if: ${{ !cancelled() }} + uses: ./.github/workflows/dependency-review.yml + with: + run_review: ${{ needs.repository-quality.result != 'success' || needs.repository-quality.outputs.dependency_review_required != 'false' }} diff --git a/.github/workflows/repository-quality.yml b/.github/workflows/repository-quality.yml index e333f8267..184af6323 100644 --- a/.github/workflows/repository-quality.yml +++ b/.github/workflows/repository-quality.yml @@ -54,6 +54,9 @@ on: codeql_java_kotlin: description: "Java/Kotlin CodeQL 분석이 필요한지 여부" value: ${{ jobs.repository-quality.outputs.codeql_java_kotlin }} + dependency_review_required: + description: "의존성 그래프 입력이 바뀌어 Dependency Review 가 필요한지 여부" + value: ${{ jobs.repository-quality.outputs.dependency_review_required }} push: branches: [develop, main] @@ -87,6 +90,7 @@ jobs: screenshot_tasks: ${{ steps.classify-documentation-changes.outputs.screenshot_tasks }} codeql_actions: ${{ steps.classify-documentation-changes.outputs.codeql_actions }} codeql_java_kotlin: ${{ steps.classify-documentation-changes.outputs.codeql_java_kotlin }} + dependency_review_required: ${{ steps.classify-documentation-changes.outputs.dependency_review_required }} steps: - name: Clone repo uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 @@ -109,7 +113,8 @@ jobs: run: node .github/scripts/ensure-api-quota.mjs ensure --max-wait 60 # GitHub 의 server-side paths 평가는 유한한 diff 앞부분만 보므로, 그 경계를 넘는 PR 을 - # 그대로 허용하면 뒤쪽 dependency 파일이 graph/review workflow 를 우회할 수 있다. + # 그대로 허용하면 뒤쪽 파일이 paths 필터를 쓰는 workflow 를 우회할 수 있다. Dependency + # Review 는 #2258 부터 아래 전체 파일 분류로 판정하지만 다른 paths 필터 workflow 가 남아 있다. # 공식 문서의 상충하는 300/3,000 한도 중 보수적인 300을 fail-closed 경계로 쓴다. - name: Guard server-side path filter boundary id: changed-files