Repository navigation
feat: output json integration - #9172
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.
- Replace sys.exit(1) with raise UserException in error handlers to
preserve telemetry tracking via @track_command decorator
- Wrap both container and in-process build paths in try/except so
resource_name is tagged on BuildError regardless of build mode
- Include layers in JSON success output alongside functions
- Rename field from logical_id to resource_id (accurate for nested stacks)
- Add type field ("function" or "layer") to each resource entry
Move the try/except BuildError from app_builder._build_function (which only covered the ZIP in-process path) to DefaultBuildStrategy's build_single_function_definition and build_single_layer_definition. This ensures resource_name is tagged for ALL build paths: - ZIP functions (in-process and container) - IMAGE functions (Docker builds) - Layer builds Known limitation: BuildInsideContainerError does not inherit from BuildError (it inherits from Exception directly), so it won't get resource_name tagged by this mechanism. Fixing this would require changing the exception hierarchy which is out of scope for this PR. The error type and message are still present in JSON output for this case — only the resource field is missing. Also fixes unit test assertion to expect the new output="text" kwarg.
… types Expand the except clause in DefaultBuildStrategy to also catch UnsupportedRuntimeException, BuildInsideContainerError, and UnsupportedBuilderLibraryVersionError. These exceptions don't inherit from BuildError but can originate from a specific function build (deprecated runtime, Docker pull failure, missing builder in container). Uses dynamic attribute assignment (ex.resource_name = ...) since these classes don't define resource_name in __init__. The consumer in build_context.py already uses getattr(ex, "resource_name", None) which handles both cases safely.
- Apply black formatting to command.py and build_strategy.py - Fix ruff import ordering in build_strategy.py - Add type: ignore comments for mypy union-attr and index errors (dynamic resource_name assignment on non-BuildError exceptions) - Regenerate schema/samcli.json to include the new --output parameter
Add --output json option to sam deploy that emits newline-delimited JSON (NDJSON) streaming output instead of human-formatted tables. Each deploy stage emits structured JSON lines in real-time as CloudFormation events arrive, preventing process timeouts during long-running deployments. Line types emitted: - changeset: one per resource change (action, logical_id, resource_type) - event: streaming CFN stack events during deploy - outputs: stack outputs after completion - result: final status line (SUCCESS/FAILED/NO_CHANGES/CHANGESET_CREATED)
Ensures stdout is pure NDJSON when --output json is used by: - Forcing no_progressbar=True on both PackageContext and DeployContext S3 uploaders, preventing upload progress lines from leaking to stderr - Raising samcli logger to WARNING level to suppress LOG.info messages
…eaming Thread output_mode=self.output_mode through the two describe_stack_events calls in rollback_delete_stack, preventing table-formatted text from leaking into the NDJSON stream during failure/rollback scenarios.
When --output json and --confirm-changeset are both set, emit a structured result line with status CONFIRMATION_REQUIRED and return without executing, instead of silently skipping the confirmation and proceeding with the deploy.
Move the FAILED result line to after rollback_delete_stack finishes, ensuring the result record is always the last line in the NDJSON stream. Previously it was emitted before rollback, causing event records to appear after the terminal result line.
Redirect stdout to /dev/null while manage_stack() runs in JSON mode,
preventing its click.echo calls ("Creating the required resources...",
"Successfully created!") from corrupting the NDJSON stream.
…s JSON
- Raise UsageError when --guided and --output json are combined, since
guided mode requires interactive prompts incompatible with JSON output.
- Emit {"type": "warning", "message": "no authentication", "resource": ...}
instead of silently dropping the auth warning in JSON mode.
- Move --output into a shared @output_option decorator in _utils/options.py so deploy/init can reuse it instead of redeclaring the option - Give --output its own "Output Options" help group rather than filing it under Build Strategy, which is for options that change how the build runs - Extract the JSON/text reporting out of BuildContext.run() into _print_build_success and _print_build_failure - Collect get_resources_to_build() once in run() and reuse it - Always emit the "resource" key in error output (null when the failure is not attributable to a single resource) so the schema is predictable - Hoist the wrapped_from lookup to the top of the except block instead of computing it in both branches - Drop the unused resource_name param from BuildError; build_strategy sets the attribute at runtime for all handled exception types
Cover _print_build_success and _print_build_failure across JSON and text modes: resource list shapes for functions/layers, architecture edge cases, the always-present "resource" error key, error-type passthrough, and the text-banner suppression path. 23 tests following the existing TestBuildContext_<method> convention.
- Rename output helpers to structured_output_option (avoid shadowing list command) - Use OutputOption enum instead of string literals - Emit JSON error for InvalidBuildDirException; soften "always present" comment - Single-line JSON (remove indent=2) - Add resource_name class attribute to all 4 build exception types, drop type: ignore - Add end-to-end run() JSON test; remove 6 redundant helper tests - Use function.architecture (resolves x86_64 default) instead of architectures[0] - Route interactive prompts to stderr (err=True) so stdout stays pure JSON
Move all --output json failure serialization to a single except UserException handler in do_cli, covering every failure path uniformly (missing layer BuildMethod, invalid build dir, pre-processing errors) instead of only the exception types caught inside run(). - Add build_failure_json() helper in build_context as the single source of truth for the failure wire format - do_cli uses the helper and checks OutputOption(output) is OutputOption.json (enum, not raw string literal) - run() only prints the text "Build Failed" banner and re-raises UserException carrying resource_name; no double-emit - Add resource_name class attribute to UserException - Move JSON failure tests to test_command.py (do_cli level); simplify _print_build_failure tests to text-banner only
- Merge two near-identical "stays silent" tests into one parameterized test - Use OutputOption enum instead of raw "text"/"json" strings so the tests match how the real code sets _output (via enum conversion in __init__), removing a latent fragility where raw strings passed only by coincidence
UnsupportedBuilderException (raised for an invalid Metadata.BuildMethod, e.g. a template typo) subclasses plain Exception, so it escaped both run()'s except tuple and do_cli's UserException handler - exiting 1 with empty stdout in --output json. - Add it to run()'s except tuple so it converts to UserException and routes through build_failure_json, matching its sibling UnsupportedRuntimeException - Add it to the DefaultBuildStrategy except tuples so resource_name is attributed - Add resource_name class attribute (Comment aws#5 pattern) - Extend test_must_catch_known_exceptions to cover it
…ilures Failure paths that know exactly which resource failed were reporting "resource": null in --output json: - FunctionNotFound handler had a dead _print_build_failure(print_text_banner=False) no-op and a getattr that FunctionNotFound never carries. Both failure handlers now fall back to self._resource_identifier, so sam build <bad-name> reports that name. - MissingBuildMethodException (raised from _collect_single_buildable_layer, a UserException) was not in run()'s except tuple, so it reached do_cli with no resource attribution. Added it; the layer name now populates via the fallback. Tests: use OutputOption.text (not the raw string) in the two success-path text-mode tests so they actually exercise the enum comparison, and assert resource_name fallback in test_must_catch_known_exceptions.
…comment - Extend the try in _setup_build_dir to cover shutil.rmtree, not just mkdir. An OSError while clearing the build dir (permission denied, file held open on Windows) now surfaces as InvalidBuildDirException -> JSON error, instead of a bare OSError that exits 1 with empty stdout in --output json. - Soften the do_cli handler comment: it covers user-facing failures, not "every failure path"; note that an unexpected internal error (bare Exception) is not serialized. - Add test_rmtree_oserror_is_converted_to_invalid_build_dir.
… param - Change do_cli's except UserException to except Exception so every build-path failure emits a JSON document, including bare-Exception template errors (InvalidLayerReference, RemoteStackLocationNotSupported, MissingCodeUri, InvalidTemplateFile) that are not UserException subclasses. Re-raises unconditionally so @track_command still records telemetry and wraps non-UserException as UnhandledException. - Drop the now-dead print_text_banner parameter from _print_build_failure; no caller passes False since the FunctionNotFound handler stopped calling it. - Add test_json_failure_for_bare_exception; simplify the silence test.
process_env_var and process_image_options ran outside the try, so an InvalidImageException from a malformed --build-image (e.g. --build-image MyFunc=) exited 1 with empty stdout in --output json. Moving both inside the try routes them through the central JSON failure handler.
The three comments claimed do_cli's handler serializes "every error reachable from the build". That is only true for execution failures once do_cli runs. Errors raised during click option processing (invalid flags, or a --hook-name prepare-hook failure re-raised by track_command) occur before do_cli and surface as click's standard usage/stderr output, not JSON. Soften the three comments to state the guarantee covers execution failures and note that invocation errors surface via click's stderr, so consumers treat "exit != 0 with empty stdout" as an invocation error. No behavior change.
…ailures - Add package_type to each function in the JSON success document so Image functions (runtime: null) are distinguishable from Zip, via a new _function_to_json helper. - Restrict the resource_name fallback: InvalidBuildGraphException and UnsupportedBuilderLibraryVersionError are not resource-specific, so they keep resource=None instead of being attributed to whatever resource the user named. - Document that error.resource is a representative id when functions share a build definition (points to get_resource_full_paths for the full set). - Tests: package_type for image functions; per-type resource_name expectations.
The JSON success document reports resources[].resource_id as the resource's full_path (nested-stack-qualified, e.g. ChildStack/MyFn), but the failure fallback reported the raw CLI identifier (MyFn). Consumers could not correlate a failure back to the resource_id they saw on success. Add _resolve_resource_full_path(), which looks the identifier up via the same function/layer providers the success document uses and returns its full_path, falling back to the raw identifier only when lookup fails. Both fallback paths (FunctionNotFound and the general handler) now route through it.
The comment pointed JSON consumers at get_resource_full_paths() as an escape hatch for the full set of collapsed functions, but that method is internal and nothing in the failure document exposes it. Naming one representative is the deliberate contract — error.resource is a single value. Update the comment to say so and stop describing an escape hatch that does not exist for this output.
Address reviewer feedback that some inline comments were long and hard to read. Shorten the failure-attribution, mount-with-write, beta-prompt, and build- strategy comments; no code or behavior changes.
…hanged The eager option callbacks now defer template parse errors so they can reach do_cli and be serialized in --output json mode. On their own, that let do_cli run side effects (e.g. manage_stack under --resolve-s3) before a malformed/ missing template surfaced. Parse the template as the first step inside do_cli's try so the error raises before any side effect: text mode matches the prior behavior (same exit code/stderr, no side effect) and json mode still serializes the failure. Tests added for both modes.
…failure The shared --template callback now defers template parse errors so deploy can serialize them as JSON. package and sync call manage_stack (which creates/looks up the managed CloudFormation stack and S3 bucket) before parsing, so on a malformed template they would touch AWS before failing. Parse the template as the first step of each do_cli so the error raises before any side effect, matching the prior behavior. Existing tests stub the new parse.
…raise a clean error
There was a problem hiding this comment.
Code Review Results
Reviewed: 9101836..363d8dc
Files: 40 (24 source files reviewed in detail; 16 test files skimmed)
Comments: 3
Most previously raised issues are addressed in this revision: build now emits absolute paths, both commands share type: "result" + lowercase status + error.{type,message,resources}, the resolved bucket is surfaced as an info line, the unreachable confirmation_required status is gone, changeset_id and detailed_status are back in the stream, the mount-with-write case fails with an actionable error, the try in deploy now wraps S3/ECR resolution, and the stdout leaks (cdk_support_decorators, warn_parameter_based_collections, _warn_preview_runtime, experimental/mount prompts) are routed to stderr. I also verified the deferred template-parse errors do resurface for every --template-file consumer (build, deploy, package, sync, validate, publish, list, local), so those two regressions are compensated.
Reverts the shared --template callback deferral, the get_template_artifacts_ format_or_empty helper, the image_repository_validation get_stacks catch, and the compensating up-front get_template_data parses in deploy/package/sync/ validate (commits e6137d4, 8438a07, c798e75, 363d8dc). A malformed/missing template surfaces during option processing before do_cli, so in --output json mode it now exits 1 with the error on stderr and empty stdout (matching text mode and develop) rather than a serialized JSON failure. The deferral changed behavior across every command sharing the --template option, which was not worth the JSON-doc gain for a config-time error. Keeps the deploy failure resources-key parity and the deploy_context click.echo cleanup.
reedham-aws
left a comment
There was a problem hiding this comment.
This is the only comment I have remaining, nice work!
| if output_mode is OutputOption.json: | ||
| click.echo( | ||
| json.dumps( | ||
| { | ||
| "type": "result", | ||
| "status": "failure", | ||
| # resources is always present (null here) to match build's failure shape, so a | ||
| # consumer can read error.resources uniformly across commands. Deploy failures | ||
| # are not attributed to specific resources, so it is null. | ||
| "error": {"type": type(ex).__name__, "message": str(ex), "resources": None}, |
There was a problem hiding this comment.
One final concern I have here is that in build, we have a dedicated helper in build_context.py that creates the error: https://github.com/madhavdonthula1/aws-sam-cli/blob/74f97c042f4eb1842f5b8d4a60888b36cadcdf72/samcli/commands/build/build_context.py#L78-L97
Here, we do it directly in the function. The result is that they're slightly different as this one does not use "wrapped_from".
Ideally, they would share a function for this as the types are all pretty generic.
There was a problem hiding this comment.
Share a single failure_result_json helper between build and deploy. build had build_failure_json while deploy inlined its own dict, and they'd drifted (deploy lacked wrapped_from and hardcoded resources: null). Extract failure_result_json in observability/util.py; build_failure_json delegates to it and deploy calls it directly, so both emit the same failure shape.
build had build_failure_json while deploy inlined its own dict, and they had drifted: deploy lacked wrapped_from and hardcoded resources: null. Extract failure_result_json in observability/util.py as the single source of truth; build_failure_json delegates to it and deploy calls it directly, so both emit the same failure shape and deploy now picks up wrapped_from too.
roger-zhangg
left a comment
There was a problem hiding this comment.
LGTM, let's merge and see if integration test picks up anything needs fixing.
|
Merge failed on flaky integ tests. |
…ests (#9175) The output json integration (#9172) moved the CDK not-supported warning from stdout to stderr so it does not pollute the machine-readable --output json stdout stream. Three integration tests still asserted the warning on stdout and now fail on develop: - package/test_package_command_zip.py::test_package_logs_warning_for_cdk_project - deploy/test_deploy_command.py::test_deploy_logs_warning_with_cdk_project - validate/test_validate_command.py::test_validate_logs_warning_for_cdk_project Update each to assert the warning on stderr (capturing stderr in the package test's Popen). Test-only change; product behavior is unchanged.
| ) as deploy_context: | ||
| deploy_context.run() | ||
| except Exception as ex: | ||
| # Emit a terminal failure result for errors the per-step handlers miss, so the JSON stream |
There was a problem hiding this comment.
[ERROR_HANDLING] The JSON failure document is built from the raw exception, but cli is wrapped in @command_exception_handler (line 182), which rewrites several exceptions on the way out. do_cli's handler runs first, so the machine-readable stream and the human stderr message disagree for the most common failures:
except Exception as ex:
if output_mode is OutputOption.json:
click.echo(failure_result_json(ex))
raisePer samcli/commands/_utils/command_exception_handler.py, NoRegionError → RegionError ("No region information found. Please provide --region..."), ClientError → AWSServiceClientError (message plus a docs link), BotoCoreError → SDKError. A missing region therefore emits error.type: "NoRegionError" with botocore's raw text in JSON, while text mode prints the actionable RegionError message. Consider running the same mapping before serializing (or serializing inside/after the handler) so error.type/error.message match what text mode reports. This was raised previously and is unchanged.
| image_repositories = sync_ecr_stack( | ||
| template_file, stack_name, region, s3_bucket, s3_prefix, image_repositories | ||
| ) | ||
| if guided and output_mode is OutputOption.json: |
There was a problem hiding this comment.
[GENERAL] The two incompatibility guards are raised before the try, so they remain the one class of do_cli failure that produces no terminal result object:
if guided and output_mode is OutputOption.json:
raise click.UsageError("--guided is not compatible with --output json")
if confirm_changeset and output_mode is OutputOption.json:
raise click.UsageError("--confirm-changeset is not compatible with --output json")Click prints the usage error to stderr and exits 2, leaving stdout completely empty. Since confirm_changeset is persisted into samconfig.toml by --guided, this is the easiest way for a CI/agent consumer to hit it — and the consumer sees an empty stream rather than a {"type": "result", "status": "failure"} line it can parse. Moving the guards inside the try (or emitting the failure document before raising) would keep the contract "every terminated run ends with a result object." Raised twice previously without resolution.
|
|
||
| if no_execute_changeset: | ||
| if self._output_mode is OutputOption.json: | ||
| click.echo( |
There was a problem hiding this comment.
[GENERAL] The NDJSON stream is still written through two different paths: deploy_context emits every line with click.echo(json.dumps(...)) (changeset_created, no_changes, warning, success), while deployer.py emits changeset/event/outputs lines with sys.stdout.write(json.dumps(...) + "\n") plus an explicit flush(). That contradicts the rationale given when this was raised earlier ("Every line in the NDJSON output uses sys.stdout.write(...) + "\n" followed by an explicit flush, both here in deploy_context and across deployer.py ... I kept it uniform on purpose"). Two writers over one fd with different buffering/flush behavior is exactly the kind of thing that makes interleaving order dependent on the terminal/pipe, and it means a reader of this code cannot tell which convention new lines should follow. Pick one emitter (a small _emit_json(obj) helper would do) and route all lines through it.
| for k, v in changes.items(): | ||
| for value in v: | ||
| row_color = self.deploy_color.get_changeset_action_color(action=k) | ||
| if kwargs.get("output_mode") == OutputOption.json.value: |
There was a problem hiding this comment.
[GENERAL] Deployer now normalizes and stores the mode as an enum in init (self.output_mode), but describe_changeset and describe_stack_events still read it from an optional, untyped kwarg:
if kwargs.get("output_mode") == OutputOption.json.value:Because kwargs.get defaults to None, any call site that forgets to thread output_mode=self.output_mode.value silently falls back to the table branch and injects ANSI-colored column text into the middle of a JSON-lines stream. All four current call sites pass it (wait_for_execute, create_and_wait_for_changeset, two in rollback_delete_stack), so this is latent rather than broken today — but the instance already knows the mode. These two methods can branch on self.output_mode is OutputOption.json and let the kwarg exist purely for table_print to decide whether to draw the table chrome, which removes the possibility of drift.
…ests (aws#9177) The output json integration (aws#9172) moved the CDK not-supported warning from stdout to stderr so it does not pollute the machine-readable --output json stdout stream. Three integration tests still asserted the warning on stdout and now fail on develop: - package/test_package_command_zip.py::test_package_logs_warning_for_cdk_project - deploy/test_deploy_command.py::test_deploy_logs_warning_with_cdk_project - validate/test_validate_command.py::test_validate_logs_warning_for_cdk_project Update each to assert the warning on stderr (capturing stderr in the package test's Popen). Test-only change; product behavior is unchanged.
Add
--output jsonto sam build and sam deployOverview
--output jsonflag to both sam build and sam deploy.OutputOptionenum.Important clarification on output shape
typefield.Shared result contract (both commands)
type: "result"and a lowercasestatus.status: "failure"witherror: {type, message}.error.resources(the affected resources), which is additive.sam build files
samcli/commands/build/command.py--outputflag and threads it into do_cli.samcli/commands/build/build_context.pysamcli/commands/build/utils.pysamcli/lib/build/build_strategy.pysamcli/lib/build/exceptions.pyandsamcli/lib/build/workflow_config.pyresource_nameslist attribute to the build exception classes.samcli/commands/build/core/options.pysam deploy files
samcli/commands/deploy/command.py--outputflag as build.--guidedand--confirm-changesetwith JSON up front.--resolve-s3in JSON mode.{type: info}line.samcli/commands/deploy/deploy_context.py{type: warning}lines.samcli/lib/deploy/deployer.pysamcli/commands/deploy/core/options.pyShared and support files
samcli/commands/_utils/options.py--outputflag decorator.samcli/commands/_utils/table_print.pysamcli/commands/_utils/cdk_support_decorators.pysamcli/commands/package/package_context.pysamcli/lib/package/language_extensions_packaging.pysamcli/commands/exceptions.pyresource_namesattribute to the base UserException.Testing
make prpasses and coverage stays above the required bar.