1
0

check-doc-endpoints.sh 4.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123
  1. #!/usr/bin/env bash
  2. # Doc accuracy guard.
  3. #
  4. # Greps `doc/*.md` and the README for `/api/v1/<...>` paths and
  5. # compares against the OpenAPI document. The matcher templatizes
  6. # literal IDs, IPs, and category slugs so `/api/v1/admin/ips/{ip}`
  7. # in the spec covers `/api/v1/admin/ips/203.0.113.42` in prose.
  8. #
  9. # Also ensures every token kind and role name appears at least once
  10. # in the doc set — lazy proxy for "the docs cover the auth model".
  11. #
  12. # Allowlisted paths (declared in `KNOWN_OK`) are accepted without a
  13. # spec entry — for things like `/api/v1/openapi.yaml` itself, which
  14. # is a public asset, not a typed API endpoint.
  15. #
  16. # Run from the repo root:
  17. # ./scripts/check-doc-endpoints.sh
  18. set -euo pipefail
  19. REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
  20. cd "$REPO_ROOT"
  21. SPEC="api/public/openapi.yaml"
  22. [ -s "$SPEC" ] || { echo "[fail] $SPEC missing or empty — run composer openapi:build" >&2; exit 1; }
  23. DOCS=( README.md doc/architecture.md doc/api-overview.md doc/auth-flows.md doc/frontend-development.md doc/api-reference.md )
  24. # Paths declared in docs but intentionally outside the spec.
  25. KNOWN_OK=$(cat <<'EOF'
  26. /api/v1/openapi.yaml
  27. /api/v1/auth/oauth
  28. /api/v1/auth/oauth/start
  29. /api/v1/auth/oauth/callback
  30. /api/v1/auth/oauth/refresh
  31. /api/v1/auth/oauth/revoke
  32. EOF
  33. )
  34. # Templatize: replace IPv4 octets, IPv6 segments, and bare integers
  35. # in path tail with the OpenAPI param placeholders {ip} / {id} /
  36. # {name}.
  37. templatize() {
  38. sed -E '
  39. # IPv4-shaped path tail
  40. s#/[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+(/|$)#/{ip}\1#g
  41. # IPv6 (rough): hexes with at least one colon
  42. s#/[0-9a-fA-F:]*:[0-9a-fA-F:]+(/|$)#/{ip}\1#g
  43. # Trailing bare integer → {id}
  44. s#/[0-9]+(/|$)#/{id}\1#g
  45. # /jobs/trigger/<name> → /jobs/trigger/{name}
  46. s#/jobs/trigger/[A-Za-z0-9_-]+#/jobs/trigger/{name}#g
  47. # /admin/categories/{slug} after templatize: leave as is
  48. '
  49. }
  50. # ---- Pull doc paths ----
  51. # Match `/api/v1/...` followed by allowed chars; bound at quote /
  52. # whitespace / closing punct that markdown wraps them in. Strip
  53. # query strings and trailing punctuation.
  54. docs_paths=$(grep -hoE '/api/v1/[A-Za-z0-9_/{}.:-]+' "${DOCS[@]}" 2>/dev/null \
  55. | sed -E 's/[?#].*$//' \
  56. | sed -E 's/[.,;:`)]+$//' \
  57. | sed -E 's#/+$##' \
  58. | grep -vE '^/api/v1$' \
  59. | grep -vE '^/api/v1/(admin|auth)$' \
  60. | sort -u \
  61. | templatize \
  62. | sort -u)
  63. # ---- Pull spec paths ----
  64. # OpenAPI YAML: every line at exactly two-space indent under `paths:`
  65. # whose key starts with `/api/v1/` is an endpoint. Quoted keys are
  66. # stripped.
  67. spec_paths=$(awk '
  68. /^paths:/ {p=1; next}
  69. p && /^[A-Za-z]/ {p=0}
  70. p && /^ [^ ]/ {
  71. line=$0
  72. sub(/^ /, "", line)
  73. sub(/:$/, "", line)
  74. gsub(/^[ ]+|[ ]+$/, "", line)
  75. gsub(/^['"'"'"]/, "", line)
  76. gsub(/['"'"'"]$/, "", line)
  77. if (line ~ /^\/api\/v1/) print line
  78. }
  79. ' "$SPEC" | sort -u)
  80. # ---- Compare ----
  81. missing=$(
  82. while IFS= read -r p; do
  83. [ -z "$p" ] && continue
  84. # Allowlisted?
  85. if printf '%s\n' "$KNOWN_OK" | grep -qFx "$p"; then
  86. continue
  87. fi
  88. # Direct match?
  89. if printf '%s\n' "$spec_paths" | grep -qFx "$p"; then
  90. continue
  91. fi
  92. # Match against spec with the spec also templatized? No — spec
  93. # already uses {ip}/{id} placeholders; the templatize sed
  94. # above on $docs_paths already aligns them.
  95. echo "$p"
  96. done <<<"$docs_paths"
  97. )
  98. if [ -n "$missing" ]; then
  99. echo "[fail] doc-mentioned paths not present in $SPEC:"
  100. echo "$missing" | sed 's/^/ - /'
  101. exit 1
  102. fi
  103. # ---- Token-kind / role spelling ----
  104. for word in reporter consumer admin service viewer operator; do
  105. if ! grep -qE "\\b$word\\b" "${DOCS[@]}"; then
  106. echo "[fail] doc set never mentions '$word'"
  107. exit 1
  108. fi
  109. done
  110. docs_count=$(echo "$docs_paths" | grep -c . || true)
  111. spec_count=$(echo "$spec_paths" | grep -c . || true)
  112. echo "[ok] docs reference $docs_count /api/v1/* paths; spec declares $spec_count"