Skip to content

Commit 7dbe40f

Browse files
committed
feat: add MkDocs documentation site with GitHub Pages deployment
Adds a searchable, navigable documentation site built with MkDocs and the Material theme. The site includes all 159 role READMEs, playbook documentation, and reference material.
1 parent 07dfb30 commit 7dbe40f

6 files changed

Lines changed: 285 additions & 0 deletions

File tree

.github/workflows/docs.yml

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
name: 'Deploy Documentation'
2+
3+
on:
4+
push:
5+
branches:
6+
- 'main'
7+
8+
permissions:
9+
contents: 'read'
10+
pages: 'write'
11+
id-token: 'write'
12+
13+
concurrency:
14+
group: 'pages'
15+
cancel-in-progress: true
16+
17+
jobs:
18+
build:
19+
runs-on: 'ubuntu-latest'
20+
steps:
21+
- name: 'Checkout repository'
22+
uses: 'actions/checkout@v6'
23+
24+
- name: 'Set up Python'
25+
uses: 'actions/setup-python@v6'
26+
with:
27+
python-version: '3.12'
28+
29+
- name: 'Install dependencies'
30+
run: 'pip install mkdocs mkdocs-material'
31+
32+
- name: 'Generate docs structure'
33+
run: 'python3 tools/build-docs'
34+
35+
- name: 'Build documentation'
36+
run: 'mkdocs build --strict'
37+
38+
- name: 'Upload Pages artifact'
39+
uses: 'actions/upload-pages-artifact@v4'
40+
with:
41+
path: 'site'
42+
43+
deploy:
44+
needs: 'build'
45+
runs-on: 'ubuntu-latest'
46+
environment:
47+
name: 'github-pages'
48+
url: '${{ steps.deployment.outputs.page_url }}'
49+
steps:
50+
- name: 'Deploy to GitHub Pages'
51+
id: 'deployment'
52+
uses: 'actions/deploy-pages@v5'

.gitignore

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,15 @@ playbooks/test.yml
88
roles/test
99
context/
1010
particle/.vagrant
11+
12+
# mkdocs documentation
13+
/docs/CHANGELOG.md
14+
/docs/CODE_OF_CONDUCT.md
15+
/docs/compatibility.md
16+
/docs/contributing.md
17+
/docs/playbooks.md
18+
/docs/roles/
19+
/docs/security.md
20+
/docs/stigs.md
21+
/mkdocs.yml
22+
/site

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ tbd
3030

3131
### Added
3232

33+
* Add MkDocs-based documentation site, deployed automatically to GitHub Pages via `tools/build-docs` and a GitHub Actions workflow
3334
* **CONTRIBUTING**: Document semantic parameter ordering for Ansible modules
3435
* **playbooks**: Add `example.yml` and `setup_example.yml` playbooks as development references
3536
* **role:example**: Add complete example role with defaults, handlers, tasks, templates, and vars as a reference for consistent role development

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -406,6 +406,8 @@ See `ansible-doc -t lookup linuxfabrik.lfops.bitwarden_item` for all options.
406406

407407
## Documentation
408408

