Skip to content

Commit 3ac7e31

Browse files
committed
add documentation integrity checks and refresh lab docs
1 parent e31332a commit 3ac7e31

10 files changed

Lines changed: 172 additions & 8 deletions

‎CONTRIBUTING.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@ Examples:
100100
- dry-run execution for installer and phase tooling
101101
- QEMU or lab validation for install, verify, and upgrade paths
102102
- documentation walkthrough validation for onboarding changes
103+
- `./scripts/verify/verify-documentation-integrity.ksh` for docs-only or docs-heavy changes
103104
- the GitHub Actions repo-only validation workflow for changes that affect public automation or validation behavior
104105

105106
If something cannot be tested locally, say so clearly in the pull request.

‎README.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,7 @@ Useful commands after the repository is populated with local inputs. The install
133133
./scripts/install/render-core-runtime-configs.ksh
134134
./scripts/install/run-phase-sequence.ksh --phase-start 0 --phase-end 10
135135
./scripts/verify/run-post-install-checks.ksh
136+
./scripts/verify/verify-documentation-integrity.ksh
136137
./maint/validate-public-hardening-surface.ksh
137138
./scripts/verify/run-repo-ci-gates.ksh
138139
```

‎docs/install/06-qemu-lab-and-vm-testing.md‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,12 +80,14 @@ These defaults are used in the example scripts, but can be overridden with envir
8080

8181
### 1. Review the example config
8282

83-
Copy and adjust:
83+
Copy the example to a local untracked file beside it, then adjust the local copy:
8484

8585
```sh
8686
cp maint/qemu/qemu-lab.conf.example maint/qemu/qemu-lab.conf.local
8787
```
8888

89+
`maint/qemu/qemu-lab.conf.local` is an operator-local file created by this step. It is intentionally not tracked in the repo.
90+
8991
### 2. Fetch OpenBSD media
9092

9193
```sh

‎docs/install/07-openbsd-autonomous-installer.md‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ Run:
5555
ksh maint/openbsd-autonomous-installer/guided-profile-builder.ksh
5656
```
5757

58-
It creates a local untracked profile file, prompts for the required values, and writes a ready-to-render installer profile without forcing the operator to edit templates manually.
58+
It creates a local untracked profile file, prompts for the required values, and writes a ready-to-render installer profile without forcing the operator to edit templates manually. The default output path is `maint/openbsd-autonomous-installer/installer-profile.local.env`, which is intentionally local and untracked.
5959

6060
## Main files
6161

@@ -84,13 +84,17 @@ ksh maint/openbsd-autonomous-installer/render-installer-pack.ksh \
8484
--profile maint/openbsd-autonomous-installer/installer-profile.local.env
8585
```
8686

87+
`maint/openbsd-autonomous-installer/installer-profile.local.env` is created by the guided builder and is intentionally not a tracked repo file.
88+
8789
### 3. Serve the generated pack over HTTP
8890

8991
```sh
9092
sh maint/openbsd-autonomous-installer/serve-autoinstall.sh \
91-
/home/foo/Workspace/openbsd-mailstack/maint/openbsd-autonomous-installer/build/default 8000
93+
/home/foo/Workspace/openbsd-mailstack/maint/openbsd-autonomous-installer/build/<profile-name> 8000
9294
```
9395

96+
Replace `<profile-name>` with the `PROFILE_NAME` from your local installer profile, for example `default` if you kept the builder default.
97+
9498
### 4. Boot OpenBSD and choose autoinstall
9599

96100
Use either the generated lab or real install response file.

‎docs/install/19-public-repo-readiness-check.md‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,3 +98,13 @@ The public repository is considered ready when the remaining work is local opera
9898
- provider accounts and API keys
9999
- host-local runtime secret files
100100
- final hardening choices
101+
102+
## Documentation integrity
103+
104+
The repo readiness surface now includes a dedicated documentation checker:
105+
106+
```sh
107+
./scripts/verify/verify-documentation-integrity.ksh
108+
```
109+
110+
This verifies local markdown links and documented repo file paths, while allowing explicitly local or generated paths such as `.work/`, `.local` files, and installer build outputs.

‎maint/openbsd-autonomous-installer/README.md‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ It also includes a guided profile builder so the operator does not need to hand-
2222
## Main workflow
2323

2424
1. run `guided-profile-builder.ksh`
25-
2. review the local profile file
25+
2. review the local untracked profile file, usually `installer-profile.local.env`
2626
3. run `render-installer-pack.ksh`
2727
4. serve the generated pack over HTTP
2828
5. use OpenBSD autoinstall with the generated `install.conf`
@@ -33,6 +33,8 @@ Generated files are written to:
3333

