Skip to content

Commit e8bcff3

Browse files
committed
feat(package): merge SAR Metadata exports back into LE child templates
Extend merge_language_extensions_s3_uris with a registry-driven Metadata pass that copies rewritten property values (LicenseUrl, ReadmeUrl) from the exported template back into the original (Fn::ForEach-preserving) template after LE expansion. Without this pass, when a child stack uses Transform: AWS::LanguageExtensions and declares AWS::ServerlessRepo::Application metadata, sam package silently dropped the License/Readme S3 URLs from the merged output — they were uploaded but never wired into the template the user deploys. Implementation iterates METADATA_EXPORTS (the registry added in the prior commit) so future metadata types pick up merge support without touching the merge walker.
1 parent 1783eda commit e8bcff3

2 files changed

Lines changed: 125 additions & 0 deletions

File tree

samcli/lib/package/language_extensions_packaging.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
substitute_loop_variable,
2828
)
2929
from samcli.lib.cfn_language_extensions.utils import FOREACH_REQUIRED_ELEMENTS, is_foreach_key
30+
from samcli.lib.package.packageable_resources import METADATA_EXPORTS
3031

3132
LOG = logging.getLogger(__name__)
3233

@@ -82,6 +83,8 @@ def merge_language_extensions_s3_uris(
8283

8384
_update_resources_with_s3_uris(original_resources, exported_resources, dynamic_prop_keys)
8485

86+
_merge_metadata(result.get("Metadata", {}), exported_template.get("Metadata", {}))
87+
8588
return result
8689

8790

@@ -183,6 +186,24 @@ def _resolve_property_paths(prop_names: List[str], properties: Dict[str, Any]) -
183186
# ---------------------------------------------------------------------------
184187

185188

189+
def _merge_metadata(
190+
original_metadata: Dict[str, Any],
191+
exported_metadata: Dict[str, Any],
192+
) -> None:
193+
"""Copy rewritten property values from exported_metadata into
194+
original_metadata for every (metadata_type, property_name) declared
195+
in METADATA_EXPORTS. Mutates original_metadata in place.
196+
"""
197+
for spec in METADATA_EXPORTS:
198+
original_entry = original_metadata.get(spec.metadata_type)
199+
exported_entry = exported_metadata.get(spec.metadata_type)
200+
if not isinstance(original_entry, dict) or not isinstance(exported_entry, dict):
201+
continue
202+
for prop_name in spec.property_names:
203+
if prop_name in exported_entry:
204+
original_entry[prop_name] = exported_entry[prop_name]
205+
206+
186207
def _update_resources_with_s3_uris(
187208
original_resources: Dict[str, Any],
188209
exported_resources: Dict[str, Any],
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
"""Unit tests for samcli.lib.package.language_extensions_packaging.
2+
3+
Resource-property merge tests live in test_artifact_exporter.py; this file
4+
focuses on the Metadata merge pass added for the registry-driven merge.
5+
"""
6+
7+
from unittest import TestCase
8+
9+
from samcli.lib.package.language_extensions_packaging import merge_language_extensions_s3_uris
10+
11+
12+
class TestMergeMetadata(TestCase):
13+
def test_serverless_repo_license_url_is_merged(self):
14+
original = {
15+
"Transform": "AWS::LanguageExtensions",
16+
"Metadata": {
17+
"AWS::ServerlessRepo::Application": {
18+
"Name": "MyApp",
19+
"LicenseUrl": "./LICENSE.txt",
20+
"ReadmeUrl": "./README.md",
21+
}
22+
},
23+
"Resources": {},
24+
}
25+
exported = {
26+
"Metadata": {
27+
"AWS::ServerlessRepo::Application": {
28+
"Name": "MyApp",
29+
"LicenseUrl": "s3://bucket/license-md5",
30+
"ReadmeUrl": "s3://bucket/readme-md5",
31+
}
32+
},
33+
"Resources": {},
34+
}
35+
36+
result = merge_language_extensions_s3_uris(original, exported)
37+
38+
sar = result["Metadata"]["AWS::ServerlessRepo::Application"]
39+
self.assertEqual(sar["LicenseUrl"], "s3://bucket/license-md5")
40+
self.assertEqual(sar["ReadmeUrl"], "s3://bucket/readme-md5")
41+
self.assertEqual(sar["Name"], "MyApp") # unrelated keys preserved
42+
43+
def test_metadata_without_serverless_repo_is_unchanged(self):
44+
original = {
45+
"Metadata": {"OtherKey": {"Foo": "./bar"}},
46+
"Resources": {},
47+
}
48+
exported = {
49+
"Metadata": {"OtherKey": {"Foo": "./bar"}},
50+
"Resources": {},
51+
}
52+
53+
result = merge_language_extensions_s3_uris(original, exported)
54+
55+
self.assertEqual(result["Metadata"], {"OtherKey": {"Foo": "./bar"}})
56+
57+
def test_missing_metadata_section_in_either_template_is_safe(self):
58+
# No Metadata in original
59+
result = merge_language_extensions_s3_uris(
60+
{"Resources": {}},
61+
{"Metadata": {"AWS::ServerlessRepo::Application": {"LicenseUrl": "s3://x"}}, "Resources": {}},
62+
)
63+
self.assertNotIn("Metadata", result)
64+
65+
# No Metadata in exported
66+
original = {
67+
"Metadata": {"AWS::ServerlessRepo::Application": {"LicenseUrl": "./LICENSE"}},
68+
"Resources": {},
69+
}
70+
result = merge_language_extensions_s3_uris(original, {"Resources": {}})
71+
# Original retained, since exporter never wrote anything
72+
self.assertEqual(result["Metadata"]["AWS::ServerlessRepo::Application"]["LicenseUrl"], "./LICENSE")
73+
74+
def test_partial_serverless_repo_export_preserves_unwritten_properties(self):
75+
"""If the exported template has only LicenseUrl written (no ReadmeUrl),
76+
the original's ReadmeUrl must be left untouched. Guards the
77+
`if prop_name in exported_entry:` check in _merge_metadata.
78+
"""
79+
original = {
80+
"Metadata": {
81+
"AWS::ServerlessRepo::Application": {
82+
"Name": "MyApp",
83+
"LicenseUrl": "./LICENSE.txt",
84+
"ReadmeUrl": "./README.md",
85+
}
86+
},
87+
"Resources": {},
88+
}
89+
exported = {
90+
"Metadata": {
91+
"AWS::ServerlessRepo::Application": {
92+
"Name": "MyApp",
93+
"LicenseUrl": "s3://bucket/license-md5",
94+
# ReadmeUrl deliberately absent
95+
}
96+
},
97+
"Resources": {},
98+
}
99+
100+
result = merge_language_extensions_s3_uris(original, exported)
101+
102+
sar = result["Metadata"]["AWS::ServerlessRepo::Application"]
103+
self.assertEqual(sar["LicenseUrl"], "s3://bucket/license-md5")
104+
self.assertEqual(sar["ReadmeUrl"], "./README.md") # untouched

0 commit comments

Comments
 (0)