-
Notifications
You must be signed in to change notification settings - Fork 1
272 lines (261 loc) · 10.8 KB
/
ci.yml
File metadata and controls
272 lines (261 loc) · 10.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
name: CI
# 트리거: PR / main 푸시 — test 계열만 (wheel 빌드 + 배포는 publish.yml)
# 문서·라이선스·gitignore 전용 변경은 changes job + job-level if 패턴으로 if-skip
# (status: success). paths-ignore 안 씀 — required check "All tests passed" 가
# expected 로 stuck 되는 GitHub 함정 회피.
on:
push:
branches: [main]
pull_request:
branches: [main]
workflow_dispatch: {}
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
jobs:
# * 코드 변경 감지 — docs / LICENSE / .gitignore 전용 변경이면 test 잡 if-skip
# (status: success). 모든 변경 파일이 exclude 패턴에 매치돼야 code=false.
# base SHA 부재 / zero SHA 면 안전 측면으로 code=true (= 전체 실행).
changes:
name: Detect code changes
runs-on: ubuntu-latest
outputs:
code: ${{ steps.filter.outputs.code }}
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 0
- id: filter
env:
BASE_SHA: ${{ github.event.pull_request.base.sha || github.event.before }}
run: |
if [ -z "$BASE_SHA" ] || [ "$BASE_SHA" = "0000000000000000000000000000000000000000" ]; then
echo "code=true" >> "$GITHUB_OUTPUT"
exit 0
fi
# ^ three-dot diff = base..HEAD 공통 조상부터의 변경만 (PR diff 와 동일)
if git diff --name-only "$BASE_SHA"...HEAD \
| grep -qvE '(^|/)([^/]+\.md|LICENSE[^/]*|\.gitignore)$|^docs/'; then
echo "code=true" >> "$GITHUB_OUTPUT"
else
echo "code=false" >> "$GITHUB_OUTPUT"
fi
# * Linux abi3 wheel 1회 빌드 → 모든 Linux 잡(test×4 / slow / core-only)이 공유
# abi3-py310 이라 py3.10/3.11/3.12/3.13 가 동일 wheel 재사용 가능.
# macOS/Windows 는 단일 잡이라 빌드/테스트 분리 이득이 없어 그대로 매번 빌드.
build-linux-wheel:
name: Build Linux abi3 wheel
needs: changes
if: needs.changes.outputs.code == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
with:
submodules: recursive
# ^ skia-safe 가 Linux 에서 freetype / fontconfig 를 동적 링크 — ubuntu runner 에 미내장.
# macOS / Windows 는 frameworks / 시스템 라이브러리로 자체 해결되어 별도 단계 불필요.
- name: Install skia-safe system dependencies (Linux)
run: |
sudo apt-get update -qq
sudo apt-get install -y --no-install-recommends libfreetype-dev libfontconfig1-dev
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
with:
save-if: ${{ github.ref == 'refs/heads/main' }}
- uses: astral-sh/setup-uv@v8.1.0
with:
python-version: "3.12"
- run: uv sync --no-install-project --group all
- run: uv run maturin build --release --out dist
- uses: actions/upload-artifact@v7
with:
name: rhwp-python-linux-wheel
path: dist/*.whl
retention-days: 1
# * 메인 테스트 + 린트 + 타입체크 (Linux × 전 Python 버전 — wheel 공유)
test:
name: Test (Linux / py${{ matrix.python }})
needs: [build-linux-wheel, changes]
if: needs.changes.outputs.code == 'true'
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
include:
- { python: "3.10", lint: true }
- { python: "3.11" }
- { python: "3.12" }
- { python: "3.13" }
defaults:
run:
shell: bash
steps:
- uses: actions/checkout@v6
with:
submodules: recursive
- uses: astral-sh/setup-uv@v8.1.0
with:
python-version: ${{ matrix.python }}
- run: uv sync --no-install-project --group all
- uses: actions/download-artifact@v8
with:
name: rhwp-python-linux-wheel
path: dist/
- run: uv pip install --reinstall dist/*.whl
# ^ --no-sync: build-linux-wheel 가 만든 wheel 을 그대로 사용. uv 가 lock 기준으로 프로젝트
# 를 editable 빌드하려 하면 native-skia 시스템 의존성 (freetype / fontconfig dev) 필요해서 fail.
- name: Run pytest (not slow) with coverage
run: uv run --no-sync pytest tests/ -m "not slow" --cov=rhwp --cov-report=term-missing -v
- name: Run pyright (normal)
if: matrix.lint
run: |
uv run --no-sync pyright python/ \
tests/test_smoke.py tests/test_parse.py tests/test_text_extraction.py \
tests/test_errors.py tests/test_svg_rendering.py tests/test_pdf_rendering.py \
tests/test_langchain_loader.py tests/test_langchain_loader_ir.py \
tests/test_ir_schema.py tests/test_ir_roundtrip.py tests/test_ir_tables.py \
tests/test_ir_iter_blocks.py tests/test_ir_schema_export.py \
tests/test_ir_picture.py tests/test_ir_furniture.py \
tests/test_ir_formula.py tests/test_ir_footnote.py \
tests/test_ir_list.py tests/test_ir_caption.py \
tests/test_ir_toc.py tests/test_ir_field.py \
tests/test_ir_marker_char_offset.py \
tests/test_view_markdown.py tests/test_view_html.py \
tests/test_view_baseline.py \
tests/test_cli.py \
tests/test_mcp_server.py \
tests/conftest.py tests/type_check_samples.py
- name: Run pyright (intentional errors — expect 4)
if: matrix.lint
run: |
set +e
uv run --no-sync pyright --outputjson tests/type_check_errors.py > pyright-errors.json
count=$(uv run --no-sync python -c "import json; print(json.load(open('pyright-errors.json'))['summary']['errorCount'])")
echo "intentional error count: $count"
if [ "$count" != "4" ]; then
echo "::error::expected 4 intentional errors, got $count"
exit 1
fi
# * macOS / Windows 스모크 — 단일 잡이라 wheel 분리 이득 없음 → 직접 maturin develop
test-other-os:
name: Test (${{ matrix.os }} / py3.12)
needs: changes
if: needs.changes.outputs.code == 'true'
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [macos-latest, windows-latest]
defaults:
run:
shell: bash
steps:
- uses: actions/checkout@v6
with:
submodules: recursive
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
with:
save-if: ${{ github.ref == 'refs/heads/main' }}
- uses: astral-sh/setup-uv@v8.1.0
with:
python-version: "3.12"
- run: uv sync --no-install-project --group all
- run: uv run maturin develop --release
- run: uv run pytest tests/ -m "not slow" -v
# * PDF 렌더링 — 느려서 별도 잡, Linux wheel 재사용
test-slow:
name: Test slow (Linux / py3.12 — PDF)
needs: [build-linux-wheel, changes]
if: needs.changes.outputs.code == 'true'
runs-on: ubuntu-latest
defaults:
run:
shell: bash
steps:
- uses: actions/checkout@v6
with:
submodules: recursive
- uses: astral-sh/setup-uv@v8.1.0
with:
python-version: "3.12"
- run: uv sync --no-install-project --group testing
- uses: actions/download-artifact@v8
with:
name: rhwp-python-linux-wheel
path: dist/
- run: uv pip install --reinstall dist/*.whl
- run: uv run --no-sync pytest tests/ -m slow -v
# * Rust unit tests — src/ir.rs 의 #[cfg(test)] 모듈 실행
# Cargo.toml 의 default features 에서 extension-module 이 빠져 있어 libpython 링크 시도 안 함.
# utf16_to_cp / simple_eq_text_alt / field_type_to_str / caption_direction_to_str /
# assert_position_invariant (#[should_panic]) 검증 — Python 측에서 우회 불가능한 Rust 도메인 로직.
cargo-test:
name: Cargo test (Rust unit tests)
needs: changes
if: needs.changes.outputs.code == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
with:
submodules: recursive
# ^ skia-safe 가 Linux 에서 freetype / fontconfig 를 동적 링크 — build-linux-wheel 와 동일.
- name: Install skia-safe system dependencies (Linux)
run: |
sudo apt-get update -qq
sudo apt-get install -y --no-install-recommends libfreetype-dev libfontconfig1-dev
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
with:
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: Run cargo test (default features — extension-module disabled)
run: cargo test --lib
# * extras 미설치 시 langchain 테스트가 importorskip 로 auto-skip 되는지 검증
test-core-only:
name: Test without extras (importorskip auto-skip)
needs: [build-linux-wheel, changes]
if: needs.changes.outputs.code == 'true'
runs-on: ubuntu-latest
defaults:
run:
shell: bash
steps:
- uses: actions/checkout@v6
with:
submodules: recursive
- uses: astral-sh/setup-uv@v8.1.0
with:
python-version: "3.12"
- name: Install pytest only (no langchain extras — intentional)
run: |
uv venv
uv pip install pytest
- uses: actions/download-artifact@v8
with:
name: rhwp-python-linux-wheel
path: dist/
- run: uv pip install dist/*.whl
- name: Run pytest — extras-gated tests must auto-skip via importorskip
# ^ 파일-레벨 importorskip 은 해당 파일 전체를 skip 1개로 카운트.
# v0.6.0 기준 gated 파일: test_langchain_loader.py + test_langchain_loader_ir.py
# (langchain-core), test_ir_schema_export.py (jsonschema), test_cli.py (typer),
# test_mcp_server.py (fastmcp), test_render_png.py (Pillow) → 총 6 파일.
# test_async.py 는 v0.3.0 부터 stdlib 만 사용 (aiofiles 의존성 제거).
run: |
uv run --no-sync pytest tests/ -m "not slow" -v | tee pytest-output.txt
if ! grep -qE '(^|[^0-9])6 skipped([^0-9]|$)' pytest-output.txt; then
echo "::error::expected 6 extras-gated files to auto-skip via importorskip (langchain×2, jsonschema, typer, fastmcp, Pillow)"
exit 1
fi
all-tests-passed:
name: All tests passed
if: always()
runs-on: ubuntu-latest
needs: [changes, build-linux-wheel, test, test-other-os, test-slow, test-core-only, cargo-test]
steps:
- uses: re-actors/alls-green@release/v1
with:
jobs: ${{ toJSON(needs) }}
allowed-skips: build-linux-wheel, test, test-other-os, test-slow, test-core-only, cargo-test