Skip to content

Commit 2b41bd3

Browse files
committed
Name benchmark runs with the latest PyMuPDF release and branch
1 parent 8c63e84 commit 2b41bd3

10 files changed

Lines changed: 280 additions & 43 deletions

File tree

‎.github/scripts/pymupdf_source_stack/README.md‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,14 @@ the adapters in this directory translate GitHub environment variables and
1010
output files to that shared core. GitHub-only publishing and failure-summary
1111
behavior remains here.
1212

13+
`launch_benchmark.py` reads the latest published PyMuPDF release tag and sends
14+
a `parsebench-version-lab` repository dispatch. This supplies the version
15+
before GitHub evaluates the benchmark's `run-name`, without adding a version
16+
input to the manual form. The launcher uses its built-in token with
17+
`contents: write` for dispatch; no extra secret is needed. The benchmark retains
18+
its workflow identity for indexing, checks out the launcher's exact repository
19+
commit, and records the launch run ID and release label alongside source provenance.
20+
1321
MuPDF follows the version declared in the selected PyMuPDF commit's `setup.py`;
1422
developers do not select it separately. The workflow resolves that native
1523
release tag to a full SHA before checkout, then gives PyMuPDF's build system a reproducible
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
#!/usr/bin/env python3
2+
"""Resolve the release label before GitHub creates the benchmark run title."""
3+
4+
from __future__ import annotations
5+
6+
import json
7+
import re
8+
import subprocess
9+
10+
from common import append_summary, env, markdown_cell
11+
from configure import GROUPS, RUN_SCOPES, selected
12+
13+
14+
def main() -> int:
15+
branch = env("PYMUPDF_BRANCH").strip()
16+
if not branch:
17+
raise SystemExit("A PyMuPDF branch name is required")
18+
scope = env("RUN_SCOPE_SELECTION")
19+
group = env("GROUP_SELECTION")
20+
selected(RUN_SCOPES, scope, "test size")
21+
selected(GROUPS, group, "document category")
22+
release_result = subprocess.run(
23+
["gh", "api", "repos/pymupdf/PyMuPDF/releases/latest"],
24+
check=True,
25+
capture_output=True,
26+
text=True,
27+
)
28+
release = json.loads(release_result.stdout)
29+
tag = release.get("tag_name")
30+
if not isinstance(tag, str) or not re.fullmatch(r"v?\d+(?:\.\d+)+", tag):
31+
raise SystemExit(f"Latest PyMuPDF release has an unsupported tag: {tag!r}")
32+
repository = env("GITHUB_REPOSITORY")
33+
payload = {
34+
"event_type": "parsebench-version-lab",
35+
"client_payload": {
36+
"pymupdf_version": tag.removeprefix("v"),
37+
"pymupdf_tag": tag,
38+
"pymupdf_branch": branch,
39+
"run_scope": scope,
40+
"group": group,
41+
"benchmark_ref": env("GITHUB_REF_NAME"),
42+
"benchmark_sha": env("GITHUB_SHA"),
43+
"launcher_run_id": env("GITHUB_RUN_ID"),
44+
},
45+
}
46+
subprocess.run(
47+
["gh", "api", "--method", "POST", f"repos/{repository}/dispatches", "--input", "-"],
48+
input=json.dumps(payload),
49+
text=True,
50+
check=True,
51+
)
52+
benchmark_url = (
53+
f"{env('GITHUB_SERVER_URL')}/{repository}/actions/workflows/pymupdf-source-stack-parsebench.yml"
54+
)
55+
append_summary(
56+
[
57+
"## Benchmark launched",
58+
"",
59+
f"[Open benchmark runs]({benchmark_url})",
60+
"",
61+
f"- PyMuPDF release label: **{markdown_cell(tag)}**",
62+
f"- Branch to build: **{markdown_cell(branch)}**",
63+
f"- Test size: {markdown_cell(scope)}",
64+
f"- Document category: {markdown_cell(group)}",
65+
"",
66+
"The benchmark runs separately. Its conclusion and results appear in the linked workflow.",
67+
]
68+
)
69+
return 0
70+
71+
72+
if __name__ == "__main__":
73+
raise SystemExit(main())

