31
31
32
32
# Define constants for error strings to make checking against them more robust:
33
33
ERROR_ENABLE_TRAVIS = "Unable to enable Travis build"
34
+ ERROR_README_DOWNLOAD_FAILED = "Failed to download README"
35
+ ERROR_README_IMAGE_MISSING_ALT = "README image missing alt text"
36
+ ERROR_README_DUPLICATE_ALT_TEXT = "README has duplicate alt text"
37
+ ERROR_README_MISSING_DISCORD_BADGE = "README missing Discord badge"
38
+ ERROR_README_MISSING_RTD_BADGE = "README missing ReadTheDocs badge"
39
+ ERROR_README_MISSING_TRAVIS_BADGE = "README missing Travis badge"
34
40
ERROR_MISMATCHED_READTHEDOCS = "Mismatched readthedocs.yml"
35
41
ERROR_MISSING_EXAMPLE_FILES = "Missing .py files in examples folder"
36
42
ERROR_MISSING_EXAMPLE_FOLDER = "Missing examples folder"
37
43
ERROR_MISSING_LIBRARIANS = "Likely missing CircuitPythonLibrarians team."
38
44
ERROR_MISSING_LICENSE = "Missing license."
39
45
ERROR_MISSING_LINT = "Missing lint config"
46
+ ERROR_MISSING_CODE_OF_CONDUCT = "Missing CODE_OF_CONDUCT.md"
47
+ ERROR_MISSING_README_RST = "Missing README.rst"
40
48
ERROR_MISSING_READTHEDOCS = "Missing readthedocs.yml"
41
49
ERROR_MISSING_TRAVIS_CONFIG = "Missing .travis.yml"
42
50
ERROR_NOT_IN_BUNDLE = "Not in bundle."
43
51
ERROR_OLD_TRAVIS_CONFIG = "Old travis config"
52
+ ERROR_TRAVIS_DOESNT_KNOW_REPO = "Travis doesn't know of repo"
44
53
ERROR_TRAVIS_ENV = "Unable to read Travis env variables"
45
54
ERROR_TRAVIS_GITHUB_TOKEN = "Unable to find or create (no auth) GITHUB_TOKEN env variable"
46
55
ERROR_TRAVIS_TOKEN_CREATE = "Token creation failed"
49
58
ERRRO_UNABLE_PULL_REPO_EXAMPLES = "Unable to retrieve examples folder contents"
50
59
ERROR_WIKI_DISABLED = "Wiki should be disabled"
51
60
ERROR_ONLY_ALLOW_MERGES = "Only allow merges, disallow rebase and squash"
61
+ ERROR_RTD_SUBPROJECT_FAILED = "Failed to list CircuitPython subprojects on ReadTheDocs"
62
+ ERROR_RTD_SUBPROJECT_MISSING = "ReadTheDocs missing as a subproject on CircuitPython"
63
+ ERROR_RTD_ADABOT_MISSING = "ReadTheDocs project missing adabot as owner"
64
+ ERROR_RTD_VALID_VERSIONS_FAILED = "Failed to fetch ReadTheDocs valid versions"
65
+ ERROR_RTD_FAILED_TO_LOAD_BUILDS = "Unable to load builds webpage"
66
+ ERROR_RTD_FAILED_TO_LOAD_BUILD_INFO = "Failed to load build info"
67
+ ERROR_RTD_OUTPUT_HAS_WARNINGS = "ReadTheDocs latest build has warnings and/or errors"
68
+ ERROR_RTD_AUTODOC_FAILED = "Autodoc failed on ReadTheDocs. (Likely need to automock an import.)"
69
+ ERROR_RTD_SPHINX_FAILED = "Sphinx missing files"
70
+ ERROR_GITHUB_RELEASE_FAILED = "Failed to fetch latest release from GitHub"
71
+ ERROR_RTD_MISSING_LATEST_RELEASE = "ReadTheDocs missing the latest release. (Likely the webhook isn't set up correctly.)"
72
+
73
+ # These are warnings or errors that sphinx generate that we're ok ignoring.
74
+ RTD_IGNORE_NOTICES = ("WARNING: html_static_path entry" , "WARNING: nonlocal image URI found:" )
52
75
53
76
# Constant for bundle repo name.
54
77
BUNDLE_REPO_NAME = "Adafruit_CircuitPython_Bundle"
57
80
# full name on Github (like Adafruit_CircuitPython_Bundle).
58
81
BUNDLE_IGNORE_LIST = [BUNDLE_REPO_NAME ]
59
82
83
+ # Cache CircuitPython's subprojects on ReadTheDocs so its not fetched every repo check.
84
+ rtd_subprojects = None
60
85
61
86
def parse_gitmodules (input_text ):
62
87
"""Parse a .gitmodules file and return a list of all the git submodules
@@ -235,10 +260,47 @@ def validate_repo_state(repo):
235
260
# bundle itself and possibly
236
261
# other repos.
237
262
errors .append (ERROR_NOT_IN_BUNDLE )
238
- if full_repo ["allow_squash_merge" ] or full_repo ["allow_rebase_merge" ]:
263
+ if "allow_squash_merge" not in full_repo or full_repo ["allow_squash_merge" ] or full_repo ["allow_rebase_merge" ]:
239
264
errors .append (ERROR_ONLY_ALLOW_MERGES )
240
265
return errors
241
266
267
+ def validate_readme (repo , download_url ):
268
+ # We use requests because file contents are hosted by githubusercontent.com, not the API domain.
269
+ contents = requests .get (download_url )
270
+ if not contents .ok :
271
+ return [ERROR_README_DOWNLOAD_FAILED ]
272
+
273
+ errors = []
274
+ badges = {}
275
+ current_image = None
276
+ for line in contents .text .split ("\n " ):
277
+ if line .startswith (".. image" ):
278
+ current_image = {}
279
+
280
+ if line .strip () == "" and current_image is not None :
281
+ if "alt" not in current_image :
282
+ errors .append (ERROR_README_IMAGE_MISSING_ALT )
283
+ elif current_image ["alt" ] in badges :
284
+ errors .append (ERROR_README_DUPLICATE_ALT_TEXT )
285
+ else :
286
+ badges [current_image ["alt" ]] = current_image
287
+ current_image = None
288
+ elif current_image is not None :
289
+ first , second , value = line .split (":" , 2 )
290
+ key = first .strip (" ." ) + second .strip ()
291
+ current_image [key ] = value .strip ()
292
+
293
+ if "Discord" not in badges :
294
+ errors .append (ERROR_README_MISSING_DISCORD_BADGE )
295
+
296
+ if "Documentation Status" not in badges :
297
+ errors .append (ERROR_README_MISSING_RTD_BADGE )
298
+
299
+ if "Build Status" not in badges :
300
+ errors .append (ERROR_README_MISSING_TRAVIS_BADGE )
301
+
302
+ return errors
303
+
242
304
def validate_contents (repo ):
243
305
"""Validate the contents of a repository meets current CircuitPython
244
306
criteria (within reason, functionality checks are not possible). Expects
@@ -262,6 +324,19 @@ def validate_contents(repo):
262
324
if ".pylintrc" not in files :
263
325
errors .append (ERROR_MISSING_LINT )
264
326
327
+ if "CODE_OF_CONDUCT.md" not in files :
328
+ errors .append (ERROR_MISSING_CODE_OF_CONDUCT )
329
+
330
+ if "README.rst" not in files :
331
+ errors .append (ERROR_MISSING_README_RST )
332
+ else :
333
+ readme_info = None
334
+ for f in content_list :
335
+ if f ["name" ] == "README.rst" :
336
+ readme_info = f
337
+ break
338
+ errors .extend (validate_readme (repo , readme_info ["download_url" ]))
339
+
265
340
if ".travis.yml" in files :
266
341
file_info = content_list [files .index (".travis.yml" )]
267
342
if file_info ["size" ] > 1000 :
@@ -307,7 +382,7 @@ def validate_travis(repo):
307
382
if not result .ok :
308
383
#print(result, result.request.url, result.request.headers)
309
384
#print(result.text)
310
- return ["Travis error with repo:" , repo [ "full_name" ] ]
385
+ return [ERROR_TRAVIS_DOESNT_KNOW_REPO ]
311
386
result = result .json ()
312
387
if not result ["active" ]:
313
388
activate = travis .post (repo_url + "/activate" )
@@ -354,6 +429,91 @@ def validate_travis(repo):
354
429
return [ERROR_TRAVIS_GITHUB_TOKEN ]
355
430
return []
356
431
432
+ def validate_readthedocs (repo ):
433
+ if not (repo ["owner" ]["login" ] == "adafruit" and
434
+ repo ["name" ].startswith ("Adafruit_CircuitPython" )):
435
+ return []
436
+ if repo ["name" ] in BUNDLE_IGNORE_LIST :
437
+ return []
438
+ global rtd_subprojects
439
+ if not rtd_subprojects :
440
+ rtd_response = requests .get ("https://readthedocs.org/api/v2/project/74557/subprojects/" )
441
+ if not rtd_response .ok :
442
+ return [ERROR_RTD_SUBPROJECT_FAILED ]
443
+ rtd_subprojects = {}
444
+ for subproject in rtd_response .json ()["subprojects" ]:
445
+ rtd_subprojects [sanitize_url (subproject ["repo" ])] = subproject
446
+
447
+ repo_url = sanitize_url (repo ["clone_url" ])
448
+ if repo_url not in rtd_subprojects :
449
+ return [ERROR_RTD_SUBPROJECT_MISSING ]
450
+
451
+ errors = []
452
+ subproject = rtd_subprojects [repo_url ]
453
+
454
+ if 105398 not in subproject ["users" ]:
455
+ errors .append (ERROR_RTD_ADABOT_MISSING )
456
+
457
+ valid_versions = requests .get (
458
+ "https://readthedocs.org/api/v2/project/{}/valid_versions/" .format (subproject ["id" ]))
459
+ if not valid_versions .ok :
460
+ errors .append (ERROR_RTD_VALID_VERSIONS_FAILED )
461
+ else :
462
+ valid_versions = valid_versions .json ()
463
+ latest_release = github .get ("/repos/{}/releases/latest" .format (repo ["full_name" ]))
464
+ if not latest_release .ok :
465
+ errors .append (ERROR_GITHUB_RELEASE_FAILED )
466
+ else :
467
+ if latest_release .json ()["tag_name" ] not in valid_versions ["flat" ]:
468
+ errors .append (ERROR_RTD_MISSING_LATEST_RELEASE )
469
+
470
+ # There is no API which gives access to a list of builds for a project so we parse the html
471
+ # webpage.
472
+ builds_webpage = requests .get (
473
+ "https://readthedocs.org/projects/{}/builds/" .format (subproject ["slug" ]))
474
+ if not builds_webpage .ok :
475
+ errors .append (ERROR_RTD_FAILED_TO_LOAD_BUILDS )
476
+ else :
477
+ for line in builds_webpage .text .split ("\n " ):
478
+ if "<div id=\" build-" in line :
479
+ build_id = line .split ("\" " )[1 ][len ("build-" ):]
480
+ # We only validate the most recent build. So, break when the first is found.
481
+ break
482
+ build_info = requests .get ("https://readthedocs.org/api/v2/build/{}/" .format (build_id ))
483
+ if not build_info .ok :
484
+ errors .append (ERROR_RTD_FAILED_TO_LOAD_BUILD_INFO )
485
+ else :
486
+ build_info = build_info .json ()
487
+ output_ok = True
488
+ autodoc_ok = True
489
+ sphinx_ok = True
490
+ for command in build_info ["commands" ]:
491
+ if command ["command" ].endswith ("_build/html" ):
492
+ for line in command ["output" ].split ("\n " ):
493
+ if "... " in line :
494
+ _ , line = line .split ("... " )
495
+ if "WARNING" in line or "ERROR" in line :
496
+ if not line .startswith (("WARNING" , "ERROR" )):
497
+ line = line .split (" " , 1 )[1 ]
498
+ if not line .startswith (RTD_IGNORE_NOTICES ):
499
+ output_ok = False
500
+ print ("error:" , line )
501
+ elif line .startswith ("ImportError" ):
502
+ print (line )
503
+ autodoc_ok = False
504
+ elif line .startswith ("sphinx.errors" ) or line .startswith ("SphinxError" ):
505
+ print (line )
506
+ sphinx_ok = False
507
+ break
508
+ if not output_ok :
509
+ errors .append (ERROR_RTD_OUTPUT_HAS_WARNINGS )
510
+ if not autodoc_ok :
511
+ errors .append (ERROR_RTD_AUTODOC_FAILED )
512
+ if not sphinx_ok :
513
+ errors .append (ERROR_RTD_SPHINX_FAILED )
514
+
515
+ return errors
516
+
357
517
def validate_repo (repo ):
358
518
"""Run all the current validation functions on the provided repository and
359
519
return their results as a list of string errors.
@@ -453,7 +613,7 @@ def print_circuitpython_download_stats():
453
613
# Functions to run on repositories to validate their state. By convention these
454
614
# return a list of string errors for the specified repository (a dictionary
455
615
# of Github API repository object state).
456
- validators = [validate_repo_state , validate_travis , validate_contents ]
616
+ validators = [validate_repo_state , validate_travis , validate_contents , validate_readthedocs ]
457
617
# Submodules inside the bundle (result of get_bundle_submodules)
458
618
bundle_submodules = []
459
619
0 commit comments