From de49a612073d2e69a4324b2931a84ac487944d50 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 6 Sep 2026 22:36:41 +0900 Subject: [PATCH 1/4] test(ci): reproduce final classifier backoff without retry --- tests/test_docs_only_pr_runner_admission.py | 46 +++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/tests/test_docs_only_pr_runner_admission.py b/tests/test_docs_only_pr_runner_admission.py index 119e6fe766..fe35399d84 100644 --- a/tests/test_docs_only_pr_runner_admission.py +++ b/tests/test_docs_only_pr_runner_admission.py @@ -303,6 +303,52 @@ def test_strix_invalid_or_unreadable_admission_fails_the_metadata_job( assert len(calls) == (1 if scenario == "api-error" else 0) +@pytest.mark.parametrize("filename", GATE_WORKFLOWS) +@pytest.mark.parametrize("success_attempt", (0, 1, 2, 3)) +def test_classifier_sleeps_only_before_another_attempt( + tmp_path: Path, filename: str, success_attempt: int +) -> None: + """Run the real classifier without network or waits; preserve retry coverage.""" + calls = tmp_path / "calls" + outputs = tmp_path / "outputs" + prelude = ''' +attempt_count=0 +gh() { + printf 'request\\n' >> "$CALLS" + attempt_count=$(wc -l < "$CALLS") + if [ "$SUCCESS_ATTEMPT" -gt 0 ] && [ "$attempt_count" -eq "$SUCCESS_ATTEMPT" ]; then + printf 'docs/readme.md\\n' + else + return 1 + fi +} +sleep() { printf '%s\\n' "$1" >> "$SLEEPS"; } +''' + sleeps = tmp_path / "sleeps" + job = _top_level_job_block(_read(filename), "changed-scope") + result = subprocess.run( + [shutil.which("bash") or "/bin/bash", "-c", prelude + _step_shell(job, "Classify changed paths")], + env={ + "PATH": "/usr/bin:/bin", + "CALLS": str(calls), + "SLEEPS": str(sleeps), + "GITHUB_OUTPUT": str(outputs), + "SUCCESS_ATTEMPT": str(success_attempt), + "REPO": "owner/repo", + "PR": "17", + "EXPECTED_FILES": "1", + }, + capture_output=True, + text=True, + check=False, + ) + assert result.returncode == 0, result.stderr + assert len(calls.read_text().splitlines()) == (success_attempt or 3) + assert (sleeps.read_text().splitlines() if sleeps.exists() else []) == ["3", "6"][: (success_attempt or 3) - 1] + expected = "false" if success_attempt else "true" + assert _read_outputs(outputs) == {"code": expected, "deps": expected} + + def test_gate_classifier_shell_is_byte_identical_across_the_workflows(): """The shared changed-path classifier shell must not drift.""" classifier_bodies = set() From ffdc11686ed0fddc03a5d557cc8e2e54cf89c21d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 6 Sep 2026 22:37:59 +0900 Subject: [PATCH 2/4] fix(ci): skip backoff after the final scope lookup --- .github/workflows/sast-semgrep.yml | 2 +- .github/workflows/security-scan.yml | 2 +- .github/workflows/strix.yml | 2 +- .../required-workflow-path-filter-boundary.md | 16 ++++++++++++++++ 4 files changed, 19 insertions(+), 3 deletions(-) diff --git a/.github/workflows/sast-semgrep.yml b/.github/workflows/sast-semgrep.yml index 12b7013da3..b9991ae699 100644 --- a/.github/workflows/sast-semgrep.yml +++ b/.github/workflows/sast-semgrep.yml @@ -76,7 +76,7 @@ jobs: break fi changed="" - sleep $((attempt * 3)) + if [ "$attempt" -lt 3 ]; then sleep $((attempt * 3)); fi done # GitHub caps /pulls/N/files at 3000 entries; a short list would hide # source files behind a doc-only verdict, so require an exact count. diff --git a/.github/workflows/security-scan.yml b/.github/workflows/security-scan.yml index 500e22b4ab..ff6651238d 100644 --- a/.github/workflows/security-scan.yml +++ b/.github/workflows/security-scan.yml @@ -99,7 +99,7 @@ jobs: break fi changed="" - sleep $((attempt * 3)) + if [ "$attempt" -lt 3 ]; then sleep $((attempt * 3)); fi done # GitHub caps /pulls/N/files at 3000 entries; a short list would hide # source files behind a doc-only verdict, so require an exact count. diff --git a/.github/workflows/strix.yml b/.github/workflows/strix.yml index 24d5a27af0..10202f0199 100644 --- a/.github/workflows/strix.yml +++ b/.github/workflows/strix.yml @@ -181,7 +181,7 @@ jobs: break fi changed="" - sleep $((attempt * 3)) + if [ "$attempt" -lt 3 ]; then sleep $((attempt * 3)); fi done # GitHub caps /pulls/N/files at 3000 entries; a short list would hide # source files behind a doc-only verdict, so require an exact count. diff --git a/docs/doctoring/required-workflow-path-filter-boundary.md b/docs/doctoring/required-workflow-path-filter-boundary.md index 65abac141a..0a58bbc772 100644 --- a/docs/doctoring/required-workflow-path-filter-boundary.md +++ b/docs/doctoring/required-workflow-path-filter-boundary.md @@ -207,6 +207,22 @@ repository: `changed-scope` (and `detect-languages` for CodeQL) succeed while `scorecard` report `skipped`, and the **run conclusion** is `success`, not `skipped`. +## Final-attempt backoff correction (2026-09-06, proposed) + +The three current classifier copies in Security Scan, Semgrep, and Strix +slept after all three failed file-list requests, including nine seconds after +the final request when no retry remained. Keep three requests and the first +three- and six-second backoffs; omit only the final sleep. In the all-failed +path, requested sleep totals fall from 18 to 9 seconds (50%), not a measured +50% reduction in job runtime or organization queue occupancy. Success paths +and incomplete-list full scanning are unchanged. + +The production-shell regression replaces only GitHub and sleep at the test +boundary. Across all three workflows, first/second/third success and complete +failure cover 12 cases. The RED commit `de49a612` produced 3 failures and +9 passes; all failures recorded an extra final sleep. No provider timeout, +trigger, permission, required context, or scanner policy is changed. + ## Safety boundary This repair does not weaken any scanner's actual coverage. Every gate From 993cee1b766a7d062d1431387793b0e662bbf427 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 7 Sep 2026 00:08:20 +0900 Subject: [PATCH 3/4] test(ci): cover CodeQL classifier final backoff --- tests/test_docs_only_pr_runner_admission.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/tests/test_docs_only_pr_runner_admission.py b/tests/test_docs_only_pr_runner_admission.py index fe35399d84..109b27c057 100644 --- a/tests/test_docs_only_pr_runner_admission.py +++ b/tests/test_docs_only_pr_runner_admission.py @@ -303,7 +303,7 @@ def test_strix_invalid_or_unreadable_admission_fails_the_metadata_job( assert len(calls) == (1 if scenario == "api-error" else 0) -@pytest.mark.parametrize("filename", GATE_WORKFLOWS) +@pytest.mark.parametrize("filename", (*GATE_WORKFLOWS, "codeql-pr.yml")) @pytest.mark.parametrize("success_attempt", (0, 1, 2, 3)) def test_classifier_sleeps_only_before_another_attempt( tmp_path: Path, filename: str, success_attempt: int @@ -325,7 +325,8 @@ def test_classifier_sleeps_only_before_another_attempt( sleep() { printf '%s\\n' "$1" >> "$SLEEPS"; } ''' sleeps = tmp_path / "sleeps" - job = _top_level_job_block(_read(filename), "changed-scope") + job_name = "detect-languages" if filename == "codeql-pr.yml" else "changed-scope" + job = _top_level_job_block(_read(filename), job_name) result = subprocess.run( [shutil.which("bash") or "/bin/bash", "-c", prelude + _step_shell(job, "Classify changed paths")], env={ @@ -346,7 +347,10 @@ def test_classifier_sleeps_only_before_another_attempt( assert len(calls.read_text().splitlines()) == (success_attempt or 3) assert (sleeps.read_text().splitlines() if sleeps.exists() else []) == ["3", "6"][: (success_attempt or 3) - 1] expected = "false" if success_attempt else "true" - assert _read_outputs(outputs) == {"code": expected, "deps": expected} + expected_outputs = {"code": expected} + if filename != "codeql-pr.yml": + expected_outputs["deps"] = expected + assert _read_outputs(outputs) == expected_outputs def test_gate_classifier_shell_is_byte_identical_across_the_workflows(): From f8937d7d56fb81f1c98dbfdee604b015e74e7b7d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 7 Sep 2026 00:08:43 +0900 Subject: [PATCH 4/4] fix(ci): omit CodeQL final scope retry sleep --- .github/workflows/codeql-pr.yml | 4 +++- .../required-workflow-path-filter-boundary.md | 11 +++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/.github/workflows/codeql-pr.yml b/.github/workflows/codeql-pr.yml index cb07ad2fab..182eac1989 100644 --- a/.github/workflows/codeql-pr.yml +++ b/.github/workflows/codeql-pr.yml @@ -115,7 +115,9 @@ jobs: break fi changed="" - sleep $((attempt * 3)) + if [ "$attempt" -lt 3 ]; then + sleep $((attempt * 3)) + fi done # GitHub caps /pulls/N/files at 3000 entries; a short list would hide # source files behind a doc-only verdict, so require an exact count. diff --git a/docs/doctoring/required-workflow-path-filter-boundary.md b/docs/doctoring/required-workflow-path-filter-boundary.md index 0a58bbc772..860c4a01d3 100644 --- a/docs/doctoring/required-workflow-path-filter-boundary.md +++ b/docs/doctoring/required-workflow-path-filter-boundary.md @@ -223,6 +223,17 @@ failure cover 12 cases. The RED commit `de49a612` produced 3 failures and 9 passes; all failures recorded an extra final sleep. No provider timeout, trigger, permission, required context, or scanner policy is changed. +### CodeQL 동일 경로 보완 (2026-09-07, Proposed) + +CodeQL의 `detect-languages` 안에도 같은 마지막 대기가 남아 있었다. +기존 셸 실행 테스트에 CodeQL을 추가한 `993cee1b`에서 1개 실패와 +15개 성공을 확인했다. 실패 경로는 세 번 요청한 뒤 3·6·9초 대기를 +기록했다. 마지막 대기만 제거하며, 세 번의 요청과 앞선 3·6초 대기, +조회 실패 시 `code=true`로 전체 검사하는 동작은 유지한다. +네 workflow의 성공 시점 세 가지와 전체 실패를 합쳐 16개 경로를 검증한다. +이는 해당 경로의 요청 대기를 18초에서 9초로 줄이는 수정이며, +조직 전체 적체나 실제 job 실행 시간이 50% 줄었다는 근거는 아니다. + ## Safety boundary This repair does not weaken any scanner's actual coverage. Every gate