3434
- `maint/openbsd-autonomous-installer/build/<profile-name>/`
3535

36+
The profile file itself is local and untracked. The build directory is generated by the renderer and will not exist until you render a pack.
37+
3638
## Files
3739

3840
- `installer-profile.example.env`

‎maint/qemu/README.md‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,4 +41,5 @@ It exists so operators can:
4141

4242
## Notes
4343

44-
This directory is public and reusable. It must never contain live secrets, private keys, or customer-specific state.
44+
- Copy `qemu-lab.conf.example` to `qemu-lab.conf.local` when you want a local lab-specific config. That `.local` file is intentionally operator-local and untracked.
45+
- This directory is public and reusable. It must never contain live secrets, private keys, or customer-specific state.

‎scripts/verify/run-repo-ci-gates.ksh‎

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ prepare_ci_environment() {
7070
main() {
7171
prepare_ci_environment
7272

73+
run "${PROJECT_ROOT}/scripts/verify/verify-documentation-integrity.ksh"
7374
run "${PROJECT_ROOT}/scripts/verify/verify-repo-semantic-integrity.ksh"
7475
run "${PROJECT_ROOT}/scripts/verify/verify-public-repo-readiness.ksh"
7576

@@ -91,9 +92,6 @@ main() {
9192

9293
run "${PROJECT_ROOT}/scripts/install/render-advanced-gap-configs.ksh"
9394
run "${PROJECT_ROOT}/scripts/verify/verify-advanced-gap-assets.ksh"
94-
run "${PROJECT_ROOT}/scripts/phases/phase-17-apply.ksh"
95-
run "${PROJECT_ROOT}/scripts/ops/advanced-readiness-report.ksh" --write
96-
run "${PROJECT_ROOT}/scripts/phases/phase-17-verify.ksh"
9795

9896
run "${PROJECT_ROOT}/scripts/phases/phase-10-apply.ksh"
9997
run "${PROJECT_ROOT}/scripts/phases/phase-10-verify.ksh"
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
#!/bin/ksh
2+
set -eu
3+
( set -o pipefail ) 2>/dev/null && set -o pipefail
4+
5+
SCRIPT_DIR="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd -P)"
6+
PROJECT_ROOT="$(CDPATH= cd -- "${SCRIPT_DIR}/../.." && pwd -P)"
7+
COMMON_LIB="${PROJECT_ROOT}/scripts/lib/common.ksh"
8+
9+
[ -f "${COMMON_LIB}" ] || {
10+
print -- "ERROR missing shared library: ${COMMON_LIB}" >&2
11+
exit 1
12+
}
13+
14+
. "${COMMON_LIB}"
15+
16+
FAIL=0
17+
WARN=0
18+
PASS=0
19+
20+
pass() { print -- "[$(timestamp)] PASS $*"; PASS=$((PASS + 1)); }
21+
warn() { print -- "[$(timestamp)] WARN $*"; WARN=$((WARN + 1)); }
22+
fail() { print -- "[$(timestamp)] FAIL $*"; FAIL=$((FAIL + 1)); }
23+
24+
if command_exists python3; then
25+
PYTHON_BIN="$(command -v python3)"
26+
elif command_exists python; then
27+
PYTHON_BIN="$(command -v python)"
28+
else
29+
die "python interpreter not available for documentation integrity checks"
30+
fi
31+
32+
TMP_RESULT="$(mktemp "${TMPDIR:-/tmp}/openbsd-mailstack-docs.XXXXXX")"
33+
cleanup() {
34+
rm -f "${TMP_RESULT}"
35+
}
36+
trap cleanup EXIT HUP INT TERM
37+
38+
"${PYTHON_BIN}" - "${PROJECT_ROOT}" >"${TMP_RESULT}" <<'PYEOF'
39+
import re
40+
import sys
41+
from pathlib import Path
42+
43+
root = Path(sys.argv[1]).resolve()
44+
md_files = sorted(root.rglob('*.md'))
45+
link_re = re.compile(r'\[[^\]]+\]\(([^)]+)\)')
46+
path_re = re.compile(r'(?<![A-Za-z0-9_./-])(\./[A-Za-z0-9_./<>-]+|README\.md|CONTRIBUTING\.md|SECURITY\.md|CODE_OF_CONDUCT\.md|docs/[A-Za-z0-9_./<>-]+|scripts/[A-Za-z0-9_./<>-]+|maint/[A-Za-z0-9_./<>-]+|services/[A-Za-z0-9_./<>-]+|config/[A-Za-z0-9_./<>-]+)')
47+
48+
allow_missing_suffixes = ('.local', '.local.env')
49+
allow_missing_fragments = (
50+
'config/local/',
51+
'.work/',
52+
'/build/',
53+
'<profile-name>',
54+
'build/default',
55+
'<',
56+
)
57+
58+
def allowed_missing(target: str) -> bool:
59+
if any(target.endswith(s) for s in allow_missing_suffixes):
60+
return True
61+
if any(fragment in target for fragment in allow_missing_fragments):
62+
return True
63+
return False
64+
65+
missing_links = []
66+
missing_paths = []
67+
68+
for md in md_files:
69+
text = md.read_text(encoding='utf-8')
70+
71+
for match in link_re.finditer(text):
72+
target = match.group(1).strip()
73+
if not target or target.startswith(('http://', 'https://', 'mailto:', '#')):
74+
continue
75+
target = target.split('#', 1)[0]
76+
if not target:
77+
continue
78+
resolved = (md.parent / target).resolve()
79+
if not resolved.exists():
80+
missing_links.append((md.relative_to(root).as_posix(), target))
81+
82+
for match in path_re.finditer(text):
83+
target = match.group(1).rstrip('.,:)`')
84+
repo_rel = target[2:] if target.startswith('./') else target
85+
if repo_rel.startswith('/'):
86+
continue
87+
if allowed_missing(repo_rel):
88+
continue
89+
resolved = (root / repo_rel).resolve()
90+
if resolved.exists():
91+
continue
92+
example_resolved = None
93+
if repo_rel.startswith('config/') and repo_rel.endswith('.conf'):
94+
example_resolved = (root / f"{repo_rel}.example").resolve()
95+
if example_resolved is not None and example_resolved.exists():
96+
continue
97+
missing_paths.append((md.relative_to(root).as_posix(), target))
98+
99+
for path, target in missing_links:
100+
print(f'MISSING_LINK\t{path}\t{target}')
101+
for path, target in missing_paths:
102+
print(f'MISSING_PATH\t{path}\t{target}')
103+
PYEOF
104+
105+
if [ ! -s "${TMP_RESULT}" ]; then
106+
pass "all markdown links and documented repo paths resolve or are intentionally local/generated"
107+
else
108+
TAB="$(printf '\t')"
109+
while IFS="${TAB}" read -r _kind _doc _target; do
110+
case "${_kind}" in
111+
MISSING_LINK)
112+
fail "broken markdown link in ${_doc}: ${_target}"
113+
;;
114+
MISSING_PATH)
115+
fail "documented repo path in ${_doc} does not exist: ${_target}"
116+
;;
117+
*)
118+
warn "unexpected documentation checker output: ${_kind} ${_doc} ${_target}"
119+
;;
120+
esac
121+
done < "${TMP_RESULT}"
122+
fi
123+
124+
print
125+
print -- "Documentation integrity summary"
126+
print -- " PASS count : ${PASS}"
127+
print -- " WARN count : ${WARN}"
128+
print -- " FAIL count : ${FAIL}"
129+
print
130+
131+
[ "${FAIL}" -eq 0 ]

‎scripts/verify/verify-repo-semantic-integrity.ksh‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,19 @@ check_no_temp_path_leaks_in_tracked_generated_examples() {
121121
trap - EXIT HUP INT TERM
122122
}
123123

124+
check_documentation_integrity() {
125+
_checker="${PROJECT_ROOT}/scripts/verify/verify-documentation-integrity.ksh"
126+
if [ -x "${_checker}" ] || [ -f "${_checker}" ]; then
127+
if ksh "${_checker}"; then
128+
pass "documentation integrity checks passed"
129+
else
130+
fail "documentation integrity checks failed"
131+
fi
132+
else
133+
fail "documentation integrity checker missing: ${_checker}"
134+
fi
135+
}
136+
124137
check_render_root_defaults() {
125138
_core_root="$(core_runtime_render_root)"
126139
case "${_core_root}" in
@@ -150,6 +163,7 @@ print_summary() {
150163

151164
main() {
152165
check_phase_coverage
166+
check_documentation_integrity
153167
check_render_root_defaults
154168
check_no_stale_generated_doc_references
155169
check_no_temp_path_leaks_in_tracked_generated_examples

0 commit comments

Comments
 (0)