├── 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 | [](https://github.com/dfm/rtds-action/actions?query=workflow%3ADocs)
4 | [](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 |
--------------------------------------------------------------------------------