summaryrefslogtreecommitdiff
path: root/AGENTS.md
blob: f445a7c7f808cccb8023e41e559505a447176194 (plain)
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
# AGENTS.md

This file provides guidance to Claude Code (claude.ai/code) when working
with code in this repository.

## Project

VyOS user documentation, built with Sphinx and hosted on Read the Docs
at https://docs.vyos.io. Sources are MyST Markdown (`.md`) — the
migration off RST is complete, `source_suffix` in `docs/conf.py` is
`['.md']` only, and all canonical pages are `.md`.

Pre-migration RST originals are archived under `docs/_rst_legacy/` for
reference only — they are excluded from the build, not consulted by
Sphinx, and not indexed by Context7. Do not edit them.

## Build

```bash
# Docker (recommended — bundles Sphinx and the MyST/RTD plugin set)
docker build -t vyos/vyos-documentation docker
docker run --rm -it -v "$(pwd)":/vyos -w /vyos/docs \
  -e GOSU_UID=$(id -u) -e GOSU_GID=$(id -g) \
  vyos/vyos-documentation make html

# Live-reload server on port 8000
docker run --rm -it -p 8000:8000 -v "$(pwd)":/vyos -w /vyos/docs \
  -e GOSU_UID=$(id -u) -e GOSU_GID=$(id -g) \
  vyos/vyos-documentation make livehtml

# Local (Python 3, see requirements.txt for pinned versions)
pip install -r requirements.txt
cd docs && make html
```

Output: `docs/_build/html/`.

## Lint

The repo doesn't ship a local lint config or pin a linter binary. CI
runs `scripts/doc-linter.py` (in-repo, invoked from
`.github/workflows/lint-doc.yml`) on changed files only, scoped to
`docs/` — see the CI section below. For local checks, manually grep
for the rules in [Source conventions](#source-conventions) (line
length, address space, suppression markers).

## Branches and versions

One long-lived branch per VyOS release line. Branch names are
constellations sorted by area:

| Branch | VyOS version |
|--------|--------------|
| `rolling` | rolling / 1.5+ (default branch — all new docs target this) |
| `circinus` | 1.5.x |
| `sagitta` | 1.4.x |
| `equuleus` | 1.3.x (legacy) |
| `crux` | 1.2.x (legacy) |

PRs target `rolling`. After merge, request backports via a **post-merge
comment** on the PR. Multiple branches go in a single command,
space-separated:

```text
@Mergifyio backport circinus sagitta
```

Only **Maintainers team members** can invoke `@Mergifyio` commands —
Mergify silently drops commands from anyone outside the team (no error
reply). If a backport doesn't trigger, check team membership first. Ask
a Maintainer to post the comment on your behalf.

Mergify only reads commands from **PR comments** — mentions in the PR
body are ignored.
Mergify is configured at the org level (no `.mergify.yml` in the repo).
The PR template has a `## Backport` section to declare intent, but that
does not trigger the backport; the comment does.

## Architecture

### Sphinx config (`docs/conf.py`)

- `source_suffix = ['.md']` — Sphinx only picks up MyST Markdown
  sources. The pre-migration RST originals under `docs/_rst_legacy/`
  are not registered as a source extension and are excluded from the
  build.
- MyST extensions: `colon_fence`, `deflist`, `fieldlist`, `substitution`.
- `myst_fence_as_directive = ["cfgcmd", "opcmd", "cmdincludemd"]` —
  MyST fences with these names get parsed as if they were RST
  directives. This is how command pages stay format-portable.
- Custom modules live in `docs/_ext/` (only files listed in
  `extensions = [...]` in `conf.py` are actual Sphinx extensions; the
  others are support scripts loaded ad hoc):
  - `vyos.py` (Sphinx extension, registered as `vyos`) — defines the
    `cfgcmd`, `opcmd`, `cmdinclude`, `cmdincludemd`, `cfgcmdlist`,
    `opcmdlist` directives and `cfgcmd`/`opcmd` roles that drive
    command coverage tracking.
  - `autosectionlabel.py` (Sphinx extension, registered as
    `autosectionlabel`) — connects to `doctree-read` to register
    sections as labels.
  - `testcoverage.py` — standalone helper that reads VyOS XML command
    definitions and exposes coverage stats; not a Sphinx extension.
  - `releasenotes.py` — standalone release-notes/changelog generator
    script; not a Sphinx extension.

### Source files

- `docs/<subdir>/<page>.md` — canonical MyST source for every page.
  The migration off RST is complete.
