Work in progress
All checks were successful
Build Antora container image for Fedora Docs / build (x86_64, latest, buildroot) (push) Successful in 54s
Build Antora container image for Fedora Docs / merge (x86_64, latest, buildroot) (push) Successful in 27s
Build buildroot container image for Fedora Docs / build (x86_64, latest, buildroot) (push) Successful in 1m26s
Build buildroot container image for Fedora Docs / merge (x86_64, latest, buildroot) (push) Successful in 46s
Build container image for testing the Fedora Council Docs / build (push) Successful in 6m2s
All checks were successful
Build Antora container image for Fedora Docs / build (x86_64, latest, buildroot) (push) Successful in 54s
Build Antora container image for Fedora Docs / merge (x86_64, latest, buildroot) (push) Successful in 27s
Build buildroot container image for Fedora Docs / build (x86_64, latest, buildroot) (push) Successful in 1m26s
Build buildroot container image for Fedora Docs / merge (x86_64, latest, buildroot) (push) Successful in 46s
Build container image for testing the Fedora Council Docs / build (push) Successful in 6m2s
Signed-off-by: Hristo Marinov <hricky@fedoraproject.org>
This commit is contained in:
parent
1efded10b5
commit
d7450432f0
1 changed files with 116 additions and 0 deletions
116
ci/monitor-timestamps.py
Executable file
116
ci/monitor-timestamps.py
Executable file
|
|
@ -0,0 +1,116 @@
|
|||
#! /usr/bin/env python3
|
||||
|
||||
import re
|
||||
import socket
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
from datetime import datetime, timedelta, timezone
|
||||
|
||||
TIMEOUT = 10
|
||||
UTC = timezone.utc
|
||||
|
||||
STALE_THRESHOLD_MINUTES = 100
|
||||
|
||||
URLS = {
|
||||
"production": "https://docs.fedoraproject.org/en-US/docs/",
|
||||
"staging": "https://docs.stg.fedoraproject.org/en-US/docs/",
|
||||
}
|
||||
|
||||
|
||||
def fetch_page(url):
|
||||
"""Fetch the webpage and return its content."""
|
||||
try:
|
||||
request = urllib.request.Request(url)
|
||||
with urllib.request.urlopen(request, timeout=TIMEOUT) as response:
|
||||
return response.read().decode('utf-8')
|
||||
except socket.timeout:
|
||||
raise RuntimeError(f"Timeout fetching {url}")
|
||||
except urllib.error.HTTPError as e:
|
||||
raise RuntimeError(f"HTTP error {e.code} from {url}")
|
||||
except urllib.error.URLError as e:
|
||||
raise RuntimeError(f"URL error from {url}: {e.reason}")
|
||||
|
||||
|
||||
def extract_build_timestamp(html):
|
||||
"""Extract the build timestamp from the footer."""
|
||||
footer_match = re.search(r"<footer[^>]*>.*?</footer>", html, re.DOTALL)
|
||||
if not footer_match:
|
||||
raise ValueError("Could not find footer in HTML")
|
||||
footer_content = footer_match.group(0)
|
||||
|
||||
timestamp_match = re.search(
|
||||
r"Last build:\s*(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})\s+UTC",
|
||||
footer_content
|
||||
)
|
||||
if not timestamp_match:
|
||||
raise ValueError("Could not find build timestamp in footer")
|
||||
|
||||
timestamp_str = timestamp_match.group(1)
|
||||
dt = datetime.strptime(timestamp_str, "%Y-%m-%d %H:%M:%S")
|
||||
return dt.replace(tzinfo=UTC)
|
||||
|
||||
|
||||
def check_staleness(url):
|
||||
"""Check if the build timestamp is stale (older than STALE_THRESHOLD_MINUTES)."""
|
||||
html = fetch_page(url)
|
||||
build_timestamp = extract_build_timestamp(html)
|
||||
current_time = datetime.now(UTC)
|
||||
time_diff = current_time - build_timestamp
|
||||
is_stale = time_diff.total_seconds() > STALE_THRESHOLD_MINUTES * 60
|
||||
|
||||
return {
|
||||
'is_stale': is_stale,
|
||||
'build_timestamp': build_timestamp,
|
||||
'current_time': current_time,
|
||||
'time_diff': time_diff,
|
||||
}
|
||||
|
||||
|
||||
def format_time_diff(total_seconds):
|
||||
"""Format time difference in human-readable form with proper pluralization."""
|
||||
days = int(total_seconds // 86400)
|
||||
hours = int((total_seconds % 86400) // 3600)
|
||||
minutes = int((total_seconds % 3600) // 60)
|
||||
|
||||
parts = []
|
||||
if days > 0:
|
||||
parts.append(f"{days} day{'s' if days != 1 else ''}")
|
||||
if hours > 0:
|
||||
parts.append(f"{hours} hour{'s' if hours != 1 else ''}")
|
||||
if minutes > 0 and days == 0:
|
||||
parts.append(f"{minutes} minute{'s' if minutes != 1 else ''}")
|
||||
|
||||
return " and ".join(parts) if parts else "0 minutes"
|
||||
|
||||
|
||||
def main():
|
||||
results = {}
|
||||
for instance, url in URLS.items():
|
||||
try:
|
||||
staleness_result = check_staleness(url)
|
||||
results[instance] = {
|
||||
'status': "STALE" if staleness_result['is_stale'] else "RECENT",
|
||||
'build_timestamp': staleness_result['build_timestamp'],
|
||||
'current_time': staleness_result['current_time'],
|
||||
'time_diff': staleness_result['time_diff'],
|
||||
}
|
||||
except (RuntimeError, ValueError) as e:
|
||||
results[instance] = {'status': "ERROR", 'error': str(e)}
|
||||
|
||||
print("Build Status:")
|
||||
for instance, result in results.items():
|
||||
if result['status'] != "ERROR":
|
||||
time_str = format_time_diff(result['time_diff'].total_seconds())
|
||||
build_ts = result['build_timestamp'].replace(microsecond=0)
|
||||
build_ts_str = build_ts.strftime('%Y-%m-%d %H:%M:%S UTC')
|
||||
print(f" {instance.capitalize():11} {result['status']:6} {build_ts_str} ({time_str} ago)")
|
||||
else:
|
||||
print(f" {instance.capitalize():11} {result['status']:6} {result['error']}")
|
||||
|
||||
stale_instances = [instance.capitalize() for instance, result in results.items() if result['status'] != "RECENT"]
|
||||
|
||||
return 1 if stale_instances else 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Loading…
Add table
Add a link
Reference in a new issue