├── docs ├── .gitignore ├── requirements.txt ├── tutorials │ ├── tutorial.py │ └── another-tutorial.py ├── Makefile ├── conf.py └── index.rst ├── .gitattributes ├── compiled └── .gitignore ├── .gitignore ├── .github ├── workflows │ ├── requirements.txt │ ├── compile.yml │ ├── docs.yml │ └── publish.yml └── dependabot.yml ├── readthedocs.yml ├── MANIFEST.in ├── src ├── rtds_action │ ├── __init__.py │ └── rtds_action.py └── js │ └── index.js ├── action.yml ├── pyproject.toml ├── package.json ├── LICENSE ├── setup.py └── README.md /docs/.gitignore: -------------------------------------------------------------------------------- 1 | _build 2 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | compiled/* linguist-generated 2 | -------------------------------------------------------------------------------- /compiled/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | !index.js 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.ipynb 2 | node_modules 3 | rtds_action_version.py 4 | build 5 | dist 6 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | sphinx 2 | nbsphinx 3 | sphinx_material 4 | recommonmark 5 | sphinx_copybutton -------------------------------------------------------------------------------- /.github/workflows/requirements.txt: -------------------------------------------------------------------------------- 1 | numpy 2 | matplotlib 3 | jupyter 4 | nbconvert 5 | jupytext 6 | scikit-learn 7 | -------------------------------------------------------------------------------- /readthedocs.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | python: 4 | version: 3.7 5 | install: 6 | - requirements: docs/requirements.txt 7 | - method: pip 8 | path: . 9 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "monthly" 7 | 8 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.rst LICENSE *.toml 2 | 3 | exclude *.yml dist/*.js .gitignore *.json 4 | recursive-exclude docs * 5 | recursive-exclude .github * 6 | recursive-exclude src/js * 7 | 8 | -------------------------------------------------------------------------------- /src/rtds_action/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | __all__ = ["setup", "__version__"] 4 | 5 | from .rtds_action import setup 6 | from .rtds_action_version import __version__ 7 | 8 | __uri__ = "https://github.com/dfm/rtds-action" 9 | __author__ = "Daniel Foreman-Mackey" 10 | __email__ = "foreman.mackey@gmail.com" 11 | __license__ = "MIT" 12 | __description__ = "Interface GitHub Actions and ReadTheDocs" 13 | -------------------------------------------------------------------------------- /action.yml: -------------------------------------------------------------------------------- 1 | name: "ReadTheDocs Trigger" 2 | description: "Trigger a build on ReadTheDocs" 3 | inputs: 4 | webhook_url: 5 | description: "The webhook URL from ReadTheDocs. This will have the form: https://readthedocs.org/api/v2/webhook/{webhook_id}/" 6 | required: true 7 | webhook_token: 8 | description: "The webhook token from ReadTheDocs" 9 | required: true 10 | commit_ref: 11 | description: "The reference for the current commit" 12 | required: true 13 | runs: 14 | using: "node16" 15 | main: "compiled/index.js" 16 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools>=40.6.0", "wheel", "setuptools_scm"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [tool.black] 6 | line-length = 79 7 | exclude = ''' 8 | /( 9 | \.eggs 10 | | \.git 11 | | \.hg 12 | | \.mypy_cache 13 | | \.tox 14 | | \.venv 15 | | _build 16 | | buck-out 17 | | build 18 | | dist 19 | | docs/tutorials 20 | )/ 21 | ''' 22 | 23 | [tool.isort] 24 | skip_glob = ["docs/tutorials/*.py"] 25 | line_length = 79 26 | multi_line_output = 3 27 | include_trailing_comma = true 28 | force_grid_wrap = 0 29 | use_parentheses = true 30 | -------------------------------------------------------------------------------- /docs/tutorials/tutorial.py: -------------------------------------------------------------------------------- 1 | # --- 2 | # jupyter: 3 | # jupytext: 4 | # text_representation: 5 | # extension: .py 6 | # format_name: light 7 | # format_version: '1.5' 8 | # jupytext_version: 1.5.1 9 | # kernelspec: 10 | # display_name: Python 3 11 | # language: python 12 | # name: python3 13 | # --- 14 | 15 | # %matplotlib inline 16 | 17 | # # A tutorial 18 | # 19 | # This is a Jupyter notebook. 20 | 21 | import numpy as np 22 | import matplotlib.pyplot as plt 23 | 24 | # ## Plotting 25 | # 26 | # It has a plot: 27 | 28 | x = np.linspace(0, 10, 5000) 29 | plt.plot(x, np.sin(x)) 30 | plt.xlabel("x") 31 | plt.ylabel("y"); 32 | 33 | 34 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rtds-action", 3 | "version": "0.1.0", 4 | "description": "Interface GitHub Actions and ReadTheDocs", 5 | "main": "src/js/index.js", 6 | "scripts": { 7 | "package": "ncc build src/js/index.js -o compiled" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/dfm/rtds-action.git" 12 | }, 13 | "keywords": [], 14 | "author": "", 15 | "license": "MIT", 16 | "bugs": { 17 | "url": "https://github.com/dfm/rtds-action/issues" 18 | }, 19 | "homepage": "https://github.com/dfm/rtds-action#readme", 20 | "dependencies": { 21 | "@actions/core": "^1.9.1", 22 | "@actions/github": "^5.0.0", 23 | "node-fetch": ">=3.2.10" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /.github/workflows/compile.yml: -------------------------------------------------------------------------------- 1 | name: Compile 2 | on: 3 | push: 4 | branches: 5 | - main 6 | pull_request: 7 | 8 | permissions: 9 | contents: write 10 | 11 | jobs: 12 | compile: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v4 16 | - uses: actions/setup-node@v4 17 | with: 18 | node-version: '16' 19 | - run: | 20 | npm install 21 | npm i -g @vercel/ncc 22 | - run: ncc build src/js/index.js --out compiled 23 | - uses: actions/upload-artifact@v4 24 | with: 25 | name: compiled 26 | path: compiled/index.js 27 | - if: github.ref == 'refs/heads/main' 28 | uses: JamesIves/github-pages-deploy-action@v4 29 | with: 30 | branch: branch 31 | folder: compiled 32 | target-folder: compiled 33 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | BUILDDIR = _build 8 | 9 | # Internal variables. 10 | PAPEROPT_a4 = -D latex_paper_size=a4 11 | PAPEROPT_letter = -D latex_paper_size=letter 12 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(SPHINXOPTS) . 13 | 14 | default: dirhtml 15 | 16 | .PHONY: clean 17 | clean: 18 | rm -rf $(BUILDDIR)/* $(TUTORIALS) 19 | 20 | .PHONY: html 21 | html: 22 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 23 | @echo 24 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 25 | 26 | .PHONY: dirhtml 27 | dirhtml: 28 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 29 | @echo 30 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 31 | 32 | .PHONY: singlehtml 33 | singlehtml: 34 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 35 | @echo 36 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 37 | -------------------------------------------------------------------------------- /src/js/index.js: -------------------------------------------------------------------------------- 1 | const core = require("@actions/core"); 2 | import fetch from 'node-fetch'; 3 | 4 | try { 5 | const webhookUrl = core.getInput("webhook_url", { required: true }); 6 | const webhookToken = core.getInput("webhook_token", { required: true }); 7 | 8 | // Extract the branch name from the ref 9 | // const ref = github.context.payload.ref; 10 | const ref = core.getInput("commit_ref", { required: true }); 11 | const branchname = ref.split("/").slice(2).join("/"); 12 | 13 | // Format the request parameters 14 | const params = new URLSearchParams(); 15 | params.append("branches", branchname); 16 | params.append("token", webhookToken); 17 | 18 | // Execute the request 19 | (async () => { 20 | try { 21 | const response = await fetch(webhookUrl, { 22 | method: "POST", 23 | body: params, 24 | }); 25 | const json = await response.json(); 26 | console.log(json); 27 | } catch (error) { 28 | core.setFailed(error.message); 29 | } 30 | })(); 31 | } catch (error) { 32 | core.setFailed(error.message); 33 | } 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Dan Foreman-Mackey 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.github/workflows/docs.yml: -------------------------------------------------------------------------------- 1 | name: Docs 2 | on: 3 | push: 4 | branches: 5 | - main 6 | pull_request: 7 | release: 8 | types: 9 | - published 10 | 11 | jobs: 12 | notebooks: 13 | name: "Build the notebooks for the docs" 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v4 17 | 18 | - name: Set up Python 19 | uses: actions/setup-python@v5 20 | with: 21 | python-version: 3.8 22 | 23 | - name: Install dependencies 24 | run: | 25 | python -m pip install -U pip 26 | python -m pip install -r .github/workflows/requirements.txt 27 | 28 | - name: Execute the notebooks 29 | run: | 30 | jupytext --to ipynb --execute docs/tutorials/*.py 31 | 32 | - uses: actions/upload-artifact@v4 33 | with: 34 | name: notebooks-for-${{ github.sha }} 35 | path: docs/tutorials 36 | 37 | - name: Trigger RTDs build 38 | if: github.event_name != 'pull_request' 39 | uses: ./ 40 | with: 41 | webhook_url: ${{ secrets.RTDS_WEBHOOK_URL }} 42 | webhook_token: ${{ secrets.RTDS_WEBHOOK_TOKEN }} 43 | commit_ref: ${{ github.ref }} 44 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish 2 | on: 3 | push: 4 | branches: 5 | - main 6 | pull_request: 7 | branches: 8 | - main 9 | release: 10 | types: 11 | - published 12 | 13 | jobs: 14 | build: 15 | name: Build source distribution and universal wheel 16 | runs-on: ubuntu-latest 17 | steps: 18 | - uses: actions/checkout@v4 19 | with: 20 | fetch-depth: 0 21 | 22 | - uses: actions/setup-python@v5 23 | name: Install Python 24 | with: 25 | python-version: "3.8" 26 | 27 | - name: Build 28 | run: | 29 | python -m pip install -U pip pep517 twine setuptools_scm 30 | python -m pep517.build . 31 | 32 | - name: Test the sdist 33 | run: | 34 | python -m venv venv-sdist 35 | venv-sdist/bin/python -m pip install dist/rtds_action*.tar.gz 36 | venv-sdist/bin/python -c "import rtds_action;print(rtds_action.__version__)" 37 | 38 | - name: Test the wheel 39 | run: | 40 | python -m venv venv-wheel 41 | venv-wheel/bin/python -m pip install dist/rtds_action*.whl 42 | venv-wheel/bin/python -c "import rtds_action;print(rtds_action.__version__)" 43 | 44 | - uses: actions/upload-artifact@v4 45 | with: 46 | path: dist/rtds_action* 47 | 48 | upload_pypi: 49 | needs: [build] 50 | runs-on: ubuntu-latest 51 | if: github.event_name == 'release' && github.event.action == 'published' 52 | steps: 53 | - uses: actions/download-artifact@v4 54 | with: 55 | name: artifact 56 | path: dist 57 | 58 | - uses: pypa/gh-action-pypi-publish@master 59 | with: 60 | user: __token__ 61 | password: ${{ secrets.pypi_password }} 62 | # To test: repository_url: https://test.pypi.org/legacy/ 63 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import os 4 | import sphinx_material 5 | from pkg_resources import DistributionNotFound, get_distribution 6 | 7 | # Fanciness to get version number 8 | try: 9 | __version__ = get_distribution("rtds_action").version 10 | except DistributionNotFound: 11 | __version__ = "dev" 12 | 13 | 14 | extensions = [ 15 | # For processing notebooks 16 | "nbsphinx", 17 | # The topic of these docs 18 | "rtds_action", 19 | # Nicer docs 20 | "sphinx_copybutton", 21 | ] 22 | 23 | # Settings for GitHub actions integration 24 | rtds_action_github_repo = "dfm/rtds-action" 25 | rtds_action_path = "tutorials" 26 | rtds_action_artifact_prefix = "notebooks-for-" 27 | rtds_action_github_token = os.environ.get("GITHUB_TOKEN", None) 28 | 29 | # General settings 30 | source_suffix = ".rst" 31 | master_doc = "index" 32 | 33 | project = "ReadTheDocs + GitHub Actions" 34 | author = "Dan Foreman-Mackey" 35 | copyright = "2020, " + author 36 | version = __version__ 37 | release = __version__ 38 | 39 | exclude_patterns = ["_build"] 40 | 41 | # HTML theme 42 | html_show_sourcelink = False 43 | html_sidebars = { 44 | "**": [ 45 | "logo-text.html", 46 | "globaltoc.html", 47 | "localtoc.html", 48 | "searchbox.html", 49 | ] 50 | } 51 | 52 | extensions.append("sphinx_material") 53 | html_theme_path = sphinx_material.html_theme_path() 54 | html_context = sphinx_material.get_html_context() 55 | html_theme = "sphinx_material" 56 | html_title = "Interface ReadTheDocs and GitHub Actions" 57 | html_short_title = "ReadTheDocs + GitHub Actions" 58 | html_theme_options = { 59 | "nav_title": "rtds-action", 60 | "logo_icon": "", 61 | "color_primary": "blue", 62 | "color_accent": "light-blue", 63 | "repo_url": "https://github.com/dfm/rtds-action", 64 | "repo_name": "rtds-action", 65 | "globaltoc_depth": 1, 66 | "globaltoc_collapse": False, 67 | "globaltoc_includehidden": False, 68 | "heroes": { 69 | "index": "Don't save executed Jupyter notebooks to your git repos " 70 | "ever again!" 71 | }, 72 | "nav_links": [], 73 | } 74 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # Inspired by: 4 | # https://hynek.me/articles/sharing-your-labor-of-love-pypi-quick-and-dirty/ 5 | 6 | import codecs 7 | import os 8 | import re 9 | 10 | from setuptools import find_packages, setup 11 | 12 | # PROJECT SPECIFIC 13 | 14 | NAME = "rtds_action" 15 | PACKAGES = find_packages(where="src") 16 | META_PATH = os.path.join("src", "rtds_action", "__init__.py") 17 | CLASSIFIERS = [ 18 | "Development Status :: 4 - Beta", 19 | "Intended Audience :: Developers", 20 | "License :: OSI Approved :: MIT License", 21 | "Operating System :: OS Independent", 22 | "Programming Language :: Python", 23 | "Programming Language :: Python :: 3", 24 | ] 25 | INSTALL_REQUIRES = [ 26 | "sphinx>=1.7.5", 27 | "setuptools>=40.6.0", 28 | "setuptools_scm", 29 | "requests", 30 | ] 31 | EXTRA_REQUIRE = { 32 | "docs": [], 33 | } 34 | EXTRA_REQUIRE["dev"] = EXTRA_REQUIRE["docs"] + [ 35 | "pre-commit", 36 | "black", 37 | "isort", 38 | "toml", 39 | "flake8", 40 | "jupytext", 41 | "jupyterlab", 42 | "pep517", 43 | "twine", 44 | ] 45 | 46 | # END PROJECT SPECIFIC 47 | 48 | 49 | HERE = os.path.dirname(os.path.realpath(__file__)) 50 | 51 | 52 | def read(*parts): 53 | with codecs.open(os.path.join(HERE, *parts), "rb", "utf-8") as f: 54 | return f.read() 55 | 56 | 57 | def find_meta(meta, meta_file=read(META_PATH)): 58 | meta_match = re.search( 59 | r"^__{meta}__ = ['\"]([^'\"]*)['\"]".format(meta=meta), meta_file, re.M 60 | ) 61 | if meta_match: 62 | return meta_match.group(1) 63 | raise RuntimeError("Unable to find __{meta}__ string.".format(meta=meta)) 64 | 65 | 66 | if __name__ == "__main__": 67 | setup( 68 | name=NAME, 69 | use_scm_version={ 70 | "write_to": os.path.join( 71 | "src", NAME, "{0}_version.py".format(NAME) 72 | ), 73 | "write_to_template": '__version__ = "{version}"\n', 74 | }, 75 | author=find_meta("author"), 76 | author_email=find_meta("email"), 77 | maintainer=find_meta("author"), 78 | maintainer_email=find_meta("email"), 79 | url=find_meta("uri"), 80 | license=find_meta("license"), 81 | description=find_meta("description"), 82 | long_description=read("README.md"), 83 | long_description_content_type="text/markdown", 84 | packages=PACKAGES, 85 | package_dir={"": "src"}, 86 | include_package_data=True, 87 | install_requires=INSTALL_REQUIRES, 88 | extras_require=EXTRA_REQUIRE, 89 | classifiers=CLASSIFIERS, 90 | zip_safe=False, 91 | options={"bdist_wheel": {"universal": "1"}}, 92 | ) 93 | -------------------------------------------------------------------------------- /docs/tutorials/another-tutorial.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # --- 3 | # jupyter: 4 | # jupytext: 5 | # text_representation: 6 | # extension: .py 7 | # format_name: light 8 | # format_version: '1.5' 9 | # jupytext_version: 1.5.1 10 | # kernelspec: 11 | # display_name: Python 3 12 | # language: python 13 | # name: python3 14 | # --- 15 | 16 | # %matplotlib inline 17 | 18 | # # Another tutorial 19 | # 20 | # This is *also* a Jupyter notebook. 21 | # This time, let's do some Machine Learning™, based on [this (BSD-3 licensed) tutorial by Phil Roth](https://scikit-learn.org/stable/auto_examples/cluster/plot_kmeans_assumptions.html) from scikit-learn. 22 | # 23 | # ## Simulated data 24 | # 25 | # First we simulate some data: 26 | 27 | # + 28 | import matplotlib.pyplot as plt 29 | from sklearn.datasets import make_blobs 30 | 31 | n_samples = 1500 32 | random_state = 170 33 | X, y = make_blobs(n_samples=n_samples, random_state=random_state) 34 | 35 | plt.scatter(X[:, 0], X[:, 1], c="k") 36 | plt.xlabel("x1") 37 | plt.ylabel("x2"); 38 | # - 39 | 40 | # ## Clustering 41 | # 42 | # Now let's cluster using K-means with several different assumptions about the parameters: 43 | 44 | # + 45 | import numpy as np 46 | from sklearn.cluster import KMeans 47 | 48 | plt.figure(figsize=(12, 12)) 49 | 50 | # Incorrect number of clusters 51 | y_pred = KMeans(n_clusters=2, random_state=random_state).fit_predict(X) 52 | 53 | plt.subplot(221) 54 | plt.scatter(X[:, 0], X[:, 1], c=y_pred) 55 | plt.title("Incorrect Number of Blobs") 56 | 57 | # Anisotropicly distributed data 58 | transformation = [[0.60834549, -0.63667341], [-0.40887718, 0.85253229]] 59 | X_aniso = np.dot(X, transformation) 60 | y_pred = KMeans(n_clusters=3, random_state=random_state).fit_predict(X_aniso) 61 | 62 | plt.subplot(222) 63 | plt.scatter(X_aniso[:, 0], X_aniso[:, 1], c=y_pred) 64 | plt.title("Anisotropicly Distributed Blobs") 65 | 66 | # Different variance 67 | X_varied, y_varied = make_blobs(n_samples=n_samples, 68 | cluster_std=[1.0, 2.5, 0.5], 69 | random_state=random_state) 70 | y_pred = KMeans(n_clusters=3, random_state=random_state).fit_predict(X_varied) 71 | 72 | plt.subplot(223) 73 | plt.scatter(X_varied[:, 0], X_varied[:, 1], c=y_pred) 74 | plt.title("Unequal Variance") 75 | 76 | # Unevenly sized blobs 77 | X_filtered = np.vstack((X[y == 0][:500], X[y == 1][:100], X[y == 2][:10])) 78 | y_pred = KMeans(n_clusters=3, 79 | random_state=random_state).fit_predict(X_filtered) 80 | 81 | plt.subplot(224) 82 | plt.scatter(X_filtered[:, 0], X_filtered[:, 1], c=y_pred) 83 | plt.title("Unevenly Sized Blobs"); 84 | # - 85 | 86 | # ## Conclusion 87 | # 88 | # Your choices matter when teaching the machines. 89 | 90 | 91 | -------------------------------------------------------------------------------- /src/rtds_action/rtds_action.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | __all__ = ["setup"] 4 | 5 | import subprocess 6 | import time 7 | from io import BytesIO 8 | from zipfile import ZipFile 9 | 10 | import requests 11 | from sphinx.util import logging 12 | from sphinx.errors import ExtensionError 13 | 14 | logger = logging.getLogger(__name__) 15 | 16 | 17 | def config_inited(app, config, retries=3): 18 | prefix = config["rtds_action_artifact_prefix"] 19 | path = config["rtds_action_path"] 20 | repo = config["rtds_action_github_repo"] 21 | raise_error = config["rtds_action_error_if_missing"] 22 | if repo is None: 23 | raise ExtensionError( 24 | "rtds_action: missing required argument 'rtds_action_github_repo'" 25 | ) 26 | token = config["rtds_action_github_token"] 27 | if token is None: 28 | raise ExtensionError( 29 | "rtds_action: missing required argument 'rtds_action_github_token'" 30 | ) 31 | 32 | try: 33 | git_hash = ( 34 | subprocess.check_output(["git", "rev-parse", "HEAD"]) 35 | .strip() 36 | .decode("ascii") 37 | ) 38 | except subprocess.CalledProcessError: 39 | logger.warn("rtds_action: can't get git hash") 40 | return 41 | 42 | r = requests.get( 43 | f"https://api.github.com/repos/{repo}/actions/artifacts", 44 | params=dict(per_page=100), 45 | headers={"Authorization": f"token {token}"}, 46 | ) 47 | if r.status_code != 200: 48 | logger.warn(f"Can't list files ({r.status_code})") 49 | return 50 | 51 | expected_name = f"{prefix}{git_hash}" 52 | result = r.json() 53 | for artifact in result.get("artifacts", []): 54 | if artifact["name"] == expected_name: 55 | logger.info(artifact) 56 | r = requests.get( 57 | artifact["archive_download_url"], 58 | headers={"Authorization": f"token {token}"}, 59 | ) 60 | 61 | if r.status_code != 200: 62 | logger.warn(f"Can't download artifact ({r.status_code})") 63 | return 64 | 65 | with ZipFile(BytesIO(r.content)) as f: 66 | f.extractall(path=path) 67 | 68 | return 69 | 70 | logger.warn( 71 | f"rtds_action: can't find expected artifact '{expected_name}' " 72 | f"at https://api.github.com/repos/{repo}/actions/artifacts" 73 | ) 74 | if retries > 0: 75 | logger.warn("Trying again") 76 | time.sleep(30) 77 | config_inited(app, config, retries=retries - 1) 78 | else: 79 | if raise_error: 80 | raise Exception( 81 | f"rtds_action: can't find expected artifact '{expected_name}' " 82 | f"at https://api.github.com/repos/{repo}/actions/artifacts" 83 | ) 84 | 85 | def setup(app): 86 | app.add_config_value("rtds_action_artifact_prefix", "", rebuild="env") 87 | app.add_config_value("rtds_action_path", ".", rebuild="env") 88 | app.add_config_value("rtds_action_github_repo", None, rebuild="env") 89 | app.add_config_value("rtds_action_github_token", None, rebuild="env") 90 | app.add_config_value("rtds_action_error_if_missing", False, rebuild="env") 91 | app.connect("config-inited", config_inited) 92 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | Interface ReadTheDocs and GitHub Actions 2 | ======================================== 3 | 4 | I like to use `ReadTheDocs `__ to build (and 5 | version!) my docs, but I *also* like to use `Jupyter 6 | notebooks `__ to write tutorials. Unfortunately, 7 | this has always meant that I needed to check executed notebooks (often 8 | with large images) into my git repository, causing huge amounts of 9 | bloat. Futhermore, the executed notebooks would often get out of sync 10 | with the development of the code. **No more!!** 11 | 12 | *This library avoids these issues by executing code on* `GitHub 13 | Actions `_, *uploading build 14 | artifacts (in this case, executed Jupter notebooks), and then (only 15 | then!) triggering a ReadTheDocs build that can download the executed 16 | notebooks.* 17 | 18 | There is still some work required to set up this workflow, but this 19 | library has three pieces that make it a bit easier: 20 | 21 | 1. A GitHub action that can be used to trigger a build for the current 22 | branch on ReadTheDocs. 23 | 2. A Sphinx extension that interfaces with the GitHub API to download 24 | the artifact produced for the target commit hash. 25 | 3. Some documentation that shows you how to set all this up! 26 | 27 | Usage 28 | ----- 29 | 30 | 1. Set up ReadTheDocs 31 | ~~~~~~~~~~~~~~~~~~~~~ 32 | 33 | 1. First, you’ll need to import your project as usual. If you’ve already 34 | done that, don’t worry: this will also work with existing ReadTheDocs 35 | projects. 36 | 2. Next, go to the admin page for your project on ReadTheDocs, click on 37 | ``Integrations`` (the URL is something like 38 | ``https://readthedocs.org/dashboard/YOUR_PROJECT_NAME/integrations/``). 39 | 3. Click ``Add integration`` and select 40 | ``Generic API incoming webhook``. 41 | 4. Take note of the webhook ``URL`` and ``token`` on this page for use 42 | later. 43 | 44 | You should also edit your webhook settings on GitHub by going to 45 | ``https://github.com/USERNAME/REPONAME/settings/hooks`` and clicking "Edit" 46 | next to the ReadTheDocs hook. On that page, you should un-check the 47 | ``Pushes`` option. 48 | 49 | 2. Set up GitHub Actions workflow 50 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 51 | 52 | In this example, we’ll assume that we have tutorials written as Jupyter 53 | notebooks, saved as Python scripts using 54 | `Jupytext `__ 55 | (because that’s probably what you should be doing anyways!) in a 56 | directory called ``docs/tutorials``. 57 | 58 | First, you’ll need to add the ReadTheDocs webhook URL and token that you 59 | recorded above as “secrets” for your GitHub project by going to the URL 60 | ``https://github.com/USERNAME/REPONAME/settings/secrets``. I’ll call 61 | them ``RTDS_WEBHOOK_URL`` (include the ``https``!) and 62 | ``RTDS_WEBHOOK_TOKEN`` respectively. 63 | 64 | For this use case, we can create the workflow 65 | ``.github/workflows/docs.yml`` as follows: 66 | 67 | .. code:: yaml 68 | 69 | name: Docs 70 | on: [push, release] 71 | 72 | jobs: 73 | notebooks: 74 | name: "Build the notebooks for the docs" 75 | runs-on: ubuntu-latest 76 | steps: 77 | - uses: actions/checkout@v2 78 | 79 | - name: Set up Python 80 | uses: actions/setup-python@v2 81 | with: 82 | python-version: 3.8 83 | 84 | - name: Install dependencies 85 | run: | 86 | python -m pip install -U pip 87 | python -m pip install -r .github/workflows/requirements.txt 88 | 89 | - name: Execute the notebooks 90 | run: | 91 | jupytext --to ipynb --execute docs/tutorials/*.py 92 | 93 | - uses: actions/upload-artifact@v2 94 | with: 95 | name: notebooks-for-${{ github.sha }} 96 | path: docs/tutorials 97 | 98 | - name: Trigger RTDs build 99 | uses: dfm/rtds-action@v1 100 | with: 101 | webhook_url: ${{ secrets.RTDS_WEBHOOK_URL }} 102 | webhook_token: ${{ secrets.RTDS_WEBHOOK_TOKEN }} 103 | commit_ref: ${{ github.ref }} 104 | 105 | Here, we’re also assuming that we’ve added a ``pip`` requirements file 106 | at ``.github/workflows/requirements.txt`` with the dependencies required 107 | to execute the notebooks. Also note that in the ``upload-artifact`` step 108 | we give our artifact that depends on the hash of the current commit. 109 | This is crucial! We also need to take note of the ``notebooks-for-`` 110 | prefix because we’ll use that later. 111 | 112 | It’s worth emphasizing here that the only “special” steps in this 113 | workflow are the last two. You can do whatever you want to generate your 114 | artifact in the previous steps (for example, you could use ``conda`` 115 | instead of ``pip``) because this workflow is not picky about how you get 116 | there! 117 | 118 | 3. Set up Sphinx 119 | ~~~~~~~~~~~~~~~~ 120 | 121 | Finally, you can edit the ``conf.py`` for your Sphinx documentation to 122 | add support for fetching the artifact produced by your action. Here is a 123 | minimal example: 124 | 125 | .. code:: python 126 | 127 | import os 128 | 129 | extensions = [... "rtds_action"] 130 | 131 | # The name of your GitHub repository 132 | rtds_action_github_repo = "USERNAME/REPONAME" 133 | 134 | # The path where the artifact should be extracted 135 | # Note: this is relative to the conf.py file! 136 | rtds_action_path = "tutorials" 137 | 138 | # The "prefix" used in the `upload-artifact` step of the action 139 | rtds_action_artifact_prefix = "notebooks-for-" 140 | 141 | # A GitHub personal access token is required, more info below 142 | rtds_action_github_token = os.environ["GITHUB_TOKEN"] 143 | 144 | Where we have added the custom extension and set the required 145 | configuration parameters. 146 | 147 | You’ll need to provide ReadTheDocs with a GitHub personal access token 148 | (it only needs the ``public_repo`` scope if your repo is public). You 149 | can generate a new token by going to `your GitHub settings 150 | page `__. Then, save it as an 151 | environment variable (called ``GITHUB_TOKEN`` in this case) on 152 | ReadTheDocs. 153 | 154 | Examples 155 | -------- 156 | 157 | Here are some example tutorials! See `the GitHub repository 158 | `_ for the source of this example site. 159 | 160 | .. toctree:: 161 | :maxdepth: 2 162 | :caption: Tutorials 163 | 164 | tutorials/tutorial 165 | tutorials/another-tutorial 166 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Interface Read the Docs and GitHub Actions 2 | 3 | [![Docs](https://github.com/dfm/rtds-action/workflows/Docs/badge.svg)](https://github.com/dfm/rtds-action/actions?query=workflow%3ADocs) 4 | [![Documentation Status](https://readthedocs.org/projects/rtds-action/badge/?version=latest)](https://rtds-action.readthedocs.io/en/latest/?badge=latest) 5 | 6 | I like to use [Read the Docs](https://readthedocs.org/) to build (and version!) my 7 | docs, but I _also_ like to use [Jupyter notebooks](https://jupyter.org/) to 8 | write tutorials. Unfortunately, even though 9 | [notebooks can be executed on Read the Docs](https://docs.readthedocs.io/en/stable/guides/jupyter.html), 10 | some of them take a very long time to run or 11 | need special Docker environments to execute, 12 | which goes beyond what the platform supports. In these cases I needed to check 13 | executed notebooks (often with large images) into my git repository, causing 14 | huge amounts of bloat. Futhermore, the executed notebooks would often get out of 15 | sync with the development of the code. **No more!!** 16 | 17 | _This library avoids these issues by executing code on [GitHub 18 | Actions](https://github.com/features/actions), uploading build artifacts (in 19 | this case, executed Jupter notebooks), and then (only then!) triggering a 20 | Read the Docs build that can download the executed notebooks._ 21 | 22 | There is still some work required to set up this workflow, but this library has 23 | three pieces that make it a bit easier: 24 | 25 | 1. A GitHub action that can be used to trigger a build for the current branch on 26 | Read the Docs. 27 | 2. A Sphinx extension that interfaces with the GitHub API to download the 28 | artifact produced for the target commit hash. 29 | 3. Some documentation that shows you how to set all this up! 30 | 31 | ## Usage 32 | 33 | The following gives the detailed steps of the process of setting up a project 34 | using this workflow. But you can also see a fully functional example in this 35 | repository. The documentation source is the `docs` directory and the 36 | `.github/workflows` directory includes a workflow that is executed to build the 37 | docs using this package. The rendered page is available at 38 | [rtds-action.readthedocs.io](https://rtds-action.readthedocs.io). 39 | 40 | ### 1. Set up Read the Docs 41 | 42 | 1. First, you'll need to import your project as usual. If you've already done 43 | that, don't worry: this will also work with existing Read the Docs projects. 44 | 2. Next, go to the admin page for your project on Read the Docs, click on 45 | `Integrations` (the URL is something like 46 | `https://readthedocs.org/dashboard/YOUR_PROJECT_NAME/integrations/`). 47 | 3. Click `Add integration` and select `Generic API incoming webhook`. 48 | 4. Take note of the webhook `URL` and `token` on this page for use later. 49 | 50 | You should also edit your webhook settings on GitHub by going to 51 | `https://github.com/USERNAME/REPONAME/settings/hooks` and clicking "Edit" 52 | next to the Read the Docs hook. On that page, you should un-check the `Pushes` 53 | option. 54 | 55 | ### 2. Set up GitHub Actions workflow 56 | 57 | In this example, we'll assume that we have tutorials written as Jupyter 58 | notebooks, saved as Python scripts using 59 | [Jupytext](https://jupytext.readthedocs.io/en/latest/introduction.html) (because 60 | that's probably what you should be doing anyways!) in a directory called 61 | `docs/tutorials`. 62 | 63 | First, you'll need to add the Read the Docs webhook URL and token that you 64 | recorded above as "secrets" for your GitHub project by going to the URL 65 | `https://github.com/USERNAME/REPONAME/settings/secrets`. I'll call them 66 | `RTDS_WEBHOOK_URL` (include the `https`!) and `RTDS_WEBHOOK_TOKEN` respectively. 67 | 68 | For this use case, we can create the workflow `.github/workflows/docs.yml` as 69 | follows: 70 | 71 | ```yaml 72 | name: Docs 73 | on: [push, release] 74 | 75 | jobs: 76 | notebooks: 77 | name: "Build the notebooks for the docs" 78 | runs-on: ubuntu-latest 79 | steps: 80 | - uses: actions/checkout@v2 81 | 82 | - name: Set up Python 83 | uses: actions/setup-python@v2 84 | with: 85 | python-version: 3.8 86 | 87 | - name: Install dependencies 88 | run: | 89 | python -m pip install -U pip 90 | python -m pip install -r .github/workflows/requirements.txt 91 | 92 | - name: Execute the notebooks 93 | run: | 94 | jupytext --to ipynb --execute docs/tutorials/*.py 95 | 96 | - uses: actions/upload-artifact@v2 97 | with: 98 | name: notebooks-for-${{ github.sha }} 99 | path: docs/tutorials 100 | 101 | - name: Trigger RTDs build 102 | uses: dfm/rtds-action@v1 103 | with: 104 | webhook_url: ${{ secrets.RTDS_WEBHOOK_URL }} 105 | webhook_token: ${{ secrets.RTDS_WEBHOOK_TOKEN }} 106 | commit_ref: ${{ github.ref }} 107 | ``` 108 | 109 | Here, we're also assuming that we've added a `pip` requirements file at 110 | `.github/workflows/requirements.txt` with the dependencies required to execute 111 | the notebooks. Also note that in the `upload-artifact` step we give our artifact 112 | that depends on the hash of the current commit. This is crucial! We also need to 113 | take note of the `notebooks-for-` prefix because we'll use that later. 114 | 115 | It's worth emphasizing here that the only "special" steps in this workflow are 116 | the last two. You can do whatever you want to generate your artifact in the 117 | previous steps (for example, you could use `conda` instead of `pip`) because 118 | this workflow is not picky about how you get there! 119 | 120 | ### 3. Set up Sphinx 121 | 122 | Finally, you can edit the `conf.py` for your Sphinx documentation to add support 123 | for fetching the artifact produced by your action. Here is a minimal example: 124 | 125 | ```python 126 | import os 127 | 128 | extensions = [... "rtds_action"] 129 | 130 | # The name of your GitHub repository 131 | rtds_action_github_repo = "USERNAME/REPONAME" 132 | 133 | # The path where the artifact should be extracted 134 | # Note: this is relative to the conf.py file! 135 | rtds_action_path = "tutorials" 136 | 137 | # The "prefix" used in the `upload-artifact` step of the action 138 | rtds_action_artifact_prefix = "notebooks-for-" 139 | 140 | # A GitHub personal access token is required, more info below 141 | rtds_action_github_token = os.environ["GITHUB_TOKEN"] 142 | 143 | # Whether or not to raise an error on Read the Docs if the 144 | # artifact containing the notebooks can't be downloaded (optional) 145 | rtds_action_error_if_missing = False 146 | ``` 147 | 148 | Where we have added the custom extension and set the required configuration 149 | parameters. 150 | 151 | You'll need to provide Read the Docs with a GitHub personal access token (it only 152 | needs the `public_repo` scope if your repo is public). You can generate a new 153 | token by going to [your GitHub settings 154 | page](https://github.com/settings/tokens). Then, save it as an environment 155 | variable (called `GITHUB_TOKEN` in this case) on Read the Docs. 156 | 157 | ## Development 158 | 159 | For now, just a note: if you edit `src/js/index.js`, you _must_ run `npm run package` to generate the compiled action source. 160 | --------------------------------------------------------------------------------