c2087cb0ed189d946439a090d43d2dba5d71a9eb
lrnassar
  Wed Aug 5 17:28:22 2026 -0700
Fix error containment and a wasted retry sleep in the VCEP version notifier per CR feedback. refs #37795

ClinGen's version field is a free-form string, so an unparseable value raised
ValueError past the per-VCEP except RuntimeError in checkVcep. That aborted the
whole run, skipping the remaining VCEPs and mailing a traceback instead of the
report. normalizeVersion now reports the offending string as a RuntimeError, and
the version comparison moved inside the try so it is caught, keeping the damage
to a single failure line.

fetchUrl also slept 30 seconds after its final failed attempt, which could not
help. It now only sleeps between attempts.

diff --git src/hg/utils/otto/vcepVersions/checkVcepVersions.py src/hg/utils/otto/vcepVersions/checkVcepVersions.py
index be80d370ca2..8af0f3c687e 100755
--- src/hg/utils/otto/vcepVersions/checkVcepVersions.py
+++ src/hg/utils/otto/vcepVersions/checkVcepVersions.py
@@ -43,45 +43,53 @@
     },
     "InSiGHT Lynch Syndrome VCEP": {
         "hubUrl": "https://hgdownload.soe.ucsc.edu/hubs/insight/insight.html",
         "hubRegex": r"<h1>InSiGHT specs\s+(\d+(?:\.\d+)+)</h1>",
         "affiliation": "50099",
         "genes": ["MLH1", "MSH2", "MSH6", "PMS2"],
     },
 }
 
 cspecUrl = "https://cspec.genome.network/cspec/ui/svi/affiliation/"
 
 
 def fetchUrl(url):
     """Fetch url and return its text. Retries a few times so a transient network
     blip does not turn into a false alarm, then raises."""
+    attempts = 5
     lastErr = None
-    for attempt in range(5):
+    for attempt in range(attempts):
         try:
             with urllib.request.urlopen(url, timeout=120) as resp:
                 return resp.read().decode("utf-8", "replace")
         except (urllib.error.URLError, OSError) as e:
             lastErr = e
+            if attempt < attempts - 1:
                 time.sleep(30)
-    raise RuntimeError("could not fetch " + url + " after 5 attempts: " + str(lastErr))
+    raise RuntimeError("could not fetch " + url + " after " + str(attempts) +
+                       " attempts: " + str(lastErr))
 
 
 def normalizeVersion(version):
     """Turn a dotted version into a tuple for comparison, dropping trailing
-    zeroes so ClinGen's '2.0' matches our page's '2.0.0'."""
+    zeroes so ClinGen's '2.0' matches our page's '2.0.0'. ClinGen's version is a
+    free-form string, so an unparseable one is reported rather than raising past
+    the per-VCEP error handling and killing the rest of the run."""
+    try:
         parts = [int(p) for p in version.split(".")]
+    except ValueError:
+        raise RuntimeError("could not parse version string '" + version + "'")
     while len(parts) > 1 and parts[-1] == 0:
         parts.pop()
     return tuple(parts)
 
 
 def getOurVersion(config):
     """Scrape the version we publish from a hub description page."""
     html = fetchUrl(config["hubUrl"])
     match = re.search(config["hubRegex"], html)
     if match is None:
         raise RuntimeError("no version found on " + config["hubUrl"] +
                            " (page wording changed? regex: " + config["hubRegex"] + ")")
     return match.group(1)
 
 
@@ -105,36 +113,36 @@
             if gene.get("label") in config["genes"]:
                 versions[gene["label"]] = svi["version"]
 
     missing = [g for g in config["genes"] if g not in versions]
     if missing:
         raise RuntimeError("no released CSpec spec for " + ", ".join(missing) + " at " + url)
     return versions
 
 
 def checkVcep(name, config, report):
     """Compare one VCEP and append any mismatch to report. Returns True on
     success, False if the check itself could not be completed."""
     try:
         ourVersion = getOurVersion(config)
         clinGenVersions = getClinGenVersions(config)
+        stale = {g: v for g, v in clinGenVersions.items()
+                 if normalizeVersion(v) != normalizeVersion(ourVersion)}
     except RuntimeError as e:
         report.append(name + ": check failed: " + str(e))
         return False
 
-    stale = {g: v for g, v in clinGenVersions.items()
-             if normalizeVersion(v) != normalizeVersion(ourVersion)}
     if stale:
         geneList = ", ".join(g + "=" + stale[g] for g in sorted(stale))
         report.append(name + " is out of date.")
         report.append("  our hub page: " + ourVersion + "  (" + config["hubUrl"] + ")")
         report.append("  ClinGen CSpec: " + geneList +
                       "  (" + cspecUrl + config["affiliation"] + ")")
     return True
 
 
 def main():
     report = []
     ok = True
     for name in sorted(vcepConfig):
         if not checkVcep(name, vcepConfig[name], report):
             ok = False