1313import hashlib
1414import json
1515import os
16- import platform
1716import shutil
1817import subprocess
1918import sys
2322from urllib .parse import quote
2423
2524import boto3
25+ import yaml
2626from botocore .exceptions import ClientError
2727from rich .console import Console
2828from rich .progress import (
@@ -53,10 +53,50 @@ def __init__(self, verbose=False):
5353 self .public = False
5454 self .main_template = "idp-main.yaml"
5555 self .use_container_flag = ""
56- self . stat_cmd = None
56+
5757 self .s3_client = None
5858 self .cf_client = None
5959 self ._is_lib_changed = False
60+ self .skip_validation = False
61+
62+ def clean_checksums (self ):
63+ """Delete all .checksum files in main, patterns, options, and lib directories"""
64+ self .console .print ("[yellow]🧹 Cleaning all .checksum files...[/yellow]" )
65+
66+ checksum_paths = [
67+ ".checksum" , # main
68+ "lib/.checksum" , # lib
69+ ]
70+
71+ # Add patterns checksum files
72+ patterns_dir = "patterns"
73+ if os .path .exists (patterns_dir ):
74+ for item in os .listdir (patterns_dir ):
75+ pattern_path = os .path .join (patterns_dir , item )
76+ if os .path .isdir (pattern_path ):
77+ checksum_paths .append (f"{ pattern_path } /.checksum" )
78+
79+ # Add options checksum files
80+ options_dir = "options"
81+ if os .path .exists (options_dir ):
82+ for item in os .listdir (options_dir ):
83+ option_path = os .path .join (options_dir , item )
84+ if os .path .isdir (option_path ):
85+ checksum_paths .append (f"{ option_path } /.checksum" )
86+
87+ deleted_count = 0
88+ for checksum_path in checksum_paths :
89+ if os .path .exists (checksum_path ):
90+ os .remove (checksum_path )
91+ self .console .print (f"[green] ✓ Deleted { checksum_path } [/green]" )
92+ deleted_count += 1
93+
94+ if deleted_count == 0 :
95+ self .console .print ("[dim] No .checksum files found to delete[/dim]" )
96+ else :
97+ self .console .print (
98+ f"[green]✅ Deleted { deleted_count } .checksum files - full rebuild will be triggered[/green]"
99+ )
60100
61101 def log_verbose (self , message , style = "dim" ):
62102 """Log verbose messages if verbose mode is enabled"""
@@ -119,7 +159,7 @@ def print_usage(self):
119159 """Print usage information with Rich formatting"""
120160 self .console .print ("\n [bold cyan]Usage:[/bold cyan]" )
121161 self .console .print (
122- " python3 publish.py <cfn_bucket_basename> <cfn_prefix> <region> [public] [--max-workers N] [--verbose]"
162+ " python3 publish.py <cfn_bucket_basename> <cfn_prefix> <region> [public] [--max-workers N] [--verbose] [--no-validate] [--clean-build] "
123163 )
124164
125165 self .console .print ("\n [bold cyan]Parameters:[/bold cyan]" )
@@ -140,6 +180,12 @@ def print_usage(self):
140180 self .console .print (
141181 " [yellow][--verbose, -v][/yellow]: Optional. Enable verbose output for debugging"
142182 )
183+ self .console .print (
184+ " [yellow][--no-validate][/yellow]: Optional. Skip CloudFormation template validation"
185+ )
186+ self .console .print (
187+ " [yellow][--clean-build][/yellow]: Optional. Delete all .checksum files to force full rebuild"
188+ )
143189
144190 def check_parameters (self , args ):
145191 """Check and validate input parameters"""
@@ -197,6 +243,13 @@ def check_parameters(self, args):
197243 elif arg in ["--verbose" , "-v" ]:
198244 # Verbose flag is already handled by Typer, just acknowledge it here
199245 pass
246+ elif arg == "--no-validate" :
247+ self .skip_validation = True
248+ self .console .print (
249+ "[yellow]CloudFormation template validation will be skipped[/yellow]"
250+ )
251+ elif arg == "--clean-build" :
252+ self .clean_checksums ()
200253 else :
201254 self .console .print (
202255 f"[yellow]Warning: Unknown argument '{ arg } ' ignored[/yellow]"
@@ -228,12 +281,6 @@ def setup_environment(self):
228281 self .prefix_and_version = f"{ self .prefix } /{ self .version } "
229282 self .bucket = f"{ self .bucket_basename } -{ self .region } "
230283
231- # Set platform-specific commands
232- if platform .machine () == "x86_64" :
233- self .stat_cmd = "stat --format='%Y'"
234- else :
235- self .stat_cmd = "stat -f %m"
236-
237284 # Set UDOP model path based on region
238285 if self .region == "us-east-1" :
239286 self .public_sample_udop_model = "s3://aws-ml-blog-us-east-1/artifacts/genai-idp/udop-finetuning/rvl-cdip/model.tar.gz"
@@ -528,7 +575,8 @@ def build_and_package_template(self, directory, force_rebuild=False):
528575 self ._delete_checksum_file (directory )
529576 self .log_verbose (f"Exception in build_and_package_template: { e } " )
530577 self .log_verbose (f"Traceback: { traceback .format_exc ()} " )
531- return False
578+ self .console .print (f"[red]❌ Build failed for { directory } : { e } [/red]" )
579+ sys .exit (1 )
532580
533581 return True
534582
@@ -878,18 +926,25 @@ def _check_requirements_has_idp_common_pkg(self, func_dir):
878926
879927 return False , "No idp_common_pkg found in requirements.txt"
880928 except Exception as e :
881- return False , f"Error reading requirements.txt: { e } "
929+ self .console .print (
930+ f"[red]❌ Error reading requirements.txt in { func_dir } : { e } [/red]"
931+ )
932+ sys .exit (1 )
882933
883934 def _extract_function_name (self , dir_name , template_path ):
884935 """Extract CloudFormation function name from template by matching CodeUri."""
885936 try :
886- import yaml
887-
888937 # Create a custom loader that ignores CloudFormation intrinsic functions
889938 class CFLoader (yaml .SafeLoader ):
890939 pass
891940
892941 def construct_unknown (loader , node ):
942+ if isinstance (node , yaml .ScalarNode ):
943+ return loader .construct_scalar (node )
944+ elif isinstance (node , yaml .SequenceNode ):
945+ return loader .construct_sequence (node )
946+ elif isinstance (node , yaml .MappingNode ):
947+ return loader .construct_mapping (node )
893948 return None
894949
895950 # Add constructors for CloudFormation intrinsic functions
@@ -915,17 +970,21 @@ def construct_unknown(loader, node):
915970 for func in cf_functions :
916971 CFLoader .add_constructor (func , construct_unknown )
917972
918- with open (template_path , "r" ) as f :
973+ with open (template_path , "r" , encoding = "utf-8" ) as f :
919974 template = yaml .load (f , Loader = CFLoader )
920975
976+ if not template or not isinstance (template , dict ):
977+ raise Exception (f"Failed to parse YAML template: { template_path } " )
978+
921979 resources = template .get ("Resources" , {})
922980 for resource_name , resource_config in resources .items ():
923981 if (
924982 resource_config
983+ and isinstance (resource_config , dict )
925984 and resource_config .get ("Type" ) == "AWS::Serverless::Function"
926985 ):
927986 properties = resource_config .get ("Properties" , {})
928- if properties :
987+ if properties and isinstance ( properties , dict ) :
929988 code_uri = properties .get ("CodeUri" , "" )
930989 if isinstance (code_uri , str ):
931990 code_uri = code_uri .rstrip ("/" )
@@ -934,12 +993,15 @@ def construct_unknown(loader, node):
934993 )
935994 if code_dir == dir_name :
936995 return resource_name
937-
938- return dir_name
996+ raise Exception (
997+ f"No CloudFormation function found for directory { dir_name } in template { template_path } "
998+ )
939999
9401000 except Exception as e :
941- self .log_verbose (f"Error reading template { template_path } : { e } " )
942- return dir_name
1001+ self .console .print (
1002+ f"[red]❌ Error extracting function name for { dir_name } from { template_path } : { e } [/red]"
1003+ )
1004+ sys .exit (1 )
9431005
9441006 def _validate_idp_common_in_build (self , template_dir , function_name , source_path ):
9451007 """Validate that idp_common package exists in the built Lambda function."""
@@ -1298,10 +1360,15 @@ def build_main_template(self, webui_zipfile, components_needing_rebuild):
12981360 )
12991361
13001362 # Validate the template
1301- template_url = f"https://s3.{ self .region } .amazonaws.com/{ self .bucket } /{ templates [0 ][0 ]} "
1302- self .console .print (f"[cyan]Validating template: { template_url } [/cyan]" )
1303- self .cf_client .validate_template (TemplateURL = template_url )
1304- self .console .print ("[green]✅ Template validation passed[/green]" )
1363+ if self .skip_validation :
1364+ self .console .print (
1365+ "[yellow]⚠️ Skipping CloudFormation template validation[/yellow]"
1366+ )
1367+ else :
1368+ template_url = f"https://s3.{ self .region } .amazonaws.com/{ self .bucket } /{ templates [0 ][0 ]} "
1369+ self .console .print (f"[cyan]Validating template: { template_url } [/cyan]" )
1370+ self .cf_client .validate_template (TemplateURL = template_url )
1371+ self .console .print ("[green]✅ Template validation passed[/green]" )
13051372
13061373 except ClientError as e :
13071374 # Delete checksum on template validation failure
0 commit comments