-
Notifications
You must be signed in to change notification settings - Fork 29
440 lines (406 loc) · 21.6 KB
/
Copy pathdocs.yml
File metadata and controls
440 lines (406 loc) · 21.6 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
#
# Copyright (c) 2023 Christian Mazakas
# Copyright (c) 2023 Alan de Freitas
#
# 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/capy/
#
name: Documentation
on:
push:
branches:
- master
- develop
- develop-2
- 'pr/**'
paths:
- 'doc/**'
- 'include/**'
- '*.adoc'
- 'README.adoc'
- '.github/workflows/docs.yml'
pull_request:
paths:
- 'doc/**'
- 'include/**'
- '*.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 blocking 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:
jobs:
antora:
name: Antora Docs
runs-on: 'ubuntu-latest'
# The a11y contrast gate (E4) is Review tier, not blocking (doc/STYLE_GUIDE.md
# Part F.0 — demoted alongside E2: all gated failures were color-contrast on
# shared Antora theme nav chrome, not Capy-fixable). pa11y still needs a
# launchable browser to run the scan as a non-blocking report: .pa11yci.json
# defaults to /usr/bin/chromium (local), but ubuntu-latest ships google-chrome
# at /usr/bin/google-chrome and has no chromium. run-a11y.mjs reads
# PA11Y_CHROME_PATH to override the config, so point it at the runner's
# pre-installed Chrome for every step that runs the a11y scan (the scan step
# and the omnibus report).
env:
PA11Y_CHROME_PATH: /usr/bin/google-chrome
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 Antora/build_antora.sh pulls in via `npm ci` —
# that has 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 (A7 Capy.PartHeadings on the pages; C4/C9/C10 on both
# surfaces since Phase-4 exit) the gate's gated-skip path would fail the job on
# missing infra. Note the docstring corpus needs asciidoctor too: the extracted
# files are `.adoc`, so a missing Ruby CLI skips that slice as well. The apt package
# installs /usr/bin/asciidoctor, on PATH for every later step. This first
# step runs before the Antora build, the Vale steps, and the blocking gate.
- name: Install packages
uses: alandefreitas/cpp-actions/package-install@v1.9.0
with:
apt-get: git cmake asciidoctor
- name: Clone Boost.Capy
uses: actions/checkout@v4
with:
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: capy-root
scan-modules-ignore: 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
# 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"/capy-root "libs/$module"
- uses: actions/setup-node@v4
with:
node-version: 18
- name: "Docs: build site"
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/capy
cd doc
bash ./build_antora.sh
# 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
- name: "Docs: upload site artifact"
uses: actions/upload-artifact@v4
with:
name: antora-docs
path: boost-root/libs/capy/doc/build/site
# --- Doc-quality checks (Style Guide Part F.0 / doc improvement plan Task 2) ---
# Individual check steps below stay non-blocking (continue-on-error: true): they
# print each tool's raw findings for review. Enforcement is done by the
# no-new-violations comparator, which diffs a fresh run against baseline.json.
# The gated rules are A1/A6/A7/B2/D2/ANCHOR + MrDocs-no-warnings + C2/C4/C9/C10.
# The gate step below names them via --gate and REPORTS them: it prints each with
# file, line and an excerpt, and annotates it on the diff, but runs without
# --strict, so it does not fail the job. It keeps continue-on-error: false so a
# crash in the comparator itself still surfaces. Re-blocking is adding --strict
# back to that one step. E4 (a11y contrast) is Review tier, not gated
# (doc/STYLE_GUIDE.md Part F.0). Everything outside the gated slice is reported by
# the "Lint: remaining backlog" step.
# The accuracy gate for .adoc example code (B2/B3/D2 correctness) is separate and
# stays a hard gate: the boost_capy_doc_tests b2 target defined in
# test/doc/Jamfile, run via `./b2 libs/capy/test` in ci.yml, not this job
# (test/doc/CMakeLists.txt defines the equivalent CMake target).
- 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/capy/doc/node_modules/.bin" >> "$GITHUB_PATH"
- name: "Lint: sync Vale styles"
if: always()
continue-on-error: true
working-directory: boost-root/libs/capy/doc
run: vale sync
- name: "Lint: Vale over pages"
if: always()
continue-on-error: true
working-directory: boost-root/libs/capy/doc
# Vale's exit codes: 0 = no alerts, 1 = alerts found, 2 = fatal (e.g.
# asciidoctor off PATH). 1 is the normal state on this corpus and fires
# every run, which surfaced as a red "Process completed with exit code 1"
# annotation saying nothing -- the alerts themselves are reported, with
# file, line and quote, by the no-new-violations steps below. Treat 1 as
# success so only a genuine Vale failure annotates.
run: |
vale modules; s=$?
[ "$s" -le 1 ] || exit "$s"
- name: "Lint: Vale over docstrings"
if: always()
continue-on-error: true
working-directory: boost-root/libs/capy/doc
# Vale's exit codes: 0 = no alerts, 1 = alerts found, 2 = fatal (e.g.
# asciidoctor off PATH). 1 is the normal state on this corpus and fires
# every run, which surfaced as a red "Process completed with exit code 1"
# annotation saying nothing -- the alerts themselves are reported, with
# file, line and quote, by the no-new-violations steps below. Treat 1 as
# success so only a genuine Vale failure annotates.
run: |
node lint/extract-docstrings.mjs
vale lint/.docstrings; s=$?
[ "$s" -le 1 ] || exit "$s"
- name: "Lint: structure (doc-lint.mjs)"
if: always()
continue-on-error: true
working-directory: boost-root/libs/capy/doc
run: node lint/doc-lint.mjs
- name: "Lint: accessibility (pa11y-ci)"
if: always()
continue-on-error: true
working-directory: boost-root/libs/capy/doc
run: node lint/run-a11y.mjs
- name: "Lint: reference warnings (MrDocs)"
if: always()
continue-on-error: true
working-directory: boost-root/libs/capy/doc
run: node lint/mrdocs-warnings.mjs
# selftest.mjs mutates the linters and asserts they notice. It is the only
# thing standing between a silent linter regression and a green run — most
# of the gates here (A1's value whitelist, A7's numeral match, B2's block
# walk, the role=output exemption boundary) were fail-open at some point
# and were only found by planting a violation. SHAPE in particular is
# advisory and reads 0, so a broken looksLikeCode() is invisible everywhere
# else. Non-blocking for now: it is a meta-test, and it can go red for
# reasons unrelated to the documentation (a refactor of the linters
# themselves). Promote to blocking once it has proven quiet.
- name: "Lint: linter self-test"
if: always()
continue-on-error: true
working-directory: boost-root/libs/capy/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/capy/doc
run: node lint/check-no-new-violations.mjs --show-baseline
# The gate: reports any NEW A1/A6/A7/B2/D2/ANCHOR violation, any
# NEW MrDocs reference-surface warning, and — promoted at Phase-4 exit — any NEW
# C2/C4/C9/C10 wording violation on EITHER surface. A1/A6/B2/D2 are doc_lint
# fingerprints; A7 is the Vale rule Capy.PartHeadings; MrDocs-no-warnings gates
# the whole mrdocs_warnings check; C4/C9/C10 are the Vale rules
# Capy.SimpleTense / Capy.NoFluff / Capy.Terminology, gated over both the .adoc
# pages (vale_adoc) and the extracted header docstrings (vale_docstrings), so
# vale_docstrings is now a GATED check too. C2's authority is
# lint/sentence-length.mjs, not a Vale rule — Capy.SentenceLength is
# `level: suggestion` and enforces nothing (see
# .vale/styles/Capy/SentenceLength.yml) — so C2 gates that script's hard slice.
# E4 (a11y contrast) is Review tier, not gated — see doc/STYLE_GUIDE.md Part F.0
# (demoted like E2: the gated failures were color-contrast on shared Antora theme
# nav chrome, which Capy cannot fix). 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); a skip of the non-blocking a11y scan
# does not. The pre-existing backlog is grandfathered by baseline.json.
#
# 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 measured twice on this branch; it is why the A7 spec is written
# `Capy\.PartHeadings$` and not `^Capy\.PartHeadings$`.
# * 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` design-essay slice (doc/STYLE_GUIDE.md Part C2 makes the
# 25-word limit soft in essays; 67 findings under
# modules/ROOT/pages/9.design/ and .../A.specification-methods/ are an
# explicit carve-out, not a backlog).
# Fingerprint-shape contract: doc/lint/README.md. Never promote a rule here on the
# strength of a green run — plant a violation and watch this step fail first.
#
# `sentence_length` was added after the previous baseline snapshot, so nothing in
# its slice was grandfathered and every finding read as new. The 2026-08-25 reseed
# took a snapshot that includes it: 65 fingerprints, 7 C2 and 58 advisory-C2. The
# 7 are PR #383's, accepted as backlog by maintainer decision.
#
# Do NOT add a suppression mechanism, and do NOT reseed baseline.json locally — a
# local run grandfathers hundreds of local-vs-CI drift fingerprints. Reseed via the
# `workflow_dispatch` steps at the end of this job, per doc/lint/README.md.
# By contrast the C4/C9/C10 gates (both surfaces) are GREEN today with no reseed
# needed: their three residual .adoc findings sit inside two verbatim third-party
# quoted passages and are already grandfathered by baseline.json.
- name: "Lint: gate (new violations)"
if: always()
continue-on-error: false
working-directory: boost-root/libs/capy/doc
run: |
# Reports gated findings and annotates them on the diff; does NOT fail the
# job (no --strict). The two when_any.hpp C2 refusals this once expected are
# gone — resolved before #383 — and the live gated findings are #383's own.
node lint/check-no-new-violations.mjs \
--gate 'doc_lint:^(A1|A6|B2|D2|ANCHOR):' \
--gate 'vale_adoc:Capy\.PartHeadings$' \
--gate 'mrdocs_warnings:.*' \
--gate 'sentence_length:^C2:' \
--gate 'vale_adoc:(Capy\.SimpleTense|Capy\.NoFluff|Capy\.Terminology)$' \
--gate 'vale_docstrings:(Capy\.SimpleTense|Capy\.NoFluff|Capy\.Terminology)$'
# --- 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 by hundreds of fingerprints (measured: 297 — a different MrDocs
# 0.8.0 build hash, chromium vs google-chrome, 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 (Ruby asciidoctor for Vale's
# .adoc scope, doc/node_modules/.bin, the RUNNER_TEMP vale binary) and the
# same PA11Y_CHROME_PATH the blocking 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 the gate's
# environment.
#
# Two safety properties of the ordering and paths below:
# * these steps run AFTER the blocking 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. Maintainer procedure, including how
# to read the report and when NOT to reseed: doc/lint/README.md.
- name: "Reseed: generate candidate"
if: always() && github.event_name == 'workflow_dispatch'
working-directory: boost-root/libs/capy/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 merge-blocking 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 blocking step above, forget this one, and the report
# keeps printing "none gated" for a rule that now blocks merges — the safety
# net stops covering exactly the rule that was just deemed important enough
# to gate. Extraction means there is one copy, in the blocking step, and this
# step cannot disagree with it. If extraction yields nothing (someone
# reformatted the blocking step's arguments), the 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/capy/doc
run: |
set -uo pipefail
out="$RUNNER_TEMP/baseline-candidate"
workflow=../.github/workflows/docs.yml
# Read ONLY the blocking step's run block: from its comparator invocation
# to the blank line that ends it, then stop. `awk ... {exit}` rather than a
# sed range because a sed range restarts at every match of its start
# pattern — including the copy of that pattern in this very extractor,
# which silently pulled the extractor's own quoting into the gate spec.
# Stopping at the first block also makes the source of truth unambiguous:
# the step that actually blocks merges. `grep -o` takes every occurrence
# per line, in case the arguments are ever reflowed onto one.
gate_args=()
while IFS= read -r spec; do
gate_args+=(--gate "$spec")
done < <(
awk '/name: "Lint: gate/ { inblock = 1 }
inblock && /^[[:space:]]*$/ { exit }
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[*]}"
status=0
node lint/baseline-diff.mjs lint/baseline.json "$out/baseline.json" \
"${gate_args[@]}" | tee "$out/baseline-diff.txt" || status=$?
# Full text diff of the file itself: the only place a single changed
# fingerprint is visible verbatim. `diff` exits 1 when files differ.
diff -u lint/baseline.json "$out/baseline.json" > "$out/baseline.json.diff" || true
# GitHub rejects a step summary over 1 MiB. A real report is ~14 KB, but a
# pathological candidate must not turn a reporting step into an infra
# failure, 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