From 0db86f2f84825395fa1f249a4bf83ca9da69a22e Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sat, 16 Nov 2024 15:05:41 -0500 Subject: [PATCH] build: automate updating readthedocs --- Makefile | 6 ++- ci/session.py | 26 +++++------ ci/update-rtfd.py | 107 ++++++++++++++++++++++++++++++++++++++++++++++ howto.txt | 8 +--- 4 files changed, 128 insertions(+), 19 deletions(-) create mode 100644 ci/update-rtfd.py diff --git a/Makefile b/Makefile index fd510301a..de94165c9 100644 --- a/Makefile +++ b/Makefile @@ -177,10 +177,11 @@ sample_html_beta: _sample_cog_html ## Generate sample HTML report for a beta rel ##@ Kitting: making releases .PHONY: release_version edit_for_release cheats relbranch relcommit1 relcommit2 -.PHONY: kit pypi_upload test_upload kit_local build_kits +.PHONY: kit pypi_upload test_upload kit_local build_kits update_rtd .PHONY: tag bump_version REPO_OWNER = nedbat/coveragepy +RTD_PROJECT = coverage release_version: #: Update the version for a release. python igor.py release_version @@ -228,6 +229,9 @@ tag: #: Make a git tag with the version number (see howto.txt). git tag -s -m "Version $$(python setup.py --version)" $$(python setup.py --version) git push --follow-tags +update_rtd: #: Update ReadTheDocs with the versions to show + python ci/update-rtfd.py $(RTD_PROJECT) + bump_version: #: Edit sources to bump the version after a release (see howto.txt). git switch -c nedbat/bump-version python igor.py bump_version diff --git a/ci/session.py b/ci/session.py index 2d42b12bf..bab820d4f 100644 --- a/ci/session.py +++ b/ci/session.py @@ -8,23 +8,25 @@ import requests -_SESSION = None +_SESSIONS = {} -def get_session(): - """Get a properly authenticated requests Session.""" +def get_session(env="GITHUB_TOKEN"): + """Get a properly authenticated requests Session. - global _SESSION + Get the token from the `env` environment variable. + """ - if _SESSION is None: - # If GITHUB_TOKEN is in the environment, use it. - token = os.environ.get("GITHUB_TOKEN") + session = _SESSIONS.get(env) + if session is None: + token = os.environ.get(env) if token is None: - sys.exit("!! Must have a GITHUB_TOKEN") + sys.exit(f"!! Must have {env}") - _SESSION = requests.session() - _SESSION.headers["Authorization"] = f"token {token}" + session = requests.session() + session.headers["Authorization"] = f"token {token}" # requests.get() will always prefer the .netrc file even if a header # is already set. This tells it to ignore the .netrc file. - _SESSION.trust_env = False + session.trust_env = False + _SESSIONS[env] = session - return _SESSION + return session diff --git a/ci/update-rtfd.py b/ci/update-rtfd.py new file mode 100644 index 000000000..5347bdbe4 --- /dev/null +++ b/ci/update-rtfd.py @@ -0,0 +1,107 @@ +""" +Update ReadTheDocs to show and hide releases. +""" + +import re +import sys + +from session import get_session + +# How many from each level to show. +NUM_MAJORS = 3 +NUM_MINORS = 4 +OLD_MINORS = 1 +NUM_MICROS = 1 +OLD_MICROS = 1 + + +def get_all_versions(project): + """Pull all the versions for a project from ReadTheDocs.""" + versions = [] + session = get_session("RTFD_TOKEN") + + url = f"https://readthedocs.org/api/v3/projects/{project}/versions/" + while url: + resp = session.get(url) + resp.raise_for_status() + data = resp.json() + versions.extend(data["results"]) + url = data["next"] + return versions + + +def version_tuple(vstr): + """Convert a tag name into a version_info tuple.""" + m = re.fullmatch(r"[^\d]*(\d+)\.(\d+)(?:\.(\d+))?(?:([abc])(\d+))?", vstr) + if not m: + return None + return ( + int(m[1]), + int(m[2]), + int(m[3] or 0), + (m[4] or "final"), + int(m[5] or 0), + ) + + +def main(project): + """Update ReadTheDocs for the versions we want to show.""" + + # Get all the tags. Where there are dupes, keep the shorter tag for a version. + versions = get_all_versions(project) + versions.sort(key=(lambda v: len(v["verbose_name"])), reverse=True) + vdict = {} + for v in versions: + if v["type"] == "tag": + vinfo = version_tuple(v["verbose_name"]) + if vinfo and vinfo[3] == "final": + vdict[vinfo] = v + + # Decide which to show and update them. + + majors = set() + minors = set() + micros = set() + minors_to_show = NUM_MINORS + micros_to_show = NUM_MICROS + + session = get_session("RTFD_TOKEN") + version_list = sorted(vdict.items(), reverse=True) + for vi, ver in version_list: + if vi[:1] not in majors: + majors.add(vi[:1]) + minors = set() + if len(majors) > 1: + minors_to_show = OLD_MINORS + micros_to_show = OLD_MICROS + if vi[:2] not in minors: + minors.add(vi[:2]) + micros = set() + if vi[:3] not in micros: + micros.add(vi[:3]) + + show_it = ( + len(majors) <= NUM_MAJORS + and len(minors) <= minors_to_show + and len(micros) <= micros_to_show + ) + active = ver["active"] or (len(majors) <= NUM_MAJORS) + hidden = not show_it + + update = ver["active"] != active or ver["hidden"] != hidden + if update: + print(f"Updating {ver['verbose_name']} to {active=}, {hidden=}") + url = ver["_links"]["_self"] + resp = session.patch(url, data={"active": active, "hidden": hidden}) + resp.raise_for_status() + + # Set the default version. + latest = version_list[0][1] + print(f"Setting default version to {latest['slug']}") + url = latest["_links"]["project"] + resp = session.patch(url, data={"default_version": latest["slug"]}) + resp.raise_for_status() + + +if __name__ == "__main__": + main(sys.argv[1]) diff --git a/howto.txt b/howto.txt index add59d6d8..55a5e55c0 100644 --- a/howto.txt +++ b/howto.txt @@ -69,19 +69,15 @@ $ deopvars - Bump version: $ make bump_version - Update readthedocs - - @ https://readthedocs.org/projects/coverage/versions/ + - IF PRE-RELEASE - find the latest tag in the inactive list, edit it, make it active. - keep just the latest version of each x.y release, make the rest active but hidden. - pre-releases should be hidden - IF NOT PRE-RELEASE: - - @ https://readthedocs.org/dashboard/coverage/advanced/ - - change the "default version" to the new version + $ make update_rtd - Once CI passes, merge the bump-version branch to master and push it $ gshipit -- things to automate: - - readthedocs api to do the readthedocs changes - * Testing