-
Notifications
You must be signed in to change notification settings - Fork 0
449 lines (416 loc) · 17.5 KB
/
Copy pathapi-docs.yml
File metadata and controls
449 lines (416 loc) · 17.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
name: API docs (C++)
# Auto-generates per-package API reference from Doxygen XML for this
# vcpkg-published C++ workspace and opens a PR against
# resq-software/docs. Doxygen extracts class/function declarations
# from header comments, and moxygen converts the resulting XML to
# Markdown that Mintlify can serve.
on:
workflow_dispatch:
inputs:
ref:
description: Tag or branch to document
required: false
type: string
push:
tags:
- 'v*'
- '*-v*'
permissions:
contents: read
concurrency:
# Tag pushes can trigger workflow_dispatch in flight; cancel in
# progress so the docs PR always reflects the most recent ref.
group: api-docs-${{ github.ref }}
cancel-in-progress: true
jobs:
generate:
runs-on: ubuntu-latest
timeout-minutes: 15
env:
OUTPUT_DIR: generated-docs
DOCS_TARGET: sdks/cpp/api
# Public packages to document. Each must have a top-level
# `include/` directory whose .hpp files carry Doxygen comments.
# Tests, internal helpers, and CMake module files are skipped
# by the include-only Doxygen INPUT below.
PUBLIC_PACKAGES: >-
resq-common
steps:
- name: Resolve ref metadata
# Single source of truth for the ref this run documents.
#
# The ref is routed through env: instead of being inlined via
# ${{ }}. Inlining at template-expansion time would interpolate
# the raw string into the shell literal, so a tag name with a
# single quote (Git allows it) could break out of the quoted
# context. Env indirection keeps user-controlled data on the
# variable side of the shell parser, where it cannot escape.
env:
REF_RAW: ${{ inputs.ref || github.ref_name }}
run: |
raw="$REF_RAW"
raw="${raw#refs/tags/}"
raw="${raw#refs/heads/}"
slug="${raw//\//-}"
slug="${slug//\@/-}"
{
echo "DOCS_REF_NAME=$raw"
echo "DOCS_REF_SLUG=$slug"
} >> "$GITHUB_ENV"
- name: Checkout source repo
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
ref: ${{ inputs.ref || github.ref }}
persist-credentials: false
- name: Install Doxygen + moxygen
# Doxygen ships in apt and is the source-of-truth XML
# extractor; moxygen is an npm package that converts the
# XML to one markdown file per class/group. Pinning npm
# globally is acceptable here because the runner is
# disposable.
run: |
sudo apt-get update -qq
sudo apt-get install -yqq doxygen graphviz
npm install --location=global moxygen@2.1.7
- name: Run Doxygen + moxygen per package
# Loop over each public package, run Doxygen against just its
# include/ directory (so we don't pull in vendored deps), and
# convert the resulting XML to one .md per class/namespace.
# GENERATE_HTML=NO keeps the artifact size sane.
run: |
set -euo pipefail
mkdir -p "$OUTPUT_DIR"
rm -rf "${OUTPUT_DIR:?}"/*
generated=0
for pkg in $PUBLIC_PACKAGES; do
pkg_dir="packages/$pkg"
inc_dir="$pkg_dir/include"
if [ ! -d "$inc_dir" ]; then
echo "::warning ::skipping $pkg (no include/ dir at $inc_dir)"
continue
fi
out_dir="$OUTPUT_DIR/$pkg"
mkdir -p "$out_dir"
xml_dir="$(mktemp -d)"
cat <<DOXY > "$xml_dir/Doxyfile"
PROJECT_NAME = "$pkg"
INPUT = $inc_dir
RECURSIVE = YES
EXTRACT_ALL = YES
EXTRACT_PRIVATE = NO
EXTRACT_STATIC = YES
GENERATE_XML = YES
XML_OUTPUT = $xml_dir/xml
GENERATE_HTML = NO
GENERATE_LATEX = NO
QUIET = YES
WARN_IF_UNDOCUMENTED = NO
WARN_IF_DOC_ERROR = NO
JAVADOC_AUTOBRIEF = YES
MARKDOWN_SUPPORT = YES
AUTOLINK_SUPPORT = YES
BUILTIN_STL_SUPPORT = YES
DOXY
doxygen "$xml_dir/Doxyfile"
# moxygen takes the XML dir and writes one .md file per
# class via --classes; the output template uses %s as
# the class name.
moxygen --classes \
--output "$out_dir/%s.md" \
"$xml_dir/xml/" || {
echo "::warning ::moxygen failed for $pkg; emitting stub"
{
echo "# $pkg"
echo
echo "_Doxygen extraction failed for this package._"
} > "$out_dir/README.md"
}
rm -rf "$xml_dir"
generated=$((generated + 1))
done
if [ "$generated" -eq 0 ]; then
echo "::error ::no packages generated; aborting empty doc PR"
exit 1
fi
- name: Prefix bare-filename intra-page links with ./
# moxygen emits same-directory cross-references as bare
# filenames, e.g. `[GeoPoint](resq-common-geo-GeoPoint.md)`.
# Mintlify accepts `(../foo/bar.md)` but rejects bare
# filenames as broken. Prefix any bare-filename `.md` link
# with `./` so the resolver keeps it as a relative path.
# External URLs (https://...) and already-relative paths
# (./, ../) are left alone because the leading-char class
# `[A-Za-z0-9]` excludes both. Same approach as the
# TypeScript template.
working-directory: ${{ env.OUTPUT_DIR }}
run: |
find . -type f -name "*.md" -print0 | while IFS= read -r -d '' f; do
sed -E -i \
's|\]\(([A-Za-z0-9][^)/]*\.md(#[^)]*)?)\)|](./\1)|g' \
"$f"
done
- name: Strip noise annotations
# moxygen tags every header-defined method with `inline` in
# the class summary table. It's a compilation directive, not
# API surface, and it appears on practically every row of
# every class — pure noise for a reader. `const`, `static`,
# `noexcept`, `virtual` are kept because they describe
# observable behavior.
working-directory: ${{ env.OUTPUT_DIR }}
run: |
find . -type f -name "*.md" -print0 | while IFS= read -r -d '' f; do
# shellcheck disable=SC2016
# Backticks in the sed pattern are literal characters
# matching the markdown code-span delimiters around
# `inline` in moxygen output, not shell command
# substitution. Single quotes are intentional.
sed -i -E 's/ `inline`//g' "$f"
done
- name: Strip moxygen breadcrumbs
# moxygen prepends a breadcrumb on the first line of each
# page like `[**parent**](parent.md)` linking to .md files
# we don't ship. Strip just the leading breadcrumb (line 1
# only) rather than every link line in the body, which would
# delete legitimate cross-references later in the document.
working-directory: ${{ env.OUTPUT_DIR }}
run: |
find . -type f -name "*.md" -print0 | while IFS= read -r -d '' f; do
python3 - "$f" <<'PY'
import pathlib, re, sys
p = pathlib.Path(sys.argv[1])
lines = p.read_text(encoding="utf-8").splitlines(keepends=True)
# Strip leading breadcrumb-style lines (single markdown link
# to a .md target with optional bold text) plus any blank
# lines that follow it. Stop at the first non-breadcrumb,
# non-blank line.
breadcrumb = re.compile(r"^\[[^\]]+\]\([^)]*\.md\)\s*$")
i = 0
while i < len(lines):
s = lines[i].strip()
if not s or breadcrumb.match(s):
i += 1
continue
break
if i:
p.write_text("".join(lines[i:]), encoding="utf-8")
PY
done
- name: Escape MDX-special characters outside code regions
# Mintlify parses .md as MDX. Literal `{ ... }` in prose is
# interpreted as a JSX expression; literal `<X>` (e.g.
# `Result<T>`, `std::vector<int>`) is parsed as a JSX
# component reference, which fails when the "component" isn't
# defined. Backtick-span detection (same walker as the Python
# template — see api-docs.python.yml) keeps code spans
# verbatim.
#
# Unlike the Python template, this one is more aggressive
# about `<`: it escapes `<` followed by anything except `!`
# (HTML comment) or `/` (closing tag). The Python template
# passes through `<X` where X is a letter because
# pydoc-markdown emits `<a id="...">` HTML anchors that need
# to render. Doxygen-derived markdown can still legitimately
# contain template syntax in prose (e.g. function summaries),
# so escape-all is the safer rule here.
working-directory: ${{ env.OUTPUT_DIR }}
run: |
find . -type f -name "*.md" -print0 | while IFS= read -r -d '' f; do
awk '
BEGIN { in_fence = 0 }
/^```/ { in_fence = !in_fence; print; next }
{
if (in_fence) { print; next }
s = $0
out = ""
n = length(s)
i = 1
while (i <= n) {
c = substr(s, i, 1)
if (c == "`") {
runlen = 0
while (i + runlen <= n && substr(s, i + runlen, 1) == "`") runlen++
j = i + runlen
close_at = 0
while (j <= n) {
if (substr(s, j, 1) == "`") {
k = 0
while (j + k <= n && substr(s, j + k, 1) == "`") k++
if (k == runlen) { close_at = j; break }
j += k
} else {
j++
}
}
if (close_at > 0) {
end = close_at + runlen - 1
out = out substr(s, i, end - i + 1)
i = end + 1
} else {
out = out substr(s, i, runlen)
i += runlen
}
} else if (c == "{") {
out = out "{"; i++
} else if (c == "}") {
out = out "}"; i++
} else if (c == "<") {
next_c = (i + 1 <= n) ? substr(s, i + 1, 1) : ""
# Pass through HTML comments (<!--) and closing
# tags (</foo>); escape everything else,
# including `<X>` template syntax.
if (next_c == "!" || next_c == "/") {
out = out c; i++
} else {
out = out "<"; i++
}
} else {
out = out c; i++
}
}
print out
}
' "$f" > "$f.tmp" && mv "$f.tmp" "$f"
done
- name: Write top-level index
# Stitch a small README.mdx at the top of the api/ folder
# listing the public packages with their vcpkg.json versions.
# Emit .mdx because Mintlify's docs.json nav resolver only
# matches .mdx for page ids.
run: |
python3 - <<'PY' > "$OUTPUT_DIR/README.mdx"
import json
import os
import pathlib
ref_name = os.environ.get("DOCS_REF_NAME", "main")
repo = os.environ.get("GITHUB_REPOSITORY", "")
out = pathlib.Path(os.environ["OUTPUT_DIR"])
packages = (os.environ.get("PUBLIC_PACKAGES") or "").split()
def read_version(pkg: str) -> str:
vcpkg_json = pathlib.Path("packages") / pkg / "vcpkg.json"
if not vcpkg_json.exists():
return "unknown"
try:
data = json.loads(vcpkg_json.read_text(encoding="utf-8"))
except json.JSONDecodeError:
return "unknown"
# vcpkg.json uses one of: version, version-string,
# version-semver, version-date.
for key in ("version", "version-semver",
"version-string", "version-date"):
if key in data:
return str(data[key])
return "unknown"
print("# ResQ C++ SDK")
print()
print(
f"Auto-generated reference for "
f"[`{repo}`](https://github.com/{repo}) "
f"at ref `{ref_name}`."
)
print()
print("## Packages")
print()
for pkg in sorted(packages):
if not (out / pkg).is_dir():
continue
print(f"- `{pkg}` — `v{read_version(pkg)}`")
PY
- name: Build pages index
# Flat JSON array of generated Markdown paths (without
# extension) so the docs repo can splice them into docs.json.
working-directory: ${{ env.OUTPUT_DIR }}
run: |
find . -name "*.md" -type f \
| sed "s|^\./||; s|\.md\$||" \
| sort > _pages.txt
jq -R -s 'split("\n") | map(select(length > 0))' _pages.txt > _pages.json
rm _pages.txt
- name: Checkout docs repo
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
repository: resq-software/docs
path: docs-checkout
token: ${{ secrets.DOCS_REPO_PR_TOKEN }}
persist-credentials: false
- name: Sync generated Markdown into docs checkout
run: |
target="docs-checkout/${DOCS_TARGET}"
mkdir -p "$target"
rm -rf "${target:?}"/*
cp -R "${OUTPUT_DIR}/." "$target/"
- name: Splice _pages.json into docs.json nav
# Mintlify only routes pages registered in docs.json. Build a
# hierarchical group structure so each package becomes its
# own collapsible group, mirroring the Python/.NET layout.
working-directory: docs-checkout
run: |
python3 - <<'PYINNER'
import json
import pathlib
PREFIX = "sdks/cpp/api"
LANG_LABEL = "C++"
docs_path = pathlib.Path("docs.json")
docs = json.loads(docs_path.read_text())
pages_path = pathlib.Path(PREFIX) / "_pages.json"
if not pages_path.exists():
raise SystemExit(f"missing {pages_path}")
raw = json.loads(pages_path.read_text())
tree: dict = {}
def insert(node, parts, full_id):
if len(parts) == 1:
node.setdefault("_files", []).append((parts[0], full_id))
return
head, *rest = parts
insert(node.setdefault("_dirs", {}).setdefault(head, {}), rest, full_id)
for p in raw:
if p == "README":
continue
full_id = f"{PREFIX}/{p}"
insert(tree, p.split("/"), full_id)
def to_mintlify(node, group_name):
pages = []
for _, full_id in sorted(node.get("_files", [])):
pages.append(full_id)
for dname, sub in sorted(node.get("_dirs", {}).items()):
pages.append(to_mintlify(sub, dname))
return pages if group_name is None else {"group": group_name, "pages": pages}
new_pages = [f"{PREFIX}/README"] + to_mintlify(tree, None)
en = next(l for l in docs["navigation"]["languages"] if l["language"] == "en")
sdks_tab = next(t for t in en["tabs"] if t["tab"] == "SDKs")
gen_group = next(g for g in sdks_tab["groups"] if g["group"] == "Generated Package References")
for sub in gen_group["pages"]:
if isinstance(sub, dict) and sub.get("group") == LANG_LABEL:
sub["pages"] = new_pages
break
else:
gen_group["pages"].append({"group": LANG_LABEL, "pages": new_pages})
docs_path.write_text(json.dumps(docs, indent=2, ensure_ascii=False))
print(f"Updated {LANG_LABEL} sub-group with {len(raw)} pages")
PYINNER
- name: Open PR in docs repo
uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1
with:
path: docs-checkout
token: ${{ secrets.DOCS_REPO_PR_TOKEN }}
author: 'resq-sw <engineer@resq.software>'
committer: 'resq-sw <engineer@resq.software>'
commit-message: |
docs(cpp): sync API reference for ${{ env.DOCS_REF_NAME }}
title: 'docs(cpp): API reference ${{ env.DOCS_REF_NAME }}'
body: |
Auto-generated by `${{ github.workflow }}` in
`${{ github.repository }}` for ref `${{ env.DOCS_REF_NAME }}`
(run: ${{ github.run_id }}).
Regenerated files under `sdks/cpp/api/`. Doxygen extracted
declarations from each package's `include/` headers; moxygen
converted them to Markdown.
branch: auto/cpp-api-${{ env.DOCS_REF_SLUG }}
base: main
delete-branch: true
add-paths: |
sdks/cpp/api/**
docs.json
labels: |
automated
docs:api-ref
language:cpp