feat: add Gitea CI workflow with podman-based lint and link check
Some checks failed
CI / lint (push) Failing after 2s
CI / link-check (push) Failing after 2s

- Markdown lint via markdownlint-cli2 container
- Internal cross-reference checker for See Also sections
- Both jobs run on linux runner using podman containers
This commit is contained in:
user
2026-02-22 06:33:28 +01:00
parent 7e8661f68b
commit a010db3450
3 changed files with 130 additions and 0 deletions

35
.gitea/workflows/ci.yml Normal file
View File

@@ -0,0 +1,35 @@
name: CI
on:
push:
branches: [master]
pull_request:
branches: [master]
jobs:
lint:
runs-on: linux
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Markdown lint
run: |
podman run --rm \
-v "${{ github.workspace }}:/work:Z" \
docker.io/davidanson/markdownlint-cli2:v0.17.2 \
"**/*.md"
link-check:
runs-on: linux
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Check internal links
run: |
podman run --rm \
-v "${{ github.workspace }}:/work:Z" \
-w /work \
docker.io/library/python:3.12-slim \
python3 scripts/check-links.py

29
.markdownlint-cli2.yaml Normal file
View File

@@ -0,0 +1,29 @@
config:
# Allow long lines in tables and code blocks
MD013:
line_length: 200
tables: false
code_blocks: false
# Allow duplicate headings across files (common in howtos)
MD024:
siblings_only: true
# Allow trailing punctuation in headings (e.g. "~/.ssh/config")
MD026: false
# Allow inline HTML (rare but sometimes needed)
MD033: false
# Allow bare URLs in markdown
MD034: false
# Allow multiple blank lines
MD012: false
globs:
- "**/*.md"
ignores:
- ".git"
- ".venv"

66
scripts/check-links.py Normal file
View File

@@ -0,0 +1,66 @@
#!/usr/bin/env python3
"""Check internal cross-references between topic files.
Scans all .md files for See Also links to other topics (backtick-quoted names)
and verifies that matching files exist under topics/.
Exit codes:
0 — all links valid
1 — broken links found
"""
import re
import sys
from pathlib import Path
TOPICS_DIR = Path("topics")
DOCS_DIR = Path("docs")
SEARCH_DIRS = [Path("."), TOPICS_DIR, DOCS_DIR]
EXCLUDE = {"docs/TEMPLATE.md"}
def find_md_files() -> list[Path]:
files = []
for d in SEARCH_DIRS:
if d.exists():
files.extend(d.glob("*.md"))
return sorted(set(files))
def extract_topic_refs(path: Path) -> list[tuple[int, str]]:
"""Extract backtick-quoted topic references from See Also sections."""
refs = []
in_see_also = False
for lineno, line in enumerate(path.read_text().splitlines(), start=1):
if re.match(r"^##\s+See Also", line, re.IGNORECASE):
in_see_also = True
continue
if in_see_also and re.match(r"^##\s+", line):
break
if in_see_also:
for match in re.finditer(r"`([a-z][a-z0-9-]*)`", line):
refs.append((lineno, match.group(1)))
return refs
def check_links() -> int:
md_files = find_md_files()
topic_names = {p.stem for p in TOPICS_DIR.glob("*.md")} if TOPICS_DIR.exists() else set()
errors = 0
for path in [p for p in md_files if str(p) not in EXCLUDE]:
for lineno, ref in extract_topic_refs(path):
if ref not in topic_names:
print(f" {path}:{lineno} broken ref `{ref}` — no topics/{ref}.md")
errors += 1
if errors:
print(f"\n {errors} broken link(s) found")
return 1
print(f" {len(md_files)} files checked, all internal links valid")
return 0
if __name__ == "__main__":
sys.exit(check_links())