Repository navigation
284 lines (259 loc) · 12.8 KB
/
Copy pathpkgdown.yaml
File metadata and controls
284 lines (259 loc) · 12.8 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
# Workflow derived from https://github.com/r-lib/actions/tree/v2/examples
# Need help debugging build failures? Start at https://github.com/r-lib/actions#where-to-find-help
# Builds the pkgdown site `DESCRIPTION` and `README.md` advertise, and
# publishes it to `gh-pages` from the default branch only.
#
# This file is NOT the r-lib stock template, and every departure below carries
# a comment naming the property it buys, so a later template sync is a merge
# rather than a copy. The collapse into the stock single-job template is what
# this shape was restored from.
#
# The `paths-ignore` list has to meet the same test the gating workflows' copies
# meet: these paths cannot change what the run sees. `cairn/**` and `.claude/**`
# are nothing pkgdown reads. `CLAUDE.md` is the one that needs saying, because
# pkgdown DOES read it -- `pkgdown:::package_mds()` globs every root and
# `.github/` markdown file against a hardcoded exclusion list with no config
# knob, and a local build produced `docs/CLAUDE.html`. It is on this list only
# because the `Drop the repo-internal sources` step below removes it before the
# build ever reads it; delete that step and this entry becomes a lie.
#
# The list is spelled out under both triggers because GitHub Actions does not
# resolve YAML anchors in workflow files; `.github/ci-usage.py` reads every
# copy back separately -- both triggers here and both in every other workflow
# carrying the filter -- and exits non-zero if any two of them disagree.
on:
push:
branches: [main, master]
paths-ignore:
- 'cairn/**'
- 'CLAUDE.md'
- '.claude/**'
pull_request:
paths-ignore:
- 'cairn/**'
- 'CLAUDE.md'
- '.claude/**'
# A published release builds the site but does not publish it: the `deploy`
# job below guards on the ref being the default branch, and a release run's
# ref is the tag. The trigger is kept as a build check at the released
# version, never as a second publishing path.
release:
types: [published]
workflow_dispatch:
name: pkgdown.yaml
permissions: read-all
# Same reasoning as the gating workflows: a superseded run answers a question
# about a commit nobody is waiting on. The default branch is exempt because it
# is a distribution channel -- and here it is the branch that actually
# publishes, so a cancelled run there would leave the site half-written.
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.ref_name != github.event.repository.default_branch }}
# Two jobs, deliberately. Building the site EXECUTES code from the ref being
# built -- the vignettes, every roxygen `@examples` block, and whatever those
# load out of Suggests -- so the build job must not hold a token that can write
# to this repository. It runs at the workflow's `read-all` and hands `docs/` on
# as an artifact; only `deploy`, which runs no repository code at all, takes
# `contents: write`, and only on the default branch. Collapsing these back into
# one job would put a writable token in the same process as PR-authored code on
# every pull request.
jobs:
build:
runs-on: ubuntu-latest
# Not one of the suite-running workflows' measured caps, each priced
# against a hang inside `test_check("nestedtune")` -- 30 minutes on
# R-CMD-check's check step, 40 on windows; this job runs no tests and the
# vignettes start no daemons. It is here only so a runaway build fails in
# minutes instead of burning GitHub's 360-minute default.
timeout-minutes: 20
env:
GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }}
steps:
- uses: actions/checkout@v7
- uses: r-lib/actions/setup-pandoc@v2
# Not stock either: `use-public-rspm` takes the builder and its
# dependencies as Linux binaries from Posit's public repository instead of
# compiling them from source on the runner.
- uses: r-lib/actions/setup-r@v2
with:
use-public-rspm: true
# `local::.` installs the package itself, which is what makes
# `install = FALSE` correct on the build step below. The builder is NOT
# named here: `needs: website` resolves it from `Config/Needs/website` in
# DESCRIPTION. Adding `any::pkgdown` back would install it either way,
# which is the ad-hoc line that field replaced -- and it would make the
# field decorative, since dropping it would still build green.
- uses: r-lib/actions/setup-r-dependencies@v2
with:
extra-packages: local::.
needs: website
# pkgdown renders every root and `.github/` markdown file it does not
# already own -- `pkgdown:::package_mds()` globs them against a hardcoded
# exclusion list, with no config knob to add to it -- so `CLAUDE.md` (this
# repo's agent instructions) and `.github/ci-usage-baseline.md` (a CI cost
# measurement) both become public pages.
#
# Removing the SOURCES before the build, rather than the built HTML after
# it, is what keeps them out of the derived artifacts too:
# `pkgdown:::get_site_paths()` globs every `*.html` under `docs/`, and both
# `sitemap.xml` and `search.json` are built from it, so a post-build delete
# would leave the pages listed in the sitemap and their full text in the
# site search index. This touches the runner's checkout only.
- name: Drop the repo-internal sources
run: rm -f CLAUDE.md .github/ci-usage-baseline.md
shell: bash
# Catches an export or article missing from `_pkgdown.yml` before the
# build spends time on a site that is already wrong.
- name: Check pkgdown config
run: pkgdown::check_pkgdown()
shell: Rscript {0}
- name: Build site
run: pkgdown::build_site_github_pages(new_process = FALSE, install = FALSE)
shell: Rscript {0}
# Guards the removal above: if the `Drop the repo-internal sources` step is
# ever weakened, this is what turns the resulting publication into a failed
# run instead of a live page.
#
# It iterates the repository's own markdown listing rather than naming the
# two known pages, so a markdown file added anywhere later is covered with
# no edit here. `git ls-files` reads the index, so the two files the step
# above deleted from the checkout are still in the domain. A page is
# allowed only for the five sources pkgdown is meant to publish; every
# other source must have no page at the site root, no copied markdown, no
# `sitemap.xml` entry and no `search.json` entry. The two index matches
# are anchored at the DOMAIN root -- `https?://[^/]+/<stem>.html` -- so an
# article or reference topic sharing a stem with some repo-internal file
# is not mistaken for a leak. That is the site root only while
# `_pkgdown.yml`'s `url:` has no path segment, as it does not today; give
# the site a subpath and those two arms stop matching, leaving the
# `docs/` listing arms to carry the check alone.
#
# `git ls-files` also bounds the domain to TRACKED markdown. An untracked
# or ignored root `.md` file present in the workspace would be rendered by
# pkgdown and seen by none of the four arms; nothing in this repository
# writes one, and the index read is what keeps the two files the step
# above deleted inside the domain.
- name: Check the repo-internal pages are absent
run: |
set -u
allowed='README.md
NEWS.md
LICENSE.md
.github/CODE_OF_CONDUCT.md
.github/CONTRIBUTING.md'
allowed="$(printf '%s\n' "$allowed" | sed 's/^ *//')"
for index in docs/sitemap.xml docs/search.json; do
if [ ! -s "$index" ]; then
echo "::error::$index is missing or empty, so this check cannot read it"
exit 1
fi
done
# Matched against a listing rather than with `[ -f ]`, so the
# comparison stays case-sensitive on a case-insensitive filesystem
# too: `cairn/references/INDEX.md` must not read as `docs/index.html`.
root_files="$(ls -1 docs)"
sources="$(git ls-files -- '*.md' '.github/*.md' | sort -u)"
if [ -z "$sources" ]; then
echo "::error::the markdown listing came back empty, so this check looked at nothing"
exit 1
fi
echo "checking $(printf '%s\n' "$sources" | wc -l) tracked markdown sources"
leaked=0
while IFS= read -r src; do
[ -n "$src" ] || continue
if printf '%s\n' "$allowed" | grep -qxF "$src"; then
continue
fi
stem="$(basename "$src" .md)"
stem_re="$(printf '%s' "$stem" | sed -e 's/[][\.*^$(){}?+|/]/\\&/g')"
root_re="<loc>https?://[^/]+/${stem_re}\.html</loc>"
search_re="\"https?://[^/]+/${stem_re}\.html\""
if printf '%s\n' "$root_files" | grep -qxF "$stem.html"; then
echo "::error::$src reached the built site as docs/$stem.html"
leaked=1
fi
if printf '%s\n' "$root_files" | grep -qxF "$stem.md"; then
echo "::error::$src was copied into the built site as docs/$stem.md"
leaked=1
fi
if grep -qE "$root_re" docs/sitemap.xml; then
echo "::error::$src is listed in docs/sitemap.xml at the site root"
leaked=1
fi
if grep -qE "$search_re" docs/search.json; then
echo "::error::$src is indexed in docs/search.json at the site root"
leaked=1
fi
done <<EOF
$sources
EOF
exit "$leaked"
shell: bash
# The two pages `DESCRIPTION` and `README.md` advertise. A pkgdown build
# that silently stopped emitting either would still deploy a green-looking
# site whose advertised links 404.
- name: Check the advertised pages exist
run: |
missing=0
for page in docs/index.html docs/articles/nested-cv.html; do
if [ ! -f "$page" ]; then
echo "::error::$page is missing from the built site"
missing=1
fi
done
exit "$missing"
shell: bash
# `include-hidden-files` because `build_site_github_pages()` writes
# `docs/.nojekyll`, and upload-artifact excludes dotfiles by default. The
# two-job shape is what makes this needed: the single-job template
# deployed `docs/` straight from the build and never lost it. `.nojekyll`
# first reached `gh-pages` on 2026-08-31, under the collapsed shape, and
# is absent from every deploy the earlier two-job shape made -- restoring
# that shape without this line would have dropped it again, invisibly,
# because `clean: false` leaves the copy already on the branch in place.
- name: Upload the built site
uses: actions/upload-artifact@v7
with:
name: pkgdown-site
path: docs
include-hidden-files: true
retention-days: 1
# Runs no code from the ref being built -- it checks the ref out, unpacks an
# artifact and pushes it, and executes nothing it checked out -- which is what
# makes `contents: write` safe to hold here and nowhere else in this
# repository.
#
# `github.ref_name` rather than `github.event_name != 'pull_request'`: the
# looser form also publishes from a `workflow_dispatch` on a feature branch,
# which is exactly the accident this guard exists to refuse. It is on the job
# so a pull request never starts the writable job at all.
deploy:
needs: build
if: github.ref_name == github.event.repository.default_branch
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: write
steps:
# The deploy action's first act is `git config user.name` inside the
# workspace, so it needs one to be a git checkout: without this step the
# job dies at `fatal: not in a git directory` before it reads the
# artifact. Nothing here runs what is checked out -- see the job comment
# above -- and the artifact is unpacked into `docs/`, which is gitignored,
# so the checkout and the built site never contend for the same paths.
- uses: actions/checkout@v7
- uses: actions/download-artifact@v8
with:
name: pkgdown-site
path: docs
# Pinned by commit SHA, not by tag: tags are mutable, and this is the one
# action in the repository that runs with a token able to write to it.
# `test-coverage.yaml` pins codecov the same way.
- name: Deploy to gh-pages
uses: JamesIves/github-pages-deploy-action@fa24774553152dd7873cd16ebd8d959b010c5445 # v4.9.0
with:
# Publication is additive, so a page renamed away before 1.0 stays
# reachable at its old address.
clean: false
branch: gh-pages
folder: docs