Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
04ecb19
feat(cfn-lang-ext): require enabled kwarg on expand_language_extensions
bnusunny May 19, 2026
b3ea2de
feat(cfn-lang-ext): add resolve_language_extensions_enabled resolver
bnusunny May 19, 2026
31e8082
feat(cli): add --language-extensions Click option decorator
bnusunny May 19, 2026
f0dc275
feat(providers): add language_extensions_enabled kwarg to get_stacks
bnusunny May 19, 2026
038c2db
feat(build): plumb --language-extensions flag through BuildContext
bnusunny May 20, 2026
b07fdc2
feat(package): plumb --language-extensions flag through PackageContext
bnusunny May 20, 2026
d88e058
refactor(build): hoist resolve_language_extensions_enabled to module-…
bnusunny May 20, 2026
60c8cc1
feat(deploy): plumb --language-extensions flag through DeployContext
bnusunny May 20, 2026
958fbd5
fix(deploy): preserve cycle-breaking inline imports in command.py
bnusunny May 20, 2026
85b95a0
feat(sync): plumb --language-extensions flag through sync stack
bnusunny May 20, 2026
83aac78
feat(validate): plumb --language-extensions flag through SamTemplateV…
bnusunny May 20, 2026
a55a99c
feat(local): plumb --language-extensions flag through InvokeContext
bnusunny May 20, 2026
8dc1a89
feat(package): plumb language_extensions_enabled through Template export
bnusunny May 20, 2026
b7219e4
chore(cfn-lang-ext): make language_extensions_enabled explicit on ins…
bnusunny May 20, 2026
9536a73
test(cfn-lang-ext): lock in single-telemetry-event behavior
bnusunny May 20, 2026
b3fab95
docs(cfn-lang-ext): document opt-in flag and env var
bnusunny May 20, 2026
afe088e
test(integration): exercise --language-extensions in ForEach build tests
bnusunny May 20, 2026
9562a8a
style: apply black formatting
bnusunny May 20, 2026
0d96fac
chore: regenerate samcli.json schema with language_extensions option
bnusunny May 20, 2026
217cf2b
fix(deploy): resolve mypy type errors in deploy_context and guided_co…
bnusunny May 20, 2026
d7e6b4a
test(integration): add else-branch assertion to opt-out regression test
bnusunny May 20, 2026
6fbb27b
fix(local): thread language_extensions_enabled through RefreshableSam…
bnusunny May 20, 2026
01fb8ee
test(integration): add --language-extensions to TestBuildWithLanguage…
bnusunny May 20, 2026
8f28988
test(integration): add --language-extensions to all LE integration tests
bnusunny May 20, 2026
44962bf
test(deploy): add build step to LE deploy integration test
bnusunny May 20, 2026
27e09b8
test(integration): add --language-extensions to invoke/start-api comm…
bnusunny May 20, 2026
912fbbd
ci: trigger integration test run
bnusunny May 20, 2026
bdcd614
test(deploy): add --language-extensions to deploy commands for LE tem…
bnusunny May 20, 2026
ed0d4f9
fix(deploy): thread language_extensions to PackageContext in deploy c…
bnusunny May 20, 2026
9f5934b
fix(sync): guard _detect_foreach_code_changes when LE is disabled
bnusunny May 20, 2026
d0f7f41
test(deploy): add s3_bucket to LE deploy test
bnusunny May 20, 2026
8ceafc3
Merge branch 'develop' into le-disable-and-backcompat-tests
bnusunny May 20, 2026
e03b99b
Merge branch 'develop' into le-disable-and-backcompat-tests
bnusunny May 21, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 59 additions & 3 deletions docs/cfn-language-extensions.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,71 @@ SAM CLI now supports templates that use the `AWS::LanguageExtensions` transform,

## How it works

When SAM CLI detects `AWS::LanguageExtensions` in a template's `Transform` section, it expands language extension constructs locally before running SAM transforms. This enables `sam build`, `sam package`, `sam deploy`, `sam sync`, `sam validate`, `sam local invoke`, and `sam local start-api` to work with templates that use these constructs.
When SAM CLI detects `AWS::LanguageExtensions` in a template's `Transform` section *and* the customer has opted in to local processing, it expands language extension constructs locally before running SAM transforms. This enables `sam build`, `sam package`, `sam deploy`, `sam sync`, `sam validate`, `sam local invoke`, `sam local start-api`, and `sam local start-lambda` to work with templates that use these constructs.

The expansion happens in two phases:
**Local processing is off by default.** When off, SAM CLI passes the template through unchanged; CloudFormation processes the `AWS::LanguageExtensions` transform server-side at deploy time (the pre-1.160.0 behavior).

The expansion happens in two phases when enabled:

