Skip to content

Commit f8dd085

Browse files
TMHSDigitalclaude
andcommitted
fix(templates): keep exit 2 for argparse usage errors only
Both headless templates also returned 2 for bad input (no mesh in the .blend, a missing --input file), so a CI caller could not tell a bad invocation from bad data. CONTRIBUTING even called the no-mesh 2 correct. The headless template's no-mesh is now 5, matching the pipeline's no-mesh, and the pipeline's missing input is 13. The existing 3, 4 and 5-12 keep their meaning. Verified on 5.2.1: no-mesh exits 5, missing input 13, --preset bogus still 2. The smoke steps assert the new codes, and the exit-code gate now rejects a literal 2 (and a non-usage row 2) in templates/; the old pipeline fails it. Closes #490 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: TMHSDigital <154358121+TMHSDigital@users.noreply.github.com>
1 parent f6d0439 commit f8dd085

7 files changed

Lines changed: 33 additions & 14 deletions

File tree

‎.github/workflows/blender-smoke.yml‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -213,14 +213,14 @@ jobs:
213213
test -s "$RUNNER_TEMP/out/out.glb" || { echo "::error::glTF output missing/empty"; exit 1; }
214214
head -c4 "$RUNNER_TEMP/out/out.glb" | grep -q "glTF" || { echo "::error::not a glTF binary"; exit 1; }
215215
216-
- name: Headless template no-mesh path returns exit 2
216+
- name: Headless template no-mesh path returns exit 5
217217
run: |
218218
set +e
219219
xvfb-run -a "$BLENDER" --background "$RUNNER_TEMP/out/empty.blend" \
220220
--python-exit-code 1 --python templates/headless-batch-script-template/script.py -- --output "$RUNNER_TEMP/out/none.glb"
221221
code=$?
222222
set -e
223-
[ "$code" -eq 2 ] || { echo "::error::expected exit 2 for no-mesh input, got $code"; exit 1; }
223+
[ "$code" -eq 5 ] || { echo "::error::expected exit 5 for no-mesh input, got $code"; exit 1; }
224224
echo "no-mesh exit code = $code (correct)"
225225
226226
- name: Headless template applies the new modifier after the existing stack (#468)
@@ -261,7 +261,7 @@ jobs:
261261
test -s "$RUNNER_TEMP/out/pipeline/collider.glb" || { echo "::error::collider missing/empty"; exit 1; }
262262
head -c4 "$RUNNER_TEMP/out/pipeline/lod0.glb" | grep -q "glTF" || { echo "::error::lod0 not a glTF binary"; exit 1; }
263263
264-
- name: Headless pipeline template missing input returns exit 2
264+
- name: Headless pipeline template missing input returns exit 13
265265
run: |
266266
set +e
267267
xvfb-run -a "$BLENDER" --background --python-exit-code 1 --python templates/ai-asset-pipeline-template/pipeline.py -- \
@@ -270,7 +270,7 @@ jobs:
270270
--preset unity
271271
code=$?
272272
set -e
273-
[ "$code" -eq 2 ] || { echo "::error::expected exit 2 for missing input, got $code"; exit 1; }
273+
[ "$code" -eq 13 ] || { echo "::error::expected exit 13 for missing input, got $code"; exit 1; }
274274
echo "missing-input exit code = $code (correct)"
275275
276276
- name: Pipeline template keeps every part of a shared-mesh, parented GLB (#462, #467)

‎CONTRIBUTING.md‎

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -235,10 +235,11 @@ code; there is no rule against it.
235235
**Per-script exits** (examples and headless templates). `0` success. `2`
236236
argument or usage error, matching argparse. `3` and above for that script's
237237
own sequential check failures, in the order the checks run. These codes are
238-
file-local and are not portable. `no-mesh` is `2` in
239-
`templates/headless-batch-script-template/` and `5` in
240-
`templates/ai-asset-pipeline-template/`; both are correct. Copy a template's
241-
own table from that template, not from this paragraph.
238+
file-local and are not portable, except that `2` is argparse's usage exit
239+
everywhere: no template or example spends it on a check, so a CI caller can
240+
tell a bad invocation from bad input (#490; `tests/check_exit_code_readme.py`
241+
enforces it). `no-mesh` is `5` in both templates. Copy a template's own table
242+
from that template, not from this paragraph.
242243

243244
**Reserved in examples/.** Three codes mean one thing in every example. `2`
244245
is argparse's usage exit only, never a check. `10` is the gallery framing

‎templates/ai-asset-pipeline-template/README.md‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ as `export-preset-axis` number their own checks independently.
8080
| Code | Meaning |
8181
| --- | --- |
8282
| 0 | Success |
83-
| 2 | Input file missing, or argparse rejected the flags (including an unsupported `--preset`) |
83+
| 2 | argparse rejected the flags, including an unsupported `--preset` (usage error only) |
8484
| 3 | Input is not a readable GLB (bad magic or import failure) |
8585
| 4 | `--lod-budgets` missing, non-positive, or not strictly decreasing |
8686
| 5 | Import produced no mesh |
@@ -89,6 +89,7 @@ as `export-preset-axis` number their own checks independently.
8989
| 8 | The joined asset's largest extent is outside 0.01 m to 100 m (wrong source units) |
9090
| 9 | An LOD's evaluated triangle count is over its `--lod-budgets` entry |
9191
| 12 | `transform_apply` or `object.join` raised `RuntimeError` (for example multi-user data that was not isolated) |
92+
| 13 | `--input` file does not exist |
9293

9394
10 and 11 are skipped on purpose. The repo's shared render gates use them
9495
(framing and asset quality), so a template never reuses them.

‎templates/ai-asset-pipeline-template/pipeline.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -361,7 +361,7 @@ def main():
361361

362362
if not os.path.isfile(args.input):
363363
print(f"ERROR: input file missing: {args.input}", file=sys.stderr)
364-
return 2
364+
return 13 # not 2: that is argparse's usage exit
365365

366366
if not glb_magic_ok(args.input):
367367
print(f"ERROR: input is not a readable GLB: {args.input}", file=sys.stderr)

‎templates/headless-batch-script-template/README.md‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,17 +48,18 @@ is legal; argparse usage is `2`). Not a repo-wide table.
4848
| Code | Meaning |
4949
| --- | --- |
5050
| 0 | Success |
51-
| 2 | argparse rejected the flags, or no mesh objects in the input `.blend` |
51+
| 2 | argparse rejected the flags (usage error only) |
5252
| 3 | Modifier apply raised `RuntimeError` |
5353
| 4 | glTF export raised `RuntimeError` |
54+
| 5 | No mesh objects in the input `.blend` (same code as the pipeline template's no-mesh) |
5455

5556
## Expected environment
5657

5758
- Blender on the system `PATH` (or invoked by absolute path).
5859
- Write access to the output directory. The script does not create
5960
parent directories; create them in your shell wrapper if needed.
6061
- Input `.blend` exists and contains at least one mesh object. With no
61-
meshes the script returns exit code 2.
62+
meshes the script returns exit code 5.
6263

6364
## Common gotchas
6465

‎templates/headless-batch-script-template/script.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ def main():
9696
mesh_objects = [obj for obj in bpy.data.objects if obj.type == "MESH"]
9797
if not mesh_objects:
9898
print("ERROR: no mesh objects in the input .blend", file=sys.stderr)
99-
return 2
99+
return 5 # not 2: that is argparse's usage exit
100100

101101
print(f"Found {len(mesh_objects)} mesh object(s): {[o.name for o in mesh_objects]}")
102102

‎tests/check_exit_code_readme.py‎

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -371,9 +371,11 @@ def readme_table_rows(text):
371371
return rows
372372

373373

374-
def reserved_row_errors(rows, readme_rel):
374+
def reserved_row_errors(rows, readme_rel, codes=None):
375375
errors = []
376376
for code, (pattern, meaning) in RESERVED.items():
377+
if codes is not None and code not in codes:
378+
continue
377379
text = rows.get(code)
378380
if text is None:
379381
continue
@@ -469,6 +471,20 @@ def check_file(path):
469471
if os.path.isfile(readme):
470472
rows = readme_table_rows(open(readme, encoding="utf-8").read())
471473
errors.extend(reserved_row_errors(rows, relpath(readme)))
474+
elif rel.startswith("templates/"):
475+
# Templates are copied into CI pipelines, where exit 2 must mean a bad
476+
# invocation and nothing else (#490). 10 and 11 are example-gallery
477+
# gates, so only 2 is reserved here.
478+
for lineno, code in literal_reserved_sites(tree, passthrough_helpers(tree)):
479+
if code == 2:
480+
errors.append(
481+
f"{rel}:{lineno}: literal exit 2 is argparse's usage exit; "
482+
"give this check its own code"
483+
)
484+
readme = os.path.join(os.path.dirname(path), "README.md")
485+
if os.path.isfile(readme):
486+
rows = readme_table_rows(open(readme, encoding="utf-8").read())
487+
errors.extend(reserved_row_errors(rows, relpath(readme), codes={2}))
472488

473489
nonzero = {}
474490
for code, lineno in visitor.literals:

0 commit comments

Comments
 (0)