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"
47
56
ERROR_UNABLE_PULL_REPO_CONTENTS = "Unable to pull repo contents"
48
57
ERROR_UNABLE_PULL_REPO_DETAILS = "Unable to pull repo details"
49
58
ERRRO_UNABLE_PULL_REPO_EXAMPLES = "Unable to retrieve examples folder contents"
50
59
ERROR_WIKI_DISABLED = "Wiki should be disabled"
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:" )
51
75
52
76
# Constant for bundle repo name.
53
77
BUNDLE_REPO_NAME = "Adafruit_CircuitPython_Bundle"
56
80
# full name on Github (like Adafruit_CircuitPython_Bundle).
57
81
BUNDLE_IGNORE_LIST = [BUNDLE_REPO_NAME ]
58
82
83
+ # Cache CircuitPython's subprojects on ReadTheDocs so its not fetched every repo check.
84
+ rtd_subprojects = None
59
85
60
86
def parse_gitmodules (input_text ):
61
87
"""Parse a .gitmodules file and return a list of all the git submodules
@@ -234,6 +260,45 @@ def validate_repo_state(repo):
234
260
# bundle itself and possibly
235
261
# other repos.
236
262
errors .append (ERROR_NOT_IN_BUNDLE )
263
+ if "allow_squash_merge" not in full_repo or full_repo ["allow_squash_merge" ] or full_repo ["allow_rebase_merge" ]:
264
+ errors .append (ERROR_ONLY_ALLOW_MERGES )
265
+ return errors
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
+
237
302
return errors
238
303
239
304
def validate_contents (repo ):
@@ -259,6 +324,19 @@ def validate_contents(repo):
259
324
if ".pylintrc" not in files :
260
325
errors .append (ERROR_MISSING_LINT )
261
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
+
262
340
if ".travis.yml" in files :
263
341
file_info = content_list [files .index (".travis.yml" )]
264
342
if file_info ["size" ] > 1000 :
@@ -304,7 +382,7 @@ def validate_travis(repo):
304
382
if not result .ok :
305
383
#print(result, result.request.url, result.request.headers)
306
384
#print(result.text)
307
- return ["Travis error with repo:" , repo [ "full_name" ] ]
385
+ return [ERROR_TRAVIS_DOESNT_KNOW_REPO ]
308
386
result = result .json ()
309
387
if not result ["active" ]:
310
388
activate = travis .post (repo_url + "/activate" )
@@ -351,6 +429,91 @@ def validate_travis(repo):
351
429
return [ERROR_TRAVIS_GITHUB_TOKEN ]
352
430
return []
353
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
+
354
517
def validate_repo (repo ):
355
518
"""Run all the current validation functions on the provided repository and
356
519
return their results as a list of string errors.
@@ -450,7 +613,7 @@ def print_circuitpython_download_stats():
450
613
# Functions to run on repositories to validate their state. By convention these
451
614
# return a list of string errors for the specified repository (a dictionary
452
615
# of Github API repository object state).
453
- validators = [validate_repo_state , validate_travis , validate_contents ]
616
+ validators = [validate_repo_state , validate_travis , validate_contents , validate_readthedocs ]
454
617
# Submodules inside the bundle (result of get_bundle_submodules)
455
618
bundle_submodules = []
456
619
@@ -497,8 +660,6 @@ def print_circuitpython_download_stats():
497
660
repos_by_error [error ] = []
498
661
repos_by_error [error ].append (repo ["html_url" ])
499
662
gather_insights (repo , insights , since )
500
- circuitpython_repo = github .get ("/repos/adafruit/circuitpython" ).json ()
501
- gather_insights (circuitpython_repo , insights , since )
502
663
print ("State of CircuitPython + Libraries" )
503
664
print ("* {} pull requests merged" .format (insights ["merged_prs" ]))
504
665
authors = insights ["pr_merged_authors" ]
@@ -520,12 +681,13 @@ def print_circuitpython_download_stats():
520
681
# print("- [ ] [{0}](https://github.com/{1})".format(repo["name"], repo["full_name"]))
521
682
print ("{} out of {} repos need work." .format (need_work , len (repos )))
522
683
523
- list_repos_for_errors = [ERROR_WIKI_DISABLED , ERROR_MISSING_LIBRARIANS ,
524
- ERROR_ENABLE_TRAVIS , ERROR_NOT_IN_BUNDLE ]
684
+ list_repos_for_errors = [ERROR_NOT_IN_BUNDLE ]
685
+
525
686
for error in repos_by_error :
526
687
if len (repos_by_error [error ]) == 0 :
527
688
continue
528
689
print ()
529
- print (error , "- {}" .format (len (repos_by_error [error ])))
530
- if error in list_repos_for_errors :
690
+ error_count = len (repos_by_error [error ])
691
+ print ("{} - {}" .format (error , error_count ))
692
+ if error_count <= 5 or error in list_repos_for_errors :
531
693
print ("\n " .join (repos_by_error [error ]))
0 commit comments