‎.github/scripts/pymupdf_source_stack/write_run_metadata.py‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ def main() -> int:
2020
"pipeline": env("PIPELINE"),
2121
"benchmark_ref_input": env("BENCHMARK_REF"),
2222
"checked_out_sha": git_sha(),
23+
"pymupdf_release_tag": os.environ.get("PYMUPDF_RELEASE_TAG"),
24+
"launcher_run_id": os.environ.get("LAUNCHER_RUN_ID"),
2325
"github_ref": env("GITHUB_REF"),
2426
"github_sha": env("GITHUB_SHA"),
2527
"github_repository": env("GITHUB_REPOSITORY"),
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
name: Run ParseBench Version Lab
2+
3+
run-name: >-
4+
${{ format('Launch PyMuPDF · {0} · {1} · {2}', inputs.pymupdf_branch,
5+
inputs.run_scope == 'Quick test (15 cases)' && 'Quick 15' || 'Full ~2k',
6+
inputs.group == 'All categories' && 'All' || inputs.group) }}
7+
8+
on:
9+
workflow_dispatch:
10+
inputs:
11+
pymupdf_branch:
12+
description: "PyMuPDF branch to benchmark (for example main or your feature branch)."
13+
required: true
14+
default: "main"
15+
type: string
16+
run_scope:
17+
description: "How much data should be tested?"
18+
required: true
19+
default: "Quick test (15 cases)"
20+
type: choice
21+
options:
22+
- "Quick test (15 cases)"
23+
- "Full benchmark (~2,000 pages)"
24+
group:
25+
description: "Which document category should be tested?"
26+
required: true
27+
default: "All categories"
28+
type: choice
29+
options:
30+
- "All categories"
31+
- Charts
32+
- Tables
33+
- "Page layout"
34+
- "Text content"
35+
- "Text formatting"
36+
37+
permissions:
38+
contents: write # Required to send the repository_dispatch event.
39+
40+
jobs:
41+
launch:
42+
name: Resolve latest PyMuPDF release and start benchmark
43+
runs-on: ubuntu-24.04
44+
timeout-minutes: 5
45+
steps:
46+
- name: Checkout launcher
47+
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
48+
with:
49+
ref: ${{ github.sha }}
50+
persist-credentials: false
51+
sparse-checkout: |
52+
.github/scripts/pymupdf_source_stack
53+
tools/version_lab/src
54+
55+
- name: Launch named benchmark
56+
env:
57+
GH_TOKEN: ${{ github.token }}
58+
PYMUPDF_BRANCH: ${{ inputs.pymupdf_branch }}
59+
RUN_SCOPE_SELECTION: ${{ inputs.run_scope }}
60+
GROUP_SELECTION: ${{ inputs.group }}
61+
run: python3 .github/scripts/pymupdf_source_stack/launch_benchmark.py

‎.github/workflows/pymupdf-source-stack-parsebench.yml‎

Lines changed: 23 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -3,50 +3,28 @@ name: ParseBench Version Lab
33
run-name: >-
44
${{
55
format(
6-
'pymupdf4llm_html_tables_rapidocr_v3 · {0} · {1} · {2}',
7-
inputs.pymupdf_branch,
8-
inputs.run_scope == 'Quick test (15 cases)' && 'Quick 15' || 'Full ~2k',
9-
inputs.group == 'All categories' && 'All' ||
10-
inputs.group == 'Page layout' && 'Layout' ||
11-
inputs.group
6+
'PyMuPDF {0} · {1} · {2} · {3}',
7+
github.event.client_payload.pymupdf_version,
8+
github.event.client_payload.pymupdf_branch,
9+
github.event.client_payload.run_scope == 'Quick test (15 cases)' && 'Quick 15' || 'Full ~2k',
10+
github.event.client_payload.group == 'All categories' && 'All' ||
11+
github.event.client_payload.group == 'Page layout' && 'Layout' ||
12+
github.event.client_payload.group
1213
)
1314
}}
1415
1516
on:
16-
workflow_dispatch:
17-
inputs:
18-
pymupdf_branch:
19-
description: "PyMuPDF branch to benchmark (for example main or your feature branch)."
20-
required: true
21-
default: "main"
22-
type: string
23-
run_scope:
24-
description: "How much data should be tested?"
25-
required: true
26-
default: "Quick test (15 cases)"
27-
type: choice
28-
options:
29-
- "Quick test (15 cases)"
30-
- "Full benchmark (~2,000 pages)"
31-
group:
32-
description: "Which document category should be tested?"
33-
required: true
34-
default: "All categories"
35-
type: choice
36-
options:
37-
- "All categories"
38-
- Charts
39-
- Tables
40-
- "Page layout"
41-
- "Text content"
42-
- "Text formatting"
17+
repository_dispatch:
18+
types: [parsebench-version-lab]
4319

4420
permissions:
4521
contents: read
4622
packages: read
4723

