Repository navigation
523 lines (482 loc) · 24.7 KB
/
Copy pathdocs.yml
File metadata and controls
523 lines (482 loc) · 24.7 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
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
#
# Copyright (c) 2026 Steve Gerbino
# Copyright (c) 2026 Michael Vandeberg
#
# Distributed under the Boost Software License, Version 1.0. (See accompanying
# file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
#
# Official repository: https://github.com/cppalliance/corosio/
#
name: Documentation
on:
push:
branches:
- master
- develop*
paths:
- 'doc/**'
- 'include/**'
- 'test/doc/reference/**'
- '*.adoc'
- 'README.adoc'
- '.github/workflows/docs.yml'
pull_request:
paths:
- 'doc/**'
- 'include/**'
- 'test/doc/reference/**'
- '*.adoc'
- 'README.adoc'
- '.github/workflows/docs.yml'
# Manual trigger, for authoring a replacement doc/lint/baseline.json in the CI
# environment — see the "Baseline reseed" steps at the end of the antora job.
# A reseed REWRITES the reference point of the doc-quality gate, so it must never
# happen on push or pull_request: an automatic reseed would absorb real
# regressions into the grandfathered backlog, which is precisely what the gate
# exists to prevent. workflow_dispatch is the only trigger that reaches those
# steps (they are additionally guarded on github.event_name), and they only
# upload an artifact for a human to review and commit.
workflow_dispatch:
inputs:
allow_emptied:
description: >-
Space-separated gated checks whose backlog is genuinely closed.
baseline-diff.mjs refuses a candidate where a gated check drops to
zero, because a crashed check looks identical -- this is how you say
the zero is real. Verify first, then name the check here so the
decision is recorded in the run log.
required: false
default: ''
jobs:
antora:
name: Antora Docs
runs-on: 'ubuntu-latest'
defaults:
run:
shell: bash
steps:
# asciidoctor here is the Ruby CLI that Vale 3.x shells out to when linting
# .adoc files (its lintAdoc scope). It is NOT the same as the JS
# @asciidoctor/core that build_antora.sh pulls in via npm — that provides no
# `asciidoctor` binary on PATH. Without the Ruby CLI, `vale modules` and
# baseline.mjs's vale_adoc error with "asciidoctor not found" — the check is
# marked skipped — and because vale_adoc AND vale_docstrings are both GATED
# checks, the gate's gated-skip path fails the job on missing infra rather
# than passing a run that measured nothing. The docstring corpus needs it
# too: the extracted files are .adoc, so a missing Ruby CLI skips that slice
# as well. Installed here, in the first step, so it is on PATH for the
# Antora build and every lint step after it.
- name: Install packages
uses: alandefreitas/cpp-actions/package-install@v1.9.0
with:
apt-get: git cmake asciidoctor
- name: Clone Boost.Corosio
uses: actions/checkout@v4
with:
path: corosio-root
- name: Resolve Capy branch
id: capy-ref
uses: ./corosio-root/.github/actions/resolve-capy
- name: Clone Capy
uses: actions/checkout@v4
with:
repository: ${{ steps.capy-ref.outputs.repo }}
ref: ${{ steps.capy-ref.outputs.ref }}
path: capy-root
- name: Clone Boost
uses: alandefreitas/cpp-actions/boost-clone@v1.9.0
id: boost-clone
with:
branch: ${{ (github.ref_name == 'master' && github.ref_name) || 'develop' }}
boost-dir: boost-source
modules-exclude-paths: ''
scan-modules-dir: corosio-root
scan-modules-ignore: corosio,capy
- name: Patch Boost
id: patch
shell: bash
run: |
set -xe
pwd
ls
ls -lah boost-source
# Identify boost module being tested
module=${GITHUB_REPOSITORY#*/}
echo "module=$module" >> $GITHUB_OUTPUT
# Identify GitHub workspace root
workspace_root=$(echo "$GITHUB_WORKSPACE" | sed 's/\\/\//g')
echo -E "workspace_root=$workspace_root" >> $GITHUB_OUTPUT
# Remove module from boost-source
rm -r "boost-source/libs/$module" || true
rm -r "boost-source/libs/capy" || true
# boost-clone uses sparse checkout which excludes CMakeLists.txt files
# Disable sparse checkout to get full source trees for add_subdirectory in cmake_test
cd boost-source
if git sparse-checkout list > /dev/null 2>&1; then
echo "Disabling sparse checkout..."
git sparse-checkout disable
echo "Fetching any missing objects..."
git fetch origin --no-tags
git checkout
fi
echo "Verifying libs/mp11/CMakeLists.txt exists..."
ls -la libs/mp11/CMakeLists.txt || echo "WARNING: libs/mp11/CMakeLists.txt not found!"
cd ..
# Copy cached boost-source to an isolated boost-root
cp -rL boost-source boost-root
# Set boost-root output
cd boost-root
boost_root="$(pwd)"
boost_root=$(echo "$boost_root" | sed 's/\\/\//g')
echo -E "boost_root=$boost_root" >> $GITHUB_OUTPUT
# Patch boost-root with workspace module
cp -r "$workspace_root"/corosio-root "libs/$module"
# Patch boost-root with capy dependency
cp -r "$workspace_root"/capy-root "libs/capy"
- uses: actions/setup-node@v4
with:
node-version: 18
- name: Build Antora Docs
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
git config --global --add safe.directory "$(pwd)"
BOOST_SRC_DIR="$(pwd)/boost-root"
export BOOST_SRC_DIR
cd boost-root/libs/corosio
cd doc
# Tee'd purely to keep the build log readable in the step output;
# Antora exits zero even on failure, which is why the checks below
# exist.
set -o pipefail
bash ./build_antora.sh 2>&1 | tee "$RUNNER_TEMP/antora.log"
# Antora returns zero even if it fails, so we check if the site directory exists
if [ ! -d "build/site" ]; then
echo "Antora build failed"
exit 1
fi
# BLOCKING, but deliberately not a count. A MrDocs without the
# extension installed ignores the script entirely and renders the
# reference with no examples while still reporting success, so something
# has to notice. Checking that one known example reached the HTML catches
# that without asking anyone to maintain a number: this example exists
# only in test/doc/reference/socket_option__no_delay.record.cpp, never in
# a header.
- name: Doc-quality - reference examples were injected (BLOCKING)
run: |
set -euo pipefail
site=boost-root/libs/corosio/doc/build/site
if ! grep -rqF disable_nagle_on_a_connected_socket "$site/corosio/reference"; then
echo "No injected example found in the rendered reference." >&2
echo "The reference-snippets transform did not run, or its output" >&2
echo "did not reach the HTML. doc/build_antora.sh installs the" >&2
echo "extension into a MrDocs and exports MRDOCS_ROOT; check that it" >&2
echo "did, and that the Antora reference extension accepted it -- it" >&2
echo "logs 'Using local MrDocs' at debug level, and setting a" >&2
echo "'version' in doc/local-playbook.yml makes it reject a local" >&2
echo "install and silently download its own instead." >&2
exit 1
fi
echo "the rendered reference carries its injected examples"
# --- Documentation quality (doc/STYLE_GUIDE.md Part F) -------------------
# The enforcement tiers live in doc/lint/; doc/lint/README.md is the operator
# guide and carries this repository's F4 bite-test log.
#
# POSTURE, and it is deliberate: the gate step below REPORTS without --strict,
# so it annotates new findings on the diff but does not fail the job, and
# selftest.mjs runs continue-on-error. Both flips — adding --strict here and
# promoting selftest.mjs to blocking — are the exit criteria of the final
# remediation phase in doc/design/style-guide-compliance.md section 6. Until
# then a new violation is reported but lands, which the per-phase reseed
# cadence in that document is what keeps bounded.
#
# The accuracy gate for .adoc example code (B2/B3 correctness) is separate and
# already hard: the doc snippet/program targets built by test/doc, run from
# ci.yml, not this job.
- name: "Lint: install Vale"
if: always()
continue-on-error: true
run: |
mkdir -p "$RUNNER_TEMP/vale-bin"
curl -sSL https://github.com/errata-ai/vale/releases/download/v3.15.1/vale_3.15.1_Linux_64-bit.tar.gz \
| tar -xz -C "$RUNNER_TEMP/vale-bin" vale
echo "$RUNNER_TEMP/vale-bin" >> "$GITHUB_PATH"
echo "$(pwd)/boost-root/libs/corosio/doc/node_modules/.bin" >> "$GITHUB_PATH"
- name: "Lint: sync Vale styles"
if: always()
continue-on-error: true
working-directory: boost-root/libs/corosio/doc
run: vale sync
# Vale's exit codes: 0 = no alerts, 1 = alerts found, 2 = fatal (e.g. no
# asciidoctor on PATH). Only 2 is a tooling failure worth surfacing here; 1
# is the backlog, which the gate step below is what judges. So map 1 to
# success and let anything else through.
- name: "Lint: Vale over pages"
if: always()
continue-on-error: true
working-directory: boost-root/libs/corosio/doc
run: |
vale modules; s=$?
if [ "$s" = "1" ]; then exit 0; else exit "$s"; fi
- name: "Lint: Vale over docstrings"
if: always()
continue-on-error: true
working-directory: boost-root/libs/corosio/doc
run: |
node lint/extract-docstrings.mjs
vale lint/.docstrings; s=$?
if [ "$s" = "1" ]; then exit 0; else exit "$s"; fi
- name: "Lint: structure (doc-lint.mjs)"
if: always()
continue-on-error: true
working-directory: boost-root/libs/corosio/doc
run: node lint/doc-lint.mjs
- name: "Lint: sentence length (C2)"
if: always()
continue-on-error: true
working-directory: boost-root/libs/corosio/doc
run: node lint/sentence-length.mjs
# BLOCKING, and deliberately outside the baseline/comparator posture above: a
# tagged include that no longer resolves renders as an EMPTY code block on the
# page, so the reader silently loses the example. There is no backlog to
# grandfather here — every tagged include resolved at the port — so this one
# stays a hard gate.
- name: "Lint: include tags resolve (BLOCKING)"
if: always()
continue-on-error: false
working-directory: boost-root/libs/corosio/doc
run: node lint/check-include-tags.mjs
- name: "Lint: reference warnings (MrDocs)"
if: always()
continue-on-error: true
working-directory: boost-root/libs/corosio/doc
run: node lint/mrdocs-warnings.mjs
# selftest.mjs mutates the linters and asserts they notice. It is the only
# standing guard between a silent linter regression and a green run — several
# of these checks (A1's value whitelist, A7's numeral match, B2's block walk,
# the role=output exemption boundary) were fail-open at some point in Capy and
# were only ever found by planting a violation. SHAPE in particular is
# advisory and reads 0, so a broken looksLikeCode() is invisible everywhere
# else. BLOCKING as of the final remediation phase: it has no baseline and no
# environment dependence — it plants violations in throwaway fixtures and
# asserts the linters notice — so a red run here means a linter regressed, which
# is exactly the failure nothing else can see.
- name: "Lint: linter self-test (BLOCKING)"
if: always()
continue-on-error: false
working-directory: boost-root/libs/corosio/doc
run: node lint/selftest.mjs
# The remaining backlog, as warnings: every finding present RIGHT NOW that
# baseline.json grandfathers. Not the baseline file's contents — the baseline
# is only reseeded when something is ADDED, so it accumulates dead clauses for
# findings already fixed. This prints the live intersection, which is the
# actual worklist.
- name: "Lint: remaining backlog"
if: always()
continue-on-error: true
working-directory: boost-root/libs/corosio/doc
run: node lint/check-no-new-violations.mjs --show-baseline
# The gate: reports any NEW A1/A6/B2/D2/ANCHOR violation, any NEW MrDocs
# reference-surface warning, any NEW C2 sentence-length violation in the hard
# slice, and any NEW C4/C9/C10 wording violation on EITHER surface.
#
# THE TWO GATE-SPEC SHAPES DIFFER, AND THE DIFFERENCE IS LOAD-BEARING.
# check-no-new-violations.mjs tests each regex against the WHOLE fingerprint.
# * Vale fingerprints are `file:#N:Check.Name` — check name at the TAIL. So the
# Vale specs tail-anchor with `$` and MUST NOT carry a leading `^`. An
# `^`-anchored Vale spec matches nothing and reports `gated: true,
# gatedNew: 0` — a gate that says it is gating while checking nothing. That
# was bite-tested on this corpus (doc/lint/README.md): the `^`-anchored
# form exits 0 against a planted A7 heading, the tail-anchored form exits 1.
# * sentence_length fingerprints are `C2:file:#N:message` — rule at the HEAD.
# So `^C2:` is the correct shape THERE, and it deliberately cannot reach
# the `advisory-C2` slice (doc/STYLE_GUIDE.md C2 relaxes the 25-word limit
# for 2.networking-tutorial/, which is essay-style protocol theory).
# * doc_lint is also rule-at-the-head, hence `^(A1|A6|B2|D2|ANCHOR):`. SHOULD
# a rule be added there, note SHAPE is deliberately excluded: it is an
# advisory content heuristic, not a defect.
#
# A skip of ANY gated check (doc_lint / vale_adoc / vale_docstrings /
# sentence_length / mrdocs_warnings) fails the gate — can't verify a gated rule
# = not a pass. Bite-tested: a crashed extract-docstrings.mjs marks
# vale_docstrings and sentence_length SKIPPED and exits non-zero, rather than
# reading zero findings as success.
#
# Do NOT reseed baseline.json locally — a local run grandfathers local-vs-CI
# drift as if it were the real backlog. Reseed via the workflow_dispatch steps
# below, per doc/lint/README.md.
# THE GATE IS SPLIT IN TWO. Everything blocks EXCEPT the MrDocs
# reference-surface check, and that exception is evidence-based.
#
# A1/A6/B2/D2/ANCHOR, C2, and the C4/C9/C10/A7 wording rules are all STRICT.
# baseline.json is now authored by this job (workflow_dispatch reseed), so a
# strict comparison is CI-against-CI and carries no environment drift. The
# wording rules additionally have an EMPTY gated subset — zero baselined
# C4/C9/C10/A7 fingerprints on either corpus — so any match at all is a real
# regression, which is why they were safe to promote even from a local run.
#
# mrdocs_warnings deliberately stays REPORTING. The baseline is zero, so its
# `.*` spec would gate every warning that ever appears — and MrDocs itself is
# a rolling `develop-release` build whose output demonstrably moves: the same
# asset reported 460 warnings under 0.8.0 and 352 under 2026.9.5, days apart,
# on an unchanged tree. Gating `.*` on a tool that rewrites its own output
# would fail this job for upstream reasons that have nothing to do with
# Corosio's documentation — the same mistake as the version pin that skipped
# this check on the first reseed (doc/lint/mrdocs-warnings.mjs). Promote it
# only alongside a pinned MrDocs.
#
# Do NOT "fix" a strict-step failure by reseeding locally. Reseed via the
# workflow_dispatch steps below, per doc/lint/README.md.
- name: "Lint: gate (BLOCKING)"
if: always()
continue-on-error: false
working-directory: boost-root/libs/corosio/doc
run: |
node lint/check-no-new-violations.mjs --strict \
--gate 'doc_lint:^(A1|A6|B2|D2|ANCHOR):' \
--gate 'sentence_length:^C2:' \
--gate 'vale_adoc:Corosio\.PartHeadings$' \
--gate 'vale_adoc:(Corosio\.SimpleTense|Corosio\.NoFluff|Corosio\.Terminology)$' \
--gate 'vale_docstrings:(Corosio\.SimpleTense|Corosio\.NoFluff|Corosio\.Terminology)$'
- name: "Lint: gate (reference surface, reporting)"
if: always()
continue-on-error: false
working-directory: boost-root/libs/corosio/doc
run: |
# Reports and annotates; does NOT fail the job (no --strict). See the
# rolling-build reasoning above the blocking step.
node lint/check-no-new-violations.mjs \
--gate 'mrdocs_warnings:.*'
# --- Baseline reseed (workflow_dispatch only) ---------------------------
# doc/lint/baseline.json is the gate's reference point: anything in it is
# grandfathered. It goes stale as the backlog is worked down (a fix removes
# findings but not their baseline entries), and a stale-high baseline
# grandfathers findings that no longer exist — so they can be reintroduced and
# the gate stays green. Retiring them needs a regenerated baseline.
#
# Regenerating on a developer machine is NOT safe: a local run differs from a
# CI run (a different MrDocs develop build hash, file-processing order), and
# committing those differences would grandfather environment drift as if it
# were the real backlog. So the candidate is authored HERE, by the same job,
# on the same runner image, with the same PATH the gate above just used.
# Reusing the gate's own job — rather than a second job that re-creates its
# setup — is deliberate: an imitated environment is exactly the bug this
# avoids, and it cannot drift from the gate's environment because it IS that
# environment.
#
# Two safety properties of the ordering and paths below:
# * these steps run AFTER the gate, and
# * the candidate is written to RUNNER_TEMP, never to the checked-out
# doc/lint/baseline.json,
# so the gate in this same run still compares against the COMMITTED baseline.
# A candidate that overwrote it first would make the gate compare a run
# against itself and pass unconditionally.
#
# The job never commits or pushes. It uploads a candidate for review; a human
# reads the diff and commits it.
- name: "Reseed: generate candidate"
if: always() && github.event_name == 'workflow_dispatch'
working-directory: boost-root/libs/corosio/doc
run: |
set -euo pipefail
mkdir -p "$RUNNER_TEMP/baseline-candidate"
node lint/baseline.mjs "$RUNNER_TEMP/baseline-candidate/baseline.json"
# Reports per-check counts before/after and, per check and rule, which
# fingerprints the candidate would ADD (grandfather) and REMOVE (retire). Any
# ADDED fingerprint matching the gate spec is a finding a reseed would
# silently un-gate; those are named individually and fail this step. So do a
# SKIPPED check and a GATED check that collapsed to zero findings — both
# would wipe a gated check's whole grandfathered backlog.
#
# The gate spec is EXTRACTED from this workflow file rather than restated
# here. A second verbatim copy is a rot hazard with a silent failure mode:
# promote a rule in the gate step above, forget this one, and the report keeps
# printing "none gated" for a rule that now blocks — the safety net stops
# covering exactly the rule just deemed important enough to gate. Extraction
# means there is one copy. If extraction yields nothing (someone reformatted
# the gate step's arguments), this step FAILS rather than reporting against an
# empty gate spec, which would look identical to "no gated additions".
- name: "Reseed: report changes"
if: always() && github.event_name == 'workflow_dispatch'
working-directory: boost-root/libs/corosio/doc
env:
ALLOW_EMPTIED: ${{ inputs.allow_emptied }}
run: |
set -uo pipefail
out="$RUNNER_TEMP/baseline-candidate"
workflow=../.github/workflows/docs.yml
# Read the run blocks of BOTH gate steps: each starts at its `- name:` line
# and ends at the next blank line. Two details are load-bearing:
# * the toggle is `inblock = 0`, not `exit` -- the gate is split in two
# steps, and exiting at the first blank line would silently drop the
# second step's specs from this safety net.
# * the pattern is anchored to `^ *- name:`, because this awk program
# CONTAINS the string it searches for. Without the anchor it matches
# its own source line and captures the grep/sed lines below as if they
# were gate specs (measured: two junk entries, one an invalid regex).
gate_args=()
while IFS= read -r spec; do
gate_args+=(--gate "$spec")
done < <(
awk '/^[[:space:]]*- name: "Lint: gate/ { inblock = 1; next }
inblock && /^[[:space:]]*$/ { inblock = 0 }
inblock' "$workflow" \
| grep -o -- "--gate '[^']*'" \
| sed "s/^--gate '//; s/'\$//" \
| sort -u
)
if [ "${#gate_args[@]}" -eq 0 ]; then
echo "::error title=Gate spec not found::could not extract any --gate spec from $workflow; refusing to report against an empty gate spec"
exit 1
fi
echo "gate spec extracted from $workflow: ${gate_args[*]}"
# A gated check dropping to zero is refused by default, because that is
# indistinguishable from a crash. Naming it here is the acknowledgement,
# and it lands in the run log next to the report it authorised.
# ALLOW_EMPTIED arrives through `env:` rather than template
# interpolation: the value is attacker-chosen text and would otherwise
# be pasted straight into this script. Note that a run: block is
# scanned for expressions in full, comments included -- writing an
# empty expression pair here, even inside a comment, is a parse error
# ("An expression was expected") for the whole workflow.
# Unquoted on purpose -- word splitting is what turns the space-separated
# input into separate checks. An empty value splits to zero words, so the
# loop simply does not run.
allow_args=()
for check in ${ALLOW_EMPTIED:-}; do
allow_args+=(--allow-emptied "$check")
done
if [ "${#allow_args[@]}" -gt 0 ]; then
echo "::warning title=Emptied gated check accepted::${ALLOW_EMPTIED}"
fi
status=0
node lint/baseline-diff.mjs lint/baseline.json "$out/baseline.json" \
"${gate_args[@]}" "${allow_args[@]+"${allow_args[@]}"}" \
| tee "$out/baseline-diff.txt" || status=$?
diff -u lint/baseline.json "$out/baseline.json" > "$out/baseline.json.diff" || true
# GitHub rejects a step summary over 1 MiB, so cap it and point at the
# artifact for the full text.
{
echo '## Candidate doc/lint/baseline.json'
echo
echo 'Download the `doc-lint-baseline-candidate` artifact. Do not commit it'
echo 'without accounting for every ADDED fingerprint below. Full untruncated'
echo 'report: `baseline-diff.txt` in that artifact.'
echo
echo '```'
head -c 900000 "$out/baseline-diff.txt"
echo '```'
} >> "$GITHUB_STEP_SUMMARY"
exit "$status"
- name: "Reseed: upload candidate"
if: always() && github.event_name == 'workflow_dispatch'
uses: actions/upload-artifact@v4
with:
name: doc-lint-baseline-candidate
path: ${{ runner.temp }}/baseline-candidate
if-no-files-found: error
- name: Create Antora Docs Artifact
uses: actions/upload-artifact@v4
with:
name: antora-docs
path: boost-root/libs/corosio/doc/build/site