- `docs/_include/<name>.txt` — shared RST snippets included into MyST
  pages via `cmdincludemd`. Their content is parsed as RST so the
  legacy templates keep working unchanged.
- `docs/_rst_legacy/<subdir>/rst-<page>.rst` — archived pre-migration
  RST originals. Excluded from the Sphinx build and from the Context7
  index. Reference only.

**Editing rules:**

- Existing page: edit the `.md`. Do not touch the archived original
  under `_rst_legacy/`.
- New page: write it as `.md` from the start. The `md-` prefix that
  earlier MyST migration commits used is gone — never add it.
- `_include/*.txt` snippets stay RST — see the next section.

### Command directives

The VyOS-specific Sphinx directives are `cfgcmd`, `opcmd`, and
`cmdincludemd`. In MyST pages they are written as fenced code blocks
with `{cfgcmd}`, `{opcmd}`, or `{cmdincludemd}` as the info string
(enabled by `myst_fence_as_directive`). They are tracked for command
coverage — do **not** replace them with plain `text` or `bash`
fences.

For RST contexts (`{eval-rst}` blocks and `_include/*.txt` snippets),
the directives are written `.. cfgcmd::`, `.. opcmd::`, and
`.. cmdinclude::`. `cmdinclude` is the RST-side include form;
`cmdincludemd` is the MyST-side form. They resolve to the same
include logic but follow the host file's parser, so pick
`cmdinclude` in `.txt`/RST contexts and `cmdincludemd` in `.md`.

## Source conventions

### RST heading hierarchy

Applies only to RST contexts — `_include/*.txt` snippets and
`{eval-rst}` blocks inside MyST pages. Canonical pages are MyST and
use ATX `#` / `##` / `###` etc. headings; this hierarchy does not
apply to them.

```
##### Title (overline+underline, one per file)
***** Chapters
===== Sections
----- Subsections
^^^^^ Subsubsections
""""" Paragraphs
```

The first heading in every embedded RST snippet that introduces a
title uses `#` overline+underline. Field lists (e.g.,
`:lastproofread:`) or labels may precede it.

### Formatting

- 80-character line limit (exception: inside `.. code-block::` /
  fenced code blocks — `<pre>` preserves source verbatim).
- American English.
- Indent with 2 spaces.
- Blank lines around headings.
- Inline code: single backticks in MyST (the canonical form). Double
  backticks only inside `{eval-rst}` blocks and `_include/*.txt`
  snippets, per RST convention.

### IP addresses (linter-enforced)

Allowed without suppression:
- RFC 5737 IPv4 docs: `192.0.2.0/24`, `198.51.100.0/24`, `203.0.113.0/24`
- RFC 3849 IPv6 docs: `2001:db8::/32`
- RFC 1918 private ranges: `10.0.0.0/8`, `172.16.0.0/12`, `192.168.0.0/16`
- Loopback (`127.0.0.0/8`), link-local (`169.254.0.0/16`), `0.0.0.0/0`

Allowed ASN: `64496-64511` (16-bit), `65536-65551` (32-bit).
Allowed MAC ranges: `00-53-00`–`00-53-FF` (unicast),
`90-10-00`–`90-10-FF` (multicast).

**Requires `stop/start_vyoslinter` suppression:**

