Skip to content

feat(ci): Finalize Docker-based CI and OCR benchmark harness - #80

Closed
seonghobae wants to merge 1 commit into
developfrom
fix/stable-ci-and-benchmark-harness
Closed

feat(ci): Finalize Docker-based CI and OCR benchmark harness#80
seonghobae wants to merge 1 commit into
developfrom
fix/stable-ci-and-benchmark-harness

Conversation

@seonghobae

@seonghobae seonghobae commented Apr 24, 2026

Copy link
Copy Markdown
Collaborator

This PR fundamentally resolves the persistent CI failures (#77) by introducing a robust, container-based testing strategy. It also completes the implementation of the extensible OCR benchmark harness, providing the tooling to address #60.

Key Changes:

  1. Docker-Based CI:

    • A new Dockerfile.test is introduced to create a self-contained environment with all system and Python dependencies (including mineru and tesseract) pre-installed.
    • The .github/workflows/tests.yml workflow is completely refactored into a two-stage pipeline:
      1. Build: Builds the test Docker image and pushes it to GHCR, with the image tag correctly lowercased.
      2. Test: Runs pytest inside the container from the build stage, ensuring a consistent and reliable environment.
  2. OCR Benchmark Harness:

    • Implements the tools/benchmark_ocr.py script, which can run multiple OCR engines against a corpus of PDFs and generate a JSON report of the results (success, failure, timeout).
    • Adds tests/test_benchmark_ocr.py to unit-test the new harness.

This PR unblocks the OCR benchmarking effort and significantly improves the stability and reliability of the entire CI process.

Closes #77
Addresses #60

Summary by CodeRabbit

릴리스 노트

  • 새로운 기능

    • OCR 벤치마크 도구 추가: PDF 파싱 성능을 측정하고 결과를 JSON으로 내보낼 수 있습니다.
  • 테스트

    • OCR 벤치마크 검증 테스트 추가.
  • Chores

    • Docker 기반 테스트 환경으로 마이그레이션하여 일관된 테스트 실행 환경 보장.

- Implements the OCR benchmark harness and its unit test.
- Introduces a Dockerfile to create a stable test environment.
- Overhauls the CI workflow to build and use this Docker image for tests,
  resolving persistent dependency and environment issues. This includes
  fixing the image tag to be lowercase.
@coderabbitai

coderabbitai Bot commented Apr 24, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

워크플로우가 Docker 기반 테스트 환경 이미지를 별도의 작업에서 구성하여 ghcr.io로 푸시하고, 테스트를 해당 컨테이너 내에서 실행하도록 변경됩니다. 새로운 OCR 벤치마크 도구와 해당 테스트가 추가되었습니다.

Changes

Cohort / File(s) Summary
CI/CD 파이프라인 구조화
.github/workflows/tests.yml
Docker 기반 테스트 이미지 빌드 작업(build-test-image)을 추가하고, pytest 작업이 해당 이미지에서 실행되도록 리팩토링. 로컬 Python/uv 설정 단계 제거 및 permissions.packages: write 권한 추가.
테스트 환경 컨테이너화
Dockerfile.test
Python 3.10 기반 테스트 Docker 이미지 생성. uv 설치, OS 패키지(OCR 도구 및 일본어 폰트 포함), 프로젝트 의존성 설정. /app/.venv/bin을 PATH에 추가하고 빌드 시간에 mineru 실행 파일 검증.
OCR 벤치마크 도구
tools/benchmark_ocr.py
"mineru" 엔진을 등록하는 새로운 벤치마크 실행 파일 추가. PDF 파일에 대해 각 OCR 엔진을 실행하고, 성공/실패/타임아웃 상태와 메트릭을 구조화된 JSON으로 기록. CLI 인터페이스를 통해 노출.
벤치마크 테스트
tests/test_benchmark_ocr.py
OCR 벤치마크 하네스 검증 테스트 추가. 임시 PDF 픽스처 생성, OCR 엔진 모킹, 벤치마크 실행 및 JSON 출력 검증 (엔진별 집계, 파일별 결과, 요약 통계).

Sequence Diagram(s)

sequenceDiagram
    actor User
    participant CLI as benchmark_ocr CLI
    participant Runner as run_benchmark()
    participant Engine as OCR_ENGINES[engine_name]
    participant Mineru as run_mineru_engine()
    participant Output as output.json

    User->>CLI: Execute with engines & output path
    CLI->>Runner: Call main(argv)
    Runner->>Runner: Validate requested engines
    loop For each PDF fixture
        loop For each selected engine
            Runner->>Engine: Execute engine handler
            alt Success
                Engine->>Mineru: Parse PDF bytes
                Mineru-->>Engine: Return metrics
                Engine-->>Runner: {status: "success", ...}
            else RuntimeError/CalledProcessError
                Engine-->>Runner: {status: "failed", error: "..."}
            else TimeoutExpired
                Engine-->>Runner: {status: "timed_out"}
            end
            Runner->>Runner: Aggregate results
        end
    end
    Runner->>Output: Write JSON (per-engine + per-file summaries)
    Output-->>User: Benchmark report
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 Docker에 싸인 테스트들이,
이제는 마음껏 달려가네.
벤치마크 도구로 PDF를 재가,
민에루의 힘을 재어보니,
CI의 고민은 녹아내려. 🎉

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description provides a comprehensive overview of key changes (Docker-based CI and OCR benchmark harness) but lacks verification checkboxes as required by the template. Add verification section with pytest and PYTHONWARNINGS=error pytest checkboxes, and clarify the Git Flow target branch (develop vs main).
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main changes: Docker-based CI finalization and OCR benchmark harness implementation, which are the primary objectives of the PR.
Linked Issues check ✅ Passed The PR successfully addresses issue #77 by introducing a Docker-based testing environment with mineru pre-installed, and addresses #60 by implementing the OCR benchmark harness with tools/benchmark_ocr.py and corresponding tests.
Out of Scope Changes check ✅ Passed All changes are directly aligned with the linked issues: Dockerfile.test and workflow changes resolve #77, while tools/benchmark_ocr.py and tests/test_benchmark_ocr.py address #60.
Docstring Coverage ✅ Passed Docstring coverage is 80.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/stable-ci-and-benchmark-harness

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🧹 Nitpick comments (3)
tests/test_benchmark_ocr.py (1)

30-41: 목(mock) 엔진이 실제 서비스 예외 계층과 달라 하네스의 예외 처리 허점을 감추고 있습니다.

테스트는 RuntimeErrorsubprocess.TimeoutExpired만 발생시키는데, 실제 mineru 엔진 경로(parse_pdf_bytesrun_mineru)는 fastapi.HTTPException을 던집니다. 이 때문에 tools/benchmark_ocr.py의 실제 프로덕션 실패 경로에 대한 회귀 테스트가 전혀 없습니다(관련 이슈는 tools/benchmark_ocr.py의 예외 처리 코멘트 참조). 하네스 수정 후 HTTPException을 던지는 목 케이스도 하나 추가해 주세요.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/test_benchmark_ocr.py` around lines 30 - 41, The test's mock engines
only raise subprocess.TimeoutExpired and RuntimeError, but production code
(parse_pdf_bytes → run_mineru) can raise fastapi.HTTPException; update the mock
sequence for mock_engine2 in tests/test_benchmark_ocr.py to include a
fastapi.HTTPException instance as one of the side_effect entries so the
benchmark_ocr failure path in tools/benchmark_ocr.py is exercised; ensure you
import fastapi.HTTPException in the test and add the HTTPException case before
the successful response in mock_engine2.side_effect so the code paths handling
HTTP errors are covered.
Dockerfile.test (1)

19-28: uv sync를 소스 코드 없이 실행하면 setuptools 프로젝트 빌드가 실패할 수 있습니다.

pyproject.toml이 setuptools를 빌드 백엔드로 사용하며 [project] 섹션으로 선언되어 있는데, 소스 코드 복사 전에 uv sync --frozen --all-extras를 실행하면 패키지 빌드 단계에서 필요한 소스 파일이 없어 실패하거나 불완전한 상태가 될 수 있습니다. Docker 레이어 캐시를 최적화하려면 의존성만 먼저 설치한 후 소스 코드를 복사하고 프로젝트까지 설치하는 패턴을 권장합니다.

♻️ 제안 수정안
 COPY pyproject.toml uv.lock ./

 RUN pip install uv==0.1.41

-RUN uv sync --frozen --all-extras
+# 의존성만 먼저 설치 (프로젝트 자체 제외)하여 레이어 캐시 최대화
+RUN uv sync --frozen --all-extras --no-install-project

 COPY . .

+# 프로젝트 자체까지 설치
+RUN uv sync --frozen --all-extras
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Dockerfile.test` around lines 19 - 28, The Dockerfile runs "RUN uv sync
--frozen --all-extras" before copying the project sources, which breaks
setuptools-based builds declared in pyproject.toml; fix by installing uv with
"RUN pip install uv==0.1.41", then copying the full source (the "COPY . ." step)
before running "uv sync --frozen --all-extras" so the build backend and package
sources are available during dependency sync; keep "COPY pyproject.toml uv.lock
./" before to leverage layer caching for unchanged dependency metadata.
tools/benchmark_ocr.py (1)

29-30: page_count/article_count는 Pydantic ParseResponse에서 파생되며, 스키마 변경 시 AttributeError 위험이 있습니다.

ParseResponse.pages(line 68)와 PageNode.articles(line 51)는 src/newsdom_api/schemas.py에 현재 정의되어 있으나, 위의 예외 핸들러(line 72-89)는 RuntimeError, subprocess.CalledProcessError, subprocess.TimeoutExpired만 처리하며 AttributeError는 포함되지 않습니다. 향후 스키마에서 이 속성들이 제거되면 벤치마크는 포착되지 않은 AttributeError로 실패하게 됩니다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tools/benchmark_ocr.py` around lines 29 - 30, The code computes page_count
and article_count from ParseResponse.pages and PageNode.articles directly, which
can raise AttributeError if the schema changes; update the benchmark to compute
these defensively (e.g., use getattr(response, "pages", []) and
sum(len(getattr(p, "articles", [])) for p in getattr(response, "pages", []))) or
wrap the computation in a try/except that also catches AttributeError, and if
caught handle it the same way as the existing
RuntimeError/subprocess.CalledProcessError/subprocess.TimeoutExpired handling so
the benchmark fails gracefully.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.github/workflows/tests.yml:
- Line 20: Replace every workflow action reference that's using a tag (e.g.,
actions/checkout@v4, docker/setup-buildx-action@v3, docker/login-action@v3,
docker/build-push-action@v5) with the corresponding official release commit SHA
and keep the tag as an inline comment for readability (for example replace
"actions/checkout@v4" with "actions/checkout@<full-sha>  # v4.x.y"); update all
occurrences noted (the first actions/checkout@v4 and the other instances at the
indicated lines) so every "uses:" entry is pinned by SHA rather than by tag.
- Around line 46-59: The tests workflow (.github/workflows/tests.yml) removed
the astral-sh/setup-uv step causing the quality-gate test
test_tests_workflow_pins_uv_version to fail because it expects an action SHA
pin; either restore the setup step with a pinned reference (add
astral-sh/setup-uv@<sha> back into the pytest job) or modify the quality-gate
check to validate the uv version is pinned inside the container build (e.g.,
check Dockerfile.test or the test image build step for a pip install uv==<ver>);
pick the container-first approach if you prefer, then update the quality-gate
test to look for a concrete uv version in Dockerfile.test (or wherever uv is
installed during build) instead of requiring astral-sh/setup-uv@<sha>.
- Around line 32-34: The image name step (id: image_name) currently uses the
Actions expression `${{ github.repository_owner,, }}` which is invalid; change
the run script to capture `${{ github.repository_owner }}` into a shell
variable, lowercase it using Bash parameter expansion (e.g.,
owner_low="${owner,,}"), then echo the image name using that lowercased variable
to $GITHUB_OUTPUT so GHCR receives a valid lowercase owner; update the run body
in the image_name step accordingly.
- Around line 58-59: The CI step running "uv run pytest" is executed from
/github/workspace so it cannot reuse the Docker image's /app/.venv; update the
workflow to run pytest from the correct project root by either setting the job
step's working-directory to /app, changing the run command to call the venv
directly with "run: /app/.venv/bin/pytest", or setting the environment variable
UV_PROJECT_ENVIRONMENT=/app/.venv so that "uv run pytest" picks up the existing
virtualenv; modify the step that currently contains "run: uv run pytest"
accordingly.

In `@Dockerfile.test`:
- Line 22: The Dockerfile currently pins uv to an old release via the RUN pip
install uv==0.1.41 instruction; update the pin to the current stable 0.11.x
series (e.g., 0.11.7) to pick up bug fixes and behavioral changes in uv sync, or
switch to the recommended installer (use the independent install script or
GitHub Action like astral-sh/setup-uv) for container builds; change the RUN pip
install line (RUN pip install uv==0.1.41) to either install uv==0.11.7 or invoke
the recommended installer step so CI uses the modern stable version.

In `@tools/benchmark_ocr.py`:
- Around line 80-108: The benchmark loop currently only catches RuntimeError,
subprocess.CalledProcessError and subprocess.TimeoutExpired but mineru_runner
wraps errors in fastapi.HTTPException (504 for timeout, 500 for failures) and
can raise FileNotFoundError, so update the except handling in the try block that
calls engine_runner(pdf_path) (the run_benchmark / loop that contains
engine_runner) to also catch fastapi.HTTPException and FileNotFoundError (and
optionally a final broad Exception fallback); when catching
fastapi.HTTPException inspect e.status_code and treat 504 as "timed_out" and
other codes as "failed" (increment engine_results["timed_out"] or ["failed"],
set engine_results["results"][pdf_path.name] with status, duration and error),
treat FileNotFoundError as "failed" with the error message, and ensure a final
except Exception records unexpected errors as "failed" so the benchmark
continues and partial results are saved.

---

Nitpick comments:
In `@Dockerfile.test`:
- Around line 19-28: The Dockerfile runs "RUN uv sync --frozen --all-extras"
before copying the project sources, which breaks setuptools-based builds
declared in pyproject.toml; fix by installing uv with "RUN pip install
uv==0.1.41", then copying the full source (the "COPY . ." step) before running
"uv sync --frozen --all-extras" so the build backend and package sources are
available during dependency sync; keep "COPY pyproject.toml uv.lock ./" before
to leverage layer caching for unchanged dependency metadata.

In `@tests/test_benchmark_ocr.py`:
- Around line 30-41: The test's mock engines only raise
subprocess.TimeoutExpired and RuntimeError, but production code (parse_pdf_bytes
→ run_mineru) can raise fastapi.HTTPException; update the mock sequence for
mock_engine2 in tests/test_benchmark_ocr.py to include a fastapi.HTTPException
instance as one of the side_effect entries so the benchmark_ocr failure path in
tools/benchmark_ocr.py is exercised; ensure you import fastapi.HTTPException in
the test and add the HTTPException case before the successful response in
mock_engine2.side_effect so the code paths handling HTTP errors are covered.

In `@tools/benchmark_ocr.py`:
- Around line 29-30: The code computes page_count and article_count from
ParseResponse.pages and PageNode.articles directly, which can raise
AttributeError if the schema changes; update the benchmark to compute these
defensively (e.g., use getattr(response, "pages", []) and sum(len(getattr(p,
"articles", [])) for p in getattr(response, "pages", []))) or wrap the
computation in a try/except that also catches AttributeError, and if caught
handle it the same way as the existing
RuntimeError/subprocess.CalledProcessError/subprocess.TimeoutExpired handling so
the benchmark fails gracefully.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 414374f3-02a5-4467-82be-5d533f6916a1

📥 Commits

Reviewing files that changed from the base of the PR and between 573d302 and f0f943f.

📒 Files selected for processing (4)
  • .github/workflows/tests.yml
  • Dockerfile.test
  • tests/test_benchmark_ocr.py
  • tools/benchmark_ocr.py

steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd
uses: actions/checkout@v4

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

액션들이 SHA로 핀되어 있지 않아 quality-gate 파이프라인이 실패합니다.

리포지토리 정책상 워크플로우의 모든 액션은 SHA로 핀되어야 합니다(test_workflow_actions_are_pinned_by_sha 실패). 현재 actions/checkout@v4, docker/setup-buildx-action@v3, docker/login-action@v3, docker/build-push-action@v5, 그리고 하단의 actions/checkout@v4 모두 태그로만 참조되고 있어 이 중 첫 번째가 보고되어 있고 나머지도 순차적으로 실패할 것입니다. 각 액션을 공식 릴리스의 커밋 SHA로 교체하고, 가독성을 위해 태그를 주석으로 남겨 주세요.

🔒 예시 (버전 주석 유지)
- uses: actions/checkout@<full-sha>  # v4.x.y
- uses: docker/setup-buildx-action@<full-sha>  # v3.x.y
- uses: docker/login-action@<full-sha>  # v3.x.y
- uses: docker/build-push-action@<full-sha>  # v5.x.y

Also applies to: 23-23, 26-26, 37-37, 56-56

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/tests.yml at line 20, Replace every workflow action
reference that's using a tag (e.g., actions/checkout@v4,
docker/setup-buildx-action@v3, docker/login-action@v3,
docker/build-push-action@v5) with the corresponding official release commit SHA
and keep the tag as an inline comment for readability (for example replace
"actions/checkout@v4" with "actions/checkout@<full-sha>  # v4.x.y"); update all
occurrences noted (the first actions/checkout@v4 and the other instances at the
indicated lines) so every "uses:" entry is pinned by SHA rather than by tag.

Comment on lines +32 to +34
- name: Define image name and tag
id: image_name
run: echo "name=ghcr.io/${{ github.repository_owner,, }}/newsdom-api/test:${{ github.sha }}" | tee -a $GITHUB_OUTPUT

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🌐 Web query:

Does the GitHub Actions expression language support the ,,Bash-style lowercase parameter expansion (e.g.,${{ github.repository_owner,, }})?

💡 Result:

No. GitHub Actions expressions do not support Bash-style lowercase parameter expansion syntax like ${{ github.repository_owner,, }}. GitHub’s expression language supports a defined set of literals, operators, and built-in functions (documented in the Expressions reference). It does not document shell-style parameter expansion operators ,, as part of the expression syntax, and the docs describe expressions as evaluating context references plus supported functions/operators rather than performing Bash parameter expansions. If you need lowercase, do it in a runner step using the shell (for example ${VAR,,} in Bash) or use an action designed for string case conversion (e.g., a JS-based action calling toLowerCase), then pass the result via outputs/env.

Citations:


${{ github.repository_owner,, }}는 유효한 GitHub Actions 표현식이 아니므로 이미지 이름이 잘못 생성되어 푸시가 실패합니다.

,,는 Bash 파라미터 확장 문법이며, GitHub Actions 표현식 언어는 이를 지원하지 않습니다. github.repository_owner가 대문자를 포함하면 GHCR은 소문자 이름만 허용하므로 푸시가 실패합니다. Bash 스텝에서 소문자로 변환하고 결과를 출력으로 전달해야 합니다.

수정안
-      - name: Define image name and tag
-        id: image_name
-        run: echo "name=ghcr.io/${{ github.repository_owner,, }}/newsdom-api/test:${{ github.sha }}" | tee -a $GITHUB_OUTPUT
+      - name: Define image name and tag
+        id: image_name
+        env:
+          OWNER: ${{ github.repository_owner }}
+          SHA: ${{ github.sha }}
+        run: |
+          owner_lc="${OWNER,,}"
+          echo "name=ghcr.io/${owner_lc}/newsdom-api/test:${SHA}" | tee -a "$GITHUB_OUTPUT"
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
- name: Define image name and tag
id: image_name
run: echo "name=ghcr.io/${{ github.repository_owner,, }}/newsdom-api/test:${{ github.sha }}" | tee -a $GITHUB_OUTPUT
- name: Define image name and tag
id: image_name
env:
OWNER: ${{ github.repository_owner }}
SHA: ${{ github.sha }}
run: |
owner_lc="${OWNER,,}"
echo "name=ghcr.io/${owner_lc}/newsdom-api/test:${SHA}" | tee -a "$GITHUB_OUTPUT"
🧰 Tools
🪛 actionlint (1.7.12)

[error] 34-34: parser did not reach end of input after parsing the expression. 2 remaining token(s) in the input: ",", ","

(expression)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/tests.yml around lines 32 - 34, The image name step (id:
image_name) currently uses the Actions expression `${{ github.repository_owner,,
}}` which is invalid; change the run script to capture `${{
github.repository_owner }}` into a shell variable, lowercase it using Bash
parameter expansion (e.g., owner_low="${owner,,}"), then echo the image name
using that lowercased variable to $GITHUB_OUTPUT so GHCR receives a valid
lowercase owner; update the run body in the image_name step accordingly.

Comment on lines +46 to 59
pytest:
name: Run Pytest in Container
runs-on: ubuntu-latest
needs: build-test-image
container:
image: ${{ needs.build-test-image.outputs.image_name }}
env:
PYTHONWARNINGS: error
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Run tests with warnings as errors
env:
PYTHONWARNINGS: error
- name: Run tests
run: uv run pytest

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

astral-sh/setup-uv 핀 부재로 quality-gatetest_tests_workflow_pins_uv_version이 실패합니다.

이 PR은 uv 설치를 컨테이너 이미지 빌드 시점으로 옮겼기 때문에 워크플로우에서 astral-sh/setup-uv 스텝이 사라졌지만, 품질 게이트 테스트는 여전히 해당 액션의 SHA 핀 존재를 요구합니다. 둘 중 하나를 선택해야 합니다:

  • 품질 게이트 테스트를 컨테이너 내 uv 핀(예: Dockerfile.testpip install uv==<ver>)을 검증하도록 업데이트, 또는
  • pytest 잡에 다시 astral-sh/setup-uv@<sha>를 추가(컨테이너 내부 uv를 선호한다면 중복이 됩니다).

컨테이너 기반 전략을 유지하는 방향이라면 첫 번째 옵션이 자연스럽습니다.

품질 게이트 테스트가 어느 파일에 있는지 알려주시면 컨테이너 내부 uv 핀을 검사하도록 수정하는 패치를 제안해 드릴 수 있습니다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/tests.yml around lines 46 - 59, The tests workflow
(.github/workflows/tests.yml) removed the astral-sh/setup-uv step causing the
quality-gate test test_tests_workflow_pins_uv_version to fail because it expects
an action SHA pin; either restore the setup step with a pinned reference (add
astral-sh/setup-uv@<sha> back into the pytest job) or modify the quality-gate
check to validate the uv version is pinned inside the container build (e.g.,
check Dockerfile.test or the test image build step for a pip install uv==<ver>);
pick the container-first approach if you prefer, then update the quality-gate
test to look for a concrete uv version in Dockerfile.test (or wherever uv is
installed during build) instead of requiring astral-sh/setup-uv@<sha>.

Comment on lines +58 to 59
- name: Run tests
run: uv run pytest

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🌐 Web query:

How does uv runbehave whenUV_PROJECT_ENVIRONMENTis unset and the current working directory differs from the directory whereuv sync was originally run? Does it create a new .venv?

💡 Result:

When UV_PROJECT_ENVIRONMENT is unset, uv run discovers the project root by searching upwards from the current working directory (CWD) for the nearest ancestor directory containing a pyproject.toml file (with a [project] table). It then uses the .venv in that discovered project root directory. If the CWD is within the project directory where uv sync was run (i.e., the project root or a subdirectory), uv run will find the same project root and use the existing .venv there (creating it if missing). If the CWD differs from that directory—specifically, if the CWD is outside the project tree (no pyproject.toml found upwards from CWD)—then uv run will not discover any project. In that case, it falls back to non-project behavior: it does not use or create a project .venv, but may create/use other environments depending on context (e.g., no persistent .venv creation tied to a project). uv run does not create a new .venv in the CWD or elsewhere unless using flags like --isolated or specific script modes. It only manages the project .venv when a project is discovered.

Citations:


🏁 Script executed:

# Find and examine the Dockerfile.test to see the actual setup
find . -name "Dockerfile.test" -o -name "Dockerfile" -o -name "*.Dockerfile" | head -20

Repository: Seongho-Bae/newsdom-api

Length of output: 127


🏁 Script executed:

# Look at the complete GitHub Actions workflow configuration
cat -n .github/workflows/tests.yml | head -80

Repository: Seongho-Bae/newsdom-api

Length of output: 1932


🏁 Script executed:

# Check if pyproject.toml exists and where it's located
find . -name "pyproject.toml" -type f

Repository: Seongho-Bae/newsdom-api

Length of output: 83


🏁 Script executed:

cat -n ./Dockerfile.test

Repository: Seongho-Bae/newsdom-api

Length of output: 1382


🏁 Script executed:

cat -n ./pyproject.toml | head -30

Repository: Seongho-Bae/newsdom-api

Length of output: 959


GHA 컨테이너 작업 단계에서 Dockerfile에 포함된 .venv 재사용이 불가능합니다.

actions/checkout@v4는 컨테이너 내에서 저장소를 기본적으로 /github/workspace에 체크아웃합니다. uv run pytest/github/workspace에서 실행하면, uv는 현재 디렉터리에서 pyproject.toml을 찾아 프로젝트 루트를 /github/workspace로 해석합니다. Dockerfile에서 /app에 구축한 .venv는 이 프로젝트 트리 내에 존재하지 않으므로 발견되지 않습니다. 이미지 빌드 단계의 노력이 활용되지 않으며, 대신 새 가상 환경이 필요하게 됩니다.

다음 중 하나로 해결할 수 있습니다:

  • GHA 단계에서 working-directory: /app를 설정하여 올바른 프로젝트 루트에서 실행
  • run: /app/.venv/bin/pytest로 직접 호출
  • env: UV_PROJECT_ENVIRONMENT=/app/.venv를 설정하여 명시적으로 가상 환경 경로 지정
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/tests.yml around lines 58 - 59, The CI step running "uv
run pytest" is executed from /github/workspace so it cannot reuse the Docker
image's /app/.venv; update the workflow to run pytest from the correct project
root by either setting the job step's working-directory to /app, changing the
run command to call the venv directly with "run: /app/.venv/bin/pytest", or
setting the environment variable UV_PROJECT_ENVIRONMENT=/app/.venv so that "uv
run pytest" picks up the existing virtualenv; modify the step that currently
contains "run: uv run pytest" accordingly.

Comment thread Dockerfile.test
COPY pyproject.toml uv.lock ./

# Install uv via pip
RUN pip install uv==0.1.41

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

🧩 Analysis chain

🌐 Web query:

What is the current recommended stable version of the Python uv package manager in 2026, and are there any known issues with uv 0.1.41?

💡 Result:

The current recommended stable version of the Python uv package manager is 0.11.7, released on 2026-04-15. uv is described as stable and ready for production use. uv 0.1.41 was an early release from 2024-05-08 with a single documented bug fix: "Remove unconstrained version error from requirements (#3443)". No major or ongoing known issues are reported for it in recent sources; it appears to be a minor historical version superseded by many stable updates.

Citations:


uv==0.1.41 버전 핀이 매우 오래된 버전입니다

uv 0.1.41은 2024년 5월 릴리스된 버전으로, 현재 권장 안정 버전인 0.11.7(2026년 4월 릴리스)와 비교했을 때 상당한 기능 및 동작 차이가 있습니다. 특히 uv sync의 프로젝트 설치 동작이 변경되었을 가능성이 높습니다. CI 재현성을 유지하면서도 버그 픽스와 성능 개선을 반영할 수 있도록 최신 안정 버전(0.11.x)으로 핀을 업데이트하는 것을 권장합니다.

추가로, 공식 문서에서 권장하는 설치 방식은 독립 설치 스크립트 또는 GitHub Actions에서는 astral-sh/setup-uv를 사용하는 것입니다. 컨테이너 환경에서는 pip install uv==<version> 유지는 가능하지만, 버전을 최신 안정 버전으로 업그레이드하는 것이 좋습니다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Dockerfile.test` at line 22, The Dockerfile currently pins uv to an old
release via the RUN pip install uv==0.1.41 instruction; update the pin to the
current stable 0.11.x series (e.g., 0.11.7) to pick up bug fixes and behavioral
changes in uv sync, or switch to the recommended installer (use the independent
install script or GitHub Action like astral-sh/setup-uv) for container builds;
change the RUN pip install line (RUN pip install uv==0.1.41) to either install
uv==0.11.7 or invoke the recommended installer step so CI uses the modern stable
version.

Comment thread tools/benchmark_ocr.py
Comment on lines +80 to +108
try:
result = engine_runner(pdf_path)
duration = time.monotonic() - start_time
engine_results["success"] += 1
engine_results["results"][pdf_path.name] = {
"status": "success",
"duration": round(duration, 2),
**result,
}
print(f" [SUCCESS] {pdf_path.name} in {duration:.2f}s")

except (RuntimeError, subprocess.CalledProcessError) as e:
duration = time.monotonic() - start_time
engine_results["failed"] += 1
engine_results["results"][pdf_path.name] = {
"status": "failed",
"duration": round(duration, 2),
"error": str(e),
}
print(f" [FAILED] {pdf_path.name} in {duration:.2f}s")

except subprocess.TimeoutExpired:
duration = time.monotonic() - start_time
engine_results["timed_out"] += 1
engine_results["results"][pdf_path.name] = {
"status": "timed_out",
"duration": round(duration, 2),
}
print(f" [TIMED OUT] {pdf_path.name} after {duration:.2f}s")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# run_mineru 내부에서 어떤 예외 타입이 실제로 발생하는지 재확인
rg -nP --type=py -C2 'raise (HTTPException|FileNotFoundError|RuntimeError|subprocess\.)' src/newsdom_api/mineru_runner.py

Repository: Seongho-Bae/newsdom-api

Length of output: 1457


🏁 Script executed:

cat -n tools/benchmark_ocr.py | head -150

Repository: Seongho-Bae/newsdom-api

Length of output: 5983


mineru 엔진의 실제 예외가 여기서 포착되지 않아 벤치마크가 중단될 수 있습니다.

parse_pdf_bytesrun_mineru의 구현(src/newsdom_api/mineru_runner.py)은 subprocess.TimeoutExpiredsubprocess.CalledProcessErrorfastapi.HTTPException으로 래핑해서 다시 던집니다(상태 코드 504, 500). 또한 mineru 실행 파일을 찾지 못하거나 필수 JSON 파일이 없으면 FileNotFoundError를 발생시킵니다.

현재 핸들러 (RuntimeError, subprocess.CalledProcessError)(91행)와 subprocess.TimeoutExpired(101행)는 이들 예외 중 어느 것도 매칭되지 않습니다. 따라서 mineru이 타임아웃하거나 실패할 때마다 예외가 루프 밖으로 전파되어 벤치마크 실행 자체가 중단되며, "failed"/"timed_out" 카운터가 증가하지 않고 부분 결과도 JSON으로 남지 않습니다.

가장 견고한 해법은 run_mineru_engine이 내부에서 HTTPException/FileNotFoundError를 의미 있는 예외로 변환하거나, 또는 run_benchmarkexcept 절에 포괄적인 Exception 폴백을 추가해 예기치 않은 예외도 "failed"로 기록하는 것입니다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tools/benchmark_ocr.py` around lines 80 - 108, The benchmark loop currently
only catches RuntimeError, subprocess.CalledProcessError and
subprocess.TimeoutExpired but mineru_runner wraps errors in
fastapi.HTTPException (504 for timeout, 500 for failures) and can raise
FileNotFoundError, so update the except handling in the try block that calls
engine_runner(pdf_path) (the run_benchmark / loop that contains engine_runner)
to also catch fastapi.HTTPException and FileNotFoundError (and optionally a
final broad Exception fallback); when catching fastapi.HTTPException inspect
e.status_code and treat 504 as "timed_out" and other codes as "failed"
(increment engine_results["timed_out"] or ["failed"], set
engine_results["results"][pdf_path.name] with status, duration and error), treat
FileNotFoundError as "failed" with the error message, and ensure a final except
Exception records unexpected errors as "failed" so the benchmark continues and
partial results are saved.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

CI: pytest consistently fails due to mineru dependency issues

1 participant