4824
env:
4925
PIPELINE: pymupdf4llm_html_tables_rapidocr_v3
26+
PYMUPDF_RELEASE_TAG: ${{ github.event.client_payload.pymupdf_tag }}
27+
LAUNCHER_RUN_ID: ${{ github.event.client_payload.launcher_run_id }}
5028
GCP_PROJECT_ID: pypi-download-analysis-457820
5129
GCS_PREFIX: parsebench/pymupdf_source_stack
5230
UV_NO_SYNC: "1"
@@ -72,14 +50,17 @@ jobs:
7250
steps:
7351
- name: Checkout ParseBench
7452
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
53+
with:
54+
ref: ${{ github.event.client_payload.benchmark_sha }}
55+
persist-credentials: false
7556

7657
- name: Initialize run configuration
7758
id: config
7859
env:
79-
BENCHMARK_REF: ${{ github.ref_name }}
80-
GROUP_SELECTION: ${{ inputs.group }}
81-
PYMUPDF_BRANCH: ${{ inputs.pymupdf_branch }}
82-
RUN_SCOPE_SELECTION: ${{ inputs.run_scope }}
60+
BENCHMARK_REF: ${{ github.event.client_payload.benchmark_ref }}
61+
GROUP_SELECTION: ${{ github.event.client_payload.group }}
62+
PYMUPDF_BRANCH: ${{ github.event.client_payload.pymupdf_branch }}
63+
RUN_SCOPE_SELECTION: ${{ github.event.client_payload.run_scope }}
8364
run: python3 "$WORKFLOW_SCRIPTS/configure.py"
8465

8566
- name: Resolve PyMuPDF branch and bundled sources
@@ -177,13 +158,13 @@ jobs:
177158

178159
- name: Add selected configuration to workflow summary
179160
env:
180-
BENCHMARK_REF: ${{ github.ref_name }}
161+
BENCHMARK_REF: ${{ github.event.client_payload.benchmark_ref }}
181162
DATASET_REPOSITORY: ${{ steps.dataset.outputs.repository }}
182163
DATASET_REQUESTED_REF: ${{ steps.dataset.outputs.requested_ref }}
183164
DATASET_SHA: ${{ steps.dataset.outputs.sha }}
184-
GROUP_SELECTION: ${{ inputs.group }}
165+
GROUP_SELECTION: ${{ github.event.client_payload.group }}
185166
OUTPUT_DIR: ${{ steps.config.outputs.output_dir }}
186-
RUN_SCOPE_SELECTION: ${{ inputs.run_scope }}
167+
RUN_SCOPE_SELECTION: ${{ github.event.client_payload.run_scope }}
187168
run: python3 "$WORKFLOW_SCRIPTS/write_run_summary.py"
188169

189170
- name: Install ParseBench dependencies without selected PyMuPDF packages
@@ -313,6 +294,7 @@ jobs:
313294
- name: Checkout publishing helper
314295
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
315296
with:
297+
ref: ${{ github.event.client_payload.benchmark_sha }}
316298
persist-credentials: false
317299
sparse-checkout: |
318300
.github/scripts/pymupdf_source_stack
@@ -357,6 +339,7 @@ jobs:
357339
- name: Checkout summary helpers
358340
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
359341
with:
342+
ref: ${{ github.event.client_payload.benchmark_sha }}
360343
persist-credentials: false
361344
sparse-checkout: |
362345
.github/scripts/pymupdf_source_stack

‎tests/test_pymupdf_source_stack_workflow.py‎

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ def _load_module(name: str):
2727

2828

2929
configure = _load_module("configure")
30+
launcher = _load_module("launch_benchmark")
3031
benchmark = importlib.import_module("parsebench_version_lab.runtime_benchmark")
3132
provenance = importlib.import_module("parsebench_version_lab.provenance")
3233
resolve_dataset = _load_module("resolve_dataset")
@@ -48,6 +49,57 @@ def test_safe_ref_produces_bounded_path_component(value: str, expected: str) ->
4849
assert configure.safe_ref(value) == expected
4950

5051