- Real public IPs (e.g., a DNS server's address in a DNS forwarder
  example, or an upstream peer's address in an EBGP example).
- NAT64 well-known prefix `64:ff9b::/96`.
- Lines over 80 chars (URLs, certificate fingerprints).

### Linter suppression markers

```rst
.. stop_vyoslinter

.. code-block:: none

   content with real IPs or long lines here

.. start_vyoslinter
```

In MyST `.md` files use the comment form `% stop_vyoslinter` /
`% start_vyoslinter` for top-level Markdown content. Inside
`{eval-rst}` blocks (where the embedded content is parsed as RST)
keep the RST form `.. stop_vyoslinter` / `.. start_vyoslinter` — the
linter scans the source line literally and only the form that matches
the surrounding parser is recognized. Likewise, `.txt` template files
(included via `{include}` or `cmdincludemd`) keep the RST form.

Markers must always come in pairs. Indentation may match the
surrounding directive (indented inside a block) or sit at column 0
(top-level) — both are valid.

### Configuration page structure

1. **Theory** — what it is, when to use it, relevant RFCs.
2. **Configuration** — all CLI options as `.. cfgcmd::` directives
   (in RST contexts) or `{cfgcmd}` fenced code blocks (in MD).
3. **Examples** — practical configurations with topology diagrams.
4. **Known issues** — problems and workarounds.
5. **Debugging** — log collection, `show` commands, state indicators.

### `{todo}` markers

In MyST pages, write TODO markers as `{todo}` fenced directives
(triple-backtick or `:::` fenced blocks with `{todo}` as the info
string). In RST contexts (`{eval-rst}` blocks, `_include/*.txt`
snippets) use the RST form `.. TODO::`. Two valid uses:

1. **Tracking** marker on pages that still need `cfgcmd`/`opcmd`
   conversion — intentional.
2. **Stale** marker on pages that already have full content — should
   be removed.

A PR that both adds and removes TODOs is not contradictory; intent matters.

## LLM-Facing Files (`llms.txt`, `llms-full.txt`)

Both files are regenerated on every `html` and `readthedocs` builder run.
The `dirhtml` builder is intentionally skipped — production publishes
only via `html`/`readthedocs`, and we don't render `llms.txt` for builds
we don't ship. Local `make dirhtml` is a developer convenience and
won't emit `llms.txt`.

Files are shipped at the docs root for each version
(`https://docs.vyos.io/en/<version>/llms.txt`, `.../llms-full.txt`).

- **`llms-full.txt`** — auto-generated by the `sphinx_llms_txt` extension from
  the full corpus. No curation; configured by `llms_txt_file = False` (which
  disables the extension's *index* output, not the full output).
- **`llms.txt`** — curated overview rendered at build time from
  `docs/_templates/llms.txt.j2`. URLs and the version line are interpolated
  from `html_baseurl` and `release` so the file always matches the branch.
  The render lives in `_write_llms_txt(app, exception)` in `docs/conf.py`,
  wired via `app.connect('build-finished', ...)`.

When adding new top-level sections to the docs, add a corresponding
bullet in `docs/_templates/llms.txt.j2`. Branch-specific differences
(e.g. sagitta has no `vpp/index.md` or `contributing/index.md`) live
in that branch's copy of the template.

## Read the Docs Layout

RTD slugs as of 2026-05-04 (verified via API). Re-verify via the RTD
Versions API (project `vyos`) and update the date stamp before editing this
table.

| Slug | Verbose | Branch | Role |
|---|---|---|---|
| `rolling` | rolling | `rolling` | canonical for rolling/next major |
| `1.5` | circinus | `circinus` | canonical for current LTS |
| `1.4` | sagitta | `sagitta` | canonical for previous LTS |
| `1.3`, `1.2` | equuleus, crux | older | canonical for older releases |

URL-level redirect aliases (resolve to the canonicals above):
`/en/latest/* → /en/rolling/`, `/en/lts/* → /en/1.5/`,
`/en/stable/* → /en/lts/`, `/en/circinus/* → /en/1.5/`,
`/en/sagitta/* → /en/1.4/`, `/en/equuleus/* → /en/1.3/`,
`/en/crux/* → /en/1.2/`.

`html_baseurl` per branch must point at the canonical (numeric or `rolling`),
not the alias, so `<link rel="canonical">` and the sitemap match what RTD
serves and crawlers skip the redirect hop.

## CI

- **doc-linter** (`scripts/doc-linter.py` in-repo, invoked via
  `.github/workflows/lint-doc.yml`) — line length and IP rules, on
  changed files under `docs/` only. Repo-root meta files
  (README.md, AGENTS.md, `.github/copilot-instructions.md`) are out
  of scope.
- **Sphinx build** — runs on Read the Docs for every PR; preview URL
  appears as a check.
- **CLA check** — contributors must sign the VyOS CLA before merge.
- **Conflict check** — fails the PR if it doesn't merge cleanly into base.

### Bot review workflow

Two bots run at separate stages — do not mix them:

| Bot | When to trigger | How |
|-----|-----------------|-----|
| **Copilot** | Draft PRs only | Comment `@copilot review` |
| **CodeRabbit** | Ready-for-review PRs only | Comment `@coderabbitai review` |

Auto-reviews are disabled on this repo — both bots are triggered
manually via the comments shown above.

Workflow:
1. Open PR as draft (`gh pr create --draft`).
2. Iterate; when complete, comment `@copilot review`.
3. Address Copilot threads, re-request after each fix round until
   Copilot is silent.
4. Flip to ready (`gh pr ready <num>`), then comment `@coderabbitai review`.
5. Address CodeRabbit threads the same way.

Never trigger `@copilot review` on a ready-for-review PR.