Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
89 changes: 89 additions & 0 deletions .github/scripts/check_site.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
# Copyright (c) 2026 inovex GmbH
#
# SPDX-License-Identifier: Apache-2.0
"""Check the internal links of the assembled Pages site.

Usage: python check_site.py <site dir>

The site is served under a path prefix (https://tiacsys.github.io/zdocs/).
A static server maps a URL to a file, so the check resolves every href and
src of every HTML page against the file system, relative to the page:

- the target file must exist (a directory needs an index.html),
- the target must stay inside the site directory (a link that climbs out
of it leaves the prefix and fails on GitHub Pages),
- a #fragment on an HTML target must name an id or a name in that page.

A ?query is ignored. Links with a scheme (https:, mailto:, ...) and
protocol-relative links are not checked. The exit status is 1 if there is
a broken link.
"""

import sys
from html.parser import HTMLParser
from pathlib import Path
from urllib.parse import unquote, urldefrag, urlparse


class Page(HTMLParser):
def __init__(self):
super().__init__()
self.links, self.ids = [], set()

def handle_starttag(self, tag, attrs):
a = dict(attrs)
for key in ("id", "name"):
if a.get(key):
self.ids.add(a[key])
for key in ("href", "src"):
if a.get(key):
self.links.append(a[key])


_PAGES = {}


def parse(path):
if path not in _PAGES:
page = Page()
page.feed(path.read_text(encoding="utf-8", errors="replace"))
_PAGES[path] = page
return _PAGES[path]


def main(root):
root = root.resolve()
pages = sorted(root.rglob("*.html"))
broken, checked = [], 0
for page_path in pages:
for link in parse(page_path).links:
url = urlparse(link)
if url.scheme or url.netloc or link.startswith("#"):
continue
checked += 1
target_ref, fragment = urldefrag(link)
target_ref = target_ref.split("?", 1)[0]
if target_ref.startswith("/"):
broken.append((page_path, link, "absolute path"))
continue
target = (page_path.parent / unquote(target_ref)).resolve()
if not target.is_relative_to(root):
broken.append((page_path, link, "outside the site"))
continue
if target.is_dir():
target = target / "index.html"
if not target.is_file():
broken.append((page_path, link, "no such file"))
continue
if fragment and target.suffix == ".html" and unquote(fragment) not in parse(target).ids:
broken.append((page_path, link, "no such #fragment"))
print(f"check_site: {len(pages)} pages, {checked} internal links, {len(broken)} broken")
for page_path, link, reason in broken:
print(f" {page_path.relative_to(root)}: {link} ({reason})")
return 1 if broken else 0


if __name__ == "__main__":
if len(sys.argv) != 2:
sys.exit(__doc__)
sys.exit(main(Path(sys.argv[1])))
194 changes: 194 additions & 0 deletions .github/workflows/docs.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
# Copyright (c) 2026 inovex GmbH
#
# SPDX-License-Identifier: Apache-2.0
#
# Builds the documentation of zdocs itself (doc/: the manual and the API
# reference) and publishes it to GitHub Pages
# (https://tiacsys.github.io/zdocs/).
#
# - Pull request: build and checks only.
# - Push to main, or a manual run on main: build, checks, deploy.
#
# Not in this workflow: the engine unit tests, the zdocs-tests acceptance
# suite and twister runs. This document set has no PDF and no Doxygen
# document.
#
# One-time repository setting: Settings > Pages > Source = "GitHub Actions".

name: Documentation

on:
push:
branches: [main]
pull_request:
workflow_dispatch:

permissions:
contents: read

concurrency:
group: docs-${{ github.ref }}
cancel-in-progress: true

env:
# zdocs runs find_package(Doxygen REQUIRED) at configure time, also for a
# document set without a Doxygen document. The job uses the same Doxygen
# as the consumers (>= 1.16 for native \verifies and \satisfies): the
# official release binary, checked by hash. Ubuntu has older packages.
DOXYGEN_VERSION: "1.16.1"
DOXYGEN_SHA256: "a56f885d37e3aae08a99f638d17bbb381224c03a878d9e2dda4f9fa4baf1d8bd"