409+
Full documentation is available at [linuxfabrik.github.io/lfops](https://linuxfabrik.github.io/lfops/). It is automatically built and deployed on every push to `main`.
410+
409411
* **Ansible Roles**: Each role has its own README file in [`roles/<role_name>/`](roles/).
410412
* **Ansible Plugins**: Available through `ansible-doc`. For example: `ansible-doc linuxfabrik.lfops.gpg_key`.
411413
* **Changelog**: [CHANGELOG.md](CHANGELOG.md)

docs/index.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# Linuxfabrik LFOps
2+
3+
Ansible Collection of roles, playbooks and plugins for Linux-based cloud infrastructure. Covers OS hardening, MariaDB, Icinga2, Nextcloud, FreeIPA, KVM and more.
4+
5+
Made by [Linuxfabrik](https://www.linuxfabrik.ch).
6+
7+
8+
## Overview
9+
10+
LFOps is a comprehensive Ansible Collection providing 145+ playbooks and 160+ roles to bootstrap and manage Linux-based IT infrastructures. It covers the full server lifecycle from initial provisioning and hardening to application deployment, monitoring, and automated backups. LFOps supports RHEL 8/9/10, Debian, and Ubuntu.
11+
12+
13+
## Quick Start
14+
15+
1. Install via Galaxy: `ansible-galaxy collection install linuxfabrik.lfops`
16+
2. Check [Compatibility](compatibility.md) for supported platforms
17+
3. Browse [Roles](roles/acme_sh.md) and [Playbooks](playbooks.md)
18+
4. See the main [README on GitHub](https://github.com/Linuxfabrik/lfops) for detailed setup instructions
19+
20+
21+
## Links
22+
23+
- [Ansible Galaxy](https://galaxy.ansible.com/ui/repo/published/linuxfabrik/lfops/)
24+
- [GitHub Repository](https://github.com/Linuxfabrik/lfops)
25+
- [Report an Issue](https://github.com/Linuxfabrik/lfops/issues/new/choose)
26+
- [Linuxfabrik Monitoring Plugins](https://linuxfabrik.github.io/monitoring-plugins/)
27+
- [Linuxfabrik Lib (Python libraries)](https://linuxfabrik.github.io/lib/lib.html)
28+
- [Linuxfabrik Website](https://www.linuxfabrik.ch)

tools/build-docs

Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
#!/usr/bin/env python3
2+
# -*- coding: utf-8; py-indent-offset: 4 -*-
3+
#
4+
# Author: Linuxfabrik GmbH, Zurich, Switzerland
5+
# Contact: info (at) linuxfabrik (dot) ch
6+
# https://www.linuxfabrik.ch/
7+
# License: The Unlicense, see LICENSE file.
8+
9+
"""Generates the docs/ directory structure with symlinks and the mkdocs.yml navigation
10+
for the Linuxfabrik LFOps documentation site.
11+
12+
Run this tool from the repository root directory.
13+
"""
14+
15+
import os
16+
import sys
17+
18+
__author__ = 'Linuxfabrik GmbH, Zurich/Switzerland'
19+
__version__ = '2026040701'
20+
21+
REPO_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))
22+
DOCS_DIR = os.path.join(REPO_ROOT, 'docs')
23+
24+
# Top-level markdown files to include in the documentation.
25+
# Format: (source filename, target filename in docs/, nav title or None for hidden)
26+
TOP_LEVEL_DOCS = [
27+
('CHANGELOG.md', 'CHANGELOG.md', None),
28+
('CODE_OF_CONDUCT.md', 'CODE_OF_CONDUCT.md', None),
29+
('COMPATIBILITY.md', 'compatibility.md', 'Compatibility'),
30+
('CONTRIBUTING.md', 'contributing.md', 'Contributing'),
31+
('SECURITY.md', 'security.md', 'Security'),
32+
('STIGs.md', 'stigs.md', 'STIGs'),
33+
]
34+
35+
# Playbooks README
36+
PLAYBOOKS_README = ('playbooks/README.md', 'playbooks.md', 'Playbooks')
37+
38+
39+
def create_symlink(source, target):
40+
"""Create a relative symlink from target to source."""
41+
target_dir = os.path.dirname(target)
42+
rel_source = os.path.relpath(source, target_dir)
43+
if os.path.islink(target):
44+
os.unlink(target)
45+
elif os.path.exists(target):
46+
os.unlink(target)
47+
os.symlink(rel_source, target)
48+
49+
50+
def scan_roles():
51+
"""Scan roles/ directory and return sorted list of role names that have a README.md."""
52+
roles_path = os.path.join(REPO_ROOT, 'roles')
53+
if not os.path.isdir(roles_path):
54+
return []
55+
roles = []
56+
for entry in sorted(os.listdir(roles_path)):
57+
if entry == 'example':
58+
continue
59+
readme = os.path.join(roles_path, entry, 'README.md')
60+
if os.path.isfile(readme):
61+
roles.append(entry)
62+
return roles
63+
64+
65+
def generate_mkdocs_yml(roles):
66+
"""Generate the mkdocs.yml content."""
67+
lines = []
68+
lines.append("# This file is auto-generated by tools/build-docs. Do not edit manually.")
69+
lines.append("")
70+
lines.append("site_name: 'Linuxfabrik LFOps'")
71+
lines.append("site_url: 'https://linuxfabrik.github.io/lfops/'")
72+
lines.append("repo_url: 'https://github.com/Linuxfabrik/lfops'")
73+
lines.append("repo_name: 'Linuxfabrik/lfops'")
74+
lines.append("")
75+
lines.append("theme:")
76+
lines.append(" name: 'material'")
77+
lines.append(" icon:")
78+
lines.append(" logo: 'material/ansible'")
79+
lines.append(" palette:")
80+
lines.append(" - scheme: 'default'")
81+
lines.append(" primary: 'blue grey'")
82+
lines.append(" accent: 'orange'")
83+
lines.append(" toggle:")
84+
lines.append(" icon: 'material/brightness-7'")
85+
lines.append(" name: 'Switch to dark mode'")
86+
lines.append(" - scheme: 'slate'")
87+
lines.append(" primary: 'blue grey'")
88+
lines.append(" accent: 'orange'")
89+
lines.append(" toggle:")
90+
lines.append(" icon: 'material/brightness-4'")
91+
lines.append(" name: 'Switch to light mode'")
92+
lines.append(" features:")
93+
lines.append(" - content.code.copy")
94+
lines.append(" - navigation.expand")
95+
lines.append(" - navigation.instant")
96+
lines.append(" - navigation.sections")
97+
lines.append(" - navigation.top")
98+
lines.append(" - search.highlight")
99+
lines.append(" - search.suggest")
100+
lines.append(" - toc.follow")
101+
lines.append("")
102+
lines.append("markdown_extensions:")
103+
lines.append(" - admonition")
104+
lines.append(" - attr_list")
105+
lines.append(" - def_list")
106+
lines.append(" - tables")
107+
lines.append(" - toc:")
108+
lines.append(" permalink: true")
109+
lines.append("")
110+
lines.append("plugins:")
111+
lines.append(" - search")
112+
lines.append("")
113+
lines.append("validation:")
114+
lines.append(" links:")
115+
lines.append(" not_found: 'info'")
116+
lines.append("")
117+
lines.append("not_in_nav: |")
118+
lines.append(" CHANGELOG.md")
119+
lines.append(" CODE_OF_CONDUCT.md")
120+
lines.append("")
121+
122+
# Build nav
123+
lines.append("nav:")
124+
lines.append(" - 'Home': 'index.md'")
125+
lines.append(" - 'Compatibility': 'compatibility.md'")
126+
lines.append(" - 'Playbooks': 'playbooks.md'")
127+
lines.append(" - 'Roles':")
128+
for role in roles:
129+
lines.append(f" - '{role}': 'roles/{role}.md'")
130+
lines.append(" - 'STIGs': 'stigs.md'")
131+
lines.append(" - 'Contributing': 'contributing.md'")
132+
lines.append(" - 'Security': 'security.md'")
133+
lines.append("")
134+
135+
return '\n'.join(lines)
136+
137+
138+
def main():
139+
os.makedirs(DOCS_DIR, exist_ok=True)
140+
141+
# Create symlinks for top-level docs
142+
for source_name, target_name, _ in TOP_LEVEL_DOCS:
143+
source = os.path.join(REPO_ROOT, source_name)
144+
target = os.path.join(DOCS_DIR, target_name)
145+
if os.path.isfile(source):
146+
create_symlink(source, target)
147+
else:
148+
print(f'WARNING: {source_name} not found, skipping.')
149+
150+
# Create symlink for playbooks README
151+
source_name, target_name, _ = PLAYBOOKS_README
152+
source = os.path.join(REPO_ROOT, source_name)
153+
target = os.path.join(DOCS_DIR, target_name)
154+
if os.path.isfile(source):
155+
create_symlink(source, target)
156+
else:
157+
print(f'WARNING: {source_name} not found, skipping.')
158+
159+
# Create symlinks for role READMEs
160+
roles = scan_roles()
161+
target_dir = os.path.join(DOCS_DIR, 'roles')
162+
os.makedirs(target_dir, exist_ok=True)
163+
164+
# Remove stale symlinks
165+
expected_files = {f'{role}.md' for role in roles}
166+
for entry in os.listdir(target_dir):
167+
entry_path = os.path.join(target_dir, entry)
168+
if os.path.islink(entry_path) and entry not in expected_files:
169+
os.unlink(entry_path)
170+
171+
for role in roles:
172+
source = os.path.join(REPO_ROOT, 'roles', role, 'README.md')
173+
target = os.path.join(target_dir, f'{role}.md')
174+
create_symlink(source, target)
175+
176+
# Generate mkdocs.yml
177+
mkdocs_content = generate_mkdocs_yml(roles)
178+
mkdocs_path = os.path.join(REPO_ROOT, 'mkdocs.yml')
179+
with open(mkdocs_path, 'w') as f:
180+
f.write(mkdocs_content)
181+
182+
total_symlinks = len(TOP_LEVEL_DOCS) + 1 + len(roles) # +1 for playbooks README
183+
print(f'Created {total_symlinks} symlinks in docs/')
184+
print(f'Generated mkdocs.yml with {len(roles)} role entries')
185+
186+
return 0
187+
188+
189+
if __name__ == '__main__':
190+
sys.exit(main())

0 commit comments

Comments
 (0)