2cb1f903afe83b27c2561fb2cff129d27878a46a
lrnassar
  Thu Feb 12 15:23:10 2026 -0800
Adding a new flag, -allowWarnings, to hubCheck so that it doesn't exit out when it raises warnings. This way it doesn't break pipelines on common warning complaints like track desc pages, refs #30723

diff --git src/hg/utils/hubCheck/hubCheck.c src/hg/utils/hubCheck/hubCheck.c
index 62a38e38f57..821e67e2d20 100644
--- src/hg/utils/hubCheck/hubCheck.c
+++ src/hg/utils/hubCheck/hubCheck.c
@@ -36,70 +36,73 @@
   "options:\n"
   "   -noTracks             - don't check remote files for tracks, just trackDb (faster)\n"
   "   -checkSettings        - check trackDb settings to spec\n"
   "   -version=[v?|url]     - version to validate settings against\n"
   "                                     (defaults to version in hub.txt, or current standard)\n"
   "   -extra=[file|url]     - accept settings in this file (or url)\n"
   "   -level=base|required  - reject settings below this support level\n"
   "   -settings             - just list settings with support level\n"
   "   -genome=genome        - only check this genome\n"
   "   -udcDir=/dir/to/cache - place to put cache for remote bigBed/bigWigs.\n"
   "                                     Will create this directory if not existing\n"
   "   -httpsCertCheck=[abort,warn,log,none] - set the ssl certificate verification mode.\n"  
   "   -httpsCertCheckDomainExceptions= - space separated list of domains to whitelist.\n"  
   "   -printMeta            - print the metadata for each track\n"
   "   -cacheTime=N          - set cache refresh time in seconds, default %d\n"
+  "   -allowWarnings        - return 0 exit code when only warnings are found (no errors)\n"
   "   -verbose=2            - output verbosely\n"
   , cacheTime
   );
 }
 
 static struct optionSpec options[] = {
    {"version", OPTION_STRING},
    {"level", OPTION_STRING},
    {"extra", OPTION_STRING},
    {"noTracks", OPTION_BOOLEAN},
    {"settings", OPTION_BOOLEAN},
    {"checkSettings", OPTION_BOOLEAN},
    {"genome", OPTION_STRING},
    {"test", OPTION_BOOLEAN},
    {"printMeta", OPTION_BOOLEAN},
    {"udcDir", OPTION_STRING},
    {"httpsCertCheck", OPTION_STRING},
    {"httpsCertCheckDomainExceptions", OPTION_STRING},
    {"specHost", OPTION_STRING},
    {"cacheTime", OPTION_INT},
+   {"allowWarnings", OPTION_BOOLEAN},
    // intentionally undocumented option for hgHubConnect
    {"htmlOut", OPTION_BOOLEAN},
    {NULL, 0},
 };
 
 struct trackHubCheckOptions
 /* How to check track hub */
     {
     boolean checkFiles;         /* check remote files exist and are correct type */
     boolean checkSettings;      /* check trackDb settings to spec */
     boolean printMeta;          /* print out the metadata for each track */
     char *version;              /* hub spec version to check */
     char *specHost;             /* server hosting hub spec */
     char *level;                /* check hub is valid to this support level */
     char *extraFile;            /* name of extra file/url with additional settings to accept */
     char *genome;               /* only check this genome */
     /* intermediate data */
     struct hash *settings;      /* supported settings for this version */
     struct hash *extra;         /* additional trackDb settings to accept */
     struct slName *suggest;     /* list of supported settings for suggesting */
+    boolean allowWarnings;      /* return 0 exit code for warnings (no errors) */
     /* hgHubConnect only */
     boolean htmlOut;            /* put special formatted text into the errors dyString */
     };
 
 struct trackHubSettingSpec
 /* Setting name and support level, from trackDbHub.html (the spec) */
     {
     struct trackHubSettingSpec *next;
     char *name;                 /* setting name */
     char *level;                /* support level (required, base, full, new, deprecated) */
     };
 
 
 /* Mini English spell-check using axt sequence alignment code!  From JK
  * Works in this context when thresholded high.  */
