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
26 changes: 26 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Monthly pull requests to keep dependencies current. Each one runs the
# docs-checks build, so a breaking update shows up as a failed check.
#
# Major versions are ignored: they need a person to read the release notes.
# In particular MkDocs 2.0 drops the plugin and theme systems this site relies
# on, so requirements.txt keeps mkdocs below 2 on purpose.
version: 2
updates:
- package-ecosystem: pip
directory: /
schedule:
interval: monthly
ignore:
- dependency-name: "*"
update-types: ["version-update:semver-major"]
groups:
docs-dependencies:
patterns: ["*"]

- package-ecosystem: github-actions
directory: /
schedule:
interval: monthly
groups:
github-actions:
patterns: ["*"]
46 changes: 46 additions & 0 deletions .github/scripts/check_built_links.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
"""Check that every local link and image in the built site points at a real file.

`mkdocs build --strict` checks Markdown links, but not paths written in raw
HTML (e.g. `<img src="../../assets/...">` in the equipment grids). Those
paths are relative to the page's URL, and Spanish pages are served one
folder deeper (/es/...), so a path copied from an English page breaks. This
catches that.

Usage: check_built_links.py [SITE_DIR] (default: site)
Exits 1 if anything doesn't resolve.
"""

import posixpath
import re
import sys
from pathlib import Path

LINK = re.compile(r'(?:src|href)="([^"#?]+)')
EXTERNAL = re.compile(r"^(?:[a-z][a-z0-9+.-]*:|//)", re.IGNORECASE)


def main():
site = Path(sys.argv[1] if len(sys.argv) > 1 else "site")
broken = []
for page in sorted(site.rglob("*.html")):
url = "/" + page.relative_to(site).as_posix()
folder = posixpath.dirname(url) + "/"
for link in LINK.findall(page.read_text(encoding="utf-8", errors="ignore")):
if EXTERNAL.match(link):
continue
target = posixpath.normpath(posixpath.join(folder, link))
path = site / target.lstrip("/")
if not (path.is_file() or (path / "index.html").is_file()):
broken.append((url, link))

for url, link in broken:
print(f"{url}: {link} does not exist in the built site")
if broken:
print(f"\n{len(broken)} broken local link(s). Paths in raw HTML are relative to "
"the page's URL; pages under docs/es/ need one more '../' than English ones.")
sys.exit(1)
print("All local links and images resolve.")


if __name__ == "__main__":
main()
90 changes: 90 additions & 0 deletions .github/scripts/check_translations.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
"""Report on how well docs/es keeps up with docs/en.

Lists English pages with no Spanish version, Spanish pages that are still
machine-drafted, and Spanish pages whose English page has changed since the
Spanish one was last edited. On a pull request it also warns (as an inline
annotation) when the PR edits an English page without touching its Spanish
version.

This only reports; it always exits 0 so it never blocks a merge.

Usage: check_translations.py [BASE_REF]
BASE_REF the branch a pull request targets (e.g. origin/main), if any
"""

import os
import subprocess
import sys
from pathlib import Path

EN = Path("docs/en")
ES = Path("docs/es")
DRAFT_MARKER = "TODO: machine-drafted"


def git(*args):
result = subprocess.run(["git", *args], capture_output=True, text=True)
return result.stdout.strip()


def last_commit_time(path):
stamp = git("log", "-1", "--format=%ct", "--", str(path))
return int(stamp) if stamp else 0


def changed_files(base_ref):
if not base_ref:
return set()
return set(git("diff", "--name-only", f"{base_ref}...HEAD").splitlines())


def main():
base_ref = sys.argv[1] if len(sys.argv) > 1 else ""
changed = changed_files(base_ref)

missing, drafts, stale = [], [], []
for en_page in sorted(EN.rglob("*.md")):
rel = en_page.relative_to(EN)
es_page = ES / rel
if not es_page.exists():
missing.append(rel)
continue
if DRAFT_MARKER in es_page.read_text(encoding="utf-8"):
drafts.append(rel)
if last_commit_time(en_page) > last_commit_time(es_page):
stale.append(rel)
if str(en_page) in changed and str(es_page) not in changed:
print(
f"::warning file={en_page}::This PR changes the English page but "
f"not {es_page}. Update the Spanish version too, or leave it for "
"a translator (it will show up as out of date)."
)

for rel in missing:
print(f"::warning file={EN / rel}::No Spanish version at {ES / rel}.")