1. **Phase 1 (Language Extensions)** — `Fn::ForEach` loops are expanded, intrinsic functions are resolved where possible, and the template is converted to standard CloudFormation.
2. **Phase 2 (SAM Transform)** — The expanded template is processed by the SAM Translator as usual.

The original template (with `Fn::ForEach` intact) is preserved for CloudFormation deployment, since CloudFormation processes the `AWS::LanguageExtensions` transform server-side.

## Enabling Language Extensions

Local processing of `AWS::LanguageExtensions` is opt-in per command. Three equivalent activation methods, in priority order:

1. **CLI flag** — pass `--language-extensions` on a single invocation:

```bash
sam build --language-extensions
sam package --language-extensions ...
sam deploy --language-extensions ...
```

`--no-language-extensions` explicitly disables, overriding the env var below.

2. **Environment variable** — set `SAM_CLI_ENABLE_LANGUAGE_EXTENSIONS=1` to enable for the current shell:

```bash
export SAM_CLI_ENABLE_LANGUAGE_EXTENSIONS=1
sam build
sam local invoke MyFunction
```

Truthy values (case-insensitive): `1`, `true`, `yes`. Anything else, including empty string, is off.

3. **`samconfig.toml`** — persist the choice per project:

```toml
[default.build.parameters]
language_extensions = true

[default.package.parameters]
language_extensions = true

[default.deploy.parameters]
language_extensions = true

[default.sync.parameters]
language_extensions = true

[default.local_invoke.parameters]
language_extensions = true

[default.local_start_api.parameters]
language_extensions = true

[default.local_start_lambda.parameters]
language_extensions = true

[default.validate.parameters]
language_extensions = true
```

**Each command needs its own activation.** Passing `--language-extensions` to `sam build` does not propagate to a later `sam local invoke` — local processing is decided per command invocation. Use the env var or samconfig entry to enable across commands without repeating the flag.

## Fn::ForEach

`Fn::ForEach` generates multiple resources, conditions, or outputs from a single template definition:
Expand Down Expand Up @@ -286,4 +342,4 @@ The following template issues are caught locally before the SAM transform runs:

## Telemetry

SAM CLI tracks usage of `AWS::LanguageExtensions` via the `CFNLanguageExtensions` telemetry feature flag. No template content is transmitted.
SAM CLI emits a `CFNLanguageExtensions` telemetry event when a command is invoked with `--language-extensions` (or its env-var equivalent) **and** the template declares the `AWS::LanguageExtensions` transform. The event fires once per invocation; no template content is transmitted. When local processing is off (the default), no event fires.
20 changes: 20 additions & 0 deletions samcli/commands/_utils/options.py
Original file line number Diff line number Diff line change
Expand Up @@ -1074,3 +1074,23 @@ def container_env_var_file_click_option(cls):
@parameterized_option
def container_env_var_file_option(f, cls):
return container_env_var_file_click_option(cls)(f)


def language_extensions_click_option():
return click.option(
"--language-extensions/--no-language-extensions",
"language_extensions",
required=False,
default=None,
Comment thread
vicheey marked this conversation as resolved.
is_flag=True,
help=(
"Expand AWS::LanguageExtensions transforms (Fn::ForEach, Fn::Length, "
"Fn::ToJsonString, Fn::FindInMap with DefaultValue) locally before "
"running SAM transforms. Off by default. "
"Equivalent env var: SAM_CLI_ENABLE_LANGUAGE_EXTENSIONS=1."
),
)


def language_extensions_option(f):
return language_extensions_click_option()(f)
12 changes: 12 additions & 0 deletions samcli/commands/build/build_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
from samcli.lib.build.workflow_config import UnsupportedRuntimeException
from samcli.lib.cfn_language_extensions.sam_integration import (
contains_loop_variable,
resolve_language_extensions_enabled,
sanitize_resource_key_for_mapping,
substitute_loop_variable,
)
Expand Down Expand Up @@ -93,6 +94,7 @@ def __init__(
mount_with: str = MountMode.READ.value,
mount_symlinks: Optional[bool] = False,
use_buildkit: Optional[bool] = False,
language_extensions: Optional[bool] = None,
Comment thread
bnusunny marked this conversation as resolved.
) -> None:
"""
Initialize the class
Expand Down Expand Up @@ -154,6 +156,10 @@ def __init__(
Indicates if symlinks should be mounted inside the container
use_buildkit Optional[bool]:
Enable buildkit for container image builds
language_extensions Optional[bool]:
Tri-state user input from the --language-extensions/--no-language-extensions
flag. None means the user did not pass either form (resolver falls back to
SAM_CLI_ENABLE_LANGUAGE_EXTENSIONS env var).
"""

