Skip to content

feat: output json integration - #9172

Merged
reedham-aws merged 60 commits into
aws:developfrom
madhavdonthula1:feat/output-json-integration
Aug 12, 2026
Merged

reedham-aws merged 60 commits into
aws:developfrom
madhavdonthula1:feat/output-json-integration

Conversation

@madhavdonthula1

@madhavdonthula1 madhavdonthula1 commented Aug 7, 2026 •

Copy link
Copy Markdown
Contributor

Add --output json to sam build and sam deploy

Overview

  • This adds a --output json flag to both sam build and sam deploy.
  • The flag defaults to text, so every existing command behaves exactly as before.
  • JSON output is fully opt in and never turns on by itself.
  • CI jobs and agents can now read results as structured data instead of scraping text.
  • Both commands share one flag decorator and one OutputOption enum.

Important clarification on output shape

  • sam build prints exactly one JSON document at the end of the run.
  • sam deploy prints a stream of JSON lines as the deploy progresses.
  • Each deploy line is its own object with a type field.
  • The deploy types are changeset, event, outputs, warning, info, and result.
  • Streaming is intentional because a deploy runs for a long time.
  • A single document would force a consumer to wait until the very end.

Shared result contract (both commands)

  • Every terminal object has type: "result" and a lowercase status.
  • Failures use status: "failure" with error: {type, message}.
  • So a consumer can handle build and deploy failures with one code path.
  • build additionally lists error.resources (the affected resources), which is additive.

sam build files

samcli/commands/build/command.py

  • The command front door that Click runs first.
  • Adds the shared --output flag and threads it into do_cli.
  • Catches any build failure in one place and prints it as JSON.
  • Re-raises the error so telemetry and exit codes are unchanged.

samcli/commands/build/build_context.py

  • The worker that orchestrates the build.
  • Prints the JSON success document listing every built resource.
  • Reports build_dir and template_file as absolute paths in JSON.
  • The relpath transform is kept only for the human text banner.
  • Reports all affected resources as a list in error.resources.
  • Resolves a bare resource id to its full path for a matching namespace.
  • Re-raises a filesystem error as InvalidBuildDirException, not a raw OSError.
  • Skips the beta python-uv confirm prompt in JSON mode.
  • Fails with an actionable error if a JSON build needs a writable mount.

samcli/commands/build/utils.py

  • Extracts the write-mount detection out of the interactive prompt.
  • Lets the build context detect the .NET write-mount requirement without prompting.
  • Routes the mount confirm prompt to stderr in text mode.

samcli/lib/build/build_strategy.py

  • Tags build failures with the resources they affect.
  • Uses one shared context manager so every strategy attributes uniformly.
  • Also covers the cached and incremental build paths.

samcli/lib/build/exceptions.py and samcli/lib/build/workflow_config.py

  • Add a resource_names list attribute to the build exception classes.
  • Declaring it on the class also avoids mypy errors.

samcli/commands/build/core/options.py

  • Registers the output option so it shows under Output Options in help.

sam deploy files

samcli/commands/deploy/command.py

  • The deploy front door that Click runs.
  • Uses the same shared --output flag as build.
  • Rejects --guided and --confirm-changeset with JSON up front.
  • Wraps package and deploy so any failure ends the stream with a result line.
  • Redirects manage_stack progress to devnull under --resolve-s3 in JSON mode.
  • Re-surfaces the resolved bucket as a {type: info} line.
  • Forces the package progress bar off in JSON mode.

samcli/commands/deploy/deploy_context.py

  • Orchestrates the deploy and decides what to emit.
  • Parses the flag once into the OutputOption enum.
  • Suppresses the human deploy-args table and banners in JSON mode.
  • Emits terminal result lines for success, no_changes, and changeset_created.
  • The success line carries express, changeset_id, stack_name, and region.
  • Emits per-resource auth warnings as {type: warning} lines.

samcli/lib/deploy/deployer.py

  • The layer that calls CloudFormation directly.
  • Stores output mode as the OutputOption enum, raising on a bad value.
  • Emits changeset, event, and outputs as JSON lines.
  • Event lines carry detailed_status so failure detail is not lost.
  • Datetimes are serialized with isoformat.
  • Status messages like "Waiting..." are text-only.

samcli/commands/deploy/core/options.py

  • Registers the output option for the deploy help screen.

Shared and support files

samcli/commands/_utils/options.py

  • Holds the single shared --output flag decorator.
  • The accepted values are derived from the OutputOption enum, so they cannot drift.

samcli/commands/_utils/table_print.py

  • The shared table decorator skips the table chrome in JSON mode.
  • Raises on an unrecognized output mode instead of corrupting the stream.

samcli/commands/_utils/cdk_support_decorators.py

  • Routes the CDK-unsupported advisory to stderr so stdout stays clean.

samcli/commands/package/package_context.py

  • Routes the preview-runtime warning to stderr in JSON mode.

samcli/lib/package/language_extensions_packaging.py

  • Routes the parameter-collection warning to stderr unconditionally.

samcli/commands/exceptions.py

  • Adds the shared resource_names attribute to the base UserException.

Testing

  • Unit tests were added for the new JSON success, failure, and streaming paths.
  • Tests cover build attribution, cached builds, and deploy JSON output.
  • Key behaviors were mutation checked by breaking the code on purpose.
  • make pr passes and coverage stays above the required bar.

…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.
Comment thread samcli/commands/deploy/command.py Resolved
Comment thread samcli/commands/deploy/command.py Resolved
Comment thread samcli/commands/_utils/template.py Resolved
…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.

@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: 9101836..c941006
Files: 20 source files (14 test files skimmed)
Comments: 2

Comment thread samcli/commands/_utils/options.py Resolved
…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.

@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: 9101836..c798e75
Files: 38
Comments: 1

Comment thread samcli/commands/_utils/options.py Resolved

@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: 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.

Comment thread samcli/commands/deploy/command.py Resolved
Comment thread samcli/commands/deploy/command.py Resolved
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.

@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: 9101836..74f97c0
Files: 30
Comments: 3

Comment thread samcli/commands/deploy/command.py Resolved
Comment thread samcli/commands/deploy/command.py Resolved

@reedham-aws reedham-aws left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This is the only comment I have remaining, nice work!

Comment thread samcli/commands/deploy/command.py Outdated
Comment on lines +437 to +446
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},

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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 roger-zhangg left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

LGTM, let's merge and see if integration test picks up anything needs fixing.

@roger-zhangg
roger-zhangg added this pull request to the merge queue Aug 12, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Aug 12, 2026
@roger-zhangg
roger-zhangg added this pull request to the merge queue Aug 12, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Aug 12, 2026
@reedham-aws
reedham-aws added this pull request to the merge queue Aug 12, 2026
@reedham-aws

Copy link
Copy Markdown
Contributor

Merge failed on flaky integ tests.

Merged via the queue into aws:develop with commit 35e4146 Aug 12, 2026
53 checks passed
roger-zhangg pushed a commit that referenced this pull request Aug 12, 2026
…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.

@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: 9101836..e76e943
Files: 19 source files (12 test files skimmed)
Comments: 4

) 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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

Per 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:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

reedham-aws pushed a commit to madhavdonthula1/aws-sam-cli that referenced this pull request Aug 13, 2026
…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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/build sam build command area/deploy sam deploy command area/schema JSON schema file 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.

3 participants