@@ -58,14 +58,16 @@ def __init__(self, verbose=False):
5858 self .cf_client = None
5959 self ._is_lib_changed = False
6060 self .skip_validation = False
61+ self .lint_enabled = True
6162
6263 def clean_checksums (self ):
63- """Delete all .checksum files in main, patterns, options, and lib directories"""
64+ """Delete all .checksum files in main, patterns, options, lib, and ui directories"""
6465 self .console .print ("[yellow]🧹 Cleaning all .checksum files...[/yellow]" )
6566
6667 checksum_paths = [
6768 ".checksum" , # main
6869 "lib/.checksum" , # lib
70+ "src/ui/.checksum" , # ui
6971 ]
7072
7173 # Add patterns checksum files
@@ -225,7 +227,7 @@ def print_usage(self):
225227 """Print usage information with Rich formatting"""
226228 self .console .print ("\n [bold cyan]Usage:[/bold cyan]" )
227229 self .console .print (
228- " python3 publish.py <cfn_bucket_basename> <cfn_prefix> <region> [public] [--max-workers N] [--verbose] [--no-validate] [--clean-build]"
230+ " python3 publish.py <cfn_bucket_basename> <cfn_prefix> <region> [public] [--max-workers N] [--verbose] [--no-validate] [--clean-build] [--lint on|off] "
229231 )
230232
231233 self .console .print ("\n [bold cyan]Parameters:[/bold cyan]" )
@@ -252,6 +254,9 @@ def print_usage(self):
252254 self .console .print (
253255 " [yellow][--clean-build][/yellow]: Optional. Delete all .checksum files to force full rebuild"
254256 )
257+ self .console .print (
258+ " [yellow][--lint on|off][/yellow]: Optional. Enable/disable UI linting and build validation (default: on)"
259+ )
255260
256261 def check_parameters (self , args ):
257262 """Check and validate input parameters"""
@@ -314,6 +319,19 @@ def check_parameters(self, args):
314319 self .console .print (
315320 "[yellow]CloudFormation template validation will be skipped[/yellow]"
316321 )
322+ elif arg == "--lint" :
323+ if i + 1 >= len (remaining_args ):
324+ self .console .print (
325+ "[red]Error: --lint requires 'on' or 'off'[/red]"
326+ )
327+ self .print_usage ()
328+ sys .exit (1 )
329+ lint_value = remaining_args [i + 1 ].lower ()
330+ if lint_value not in ["on" , "off" ]:
331+ self .console .print ("[red]Error: --lint must be 'on' or 'off'[/red]" )
332+ self .print_usage ()
333+ sys .exit (1 )
334+ self .lint_enabled = lint_value == "on"
317335 elif arg == "--clean-build" :
318336 self .clean_checksums ()
319337 else :
@@ -1170,6 +1188,50 @@ def upload_config_library(self):
11701188 f"[green]Configuration library uploaded to s3://{ self .bucket } /{ self .prefix_and_version } /config_library[/green]"
11711189 )
11721190
1191+ def ui_changed (self ):
1192+ """Check if UI has changed based on zipfile hash, returns (changed, zipfile_path)"""
1193+ ui_hash = self .compute_ui_hash ()
1194+ zipfile_name = f"src-{ ui_hash [:16 ]} .zip"
1195+ zipfile_path = os .path .join (".aws-sam" , zipfile_name )
1196+
1197+ existing_zipfiles = (
1198+ [
1199+ f
1200+ for f in os .listdir (".aws-sam" )
1201+ if f .startswith ("src-" ) and f .endswith (".zip" )
1202+ ]
1203+ if os .path .exists (".aws-sam" )
1204+ else []
1205+ )
1206+
1207+ if zipfile_name not in existing_zipfiles :
1208+ # Remove old zipfiles
1209+ for old_zip in existing_zipfiles :
1210+ old_path = os .path .join (".aws-sam" , old_zip )
1211+ if os .path .exists (old_path ):
1212+ os .remove (old_path )
1213+ return True , zipfile_path
1214+
1215+ return not os .path .exists (zipfile_path ), zipfile_path
1216+
1217+ def start_ui_validation_parallel (self ):
1218+ """Start UI validation in parallel if needed, returns (future, executor)"""
1219+ if not self .lint_enabled or not os .path .exists ("src/ui" ):
1220+ return None , None
1221+
1222+ changed , _ = self .ui_changed ()
1223+ if not changed :
1224+ return None , None
1225+
1226+ import concurrent .futures
1227+
1228+ ui_executor = concurrent .futures .ThreadPoolExecutor (max_workers = 1 )
1229+ ui_validation_future = ui_executor .submit (self .validate_ui_build )
1230+ self .console .print (
1231+ "[cyan]🔍 Starting UI validation in parallel with builds...[/cyan]"
1232+ )
1233+ return ui_validation_future , ui_executor
1234+
11731235 def compute_ui_hash (self ):
11741236 """Compute hash of UI folder contents"""
11751237 self .console .print ("[cyan]Computing hash of ui folder contents[/cyan]" )
@@ -1219,50 +1281,24 @@ def validate_ui_build(self):
12191281
12201282 def package_ui (self ):
12211283 """Package UI source code"""
1222- ui_hash = self .compute_ui_hash ()
1223- zipfile_name = f"src-{ ui_hash [:16 ]} .zip"
1224-
1225- # Ensure .aws-sam directory exists
1226- os .makedirs (".aws-sam" , exist_ok = True )
1227-
1228- # Check if we need to rebuild
1229- existing_zipfiles = [
1230- f
1231- for f in os .listdir (".aws-sam" )
1232- if f .startswith ("src-" ) and f .endswith (".zip" )
1233- ]
1234-
1235- if existing_zipfiles and existing_zipfiles [0 ] != zipfile_name :
1236- self .console .print (
1237- f"[yellow]WebUI zipfile name changed from { existing_zipfiles [0 ]} to { zipfile_name } , forcing rebuild[/yellow]"
1238- )
1239- # Remove old zipfile
1240- for old_zip in existing_zipfiles :
1241- old_path = os .path .join (".aws-sam" , old_zip )
1242- if os .path .exists (old_path ):
1243- os .remove (old_path )
1244-
1245- zipfile_path = os .path .join (".aws-sam" , zipfile_name )
1284+ _ , zipfile_path = self .ui_changed ()
12461285
12471286 if not os .path .exists (zipfile_path ):
1248- self .console .print ("[bold cyan]PACKAGING src/ui[/bold cyan]" )
1249- self .console .print (f"[cyan]Zipping source to { zipfile_path } [/cyan]" )
1250-
1287+ os .makedirs (".aws-sam" , exist_ok = True )
12511288 with zipfile .ZipFile (zipfile_path , "w" , zipfile .ZIP_DEFLATED ) as zipf :
12521289 ui_dir = "src/ui"
12531290 exclude_dirs = {"node_modules" , "build" , ".aws-sam" }
12541291 for root , dirs , files in os .walk (ui_dir ):
1255- # Exclude specified directories from zipping
12561292 dirs [:] = [d for d in dirs if d not in exclude_dirs ]
12571293 for file in files :
1258- # Skip .env files
12591294 if file == ".env" or file .startswith (".env." ):
12601295 continue
12611296 file_path = os .path .join (root , file )
12621297 arcname = os .path .relpath (file_path , ui_dir )
12631298 zipf .write (file_path , arcname )
12641299
12651300 # Check if file exists in S3 and upload if needed
1301+ zipfile_name = os .path .basename (zipfile_path )
12661302 s3_key = f"{ self .prefix_and_version } /{ zipfile_name } "
12671303 try :
12681304 self .s3_client .head_object (Bucket = self .bucket , Key = s3_key )
@@ -1329,10 +1365,6 @@ def build_main_template(self, webui_zipfile, components_needing_rebuild):
13291365 # Main template needs rebuilding, if any component needs rebuilding
13301366 if components_needing_rebuild :
13311367 self .console .print ("[yellow]Main template needs rebuilding[/yellow]" )
1332-
1333- # Validate UI build before rebuilding
1334- self .validate_ui_build ()
1335-
13361368 # Validate Python syntax in src directory before building
13371369 if not self ._validate_python_syntax ("src" ):
13381370 raise Exception ("Python syntax validation failed" )
@@ -1757,6 +1789,32 @@ def _validate_python_syntax(self, directory):
17571789 return False
17581790 return True
17591791
1792+ def _validate_python_linting (self ):
1793+ """Validate Python linting"""
1794+ if not self .lint_enabled :
1795+ return True
1796+
1797+ self .console .print ("[cyan]🔍 Running Python linting...[/cyan]" )
1798+
1799+ # Run ruff check (same as GitLab CI lint-cicd)
1800+ result = subprocess .run (["ruff" , "check" ], capture_output = True , text = True )
1801+ if result .returncode != 0 :
1802+ self .console .print ("[red]❌ Ruff linting failed![/red]" )
1803+ self .console .print (result .stdout , style = "red" , markup = False )
1804+ return False
1805+
1806+ # Run ruff format check (same as GitLab CI lint-cicd)
1807+ result = subprocess .run (
1808+ ["ruff" , "format" , "--check" ], capture_output = True , text = True
1809+ )
1810+ if result .returncode != 0 :
1811+ self .console .print ("[red]❌ Code formatting check failed![/red]" )
1812+ self .console .print (result .stdout , style = "red" , markup = False )
1813+ return False
1814+
1815+ self .console .print ("[green]✅ Python linting passed[/green]" )
1816+ return True
1817+
17601818 def build_lib_package (self ):
17611819 """Build lib package with syntax validation"""
17621820 try :
@@ -1926,12 +1984,19 @@ def run(self, args):
19261984 # Check prerequisites
19271985 self .check_prerequisites ()
19281986
1987+ # Validate Python linting if enabled
1988+ if not self ._validate_python_linting ():
1989+ raise Exception ("Python linting validation failed" )
1990+
19291991 # Set up S3 bucket
19301992 self .setup_artifacts_bucket ()
19311993
19321994 # Perform smart rebuild detection and cache management
19331995 components_needing_rebuild = self .smart_rebuild_detection ()
19341996
1997+ # Start UI validation early in parallel
1998+ ui_validation_future , ui_executor = self .start_ui_validation_parallel ()
1999+
19352000 # clear component cache
19362001 for comp_info in components_needing_rebuild :
19372002 if comp_info ["component" ] != "lib" : # lib doesnt have sam build
@@ -2004,7 +2069,24 @@ def run(self, args):
20042069 # Upload configuration library
20052070 self .upload_config_library ()
20062071
2007- # Package UI
2072+ # Wait for UI validation to complete if it was started
2073+ if ui_validation_future :
2074+ try :
2075+ self .console .print (
2076+ "[cyan]⏳ Waiting for UI validation to complete...[/cyan]"
2077+ )
2078+ ui_validation_future .result ()
2079+ self .console .print (
2080+ "[green]✅ UI validation completed successfully[/green]"
2081+ )
2082+ except Exception as e :
2083+ self .console .print ("[red]❌ UI validation failed:[/red]" )
2084+ self .console .print (str (e ), style = "red" , markup = False )
2085+ sys .exit (1 )
2086+ finally :
2087+ ui_executor .shutdown (wait = True )
2088+
2089+ # Package UI and start validation in parallel if needed
20082090 webui_zipfile = self .package_ui ()
20092091
20102092 # Build main template
0 commit comments