jobs:
build:
name: Build documentation
runs-on: ubuntu-24.04
env:
# find_package(Zephyr) in doc/CMakeLists.txt finds Zephyr through
# ZEPHYR_BASE. West does not set it.
ZEPHYR_BASE: ${{ github.workspace }}/zephyr
steps:
# The west workspace is the job's work directory: this repository is
# the manifest repository at zdocs/, doc/west.yml adds zephyr/.
- name: Check out
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
path: zdocs
persist-credentials: false

- name: Set up Python
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: "3.12"
cache: pip
cache-dependency-path: zdocs/doc/requirements-ci.txt

- name: Install Python packages
run: python -m pip install -r zdocs/doc/requirements-ci.txt

# dot renders the graphviz diagrams of the architecture pages.
- name: Install Graphviz
run: |
sudo apt-get update
sudo apt-get install --yes --no-install-recommends graphviz

- name: Restore Doxygen
id: doxygen-cache
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: ~/doxygen
key: doxygen-${{ env.DOXYGEN_VERSION }}-${{ env.DOXYGEN_SHA256 }}

- name: Download Doxygen
if: steps.doxygen-cache.outputs.cache-hit != 'true'
run: |
tag="Release_${DOXYGEN_VERSION//./_}"
curl -sSfL -o doxygen.tar.gz \
"https://github.com/doxygen/doxygen/releases/download/${tag}/doxygen-${DOXYGEN_VERSION}.linux.bin.tar.gz"
echo "${DOXYGEN_SHA256} doxygen.tar.gz" | sha256sum --check --strict
mkdir -p ~/doxygen
tar -xzf doxygen.tar.gz -C ~/doxygen --strip-components=1
rm doxygen.tar.gz

- name: Put Doxygen on the path
run: echo "${HOME}/doxygen/bin" >> "${GITHUB_PATH}"

- name: Show tool versions
run: |
doxygen --version
dot -V
cmake --version | head -n 1
python --version
west --version
python -m pip freeze

# Zephyr is needed only as a CMake package (find_package(Zephyr
# COMPONENTS doc)) and for its doc/_extensions: no toolchain, no SDK,
# no module. A narrow, shallow update fetches Zephyr only, at depth 1.
- name: Set up the west workspace
run: |
west init -l --mf doc/west.yml zdocs
west update --narrow --fetch-opt=--depth=1
west list

- name: Configure
run: cmake -S zdocs/doc -B build/doc

# Stage two of the build is all-or-nothing, so the stage-one indexes
# of every document come first. all-docs runs doc-check as its last
# step.
- name: Build the documents
run: |
cmake --build build/doc --target doc-index
cmake --build build/doc --target all-docs

- name: Check cross-document links
run: cmake --build build/doc --target doc-check

