feat: add documentation infrastructure and service implementation guide
## Documentation Infrastructure & Service Implementation Guide This PR introduces documentation tooling and contributor resources to help maintain and expand the SelfHostBlocks project: ### Service Implementation Guide - Add comprehensive guide for implementing new SHB services - Covers contracts, testing, documentation, and integration patterns - Provides step-by-step instructions and best practices for contributors ### Redirect Management Tool - Add automated update-redirects tool to maintain documentation links - Extracts heading IDs from markdown files across docs/, modules/, demo/ - Maps source files to target HTML pages using project routing logic - Generates missing redirects in format expected by nixos-render-docs
This commit is contained in:
parent
0f22b8c2a3
commit
16fcec4d40
5 changed files with 1172 additions and 148 deletions
208
docs/generate-redirects-nixos-render-docs.py
Normal file
208
docs/generate-redirects-nixos-render-docs.py
Normal file
|
|
@ -0,0 +1,208 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
Generate redirects.json by scanning actual HTML files produced by nixos-render-docs.
|
||||
|
||||
This script implements a runtime patching mechanism to automatically generate a
|
||||
complete redirects.json file by scanning generated HTML files for real anchor
|
||||
locations, eliminating manual maintenance and ensuring accuracy.
|
||||
|
||||
ARCHITECTURE OVERVIEW:
|
||||
The script works by monkey-patching nixos-render-docs at runtime to:
|
||||
1. Disable redirect validation during HTML generation
|
||||
2. Generate HTML documentation normally
|
||||
3. Scan all generated HTML files to extract anchor IDs and their file locations
|
||||
4. Apply filtering logic to exclude system-generated anchors
|
||||
5. Generate and write redirects.json with accurate mappings
|
||||
|
||||
KEY COMPONENTS:
|
||||
- Runtime patching: Modifies nixos-render-docs behavior without source changes
|
||||
- HTML scanning: Extracts anchor IDs using regex pattern matching
|
||||
- Filtering: Excludes NixOS options (opt-*) and extra options (selfhostblock*)
|
||||
- Output generation: Creates both debug information and production redirects.json
|
||||
|
||||
IMPORTANT NOTES:
|
||||
- Uses atexit handler to ensure output is generated even if process is interrupted
|
||||
- Patches are applied on module import, making this a side-effect import
|
||||
- Error handling preserves original validation behavior in case of failure
|
||||
"""
|
||||
|
||||
import sys
|
||||
import json
|
||||
import atexit
|
||||
import os
|
||||
import re
|
||||
|
||||
# Global storage for anchor-to-file mappings discovered during HTML scanning
|
||||
# Structure: {anchor_id: html_filename}
|
||||
file_target_mapping = {}
|
||||
|
||||
def scan_html_files(output_dir, html_files):
|
||||
"""
|
||||
Scan HTML files to extract anchor IDs and build anchor-to-file mappings.
|
||||
|
||||
Discovers all HTML files in output_dir and extracts id attributes to populate
|
||||
the global file_target_mapping. Filters out NixOS system options during scanning.
|
||||
|
||||
Args:
|
||||
output_dir: Directory containing generated HTML files
|
||||
html_files: Unused parameter (always discovers files from filesystem)
|
||||
"""
|
||||
# Always discover HTML files from the output directory
|
||||
if not os.path.exists(output_dir):
|
||||
print(f"DEBUG: Output directory {output_dir} does not exist", file=sys.stderr)
|
||||
return
|
||||
|
||||
html_files = [f for f in os.listdir(output_dir) if f.endswith('.html')]
|
||||
print(f"DEBUG: Discovered {len(html_files)} HTML files in {output_dir}", file=sys.stderr)
|
||||
|
||||
# Process each HTML file to extract anchor IDs
|
||||
for html_file in html_files:
|
||||
html_path = os.path.join(output_dir, html_file)
|
||||
try:
|
||||
with open(html_path, 'r', encoding='utf-8') as f:
|
||||
html_content = f.read()
|
||||
|
||||
# Extract all id attributes using regex pattern matching
|
||||
# Matches: id="anchor-name" and captures anchor-name
|
||||
anchor_matches = re.findall(r'id="([^"]+)"', html_content)
|
||||
|
||||
# Filter and record anchor mappings
|
||||
non_opt_count = 0
|
||||
for anchor_id in anchor_matches:
|
||||
# Skip NixOS system option anchors (opt-* prefix)
|
||||
if not anchor_id.startswith('opt-'):
|
||||
file_target_mapping[anchor_id] = html_file
|
||||
non_opt_count += 1
|
||||
|
||||
if non_opt_count > 0:
|
||||
print(f"Found {non_opt_count} anchors in {html_file}", file=sys.stderr)
|
||||
|
||||
except Exception as e:
|
||||
# Log errors but continue processing other files
|
||||
print(f"Failed to scan {html_path}: {e}", file=sys.stderr)
|
||||
|
||||
def output_collected_refs():
|
||||
"""
|
||||
Generate and write the final redirects.json file from collected anchor mappings.
|
||||
|
||||
This function is registered as an atexit handler to ensure output is generated
|
||||
even if the process is interrupted. It processes the global file_target_mapping
|
||||
to create the final redirects file with appropriate filtering.
|
||||
|
||||
Output files:
|
||||
- out/redirects.json: Production redirects mapping
|
||||
"""
|
||||
import os
|
||||
|
||||
# Generate redirects from discovered HTML anchor mappings
|
||||
if file_target_mapping:
|
||||
print(f"Creating redirects from {len(file_target_mapping)} HTML mappings", file=sys.stderr)
|
||||
redirects = {}
|
||||
filtered_count = 0
|
||||
|
||||
# Apply filtering logic to exclude system-generated anchors
|
||||
for anchor_id, html_file in file_target_mapping.items():
|
||||
# Filter out:
|
||||
# - opt-*: NixOS system options
|
||||
# - selfhostblock*: Extra options from this project
|
||||
if not anchor_id.startswith('opt-') and not anchor_id.startswith('selfhostblock'):
|
||||
redirects[anchor_id] = [f"{html_file}#{anchor_id}"]
|
||||
else:
|
||||
filtered_count += 1
|
||||
|
||||
print(f"Generated {len(redirects)} redirects (filtered out {filtered_count} system options)", file=sys.stderr)
|
||||
else:
|
||||
# Fallback case - should not occur during normal operation
|
||||
print("Warning: No HTML mappings available", file=sys.stderr)
|
||||
redirects = {}
|
||||
|
||||
# Ensure output directory exists
|
||||
os.makedirs('out', exist_ok=True)
|
||||
|
||||
# Write production redirects file
|
||||
try:
|
||||
redirects_file = 'out/redirects.json'
|
||||
|
||||
with open(redirects_file, 'w') as f:
|
||||
json.dump(redirects, f, indent=2, sort_keys=True)
|
||||
|
||||
print(f"Generated redirects.json with {len(redirects)} redirects", file=sys.stderr)
|
||||
|
||||
except Exception as e:
|
||||
print(f"Failed to write redirects.json: {e}", file=sys.stderr)
|
||||
|
||||
# Register output generation to run on process exit
|
||||
atexit.register(output_collected_refs)
|
||||
|
||||
def apply_patches():
|
||||
"""
|
||||
Apply runtime monkey patches to nixos-render-docs modules.
|
||||
|
||||
This function modifies the behavior of nixos-render-docs by:
|
||||
1. Hooking into the HTML generation CLI command
|
||||
2. Temporarily disabling redirect validation during HTML generation
|
||||
3. Scanning generated HTML files to extract anchor mappings
|
||||
4. Restoring original validation behavior
|
||||
|
||||
The patching approach allows us to extract anchor information without
|
||||
modifying the nixos-render-docs source code directly.
|
||||
|
||||
Raises:
|
||||
ImportError: If nixos-render-docs modules cannot be imported
|
||||
"""
|
||||
try:
|
||||
# Import required nixos-render-docs modules
|
||||
import nixos_render_docs.html as html_module
|
||||
import nixos_render_docs.redirects as redirects_module
|
||||
import nixos_render_docs.manual as manual_module
|
||||
|
||||
# Store reference to original HTML CLI function
|
||||
original_run_cli_html = manual_module._run_cli_html
|
||||
|
||||
def patched_run_cli_html(args):
|
||||
"""
|
||||
Patched version of _run_cli_html that disables validation and scans output.
|
||||
|
||||
This wrapper function:
|
||||
1. Temporarily disables redirect validation to prevent errors
|
||||
2. Runs normal HTML generation
|
||||
3. Scans generated HTML files for anchor mappings
|
||||
4. Restores original validation behavior
|
||||
"""
|
||||
print("Generating HTML documentation...", file=sys.stderr)
|
||||
|
||||
# Temporarily disable redirect validation
|
||||
original_validate = redirects_module.Redirects.validate
|
||||
redirects_module.Redirects.validate = lambda self, targets: None
|
||||
|
||||
try:
|
||||
# Run original HTML generation
|
||||
result = original_run_cli_html(args)
|
||||
|
||||
# Determine output directory from CLI arguments
|
||||
if hasattr(args, 'outfile') and args.outfile:
|
||||
output_dir = os.path.dirname(args.outfile)
|
||||
else:
|
||||
output_dir = '.'
|
||||
|
||||
# Scan generated HTML files for anchor mappings
|
||||
scan_html_files(output_dir, None)
|
||||
print(f"Scanned {len(file_target_mapping)} anchor mappings", file=sys.stderr)
|
||||
|
||||
finally:
|
||||
# Always restore original validation function
|
||||
redirects_module.Redirects.validate = original_validate
|
||||
|
||||
return result
|
||||
|
||||
# Replace the original function with our patched version
|
||||
manual_module._run_cli_html = patched_run_cli_html
|
||||
|
||||
print("Applied patches to nixos-render-docs", file=sys.stderr)
|
||||
|
||||
except ImportError as e:
|
||||
print(f"Failed to apply patches: {e}", file=sys.stderr)
|
||||
|
||||
# Apply patches immediately when this module is imported
|
||||
# This ensures the patches are active before nixos-render-docs CLI runs
|
||||
apply_patches()
|
||||
|
|
@ -32,6 +32,10 @@ demos.md
|
|||
contributing.md
|
||||
```
|
||||
|
||||
```{=include=} chapters html:into-file=//service-implementation-guide.html
|
||||
service-implementation-guide.md
|
||||
```
|
||||
|
||||
```{=include=} appendix html:into-file=//options.html
|
||||
options.md
|
||||
```
|
||||
|
|
|
|||
|
|
@ -1,7 +1,49 @@
|
|||
{
|
||||
"adding-new-service-documentation": [
|
||||
"service-implementation-guide.html#adding-new-service-documentation"
|
||||
],
|
||||
"all-options": [
|
||||
"options.html#all-options"
|
||||
],
|
||||
"analyze-existing-services": [
|
||||
"service-implementation-guide.html#analyze-existing-services"
|
||||
],
|
||||
"api-health-check": [
|
||||
"service-implementation-guide.html#api-health-check"
|
||||
],
|
||||
"authentication-integration": [
|
||||
"service-implementation-guide.html#authentication-integration"
|
||||
],
|
||||
"authentication-integration-pitfalls": [
|
||||
"service-implementation-guide.html#authentication-integration-pitfalls"
|
||||
],
|
||||
"automated-redirect-generation": [
|
||||
"service-implementation-guide.html#automated-redirect-generation"
|
||||
],
|
||||
"best-practices-summary": [
|
||||
"service-implementation-guide.html#best-practices-summary"
|
||||
],
|
||||
"block-ssl": [
|
||||
"blocks-ssl.html#block-ssl"
|
||||
],
|
||||
"block-ssl-debug": [
|
||||
"blocks-ssl.html#block-ssl-debug"
|
||||
],
|
||||
"block-ssl-impl-lets-encrypt": [
|
||||
"blocks-ssl.html#block-ssl-impl-lets-encrypt"
|
||||
],
|
||||
"block-ssl-impl-self-signed": [
|
||||
"blocks-ssl.html#block-ssl-impl-self-signed"
|
||||
],
|
||||
"block-ssl-options": [
|
||||
"blocks-ssl.html#block-ssl-options"
|
||||
],
|
||||
"block-ssl-tests": [
|
||||
"blocks-ssl.html#block-ssl-tests"
|
||||
],
|
||||
"block-ssl-usage": [
|
||||
"blocks-ssl.html#block-ssl-usage"
|
||||
],
|
||||
"blocks": [
|
||||
"blocks.html#blocks"
|
||||
],
|
||||
|
|
@ -14,20 +56,14 @@
|
|||
"blocks-monitoring-configuration": [
|
||||
"blocks-monitoring.html#blocks-monitoring-configuration"
|
||||
],
|
||||
"blocks-monitoring-nextcloud-dashboard": [
|
||||
"blocks-monitoring.html#blocks-monitoring-nextcloud-dashboard"
|
||||
],
|
||||
"blocks-monitoring-deluge-dashboard": [
|
||||
"blocks-monitoring.html#blocks-monitoring-deluge-dashboard"
|
||||
],
|
||||
"blocks-monitoring-error-dashboard": [
|
||||
"blocks-monitoring.html#blocks-monitoring-error-dashboard"
|
||||
],
|
||||
"blocks-monitoring-performance-dashboard": [
|
||||
"blocks-monitoring.html#blocks-monitoring-performance-dashboard"
|
||||
],
|
||||
"blocks-monitoring-provisioning": [
|
||||
"blocks-monitoring.html#blocks-monitoring-provisioning"
|
||||
"blocks-monitoring-nextcloud-dashboard": [
|
||||
"blocks-monitoring.html#blocks-monitoring-nextcloud-dashboard"
|
||||
],
|
||||
"blocks-monitoring-options": [
|
||||
"blocks-monitoring.html#blocks-monitoring-options"
|
||||
|
|
@ -149,6 +185,12 @@
|
|||
"blocks-monitoring-options-shb.monitoring.subdomain": [
|
||||
"blocks-monitoring.html#blocks-monitoring-options-shb.monitoring.subdomain"
|
||||
],
|
||||
"blocks-monitoring-performance-dashboard": [
|
||||
"blocks-monitoring.html#blocks-monitoring-performance-dashboard"
|
||||
],
|
||||
"blocks-monitoring-provisioning": [
|
||||
"blocks-monitoring.html#blocks-monitoring-provisioning"
|
||||
],
|
||||
"blocks-postgresql": [
|
||||
"blocks-postgresql.html#blocks-postgresql"
|
||||
],
|
||||
|
|
@ -473,32 +515,11 @@
|
|||
"blocks-sops-usage": [
|
||||
"blocks-sops.html#blocks-sops-usage"
|
||||
],
|
||||
"blocks-sops-usage-requester": [
|
||||
"blocks-sops.html#blocks-sops-usage-requester"
|
||||
],
|
||||
"blocks-sops-usage-manual": [
|
||||
"blocks-sops.html#blocks-sops-usage-manual"
|
||||
],
|
||||
"block-ssl": [
|
||||
"blocks-ssl.html#block-ssl"
|
||||
],
|
||||
"block-ssl-debug": [
|
||||
"blocks-ssl.html#block-ssl-debug"
|
||||
],
|
||||
"block-ssl-impl-lets-encrypt": [
|
||||
"blocks-ssl.html#block-ssl-impl-lets-encrypt"
|
||||
],
|
||||
"block-ssl-impl-self-signed": [
|
||||
"blocks-ssl.html#block-ssl-impl-self-signed"
|
||||
],
|
||||
"block-ssl-options": [
|
||||
"blocks-ssl.html#block-ssl-options"
|
||||
],
|
||||
"block-ssl-tests": [
|
||||
"blocks-ssl.html#block-ssl-tests"
|
||||
],
|
||||
"block-ssl-usage": [
|
||||
"blocks-ssl.html#block-ssl-usage"
|
||||
"blocks-sops-usage-requester": [
|
||||
"blocks-sops.html#blocks-sops-usage-requester"
|
||||
],
|
||||
"blocks-ssl-options-shb.certs.cas.selfsigned": [
|
||||
"blocks-ssl.html#blocks-ssl-options-shb.certs.cas.selfsigned"
|
||||
|
|
@ -617,6 +638,27 @@
|
|||
"blocks-ssl-options-shb.certs.systemdService": [
|
||||
"blocks-ssl.html#blocks-ssl-options-shb.certs.systemdService"
|
||||
],
|
||||
"build-time-validation": [
|
||||
"service-implementation-guide.html#build-time-validation"
|
||||
],
|
||||
"check-nixos-integration": [
|
||||
"service-implementation-guide.html#check-nixos-integration"
|
||||
],
|
||||
"common-pitfalls-and-solutions": [
|
||||
"service-implementation-guide.html#common-pitfalls-and-solutions"
|
||||
],
|
||||
"complete-shb-service": [
|
||||
"service-implementation-guide.html#complete-shb-service"
|
||||
],
|
||||
"complete-workflow": [
|
||||
"service-implementation-guide.html#complete-workflow"
|
||||
],
|
||||
"configuration-issues": [
|
||||
"service-implementation-guide.html#configuration-issues"
|
||||
],
|
||||
"configuration-management": [
|
||||
"service-implementation-guide.html#configuration-management"
|
||||
],
|
||||
"contract-backup": [
|
||||
"contracts-backup.html#contract-backup"
|
||||
],
|
||||
|
|
@ -686,27 +728,6 @@
|
|||
"contracts": [
|
||||
"contracts.html#contracts"
|
||||
],
|
||||
"contracts-nixpkgs": [
|
||||
"contracts.html#contracts-nixpkgs"
|
||||
],
|
||||
"contracts-provided": [
|
||||
"contracts.html#contracts-provided"
|
||||
],
|
||||
"contracts-concept": [
|
||||
"contracts.html#contracts-concept"
|
||||
],
|
||||
"contracts-schema": [
|
||||
"contracts.html#contracts-schema"
|
||||
],
|
||||
"contracts-test": [
|
||||
"contracts.html#contracts-test"
|
||||
],
|
||||
"contracts-videos": [
|
||||
"contracts.html#contracts-videos"
|
||||
],
|
||||
"contracts-why": [
|
||||
"contracts.html#contracts-why"
|
||||
],
|
||||
"contracts-backup-options-shb.contracts.backup": [
|
||||
"contracts-backup.html#contracts-backup-options-shb.contracts.backup"
|
||||
],
|
||||
|
|
@ -743,6 +764,9 @@
|
|||
"contracts-backup-options-shb.contracts.backup.settings": [
|
||||
"contracts-backup.html#contracts-backup-options-shb.contracts.backup.settings"
|
||||
],
|
||||
"contracts-concept": [
|
||||
"contracts.html#contracts-concept"
|
||||
],
|
||||
"contracts-databasebackup-options-shb.contracts.databasebackup": [
|
||||
"contracts-databasebackup.html#contracts-databasebackup-options-shb.contracts.databasebackup"
|
||||
],
|
||||
|
|
@ -773,6 +797,15 @@
|
|||
"contracts-databasebackup-options-shb.contracts.databasebackup.settings": [
|
||||
"contracts-databasebackup.html#contracts-databasebackup-options-shb.contracts.databasebackup.settings"
|
||||
],
|
||||
"contracts-nixpkgs": [
|
||||
"contracts.html#contracts-nixpkgs"
|
||||
],
|
||||
"contracts-provided": [
|
||||
"contracts.html#contracts-provided"
|
||||
],
|
||||
"contracts-schema": [
|
||||
"contracts.html#contracts-schema"
|
||||
],
|
||||
"contracts-secret-options-shb.contracts.secret": [
|
||||
"contracts-secret.html#contracts-secret-options-shb.contracts.secret"
|
||||
],
|
||||
|
|
@ -815,6 +848,15 @@
|
|||
"contracts-ssl-options-shb.contracts.ssl.systemdService": [
|
||||
"contracts-ssl.html#contracts-ssl-options-shb.contracts.ssl.systemdService"
|
||||
],
|
||||
"contracts-test": [
|
||||
"contracts.html#contracts-test"
|
||||
],
|
||||
"contracts-videos": [
|
||||
"contracts.html#contracts-videos"
|
||||
],
|
||||
"contracts-why": [
|
||||
"contracts.html#contracts-why"
|
||||
],
|
||||
"contributing": [
|
||||
"contributing.html#contributing"
|
||||
],
|
||||
|
|
@ -857,6 +899,15 @@
|
|||
"contributing-upstream": [
|
||||
"contributing.html#contributing-upstream"
|
||||
],
|
||||
"create-comprehensive-tests": [
|
||||
"service-implementation-guide.html#create-comprehensive-tests"
|
||||
],
|
||||
"create-service-documentation": [
|
||||
"service-implementation-guide.html#create-service-documentation"
|
||||
],
|
||||
"create-service-module": [
|
||||
"service-implementation-guide.html#create-service-module"
|
||||
],
|
||||
"demo-homeassistant": [
|
||||
"demo-homeassistant.html#demo-homeassistant"
|
||||
],
|
||||
|
|
@ -866,6 +917,9 @@
|
|||
"demo-homeassistant-deploy-basic": [
|
||||
"demo-homeassistant.html#demo-homeassistant-deploy-basic"
|
||||
],
|
||||
"demo-homeassistant-deploy-colmena": [
|
||||
"demo-homeassistant.html#demo-homeassistant-deploy-colmena"
|
||||
],
|
||||
"demo-homeassistant-deploy-ldap": [
|
||||
"demo-homeassistant.html#demo-homeassistant-deploy-ldap"
|
||||
],
|
||||
|
|
@ -896,9 +950,6 @@
|
|||
"demo-homeassistant-virtual-machine": [
|
||||
"demo-homeassistant.html#demo-homeassistant-virtual-machine"
|
||||
],
|
||||
"demo-homeassistant-deploy-colmena": [
|
||||
"demo-homeassistant.html#demo-homeassistant-deploy-colmena"
|
||||
],
|
||||
"demo-nextcloud": [
|
||||
"demo-nextcloud.html#demo-nextcloud"
|
||||
],
|
||||
|
|
@ -947,12 +998,69 @@
|
|||
"demos": [
|
||||
"demos.html#demos"
|
||||
],
|
||||
"external-exporter": [
|
||||
"service-implementation-guide.html#external-exporter"
|
||||
],
|
||||
"handle-unfree-dependencies": [
|
||||
"service-implementation-guide.html#handle-unfree-dependencies"
|
||||
],
|
||||
"how-redirects-work": [
|
||||
"service-implementation-guide.html#how-redirects-work"
|
||||
],
|
||||
"implementation-considerations": [
|
||||
"service-implementation-guide.html#implementation-considerations"
|
||||
],
|
||||
"implementation-steps": [
|
||||
"service-implementation-guide.html#implementation-steps"
|
||||
],
|
||||
"iterative-development-approach": [
|
||||
"service-implementation-guide.html#iterative-development-approach"
|
||||
],
|
||||
"local-testing": [
|
||||
"service-implementation-guide.html#local-testing"
|
||||
],
|
||||
"monitoring-failures": [
|
||||
"service-implementation-guide.html#monitoring-failures"
|
||||
],
|
||||
"monitoring-implementation": [
|
||||
"service-implementation-guide.html#monitoring-implementation"
|
||||
],
|
||||
"native-prometheus-metrics": [
|
||||
"service-implementation-guide.html#native-prometheus-metrics"
|
||||
],
|
||||
"nixpkgs-integration": [
|
||||
"service-implementation-guide.html#nixpkgs-integration"
|
||||
],
|
||||
"pre-implementation-research": [
|
||||
"service-implementation-guide.html#pre-implementation-research"
|
||||
],
|
||||
"preface": [
|
||||
"index.html#preface"
|
||||
],
|
||||
"quick-reference": [
|
||||
"service-implementation-guide.html#quick-reference"
|
||||
],
|
||||
"redirect-management": [
|
||||
"service-implementation-guide.html#redirect-management"
|
||||
],
|
||||
"redirect-patterns": [
|
||||
"service-implementation-guide.html#redirect-patterns"
|
||||
],
|
||||
"required-test-variants": [
|
||||
"service-implementation-guide.html#required-test-variants"
|
||||
],
|
||||
"resources": [
|
||||
"service-implementation-guide.html#resources"
|
||||
],
|
||||
"security-best-practices": [
|
||||
"service-implementation-guide.html#security-best-practices"
|
||||
],
|
||||
"self-host-blocks-manual": [
|
||||
"index.html#self-host-blocks-manual"
|
||||
],
|
||||
"service-implementation-guide": [
|
||||
"service-implementation-guide.html#service-implementation-guide"
|
||||
],
|
||||
"services": [
|
||||
"services.html#services"
|
||||
],
|
||||
|
|
@ -968,39 +1076,6 @@
|
|||
"services-forgejo-options": [
|
||||
"services-forgejo.html#services-forgejo-options"
|
||||
],
|
||||
"services-forgejo-options-shb.forgejo.users": [
|
||||
"services-forgejo.html#services-forgejo-options-shb.forgejo.users"
|
||||
],
|
||||
"services-forgejo-options-shb.forgejo.users._name_.email": [
|
||||
"services-forgejo.html#services-forgejo-options-shb.forgejo.users._name_.email"
|
||||
],
|
||||
"services-forgejo-options-shb.forgejo.users._name_.isAdmin": [
|
||||
"services-forgejo.html#services-forgejo-options-shb.forgejo.users._name_.isAdmin"
|
||||
],
|
||||
"services-forgejo-options-shb.forgejo.users._name_.password": [
|
||||
"services-forgejo.html#services-forgejo-options-shb.forgejo.users._name_.password"
|
||||
],
|
||||
"services-forgejo-options-shb.forgejo.users._name_.password.request": [
|
||||
"services-forgejo.html#services-forgejo-options-shb.forgejo.users._name_.password.request"
|
||||
],
|
||||
"services-forgejo-options-shb.forgejo.users._name_.password.request.group": [
|
||||
"services-forgejo.html#services-forgejo-options-shb.forgejo.users._name_.password.request.group"
|
||||
],
|
||||
"services-forgejo-options-shb.forgejo.users._name_.password.request.mode": [
|
||||
"services-forgejo.html#services-forgejo-options-shb.forgejo.users._name_.password.request.mode"
|
||||
],
|
||||
"services-forgejo-options-shb.forgejo.users._name_.password.request.owner": [
|
||||
"services-forgejo.html#services-forgejo-options-shb.forgejo.users._name_.password.request.owner"
|
||||
],
|
||||
"services-forgejo-options-shb.forgejo.users._name_.password.request.restartUnits": [
|
||||
"services-forgejo.html#services-forgejo-options-shb.forgejo.users._name_.password.request.restartUnits"
|
||||
],
|
||||
"services-forgejo-options-shb.forgejo.users._name_.password.result": [
|
||||
"services-forgejo.html#services-forgejo-options-shb.forgejo.users._name_.password.result"
|
||||
],
|
||||
"services-forgejo-options-shb.forgejo.users._name_.password.result.path": [
|
||||
"services-forgejo.html#services-forgejo-options-shb.forgejo.users._name_.password.result.path"
|
||||
],
|
||||
"services-forgejo-options-shb.forgejo.backup": [
|
||||
"services-forgejo.html#services-forgejo-options-shb.forgejo.backup"
|
||||
],
|
||||
|
|
@ -1187,6 +1262,27 @@
|
|||
"services-forgejo-options-shb.forgejo.sso.sharedSecret": [
|
||||
"services-forgejo.html#services-forgejo-options-shb.forgejo.sso.sharedSecret"
|
||||
],
|
||||
"services-forgejo-options-shb.forgejo.sso.sharedSecret.request": [
|
||||
"services-forgejo.html#services-forgejo-options-shb.forgejo.sso.sharedSecret.request"
|
||||
],
|
||||
"services-forgejo-options-shb.forgejo.sso.sharedSecret.request.group": [
|
||||
"services-forgejo.html#services-forgejo-options-shb.forgejo.sso.sharedSecret.request.group"
|
||||
],
|
||||
"services-forgejo-options-shb.forgejo.sso.sharedSecret.request.mode": [
|
||||
"services-forgejo.html#services-forgejo-options-shb.forgejo.sso.sharedSecret.request.mode"
|
||||
],
|
||||
"services-forgejo-options-shb.forgejo.sso.sharedSecret.request.owner": [
|
||||
"services-forgejo.html#services-forgejo-options-shb.forgejo.sso.sharedSecret.request.owner"
|
||||
],
|
||||
"services-forgejo-options-shb.forgejo.sso.sharedSecret.request.restartUnits": [
|
||||
"services-forgejo.html#services-forgejo-options-shb.forgejo.sso.sharedSecret.request.restartUnits"
|
||||
],
|
||||
"services-forgejo-options-shb.forgejo.sso.sharedSecret.result": [
|
||||
"services-forgejo.html#services-forgejo-options-shb.forgejo.sso.sharedSecret.result"
|
||||
],
|
||||
"services-forgejo-options-shb.forgejo.sso.sharedSecret.result.path": [
|
||||
"services-forgejo.html#services-forgejo-options-shb.forgejo.sso.sharedSecret.result.path"
|
||||
],
|
||||
"services-forgejo-options-shb.forgejo.sso.sharedSecretForAuthelia": [
|
||||
"services-forgejo.html#services-forgejo-options-shb.forgejo.sso.sharedSecretForAuthelia"
|
||||
],
|
||||
|
|
@ -1211,30 +1307,42 @@
|
|||
"services-forgejo-options-shb.forgejo.sso.sharedSecretForAuthelia.result.path": [
|
||||
"services-forgejo.html#services-forgejo-options-shb.forgejo.sso.sharedSecretForAuthelia.result.path"
|
||||
],
|
||||
"services-forgejo-options-shb.forgejo.sso.sharedSecret.request": [
|
||||
"services-forgejo.html#services-forgejo-options-shb.forgejo.sso.sharedSecret.request"
|
||||
],
|
||||
"services-forgejo-options-shb.forgejo.sso.sharedSecret.request.group": [
|
||||
"services-forgejo.html#services-forgejo-options-shb.forgejo.sso.sharedSecret.request.group"
|
||||
],
|
||||
"services-forgejo-options-shb.forgejo.sso.sharedSecret.request.mode": [
|
||||
"services-forgejo.html#services-forgejo-options-shb.forgejo.sso.sharedSecret.request.mode"
|
||||
],
|
||||
"services-forgejo-options-shb.forgejo.sso.sharedSecret.request.owner": [
|
||||
"services-forgejo.html#services-forgejo-options-shb.forgejo.sso.sharedSecret.request.owner"
|
||||
],
|
||||
"services-forgejo-options-shb.forgejo.sso.sharedSecret.request.restartUnits": [
|
||||
"services-forgejo.html#services-forgejo-options-shb.forgejo.sso.sharedSecret.request.restartUnits"
|
||||
],
|
||||
"services-forgejo-options-shb.forgejo.sso.sharedSecret.result": [
|
||||
"services-forgejo.html#services-forgejo-options-shb.forgejo.sso.sharedSecret.result"
|
||||
],
|
||||
"services-forgejo-options-shb.forgejo.sso.sharedSecret.result.path": [
|
||||
"services-forgejo.html#services-forgejo-options-shb.forgejo.sso.sharedSecret.result.path"
|
||||
],
|
||||
"services-forgejo-options-shb.forgejo.subdomain": [
|
||||
"services-forgejo.html#services-forgejo-options-shb.forgejo.subdomain"
|
||||
],
|
||||
"services-forgejo-options-shb.forgejo.users": [
|
||||
"services-forgejo.html#services-forgejo-options-shb.forgejo.users"
|
||||
],
|
||||
"services-forgejo-options-shb.forgejo.users._name_.email": [
|
||||
"services-forgejo.html#services-forgejo-options-shb.forgejo.users._name_.email"
|
||||
],
|
||||
"services-forgejo-options-shb.forgejo.users._name_.isAdmin": [
|
||||
"services-forgejo.html#services-forgejo-options-shb.forgejo.users._name_.isAdmin"
|
||||
],
|
||||
"services-forgejo-options-shb.forgejo.users._name_.password": [
|
||||
"services-forgejo.html#services-forgejo-options-shb.forgejo.users._name_.password"
|
||||
],
|
||||
"services-forgejo-options-shb.forgejo.users._name_.password.request": [
|
||||
"services-forgejo.html#services-forgejo-options-shb.forgejo.users._name_.password.request"
|
||||
],
|
||||
"services-forgejo-options-shb.forgejo.users._name_.password.request.group": [
|
||||
"services-forgejo.html#services-forgejo-options-shb.forgejo.users._name_.password.request.group"
|
||||
],
|
||||
"services-forgejo-options-shb.forgejo.users._name_.password.request.mode": [
|
||||
"services-forgejo.html#services-forgejo-options-shb.forgejo.users._name_.password.request.mode"
|
||||
],
|
||||
"services-forgejo-options-shb.forgejo.users._name_.password.request.owner": [
|
||||
"services-forgejo.html#services-forgejo-options-shb.forgejo.users._name_.password.request.owner"
|
||||
],
|
||||
"services-forgejo-options-shb.forgejo.users._name_.password.request.restartUnits": [
|
||||
"services-forgejo.html#services-forgejo-options-shb.forgejo.users._name_.password.request.restartUnits"
|
||||
],
|
||||
"services-forgejo-options-shb.forgejo.users._name_.password.result": [
|
||||
"services-forgejo.html#services-forgejo-options-shb.forgejo.users._name_.password.result"
|
||||
],
|
||||
"services-forgejo-options-shb.forgejo.users._name_.password.result.path": [
|
||||
"services-forgejo.html#services-forgejo-options-shb.forgejo.users._name_.password.result.path"
|
||||
],
|
||||
"services-forgejo-usage": [
|
||||
"services-forgejo.html#services-forgejo-usage"
|
||||
],
|
||||
|
|
@ -1244,12 +1352,12 @@
|
|||
"services-forgejo-usage-configuration": [
|
||||
"services-forgejo.html#services-forgejo-usage-configuration"
|
||||
],
|
||||
"services-forgejo-usage-https": [
|
||||
"services-forgejo.html#services-forgejo-usage-https"
|
||||
],
|
||||
"services-forgejo-usage-extra-settings": [
|
||||
"services-forgejo.html#services-forgejo-usage-extra-settings"
|
||||
],
|
||||
"services-forgejo-usage-https": [
|
||||
"services-forgejo.html#services-forgejo-usage-https"
|
||||
],
|
||||
"services-forgejo-usage-ldap": [
|
||||
"services-forgejo.html#services-forgejo-usage-ldap"
|
||||
],
|
||||
|
|
@ -1259,6 +1367,9 @@
|
|||
"services-nextcloudserver": [
|
||||
"services-nextcloud.html#services-nextcloudserver"
|
||||
],
|
||||
"services-nextcloudserver-dashboard": [
|
||||
"services-nextcloud.html#services-nextcloudserver-dashboard"
|
||||
],
|
||||
"services-nextcloudserver-debug": [
|
||||
"services-nextcloud.html#services-nextcloudserver-debug"
|
||||
],
|
||||
|
|
@ -1268,9 +1379,6 @@
|
|||
"services-nextcloudserver-features": [
|
||||
"services-nextcloud.html#services-nextcloudserver-features"
|
||||
],
|
||||
"services-nextcloudserver-dashboard": [
|
||||
"services-nextcloud.html#services-nextcloudserver-dashboard"
|
||||
],
|
||||
"services-nextcloudserver-options": [
|
||||
"services-nextcloud.html#services-nextcloudserver-options"
|
||||
],
|
||||
|
|
@ -1373,20 +1481,14 @@
|
|||
"services-nextcloudserver-options-shb.nextcloud.apps.memories": [
|
||||
"services-nextcloud.html#services-nextcloudserver-options-shb.nextcloud.apps.memories"
|
||||
],
|
||||
"services-nextcloudserver-options-shb.nextcloud.apps.memories.vaapi": [
|
||||
"services-nextcloud.html#services-nextcloudserver-options-shb.nextcloud.apps.memories.vaapi"
|
||||
],
|
||||
"services-nextcloudserver-options-shb.nextcloud.apps.memories.enable": [
|
||||
"services-nextcloud.html#services-nextcloudserver-options-shb.nextcloud.apps.memories.enable"
|
||||
],
|
||||
"services-nextcloudserver-options-shb.nextcloud.apps.memories.photosPath": [
|
||||
"services-nextcloud.html#services-nextcloudserver-options-shb.nextcloud.apps.memories.photosPath"
|
||||
],
|
||||
"services-nextcloudserver-options-shb.nextcloud.apps.recognize": [
|
||||
"services-nextcloud.html#services-nextcloudserver-options-shb.nextcloud.apps.recognize"
|
||||
],
|
||||
"services-nextcloudserver-options-shb.nextcloud.apps.recognize.enable": [
|
||||
"services-nextcloud.html#services-nextcloudserver-options-shb.nextcloud.apps.recognize.enable"
|
||||
"services-nextcloudserver-options-shb.nextcloud.apps.memories.vaapi": [
|
||||
"services-nextcloud.html#services-nextcloudserver-options-shb.nextcloud.apps.memories.vaapi"
|
||||
],
|
||||
"services-nextcloudserver-options-shb.nextcloud.apps.onlyoffice": [
|
||||
"services-nextcloud.html#services-nextcloudserver-options-shb.nextcloud.apps.onlyoffice"
|
||||
|
|
@ -1430,6 +1532,12 @@
|
|||
"services-nextcloudserver-options-shb.nextcloud.apps.previewgenerator.recommendedSettings": [
|
||||
"services-nextcloud.html#services-nextcloudserver-options-shb.nextcloud.apps.previewgenerator.recommendedSettings"
|
||||
],
|
||||
"services-nextcloudserver-options-shb.nextcloud.apps.recognize": [
|
||||
"services-nextcloud.html#services-nextcloudserver-options-shb.nextcloud.apps.recognize"
|
||||
],
|
||||
"services-nextcloudserver-options-shb.nextcloud.apps.recognize.enable": [
|
||||
"services-nextcloud.html#services-nextcloudserver-options-shb.nextcloud.apps.recognize.enable"
|
||||
],
|
||||
"services-nextcloudserver-options-shb.nextcloud.apps.sso": [
|
||||
"services-nextcloud.html#services-nextcloudserver-options-shb.nextcloud.apps.sso"
|
||||
],
|
||||
|
|
@ -1457,6 +1565,27 @@
|
|||
"services-nextcloudserver-options-shb.nextcloud.apps.sso.secret": [
|
||||
"services-nextcloud.html#services-nextcloudserver-options-shb.nextcloud.apps.sso.secret"
|
||||
],
|
||||
"services-nextcloudserver-options-shb.nextcloud.apps.sso.secret.request": [
|
||||
"services-nextcloud.html#services-nextcloudserver-options-shb.nextcloud.apps.sso.secret.request"
|
||||
],
|
||||
"services-nextcloudserver-options-shb.nextcloud.apps.sso.secret.request.group": [
|
||||
"services-nextcloud.html#services-nextcloudserver-options-shb.nextcloud.apps.sso.secret.request.group"
|
||||
],
|
||||
"services-nextcloudserver-options-shb.nextcloud.apps.sso.secret.request.mode": [
|
||||
"services-nextcloud.html#services-nextcloudserver-options-shb.nextcloud.apps.sso.secret.request.mode"
|
||||
],
|
||||
"services-nextcloudserver-options-shb.nextcloud.apps.sso.secret.request.owner": [
|
||||
"services-nextcloud.html#services-nextcloudserver-options-shb.nextcloud.apps.sso.secret.request.owner"
|
||||
],
|
||||
"services-nextcloudserver-options-shb.nextcloud.apps.sso.secret.request.restartUnits": [
|
||||
"services-nextcloud.html#services-nextcloudserver-options-shb.nextcloud.apps.sso.secret.request.restartUnits"
|
||||
],
|
||||
"services-nextcloudserver-options-shb.nextcloud.apps.sso.secret.result": [
|
||||
"services-nextcloud.html#services-nextcloudserver-options-shb.nextcloud.apps.sso.secret.result"
|
||||
],
|
||||
"services-nextcloudserver-options-shb.nextcloud.apps.sso.secret.result.path": [
|
||||
"services-nextcloud.html#services-nextcloudserver-options-shb.nextcloud.apps.sso.secret.result.path"
|
||||
],
|
||||
"services-nextcloudserver-options-shb.nextcloud.apps.sso.secretForAuthelia": [
|
||||
"services-nextcloud.html#services-nextcloudserver-options-shb.nextcloud.apps.sso.secretForAuthelia"
|
||||
],
|
||||
|
|
@ -1481,27 +1610,6 @@
|
|||
"services-nextcloudserver-options-shb.nextcloud.apps.sso.secretForAuthelia.result.path": [
|
||||
"services-nextcloud.html#services-nextcloudserver-options-shb.nextcloud.apps.sso.secretForAuthelia.result.path"
|
||||
],
|
||||
"services-nextcloudserver-options-shb.nextcloud.apps.sso.secret.request": [
|
||||
"services-nextcloud.html#services-nextcloudserver-options-shb.nextcloud.apps.sso.secret.request"
|
||||
],
|
||||
"services-nextcloudserver-options-shb.nextcloud.apps.sso.secret.request.group": [
|
||||
"services-nextcloud.html#services-nextcloudserver-options-shb.nextcloud.apps.sso.secret.request.group"
|
||||
],
|
||||
"services-nextcloudserver-options-shb.nextcloud.apps.sso.secret.request.mode": [
|
||||
"services-nextcloud.html#services-nextcloudserver-options-shb.nextcloud.apps.sso.secret.request.mode"
|
||||
],
|
||||
"services-nextcloudserver-options-shb.nextcloud.apps.sso.secret.request.owner": [
|
||||
"services-nextcloud.html#services-nextcloudserver-options-shb.nextcloud.apps.sso.secret.request.owner"
|
||||
],
|
||||
"services-nextcloudserver-options-shb.nextcloud.apps.sso.secret.request.restartUnits": [
|
||||
"services-nextcloud.html#services-nextcloudserver-options-shb.nextcloud.apps.sso.secret.request.restartUnits"
|
||||
],
|
||||
"services-nextcloudserver-options-shb.nextcloud.apps.sso.secret.result": [
|
||||
"services-nextcloud.html#services-nextcloudserver-options-shb.nextcloud.apps.sso.secret.result"
|
||||
],
|
||||
"services-nextcloudserver-options-shb.nextcloud.apps.sso.secret.result.path": [
|
||||
"services-nextcloud.html#services-nextcloudserver-options-shb.nextcloud.apps.sso.secret.result.path"
|
||||
],
|
||||
"services-nextcloudserver-options-shb.nextcloud.autoDisableMaintenanceModeOnStart": [
|
||||
"services-nextcloud.html#services-nextcloudserver-options-shb.nextcloud.autoDisableMaintenanceModeOnStart"
|
||||
],
|
||||
|
|
@ -1835,6 +1943,21 @@
|
|||
"services-vaultwarden-zfs": [
|
||||
"services-vaultwarden.html#services-vaultwarden-zfs"
|
||||
],
|
||||
"test-failures": [
|
||||
"service-implementation-guide.html#test-failures"
|
||||
],
|
||||
"testing-and-validation": [
|
||||
"service-implementation-guide.html#testing-and-validation"
|
||||
],
|
||||
"understand-target-service": [
|
||||
"service-implementation-guide.html#understand-target-service"
|
||||
],
|
||||
"update-flake-configuration": [
|
||||
"service-implementation-guide.html#update-flake-configuration"
|
||||
],
|
||||
"update-redirects-automatically": [
|
||||
"service-implementation-guide.html#update-redirects-automatically"
|
||||
],
|
||||
"usage": [
|
||||
"usage.html#usage"
|
||||
],
|
||||
|
|
@ -1850,9 +1973,6 @@
|
|||
"usage-example-nixosrebuild": [
|
||||
"usage.html#usage-example-nixosrebuild"
|
||||
],
|
||||
"usage-lib": [
|
||||
"usage.html#usage-lib"
|
||||
],
|
||||
"usage-flake": [
|
||||
"usage.html#usage-flake"
|
||||
],
|
||||
|
|
@ -1868,7 +1988,10 @@
|
|||
"usage-flake-tag": [
|
||||
"usage.html#usage-flake-tag"
|
||||
],
|
||||
"usage-lib": [
|
||||
"usage.html#usage-lib"
|
||||
],
|
||||
"usage-secrets": [
|
||||
"usage.html#usage-secrets"
|
||||
]
|
||||
}
|
||||
}
|
||||
611
docs/service-implementation-guide.md
Normal file
611
docs/service-implementation-guide.md
Normal file
|
|
@ -0,0 +1,611 @@
|
|||
# SelfHostBlocks Service Implementation Guide {#service-implementation-guide}
|
||||
|
||||
This guide documents the complete process for implementing a new service in SelfHostBlocks, based on lessons learned from the nzbget implementation and analysis of existing service patterns.
|
||||
|
||||
**Note**: SelfHostBlocks aims to be "the smallest amount of code above what is available in nixpkgs" (see `docs/contributing.md`). Services should leverage existing nixpkgs options when possible and focus on providing contract integrations rather than reimplementing configuration.
|
||||
|
||||
## What Makes a "Complete" SHB Service {#complete-shb-service}
|
||||
|
||||
According to the project maintainer's criteria, a service is considered fully supported if it includes:
|
||||
|
||||
1. **SSL block integration** - HTTPS/TLS certificate management
|
||||
2. **Backup block integration** - Automated backup of service data
|
||||
3. **Monitoring integration** - Prometheus metrics and health checks
|
||||
4. **LDAP (LLDAP) integration** - Directory-based authentication
|
||||
5. **SSO (Authelia) integration** - Single sign-on authentication
|
||||
6. **Comprehensive tests** - All integration variants tested
|
||||
|
||||
## Pre-Implementation Research {#pre-implementation-research}
|
||||
|
||||
### 1. Analyze Existing Services {#analyze-existing-services}
|
||||
|
||||
Before starting, study existing services to understand patterns:
|
||||
|
||||
```bash
|
||||
# Study service patterns
|
||||
ls modules/services/ # List all services
|
||||
cat modules/services/deluge.nix # Best practice example
|
||||
cat modules/services/vaultwarden.nix # Another good example
|
||||
```
|
||||
|
||||
**Key patterns to identify:**
|
||||
- Configuration structure and options
|
||||
- How contracts are used (SSL, backup, monitoring, secrets)
|
||||
- Authentication integration approaches
|
||||
- Service-specific settings and defaults
|
||||
|
||||
### 2. Understand the Target Service {#understand-target-service}
|
||||
|
||||
Research the service you're implementing:
|
||||
- **Configuration format** (YAML, INI, JSON, etc.)
|
||||
- **Authentication methods** (built-in users, LDAP, OIDC/OAuth)
|
||||
- **API endpoints** (for monitoring/health checks)
|
||||
- **Data directories** (what needs backing up)
|
||||
- **Network requirements** (ports, protocols)
|
||||
- **Dependencies** (databases, external tools)
|
||||
|
||||
### 3. Check NixOS Integration {#check-nixos-integration}
|
||||
|
||||
Verify nixpkgs support:
|
||||
```bash
|
||||
# Check if NixOS service exists
|
||||
nix eval --impure --expr '(import <nixpkgs/nixos> { configuration = {...}: {}; }).options.services' --apply 'builtins.attrNames' --json | jq -r '.[]' | grep -i servicename
|
||||
# or search online: https://search.nixos.org/options?query=services.servicename
|
||||
```
|
||||
|
||||
If no nixpkgs integration exists, you may need to:
|
||||
- Package the service first
|
||||
- Use containerized approach
|
||||
- Request upstream nixpkgs integration
|
||||
|
||||
## Implementation Steps {#implementation-steps}
|
||||
|
||||
### 1. Create the Service Module {#create-service-module}
|
||||
|
||||
Location: `modules/services/servicename.nix`
|
||||
|
||||
**Basic structure:**
|
||||
```nix
|
||||
{ config, pkgs, lib, ... }:
|
||||
|
||||
let
|
||||
cfg = config.shb.servicename;
|
||||
contracts = pkgs.callPackage ../contracts {};
|
||||
shblib = pkgs.callPackage ../../lib {};
|
||||
fqdn = "${cfg.subdomain}.${cfg.domain}";
|
||||
|
||||
# Choose appropriate format based on service config
|
||||
settingsFormat = pkgs.formats.yaml {}; # or .ini, .json, etc.
|
||||
in
|
||||
{
|
||||
options.shb.servicename = {
|
||||
# Core options (always required)
|
||||
enable = lib.mkEnableOption "selfhostblocks.servicename";
|
||||
subdomain = lib.mkOption { ... };
|
||||
domain = lib.mkOption { ... };
|
||||
|
||||
# SSL integration (always include)
|
||||
ssl = lib.mkOption {
|
||||
description = "Path to SSL files";
|
||||
type = lib.types.nullOr contracts.ssl.certs;
|
||||
default = null;
|
||||
};
|
||||
|
||||
# Service-specific options
|
||||
port = lib.mkOption { ... };
|
||||
dataDir = lib.mkOption { ... };
|
||||
settings = lib.mkOption {
|
||||
type = lib.types.submodule {
|
||||
freeformType = settingsFormat.type;
|
||||
options = {
|
||||
# Define key options with descriptions
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
# Authentication options
|
||||
authEndpoint = lib.mkOption {
|
||||
type = lib.types.nullOr lib.types.str;
|
||||
description = "OIDC endpoint for SSO";
|
||||
default = null;
|
||||
};
|
||||
|
||||
ldap = lib.mkOption { ... }; # LDAP integration
|
||||
users = lib.mkOption { ... }; # Local user management
|
||||
|
||||
# Integration options
|
||||
backup = lib.mkOption {
|
||||
type = lib.types.submodule {
|
||||
options = contracts.backup.mkRequester {
|
||||
user = "servicename";
|
||||
sourceDirectories = [ cfg.dataDir ];
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
monitoring = lib.mkOption {
|
||||
type = lib.types.nullOr (lib.types.submodule {
|
||||
options = {
|
||||
# Service-specific monitoring options
|
||||
};
|
||||
});
|
||||
default = null;
|
||||
};
|
||||
|
||||
# System options
|
||||
extraServiceConfig = lib.mkOption { ... };
|
||||
logLevel = lib.mkOption { ... };
|
||||
};
|
||||
|
||||
config = lib.mkIf cfg.enable (lib.mkMerge [
|
||||
{
|
||||
# Base service configuration
|
||||
services.servicename = {
|
||||
enable = true;
|
||||
# Map SHB options to nixpkgs service options
|
||||
};
|
||||
|
||||
# Nginx reverse proxy
|
||||
shb.nginx.vhosts = [{
|
||||
inherit (cfg) subdomain domain ssl;
|
||||
upstream = "http://127.0.0.1:${toString cfg.port}";
|
||||
|
||||
# SSO integration
|
||||
autheliaRules = lib.mkIf (cfg.authEndpoint != null) [
|
||||
{
|
||||
domain = fqdn;
|
||||
policy = "bypass";
|
||||
resources = [ "^/api" ]; # API endpoints
|
||||
}
|
||||
{
|
||||
domain = fqdn;
|
||||
policy = "two_factor";
|
||||
resources = [ "^.*" ]; # Everything else
|
||||
}
|
||||
];
|
||||
}];
|
||||
|
||||
# User/group setup
|
||||
users.users.servicename = {
|
||||
extraGroups = [ "media" ]; # If needed for file access
|
||||
};
|
||||
|
||||
# Directory permissions
|
||||
systemd.tmpfiles.rules = [
|
||||
"d ${cfg.dataDir} 0755 servicename servicename - -"
|
||||
];
|
||||
}
|
||||
|
||||
# Monitoring configuration (conditional)
|
||||
(lib.mkIf (cfg.monitoring != null) {
|
||||
services.prometheus.scrapeConfigs = [{
|
||||
job_name = "servicename";
|
||||
static_configs = [{
|
||||
targets = [ "127.0.0.1:${toString cfg.port}" ];
|
||||
labels = {
|
||||
hostname = config.networking.hostName;
|
||||
domain = cfg.domain;
|
||||
};
|
||||
}];
|
||||
metrics_path = "/metrics"; # or appropriate endpoint
|
||||
scrape_interval = "30s";
|
||||
}];
|
||||
})
|
||||
]);
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Key Implementation Considerations {#implementation-considerations}
|
||||
|
||||
#### Configuration Management {#configuration-management}
|
||||
- **Use freeform settings** when possible: `freeformType = settingsFormat.type`
|
||||
- **Provide sensible defaults** for common options
|
||||
- **Use lib.mkDefault** for user-overridable settings
|
||||
- **Use lib.mkForce** for security-critical settings
|
||||
|
||||
#### Authentication Integration {#authentication-integration}
|
||||
- **SSO (Authelia)**: Use `autheliaRules` with appropriate bypass policies
|
||||
- **LDAP**: Follow the patterns from existing services
|
||||
- **Local users**: Use SHB secret contracts for password management
|
||||
|
||||
#### Security Best Practices {#security-best-practices}
|
||||
- **Bind to localhost**: Services should listen on `127.0.0.1` only
|
||||
- **Use nginx for TLS**: Don't configure TLS in the service itself
|
||||
- **Proper file permissions**: Use systemd.tmpfiles.rules
|
||||
- **Secret management**: Always use SHB secret contracts
|
||||
|
||||
### 3. Monitoring Implementation {#monitoring-implementation}
|
||||
|
||||
Choose the appropriate monitoring approach:
|
||||
|
||||
#### Option A: Native Prometheus Metrics {#native-prometheus-metrics}
|
||||
If the service supports Prometheus natively:
|
||||
```nix
|
||||
services.prometheus.scrapeConfigs = [{
|
||||
job_name = "servicename";
|
||||
static_configs = [{ targets = [ "127.0.0.1:${toString cfg.port}" ]; }];
|
||||
metrics_path = "/metrics";
|
||||
}];
|
||||
```
|
||||
|
||||
#### Option B: API Health Check {#api-health-check}
|
||||
If no native metrics, monitor API endpoints:
|
||||
```nix
|
||||
services.prometheus.scrapeConfigs = [{
|
||||
job_name = "servicename";
|
||||
static_configs = [{ targets = [ "127.0.0.1:${toString cfg.port}" ]; }];
|
||||
metrics_path = "/api/status"; # or appropriate endpoint
|
||||
}];
|
||||
```
|
||||
|
||||
#### Option C: External Exporter {#external-exporter}
|
||||
For services requiring dedicated exporters (like Deluge):
|
||||
```nix
|
||||
services.prometheus.exporters.servicename = {
|
||||
enable = true;
|
||||
# exporter-specific configuration
|
||||
};
|
||||
```
|
||||
|
||||
### 4. Create Comprehensive Tests {#create-comprehensive-tests}
|
||||
|
||||
Location: `test/services/servicename.nix`
|
||||
|
||||
**Test structure:**
|
||||
```nix
|
||||
{ pkgs, ... }:
|
||||
let
|
||||
testLib = pkgs.callPackage ../common.nix {};
|
||||
|
||||
# Common test scripts
|
||||
commonTestScript = testLib.mkScripts {
|
||||
hasSSL = { node, ... }: !(isNull node.config.shb.servicename.ssl);
|
||||
waitForServices = { ... }: [ "nginx.service" "servicename.service" ];
|
||||
waitForPorts = { node, ... }: [ node.config.services.servicename.port ];
|
||||
|
||||
# Service-specific connectivity test
|
||||
extraScript = { node, proto_fqdn, ... }: ''
|
||||
with subtest("service connectivity"):
|
||||
response = curl(client, "", "${proto_fqdn}/api/health")
|
||||
# Add service-specific checks
|
||||
'';
|
||||
};
|
||||
|
||||
# Monitoring test script
|
||||
prometheusTestScript = { nodes, ... }: ''
|
||||
server.wait_for_open_port(${toString nodes.server.config.services.servicename.port})
|
||||
with subtest("prometheus monitoring"):
|
||||
# Test the actual monitoring endpoint
|
||||
response = server.succeed("curl -sSf http://localhost:${port}/metrics")
|
||||
# Validate response format
|
||||
'';
|
||||
|
||||
# Base configuration
|
||||
basic = { config, ... }: {
|
||||
imports = [
|
||||
testLib.baseModule
|
||||
../../modules/services/servicename.nix
|
||||
];
|
||||
|
||||
shb.servicename = {
|
||||
enable = true;
|
||||
inherit (config.test) domain subdomain;
|
||||
# Basic configuration
|
||||
};
|
||||
};
|
||||
|
||||
in {
|
||||
# Test variants (all 6 required)
|
||||
basic = pkgs.testers.runNixOSTest { ... };
|
||||
backup = pkgs.testers.runNixOSTest { ... };
|
||||
https = pkgs.testers.runNixOSTest { ... };
|
||||
ldap = pkgs.testers.runNixOSTest { ... };
|
||||
monitoring = pkgs.testers.runNixOSTest { ... };
|
||||
sso = pkgs.testers.runNixOSTest { ... };
|
||||
}
|
||||
```
|
||||
|
||||
#### Required Test Variants {#required-test-variants}
|
||||
|
||||
1. **basic**: Core functionality without authentication
|
||||
2. **backup**: Tests backup integration
|
||||
3. **https**: Tests SSL/TLS integration
|
||||
4. **ldap**: Tests LDAP authentication
|
||||
5. **monitoring**: Tests Prometheus integration
|
||||
6. **sso**: Tests Authelia SSO integration
|
||||
|
||||
### 5. Update Flake Configuration {#update-flake-configuration}
|
||||
|
||||
Add to `flake.nix`:
|
||||
|
||||
```nix
|
||||
allModules = [
|
||||
# ... existing modules
|
||||
modules/services/servicename.nix
|
||||
];
|
||||
```
|
||||
|
||||
```nix
|
||||
checks = {
|
||||
# ... existing checks
|
||||
// (vm_test "servicename" ./test/services/servicename.nix)
|
||||
};
|
||||
```
|
||||
|
||||
### 6. Create Service Documentation {#create-service-documentation}
|
||||
|
||||
Create comprehensive documentation for the new service:
|
||||
|
||||
**Location**: `modules/services/servicename/docs/default.md`
|
||||
|
||||
```markdown
|
||||
# ServiceName Service {\#services-servicename}
|
||||
|
||||
Brief description of what the service does.
|
||||
|
||||
## Features {\#services-servicename-features}
|
||||
|
||||
- Feature 1
|
||||
- Feature 2
|
||||
|
||||
## Usage {\#services-servicename-usage}
|
||||
|
||||
### Basic Configuration {\#services-servicename-basic}
|
||||
|
||||
shb.servicename = {
|
||||
enable = true;
|
||||
domain = "example.com";
|
||||
subdomain = "servicename";
|
||||
};
|
||||
|
||||
### SSL Configuration {\#services-servicename-ssl}
|
||||
|
||||
shb.servicename.ssl.paths = {
|
||||
cert = /path/to/cert;
|
||||
key = /path/to/key;
|
||||
};
|
||||
|
||||
## Options Reference {\#services-servicename-options}
|
||||
|
||||
{=include=} options
|
||||
id-prefix: services-servicename-options-
|
||||
list-id: selfhostblocks-servicename-options
|
||||
source: @OPTIONS_JSON@
|
||||
```
|
||||
|
||||
**Important**: Use consistent heading ID patterns:
|
||||
- Service overview: `{\#services-servicename}`
|
||||
- Features: `{\#services-servicename-features}`
|
||||
- Usage sections: `{\#services-servicename-basic}`, `{\#services-servicename-ssl}`, etc.
|
||||
- Options: `{\#services-servicename-options}`
|
||||
|
||||
Note: Replace `servicename` with your actual service name (e.g., `nzbget`, `jellyfin`).
|
||||
|
||||
### 7. Update Redirects Automatically {#update-redirects-automatically}
|
||||
|
||||
After creating documentation, generate the required redirects:
|
||||
|
||||
```bash
|
||||
# Scan documentation and add missing redirects
|
||||
nix run .#update-redirects
|
||||
|
||||
# Review the changes
|
||||
git diff docs/redirects.json
|
||||
|
||||
# The tool will show what redirects were added
|
||||
```
|
||||
|
||||
The automation will:
|
||||
- Find all heading IDs in your documentation
|
||||
- Generate appropriate redirect entries
|
||||
- Add them to `docs/redirects.json`
|
||||
- Follow established naming patterns
|
||||
|
||||
### 8. Handle Unfree Dependencies {#handle-unfree-dependencies}
|
||||
|
||||
If the service requires unfree packages:
|
||||
|
||||
```nix
|
||||
# In flake.nix
|
||||
config = {
|
||||
allowUnfree = true;
|
||||
permittedInsecurePackages = [
|
||||
# List any required insecure packages
|
||||
];
|
||||
};
|
||||
```
|
||||
|
||||
Update CI workflow if needed:
|
||||
```yaml
|
||||
# In .github/workflows/build.yaml
|
||||
- name: Setup Nix
|
||||
uses: cachix/install-nix-action@v31
|
||||
with:
|
||||
extra_nix_config: |
|
||||
allow-unfree = true
|
||||
```
|
||||
|
||||
## Testing and Validation {#testing-and-validation}
|
||||
|
||||
### Local Testing {#local-testing}
|
||||
```bash
|
||||
# Test redirect automation
|
||||
nix run .#update-redirects
|
||||
|
||||
# Test all service variants (replace ${system} with your system, e.g., x86_64-linux)
|
||||
nix build .#checks.${system}.vm_servicename_basic
|
||||
nix build .#checks.${system}.vm_servicename_backup
|
||||
nix build .#checks.${system}.vm_servicename_https
|
||||
nix build .#checks.${system}.vm_servicename_ldap
|
||||
nix build .#checks.${system}.vm_servicename_monitoring
|
||||
nix build .#checks.${system}.vm_servicename_sso
|
||||
|
||||
# Or run all tests (as recommended in docs/contributing.md)
|
||||
nix flake check
|
||||
|
||||
# For interactive testing and debugging, see docs/contributing.md:
|
||||
# nix run .#checks.${system}.vm_servicename_basic.driverInteractive
|
||||
|
||||
# Test documentation build (includes redirect validation)
|
||||
nix build .#manualHtml
|
||||
```
|
||||
|
||||
### Iterative Development Approach {#iterative-development-approach}
|
||||
|
||||
1. **Start with basic functionality** - get core service working
|
||||
2. **Add SSL integration** - enable HTTPS
|
||||
3. **Add backup integration** - ensure data protection
|
||||
4. **Add monitoring** - implement health checks
|
||||
5. **Add authentication** - LDAP and SSO integration
|
||||
6. **Create documentation** - write service documentation with heading IDs
|
||||
7. **Update redirects** - run `nix run .#update-redirects` to generate redirects
|
||||
8. **Comprehensive testing** - all 6 test variants
|
||||
9. **Final validation** - ensure documentation builds correctly
|
||||
|
||||
## Common Pitfalls and Solutions {#common-pitfalls-and-solutions}
|
||||
|
||||
### Configuration Issues {#configuration-issues}
|
||||
- **Problem**: Service doesn't start due to config validation
|
||||
- **Solution**: Use `lib.mkDefault` for user settings, `lib.mkForce` for security settings
|
||||
|
||||
### Authentication Integration {#authentication-integration-pitfalls}
|
||||
- **Problem**: SSO redirect loops or access denied
|
||||
- **Solution**: Check `autheliaRules` bypass patterns for API endpoints
|
||||
|
||||
### Monitoring Failures {#monitoring-failures}
|
||||
- **Problem**: Prometheus scraping fails with 404
|
||||
- **Solution**: Verify the actual API endpoints the service provides
|
||||
|
||||
### Test Failures {#test-failures}
|
||||
- **Problem**: VM tests timeout or fail connectivity
|
||||
- **Solution**: Check `waitForServices` and `waitForPorts` configurations
|
||||
|
||||
### Nixpkgs Integration {#nixpkgs-integration}
|
||||
- **Problem**: Service options don't match SHB needs
|
||||
- **Solution**: Map SHB options to nixpkgs options, use `extraConfig` for overrides
|
||||
|
||||
## Best Practices Summary {#best-practices-summary}
|
||||
|
||||
1. **Follow existing patterns** - study deluge.nix and vaultwarden.nix
|
||||
2. **Use freeform configuration** - maximum flexibility with typed key options
|
||||
3. **Implement all contracts** - SSL, backup, monitoring, secrets
|
||||
4. **Test comprehensively** - all 6 integration variants
|
||||
5. **Security first** - localhost binding, proper permissions, secret management
|
||||
6. **Document thoroughly** - clear descriptions for all options
|
||||
7. **Iterative development** - build complexity gradually
|
||||
8. **CI/CD validation** - ensure all tests pass before submission
|
||||
|
||||
## Redirect Management {#redirect-management}
|
||||
|
||||
SelfHostBlocks uses `nixos-render-docs` for documentation generation, which includes built-in redirect validation. The `docs/redirects.json` file maps documentation identifiers to their target URLs.
|
||||
|
||||
### Automated Redirect Generation {#automated-redirect-generation}
|
||||
|
||||
SelfHostBlocks includes an automated redirect management tool that leverages the official `nixos-render-docs` ecosystem:
|
||||
|
||||
```bash
|
||||
# Generate fresh redirects from HTML documentation
|
||||
nix run .#update-redirects
|
||||
```
|
||||
|
||||
This tool:
|
||||
- **Generates HTML documentation** using `nixos-render-docs` with redirect collection enabled
|
||||
- **Scans actual HTML files** for anchor IDs to ensure perfect accuracy
|
||||
- **Creates fresh redirects** from scratch by mapping anchors to their real file locations
|
||||
- **Filters system-generated anchors** (excludes `opt-*` and `selfhostblock*` entries)
|
||||
- **Provides interactive confirmation** before updating `docs/redirects.json`
|
||||
|
||||
### How Redirects Work {#how-redirects-work}
|
||||
|
||||
1. **nixos-render-docs validation**: During documentation builds, `nixos-render-docs` automatically validates that all heading IDs have corresponding redirect entries
|
||||
2. **Automated maintenance**: The `update-redirects` tool automatically maintains `redirects.json` by:
|
||||
- Building HTML documentation with patched `nixos-render-docs`
|
||||
- Scanning generated HTML files for actual anchor IDs and their file locations
|
||||
- Creating accurate redirect mappings without guesswork or pattern matching
|
||||
3. **Manual override**: You can still manually edit `docs/redirects.json` for special cases
|
||||
|
||||
### Redirect Patterns {#redirect-patterns}
|
||||
|
||||
The automation follows these patterns when mapping headings to redirect targets:
|
||||
|
||||
| Heading ID | Source File | Redirect Target |
|
||||
|------------|-------------|-----------------|
|
||||
| `services-nzbget-basic` | `modules/services/nzbget/docs/default.md` | `["services-nzbget.html#services-nzbget-basic"]` |
|
||||
| `blocks-monitoring` | `modules/blocks/monitoring/docs/default.md` | `["blocks-monitoring.html#blocks-monitoring"]` |
|
||||
| `demo-nextcloud` | `demo/nextcloud/README.md` | `["demo-nextcloud.html#demo-nextcloud"]` |
|
||||
| `contracts` | `docs/contracts.md` | `["contracts.html#contracts"]` |
|
||||
|
||||
Note: Redirects always include the anchor link (`#heading-id`) to jump to the specific heading within the target page.
|
||||
|
||||
### Adding New Service Documentation {#adding-new-service-documentation}
|
||||
|
||||
When implementing a new service, the redirect workflow is now automated:
|
||||
|
||||
1. **Write documentation** with heading IDs:
|
||||
```markdown
|
||||
# NewService {\#services-newservice}
|
||||
## Basic Configuration {\#services-newservice-basic}
|
||||
```
|
||||
|
||||
2. **Update redirects automatically**:
|
||||
```bash
|
||||
nix run .#update-redirects
|
||||
```
|
||||
|
||||
3. **Review and commit** the changes:
|
||||
```bash
|
||||
git add docs/redirects.json modules/services/newservice/docs/default.md
|
||||
git commit -m "Add newservice documentation"
|
||||
```
|
||||
|
||||
### Build-time Validation {#build-time-validation}
|
||||
|
||||
The documentation build process will fail if:
|
||||
- Any documentation heading ID lacks a corresponding redirect entry
|
||||
- Redirect targets point to non-existent content
|
||||
- There are formatting errors in the redirects file
|
||||
|
||||
This ensures documentation links remain functional when content is moved or reorganized.
|
||||
|
||||
## Resources {#resources}
|
||||
|
||||
- **Contributing guide**: `docs/contributing.md` for authoritative development workflows and testing procedures
|
||||
- **Existing services**: `modules/services/` for patterns and implementation examples
|
||||
- **Contracts documentation**: `modules/contracts/` for understanding integration interfaces
|
||||
- **Test framework**: `test/common.nix` for testing utilities and patterns
|
||||
- **NixOS options**: https://search.nixos.org/options for upstream service options
|
||||
- **SHB documentation**: Generated docs showing existing service patterns
|
||||
- **Redirect automation**: `nix run .#update-redirects` for automated redirect management
|
||||
- **nixos-render-docs**: Built-in redirect validation and documentation generation
|
||||
|
||||
## Quick Reference {#quick-reference}
|
||||
|
||||
### Complete Workflow {#complete-workflow}
|
||||
```bash
|
||||
# 1. Implement service module
|
||||
vim modules/services/SERVICENAME.nix
|
||||
|
||||
# 2. Create tests
|
||||
vim test/services/SERVICENAME.nix
|
||||
|
||||
# 3. Update flake
|
||||
vim flake.nix # Add to allModules and checks
|
||||
|
||||
# 4. Write documentation
|
||||
vim modules/services/SERVICENAME/docs/default.md
|
||||
|
||||
# 5. Generate redirects
|
||||
nix run .#update-redirects
|
||||
|
||||
# 6. Test everything
|
||||
nix flake check # Run all tests (recommended)
|
||||
# Or test specific variants:
|
||||
# nix build .#checks.${system}.vm_SERVICENAME_basic
|
||||
nix build .#manualHtml
|
||||
|
||||
# 7. Commit changes
|
||||
git add .
|
||||
git commit -m "Add SERVICENAME with full integration"
|
||||
```
|
||||
|
||||
This guide provides a complete roadmap for implementing production-ready SelfHostBlocks services that meet the project's quality standards.
|
||||
78
flake.nix
78
flake.nix
|
|
@ -104,6 +104,58 @@
|
|||
release = builtins.readFile ./VERSION;
|
||||
};
|
||||
|
||||
# Documentation redirect generation tool - scans HTML files for anchor mappings
|
||||
packages.generateRedirects =
|
||||
let
|
||||
# Python patch to inject redirect collector
|
||||
pythonPatch = pkgs.writeText "nixos-render-docs-patch.py" ''
|
||||
# Load redirect collector patch
|
||||
try:
|
||||
import sys, os
|
||||
sys.path.insert(0, os.path.dirname(__file__) + '/..')
|
||||
import missing_refs_collector
|
||||
except Exception as e:
|
||||
print(f"Warning: Failed to load redirect collector: {e}", file=sys.stderr)
|
||||
'';
|
||||
|
||||
# Patched nixos-render-docs that collects redirects during HTML generation
|
||||
nixos-render-docs-patched = pkgs.writeShellApplication {
|
||||
name = "nixos-render-docs";
|
||||
runtimeInputs = [ pkgs.nixos-render-docs ];
|
||||
text = ''
|
||||
TEMP_DIR=$(mktemp -d); trap 'rm -rf "$TEMP_DIR"' EXIT
|
||||
|
||||
cp -r ${pkgs.nixos-render-docs}/${pkgs.python3.sitePackages}/nixos_render_docs "$TEMP_DIR/"
|
||||
chmod -R +w "$TEMP_DIR"
|
||||
cp ${./docs/generate-redirects-nixos-render-docs.py} "$TEMP_DIR/missing_refs_collector.py"
|
||||
echo '{}' > "$TEMP_DIR/empty_redirects.json"
|
||||
cat ${pythonPatch} >> "$TEMP_DIR/nixos_render_docs/__init__.py"
|
||||
|
||||
ARGS=()
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
--redirects) ARGS+=("$1" "$TEMP_DIR/empty_redirects.json"); shift 2 ;;
|
||||
*) ARGS+=("$1"); shift ;;
|
||||
esac
|
||||
done
|
||||
|
||||
export PYTHONPATH="$TEMP_DIR:''${PYTHONPATH:-}"
|
||||
nixos-render-docs "''${ARGS[@]}"
|
||||
'';
|
||||
};
|
||||
in
|
||||
(pkgs.callPackage ./docs {
|
||||
inherit nmdsrc;
|
||||
allModules = allModules ++ contractDummyModules;
|
||||
release = builtins.readFile ./VERSION;
|
||||
nixos-render-docs = nixos-render-docs-patched;
|
||||
}).overrideAttrs (old: {
|
||||
installPhase = ''
|
||||
${old.installPhase}
|
||||
ln -sf share/doc/selfhostblocks/redirects.json $out/redirects.json
|
||||
'';
|
||||
});
|
||||
|
||||
lib =
|
||||
(pkgs.callPackage ./lib {})
|
||||
// {
|
||||
|
|
@ -193,6 +245,32 @@
|
|||
--set PLAYWRIGHT_SKIP_VALIDATE_HOST_REQUIREMENTS true
|
||||
'';
|
||||
}) {};
|
||||
|
||||
# Run "nix run .#update-redirects" to regenerate docs/redirects.json
|
||||
apps.update-redirects = {
|
||||
type = "app";
|
||||
program = "${pkgs.writeShellApplication {
|
||||
name = "update-redirects";
|
||||
runtimeInputs = [ pkgs.nix pkgs.jq ];
|
||||
text = ''
|
||||
echo "=== SelfHostBlocks Redirects Updater ==="
|
||||
echo "Generating fresh ./docs/redirects.json..."
|
||||
|
||||
nix build .#generateRedirects || { echo "Error: Failed to generate redirects" >&2; exit 1; }
|
||||
[[ -f result/redirects.json ]] || { echo "Error: Generated redirects file not found" >&2; exit 1; }
|
||||
|
||||
echo "Generated $(jq 'keys | length' result/redirects.json) redirects"
|
||||
echo
|
||||
read -p "Update docs/redirects.json? This will backup the current file [y/N] " -r response
|
||||
[[ "$response" =~ ^[Yy] ]] || { echo "Aborted - no changes made"; exit 0; }
|
||||
|
||||
[[ -f docs/redirects.json ]] && cp docs/redirects.json docs/redirects.json.backup && echo "Created backup"
|
||||
cp result/redirects.json docs/redirects.json
|
||||
echo " Updated docs/redirects.json"
|
||||
echo "To verify: nix build .#manualHtml"
|
||||
'';
|
||||
}}/bin/update-redirects";
|
||||
};
|
||||
}
|
||||
) // {
|
||||
herculesCI.ciSystems = [ "x86_64-linux" ];
|
||||
|
|
|
|||
Loading…
Reference in a new issue