self._resource_identifier = resource_identifier
Expand Down Expand Up @@ -197,6 +203,7 @@ def __init__(
self._mount_with = MountMode(mount_with)
self._mount_symlinks = mount_symlinks
self._use_buildkit = use_buildkit
self._language_extensions_enabled = resolve_language_extensions_enabled(language_extensions)

def __enter__(self) -> "BuildContext":
self.set_up()
Expand All @@ -209,6 +216,7 @@ def set_up(self) -> None:
self._template_file,
parameter_overrides=self._parameter_overrides,
global_parameter_overrides=self._global_parameter_overrides,
language_extensions_enabled=self._language_extensions_enabled,
)

if remote_stack_full_paths:
Expand Down Expand Up @@ -1088,6 +1096,10 @@ def cached(self) -> bool:
def use_container(self) -> bool:
return self._use_container

@property
def language_extensions_enabled(self) -> bool:
return self._language_extensions_enabled

@property
def stacks(self) -> List[Stack]:
return self._stacks
Expand Down
6 changes: 6 additions & 0 deletions samcli/commands/build/command.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
container_env_var_file_option,
docker_common_options,
hook_name_click_option,
language_extensions_option,
manifest_option,
mount_symlinks_option,
parameter_override_option,
Expand Down Expand Up @@ -85,6 +86,7 @@
@use_container_build_option
@use_buildkit_option
@build_in_source_option
@language_extensions_option
@click.option(
"--container-env-var",
"-e",
Expand Down Expand Up @@ -164,6 +166,7 @@ def cli(
build_in_source: Optional[bool],
mount_symlinks: Optional[bool],
use_buildkit: Optional[bool],
language_extensions: Optional[bool],
) -> None:
"""
`sam build` command entry point
Expand Down Expand Up @@ -197,6 +200,7 @@ def cli(
mount_with,
mount_symlinks,
use_buildkit,
language_extensions,
) # pragma: no cover


Expand Down Expand Up @@ -225,6 +229,7 @@ def do_cli( # pylint: disable=too-many-locals, too-many-statements
mount_with: str,
mount_symlinks: Optional[bool],
use_buildkit: Optional[bool],
language_extensions: Optional[bool],
) -> None:
"""
Implementation of the ``cli`` method
Expand Down Expand Up @@ -266,6 +271,7 @@ def do_cli( # pylint: disable=too-many-locals, too-many-statements
mount_with=mount_with,
mount_symlinks=mount_symlinks,
use_buildkit=use_buildkit,
language_extensions=language_extensions,
) as ctx:
ctx.run()

Expand Down
2 changes: 1 addition & 1 deletion samcli/commands/build/core/options.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
"base_dir",
]

TEMPLATE_OPTIONS: List[str] = ["parameter_overrides"]
TEMPLATE_OPTIONS: List[str] = ["parameter_overrides", "language_extensions"]

TERRAFORM_HOOK_OPTIONS: List[str] = ["terraform_project_root_path"]