lines = ["## Spanish translations", ""]
if not (missing or drafts or stale):
lines.append("Every English page has an up-to-date, reviewed Spanish version.")
for title, pages, note in [
("No Spanish version", missing, "Spanish readers see the English page."),
("English changed since the Spanish was last edited", stale,
"The Spanish page may be missing recent changes."),
("Machine-drafted, awaiting review", drafts,
"Remove the notice and TODO comment once a fluent speaker has reviewed it."),
]:
if pages:
lines += [f"### {title} ({len(pages)})", "", note, ""]
lines += [f"- `{rel}`" for rel in pages]
lines.append("")

report = "\n".join(lines) + "\n"
print(report)
summary = os.environ.get("GITHUB_STEP_SUMMARY")
if summary:
with open(summary, "a", encoding="utf-8") as f:
f.write(report)


if __name__ == "__main__":
main()
48 changes: 48 additions & 0 deletions .github/workflows/docs-checks.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# Runs on every pull request (and on main after a merge).
#
# build: builds the site with `mkdocs build --strict`, which fails on
# broken links between pages, links to missing headings, and
# pages missing from the nav, then checks that image and link
# paths written in raw HTML point at real files. This is the
# check to require before merging.
# translations: reports which Spanish pages are missing, machine-drafted, or
# behind their English page. Report only; it never fails.
name: Docs checks

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

permissions:
contents: read

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

jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
# Same version Read the Docs uses (.readthedocs.yaml)
python-version: "3.12"
cache: pip
cache-dependency-path: requirements.txt
- run: pip install -r requirements.txt
- run: mkdocs build --strict
- name: Check image and link paths in raw HTML
run: python .github/scripts/check_built_links.py site

translations:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
# full history, to compare when each English and Spanish page last changed
fetch-depth: 0
- run: python .github/scripts/check_translations.py ${{ github.base_ref && format('origin/{0}', github.base_ref) || '' }}
78 changes: 78 additions & 0 deletions .github/workflows/link-check.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
# Checks every external link in the docs once a month.
#
# Outside sites break or move on their own schedule, so this doesn't run on
# pull requests or block anything. When links are broken it opens an issue
# (or updates the open one) labeled `broken-links`, and closes that issue
# once everything passes again. Run it by hand from the Actions tab with
# "Run workflow".
#
# Links that always fail for reasons we can't fix (login walls, bot
# blocking) get an `--exclude '<regex>'` line in the args below.
name: Link check

on:
schedule:
- cron: "23 13 1 * *" # 1st of each month, 13:23 UTC
workflow_dispatch:

permissions:
contents: read
issues: write

jobs:
links:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Check links
id: lychee
uses: lycheeverse/lychee-action@v2
with:
# Only http(s) links: links between pages are already checked by
# `mkdocs build --strict` in the docs-checks workflow. --root-dir
# lets lychee read root-relative image paths (/assets/...) as local
# files, which --scheme then skips.
# Excluded: the UniFi controller's device-adoption address (not a
# web page) and ProQuest (needs a library login).
args: >-
--no-progress
--scheme https --scheme http
--exclude-all-private
--root-dir ${{ github.workspace }}/docs
--max-retries 3 --retry-wait-time 10 --timeout 30
--accept 100..=103,200..=299,429
--exclude '^https?://unifi\.phillycommunitywireless\.org'
--exclude '^https?://search\.proquest\.com'
'docs/**/*.md' README.md
output: lychee/out.md
fail: false

- name: Open, update or close the broken-links issue
env:
GH_TOKEN: ${{ github.token }}
GH_REPO: ${{ github.repository }}
EXIT_CODE: ${{ steps.lychee.outputs.exit_code }}
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
run: |
existing=$(gh issue list --label broken-links --state open --json number --jq '.[0].number // empty')
today=$(date -u +%F)

if [ "$EXIT_CODE" != "0" ]; then
{
echo "The monthly link check found links that don't work. Fix or replace them, or add a pattern to \`.lycheeignore\` if a site blocks automated checks but works in a browser."
echo
echo "Last checked $today ([run]($RUN_URL))."
echo
cat lychee/out.md
} > issue.md
if [ -n "$existing" ]; then
gh issue edit "$existing" --body-file issue.md
gh issue comment "$existing" --body "Checked again on $today; the list above is updated."
else
gh label create broken-links --color d93f0b --description "Found by the monthly link check" --force
gh issue create --title "Broken links in the docs" --label broken-links --assignee hawc2 --body-file issue.md
fi
elif [ -n "$existing" ]; then
gh issue close "$existing" --comment "All links passed on $today ([run]($RUN_URL))."
fi
Loading
Loading