Skip to content

cli: -o /dev/null fails, and its exit 2 masks the diagnostics' exit 1 #308

Description

@OmarAlJarrah

Problem

morphic compile <spec> -o /dev/null does not work, and its failure is indistinguishable
from a different failure a caller cares about.

replaceFile publishes by rename: it writes <dir>/.<base>.tmpXXXXXXXX beside the
destination and renames it over. /dev refuses the creation, so the run dies at
create output and returns 2 — overwriting the exit 1 the diagnostics had already earned.

Reproduced on main @ dbf0054 with a spec carrying an unresolvable $ref:

$ morphic compile bad.yaml -o /dev/null; echo "EXIT=$?"
error openapi/unresolved-ref bad.yaml#: not found -- struct is nil at /components/schemas
error openapi/unresolved-ref bad.yaml#/paths/~1x/get/.../schema: unresolved $ref "#/components/schemas/Missing"
morphic: create output "/dev/null": open /dev/.null.tmpc8d319552ad96472: operation not permitted
EXIT=2

$ morphic compile bad.yaml -o out.json >/dev/null; echo "EXIT=$?"
error openapi/unresolved-ref ...
EXIT=1

Same spec, same diagnostics, different exit code — decided by whether the destination's
directory tolerates a temp file. A caller reading the exit code cannot tell "the spec has
errors" (1) from "the output could not be written" (2).

Scope

This is not specific to /dev/null. Any destination whose directory will not take a new
entry has the same shape: a character device, a read-only directory holding a writable
file, a FIFO. TestWriteParsed_ReadOnlyDirFails already pins the read-only-directory case
as a deliberate consequence of publishing by rename, and replaceFile's doc comment
records the trade honestly. What is not recorded is that the resulting exit code
overwrites a meaningful one.

Notes

morphic validate (added in #81) removes the need for -o /dev/null as a lint gate, which
was its main use. It does not address the exit-code masking, which applies to any
unwritable destination — so this is filed separately rather than folded in.

Two directions worth weighing:

  • Keep the write failure at 2 but make it not overwrite a diagnostics failure — the exit
    code would report the more serious of the two.
  • Recognise destinations that cannot be published by rename and write through directly,
    accepting that a non-regular destination gets no atomicity because it has no bytes to
    protect. This one interacts with engine: spec problems surface as Go errors and usage exit codes instead of diagnostics #58's exit-code taxonomy.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions