9393 v9.0.3.2 24/04/25 SAN couple of pre-flight fixes
9494 v9.0.3.3 24/04/25 SAN fix commit errors
9595 v9.0.4.0 24/04/25 SAN New commit design, decreased importance of uncommited at git check, added emojis ✅, bugfix on git stdout
96-
96+ v9.0.4.1 28/04/25 SAN Paranoid check added to avoid random deletion, more verbose output on file deletion, moved folder check after git as git require an empty folder when first cloning, couple of pre-flight fixes
97+ v9.0.4.2 29/04/25 SAN more explicit part 2 & 3 outputs, added RW check
98+ v9.0.4.3 29/04/25 SAN more explicit outputs for git pull & fix other output
99+ v9.0.4.4 30/04/25 SAN fix pull
97100
98101.TODO
99- Handle rights issues when executing git commands
102+ move env setup to pre-flight
100103 Review flow of step 3 for optimisations
101-
104+ Review the counters for step 3
102105 Revamp folder structure:
103- Move raws from "scriptsraw" to Category/folder /raws/
106+ Move raws from "scriptsraw" to Category_name /raws/
104107 add "uncategorised" folder
105- remove "scripts" top level folder while keeping snippets
108+ remove "scripts" top level folder while keeping snippets and move snippets raws to snippets/raws/
106109
107110 Move ID from json to an array like this and make sure that this array is never overwriten to keep tracks of IDs across instances only add current instance in step 2 if missing:
108111 "ids": [
115118 Delete script support from git ? (dedicated function required at the end of step 2, if json exist but no script matches mark for delete json and use the id of the json to tell the api to delete in trmm)
116119 Squash commit from minor update json with previous commit
117120 Add reporting support
121+
118122
119123"""
120124
133137
134138# Retrieve the git pull branch or default to 'master'
135139git_pull_branch = os .getenv ('GIT_PULL_BRANCH' , 'master' )
136- if git_pull_branch != 'master' : print (f"Git Pull Branch: { git_pull_branch } " )
137140
138141# Retrieve flags from environment variables (default to True unless set to 'false')
139142ENABLE_GIT_PULL = os .getenv ('ENABLE_GIT_PULL' , 'True' ).lower () != 'false'
147150
148151
149152def delete_obsolete_files (folder , current_scripts ):
150- print (f"Cleaning { folder } ..." )
151- obsolete = {f for f in folder .rglob ('*' ) if f .is_file () and f .relative_to (folder ) not in current_scripts }
152-
153+ if not current_scripts :
154+ print ("❌ ERROR: No valid scripts provided by api. Aborting." )
155+ sys .exit (1 )
156+ if not isinstance (current_scripts , set ):
157+ print ("❌ ERROR: 'current_scripts' must be a set. Aborting." )
158+ sys .exit (1 )
159+
160+ print (f"🧹 Cleaning { folder } ..." )
161+
162+ all_paths = list (folder .rglob ('*' ))
163+ obsolete = {f for f in all_paths if f .is_file () and f .relative_to (folder ) not in current_scripts }
164+
153165 if not obsolete :
154- print ("No files missing from the API but still present in the repo." )
166+ print ("✅ No files missing from the API but still present in the repo." )
167+ else :
168+ with open ("deletion.log" , "a" ) as log :
169+ for f in obsolete :
170+ action = "🗑️📄 Deleted" if ENABLE_WRITETOFILE else "🗑️📄 Simulated deletion of"
171+ try :
172+ if ENABLE_WRITETOFILE :
173+ f .unlink ()
174+ print (f"{ action } file no longer in the API: { f } " )
175+ log .write (f"{ action } : { f } \n " )
176+ except Exception as e :
177+ print (f"⚠️ Error deleting file { f } : { e } " )
178+ log .write (f"⚠️ Error deleting { f } : { e } \n " )
155179
156- for f in obsolete :
157- if ENABLE_WRITETOFILE :
158- try :
159- f .unlink ()
160- print (f"Deleted file no longer in the API: { f } " )
161- except Exception as e :
162- print (f"Error deleting file { f } : { e } " )
163- else :
164- print (f"Simulated deletion of file no longer in the API: { f } " )
180+ empty_dirs = [d for d in sorted (all_paths , key = lambda p : - len (p .parts )) if d .is_dir () and not any (d .iterdir ())]
165181
166- empty_dirs = [d for d in sorted (folder .rglob ('*' ), key = lambda p : - len (p .parts )) if d .is_dir () and not any (d .iterdir ())]
167182 if not empty_dirs :
168- print ("No empty directories to remove." )
169-
170- for d in empty_dirs :
171- if ENABLE_WRITETOFILE :
172- try :
173- d .rmdir ()
174- print (f"Removed empty directory: { d } " )
175- except Exception as e :
176- print (f"Could not delete dir { d } : { e } " )
177- else :
178- print (f"Simulated removal of empty directory: { d } " )
183+ print ("✅ No empty directories to remove." )
184+ else :
185+ with open ("deletion.log" , "a" ) as log :
186+ for d in empty_dirs :
187+ action = "🗑️📁 Removed" if ENABLE_WRITETOFILE else "🗑️📁 Simulated removal of"
188+ try :
189+ if ENABLE_WRITETOFILE :
190+ d .rmdir ()
191+ print (f"{ action } empty directory: { d } " )
192+ log .write (f"{ action } : { d } \n " )
193+ except Exception as e :
194+ print (f"⚠️ Could not delete dir { d } : { e } " )
195+ log .write (f"⚠️ Could not delete dir { d } : { e } \n " )
179196
180197def sanitize_filename (name : str ) -> str :
181198 removed_chars = []
@@ -223,7 +240,7 @@ def process_scripts(scripts, script_folder, script_raw_folder, shell_summary, is
223240 current .add ((folder / fname ).relative_to (script_folder ))
224241 current .add ((raw_folder / raw_name ).relative_to (script_raw_folder ))
225242
226- print (f"Processed { len (current )} { 'snippets' if is_snippet else 'scripts' } ." )
243+ print (f"Processed { len (current )} { 'snippets' if is_snippet else 'scripts' } .\n " )
227244 return current
228245
229246def compute_hash (file_path ):
@@ -237,9 +254,9 @@ def save_file(path, content, is_json=False):
237254 data = json .dumps (content , indent = 4 , ensure_ascii = False ) if is_json else content
238255 if ENABLE_WRITETOFILE :
239256 path .write_text (data , encoding = "utf-8" )
240- print (f"File saved: { path } " )
257+ print (f"File saved: { path . relative_to ( base_dir ) if base_dir else path } " )
241258 else :
242- print (f"File would be saved (simulation): { path } " )
259+ print (f"File would be saved (simulation): { path . relative_to ( base_dir ) if base_dir else path } " )
243260
244261
245262def pull_from_api (url ):
@@ -310,6 +327,7 @@ def update_api_if_needed(match, raw_data, is_snippet):
310327
311328 return False
312329
330+
313331def write_modifications_to_api (base_dir , folders ):
314332 print ("Comparing script files with JSON files..." )
315333 mismatches = []
@@ -318,7 +336,7 @@ def write_modifications_to_api(base_dir, folders):
318336 total_matches = 0
319337 total_mismatches = 0
320338 total_updated = 0
321- total_skipped = 0
339+ total_not_updated = 0
322340
323341 for folder_key , folder in folders .items ():
324342 is_snippet = folder_key == 'snippetsraw'
@@ -332,15 +350,15 @@ def write_modifications_to_api(base_dir, folders):
332350 if p .is_file () and p .stem .lower () == raw_name ), None )
333351 except Exception as e :
334352 print (f"Error matching file for { raw_path } : { e } " )
335- total_skipped += 1
353+ total_not_updated += 1
336354 continue
337355
338356 if not match :
339- print (f"No match for { 'snippet' if is_snippet else 'script' } : { raw_path } " )
340- total_skipped += 1
357+ print (f"No match for { 'snippet' if is_snippet else 'script' } : { raw_path . relative_to ( base_dir ) } " )
358+ total_not_updated += 1
341359 continue
342360
343- print (f"Matched { 'snippet' if is_snippet else 'script' } : { match } <-> { raw_path } " )
361+ print (f"Matched { 'snippet' if is_snippet else 'script' } : { match . relative_to ( base_dir ) } <-> { raw_path . relative_to ( base_dir ) } " )
344362 total_matches += 1
345363
346364 file_hash , code_hash , raw_data = compare_files_and_hashes (match , raw_path )
@@ -364,13 +382,24 @@ def write_modifications_to_api(base_dir, folders):
364382
365383 if updated :
366384 total_updated += 1
385+ else :
386+ total_not_updated += 1
387+
388+ print ("\n 🔍 Comparison Complete:" )
389+
390+ print (f"🧾 Total files checked: { total_files_checked } " )
391+ if total_matches > 0 :
392+ print (f"↔️ Total matches: { total_matches } " )
393+ if total_mismatches > 0 :
394+ print (f"🧩 Total mismatches to update: { total_mismatches } " )
395+ if total_updated > 0 :
396+ print (f"✅ Total updated: { total_updated } " )
397+ if total_not_updated > 0 :
398+ print (f"❌ Total errors: { total_not_updated } " )
399+
400+ if total_matches == total_files_checked :
401+ print ("✅ Everything is up to date in the api" )
367402
368- print ("\n Comparison Complete:" )
369- print (f"Total files checked: { total_files_checked } " )
370- print (f"Total matches: { total_matches } " )
371- print (f"Total mismatches: { total_mismatches } " )
372- print (f"Total updates: { total_updated } " )
373- print (f"Total skipped: { total_skipped } " )
374403
375404def update_to_api (item_id , payload , is_snippet = False ):
376405 """Update the API with the provided item ID and payload."""
@@ -395,13 +424,27 @@ def update_to_api(item_id, payload, is_snippet=False):
395424 print (f"Request error for { 'snippet' if is_snippet else 'script' } { item_id } : { e } " )
396425
397426def git_pull (base_dir ):
398- """Force pull the latest changes from the git repository, discarding local changes."""
399-
427+ """Force pull latest changes from the git repository if there are changes , discarding local changes."""
428+
400429 print ("Starting pull process..." , flush = True )
401430 try :
431+ print (f"Branch to pull: '{ git_pull_branch } '" )
402432 print ("Fetching latest changes from remote..." , flush = True )
403433 subprocess .check_call (['git' , '-C' , base_dir , 'fetch' , 'origin' ], stdout = sys .stdout , stderr = sys .stderr )
404434
435+ print ("Checking for incoming changes..." , flush = True )
436+ result = subprocess .run (
437+ ['git' , '-C' , base_dir , 'log' , f'HEAD..origin/{ git_pull_branch } ' , '--oneline' ],
438+ capture_output = True , text = True
439+ )
440+
441+ if not result .stdout .strip ():
442+ print ("No changes to pull. Repository is up to date." , flush = True )
443+ return
444+
445+ print ("Incoming commits:" )
446+ print (result .stdout , flush = True )
447+
405448 print (f"Resetting local branch to match 'origin/{ git_pull_branch } '..." , flush = True )
406449 subprocess .check_call (['git' , '-C' , base_dir , 'reset' , '--hard' , f'origin/{ git_pull_branch } ' ], stdout = sys .stdout , stderr = sys .stderr )
407450
@@ -498,13 +541,22 @@ def git_push(base_dir):
498541 print (f"Changes pushed to branch '{ git_pull_branch } '" )
499542
500543 else :
501- print ("No changes to commit." )
544+ print ("✅ No changes to commit." )
502545 except subprocess .CalledProcessError as e :
503546 print (f"Git operation failed: { e } " )
504547
505548def check_git_health (base_dir ):
506549 """Check the health of the Git repository."""
507550
551+ # Check the rights to read/write in the directory
552+ try :
553+ if not os .access (base_dir , os .R_OK | os .W_OK ):
554+ print (f"❌ Error: No read/write permissions for the directory '{ base_dir } '." )
555+ return False
556+ except Exception as e :
557+ print (f"❌ Error: Failed to check permissions for the directory '{ base_dir } '. { e } " )
558+ return False
559+
508560 # Check if 'git' command is available
509561 try :
510562 subprocess .check_call (['git' , '--version' ], stdout = subprocess .DEVNULL , stderr = subprocess .DEVNULL )
@@ -601,11 +653,12 @@ def check_git_health(base_dir):
601653 print ("❌ Error: Failed to check commit history." )
602654 return False
603655
604- print ("✅ Git repository health check passed." )
605656 return True
606657
658+
607659def pre_flight ():
608- global domain , scriptpath , headers
660+ global domain , headers , base_dir
661+
609662 domain = os .getenv ('DOMAIN' )
610663 api_token = os .getenv ('API_TOKEN' )
611664 scriptpath = os .getenv ('SCRIPTPATH' )
@@ -619,8 +672,13 @@ def pre_flight():
619672 if var == 'SCRIPTPATH' : print (f" - SCRIPTPATH: The local folder path for Git commands." )
620673 sys .exit (1 )
621674
675+ #Build headers
622676 headers = {"X-API-KEY" : api_token }
623- domain_for_connection = domain .replace ("https://" , "" ).replace ("http://" , "" )
677+ #Build base_dir path
678+ base_dir = Path (scriptpath ).resolve ()
679+
680+ # no http for tcp test or any trailing slash
681+ domain_for_connection = domain .replace ("https://" , "" ).replace ("http://" , "" ).rstrip ("/" )
624682
625683 try :
626684 socket .create_connection ((domain_for_connection , 443 ), timeout = 5 )
@@ -630,11 +688,13 @@ def pre_flight():
630688 print (f"❌ Error: Unable to connect to { domain } on port 443 - { e } (Obfuscated API Token: { obfuscated } )" )
631689 sys .exit (1 )
632690
691+ # Make sure domain starts with https:// and remove any trailing slash
633692 if not domain .startswith ("http://" ) and not domain .startswith ("https://" ):
634693 domain = "https://" + domain
694+ domain = domain .rstrip ("/" )
635695
696+ #Test api token for read, it is currently not possible to test for write as any request to the api would write empty file.
636697 obfuscated = api_token [:3 ] + '*' * (len (api_token ) - 6 ) + api_token [- 3 :]
637-
638698 try :
639699 response = requests .get (f"{ domain } /scripts/" , headers = headers , timeout = 5 )
640700 if response .status_code == 200 :
@@ -648,6 +708,7 @@ def pre_flight():
648708
649709 return
650710
711+
651712def check_and_create_folders (base_path , subfolders ):
652713 try :
653714 if not base_path .exists ():
@@ -671,21 +732,11 @@ def main():
671732
672733 # 0. Prep: Verify Dependencies, Set Up Environment, and Git Health Check
673734 print ("\n ===== Step 0: General Prep =====" )
674-
675- # ENV vars & network checks
735+
736+
737+ # ENV vars & network checks & prep vars
676738 pre_flight ()
677739
678- # Folder structure check
679- base_dir = Path (scriptpath ).resolve ()
680- folders = {
681- "scripts" : base_dir / "scripts" ,
682- "scriptsraw" : base_dir / "scriptsraw" ,
683- "snippets" : base_dir / "snippets" ,
684- "snippetsraw" : base_dir / "snippetsraw"
685- }
686- check_and_create_folders (base_dir , folders )
687- print ("✅ All folders created and verified." )
688-
689740 # Check the health of the Git repo
690741 if ENABLE_GIT_PULL or ENABLE_GIT_PUSH :
691742 if check_git_health (base_dir ):
@@ -695,12 +746,21 @@ def main():
695746 sys .exit (1 )
696747 else :
697748 print ("Skipping Git health check because both pull and push are disabled." )
698-
749+
750+ # Folder structure check
751+ folders = {
752+ "scripts" : base_dir / "scripts" ,
753+ "scriptsraw" : base_dir / "scriptsraw" ,
754+ "snippets" : base_dir / "snippets" ,
755+ "snippetsraw" : base_dir / "snippetsraw"
756+ }
757+ check_and_create_folders (base_dir , folders )
758+ print ("✅ All folders created and verified." )
759+
699760 print ("===== End of Step 0: General Prep =====\n " )
700761
701762 # 1. Git Pull
702763 print ("\n ===== Step 1: Git Pull =====" )
703- print (f"Branch to pull: '{ git_pull_branch } '" )
704764 if ENABLE_GIT_PULL :
705765 git_pull (base_dir )
706766 else :
@@ -716,19 +776,21 @@ def main():
716776 print ("\n ===== Step 3: Fetch and Process Scripts and Snippets =====" )
717777 # Initialize counters and sets
718778 shell_summary , current_scripts = defaultdict (int ), set ()
719- print ("Fetching scripts ..." )
779+ print ("Fetching script list ..." )
720780 user_defined_scripts = pull_from_api (f"{ domain } /scripts/?showHiddenScripts=true" )
721781 user_defined_scripts = [item for item in user_defined_scripts if item .get ('script_type' ) == 'userdefined' ]
722782 current_scripts .update (process_scripts (user_defined_scripts , folders ["scripts" ], folders ["scriptsraw" ], shell_summary ))
723783
724784 # Fetch and process snippets
725- print ("Fetching snippets..." )
785+ print ("Fetching snippets list ..." )
726786 snippets = pull_from_api (f"{ domain } /scripts/snippets/" )
727787 current_scripts .update (process_scripts (snippets , folders ["snippets" ], folders ["snippetsraw" ], shell_summary , is_snippet = True ))
728788
729789 # Output the total number of scripts exported and provide a summary of the shell counts
730790 print (f"Total number of scripts exported: { len (current_scripts )} " )
731- print ("Shell summary:" , "\n " .join (f"{ shell } : { count } " for shell , count in shell_summary .items ()))
791+ print ("Shell summary:" )
792+ for shell , count in shell_summary .items ():
793+ print (f"{ shell .strip ()} : { count } " )
732794
733795 # Remove any obsolete files that are no longer existing in the api
734796 print ("\n Remove any obsolete files" )
0 commit comments