@@ -697,32 +700,33 @@
 
         // check that the subtrack does not have any bogus subgroups that don't exist in the parent
         for (i = 0; i < membership->count; i++)
             {
             char *subgroupName = membership->subgroups[i];
             if (!subgroupingExists(tdb->parent, subgroupName))
                 {
                 warn("subtrack \"%s\" declares subgroup \"%s\", but the parent composite does not define this subgroup. Check the 'subGroup' lines in the parent stanza for a matching group name.", subtrackName, subgroupName);
                 }
             }
         }
     }
 errCatchEnd(errCatch);
 if (errCatch->gotError || errCatch->gotWarning)
     {
-    retVal = 1;
     trackDbErr(errors, errCatch->message->string, genome, tdb, options->htmlOut);
+    if (errCatch->gotError || !options->allowWarnings)
+        retVal = 1;
     }
 errCatchFree(&errCatch);
 
 return retVal;
 }
 
 int hubCheckCompositeSettings(struct trackHubGenome *genome, struct trackDb *tdb, struct dyString *errors, struct trackHubCheckOptions *options)
 /* Check composite level settings like subgroups, dimensions, sortOrder, etc.
  * Note that this function returns an int because we want to warn about all errors in a single
  * composite stanza rather than errAbort on the first error */
 {
 int retVal = 0;
 
 // for now all the combination style stanzas can get checked here, but
 // in the future they might need their own routines
@@ -737,30 +741,31 @@
     (void)membersForAllSubGroupsGet(tdb, NULL);
 
     // check that multiWigs have > 1 track
     char *multiWigSetting = trackDbLocalSetting(tdb, "container");
     if (multiWigSetting && slCount(tdb->subtracks) < 2)
         {
         errAbort("container multiWig %s has only one subtrack. multiWigs must have more than one subtrack", tdb->track);
         }
     }
 errCatchEnd(errCatch);
 
 if (errCatch->gotError || errCatch->gotWarning)
     {
     // don't add a new line because one will already be inserted by the errCatch->message
     trackDbErr(errors, errCatch->message->string, genome, tdb, options->htmlOut);
+    if (errCatch->gotError || !options->allowWarnings)
         retVal = 1;
     }
 
 return retVal;
 }
 
 void hubCheckParentsAndChildren(struct trackDb *tdb)
 /* Check that a single trackDb stanza has the correct parent and subtrack pointers */
 {
 if (tdbIsSuper(tdb) || tdbIsComposite(tdb) || tdbIsCompositeView(tdb) || tdbIsContainer(tdb))
     {
     if (tdb->subtracks == NULL)
         {
         errAbort("Track \"%s\" is declared superTrack, compositeTrack, view or "
             "container, but has no subtracks", tdb->track);
@@ -978,30 +983,31 @@
 if (errCatchStart(errCatch))
     {
     if (tdb->errMessage)  // if we found any errors when first reading in the trackDb
         errAbort("%s",tdb->errMessage);
 
     hubCheckParentsAndChildren(tdb);
     if (trackIsContainer)
         retVal |= hubCheckCompositeSettings(genome, tdb, errors, options);
 
     if (tdbIsSubtrack(tdb))
         retVal |= hubCheckSubtrackSettings(genome, tdb, errors, options);
 
     // check that type line is syntactically correct regardless of
     // if we actually want to check the data file itself
     boolean foundTypeError = checkTypeLine(genome, tdb, errors, options);
+    retVal |= foundTypeError;
 
     // No point in checking the data file if the type setting is incorrect,
     // since hubCheckBigDataUrl will error out early with a less clear message
     // if the type line is messed up. This has the added benefit of providing
     // consistent messaging on command line interface vs web interface
     if (!foundTypeError && options->checkFiles)
         hubCheckBigDataUrl(hub, genome, tdb);
 
 
     checkViewLimitsSettings(tdb);
 
     if (!sameString(tdb->track, "cytoBandIdeo"))
         {
         trackHubAddDescription(genome->trackDbFile, tdb);
         if (!tdb->html)
@@ -1012,32 +1018,33 @@
     if (!trackIsContainer && sameString(trackDbRequiredSetting(tdb, "type"), "bigWig"))
         {
         char *autoScaleSetting = trackDbLocalSetting(tdb, "autoScale");
         if (autoScaleSetting && !sameString(autoScaleSetting, "off") && !sameString(autoScaleSetting, "on"))
             {
             errAbort("track \"%s\" uses 'autoScale %s', but individual bigWig tracks only accept "
                     "'autoScale on' or 'autoScale off'. To use 'autoScale %s', move that setting to the "
                     "parent composite stanza instead.",
                     trackHubSkipHubName(tdb->track), autoScaleSetting, autoScaleSetting);
             }
         }
     }
 errCatchEnd(errCatch);
 if (errCatch->gotError || errCatch->gotWarning)
     {
-    retVal = 1;
     trackDbErr(errors, errCatch->message->string, genome, tdb, options->htmlOut);
+    if (errCatch->gotError || !options->allowWarnings)
+        retVal = 1;
     if (errCatch->gotError)
         trackDbErrorCount += 1;
     }
 errCatchFree(&errCatch);
 
 if (options->htmlOut)
     {
     if (trackIsContainer)
         {
         for (tempTdb = tdb->subtracks; tempTdb != NULL; tempTdb = tempTdb->next)
             {
             char subtrackName[512];
             safef(subtrackName, sizeof(subtrackName), "%s_%s", trackHubSkipHubName(genome->name), trackHubSkipHubName(tempTdb->track));
             textName = trackHubSkipHubName(tempTdb->longLabel);
             dyStringPrintf(errors, "%s,", makeFolderObjectString(subtrackName, textName, idName, "TRACK", TRUE, retVal));
@@ -1086,31 +1093,31 @@
             warn("warning: missing htmlPath setting for assembly hub '%s'", genome->name);
         else if (!extFileExists(htmlPath))
             warn("warning: '%s' htmlPath file does not exist or is not accessible: '%s'", genome->name, htmlPath);
         }
     boolean foundFirstGenome = FALSE;
     tdbList = trackHubTracksForGenome(hub, genome, NULL, &foundFirstGenome);
     tdbList = trackDbLinkUpGenerations(tdbList);
     tdbList = trackDbPolishAfterLinkup(tdbList, genome->name);
     trackHubPolishTrackNames(hub, tdbList);
     }
 errCatchEnd(errCatch);
 if (errCatch->gotError || errCatch->gotWarning)
     {
     openedGenome = TRUE;
     genomeErr(errors, errCatch->message->string, hub, genome, options->htmlOut);
-    if (errCatch->gotError || errCatch->gotWarning)
+    if (errCatch->gotError || !options->allowWarnings)
         genomeErrorCount += 1;
     }
 errCatchFree(&errCatch);
 
 verbose(2, "%d tracks in %s\n", slCount(tdbList), genome->name);
 struct trackDb *tdb;
 int tdbCheckVal;
 static struct dyString *tdbDyString = NULL;
 if (!tdbDyString)
     tdbDyString = dyStringNew(0);
 
 // build up the track results list, keep track of number of errors, then
 // open up genomes folder
 char *genomeName = trackHubSkipHubName(genome->name);
 for (tdb = tdbList; tdb != NULL; tdb = tdb->next)
@@ -1178,32 +1185,33 @@
 struct dyString *hubErrors = dyStringNew(0);
 int retVal = 0;
 
 if (errCatchStart(errCatch))
     {
     hub = trackHubOpen(hubUrl, "");
     char *descUrl = hub->descriptionUrl;
     if (descUrl == NULL)
         warn("warning: missing hub overview description page. Add a 'descriptionUrl hubDescription.html' line to hub.txt.");
     else if (!extFileExists(descUrl))
         warn("warning: descriptionUrl '%s' could not be accessed. Verify the file exists at the specified path and is publicly readable.", hub->descriptionUrl);
     }
 errCatchEnd(errCatch);
 if (errCatch->gotError || errCatch->gotWarning)
     {
-    retVal = 1;
     hubErr(hubErrors, errCatch->message->string, hub, options->htmlOut);
+    if (errCatch->gotError || !options->allowWarnings)
+        retVal = 1;
 
     if (options->htmlOut)
         {
         if (hub && hub->shortLabel)
             {
             dyStringPrintf(errors, "trackData['#'] = [%s,",
                 makeFolderObjectString(hub->shortLabel, "Hub Errors", "#",
                     "Click to open node", TRUE, TRUE));
             }
         else
             {
             dyStringPrintf(errors, "trackData['#'] = [%s,",
                 makeFolderObjectString("Hub Error", "Hub Errors", "#",
                     "Click to open node", TRUE, TRUE));
             }
@@ -1313,30 +1321,31 @@
 optionInit(&argc, argv, options);
 
 if (argc != 2 && !optionExists("settings"))
     usage();
 
 struct trackHubCheckOptions *checkOptions = NULL;
 AllocVar(checkOptions);
 
 checkOptions->specHost = (optionExists("test") ? "genome-test.soe.ucsc.edu" : "genome.ucsc.edu");
 checkOptions->specHost = optionVal("specHost", checkOptions->specHost);
 
 checkOptions->printMeta = optionExists("printMeta");
 checkOptions->checkFiles = !optionExists("noTracks");
 checkOptions->checkSettings = optionExists("checkSettings");
 checkOptions->genome = optionVal("genome", NULL);
+checkOptions->allowWarnings = optionExists("allowWarnings");
 
 struct trackHubSettingSpec *setting = NULL;
 AllocVar(setting);
 setting->level = optionVal("level", "all");
 if (trackHubSettingLevel(setting) < 0)
     {
     fprintf(stderr, "ERROR: Unrecognized support level %s\n\n", setting->level);
     usage();
     }
 checkOptions->level = setting->level;
 
 char *version = NULL;
 if (optionExists("version"))
     version = optionVal("version", NULL);
 checkOptions->version = version;
@@ -1375,35 +1384,33 @@
     {
     setenv("https_cert_check_domain_exceptions", httpsCertCheckDomainExceptions, 1);
     }
 
 knetUdcInstall();  // make the htslib library use udc
 
 if (optionExists("settings"))
     {
     showSettings(checkOptions);
     return 0;
     }
 
 // hgHubConnect specific option for generating a jstree of the hub errors
 checkOptions->htmlOut = optionExists("htmlOut");
 struct dyString *errors = dyStringNew(1024);
-if (trackHubCheck(argv[1], checkOptions, errors) || checkOptions->htmlOut)
-    {
-    if (checkOptions->htmlOut) // just dump errors string to stdout
+int checkResult = trackHubCheck(argv[1], checkOptions, errors);
+if (checkOptions->htmlOut)
     {
     printf("%s", errors->string);
-        return 1;
+    return checkResult ? 1 : 0;
     }
-    else
+else if (errors->stringSize > 0)
     {
     // uniquify and count errors
     struct slName *errs = slNameListFromString(errors->string, '\n');
     slUniqify(&errs, slNameCmp, slNameFree);
     int errCount = slCount(errs);
     printf("Found %d problem%s:\n", errCount, errCount == 1 ? "" : "s");
     printf("%s\n", slNameListToString(errs, '\n'));
-        return 1;
-        }
+    return checkResult ? 1 : 0;
     }
 return 0;
 }