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/.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..860c4a01d3 100644 --- a/docs/doctoring/required-workflow-path-filter-boundary.md +++ b/docs/doctoring/required-workflow-path-filter-boundary.md @@ -207,6 +207,33 @@ 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. + +### 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 diff --git a/tests/test_docs_only_pr_runner_admission.py b/tests/test_docs_only_pr_runner_admission.py index 119e6fe766..109b27c057 100644 --- a/tests/test_docs_only_pr_runner_admission.py +++ b/tests/test_docs_only_pr_runner_admission.py @@ -303,6 +303,56 @@ 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, "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 +) -> 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_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={ + "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" + 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(): """The shared changed-path classifier shell must not drift.""" classifier_bodies = set()