4848 --validate-links: Check if links are valid by making HTTP requests
4949 --timeout: Timeout for HTTP requests in seconds (default: 10)
5050 --url-mapping: Path segment mappings in format old=new (can be used multiple times)
51+ --ci-mode: CI mode: only report broken links and exit with error code on failures
5152
5253Note:
5354 The 'requests' package is required for link validation. Install it with:
@@ -640,6 +641,11 @@ def main():
640641 action = "append" ,
641642 help = "Path segment mappings in format old=new (can be used multiple times)" ,
642643 )
644+ parser .add_argument (
645+ "--ci-mode" ,
646+ action = "store_true" ,
647+ help = "CI mode: only report broken links and exit with error code on failures" ,
648+ )
643649 args = parser .parse_args ()
644650
645651 # Check for requests module if validation is enabled
@@ -666,12 +672,14 @@ def main():
666672 files_to_scan = []
667673 if args .dir :
668674 files_to_scan = find_markdown_files (args .dir )
669- print (
670- f"Found { len (files_to_scan )} markdown files in directory: { args .dir } "
671- )
675+ if not args .ci_mode :
676+ print (
677+ f"Found { len (files_to_scan )} markdown files in directory: { args .dir } "
678+ )
672679 else :
673680 files_to_scan = args .files
674- print (f"Scanning { len (files_to_scan )} specified markdown files" )
681+ if not args .ci_mode :
682+ print (f"Scanning { len (files_to_scan )} specified markdown files" )
675683
676684 if args .replace_links :
677685 # Replace links mode
@@ -689,7 +697,8 @@ def main():
689697 url_mappings ,
690698 )
691699 if replacements :
692- print (f"\n { file_path } :" )
700+ if not args .ci_mode :
701+ print (f"\n { file_path } :" )
693702 for original , (
694703 new ,
695704 is_valid ,
@@ -698,51 +707,62 @@ def main():
698707 total_replacements += 1
699708
700709 if args .validate_links and is_valid is not None :
701- status = (
702- "✅ Valid"
703- if is_valid
704- else f"❌ Broken: { error } "
705- )
706- print (f" { original } -> { new } [{ status } ]" )
707-
708710 if is_valid :
709711 valid_links += 1
712+ if not args .ci_mode :
713+ print (f" { original } -> { new } [✅ Valid]" )
710714 else :
711715 broken_links += 1
712- else :
716+ # Always print broken links even in CI mode
717+ status = f"❌ Broken: { error } "
718+ if args .ci_mode :
719+ print (f"{ file_path } :" )
720+ print (f" { original } -> { new } [{ status } ]" )
721+ elif not args .ci_mode :
713722 print (f" { original } -> { new } " )
714723 except Exception as e :
715724 print (f"Error processing { file_path } : { e } " , file = sys .stderr )
716725
717726 mode = "Would replace" if args .dry_run else "Replaced"
718- print (
719- f"\n { mode } { total_replacements } links across { len (files_to_scan )} files."
720- )
721-
722- if args .validate_links and (valid_links > 0 or broken_links > 0 ):
727+ if not args .ci_mode :
723728 print (
724- f"Link validation: { valid_links } valid, { broken_links } broken "
729+ f"\n { mode } { total_replacements } links across { len ( files_to_scan ) } files. "
725730 )
726731
732+ if args .validate_links and (valid_links > 0 or broken_links > 0 ):
733+ print (
734+ f"Link validation: { valid_links } valid, { broken_links } broken"
735+ )
736+
737+ # In CI mode, exit with error code if broken links were found
738+ if args .ci_mode and broken_links > 0 :
739+ print (f"\n Found { broken_links } broken links" )
740+ sys .exit (1 )
741+
727742 elif args .substring :
728743 # Find links mode
729744 total_matches = 0
730745 links_to_validate = []
731746 file_links_map = {}
747+ has_broken_links = False
732748
733749 for file_path in files_to_scan :
734750 try :
735751 matches = check_links_with_substring (file_path , args .substring )
736752 if matches :
737753 file_links = []
738- print (f"\n { file_path } :" )
754+ if not args .ci_mode :
755+ print (f"\n { file_path } :" )
739756 for link , line_num , _ , _ , _ in matches :
740757 # Create clickable link to the file at the specific line
741758 clickable_path = get_clickable_path (
742759 file_path , line_num
743760 )
744- print (f" Line { line_num } : { link } " )
745- print (f" ↳ { clickable_path } " )
761+
762+ if not args .ci_mode :
763+ print (f" Line { line_num } : { link } " )
764+ print (f" ↳ { clickable_path } " )
765+
746766 total_matches += 1
747767
748768 if args .validate_links :
@@ -763,7 +783,7 @@ def main():
763783 (link , line_num , link )
764784 ) # Original and transformed are the same
765785 # If neither, log it but don't validate
766- else :
786+ elif not args . ci_mode :
767787 print (
768788 f" ↳ Skipping validation (not a recognized link format)"
769789 )
@@ -773,26 +793,32 @@ def main():
773793 except Exception as e :
774794 print (f"Error processing { file_path } : { e } " , file = sys .stderr )
775795
776- print (
777- f"\n Found { total_matches } links containing '{ args .substring } ' across { len (files_to_scan )} files."
778- )
796+ if not args .ci_mode :
797+ print (
798+ f"\n Found { total_matches } links containing '{ args .substring } ' across { len (files_to_scan )} files."
799+ )
779800
780801 # Validate links if requested
781802 if args .validate_links and links_to_validate :
782- print (f"\n Validating { len (links_to_validate )} links..." )
803+ if not args .ci_mode :
804+ print (f"\n Validating { len (links_to_validate )} links..." )
783805 validation_results = validate_urls (list (set (links_to_validate )))
784806
785807 valid_count = sum (
786808 1 for result in validation_results .values () if result [0 ]
787809 )
788810 broken_count = len (validation_results ) - valid_count
789811
790- print (
791- f"\n Link validation: { valid_count } valid, { broken_count } broken"
792- )
812+ if not args .ci_mode :
813+ print (
814+ f"\n Link validation: { valid_count } valid, { broken_count } broken"
815+ )
793816
794817 if broken_count > 0 :
795- print ("\n Broken links:" )
818+ has_broken_links = True
819+ if not args .ci_mode :
820+ print ("\n Broken links:" )
821+
796822 for file_path , links in file_links_map .items ():
797823 broken_in_file = []
798824 for transformed_link , line_num , original_link in links :
@@ -822,7 +848,10 @@ def main():
822848 # Show the original link in the output, but we validated the transformed one
823849 print (f" Line { line_num } : { original_link } " )
824850 print (f" ↳ ❌ { status_info } " )
825- if original_link != transformed_link :
851+ if (
852+ original_link != transformed_link
853+ and not args .ci_mode
854+ ):
826855 print (
827856 f" ↳ Validated as: { transformed_link } "
828857 )
@@ -831,6 +860,10 @@ def main():
831860 )
832861 print (f" ↳ { clickable_path } " )
833862
863+ # In CI mode, exit with error code if broken links were found
864+ if args .ci_mode and has_broken_links :
865+ sys .exit (1 )
866+
834867
835868if __name__ == "__main__" :
836869 main ()
0 commit comments