From be690ebf2807f3bdf7a4a7f9dc53588a4454cbe3 Mon Sep 17 00:00:00 2001 From: Saketh Bandi <79930864+saketh-bandi@users.noreply.github.com> Date: Thu, 3 Sep 2026 13:43:04 -0700 Subject: [PATCH 1/4] fix: skip binary validation for copy-only workflows --- .../workflows/nodejs_npm/workflow.py | 10 +++++- .../workflows/ruby_bundler/workflow.py | 12 +++++++ .../workflows/nodejs_npm/test_nodejs_npm.py | 7 +++- .../workflows/ruby_bundler/test_ruby.py | 7 +++- .../workflows/nodejs_npm/test_workflow.py | 33 +++++++++++++++++++ .../workflows/ruby_bundler/test_workflow.py | 33 +++++++++++++++++++ 6 files changed, 99 insertions(+), 3 deletions(-) diff --git a/aws_lambda_builders/workflows/nodejs_npm/workflow.py b/aws_lambda_builders/workflows/nodejs_npm/workflow.py index 83aab8165..32bff1bce 100644 --- a/aws_lambda_builders/workflows/nodejs_npm/workflow.py +++ b/aws_lambda_builders/workflows/nodejs_npm/workflow.py @@ -67,8 +67,9 @@ def __init__(self, source_dir, artifacts_dir, scratch_dir, manifest_path, runtim if osutils is None: osutils = OSUtils() self.osutils = osutils + self._use_npm = osutils.file_exists(manifest_path) - if not osutils.file_exists(manifest_path): + if not self._use_npm: LOG.warning("package.json file not found. Continuing the build without dependencies.") self.actions = [CopySourceAction(source_dir, artifacts_dir, excludes=self.EXCLUDED_FILES)] return @@ -210,8 +211,15 @@ def get_resolvers(self): """ specialized path resolver that just returns the list of executable for the runtime on the path. """ + if not self._use_npm: + return [] return [PathResolver(runtime=self.runtime, binary="npm")] + def get_validators(self): + if not self._use_npm: + return [] + return super().get_validators() + @staticmethod def get_install_action( source_dir: str, diff --git a/aws_lambda_builders/workflows/ruby_bundler/workflow.py b/aws_lambda_builders/workflows/ruby_bundler/workflow.py index 77f3d7003..ae4c6de35 100644 --- a/aws_lambda_builders/workflows/ruby_bundler/workflow.py +++ b/aws_lambda_builders/workflows/ruby_bundler/workflow.py @@ -34,6 +34,8 @@ def __init__(self, source_dir, artifacts_dir, scratch_dir, manifest_path, runtim source_dir, artifacts_dir, scratch_dir, manifest_path, runtime=runtime, **kwargs ) + self._use_bundler = self.download_dependencies + if osutils is None: osutils = OSUtils() @@ -61,3 +63,13 @@ def __init__(self, source_dir, artifacts_dir, scratch_dir, manifest_path, runtim "download_dependencies is False and dependencies_dir is None. Copying the source files into the " "artifacts directory. " ) + + def get_resolvers(self): + if not self._use_bundler: + return [] + return super().get_resolvers() + + def get_validators(self): + if not self._use_bundler: + return [] + return super().get_validators() diff --git a/tests/integration/workflows/nodejs_npm/test_nodejs_npm.py b/tests/integration/workflows/nodejs_npm/test_nodejs_npm.py index cc613b60b..e5627f927 100644 --- a/tests/integration/workflows/nodejs_npm/test_nodejs_npm.py +++ b/tests/integration/workflows/nodejs_npm/test_nodejs_npm.py @@ -70,7 +70,12 @@ def test_builds_project_without_dependencies(self, runtime): def test_builds_project_without_manifest(self, runtime): source_dir = os.path.join(self.TEST_DATA_FOLDER, "no-manifest") - with mock.patch.object(logger, "warning") as mock_warning: + with ( + mock.patch.object(logger, "warning") as mock_warning, + mock.patch( + "aws_lambda_builders.path_resolver.which", side_effect=AssertionError("npm should not be resolved") + ), + ): self.builder.build( source_dir, self.artifacts_dir, diff --git a/tests/integration/workflows/ruby_bundler/test_ruby.py b/tests/integration/workflows/ruby_bundler/test_ruby.py index 9d84f0be5..8fffe7e73 100644 --- a/tests/integration/workflows/ruby_bundler/test_ruby.py +++ b/tests/integration/workflows/ruby_bundler/test_ruby.py @@ -135,7 +135,12 @@ def test_builds_project_with_downloaded_dependencies_and_dependencies_dir(self): def test_builds_project_without_downloaded_dependencies_without_dependencies_dir(self): source_dir = os.path.join(self.TEST_DATA_FOLDER, "with-deps") - with mock.patch.object(workflow_logger, "info") as mock_info: + with ( + mock.patch.object(workflow_logger, "info") as mock_info, + mock.patch( + "aws_lambda_builders.path_resolver.which", side_effect=AssertionError("Ruby should not be resolved") + ), + ): self.builder.build( source_dir, self.artifacts_dir, diff --git a/tests/unit/workflows/nodejs_npm/test_workflow.py b/tests/unit/workflows/nodejs_npm/test_workflow.py index 85133dc76..52adf6b78 100644 --- a/tests/unit/workflows/nodejs_npm/test_workflow.py +++ b/tests/unit/workflows/nodejs_npm/test_workflow.py @@ -12,6 +12,8 @@ MoveDependenciesAction, ) from aws_lambda_builders.architecture import ARM64 +from aws_lambda_builders.path_resolver import PathResolver +from aws_lambda_builders.validator import RuntimeValidator from aws_lambda_builders.workflows.nodejs_npm.workflow import NodejsNpmWorkflow from aws_lambda_builders.workflows.nodejs_npm.actions import ( NodejsNpmPackAction, @@ -206,6 +208,37 @@ def test_workflow_sets_up_npm_actions_without_download_dependencies_and_without_ self.assertIsInstance(workflow.actions[3], NodejsNpmrcCleanUpAction) self.assertIsInstance(workflow.actions[4], NodejsNpmLockFileCleanUpAction) + def test_workflow_without_manifest_skips_npm_resolution_and_validation(self): + self.osutils.file_exists.return_value = False + + workflow = NodejsNpmWorkflow( + "source", "artifacts", "scratch_dir", "source/manifest", runtime="nodejs20.x", osutils=self.osutils + ) + + self.assertEqual(workflow.get_resolvers(), []) + self.assertEqual(workflow.get_validators(), []) + + def test_workflow_with_manifest_retains_npm_resolution_and_validation(self): + self.osutils.file_exists.return_value = True + + workflow = NodejsNpmWorkflow( + "source", + "artifacts", + "scratch_dir", + "source/manifest", + runtime="nodejs20.x", + download_dependencies=False, + osutils=self.osutils, + ) + + resolvers = workflow.get_resolvers() + validators = workflow.get_validators() + self.assertEqual(len(resolvers), 1) + self.assertIsInstance(resolvers[0], PathResolver) + self.assertEqual(resolvers[0].binary, "npm") + self.assertEqual(len(validators), 1) + self.assertIsInstance(validators[0], RuntimeValidator) + def test_workflow_sets_up_npm_actions_without_combine_dependencies(self): self.osutils.file_exists.side_effect = [True, False, False] diff --git a/tests/unit/workflows/ruby_bundler/test_workflow.py b/tests/unit/workflows/ruby_bundler/test_workflow.py index 4d7044c3e..2e36ac307 100644 --- a/tests/unit/workflows/ruby_bundler/test_workflow.py +++ b/tests/unit/workflows/ruby_bundler/test_workflow.py @@ -2,6 +2,8 @@ from aws_lambda_builders.actions import CopySourceAction, CopyDependenciesAction, CleanUpAction from aws_lambda_builders.architecture import ARM64 +from aws_lambda_builders.path_resolver import PathResolver +from aws_lambda_builders.validator import RuntimeValidator from aws_lambda_builders.workflows.ruby_bundler.workflow import RubyBundlerWorkflow from aws_lambda_builders.workflows.ruby_bundler.actions import RubyBundlerInstallAction, RubyBundlerVendorAction @@ -44,6 +46,37 @@ def test_workflow_sets_up_bundler_actions_without_download_dependencies_without_ self.assertIsInstance(workflow.actions[0], CopySourceAction) + def test_workflow_without_download_dependencies_skips_ruby_resolution_and_validation(self): + workflow = RubyBundlerWorkflow( + "source", + "artifacts", + "scratch_dir", + "manifest", + runtime="ruby3.3", + download_dependencies=False, + ) + + self.assertEqual(workflow.get_resolvers(), []) + self.assertEqual(workflow.get_validators(), []) + + def test_workflow_with_download_dependencies_retains_ruby_resolution_and_validation(self): + workflow = RubyBundlerWorkflow( + "source", + "artifacts", + "scratch_dir", + "manifest", + runtime="ruby3.3", + download_dependencies=True, + ) + + resolvers = workflow.get_resolvers() + validators = workflow.get_validators() + self.assertEqual(len(resolvers), 1) + self.assertIsInstance(resolvers[0], PathResolver) + self.assertEqual(resolvers[0].binary, "ruby") + self.assertEqual(len(validators), 1) + self.assertIsInstance(validators[0], RuntimeValidator) + def test_must_validate_architecture(self): workflow = RubyBundlerWorkflow( "source", From 22f1994c814cc7b43d2a111fccf436b133a0120c Mon Sep 17 00:00:00 2001 From: Saketh Bandi <79930864+saketh-bandi@users.noreply.github.com> Date: Thu, 3 Sep 2026 14:04:40 -0700 Subject: [PATCH 2/4] fix: preserve runtime validation for copy-only workflows --- .../workflows/nodejs_npm/workflow.py | 13 ++++++++ .../workflows/ruby_bundler/workflow.py | 13 ++++++++ .../workflows/nodejs_npm/test_workflow.py | 32 ++++++++++++++++++ .../workflows/ruby_bundler/test_workflow.py | 33 +++++++++++++++++++ 4 files changed, 91 insertions(+) diff --git a/aws_lambda_builders/workflows/nodejs_npm/workflow.py b/aws_lambda_builders/workflows/nodejs_npm/workflow.py index 32bff1bce..fb1719618 100644 --- a/aws_lambda_builders/workflows/nodejs_npm/workflow.py +++ b/aws_lambda_builders/workflows/nodejs_npm/workflow.py @@ -13,7 +13,9 @@ LinkSinglePathAction, MoveDependenciesAction, ) +from aws_lambda_builders.exceptions import RuntimeValidatorError, WorkflowFailedError from aws_lambda_builders.path_resolver import PathResolver +from aws_lambda_builders.validator import RuntimeValidator from aws_lambda_builders.workflow import BaseWorkflow, BuildDirectory, BuildInSourceSupport, Capability from aws_lambda_builders.workflows.nodejs_npm.actions import ( NodejsNpmCIAction, @@ -220,6 +222,17 @@ def get_validators(self): return [] return super().get_validators() + def run(self): + if not self._use_npm: + self._validate_runtime() + return super().run() + + def _validate_runtime(self): + try: + RuntimeValidator(runtime=self.runtime, architecture=self.architecture).validate(None) + except RuntimeValidatorError as ex: + raise WorkflowFailedError(workflow_name=self.NAME, action_name="Validation", reason=str(ex)) from ex + @staticmethod def get_install_action( source_dir: str, diff --git a/aws_lambda_builders/workflows/ruby_bundler/workflow.py b/aws_lambda_builders/workflows/ruby_bundler/workflow.py index ae4c6de35..1c63bbb36 100644 --- a/aws_lambda_builders/workflows/ruby_bundler/workflow.py +++ b/aws_lambda_builders/workflows/ruby_bundler/workflow.py @@ -5,6 +5,8 @@ import logging from aws_lambda_builders.actions import CleanUpAction, CopyDependenciesAction, CopySourceAction +from aws_lambda_builders.exceptions import RuntimeValidatorError, WorkflowFailedError +from aws_lambda_builders.validator import RuntimeValidator from aws_lambda_builders.workflow import BaseWorkflow, BuildDirectory, BuildInSourceSupport, Capability from .actions import RubyBundlerInstallAction, RubyBundlerVendorAction @@ -73,3 +75,14 @@ def get_validators(self): if not self._use_bundler: return [] return super().get_validators() + + def run(self): + if not self._use_bundler: + self._validate_runtime() + return super().run() + + def _validate_runtime(self): + try: + RuntimeValidator(runtime=self.runtime, architecture=self.architecture).validate(None) + except RuntimeValidatorError as ex: + raise WorkflowFailedError(workflow_name=self.NAME, action_name="Validation", reason=str(ex)) from ex diff --git a/tests/unit/workflows/nodejs_npm/test_workflow.py b/tests/unit/workflows/nodejs_npm/test_workflow.py index 52adf6b78..13e0e35dc 100644 --- a/tests/unit/workflows/nodejs_npm/test_workflow.py +++ b/tests/unit/workflows/nodejs_npm/test_workflow.py @@ -12,6 +12,7 @@ MoveDependenciesAction, ) from aws_lambda_builders.architecture import ARM64 +from aws_lambda_builders.exceptions import WorkflowFailedError from aws_lambda_builders.path_resolver import PathResolver from aws_lambda_builders.validator import RuntimeValidator from aws_lambda_builders.workflows.nodejs_npm.workflow import NodejsNpmWorkflow @@ -239,6 +240,37 @@ def test_workflow_with_manifest_retains_npm_resolution_and_validation(self): self.assertEqual(len(validators), 1) self.assertIsInstance(validators[0], RuntimeValidator) + def test_workflow_without_manifest_rejects_unsupported_runtime(self): + self.osutils.file_exists.return_value = False + + workflow = NodejsNpmWorkflow( + "source", "artifacts", "scratch_dir", "source/manifest", runtime="nodejs1.x", osutils=self.osutils + ) + with self.assertRaises(WorkflowFailedError) as raised: + workflow.run() + + self.assertEqual(str(raised.exception), "NodejsNpmBuilder:Validation - Runtime nodejs1.x is not supported") + + def test_workflow_without_manifest_rejects_unsupported_architecture(self): + self.osutils.file_exists.return_value = False + + workflow = NodejsNpmWorkflow( + "source", + "artifacts", + "scratch_dir", + "source/manifest", + runtime="nodejs20.x", + architecture="invalid_arch", + osutils=self.osutils, + ) + with self.assertRaises(WorkflowFailedError) as raised: + workflow.run() + + self.assertEqual( + str(raised.exception), + "NodejsNpmBuilder:Validation - Architecture invalid_arch is not supported for runtime nodejs20.x", + ) + def test_workflow_sets_up_npm_actions_without_combine_dependencies(self): self.osutils.file_exists.side_effect = [True, False, False] diff --git a/tests/unit/workflows/ruby_bundler/test_workflow.py b/tests/unit/workflows/ruby_bundler/test_workflow.py index 2e36ac307..978995390 100644 --- a/tests/unit/workflows/ruby_bundler/test_workflow.py +++ b/tests/unit/workflows/ruby_bundler/test_workflow.py @@ -2,6 +2,7 @@ from aws_lambda_builders.actions import CopySourceAction, CopyDependenciesAction, CleanUpAction from aws_lambda_builders.architecture import ARM64 +from aws_lambda_builders.exceptions import WorkflowFailedError from aws_lambda_builders.path_resolver import PathResolver from aws_lambda_builders.validator import RuntimeValidator from aws_lambda_builders.workflows.ruby_bundler.workflow import RubyBundlerWorkflow @@ -77,6 +78,38 @@ def test_workflow_with_download_dependencies_retains_ruby_resolution_and_validat self.assertEqual(len(validators), 1) self.assertIsInstance(validators[0], RuntimeValidator) + def test_workflow_without_download_dependencies_rejects_unsupported_runtime(self): + workflow = RubyBundlerWorkflow( + "source", + "artifacts", + "scratch_dir", + "manifest", + runtime="ruby1.0", + download_dependencies=False, + ) + with self.assertRaises(WorkflowFailedError) as raised: + workflow.run() + + self.assertEqual(str(raised.exception), "RubyBundlerBuilder:Validation - Runtime ruby1.0 is not supported") + + def test_workflow_without_download_dependencies_rejects_unsupported_architecture(self): + workflow = RubyBundlerWorkflow( + "source", + "artifacts", + "scratch_dir", + "manifest", + runtime="ruby3.3", + architecture="invalid_arch", + download_dependencies=False, + ) + with self.assertRaises(WorkflowFailedError) as raised: + workflow.run() + + self.assertEqual( + str(raised.exception), + "RubyBundlerBuilder:Validation - Architecture invalid_arch is not supported for runtime ruby3.3", + ) + def test_must_validate_architecture(self): workflow = RubyBundlerWorkflow( "source", From affd0ec1c6aa83ec3e037e42ac596a57010ce92f Mon Sep 17 00:00:00 2001 From: Saketh Bandi <79930864+saketh-bandi@users.noreply.github.com> Date: Thu, 3 Sep 2026 14:24:59 -0700 Subject: [PATCH 3/4] refactor: preserve runtime validation without binaries --- aws_lambda_builders/workflow.py | 14 +++++-- .../workflows/nodejs_npm/workflow.py | 13 ------ .../workflows/ruby_bundler/workflow.py | 13 ------ tests/unit/test_workflow.py | 40 +++++++++++++++++++ .../unit/workflows/python_uv/test_workflow.py | 29 ++++++++++++++ 5 files changed, 80 insertions(+), 29 deletions(-) diff --git a/aws_lambda_builders/workflow.py b/aws_lambda_builders/workflow.py index 9e2ce835f..9a7e0b64c 100644 --- a/aws_lambda_builders/workflow.py +++ b/aws_lambda_builders/workflow.py @@ -70,8 +70,16 @@ def wrapper(self, *args, **kwargs): # pylint: disable=too-many-statements valid_paths = {} invalid_paths = {} validation_errors = [] + binaries = self.binaries + + if not binaries: + try: + RuntimeValidator(runtime=self.runtime, architecture=self.architecture).validate(None) + except RuntimeValidatorError as ex: + validation_errors.append(str(ex)) + # NOTE: we need to access binaries to get paths and resolvers, before validating. - for binary, binary_checker in self.binaries.items(): + for binary, binary_checker in binaries.items(): invalid_paths[binary] = [] try: exec_paths = ( @@ -103,8 +111,8 @@ def wrapper(self, *args, **kwargs): # pylint: disable=too-many-statements workflow_name=self.NAME, action_name="Validation", reason="\n".join(validation_errors) ) - if len(self.binaries) != len(valid_paths): - validation_failed_binaries = set(self.binaries.keys()).difference(valid_paths.keys()) + if len(binaries) != len(valid_paths): + validation_failed_binaries = set(binaries.keys()).difference(valid_paths.keys()) for validation_failed_binary in validation_failed_binaries: message = "Binary validation failed for {0}, searched for {0} in following locations : {1} which did not satisfy constraints for runtime: {2}. Do you have {0} for runtime: {2} on your PATH?".format( validation_failed_binary, invalid_paths[validation_failed_binary], self.runtime diff --git a/aws_lambda_builders/workflows/nodejs_npm/workflow.py b/aws_lambda_builders/workflows/nodejs_npm/workflow.py index fb1719618..32bff1bce 100644 --- a/aws_lambda_builders/workflows/nodejs_npm/workflow.py +++ b/aws_lambda_builders/workflows/nodejs_npm/workflow.py @@ -13,9 +13,7 @@ LinkSinglePathAction, MoveDependenciesAction, ) -from aws_lambda_builders.exceptions import RuntimeValidatorError, WorkflowFailedError from aws_lambda_builders.path_resolver import PathResolver -from aws_lambda_builders.validator import RuntimeValidator from aws_lambda_builders.workflow import BaseWorkflow, BuildDirectory, BuildInSourceSupport, Capability from aws_lambda_builders.workflows.nodejs_npm.actions import ( NodejsNpmCIAction, @@ -222,17 +220,6 @@ def get_validators(self): return [] return super().get_validators() - def run(self): - if not self._use_npm: - self._validate_runtime() - return super().run() - - def _validate_runtime(self): - try: - RuntimeValidator(runtime=self.runtime, architecture=self.architecture).validate(None) - except RuntimeValidatorError as ex: - raise WorkflowFailedError(workflow_name=self.NAME, action_name="Validation", reason=str(ex)) from ex - @staticmethod def get_install_action( source_dir: str, diff --git a/aws_lambda_builders/workflows/ruby_bundler/workflow.py b/aws_lambda_builders/workflows/ruby_bundler/workflow.py index 1c63bbb36..ae4c6de35 100644 --- a/aws_lambda_builders/workflows/ruby_bundler/workflow.py +++ b/aws_lambda_builders/workflows/ruby_bundler/workflow.py @@ -5,8 +5,6 @@ import logging from aws_lambda_builders.actions import CleanUpAction, CopyDependenciesAction, CopySourceAction -from aws_lambda_builders.exceptions import RuntimeValidatorError, WorkflowFailedError -from aws_lambda_builders.validator import RuntimeValidator from aws_lambda_builders.workflow import BaseWorkflow, BuildDirectory, BuildInSourceSupport, Capability from .actions import RubyBundlerInstallAction, RubyBundlerVendorAction @@ -75,14 +73,3 @@ def get_validators(self): if not self._use_bundler: return [] return super().get_validators() - - def run(self): - if not self._use_bundler: - self._validate_runtime() - return super().run() - - def _validate_runtime(self): - try: - RuntimeValidator(runtime=self.runtime, architecture=self.architecture).validate(None) - except RuntimeValidatorError as ex: - raise WorkflowFailedError(workflow_name=self.NAME, action_name="Validation", reason=str(ex)) from ex diff --git a/tests/unit/test_workflow.py b/tests/unit/test_workflow.py index 7484686b2..03f16faf8 100644 --- a/tests/unit/test_workflow.py +++ b/tests/unit/test_workflow.py @@ -396,6 +396,46 @@ def test_must_raise_for_incompatible_runtime_and_architecture(self): self.assertIn("Architecture invalid_arch is not supported for runtime python3.12", str(ex.exception)) + def test_empty_binaries_with_supported_runtime_executes_actions(self): + self.work.runtime = "python3.12" + self.work.architecture = "arm64" + self.work.get_resolvers = Mock(return_value=[]) + self.work.get_validators = Mock(return_value=[]) + action_mock = Mock() + self.work.actions = [action_mock] + + self.work.run() + + self.work.get_resolvers.assert_called_once_with() + self.work.get_validators.assert_called_once_with() + action_mock.execute.assert_called_once_with() + + def test_empty_binaries_with_unsupported_runtime_raises_workflow_failed_error(self): + self.work.runtime = "python1.0" + self.work.get_resolvers = Mock(return_value=[]) + self.work.get_validators = Mock(return_value=[]) + self.work.actions = [Mock()] + + with self.assertRaises(WorkflowFailedError) as raised: + self.work.run() + + self.assertEqual(str(raised.exception), "MyWorkflow:Validation - Runtime python1.0 is not supported") + + def test_empty_binaries_with_unsupported_architecture_raises_workflow_failed_error(self): + self.work.runtime = "python3.12" + self.work.architecture = "invalid_arch" + self.work.get_resolvers = Mock(return_value=[]) + self.work.get_validators = Mock(return_value=[]) + self.work.actions = [Mock()] + + with self.assertRaises(WorkflowFailedError) as raised: + self.work.run() + + self.assertEqual( + str(raised.exception), + "MyWorkflow:Validation - Architecture invalid_arch is not supported for runtime python3.12", + ) + class TestBaseWorkflow_repr(TestCase): class MyWorkflow(BaseWorkflow): diff --git a/tests/unit/workflows/python_uv/test_workflow.py b/tests/unit/workflows/python_uv/test_workflow.py index fbec50153..cf378f45b 100644 --- a/tests/unit/workflows/python_uv/test_workflow.py +++ b/tests/unit/workflows/python_uv/test_workflow.py @@ -4,6 +4,7 @@ from parameterized import parameterized_class from aws_lambda_builders.actions import CleanUpAction, CopyDependenciesAction, CopySourceAction +from aws_lambda_builders.exceptions import WorkflowFailedError from aws_lambda_builders.workflows.python_uv.actions import PythonUvBuildAction from aws_lambda_builders.workflows.python_uv.utils import EXPERIMENTAL_FLAG_BUILD_PERFORMANCE, OSUtils from aws_lambda_builders.workflows.python_uv.workflow import PythonUvWorkflow @@ -170,6 +171,34 @@ def test_get_validators(self): # UV has built-in Python version handling, no external validators needed self.assertEqual(len(validators), 0) + def test_supported_runtime_runs_without_binary_resolution(self): + action_mock = Mock() + self.workflow.actions = [action_mock] + + with patch("aws_lambda_builders.path_resolver.which", side_effect=AssertionError("binary resolution called")): + self.workflow.run() + + action_mock.execute.assert_called_once_with() + + def test_unsupported_runtime_is_rejected(self): + self.workflow.runtime = "python1.0" + + with self.assertRaises(WorkflowFailedError) as raised: + self.workflow.run() + + self.assertEqual(str(raised.exception), "PythonUvBuilder:Validation - Runtime python1.0 is not supported") + + def test_unsupported_architecture_is_rejected(self): + self.workflow.architecture = "invalid_arch" + + with self.assertRaises(WorkflowFailedError) as raised: + self.workflow.run() + + self.assertEqual( + str(raised.exception), + "PythonUvBuilder:Validation - Architecture invalid_arch is not supported for runtime python3.9", + ) + @patch("aws_lambda_builders.workflows.python_uv.workflow.detect_uv_manifest") def test_workflow_auto_detects_manifest(self, mock_detect): mock_detect.return_value = "/path/to/pyproject.toml" From b1a4a6202e6b57714e0ee692812ae4cbf70bd8fe Mon Sep 17 00:00:00 2001 From: Saketh Bandi <79930864+saketh-bandi@users.noreply.github.com> Date: Thu, 3 Sep 2026 14:51:55 -0700 Subject: [PATCH 4/4] refactor: support runtime validation without binaries --- aws_lambda_builders/workflow.py | 12 ++++++- .../workflows/python_uv/workflow.py | 4 +++ tests/unit/test_workflow.py | 13 ++++++++ .../unit/workflows/python_uv/test_workflow.py | 32 +++++++++---------- 4 files changed, 43 insertions(+), 18 deletions(-) diff --git a/aws_lambda_builders/workflow.py b/aws_lambda_builders/workflow.py index 9a7e0b64c..80b6e8a2e 100644 --- a/aws_lambda_builders/workflow.py +++ b/aws_lambda_builders/workflow.py @@ -58,6 +58,12 @@ class BuildInSourceSupport(Enum): EXCLUSIVELY_SUPPORTED = [True] +def _validate_runtime_without_binary(workflow): + runtime_validator = workflow.get_runtime_validator() + if runtime_validator: + runtime_validator.validate(None) + + # TODO: Move sanitize out to its own class. def sanitize(func): # pylint: disable=too-many-statements """ @@ -74,7 +80,7 @@ def wrapper(self, *args, **kwargs): # pylint: disable=too-many-statements if not binaries: try: - RuntimeValidator(runtime=self.runtime, architecture=self.architecture).validate(None) + _validate_runtime_without_binary(self) except RuntimeValidatorError as ex: validation_errors.append(str(ex)) @@ -338,6 +344,10 @@ def get_validators(self): """ return [RuntimeValidator(runtime=self.runtime, architecture=self.architecture)] + def get_runtime_validator(self): + """Return the validator used when the workflow does not require any binaries.""" + return RuntimeValidator(runtime=self.runtime, architecture=self.architecture) + @property def binaries(self): if not self._binaries: diff --git a/aws_lambda_builders/workflows/python_uv/workflow.py b/aws_lambda_builders/workflows/python_uv/workflow.py index 374c6beed..94f595d94 100644 --- a/aws_lambda_builders/workflows/python_uv/workflow.py +++ b/aws_lambda_builders/workflows/python_uv/workflow.py @@ -173,3 +173,7 @@ def get_validators(self): external validation of Python runtime paths. """ return [] + + def get_runtime_validator(self): + """UV manages the requested Python runtime without BaseWorkflow validation.""" + return None diff --git a/tests/unit/test_workflow.py b/tests/unit/test_workflow.py index 03f16faf8..ef2365c12 100644 --- a/tests/unit/test_workflow.py +++ b/tests/unit/test_workflow.py @@ -436,6 +436,19 @@ def test_empty_binaries_with_unsupported_architecture_raises_workflow_failed_err "MyWorkflow:Validation - Architecture invalid_arch is not supported for runtime python3.12", ) + def test_empty_binaries_can_skip_runtime_validation(self): + self.work.runtime = "unsupported" + self.work.get_resolvers = Mock(return_value=[]) + self.work.get_validators = Mock(return_value=[]) + self.work.get_runtime_validator = Mock(return_value=None) + action_mock = Mock() + self.work.actions = [action_mock] + + self.work.run() + + self.work.get_runtime_validator.assert_called_once_with() + action_mock.execute.assert_called_once_with() + class TestBaseWorkflow_repr(TestCase): class MyWorkflow(BaseWorkflow): diff --git a/tests/unit/workflows/python_uv/test_workflow.py b/tests/unit/workflows/python_uv/test_workflow.py index cf378f45b..6830c13d5 100644 --- a/tests/unit/workflows/python_uv/test_workflow.py +++ b/tests/unit/workflows/python_uv/test_workflow.py @@ -4,7 +4,6 @@ from parameterized import parameterized_class from aws_lambda_builders.actions import CleanUpAction, CopyDependenciesAction, CopySourceAction -from aws_lambda_builders.exceptions import WorkflowFailedError from aws_lambda_builders.workflows.python_uv.actions import PythonUvBuildAction from aws_lambda_builders.workflows.python_uv.utils import EXPERIMENTAL_FLAG_BUILD_PERFORMANCE, OSUtils from aws_lambda_builders.workflows.python_uv.workflow import PythonUvWorkflow @@ -171,6 +170,9 @@ def test_get_validators(self): # UV has built-in Python version handling, no external validators needed self.assertEqual(len(validators), 0) + def test_get_runtime_validator(self): + self.assertIsNone(self.workflow.get_runtime_validator()) + def test_supported_runtime_runs_without_binary_resolution(self): action_mock = Mock() self.workflow.actions = [action_mock] @@ -180,24 +182,20 @@ def test_supported_runtime_runs_without_binary_resolution(self): action_mock.execute.assert_called_once_with() - def test_unsupported_runtime_is_rejected(self): - self.workflow.runtime = "python1.0" - - with self.assertRaises(WorkflowFailedError) as raised: - self.workflow.run() - - self.assertEqual(str(raised.exception), "PythonUvBuilder:Validation - Runtime python1.0 is not supported") + def test_runtime_validation_opt_out_preserves_previous_behavior(self): + for runtime, architecture in (("python1.0", "x86_64"), ("python3.9", "invalid_arch"), (None, "x86_64")): + with self.subTest(runtime=runtime, architecture=architecture): + action_mock = Mock() + self.workflow.runtime = runtime + self.workflow.architecture = architecture + self.workflow.actions = [action_mock] - def test_unsupported_architecture_is_rejected(self): - self.workflow.architecture = "invalid_arch" + with patch( + "aws_lambda_builders.path_resolver.which", side_effect=AssertionError("binary resolution called") + ): + self.workflow.run() - with self.assertRaises(WorkflowFailedError) as raised: - self.workflow.run() - - self.assertEqual( - str(raised.exception), - "PythonUvBuilder:Validation - Architecture invalid_arch is not supported for runtime python3.9", - ) + action_mock.execute.assert_called_once_with() @patch("aws_lambda_builders.workflows.python_uv.workflow.detect_uv_manifest") def test_workflow_auto_detects_manifest(self, mock_detect):