Skip to content

feat: add --output json flag (sam build)#9136

Open
madhavdonthula1 wants to merge 1 commit into
aws:developfrom
madhavdonthula1:feat/build-output-json-clean
Open

feat: add --output json flag (sam build)#9136
madhavdonthula1 wants to merge 1 commit into
aws:developfrom
madhavdonthula1:feat/build-output-json-clean

Conversation

@madhavdonthula1

Copy link
Copy Markdown

Summary

Add --output json support to sam build

Before

Build Succeeded

Built Artifacts  : .aws-sam/build
Built Template   : .aws-sam/build/template.yaml

Commands you can use next
=========================
[*] Validate SAM template: sam validate
...

Error output is similarly unstructured:

Build Failed
Error: PythonPipBuilder:ResolveDependencies - Could not satisfy the requirement: nonexistent-package==1.0.0

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 resource field — the text error today does not identify which function
failed):

{
  "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 --output Click option. Defaults the new parameter to "text" so existing callers and
tests are unaffected.

samcli/commands/build/build_context.py (+93, -24)

  • Store the output format as self._output
  • Suppress samcli INFO-level logs in JSON mode so stdout is pure JSON
  • On success: emit a structured JSON object (status, build_dir, template_file, resources list) instead of
    the colored "Build Succeeded" banner
  • On FunctionNotFound error: emit a JSON error object (type, message) and sys.exit(1) instead of
    raising UserException for Click to render as text
  • On BuildError and other build failures: emit a JSON error object (type, message, resource) and
    sys.exit(1) — the resource field identifies which function caused the failure
  • Text mode follows the exact same code paths as before

samcli/lib/build/app_builder.py (+17, -13)

  • In _build_function(): catch BuildError from _build_function_in_process(), tag it with
    ex.resource_name = function_name, then re-raise
  • This is needed because the error originates deep in the builder where the function name isn't known.
    _build_function() is the one level that has both the error and the function identity

samcli/lib/build/exceptions.py (+4, -1)

Adds an optional resource_name field to BuildError so the exception can carry the failing function's
logical ID up to the command layer. Defaults to None; existing raisers are unaffected.

Benchmark Notes

Adding --output json to sam build does not dramatically change agent performance metrics. Tokens, tool
calls, and time are roughly equivalent between text and JSON for this command. This is expected: sam build is a relatively simple command with clear, short output, and LLM agents parse both formats
effectively. We implemented it to keep scope consistent across the project (which adds --output json to
all major SAM CLI commands), and because the structured error output with the resource field provides
explicit failure attribution that text mode lacks.

Metric Text Output JSON Output
Tokens (avg per task) ~28,800 ~28,400
Tool calls (error diagnosis) 5 4
Time (avg) ~180s ~165s
Error attribution Inferred from log line ordering Explicit resource field
Programmatic parsing Regex/sed required json.loads() directly

Screenshots

Screenshot 2026-07-20 at 12 46 40 PM Screenshot 2026-07-20 at 12 47 18 PM Screenshot 2026-07-20 at 12 47 45 PM

…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.
@madhavdonthula1
madhavdonthula1 requested a review from a team as a code owner July 20, 2026 19:48
@github-actions github-actions Bot added area/build sam build command pr/external stage/needs-triage Automatically applied to new issues and PRs, indicating they haven't been looked at. labels Jul 20, 2026

@aws-sam-tooling-bot aws-sam-tooling-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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_metrics is never called for failed sam build --output json runs, so exit_reason and exit_code for these failures are silently dropped from telemetry.
  • The exit path diverges from every other sam command, which uniformly funnels errors through UserException.

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 ex

The same issue applies to the FunctionNotFound handler around line 381 and the multi-exception handler around line 407.

download_dependencies,
self._combine_dependencies,
)
try:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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
   raise

Layer 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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/build sam build command pr/external stage/needs-triage Automatically applied to new issues and PRs, indicating they haven't been looked at.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant