Repository navigation
fix(cli): exit with code 1 when a document fails to convert - #4888
Conversation
|
✅ DCO Check Passed Thanks @kevin9327, all your commits are properly signed off. 🎉 |
Merge Protections🟢 Merge protection satisfied — ready to merge. Show 1 satisfied protection🟢 Enforce conventional commitMake sure that we follow https://www.conventionalcommits.org/en/v1.0.0/
|
AI triage (advisory)Checked commit The PR fixes CI safety: no concern found. A maintainer must still approve CI. 1 related pull request(s) in the same code
The |
There was a problem hiding this comment.
AI first review (advisory): no blocker found
This review covers the full diff at 440dd49126. It read the code and did not run it.
The PR makes export_documents return its pre-existing failure_count and raises typer.Exit(1) in both convert and convert-remote when that count is non-zero. The logic is correct: --abort-on-error raises before the return, so the paths don't interact. The tests exercise both commands with a real failed-document scenario. No blockers found; verify that PARTIAL_SUCCESS and SKIPPED statuses intentionally cause exit 1, since doc_failed = conv_res.status != ConversionStatus.SUCCESS (line 584) counts those too — but this is pre-existing behavior that the PR merely exposes.
A maintainer makes the merge decision. This review never approves the pull request.
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
dolfim-ibm
left a comment
There was a problem hiding this comment.
Thanks for picking this up, a non-zero exit on failure is the right direction. One blocker on how partial success is treated, plus a proposal to make the failure threshold configurable.
Blocker: PARTIAL_SUCCESS must not fail the run by default
doc_failed = conv_res.status != ConversionStatus.SUCCESS (main.py:583) counts PARTIAL_SUCCESS (and SKIPPED) as a failure, so with this PR a 300-page PDF where one page times out (--document-timeout) or one page hits a model error now makes docling convert exit 1. Today those runs exit 0, so this turns a lot of currently-green batch jobs and CI pipelines red.
It is also inconsistent with the converter: --abort-on-error passes raises_on_error, and DocumentConverter.convert_all deliberately does not raise on partial success (document_converter.py:773):
if raises_on_error and conv_res.status not in {
ConversionStatus.SUCCESS,
ConversionStatus.PARTIAL_SUCCESS,
}:So the strictest CLI mode (--abort-on-error) treats partial success as OK, while the default mode would exit 1 on it.
Note that the CLI also does not export PARTIAL_SUCCESS documents today. That part is pre-existing (shared with main, unchanged since the CLI was added), but it matters here: exiting 0 on partial success only makes sense if the partial document is actually written. Please fix both together:
- export
PARTIAL_SUCCESSdocuments likeSUCCESSones, and log a warning with theconv_res.errorsdetails (the same[Failure Detail]lines used for failures); - do not count them as failures by default.
FAILURE keeps exiting 1, as this PR does. The existing "empty markdown export" check (from #3429) should stay a failure.
Proposal: --fail-on to choose how strict the exit code is
Different users want different thresholds (a RAG ingestion job is fine with a missing page, a compliance archive is not), so make it an option instead of hardcoding it. Concretely:
class FailOn(str, Enum):
"""Which conversion outcome makes the CLI exit with a non-zero code."""
NEVER = "never" # always exit 0 once the run completes (old behaviour)
FAILURE = "failure" # exit 1 if any document has status FAILURE (default)
PARTIAL = "partial" # also exit 1 if any document is PARTIAL_SUCCESSfail_on: Annotated[
FailOn,
typer.Option(
"--fail-on",
help=(
"Which outcome makes docling exit with code 1: 'failure' (default) "
"if any document fails, 'partial' also if any document is only "
"partially converted, 'never' to always exit 0."
),
),
] = FailOn.FAILURE,- Add the same option to
convert-remote(defineFailOnonce indocling/cli/main.py, next toChunkerType, and import it inremote.py). --abort-on-errorstays independent: it controls when processing stops;--fail-oncontrols the exit code at the end.--abort-on-error --fail-on neverstill raises on the first failure, as today.SKIPPEDdocuments should count as neither failure nor partial.
To keep the decision in one place, have export_documents return the counts as a small structured value instead of a bare int (the same split docs/examples/batch_convert.py already uses with its success, partial_success, failure tuple):
@dataclass(frozen=True)
class ExportSummary:
success: int
partial_success: int
failure: int
def should_fail(self, fail_on: FailOn) -> bool:
if fail_on == FailOn.NEVER:
return False
if fail_on == FailOn.PARTIAL:
return self.failure > 0 or self.partial_success > 0
return self.failure > 0and in both commands:
if summary.should_fail(fail_on):
raise typer.Exit(1)Docs and tests
-
Add an "Exit codes" section to the
converthelp (mirroring the one inconvert-remote) and extend theconvert-remoteone to mention--fail-on. Also updatedocs/reference/cli.mdand the usage skill indocling/.agents/skills/docling/, since scripts will start depending on this. -
Tests worth having, on top of the two already added:
- a
PARTIAL_SUCCESSresult exits 0 by default and its output file is written; - the same result exits 1 with
--fail-on partial; - a
FAILUREresult exits 0 with--fail-on never.
The fake service client in
tests/test_cli_remote.pymakes the partial cases easy to set up without a real model run. - a
convert and convert-remote logged "failed to convert" for each failed document but still exited 0, so scripts and CI could not tell a failed run from a successful one. export_documents now returns the number of failed documents and both commands exit 1 when it is non-zero. Fixes docling-project#4887 Signed-off-by: kevin9327 <5299031+kevin9327@users.noreply.github.com>
… code PARTIAL_SUCCESS documents are now exported like SUCCESS ones, with a warning and the same [Failure Detail] lines, and no longer count as failures. SKIPPED documents count as neither. export_documents returns an ExportSummary, and convert and convert-remote exit 1 only when summary.should_fail(--fail-on) is true: 'failure' (default) on any failed document, 'partial' also on partial conversions, 'never' to always exit 0. --abort-on-error is unchanged. Document the exit codes in the convert and convert-remote help, the CLI reference and the usage skill. Signed-off-by: kevin9327 <5299031+kevin9327@users.noreply.github.com>
440dd49 to
75bd1c2
Compare
|
@dolfim-ibm thanks for the detailed review. All of it is in 75bd1c2 (rebased on current
|
Issue resolved by this Pull Request:
Resolves #4887
Problem
docling convertexits 0 even when documents fail.export_documentslogsDocument … failed to convert.and counts the failure, but the count never reaches the command, so a run where every input failed still looks successful to a shell script or CI job:convert-remotehas the same gap, even though its help lists1 runtime or connection failure (service unreachable, conversion error).Change
export_documentsreturns anExportSummary(success, partial_success, failure)instead of nothing.PARTIAL_SUCCESSdocuments are exported likeSUCCESSones. A warning names the document, followed by the same[Failure Detail]lines as for failures. They do not count as failures.SKIPPEDdocuments count as neither. The empty-Markdown check still turns a document into a failure.--fail-on {never,failure,partial}option onconvertandconvert-remote(FailOnis defined once indocling/cli/main.py). Both commands exit 1 whensummary.should_fail(fail_on)is true:failure(default) on any failed document,partialalso on partially converted ones,neveralways exits 0. The check runs after all documents have been processed and exported.--abort-on-erroris unchanged and only controls when processing stops.converthelp,convert-remoteexit codes mention--fail-on. Added the--fail-onrows todocs/reference/cli.mdand a row to the usage skill's CLI reference.Verification
tests/test_cli.py::test_cli_convert_exits_nonzero_when_a_document_fails: converts a file with a.pdfname that is not a PDF and expects exit code 1. Fails onmain(assert 0 == 1), passes with this change.tests/test_cli_remote.py::test_remote_exit_code_follows_fail_on(fake service client):FAILUREexits 1 by default and 0 with--fail-on never.PARTIAL_SUCCESSexits 0 by default with its Markdown written, and exits 1 with--fail-on partial.SKIPPEDexits 0 even with--fail-on partial.tests/test_cli_remote.py: 24 passed.ruff format --checkandruff check(v0.16.10) are clean.AI-assisted: I used an AI coding assistant to help write this change. I reproduced the bug and reviewed and ran the tests myself.
Checklist: