Repository navigation
190 lines (171 loc) · 7.43 KB
/
Copy pathdocs.yml
File metadata and controls
190 lines (171 loc) · 7.43 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
name: Docs
# Publishes https://interlock.sourceshift.ai/: the landing page at the root
# and the manual, from the Markdown under docs/, at /docs/.
#
# The site is built by Zensical -- the successor to Material for MkDocs -- and
# deployed to Cloudflare Pages as a directory of static files. There is no
# Node build step and no framework: Zensical is a Python package, so this job
# only needs an interpreter.
#
# Zensical requires Python >= 3.11, which is why this job pins 3.13 rather than
# the 3.9 floor the SDK itself supports.
#
# Pinned to an exact Zensical version on purpose. It is 0.0.x and its output
# is the deployed artefact, so an unreviewed upgrade should be a commit someone
# made, not something that happens on a Tuesday because CI resolved `latest`.
#
# Two builds, in a fixed order. zensical.landing.toml roots a one-page build
# at / and owns site/; zensical.toml then writes the manual into site/docs/
# beside it (the landing config explains why one build cannot do both). The
# deploy step publishes site/, so the deployed directory and a local build
# have the same shape.
on:
push:
branches: [main]
paths:
- "docs/**"
- "landing/**"
- "overrides/**"
- "zensical.toml"
- "zensical.landing.toml"
- "scripts/check_site.py"
- ".github/workflows/docs.yml"
pull_request:
paths:
- "docs/**"
- "landing/**"
- "overrides/**"
- "zensical.toml"
- "zensical.landing.toml"
- "scripts/check_site.py"
- ".github/workflows/docs.yml"
workflow_dispatch:
permissions:
contents: read
concurrency:
group: docs-${{ github.ref }}
cancel-in-progress: true
jobs:
build:
name: build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/setup-python@v6
with:
python-version: "3.13"
- name: Install Zensical
run: pip install zensical==0.0.70
# --strict aborts the build on any warning rather than printing it and
# carrying on. Verified locally: a broken internal link makes this exit 1.
# The docs are read on GitHub *and* on the site, so a link that resolves
# in only one of the two places is exactly the drift this catches.
- name: Build the landing page
run: zensical build -f zensical.landing.toml --clean --strict
- name: Build the manual
run: zensical build --clean --strict
# --strict already fails a broken internal link during the build; this
# keeps the check visible as its own named step (and catches a relative
# file link that Zensical would otherwise resolve differently from
# GitHub). It walks the source Markdown, so a page outside the nav is
# checked too.
- name: docs-link-check
run: |
python3 - <<'PY'
from pathlib import Path
import re
FENCE = re.compile(r"^ {0,3}(`{3,}|~{3,})(.*)$")
CODE_SPAN = re.compile(r"(?<!`)(`+)(?!`)([^\n]*?)(?<!`)\1(?!`)")
def strip_code(text):
"""Drop fenced blocks and inline code spans before parsing links.
Brackets inside code are not links: `TOOLS[name](**safe_args)`
is Python, not Markdown. Matching the raw text reports it as a
broken target, so remove code first (a Markdown parser would do
the same thing, but is not worth another dependency here).
"""
kept = []
fence = None
for line in text.splitlines():
match = FENCE.match(line)
if fence is None:
if match:
fence = match.group(1)
continue
kept.append(line)
continue
if (match and match.group(1)[0] == fence[0]
and len(match.group(1)) >= len(fence)
and not match.group(2).strip()):
fence = None
return CODE_SPAN.sub("", "\n".join(kept))
missing = []
for page in sorted(Path("docs").rglob("*.md")):
text = strip_code(page.read_text(encoding="utf-8"))
for target in re.findall(r"\]\(([^)\s]+)", text):
if target.startswith(("http://", "https://", "mailto:",
"#")):
continue
path = target.split("#", 1)[0].split("?", 1)[0]
if not path:
continue
if not (page.parent / path).resolve().exists():
missing.append("{}: {}".format(page, target))
if missing:
raise SystemExit(
"docs-link-check: unresolved links\n" + "\n".join(missing))
print("docs-link-check: every internal link resolves")
PY
# What neither build can check for itself: that the landing config still
# mirrors the manual's, that the landing's links into the manual resolve
# (they are served URLs, because the landing build cannot see the
# manual's files), and that the tree has the shape the deploy relies on.
# The landing build owns the root of site/ -- its index, its 404.html,
# and landing/_redirects copied in, the only place Pages reads one. The
# manual build owns site/docs/, whose own 404.html Pages picks for
# anything missing under /docs/, since it serves the closest 404.html
# walking up from the requested path.
- name: Check the site
run: python3 scripts/check_site.py
- name: Upload the built site
uses: actions/upload-artifact@v4
with:
name: site
path: site
retention-days: 7
deploy:
name: deploy
needs: build
# Only main publishes. A pull request builds (above) so a broken link or a
# bad config fails the PR, but it must not touch the live site.
if: github.event_name != 'pull_request'
runs-on: ubuntu-latest
environment:
name: docs
url: https://interlock.sourceshift.ai/
steps:
- uses: actions/download-artifact@v5
with:
name: site
path: site
- name: Deploy to Cloudflare Pages
uses: cloudflare/wrangler-action@v3
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
command: pages deploy site --project-name=interlock-docs --branch=main
# The deploy is live within seconds, but the custom domain can lag the
# pages.dev URL by a little, hence the retries. This checks the two
# halves of the layout: the landing at the root, the manual at /docs/,
# and a pre-move URL still finding its way there.
- name: Smoke the live site
run: |
for attempt in 1 2 3 4 5 6; do
code=$(curl -s -o /dev/null -w '%{http_code}' https://interlock.sourceshift.ai/docs/)
[ "$code" = "200" ] && break
echo "attempt $attempt: /docs/ returned $code, retrying"
sleep 10
done
test "$code" = "200"
test "$(curl -s -o /dev/null -w '%{http_code}' https://interlock.sourceshift.ai/)" = "200"
test "$(curl -s -o /dev/null -w '%{http_code}' https://interlock.sourceshift.ai/docs/getting-started/)" = "200"
test "$(curl -s -o /dev/null -w '%{redirect_url}' https://interlock.sourceshift.ai/getting-started/)" = "https://interlock.sourceshift.ai/docs/getting-started/"