During routine maintenance of the IT-Journey automated link checking system, we encountered a critical failure in our GitHub Actions workflow. The error was clear but puzzling:
KeyError: 'details'
File "analyze_links.py", line 25, in analyze_link_failures
'error': {'message': error['status']['details']},
~~~~~~~~~~~~~~~^^^^^^^^^^^
This error indicated that our Python script was trying to access a nested key structure (error['status']['details']) that didn’t exist in the actual data from the Lychee link checker output.
Working with AI assistance, we approached this systematically:
The workflow uses Lychee link checker to scan all markdown files in the IT-Journey repository, then processes the JSON results with a Python script. The error occurred when the script assumed a specific nested structure in error objects that wasn’t always present.
Before (Problematic Code):
# Convert error map to individual result format
for file_path, errors in error_map.items():
for error in errors:
results.append({
'url': error['url'],
'status': 'Failed',
'error': {'message': error['status']['details']}, # ❌ KeyError here!
'file': file_path
})
This code assumed:
error['status'] always existserror['status'] is always a dictionaryerror['status']['details'] always existsAfter (Robust Code):
# Convert error map to individual result format
for file_path, errors in error_map.items():
for error in errors:
# Extract error message more defensively
error_msg = ''
if 'status' in error:
if isinstance(error['status'], dict):
error_msg = error['status'].get('details',
error['status'].get('message',
str(error['status'])))
else:
error_msg = str(error['status'])
else:
error_msg = error.get('message', 'Unknown error')
results.append({
'url': error['url'],
'status': 'Failed',
'error': {'message': error_msg},
'file': file_path
})
We also improved the success map handling to be more defensive:
Before:
for success in successes:
results.append({
'url': success.get('url', ''), # Assumes success is always a dict
'status': 'Ok',
'file': file_path
})
After:
for success in successes:
# Handle both object and string URL formats
url = success.get('url', '') if isinstance(success, dict) else str(success)
results.append({
'url': url,
'status': 'Ok',
'file': file_path
})
Added comprehensive try-catch around the entire analysis:
try:
analysis = analyze_link_failures('link-check-results/results.json')
if analysis:
# Process successful analysis...
else:
print("Analysis failed - no data returned")
sys.exit(1)
except Exception as e:
print(f"Analysis failed with error: {e}")
print("Creating minimal analysis results for GitHub Actions...")
# Create minimal output so the workflow doesn't completely fail
with open('analysis_summary.txt', 'w') as f:
f.write("BROKEN_COUNT=0\n")
f.write("TOTAL_COUNT=0\n")
f.write("SUCCESS_RATE=0\n")
sys.exit(1)
This fix demonstrates how external tools (like Lychee) can change their output format over time, breaking assumptions in consuming code. Defensive programming prevents these fragile integrations.
This fix opens paths for further improvements:
This defensive programming approach will be applied to:
This debugging session exemplifies the power of AI-assisted development combined with solid defensive programming principles. By moving from rigid assumptions to flexible data handling, we’ve created a more robust automation system that can handle the inevitable changes in external tool outputs.
The key lesson: always assume external data might not match your expectations, and build resilience into your code from the start. This approach not only fixes immediate issues but prevents future problems and creates more maintainable systems.
This article demonstrates the IT-Journey approach of learning from failures, applying defensive programming principles, and leveraging AI assistance to create more robust automated systems. Each debugging session becomes an opportunity to strengthen the entire development ecosystem.