# The stage-two logs (html.log) hold only the warnings of the final
# build. The baseline is zero warnings.
- name: Check for warnings
run: |
if grep -nE 'WARNING|ERROR' build/doc/*/build/html.log; then
echo "::error::the documentation build has warnings"
exit 1
fi

# The site root is deploy/html/, as doc/manual/explanation/
# deploy-layout.rst describes: the documents are at <document>/. The
# landing page sits at the root, next to them. This document set has
# no Doxygen document, so the Doxygen cross-document navigation (it
# expects the deploy/ level as the root) is not in the site.
- name: Assemble the site
run: |
cp -a build/doc/deploy/html build/site
cp zdocs/doc/site/index.html build/site/index.html

- name: Check the links of the site
run: python zdocs/.github/scripts/check_site.py build/site

# Nothing private goes public: no local paths, no addresses, no tokens.
# The reference pages use jane@example.com and john@example.com as
# sample values, so example.com, .org and .net are accepted.
- name: Audit the site
working-directory: build/site
run: |
status=0
refuse() {
if grep -rlIE -- "$2" .; then
echo "::error::the site contains $1"
status=1
fi
}
refuse "a local absolute path" '/home/[^/ ]+/|/Users/|/tmp/|/wrk/'
refuse "a local server address" 'localhost:[0-9]|127\.0\.0\.1'
refuse "a token or a private key" 'ghp_|gho_|github_pat_|glpat-|AKIA[0-9A-Z]{16}|xox[baprs]-|-----BEGIN [A-Z ]*PRIVATE KEY'
if grep -rhoIE '[A-Za-z0-9._%+-]+@[A-Za-z0-9-]+(\.[A-Za-z0-9-]+)*\.[A-Za-z]{2,}' . \
| grep -vE '@example\.(com|org|net)$'; then
echo "::error::the site contains an e-mail address"
status=1
fi
exit "${status}"

- name: Upload the site
uses: actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9 # v5.0.0
with:
path: build/site

deploy:
name: Deploy to GitHub Pages
if: github.event_name != 'pull_request' && github.ref == 'refs/heads/main'
needs: build
runs-on: ubuntu-24.04
permissions:
pages: write
id-token: write
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
concurrency:
group: pages
cancel-in-progress: false
steps:
- name: Deploy
id: deployment
uses: actions/deploy-pages@368f82528645a54fb793d4d04e342629a3f51346 # v5.0.1
76 changes: 76 additions & 0 deletions doc/requirements-ci.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
# Copyright (c) 2026 inovex GmbH
#
# SPDX-License-Identifier: Apache-2.0
#
# Python packages for the documentation workflow
# (.github/workflows/docs.yml). Every package is pinned, also the indirect
# ones, so that a new release cannot change the CI build.
#
# pip install -r doc/requirements-ci.txt
#
# The engine versions are the ones in sphinx/requirements-doc.txt, and the
# document-set version is the one in doc/requirements.txt. Change a version
# here only together with the same change in those files, and with a full
# docs build. sphinx-autobuild and sphobjinv are not here: the workflow does
# not use the html-live targets or `needs: source: inventory`.

# Workspace and Zephyr module discovery (find_package(Zephyr) runs
# zephyr_module.py, which validates module.yml files with jsonschema).
west==1.4.0
jsonschema==4.26.0
PyYAML==6.0.3

# The zdocs engine (sphinx/requirements-doc.txt).
Sphinx==9.1.0
sphinx_rtd_theme==3.1.0
sphinx-needs==8.3.0
sphinxcontrib-doxylink==1.13.0

# This document set only (doc/requirements.txt).
sphinxcontrib-moderncmakedomain==3.29.0

# Pygments 2.21.0 writes quotes in highlighted code without escaping. Then
# doccheck reads the HREF_RE pattern in the viewcode page of doccheck.py as a
# dead link ("html/api/_modules/doccheck.html: dead link -> ([^"). Remove this
# pin when doccheck's HREF_RE ignores code (a separate engine fix).
Pygments==2.20.0

# Indirect packages (Python 3.12). Each version is the one that the zdocs
# test suites ran with, if that environment has the package.
alabaster==1.0.0
attrs==26.1.0
babel==2.18.0
certifi==2026.7.22
charset-normalizer==3.4.9
colorama==0.4.6
docopt==0.6.2
docutils==0.22.4
idna==3.18
imagesize==2.0.0
Jinja2==3.1.6
jsonschema-specifications==2025.9.1
jsonschema_rs==0.37.4
MarkupSafe==3.0.3
minijinja==2.22.0
packaging==26.3
pykwalify==1.8.0
pyparsing==3.3.2
python-dateutil==2.9.0.post0
referencing==0.37.0
requests==2.34.2
requests-file==2.1.0
roman-numerals==4.1.0
rpds-py==0.30.0
ruamel.yaml==0.19.1
six==1.17.0
snowballstemmer==3.1.1
sphinx-data-viewer==0.1.5
sphinxcontrib-applehelp==2.0.0
sphinxcontrib-devhelp==2.0.0
sphinxcontrib-htmlhelp==2.1.0
sphinxcontrib-jquery==4.1
sphinxcontrib-jsmath==1.0.1
sphinxcontrib-qthelp==2.0.0
sphinxcontrib-serializinghtml==2.0.0
typing_extensions==4.16.0
urllib3==2.7.0
Loading