@@ -30,6 +30,10 @@ inputs:
30
30
description : ' Whether to run Vale style checks on documentation'
31
31
required : false
32
32
default : ' true'
33
+ check-cross-references :
34
+ description : ' Whether to check for broken cross-references when files or headings change'
35
+ required : false
36
+ default : ' true'
33
37
generate-preview :
34
38
description : ' Whether to generate preview links'
35
39
required : false
@@ -84,6 +88,9 @@ outputs:
84
88
vale_results :
85
89
description : ' Results from Vale style checks'
86
90
value : ${{ steps.lint-vale.outputs.result || '' }}
91
+ cross_ref_results :
92
+ description : ' Results from cross-reference checking'
93
+ value : ${{ steps.cross-references.outputs.cross_ref_results || '' }}
87
94
88
95
runs :
89
96
using : ' composite'
@@ -110,6 +117,31 @@ runs:
110
117
${{ inputs.include-md-files == 'true' && '**.md' || '' }}
111
118
separator : ' ,'
112
119
json : true
120
+
121
+ - name : Process file lists
122
+ id : process-files
123
+ shell : bash
124
+ run : |
125
+ # Set up environment
126
+ CHANGED_FILES='${{ steps.changed-files.outputs.all_changed_files_json }}'
127
+ DELETED_FILES='${{ steps.changed-files.outputs.deleted_files_json || '[]' }}'
128
+
129
+ # Process files into different formats once
130
+ echo "md_files_comma<<EOF" >> $GITHUB_OUTPUT
131
+ echo "${{ steps.changed-files.outputs.all_changed_files }}" >> $GITHUB_OUTPUT
132
+ echo "EOF" >> $GITHUB_OUTPUT
133
+
134
+ echo "md_files_line<<EOF" >> $GITHUB_OUTPUT
135
+ echo "$CHANGED_FILES" | jq -r '.[] | select(endswith(".md"))' >> $GITHUB_OUTPUT
136
+ echo "EOF" >> $GITHUB_OUTPUT
137
+
138
+ echo "docs_files_line<<EOF" >> $GITHUB_OUTPUT
139
+ echo "$CHANGED_FILES" | jq -r '.[] | select(endswith(".md")) | select(startswith("${{ inputs.docs-dir }}/"))' >> $GITHUB_OUTPUT
140
+ echo "EOF" >> $GITHUB_OUTPUT
141
+
142
+ echo "deleted_md_files_line<<EOF" >> $GITHUB_OUTPUT
143
+ echo "$DELETED_FILES" | jq -r '.[] | select(endswith(".md"))' >> $GITHUB_OUTPUT
144
+ echo "EOF" >> $GITHUB_OUTPUT
113
145
114
146
- name : Check if manifest changed
115
147
id : manifest-check
@@ -239,7 +271,7 @@ runs:
239
271
id : lint-docs
240
272
shell : bash
241
273
run : |
242
- lint_output=$(pnpm exec markdownlint-cli2 ${{ steps.changed -files.outputs.all_changed_files }} 2>&1) || true
274
+ lint_output=$(pnpm exec markdownlint-cli2 ${{ steps.process -files.outputs.md_files_comma }} 2>&1) || true
243
275
echo "result<<EOF" >> $GITHUB_OUTPUT
244
276
echo "$lint_output" >> $GITHUB_OUTPUT
245
277
echo "EOF" >> $GITHUB_OUTPUT
@@ -256,7 +288,7 @@ runs:
256
288
shell : bash
257
289
run : |
258
290
# markdown-table-formatter requires a space separated list of files
259
- format_output=$(echo ${{ steps.changed -files.outputs.all_changed_files }} | tr ',' '\n' | pnpm exec markdown-table-formatter --check 2>&1) || true
291
+ format_output=$(echo " ${{ steps.process -files.outputs.md_files_line }}" | pnpm exec markdown-table-formatter --check 2>&1) || true
260
292
echo "result<<EOF" >> $GITHUB_OUTPUT
261
293
echo "$format_output" >> $GITHUB_OUTPUT
262
294
echo "EOF" >> $GITHUB_OUTPUT
@@ -289,7 +321,7 @@ runs:
289
321
shell : bash
290
322
run : |
291
323
# Run Vale on changed files and capture output
292
- vale_output=$(echo ${{ steps.changed -files.outputs.all_changed_files }} | tr ',' '\n' | grep '\.md$' | xargs -r vale --config=.github/docs/vale/.vale.ini --output=line 2>&1) || true
324
+ vale_output=$(echo " ${{ steps.process -files.outputs.md_files_line }}" | xargs -r vale --config=.github/docs/vale/.vale.ini --output=line 2>&1) || true
293
325
294
326
echo "result<<EOF" >> $GITHUB_OUTPUT
295
327
echo "$vale_output" >> $GITHUB_OUTPUT
@@ -300,6 +332,137 @@ runs:
300
332
echo "$vale_output"
301
333
exit 1
302
334
fi
335
+
336
+ - name : Check for broken cross-references
337
+ if : inputs.check-cross-references == 'true' && steps.docs-analysis.outputs.has_changes == 'true'
338
+ id : cross-references
339
+ shell : bash
340
+ run : |
341
+ # Get the base branch (usually main)
342
+ BASE_SHA=$(git merge-base HEAD origin/main)
343
+
344
+ echo "Checking for broken cross-references..."
345
+
346
+ # Initialize results
347
+ BROKEN_REFS=""
348
+
349
+ # Process deleted files
350
+ if [ -n "${{ steps.process-files.outputs.deleted_md_files_line }}" ]; then
351
+ echo "Processing deleted files"
352
+
353
+ # Loop through deleted markdown files
354
+ while IFS= read -r file; do
355
+ [ -z "$file" ] && continue
356
+
357
+ echo "File $file was deleted, checking for references..."
358
+
359
+ # Convert file path to potential link formats (removing .md extension)
360
+ OLD_PATH=$(echo "$file" | sed 's/\.md$//')
361
+
362
+ # Search in docs directory
363
+ DOC_REFS=$(grep -r --include="*.md" -l -E "\[$OLD_PATH\]|\($OLD_PATH\)" ${{ inputs.docs-dir }} || echo "")
364
+
365
+ # Search in codebase (excluding specific directories)
366
+ CODE_REFS=$(grep -r --include="*.{go,ts,js,py,java,cs,php}" -l "$OLD_PATH" . --exclude-dir={node_modules,.git,build,dist} || echo "")
367
+
368
+ if [ -n "$DOC_REFS" ] || [ -n "$CODE_REFS" ]; then
369
+ BROKEN_REFS="${BROKEN_REFS}## References to deleted file: $file\n\n"
370
+
371
+ if [ -n "$DOC_REFS" ]; then
372
+ BROKEN_REFS="${BROKEN_REFS}### In documentation:\n"
373
+ BROKEN_REFS="${BROKEN_REFS}$(echo "$DOC_REFS" | sed 's/^/- /')\n\n"
374
+ fi
375
+
376
+ if [ -n "$CODE_REFS" ]; then
377
+ BROKEN_REFS="${BROKEN_REFS}### In codebase:\n"
378
+ BROKEN_REFS="${BROKEN_REFS}$(echo "$CODE_REFS" | sed 's/^/- /')\n\n"
379
+ fi
380
+ fi
381
+ done <<< "${{ steps.process-files.outputs.deleted_md_files_line }}"
382
+ fi
383
+
384
+ # Process modified files for heading changes
385
+ while IFS= read -r file; do
386
+ [ -z "$file" ] && continue
387
+
388
+ if [ -f "$file" ]; then
389
+ echo "Checking for changed headings in $file..."
390
+
391
+ # Extract headings before the change
392
+ OLD_HEADINGS=$(git show "$BASE_SHA:$file" 2>/dev/null | grep -E "^#{1,6} " | sed 's/^#\{1,6\} \(.*\)$/\1/' || echo "")
393
+
394
+ # Extract current headings
395
+ NEW_HEADINGS=$(cat "$file" | grep -E "^#{1,6} " | sed 's/^#\{1,6\} \(.*\)$/\1/')
396
+
397
+ # Find removed headings
398
+ REMOVED_HEADINGS=$(comm -23 <(echo "$OLD_HEADINGS" | sort) <(echo "$NEW_HEADINGS" | sort))
399
+
400
+ if [ -n "$REMOVED_HEADINGS" ]; then
401
+ while IFS= read -r heading; do
402
+ [ -z "$heading" ] && continue
403
+
404
+ # Convert heading to anchor format (lowercase, spaces to hyphens)
405
+ ANCHOR=$(echo "$heading" | tr '[:upper:]' '[:lower:]' | tr -cd '[:alnum:] -' | tr ' ' '-')
406
+
407
+ # Search for references to this anchor in documentation
408
+ HEAD_REFS=$(grep -r --include="*.md" -l "#$ANCHOR" ${{ inputs.docs-dir }} || echo "")
409
+
410
+ if [ -n "$HEAD_REFS" ]; then
411
+ BROKEN_REFS="${BROKEN_REFS}## References to removed heading: '$heading' in $file\n\n"
412
+ BROKEN_REFS="${BROKEN_REFS}$(echo "$HEAD_REFS" | sed 's/^/- /')\n\n"
413
+ fi
414
+ done <<< "$REMOVED_HEADINGS"
415
+ fi
416
+ fi
417
+ done <<< "${{ steps.process-files.outputs.docs_files_line }}"
418
+
419
+ # Check for renamed files by comparing paths
420
+ while IFS= read -r file; do
421
+ [ -z "$file" ] && continue
422
+
423
+ # Use git to check if this is a renamed file
424
+ PREV_PATH=$(git diff --name-status "$BASE_SHA" | grep "^R" | grep "$file$" | cut -f2)
425
+
426
+ if [ -n "$PREV_PATH" ] && [ "$PREV_PATH" != "$file" ]; then
427
+ echo "File renamed from $PREV_PATH to $file, checking for references..."
428
+
429
+ # Convert old file path to potential link formats
430
+ OLD_PATH=$(echo "$PREV_PATH" | sed 's/\.md$//')
431
+
432
+ # Search in docs directory
433
+ DOC_REFS=$(grep -r --include="*.md" -l -E "\[$OLD_PATH\]|\($OLD_PATH\)" ${{ inputs.docs-dir }} || echo "")
434
+
435
+ # Search in codebase (excluding specific directories)
436
+ CODE_REFS=$(grep -r --include="*.{go,ts,js,py,java,cs,php}" -l "$OLD_PATH" . --exclude-dir={node_modules,.git,build,dist} || echo "")
437
+
438
+ if [ -n "$DOC_REFS" ] || [ -n "$CODE_REFS" ]; then
439
+ BROKEN_REFS="${BROKEN_REFS}## References to renamed file: $PREV_PATH → $file\n\n"
440
+
441
+ if [ -n "$DOC_REFS" ]; then
442
+ BROKEN_REFS="${BROKEN_REFS}### In documentation:\n"
443
+ BROKEN_REFS="${BROKEN_REFS}$(echo "$DOC_REFS" | sed 's/^/- /')\n\n"
444
+ fi
445
+
446
+ if [ -n "$CODE_REFS" ]; then
447
+ BROKEN_REFS="${BROKEN_REFS}### In codebase:\n"
448
+ BROKEN_REFS="${BROKEN_REFS}$(echo "$CODE_REFS" | sed 's/^/- /')\n\n"
449
+ fi
450
+ fi
451
+ fi
452
+ done <<< "${{ steps.process-files.outputs.md_files_line }}"
453
+
454
+ if [ -n "$BROKEN_REFS" ]; then
455
+ echo "cross_ref_results<<EOF" >> $GITHUB_OUTPUT
456
+ echo -e "$BROKEN_REFS" >> $GITHUB_OUTPUT
457
+ echo "EOF" >> $GITHUB_OUTPUT
458
+
459
+ if [ "${{ inputs.fail-on-error }}" == "true" ]; then
460
+ echo "::error::Broken cross-references found. See output for details."
461
+ exit 1
462
+ fi
463
+ else
464
+ echo "No broken cross-references found"
465
+ fi
303
466
304
467
- name : Generate Preview URL
305
468
if : inputs.generate-preview == 'true' && steps.docs-analysis.outputs.has_changes == 'true'
@@ -368,6 +531,10 @@ runs:
368
531
${{ steps.lint-vale.outputs.result != '' && steps.lint-vale.outputs.result || '' }}
369
532
${{ steps.lint-vale.outputs.result != '' && '```' || '' }}
370
533
534
+ ${{ steps.cross-references.outputs.cross_ref_results != '' && '### Broken Cross-References' || '' }}
535
+ ${{ steps.cross-references.outputs.cross_ref_results != '' && 'The following cross-references may be broken due to file or heading changes:' || '' }}
536
+ ${{ steps.cross-references.outputs.cross_ref_results != '' && steps.cross-references.outputs.cross_ref_results || '' }}
537
+
371
538
---
372
539
<sub>🤖 This comment is automatically generated and updated when documentation changes.</sub>
373
540
edit-mode : replace
0 commit comments