Expand Down
11 changes: 11 additions & 0 deletions samcli/commands/deploy/command.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
image_repositories_option,
image_repository_option,
kms_key_id_option,
language_extensions_option,
metadata_option,
no_progressbar_option,
notification_arns_option,
Expand All @@ -38,6 +39,7 @@
from samcli.commands.deploy.utils import sanitize_parameter_overrides
from samcli.lib.bootstrap.bootstrap import manage_stack, print_managed_s3_bucket_info
from samcli.lib.bootstrap.companion_stack.companion_stack_manager import sync_ecr_stack
from samcli.lib.cfn_language_extensions.sam_integration import resolve_language_extensions_enabled
from samcli.lib.cli_validation.image_repository_validation import image_repository_validation
from samcli.lib.telemetry.metric import track_command
from samcli.lib.utils import osutils
Expand Down Expand Up @@ -159,6 +161,7 @@
@signing_profiles_option
@no_progressbar_option
@capabilities_option
@language_extensions_option
@aws_creds_options
@common_options
@save_params_option
Expand Down Expand Up @@ -194,6 +197,7 @@ def cli(
signing_profiles,
resolve_s3,
resolve_image_repos,
language_extensions,
save_params,
config_file,
config_env,
Expand Down Expand Up @@ -233,6 +237,7 @@ def cli(
config_file,
config_env,
resolve_image_repos,
language_extensions,
disable_rollback,
on_failure,
max_wait_duration,
Expand Down Expand Up @@ -267,6 +272,7 @@ def do_cli(
config_file,
config_env,
resolve_image_repos,
language_extensions,
disable_rollback,
on_failure,
max_wait_duration,
Expand All @@ -279,6 +285,8 @@ def do_cli(
from samcli.commands.deploy.guided_context import GuidedContext
from samcli.commands.package.package_context import PackageContext

language_extensions_enabled = resolve_language_extensions_enabled(language_extensions)

if guided:
# Allow for a guided deploy to prompt and save those details.
guided_context = GuidedContext(
Expand All @@ -300,6 +308,7 @@ def do_cli(
config_env=config_env,
config_file=config_file,
disable_rollback=disable_rollback,
language_extensions_enabled=language_extensions_enabled,
)
guided_context.run()
else:
Expand Down Expand Up @@ -338,6 +347,7 @@ def do_cli(
profile=profile,
signing_profiles=guided_context.signing_profiles if guided else signing_profiles,
parameter_overrides=context_param_overrides,
language_extensions=language_extensions,
) as package_context:
package_context.run()

Expand Down Expand Up @@ -376,5 +386,6 @@ def do_cli(
poll_delay=poll_delay,
on_failure=on_failure,
max_wait_duration=max_wait_duration,
language_extensions=language_extensions,
) as deploy_context:
deploy_context.run()
1 change: 1 addition & 0 deletions samcli/commands/deploy/core/options.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
CONFIGURATION_OPTION_NAMES: List[str] = ["config_env", "config_file"] + SAVE_PARAMS_OPTIONS

ADDITIONAL_OPTIONS: List[str] = [
"language_extensions",
"no_progressbar",
"signing_profiles",
"template_file",
Expand Down
13 changes: 11 additions & 2 deletions samcli/commands/deploy/deploy_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
print_deploy_args,
sanitize_parameter_overrides,
)
from samcli.lib.cfn_language_extensions.sam_integration import resolve_language_extensions_enabled
from samcli.lib.deploy.deployer import Deployer
from samcli.lib.deploy.utils import FailureMode
from samcli.lib.intrinsic_resolver.intrinsics_symbol_table import IntrinsicsSymbolTable
Expand Down Expand Up @@ -75,6 +76,7 @@ def __init__(
poll_delay,
on_failure,
max_wait_duration,
language_extensions: Optional[bool] = None,
):
self.template_file = template_file
self.stack_name = stack_name
Expand All @@ -99,7 +101,7 @@ def __init__(
self.region = region
self.profile = profile
self.s3_uploader = None
self.deployer = None
self.deployer: Optional[Deployer] = None
Comment thread
vicheey marked this conversation as resolved.
self.confirm_changeset = confirm_changeset
self.signing_profiles = signing_profiles
self.use_changeset = use_changeset
Expand All @@ -108,13 +110,18 @@ def __init__(
self.on_failure = FailureMode(on_failure) if on_failure else FailureMode.ROLLBACK
self._max_template_size = 51200
self.max_wait_duration = max_wait_duration
self._language_extensions_enabled = resolve_language_extensions_enabled(language_extensions)

def __enter__(self):
return self

def __exit__(self, *args):
pass

@property
def language_extensions_enabled(self) -> bool:
return self._language_extensions_enabled

def run(self):
"""
Execute deployment based on the argument provided by customers and samconfig.toml.
Expand Down Expand Up @@ -239,13 +246,15 @@ def deploy(
self.template_file,
parameter_overrides=sanitize_parameter_overrides(self.parameter_overrides),
global_parameter_overrides=self.global_parameter_overrides,
language_extensions_enabled=self._language_extensions_enabled,
)
auth_required_per_resource = auth_per_resource(stacks)

for resource, authorization_required in auth_required_per_resource:
if not authorization_required:
click.secho(f"{resource} has no authentication.", fg="yellow")

assert self.deployer is not None
if use_changeset:
try:
result, changeset_type = self.deployer.create_and_wait_for_changeset(
Expand Down Expand Up @@ -296,7 +305,7 @@ def deploy(
role_arn=role_arn,
notification_arns=notification_arns,
s3_uploader=s3_uploader,
tags=tags,
tags=tags, # type: ignore[arg-type]
on_failure=self.on_failure,
)
LOG.debug(result)
Expand Down
5 changes: 4 additions & 1 deletion samcli/commands/deploy/guided_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ def __init__(
config_env=None,
config_file=None,
disable_rollback=None,
language_extensions_enabled: bool = False,
):
self.template_file = template_file
self.stack_name = stack_name
Expand Down Expand Up @@ -93,8 +94,9 @@ def __init__(
self.start_bold = "\033[1m"
self.end_bold = "\033[0m"
self.color = Colored()
self.function_provider = None
self.function_provider: Optional[SamFunctionProvider] = None
self.disable_rollback = disable_rollback
self._language_extensions_enabled = language_extensions_enabled

@property
def guided_capabilities(self):
Expand Down Expand Up @@ -141,6 +143,7 @@ def guided_prompts(self, parameter_override_keys):
self.template_file,
parameter_overrides=sanitize_parameter_overrides(input_parameter_overrides),
global_parameter_overrides=global_parameter_overrides,
language_extensions_enabled=self._language_extensions_enabled,
)

click.secho("\t#Shows you resources changes to be deployed and require a 'Y' to initiate deploy")
Expand Down
Loading
Loading