52+
@pytest.mark.parametrize("tag", ["2.0", "v2.1", None])
53+
def test_launcher_resolves_release_before_dispatch_and_preserves_selected_branch(
54+
tag: str | None, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
55+
) -> None:
56+
for name, value in {
57+
"PYMUPDF_BRANCH": "feature/ocr",
58+
"RUN_SCOPE_SELECTION": "Full benchmark (~2,000 pages)",
59+
"GROUP_SELECTION": "All categories",
60+
"GITHUB_REPOSITORY": "owner/repo",
61+
"GITHUB_REF_NAME": "feature/workflow",
62+
"GITHUB_SHA": "a" * 40,
63+
"GITHUB_RUN_ID": "123",
64+
"GITHUB_SERVER_URL": "https://github.com",
65+
"GITHUB_STEP_SUMMARY": str(tmp_path / "summary"),
66+
}.items():
67+
monkeypatch.setenv(name, value)
68+
69+
calls = []
70+
71+
def run(command, **kwargs):
72+
calls.append(command)
73+
if len(calls) == 1:
74+
assert command == ["gh", "api", "repos/pymupdf/PyMuPDF/releases/latest"]
75+
return subprocess.CompletedProcess(command, 0, stdout=json.dumps({"tag_name": tag}))
76+
assert command == ["gh", "api", "--method", "POST", "repos/owner/repo/dispatches", "--input", "-"]
77+
assert json.loads(kwargs["input"]) == {
78+
"event_type": "parsebench-version-lab",
79+
"client_payload": {
80+
"pymupdf_version": tag.removeprefix("v"),
81+
"pymupdf_tag": tag,
82+
"pymupdf_branch": "feature/ocr",
83+
"run_scope": "Full benchmark (~2,000 pages)",
84+
"group": "All categories",
85+
"benchmark_ref": "feature/workflow",
86+
"benchmark_sha": "a" * 40,
87+
"launcher_run_id": "123",
88+
},
89+
}
90+
return subprocess.CompletedProcess(command, 0)
91+
92+
monkeypatch.setattr(launcher.subprocess, "run", run)
93+
if tag is None:
94+
with pytest.raises(SystemExit, match="unsupported tag"):
95+
launcher.main()
96+
assert len(calls) == 1
97+
else:
98+
assert launcher.main() == 0
99+
assert len(calls) == 2
100+
assert "pymupdf-source-stack-parsebench.yml" in (tmp_path / "summary").read_text()
101+
102+
51103
def test_configure_maps_friendly_inputs_and_records_request(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
52104
monkeypatch.setenv("GITHUB_STEP_SUMMARY", str(tmp_path / "summary"))
53105
github_output = tmp_path / "github-output"

‎tools/version_lab/README.md‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,18 @@ Hugging Face commit for the entire run.
123123

124124
## Run
125125

126+
In GitHub Actions, select **Run ParseBench Version Lab** and click **Run workflow**.
127+
The launcher keeps the three inputs: PyMuPDF branch, test size, and category.
128+
It reads PyMuPDF's latest published GitHub release tag, then starts a separate
129+
**ParseBench Version Lab** benchmark named, for example,
130+
`PyMuPDF 2.0 · main · Full ~2k · All`. The launcher summary links to benchmark runs.
131+
The version in the title is the latest release label; the selected branch is
132+
still built and its exact installed version and commit are recorded in the results.
133+
The benchmark workflow definition runs from the default branch; its code and
134+
helpers are checked out at the immutable repository commit chosen in the launcher.
135+
Only the benchmark run is ingested into Supabase. The launcher finishing means
136+
the benchmark was requested; check the benchmark run for its final outcome.
137+
126138
Run all commands from the repository root.
127139

128140
Check the required tools:

‎tools/version_lab/src/parsebench_version_lab/benchmark_index.py‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
from typing import Any, Protocol
1818

1919
from .coverage import execution_coverage
20+
from .model import DEFAULT_PIPELINE
2021
from .results import finite_number, headline_metric, report_dimension
2122

2223
KNOWN_DIMENSIONS = ("chart", "table", "layout", "text_content", "text_formatting")
@@ -395,6 +396,8 @@ def _pipeline_from_run_name(run: Mapping[str, Any]) -> str | None:
395396
if not isinstance(title, str):
396397
return None
397398
first = title.split(" · ", 1)[0].strip()
399+
if first == "PyMuPDF" or first.startswith("PyMuPDF "):
400+
return DEFAULT_PIPELINE
398401
return first if first.startswith("pymupdf") else None
399402

400403

@@ -622,8 +625,8 @@ def index_run(
622625
"dataset_version_id": dataset_id,
623626
"gcs_bucket": gcs_bucket,
624627
"gcs_prefix": gcs_prefix,
625-
"head_branch": github_run.get("head_branch"),
626-
"head_sha": github_run.get("head_sha"),
628+
"head_branch": run_metadata.get("benchmark_ref_input") or github_run.get("head_branch"),
629+
"head_sha": run_metadata.get("checked_out_sha") or github_run.get("head_sha"),
627630
"source_created_at": github_run.get("created_at"),
628631
"source_updated_at": github_run.get("updated_at"),
629632
"started_at": github_run.get("run_started_at"),

0 commit comments

Comments
 (0)