Skip to content

Commit 55283cb

Browse files
committed
Update CI checker script
1 parent dc4b862 commit 55283cb

File tree

2 files changed

+114
-35
lines changed

2 files changed

+114
-35
lines changed

.github/workflows/check-links.yml

Lines changed: 49 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ concurrency:
1010
group: ${{ github.workflow }}-${{ github.ref }}
1111
cancel-in-progress: true
1212
jobs:
13-
check:
13+
check-absolute-links:
1414
if: github.event.pull_request.draft == false
1515
runs-on: ubuntu-latest
1616
steps:
@@ -19,14 +19,60 @@ jobs:
1919
- name: Set up Python
2020
uses: actions/setup-python@v4
2121
with:
22-
python-version: "3.11"
22+
python-version: '3.11'
2323
- name: Install dependencies
2424
run: pip install requests
2525
- name: Check Absolute Links
26+
id: check-absolute
2627
run: |
2728
# Only check absolute links (http/https), not relative links
28-
python docs/link_checker.py --dir docs/book --substring "http" --validate-links --timeout 15
29+
python docs/link_checker.py --dir docs/book --substring "http" --validate-links --timeout 15 --ci-mode
30+
# Continue on error so both checks can run to completion
31+
continue-on-error: true
32+
check-relative-links:
33+
if: github.event.pull_request.draft == false
34+
runs-on: ubuntu-latest
35+
steps:
36+
- name: Checkout Repository
37+
uses: actions/checkout@v4
38+
- name: Set up Python
39+
uses: actions/setup-python@v4
40+
with:
41+
python-version: '3.11'
2942
- name: Check Relative Links
43+
id: check-relative
3044
run: |-
3145
# Check if relative links resolve within the repository
3246
python scripts/check_relative_links.py --dir docs/book
47+
# Continue on error so both checks can run to completion
48+
continue-on-error: true
49+
summary:
50+
needs: [check-absolute-links, check-relative-links]
51+
if: always() && github.event.pull_request.draft == false
52+
runs-on: ubuntu-latest
53+
steps:
54+
- name: Create Summary
55+
run: |-
56+
echo "# Documentation Link Check Results" >> $GITHUB_STEP_SUMMARY
57+
58+
# Check for failures in absolute links job
59+
if [[ "${{ needs.check-absolute-links.result }}" == "failure" ]]; then
60+
echo "❌ **Absolute links check failed**" >> $GITHUB_STEP_SUMMARY
61+
echo "There are broken absolute links in the documentation. Please check the job logs for details." >> $GITHUB_STEP_SUMMARY
62+
else
63+
echo "✅ **Absolute links check passed**" >> $GITHUB_STEP_SUMMARY
64+
fi
65+
66+
# Check for failures in relative links job
67+
if [[ "${{ needs.check-relative-links.result }}" == "failure" ]]; then
68+
echo "❌ **Relative links check failed**" >> $GITHUB_STEP_SUMMARY
69+
echo "There are broken relative links in the documentation. Please check the job logs for details." >> $GITHUB_STEP_SUMMARY
70+
else
71+
echo "✅ **Relative links check passed**" >> $GITHUB_STEP_SUMMARY
72+
fi
73+
74+
# Set job status based on both child jobs
75+
if [[ "${{ needs.check-absolute-links.result }}" == "failure" || "${{ needs.check-relative-links.result }}" == "failure" ]]; then
76+
echo "::error::One or more link checks failed. Please fix the broken links."
77+
exit 1
78+
fi

docs/link_checker.py

Lines changed: 65 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@
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
5253
Note:
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"\nFound {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"\nFound {total_matches} links containing '{args.substring}' across {len(files_to_scan)} files."
778-
)
796+
if not args.ci_mode:
797+
print(
798+
f"\nFound {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"\nValidating {len(links_to_validate)} links...")
803+
if not args.ci_mode:
804+
print(f"\nValidating {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"\nLink validation: {valid_count} valid, {broken_count} broken"
792-
)
812+
if not args.ci_mode:
813+
print(
814+
f"\nLink validation: {valid_count} valid, {broken_count} broken"
815+
)
793816

794817
if broken_count > 0:
795-
print("\nBroken links:")
818+
has_broken_links = True
819+
if not args.ci_mode:
820+
print("\nBroken 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

835868
if __name__ == "__main__":
836869
main()

0 commit comments

Comments
 (0)