feat: add --output json flag (sam build)#9136
Conversation
…able output Add a --output option to `sam build` that supports "text" (default, unchanged behavior) and "json" (structured output for programmatic consumers). When --output json is specified: - Success output is a JSON object with status, build_dir, template_file, and a resources array listing each built function's logical_id, runtime, and architecture. - Error output is a JSON object with status, error type, message, and the failing resource name when available. - Build progress logs are suppressed from stdout (sent to stderr only) so that stdout contains only valid JSON. This enables CI/CD pipelines, IDE extensions, and AI-assisted developer tools to consume build results programmatically without fragile text parsing.
There was a problem hiding this comment.
Code Review Results
Reviewed: 56510b1..54aed55
Files: 5
Comments: 3
Comments on lines outside the diff:
[samcli/commands/build/build_context.py:381] [BUG] In JSON mode both error handlers call sys.exit(1) instead of raising UserException. SystemExit inherits from BaseException, not Exception, so the @track_command decorator on the build CLI (see samcli/lib/telemetry/metric.py, which only catches (UserException, click.Abort, ...) and Exception) will not catch it. That means:
_send_command_run_metricsis never called for failedsam build --output jsonruns, soexit_reasonandexit_codefor these failures are silently dropped from telemetry.- The exit path diverges from every other
samcommand, which uniformly funnels errors throughUserException.
Consider raising UserException after emitting the JSON (the top-level Click framework already turns it into exit code 1), or at minimum route through click_ctx.exit(1) after ensuring telemetry has been flushed. A minimal fix:
if self._output == "json":
click.echo(json.dumps(error_result, indent=2))
# still raise so track_command records the failure;
# suppress the default click error output separately if needed
raise UserException(str(ex), wrapped_from=wrapped_from) from exThe same issue applies to the FunctionNotFound handler around line 381 and the multi-exception handler around line 407.
| download_dependencies, | ||
| self._combine_dependencies, | ||
| ) | ||
| try: |
There was a problem hiding this comment.
[BUG] The new try/except BuildError only wraps _build_function_in_process. The container path a few lines above (self._build_function_on_container(...)) is not wrapped, so ex.resource_name is never set when a function fails to build inside a container. That path can raise BuildError subclasses such as DockerBuildFailed, DockerfileOutSideOfContext, and DockerConnectionError (see raise DockerBuildFailed(...) at lines 440–493 of this file).
Consequence: for sam build --use-container — a very common invocation — the JSON failure payload will be missing the resource field that the PR description specifically calls out as the value-add over the existing text output. Wrap both branches (or set resource_name around the outer with osutils.mkdir_temp() block) so container failures also surface the failing resource:
try:
if self._container_manager:
return self._build_function_on_container(...)
return self._build_function_in_process(...)
except BuildError as ex:
ex.resource_name = function_name
raiseLayer builds (_build_layer) exhibit the same gap and should be considered as well.
| "runtime": f.runtime, | ||
| "architecture": f.architectures[0] if f.architectures else None, | ||
| } | ||
| for f in self.get_resources_to_build().functions |
There was a problem hiding this comment.
[BUG] The JSON success payload's resources list is built only from self.get_resources_to_build().functions, dropping layers. ResourcesToBuildCollector exposes both functions and layers (see samcli/lib/providers/provider.py:229), and sam build builds both. A template that contains only AWS::Serverless::LayerVersion resources — or a mixed template — will produce a JSON success payload with an incomplete (or empty) resources array, which is misleading to any downstream tooling that iterates the list.
Include layers, e.g.:
"resources": [
{
"logical_id": f.full_path,
"type": "function",
"runtime": f.runtime,
"architecture": f.architectures[0] if f.architectures else None,
}
for f in self.get_resources_to_build().functions
] + [
{
"logical_id": layer.full_path,
"type": "layer",
"compatible_runtimes": layer.compatible_runtimes,
}
for layer in self.get_resources_to_build().layers
],Also note that f.full_path is not strictly a CloudFormation logical id for nested-stack resources (it uses ParentStack/ChildLogicalId notation); consumers reading the field named logical_id may not expect the slash-separated form shown by full_path. Either rename the field to resource_id/path, or emit the raw logical id.
Summary
Add
--output jsonsupport tosam buildBefore
Error output is similarly unstructured:
Note: The error message does not identify which function failed.
After
Success output:
{ "status": "success", "build_dir": ".aws-sam/build", "template_file": ".aws-sam/build/template.yaml", "resources": [ {"logical_id": "OrderProcessorFunction", "runtime": "python3.12", "architecture": "x86_64"}, {"logical_id": "NotificationFunction", "runtime": "python3.12", "architecture": "arm64"} ] }Failure output (note the
resourcefield — the text error today does not identify which functionfailed):
{ "status": "failure", "error": { "type": "WorkflowFailedError", "message": "PythonPipBuilder:ResolveDependencies - Could not satisfy the requirement: nonexistent-package==1.0.0", "resource": "PaymentHandlerFunction" } }File Changes
samcli/commands/build/command.py(+11)Adds the
--outputClick option. Defaults the new parameter to"text"so existing callers andtests are unaffected.
samcli/commands/build/build_context.py(+93, -24)self._outputthe colored "Build Succeeded" banner
FunctionNotFounderror: emit a JSON error object (type, message) andsys.exit(1)instead ofraising
UserExceptionfor Click to render as textBuildErrorand other build failures: emit a JSON error object (type, message, resource) andsys.exit(1)— theresourcefield identifies which function caused the failuresamcli/lib/build/app_builder.py(+17, -13)_build_function(): catchBuildErrorfrom_build_function_in_process(), tag it withex.resource_name = function_name, then re-raise_build_function()is the one level that has both the error and the function identitysamcli/lib/build/exceptions.py(+4, -1)Adds an optional
resource_namefield toBuildErrorso the exception can carry the failing function'slogical ID up to the command layer. Defaults to
None; existing raisers are unaffected.Benchmark Notes
Adding
--output jsontosam builddoes not dramatically change agent performance metrics. Tokens, toolcalls, and time are roughly equivalent between text and JSON for this command. This is expected:
sam buildis a relatively simple command with clear, short output, and LLM agents parse both formatseffectively. We implemented it to keep scope consistent across the project (which adds
--output jsontoall major SAM CLI commands), and because the structured error output with the
resourcefield providesexplicit failure attribution that text mode lacks.
resourcefieldjson.loads()directlyScreenshots