From eaeb3d483de13d605c760c43b8b7c95e3b9031f7 Mon Sep 17 00:00:00 2001 From: Raimundo Henriques Date: Tue, 29 Sep 2026 15:39:00 +0200 Subject: [PATCH 1/2] build: upgrade docs to sphinx stack 2 Upgrades the docs from v1.4 to v2, covering: - tooling layout; - dependency updates; - Read the Docs warning policy; - redirct migration; and - support for llms-txt. --- .github/copilot-instructions.md | 10 +- .readthedocs.yaml | 2 +- README.rst | 7 +- UPDATING.md | 2 +- docs/.gitignore | 16 +- docs/.sphinx/version | 1 - docs/Makefile | 158 +++++++++--------- .../{.sphinx => _dev}/.pre-commit-config.yaml | 0 docs/{.sphinx => _dev}/.pymarkdown.json | 0 docs/_dev/check_removed_urls.py | 95 +++++++++++ docs/{.sphinx => _dev}/get_vale_conf.py | 4 +- docs/{.sphinx => _dev}/pa11y.json | 0 docs/{.sphinx => _dev}/update_sp.py | 46 ++--- docs/_dev/version | 1 + docs/conf.py | 26 ++- docs/contributing/index.md | 4 +- .../snap-development/use-the-secret-portal.md | 6 +- docs/requirements.txt | 43 ++--- 18 files changed, 268 insertions(+), 153 deletions(-) delete mode 100644 docs/.sphinx/version rename docs/{.sphinx => _dev}/.pre-commit-config.yaml (100%) rename docs/{.sphinx => _dev}/.pymarkdown.json (100%) create mode 100644 docs/_dev/check_removed_urls.py rename docs/{.sphinx => _dev}/get_vale_conf.py (97%) mode change 100755 => 100644 rename docs/{.sphinx => _dev}/pa11y.json (100%) rename docs/{.sphinx => _dev}/update_sp.py (87%) mode change 100755 => 100644 create mode 100644 docs/_dev/version diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 47c7d4ce..742a5a05 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -5,8 +5,8 @@ This file contains short, concrete guidance for AI coding agents working in this ## Quick context - Project: documentation built with Sphinx (MyST/Markdown + some reST). -- Key directories: `docs/` (content), `docs/.sphinx/` (style/config helpers), `.github/workflows/` (CI). -- Primary build: virtualenv at `docs/.sphinx/venv` created by `make install` from the repo root. +- Key directories: `docs/` (content), `docs/_dev/` (style/config helpers), `.github/workflows/` (CI). +- Primary build: virtualenv at `docs/.venv` created by `make install` from the repo root. ## Most useful commands (examples) - Create venv & install deps: `make install` (from repo root). @@ -33,14 +33,14 @@ This file contains short, concrete guidance for AI coding agents working in this ## Files to check/read when changing behavior or styling - `docs/conf.py` — Sphinx configuration and extensions (canonical_sphinx, myst, intersphinx). - `docs/Makefile` — canonical make targets and environment expectations (venv location, PIPOPTS usage). -- `docs/.sphinx/.markdownlint.json` and `docs/.sphinx/spellingcheck.yaml` — editorial/style rules used by CI. -- `docs/.sphinx/pa11y.json` and `docs/.sphinx/.wordlist.txt` — accessibility and spelling exceptions. +- `docs/_dev/.pymarkdown.json` — Markdown lint configuration. +- `docs/_dev/pa11y.json` — accessibility checker configuration. - `.github/workflows/*` — CI entrypoints and special flags (e.g. docs-only triggers). ## How to propose edits (recommended checklist) 1. Run `make run` locally and visually confirm layout and link changes. 2. Run `make linkcheck` and `make vale` to catch link/style issues early. -3. Check `docs/.sphinx/*` for project-specific lint/spell rules and adhere to them. +3. Check `docs/_dev/*` for project-specific lint/spell rules and adhere to them. 4. Push a branch and open a PR; CI will validate markdown style and Sphinx build on `docs/**` changes. ## Examples from this repo (quick references) diff --git a/.readthedocs.yaml b/.readthedocs.yaml index 516fd564..ba0b194c 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -41,7 +41,7 @@ build: sphinx: builder: dirhtml configuration: docs/conf.py - fail_on_warning: false + fail_on_warning: true # If using Sphinx, optionally build your docs in additional formats such as PDF formats: diff --git a/README.rst b/README.rst index 109f82bb..e16412f2 100644 --- a/README.rst +++ b/README.rst @@ -19,8 +19,8 @@ To install the prerequisites: make install -This will create a virtual environment (``.sphinx/venv``) and install -dependency software (``.sphinx/requirements.txt``) within it. +This will create a virtual environment (``.venv``) and install dependency +software from ``requirements.txt`` within it. View the documentation ~~~~~~~~~~~~~~~~~~~~~~ @@ -43,5 +43,4 @@ The ``run`` target is therefore very convenient when preparing to submit a change to the documentation. .. LINKS -.. _`Documentation starter pack`: https://github.com/canonical/sphinx-docs-starter-pack/tree/main - +.. _`Sphinx Stack`: https://github.com/canonical/sphinx-stack diff --git a/UPDATING.md b/UPDATING.md index dced6189..6c1db0af 100644 --- a/UPDATING.md +++ b/UPDATING.md @@ -47,4 +47,4 @@ allowed outside the GitHub repository, according to the following workflow: [RTD Webpage](https://app.readthedocs.com/projects/canonical-snap/). The build takes approximately 20 minutes. 3. If the build process is successful, the generated documentation will be - hosted [here](https://canonical-snap.readthedocs-hosted.com/). \ No newline at end of file + hosted [here](https://canonical-snap.readthedocs-hosted.com/). diff --git a/docs/.gitignore b/docs/.gitignore index a10669d9..7d1319d4 100644 --- a/docs/.gitignore +++ b/docs/.gitignore @@ -1,17 +1,17 @@ # Environment *env*/ -.sphinx/venv/ +.venv/ # Sphinx -.sphinx/warnings.txt -.sphinx/.wordlist.dic -.sphinx/.doctrees/ -.sphinx/update/ -.sphinx/node_modules/ +_dev/warnings.txt +_dev/.wordlist.dic +_dev/.doctrees/ +_dev/update/ +_dev/node_modules/ # Vale -.sphinx/styles/* -.sphinx/vale.ini +_dev/styles/* +_dev/vale.ini # Build outputs _build diff --git a/docs/.sphinx/version b/docs/.sphinx/version deleted file mode 100644 index 347f5833..00000000 --- a/docs/.sphinx/version +++ /dev/null @@ -1 +0,0 @@ -1.4.1 diff --git a/docs/Makefile b/docs/Makefile index edee045f..5d7f2856 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -4,22 +4,23 @@ # You can set these variables from the command line, and also # from the environment for the first two. -SPHINXDIR = .sphinx -SPHINXOPTS ?= -c . -d $(SPHINXDIR)/.doctrees -j auto -SPHINXBUILD ?= $(VENVDIR)/bin/sphinx-build -SOURCEDIR ?= . -BUILDDIR ?= _build -VENVDIR ?= $(SPHINXDIR)/venv -PA11Y = $(SPHINXDIR)/node_modules/pa11y/bin/pa11y.js --config $(SPHINXDIR)/pa11y.json -VENV = $(VENVDIR)/bin/activate -TARGET = * -REQPDFPACKS = latexmk fonts-freefont-otf texlive-latex-recommended texlive-latex-extra texlive-fonts-recommended texlive-font-utils texlive-lang-cjk texlive-xetex plantuml xindy tex-gyre dvipng -CONFIRM_SUDO ?= N -VALE_CONFIG = $(SPHINXDIR)/vale.ini -VALEDIR ?= $(VENVDIR)/lib/python*/site-packages/vale -VOCAB_CANONICAL = $(SPHINXDIR)/styles/config/vocabularies/Canonical -SPHINX_HOST ?= 127.0.0.1 -SPHINX_PORT ?= 8000 +DEV_DIR ?= _dev +SPHINX_OPTS ?= -c . -d $(DEV_DIR)/.doctrees -j auto +SPHINX_BUILD ?= $(DOCS_VENVDIR)/bin/sphinx-build +SPHINX_HOST ?= 127.0.0.1 +SPHINX_PORT ?= 8000 +SPHINX_AUTOBUILD_OPTS ?= -D llms_txt_enabled=0 +DOCS_VENVDIR ?= .venv +DOCS_VENV ?= $(DOCS_VENVDIR)/bin/activate +DOCS_SOURCEDIR ?= . +DOCS_BUILDDIR ?= _build +DOCS_PDFPACKAGES ?= latexmk fonts-freefont-otf texlive-latex-recommended texlive-latex-extra texlive-fonts-recommended texlive-font-utils texlive-lang-cjk texlive-xetex plantuml xindy tex-gyre dvipng +DOCS_VOCAB ?= $(DEV_DIR)/styles/config/vocabularies/Canonical +VALE_DIR ?= $(DOCS_VENVDIR)/lib/python*/site-packages/vale +VALE_CONFIG ?= $(DEV_DIR)/vale.ini +PA11Y_CMD ?= $(DEV_DIR)/node_modules/pa11y/bin/pa11y.js --config $(DEV_DIR)/pa11y.json +CONFIRM_SUDO ?= N +CHECK_PATH ?= $(filter-out $(DOCS_VENVDIR) $(DOCS_BUILDDIR) $(DEV_DIR),$(wildcard *)) # Put it first so that "make" without argument is like "make help". help: @@ -37,7 +38,7 @@ help: @echo "* check inclusive language: make woke" @echo "* check accessibility: make pa11y" @echo "* check style guide compliance: make vale" - @echo "* check style guide compliance on target: make vale TARGET=*" + @echo "* check style guide compliance on target: make vale CHECK_PATH=*" @echo "* other possible targets: make " @echo "-------------------------------------------------------------" @echo @@ -47,127 +48,128 @@ help: vale-install pdf-prep pdf-prep-force clean clean-doc \ update lint-md -full-help: $(VENVDIR) - @. $(VENV); $(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) +full-help: $(DOCS_VENVDIR) + @. $(DOCS_VENV); $(SPHINX_BUILD) -M help "$(DOCS_SOURCEDIR)" "$(DOCS_BUILDDIR)" $(SPHINX_OPTS) $(O) @echo "\n\033[1;31mNOTE: This help texts shows unsupported targets!\033[0m" @echo "Run 'make help' to see supported targets." # If requirements are updated, venv should be rebuilt and timestamped. -$(VENVDIR): +$(DOCS_VENVDIR): @echo "... setting up virtualenv" - python3 -m venv $(VENVDIR) || { echo "You must install python3-venv before you can build the documentation."; exit 1; } - . $(VENV); pip install $(PIPOPTS) --require-virtualenv \ + python3 -m venv $(DOCS_VENVDIR) || { echo "You must install python3-venv before you can build the documentation."; exit 1; } + . $(DOCS_VENV); pip install $(PIPOPTS) --require-virtualenv \ --upgrade -r requirements.txt \ - --log $(VENVDIR)/pip_install.log - @test ! -f $(VENVDIR)/pip_list.txt || \ - mv $(VENVDIR)/pip_list.txt $(VENVDIR)/pip_list.txt.bak - @. $(VENV); pip list --local --format=freeze > $(VENVDIR)/pip_list.txt - @touch $(VENVDIR) + --log $(DOCS_VENVDIR)/pip_install.log + @test ! -f $(DOCS_VENVDIR)/pip_list.txt || \ + mv $(DOCS_VENVDIR)/pip_list.txt $(DOCS_VENVDIR)/pip_list.txt.bak + @. $(DOCS_VENV); pip list --local --format=freeze > $(DOCS_VENVDIR)/pip_list.txt + @touch $(DOCS_VENVDIR) pa11y-install: - @command -v $(PA11Y) >/dev/null || { \ + @test -x $(firstword $(PA11Y_CMD)) >/dev/null || { \ echo "Installing \"pa11y\" from npm..."; echo; \ - mkdir -p $(SPHINXDIR)/node_modules/ ; \ - npm install --prefix $(SPHINXDIR) pa11y; \ + mkdir -p $(DEV_DIR)/node_modules/ ; \ + npm install --prefix $(DEV_DIR) pa11y; \ } pymarkdownlnt-install: install - @. $(VENV); test -d $(VENVDIR)/lib/python*/site-packages/pymarkdown || pip install pymarkdownlnt==0.9.35 + @. $(DOCS_VENV); test -d $(DOCS_VENVDIR)/lib/python*/site-packages/pymarkdown || pip install pymarkdownlnt==0.9.35 -install: $(VENVDIR) +install: $(DOCS_VENVDIR) run: install - . $(VENV); $(VENVDIR)/bin/sphinx-autobuild -b dirhtml --host $(SPHINX_HOST) --port $(SPHINX_PORT) "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) + . $(DOCS_VENV); $(DOCS_VENVDIR)/bin/sphinx-autobuild -b dirhtml --host $(SPHINX_HOST) --port $(SPHINX_PORT) "$(DOCS_SOURCEDIR)" "$(DOCS_BUILDDIR)" $(SPHINX_OPTS) $(SPHINX_AUTOBUILD_OPTS) -# Does not depend on $(BUILDDIR) to rebuild properly at every run. +# Does not depend on $(DOCS_BUILDDIR) to rebuild properly at every run. html: install - . $(VENV); $(SPHINXBUILD) --fail-on-warning --keep-going -b dirhtml "$(SOURCEDIR)" "$(BUILDDIR)" -w $(SPHINXDIR)/warnings.txt $(SPHINXOPTS) + . $(DOCS_VENV); $(SPHINX_BUILD) --fail-on-warning --keep-going -b dirhtml "$(DOCS_SOURCEDIR)" "$(DOCS_BUILDDIR)" -w $(DEV_DIR)/warnings.txt $(SPHINX_OPTS) epub: install - . $(VENV); $(SPHINXBUILD) -b epub "$(SOURCEDIR)" "$(BUILDDIR)" -w $(SPHINXDIR)/warnings.txt $(SPHINXOPTS) + . $(DOCS_VENV); $(SPHINX_BUILD) -b epub "$(DOCS_SOURCEDIR)" "$(DOCS_BUILDDIR)" -w $(DEV_DIR)/warnings.txt $(SPHINX_OPTS) serve: html - cd "$(BUILDDIR)"; python3 -m http.server --bind $(SPHINX_HOST) $(SPHINX_PORT) + cd "$(DOCS_BUILDDIR)"; python3 -m http.server --bind $(SPHINX_HOST) $(SPHINX_PORT) clean: clean-doc - @test ! -e "$(VENVDIR)" -o -d "$(VENVDIR)" -a "$(abspath $(VENVDIR))" != "$(VENVDIR)" - rm -rf $(VENVDIR) - rm -rf $(SPHINXDIR)/node_modules/ - rm -rf $(SPHINXDIR)/styles + @test ! -e "$(DOCS_VENVDIR)" -o -d "$(DOCS_VENVDIR)" -a "$(abspath $(DOCS_VENVDIR))" != "$(DOCS_VENVDIR)" + rm -rf $(DOCS_VENVDIR) + rm -rf $(DEV_DIR)/node_modules/ + rm -rf $(DEV_DIR)/styles rm -rf $(VALE_CONFIG) clean-doc: - git clean -fx "$(BUILDDIR)" - rm -rf $(SPHINXDIR)/.doctrees + git clean -fx "$(DOCS_BUILDDIR)" + rm -rf $(DEV_DIR)/.doctrees linkcheck: install - . $(VENV) ; $(SPHINXBUILD) -b linkcheck "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) || { grep --color -F "[broken]" "$(BUILDDIR)/output.txt"; exit 1; } + . $(DOCS_VENV) ; $(SPHINX_BUILD) -b linkcheck -q "$(DOCS_SOURCEDIR)" "$(DOCS_BUILDDIR)" $(SPHINX_OPTS) || { grep --color -F "[broken]" "$(DOCS_BUILDDIR)/output.txt"; exit 1; } exit 0 pa11y: pa11y-install html - find $(BUILDDIR) -name *.html -print0 | xargs -n 1 -0 $(PA11Y) + find $(DOCS_BUILDDIR) -name *.html -print0 | xargs -n 1 -0 $(PA11Y_CMD) +# Without --return-code-scheme explicit, pymarkdownlnt returns 1 for multiple scenarios. lint-md: pymarkdownlnt-install - @. $(VENV); pymarkdownlnt --config $(SPHINXDIR)/.pymarkdown.json scan --recurse --exclude=$(SPHINXDIR)/** $(SOURCEDIR) + @. $(DOCS_VENV); pymarkdownlnt --config $(DEV_DIR)/.pymarkdown.json --return-code-scheme explicit scan --recurse $(CHECK_PATH); status=$$?; if [ $$status -eq 1 ]; then echo "No Markdown files selected for linting"; exit 0; fi; exit $$status; vale-install: install - @. $(VENV); test -f $(VALE_CONFIG) || python3 $(SPHINXDIR)/get_vale_conf.py - @echo '.Name=="Canonical.400-Enforce-inclusive-terms"' > $(SPHINXDIR)/styles/woke.filter - @echo '.Level=="error" and .Name!="Canonical.500-Repeated-words" and .Name!="Canonical.000-US-spellcheck"' > $(SPHINXDIR)/styles/error.filter - @echo '.Name=="Canonical.000-US-spellcheck"' > $(SPHINXDIR)/styles/spelling.filter - @. $(VENV); find $(VALEDIR)/vale_bin -size 195c -exec vale --version \; + @. $(DOCS_VENV); test -f $(VALE_CONFIG) || python3 $(DEV_DIR)/get_vale_conf.py + @echo '.Name=="Canonical.400-Enforce-inclusive-terms"' > $(DEV_DIR)/styles/woke.filter + @echo '.Level=="error" and .Name!="Canonical.500-Repeated-words" and .Name!="Canonical.000-US-spellcheck"' > $(DEV_DIR)/styles/error.filter + @echo '.Name=="Canonical.000-US-spellcheck"' > $(DEV_DIR)/styles/spelling.filter + @. $(DOCS_VENV); find $(VALE_DIR)/vale_bin -size 195c -exec vale --version \; woke: vale-install - @cat $(VOCAB_CANONICAL)/accept.txt > $(VOCAB_CANONICAL)/accept_backup.txt - @cat $(SOURCEDIR)/.custom_wordlist.txt >> $(VOCAB_CANONICAL)/accept.txt - @echo "Running Vale acceptable term check against $(TARGET). To change target set TARGET= with make command" - @. $(VENV); vale --config="$(VALE_CONFIG)" --filter='$(SPHINXDIR)/styles/woke.filter' --glob='*.{md,rst}' $(TARGET) - @cat $(VOCAB_CANONICAL)/accept_backup.txt > $(VOCAB_CANONICAL)/accept.txt && rm $(VOCAB_CANONICAL)/accept_backup.txt + @cat $(DOCS_VOCAB)/accept.txt > $(DOCS_VOCAB)/accept_backup.txt + @cat $(DOCS_SOURCEDIR)/.custom_wordlist.txt >> $(DOCS_VOCAB)/accept.txt + @echo "Running Vale acceptable term check against $(CHECK_PATH). To change target set CHECK_PATH= with make command" + @. $(DOCS_VENV); vale --config="$(VALE_CONFIG)" --filter='$(DEV_DIR)/styles/woke.filter' --glob='*.{md,rst}' $(CHECK_PATH) + @cat $(DOCS_VOCAB)/accept_backup.txt > $(DOCS_VOCAB)/accept.txt && rm $(DOCS_VOCAB)/accept_backup.txt vale: vale-install - @cat $(VOCAB_CANONICAL)/accept.txt > $(VOCAB_CANONICAL)/accept_backup.txt - @cat $(SOURCEDIR)/.custom_wordlist.txt >> $(VOCAB_CANONICAL)/accept.txt - @echo "Running Vale against $(TARGET). To change target set TARGET= with make command" - @. $(VENV); vale --config="$(VALE_CONFIG)" --filter='$(SPHINXDIR)/styles/error.filter' --glob='*.{md,rst}' $(TARGET) - @cat $(VOCAB_CANONICAL)/accept_backup.txt > $(VOCAB_CANONICAL)/accept.txt && rm $(VOCAB_CANONICAL)/accept_backup.txt + @cat $(DOCS_VOCAB)/accept.txt > $(DOCS_VOCAB)/accept_backup.txt + @cat $(DOCS_SOURCEDIR)/.custom_wordlist.txt >> $(DOCS_VOCAB)/accept.txt + @echo "Running Vale against $(CHECK_PATH). To change target set CHECK_PATH= with make command" + @. $(DOCS_VENV); vale --config="$(VALE_CONFIG)" --filter='$(DEV_DIR)/styles/error.filter' --glob='*.{md,rst}' $(CHECK_PATH) + @cat $(DOCS_VOCAB)/accept_backup.txt > $(DOCS_VOCAB)/accept.txt && rm $(DOCS_VOCAB)/accept_backup.txt spelling: vale-install - @cat $(VOCAB_CANONICAL)/accept.txt > $(VOCAB_CANONICAL)/accept_backup.txt - @cat $(SOURCEDIR)/.custom_wordlist.txt >> $(VOCAB_CANONICAL)/accept.txt - @echo "Running Vale against $(TARGET). To change target set TARGET= with make command" - @. $(VENV); vale --config="$(VALE_CONFIG)" --filter='$(SPHINXDIR)/styles/spelling.filter' --glob='*.{md,rst}' $(TARGET) - @cat $(VOCAB_CANONICAL)/accept_backup.txt > $(VOCAB_CANONICAL)/accept.txt && rm $(VOCAB_CANONICAL)/accept_backup.txt + @cat $(DOCS_VOCAB)/accept.txt > $(DOCS_VOCAB)/accept_backup.txt + @cat $(DOCS_SOURCEDIR)/.custom_wordlist.txt >> $(DOCS_VOCAB)/accept.txt + @echo "Running Vale against $(CHECK_PATH). To change target set CHECK_PATH= with make command" + @. $(DOCS_VENV); vale --config="$(VALE_CONFIG)" --filter='$(DEV_DIR)/styles/spelling.filter' --glob='*.{md,rst}' $(CHECK_PATH) + @cat $(DOCS_VOCAB)/accept_backup.txt > $(DOCS_VOCAB)/accept.txt && rm $(DOCS_VOCAB)/accept_backup.txt spellcheck: spelling @echo "Please note that the \`make spellcheck\` command is being deprecated in favor of \`make spelling\`" pdf-prep: install - @for packageName in $(REQPDFPACKS); do (dpkg-query -W -f='$${Status}' $$packageName 2>/dev/null | \ + @for packageName in $(DOCS_PDFPACKAGES); do (dpkg-query -W -f='$${Status}' $$packageName 2>/dev/null | \ grep -c "ok installed" >/dev/null && echo "Package $$packageName is installed") && continue || \ - (echo; echo "PDF generation requires the installation of the following packages: $(REQPDFPACKS)" && \ + (echo; echo "PDF generation requires the installation of the following packages: $(DOCS_PDFPACKAGES)" && \ echo "" && echo "Run 'sudo make pdf-prep-force' to install these packages" && echo "" && echo \ "Please be aware these packages will be installed to your system") && exit 1 ; done pdf-prep-force: apt-get update apt-get upgrade -y - apt-get install --no-install-recommends -y $(REQPDFPACKS) \ + apt-get install --no-install-recommends -y $(DOCS_PDFPACKAGES) \ pdf: pdf-prep - @. $(VENV); sphinx-build -M latexpdf "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) - @rm ./$(BUILDDIR)/latex/front-page-light.pdf || true - @rm ./$(BUILDDIR)/latex/normal-page-footer.pdf || true - @find ./$(BUILDDIR)/latex -name "*.pdf" -exec mv -t ./$(BUILDDIR) {} + - @rm -r $(BUILDDIR)/latex + @. $(DOCS_VENV); $(SPHINX_BUILD) -M latexpdf "$(DOCS_SOURCEDIR)" "$(DOCS_BUILDDIR)" $(SPHINX_OPTS) + @rm ./$(DOCS_BUILDDIR)/latex/front-page-light.pdf || true + @rm ./$(DOCS_BUILDDIR)/latex/normal-page-footer.pdf || true + @find ./$(DOCS_BUILDDIR)/latex -name "*.pdf" -exec mv -t ./$(DOCS_BUILDDIR) {} + + @rm -r $(DOCS_BUILDDIR)/latex @echo - @echo "Output can be found in ./$(BUILDDIR)" + @echo "Output can be found in ./$(DOCS_BUILDDIR)" @echo update: install - @. $(VENV); .sphinx/update_sp.py + @. $(DOCS_VENV); _dev/update_sp.py # Catch-all target: route all unknown targets to Sphinx using the new -# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +# "make mode" option. $(O) is meant as a shortcut for $(SPHINX_OPTS). %: $(MAKE) --no-print-directory install - . $(VENV); $(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + . $(DOCS_VENV); $(SPHINX_BUILD) -M $@ "$(DOCS_SOURCEDIR)" "$(DOCS_BUILDDIR)" $(SPHINX_OPTS) $(O) diff --git a/docs/.sphinx/.pre-commit-config.yaml b/docs/_dev/.pre-commit-config.yaml similarity index 100% rename from docs/.sphinx/.pre-commit-config.yaml rename to docs/_dev/.pre-commit-config.yaml diff --git a/docs/.sphinx/.pymarkdown.json b/docs/_dev/.pymarkdown.json similarity index 100% rename from docs/.sphinx/.pymarkdown.json rename to docs/_dev/.pymarkdown.json diff --git a/docs/_dev/check_removed_urls.py b/docs/_dev/check_removed_urls.py new file mode 100644 index 00000000..ce8c9167 --- /dev/null +++ b/docs/_dev/check_removed_urls.py @@ -0,0 +1,95 @@ +#! /usr/bin/env python + +"""Check for removed URLs and verify if redirects exist.""" + +import csv +import io +import sys +from pathlib import Path + + +def read_urls(path): + return { + line.strip() + for line in path.read_text(encoding="utf-8").splitlines() + if line.strip() + } + + +def read_redirect_sources(path): + sources = set() + + if not path.exists(): + return sources + + for raw_line in path.read_text(encoding="utf-8").splitlines(): + line = raw_line.strip() + if not line or line.startswith("#"): + continue + + fields = next( + csv.reader( + io.StringIO(line), + delimiter=" ", + quotechar='"', + skipinitialspace=True, + ), + [], + ) + if fields: + sources.add(fields[0]) + + return sources + + +def source_candidates_for_url(url): + clean_path = url.strip() + clean_path = clean_path.removeprefix("./") + clean_path = clean_path.removeprefix("/") + clean_path = clean_path.removesuffix(".html") + clean_path = clean_path.rstrip("/") + + if not clean_path: + return {"index.md"} + + return { + f"{clean_path}.md", + f"{clean_path}/index.md", + f"{clean_path}/", + } + + +def main(): + base_urls = Path("base/docs/urls.txt") + compare_urls = Path("compare/docs/urls.txt") + redirects = Path("compare/docs/redirects.txt") + + if not base_urls.exists(): + print(f"Error: Base URLs file not found at {base_urls}") + sys.exit(1) + if not compare_urls.exists(): + print(f"Error: Compare URLs file not found at {compare_urls}") + sys.exit(1) + + removed_urls = sorted(read_urls(base_urls) - read_urls(compare_urls)) + redirect_sources = read_redirect_sources(redirects) + + missing_redirects = [ + url + for url in removed_urls + if source_candidates_for_url(url).isdisjoint(redirect_sources) + ] + + if missing_redirects: + print("The following URLs were removed without redirects:") + print("\n".join(missing_redirects)) + print("Please ensure removed pages are redirected") + sys.exit(1) + + if removed_urls: + print("Removed URLs have redirects:") + print("\n".join(removed_urls)) + + +if __name__ == "__main__": + main() diff --git a/docs/.sphinx/get_vale_conf.py b/docs/_dev/get_vale_conf.py old mode 100755 new mode 100644 similarity index 97% rename from docs/.sphinx/get_vale_conf.py rename to docs/_dev/get_vale_conf.py index 13e7966f..b09404ae --- a/docs/.sphinx/get_vale_conf.py +++ b/docs/_dev/get_vale_conf.py @@ -15,7 +15,7 @@ datefmt='%Y-%m-%d %H:%M:%S' ) -SPHINX_DIR = os.path.join(os.getcwd(), ".sphinx") +DEV_DIR = os.path.join(os.getcwd(), "_dev") GITHUB_REPO = "canonical/documentation-style-guide" GITHUB_CLONE_URL = f"https://github.com/{GITHUB_REPO}.git" @@ -133,7 +133,7 @@ def parse_arguments(): def main(): # Define local directory paths - vale_files_dict = {file: os.path.join(SPHINX_DIR, file) for file in VALE_FILE_LIST} + vale_files_dict = {file: os.path.join(DEV_DIR, file) for file in VALE_FILE_LIST} # Parse command line arguments, default to overwrite_enabled = True overwrite_enabled = not parse_arguments().no_overwrite diff --git a/docs/.sphinx/pa11y.json b/docs/_dev/pa11y.json similarity index 100% rename from docs/.sphinx/pa11y.json rename to docs/_dev/pa11y.json diff --git a/docs/.sphinx/update_sp.py b/docs/_dev/update_sp.py old mode 100755 new mode 100644 similarity index 87% rename from docs/.sphinx/update_sp.py rename to docs/_dev/update_sp.py index a9259d02..d71299c2 --- a/docs/.sphinx/update_sp.py +++ b/docs/_dev/update_sp.py @@ -1,30 +1,30 @@ #! /usr/bin/env python -# Initial update script for the starter pack. +# Initial update script for the Sphinx Stack. # # Requires some manual intervention, but makes identifying updates and differences easier. # # For debugging, please run this script with DEBUGGING=1 -# e.g. user@device:~/git/Canonical/sphinx-docs-starter-pack/docs$ DEBUGGING=1 python .sphinx/update_sp.py +# e.g. user@device:~/git/Canonical/sphinx-stack/docs$ DEBUGGING=1 python _dev/update_sp.py import glob import logging import os -import requests import re import subprocess import sys -from requests.exceptions import RequestException +import requests from packaging.version import parse as parse_version +from requests.exceptions import RequestException SPHINX_DIR = os.path.abspath(os.path.dirname(__file__)) DOCS_DIR = os.path.abspath(os.path.join(SPHINX_DIR, '..')) REQUIREMENTS = os.path.join(DOCS_DIR, "requirements.txt") SPHINX_UPDATE_DIR = os.path.join(SPHINX_DIR, "update") -GITHUB_REPO = "canonical/sphinx-docs-starter-pack" +GITHUB_REPO = "canonical/sphinx-stack" GITHUB_API_BASE = f"https://api.github.com/repos/{GITHUB_REPO}" -GITHUB_API_SPHINX_DIR = f"{GITHUB_API_BASE}/contents/docs/.sphinx" +GITHUB_API_DEV_DIR = f"{GITHUB_API_BASE}/contents/docs/_dev" GITHUB_RAW_BASE = f"https://raw.githubusercontent.com/{GITHUB_REPO}/main" TIMEOUT = 10 # seconds @@ -43,7 +43,7 @@ def main(): except FileNotFoundError: print("WARNING\nWARNING\nWARNING") print( - "You need to update to at least version 1.0.0 of the starter pack to start using the update function." + "You need to update to at least version 1.0.0 of the Sphinx Stack to start using the update function." ) print("You may experience issues using this functionality.") logging.debug("No local version found. Setting version to None") @@ -61,15 +61,15 @@ def main(): logging.debug("Comparing versions") if parse_version(local_version) < parse_version(latest_release): logging.debug("Local version is older than the release version.") - print("Starter pack is out of date.\n") + print("Sphinx Stack is out of date.\n") - # Identify and download '.sphinx' dir files to '.sphinx/update' + # Identify and download '_dev' dir files to '_dev/update' files_updated, new_files = update_static_files() - # Write new version to file to '.sphinx/update' + # Write new version to file to '_dev/update' download_file( - GITHUB_RAW_BASE + "/docs/.sphinx/version", + GITHUB_RAW_BASE + "/docs/_dev/version", os.path.join(SPHINX_UPDATE_DIR, "version"), ) @@ -84,8 +84,8 @@ def main(): if files_updated: logging.debug("Updated files found and downloaded") print("Differences have been identified in static files.") - print("Updated files have been downloaded to '.sphinx/update'.") - print("Validate and move these files into your '.sphinx/' directory.") + print("Updated files have been downloaded to '_dev/update'.") + print("Validate and move these files into your '_dev/' directory.") else: logging.debug("No files found to update") # Provide information on NEW files @@ -94,7 +94,7 @@ def main(): print( "NOTE: New files have been downloaded\n", "See 'NEWFILES.txt' for all downloaded files\n", - "Validate and merge these files into your '.sphinx/' directory", + "Validate and merge these files into your '_dev/' directory", ) else: logging.debug("No new files found to download") @@ -130,19 +130,19 @@ def main(): except FileNotFoundError: print("requirements.txt not found") print( - "The updated starter pack has moved requirements.txt out of the '.sphinx' dir" + "The updated Sphinx Stack has moved requirements.txt out of the '_dev' dir" ) print("requirements.txt not checked, please update your requirements manually") def update_static_files(): - """Checks local files against remote for new and different files, downloads to '.sphinx/updates'""" + """Checks local files against remote for new and different files, downloads to '_dev/updates'""" files, paths = get_local_files_and_paths() new_file_list = [] - for item in query_api(GITHUB_API_SPHINX_DIR).json(): + for item in query_api(GITHUB_API_DEV_DIR).json(): logging.debug(f"Checking {item['name']}") - # Checks existing files in '.sphinx' starter pack static root for changed SHA + # Checks existing files in '_dev' Sphinx Stack static root for changed SHA if item["name"] in files and item["type"] == "file": index = files.index(item["name"]) if item["sha"] != get_git_revision_hash(paths[index]): @@ -154,16 +154,16 @@ def update_static_files(): # Indicate update script needs to be updated and re-run print("WARNING") print( - "THIS UPDATE SCRIPT IS OUT OF DATE. YOU MAY NEED TO RUN ANOTHER UPDATE AFTER UPDATING TO THE FILE IN '.sphinx/updates'." + "THIS UPDATE SCRIPT IS OUT OF DATE. YOU MAY NEED TO RUN ANOTHER UPDATE AFTER UPDATING TO THE FILE IN '_dev/updates'." ) print("WARNING\n") else: logging.debug("File hashes are equal") - # Checks nested files '.sphinx/**/**.*' for changed SHA (single level of depth) + # Checks nested files '_dev/**/**.*' for changed SHA (single level of depth) elif item["type"] == "dir": logging.debug(item["name"] + " is a directory") for nested_item in query_api( - f"{GITHUB_API_SPHINX_DIR}/{item['name']}" + f"{GITHUB_API_DEV_DIR}/{item['name']}" ).json(): logging.debug(f"Checking {nested_item['name']}") if nested_item["name"] in files: @@ -189,7 +189,7 @@ def update_static_files(): SPHINX_UPDATE_DIR, item["name"], nested_item["name"] ), ) - # Downloads NEW files in '.sphinx' starter pack static root + # Downloads NEW files in '_dev' Sphinx Stack static root else: if item["type"] == "file": logging.debug(f"No local version found of {item['name']}") @@ -225,7 +225,7 @@ def get_git_revision_hash(file) -> str: # Examines local files def get_local_files_and_paths(): - """Identify '.sphinx' local files and paths""" + """Identify '_dev' local files and paths""" logging.debug("Checking local files and paths") try: files = [] diff --git a/docs/_dev/version b/docs/_dev/version new file mode 100644 index 00000000..cd5ac039 --- /dev/null +++ b/docs/_dev/version @@ -0,0 +1 @@ +2.0 diff --git a/docs/conf.py b/docs/conf.py index e002335f..968afd21 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,5 +1,6 @@ import datetime import os +import textwrap import yaml # Configuration for the Sphinx documentation builder. @@ -91,7 +92,7 @@ # TODO: To customise the favicon, uncomment and update as needed. -# html_favicon = '.sphinx/_static/favicon.png' +# html_favicon = '_dev/_static/favicon.png' # Dictionary of values to pass into the Sphinx context for all pages: @@ -105,7 +106,7 @@ # # TODO: If there's no such website, # remove the {{ product_page }} link from the page header template - # (usually .sphinx/_templates/header.html; also, see README.rst). + # (usually _dev/_templates/header.html; also, see README.rst). "product_page": "snapcraft.io", # Product tag image; the orange part of your logo, shown in the page header # @@ -395,6 +396,7 @@ "sphinx_config_options", "sphinx_contributor_listing", "sphinx_filtered_toctree", + "sphinx_llm.txt", "sphinx_related_links", "sphinx_roles", "sphinx_terminal", @@ -404,7 +406,7 @@ "sphinx_last_updated_by_git", "sphinx.ext.intersphinx", "sphinx_sitemap", - "sphinxext.rediraffe", + "sphinx_rerediraffe", "sphinxcontrib.mermaid", ] @@ -412,6 +414,8 @@ exclude_patterns = [ "doc-cheat-sheet*", + ".venv*", + "_dev", ] # Adds custom CSS files, located under 'html_static_path' @@ -424,6 +428,21 @@ # Add redirects, so they can be updated here to land with docs being moved rediraffe_branch = "main" rediraffe_redirects = "redirects.txt" +rediraffe_dir_only = True + +############################ +# sphinx-llm configuration # +############################ + +llms_txt_description = textwrap.dedent( + """\ + This is the documentation for Snap, a software packaging and deployment system + developed by Canonical. + """ +) + +if os.environ.get("READTHEDOCS"): + markdown_http_base = html_baseurl # Adds custom JavaScript files, located under 'html_static_path' @@ -488,4 +507,3 @@ # Suppress missing xref warnings, as these are generated for targets automatically suppress_warnings = ['myst.xref_missing'] - diff --git a/docs/contributing/index.md b/docs/contributing/index.md index 8b87e417..c9a71665 100644 --- a/docs/contributing/index.md +++ b/docs/contributing/index.md @@ -16,9 +16,9 @@ The documentation for _snap_ is [hosted in GitHub](https://github.com/canonical/snap-docs) and rendered, via [Read the Docs](https://about.readthedocs.com/), to [https://snapcraft.io/docs/](https://snapcraft.io/docs/). -We use the [Sphinx documentation generator](https://www.sphinx-doc.org/) to create our documentation, which is written in [MyST Markdown](https://mystmd.org/) and built from [Canonical's Sphinx Starter Pack](https://github.com/canonical/sphinx-docs-starter-pack). +We use the [Sphinx documentation generator](https://www.sphinx-doc.org/) to create our documentation, which is written in [MyST Markdown](https://mystmd.org/) and built with [Canonical's Sphinx Stack](https://github.com/canonical/sphinx-stack). -For further details, see the [Starter Pack documentation](https://canonical-starter-pack.readthedocs-hosted.com/stable/). +For further details, see the [Sphinx Stack documentation](https://documentation.ubuntu.com/sphinx-stack/). ## The Open Documentation Academy diff --git a/docs/how-to-guides/snap-development/use-the-secret-portal.md b/docs/how-to-guides/snap-development/use-the-secret-portal.md index e83dbe5d..158ff4a9 100644 --- a/docs/how-to-guides/snap-development/use-the-secret-portal.md +++ b/docs/how-to-guides/snap-development/use-the-secret-portal.md @@ -7,11 +7,11 @@ This {ref}`portal` allows applications to get a In the following section, we will build a snap to demonstrate a fully working Secret portal example. -### Prerequisites +## Prerequisites Make sure that your OS supports the secret-portal. `xdg-desktop-portal` version must be equal or greater than 1.5.0. On Ubuntu, it is supported on Ubuntu 20.04 onwards. -### Building the snap +## Building the snap The most common way to manage secrets in Linux environments is with [libsecret](https://gnome.pages.gitlab.gnome.org/libsecret/). @@ -47,7 +47,7 @@ The snap can be built with the `snapcraft pack` command. See [Craft a snap](http When installing the snap, note that , as for the other xdg-desktop-portals, the desktop {ref}`interface must be plugged` to use the secret-portal. -### Verifying the behavior +## Verifying the behavior Install the secret-tool from the archive (sudo apt install libsecret-tools). We now have two different instances of the secret-tool: diff --git a/docs/requirements.txt b/docs/requirements.txt index 057ce3b3..2c7950bc 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,36 +1,37 @@ # Canonical theme (still needed for Furo theme and custom templates) -canonical-sphinx>=0.5.1 +canonical-sphinx~=0.6 # Extensions previously auto-loaded by canonical-sphinx myst-parser~=4.0 # v5.0.0 causes version conflicts sphinx-autobuild -sphinx-design -sphinx-notfound-page -sphinx-reredirects -sphinx-tabs -sphinxcontrib-jquery -sphinxext-opengraph +sphinx-design==0.6.1 +sphinx-notfound-page~=1.1 +sphinx-reredirects==0.1.6 +sphinx-tabs~=3.5 +sphinxcontrib-jquery~=4.1 +sphinxext-opengraph~=0.13 +sphinx-rerediraffe>=0.0.3,<1.0.0 # Extra extensions, previously bundled as canonical-sphinx-extensions -sphinx-config-options>=0.1.0 -sphinx-contributor-listing>=0.1.0 -sphinx-filtered-toctree>=0.1.0 -sphinx-related-links>=0.1.1 -sphinx-roles>=0.1.0 -sphinx-terminal>=1.0.2 -sphinx-ubuntu-images>=0.1.0 -sphinx-youtube-links>=0.1.0 +sphinx-config-options~=0.1 +sphinx-contributor-listing~=0.1 +sphinx-filtered-toctree~=0.1 +sphinx-related-links~=0.1 +sphinx-roles~=0.1 +sphinx-terminal~=1.0 +sphinx-ubuntu-images~=0.1 +sphinx-youtube-links~=0.1 # Other dependencies -packaging -sphinxcontrib-svg2pdfconverter[CairoSVG] -sphinx-last-updated-by-git -sphinx-sitemap +packaging~=26.1 +sphinxcontrib-svg2pdfconverter[CairoSVG]~=2.1 +sphinx-last-updated-by-git~=0.3 +sphinx-sitemap~=2.9 +sphinx-llm~=0.4 # Vale dependencies rst2html -vale +vale==3.13.0.0 # For snap-docs -sphinxext-rediraffe sphinxcontrib-mermaid From c398128ce200576a24ca18692b79d82dbea578cb Mon Sep 17 00:00:00 2001 From: Raimundo Henriques Date: Thu, 1 Oct 2026 13:29:48 +0200 Subject: [PATCH 2/2] build: use sphinx-stack defaults unless costumizations are in place --- docs/Makefile | 18 +++++++++++++++--- docs/_dev/check_removed_urls.py | 3 +++ docs/_dev/get_vale_conf.py | 0 docs/_dev/update_sp.py | 7 +++---- 4 files changed, 21 insertions(+), 7 deletions(-) mode change 100644 => 100755 docs/_dev/check_removed_urls.py mode change 100644 => 100755 docs/_dev/get_vale_conf.py mode change 100644 => 100755 docs/_dev/update_sp.py diff --git a/docs/Makefile b/docs/Makefile index 5d7f2856..856fa7dd 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -4,6 +4,7 @@ # You can set these variables from the command line, and also # from the environment for the first two. + DEV_DIR ?= _dev SPHINX_OPTS ?= -c . -d $(DEV_DIR)/.doctrees -j auto SPHINX_BUILD ?= $(DOCS_VENVDIR)/bin/sphinx-build @@ -108,9 +109,21 @@ linkcheck: install pa11y: pa11y-install html find $(DOCS_BUILDDIR) -name *.html -print0 | xargs -n 1 -0 $(PA11Y_CMD) -# Without --return-code-scheme explicit, pymarkdownlnt returns 1 for multiple scenarios. +# Without --return-code-scheme explicit, pymarkdownlnt returns 1 for multiple scenarios +# By using the explicit scheme, it only returns 1 when no files are found, +# which should not result in failure lint-md: pymarkdownlnt-install - @. $(DOCS_VENV); pymarkdownlnt --config $(DEV_DIR)/.pymarkdown.json --return-code-scheme explicit scan --recurse $(CHECK_PATH); status=$$?; if [ $$status -eq 1 ]; then echo "No Markdown files selected for linting"; exit 0; fi; exit $$status; + @. $(DOCS_VENV); pymarkdownlnt \ + --config $(DEV_DIR)/.pymarkdown.json \ + --return-code-scheme explicit \ + scan --recurse $(CHECK_PATH); \ + status=$$?; \ + if [ $$status -eq 1 ]; then \ + echo "No Markdown files selected for linting"; \ + exit 0; \ + fi; \ + echo "pymarkdownlnt exited with code $$status"; \ + exit $$status; vale-install: install @. $(DOCS_VENV); test -f $(VALE_CONFIG) || python3 $(DEV_DIR)/get_vale_conf.py @@ -152,7 +165,6 @@ pdf-prep: install pdf-prep-force: apt-get update - apt-get upgrade -y apt-get install --no-install-recommends -y $(DOCS_PDFPACKAGES) \ pdf: pdf-prep diff --git a/docs/_dev/check_removed_urls.py b/docs/_dev/check_removed_urls.py old mode 100644 new mode 100755 index ce8c9167..26523987 --- a/docs/_dev/check_removed_urls.py +++ b/docs/_dev/check_removed_urls.py @@ -52,6 +52,9 @@ def source_candidates_for_url(url): if not clean_path: return {"index.md"} + # A removed dirhtml URL can map back to either a page file or an + # index file. Directory-level redirects are stored with a trailing + # slash, so include that form too. return { f"{clean_path}.md", f"{clean_path}/index.md", diff --git a/docs/_dev/get_vale_conf.py b/docs/_dev/get_vale_conf.py old mode 100644 new mode 100755 diff --git a/docs/_dev/update_sp.py b/docs/_dev/update_sp.py old mode 100644 new mode 100755 index d71299c2..3014a84f --- a/docs/_dev/update_sp.py +++ b/docs/_dev/update_sp.py @@ -14,12 +14,13 @@ import re import subprocess import sys + import requests from packaging.version import parse as parse_version from requests.exceptions import RequestException SPHINX_DIR = os.path.abspath(os.path.dirname(__file__)) -DOCS_DIR = os.path.abspath(os.path.join(SPHINX_DIR, '..')) +DOCS_DIR = os.path.abspath(os.path.join(SPHINX_DIR, "..")) REQUIREMENTS = os.path.join(DOCS_DIR, "requirements.txt") SPHINX_UPDATE_DIR = os.path.join(SPHINX_DIR, "update") GITHUB_REPO = "canonical/sphinx-stack" @@ -162,9 +163,7 @@ def update_static_files(): # Checks nested files '_dev/**/**.*' for changed SHA (single level of depth) elif item["type"] == "dir": logging.debug(item["name"] + " is a directory") - for nested_item in query_api( - f"{GITHUB_API_DEV_DIR}/{item['name']}" - ).json(): + for nested_item in query_api(f"{GITHUB_API_DEV_DIR}/{item['name']}").json(): logging.debug(f"Checking {nested_item['name']}") if nested_item["name"] in files: index = files.index(nested_item["name"])