Repository navigation
360 lines (331 loc) · 16.3 KB
/
Copy pathdocs.yaml
File metadata and controls
360 lines (331 loc) · 16.3 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
name: Docs
# Triggers:
# - push to main touching docs/** or this workflow → deploy latest to GitHub Pages
# - push of a major release tag (v2.0.0, v3.0.0 …) → versioned snapshot
# - workflow_dispatch → manual deploy of latest
on:
push:
branches: [main]
paths:
- "docs/**"
- ".github/workflows/docs.yaml"
tags:
# Glob pattern (not regex): matches v1.0.0, v2.0.0, v10.0.0 …
- "v[0-9]*.0.0"
workflow_dispatch:
# Deployment strategy: GitHub Actions Pages source (actions/deploy-pages — official, no third party).
# • gh-pages branch = versioned snapshot STORAGE only (not served directly by Pages)
# • Every deploy job assembles a combined artifact:
# - latest docs built from main at the artifact root
# - each vX/ snapshot from the gh-pages branch merged in
# → single artifact deployed via actions/deploy-pages
# • Snapshot job stores the built snapshot in gh-pages branch via plain git,
# then merges a PR updating versions.json on main. That merge is itself
# GITHUB_TOKEN-authored (via the API), so it does NOT cascade into a new
# `push`-triggered run — the snapshot job explicitly dispatches this
# workflow (workflow_dispatch) for main right after merging, so the new
# snapshot goes live immediately instead of waiting for an unrelated
# docs change.
# Requires: Settings → Pages → Source: GitHub Actions.
permissions:
contents: write
pages: write
id-token: write
# Only one concurrent deployment for the same ref; cancel outdated runs.
concurrency:
group: docs-${{ github.ref }}
cancel-in-progress: true
jobs:
# ── Deploy (latest) ─────────────────────────────────────────────────────────
# Assembles a combined Pages artifact:
# 1. Builds the latest docs (base: /github-code-search/)
# 2. Merges existing versioned snapshots from the gh-pages storage branch
# into the artifact (docs/.vitepress/dist/vX/ for each stored version)
# 3. Uploads the artifact and deploys via actions/deploy-pages
# Requires: Settings → Pages → Source: GitHub Actions.
deploy:
name: Build and deploy docs
if: github.ref == 'refs/heads/main' || github.event_name == 'workflow_dispatch'
runs-on: ubuntu-latest
permissions:
contents: read
pages: write
id-token: write
environment:
name: github-pages
url: ${{ steps.deploy.outputs.page_url }}
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
# Needed to fetch the gh-pages storage branch for versioned snapshots.
fetch-depth: 0
- name: Setup Bun
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
with:
bun-version: latest
- name: Install dependencies
run: bun install --frozen-lockfile
- name: Build latest docs
run: bun run docs:build
# Base URL: /github-code-search/ (default in config.mts)
- name: Merge versioned snapshots from gh-pages storage
run: |
set -euo pipefail
if git ls-remote --heads origin gh-pages | grep -q gh-pages; then
git fetch origin gh-pages
# List all top-level entries and filter with a strict regex —
# `ls-tree` pathspecs are matched literally (no glob expansion,
# unlike `git log`/`git diff`), so `-- 'v[0-9]*'` would silently
# match nothing and skip every snapshot.
for entry in $(git ls-tree --name-only origin/gh-pages); do
if echo "$entry" | grep -qE '^v[0-9]+$'; then
echo "Merging snapshot: $entry"
mkdir -p "docs/.vitepress/dist/$entry"
git archive origin/gh-pages "$entry" | tar -x -C docs/.vitepress/dist/
# Validate: warn and remove if the extracted directory is empty.
if [ -z "$(ls -A "docs/.vitepress/dist/$entry" 2>/dev/null)" ]; then
echo "Warning: snapshot '$entry' is empty after extraction — removing."
rmdir "docs/.vitepress/dist/$entry"
fi
fi
done
else
echo "No gh-pages branch yet — skipping snapshot merge"
fi
- name: Upload Pages artifact
uses: actions/upload-pages-artifact@56afc609e74202658d3ffba0e8f6dda462b719fa # v3.0.1
with:
path: docs/.vitepress/dist
- name: Deploy to GitHub Pages
id: deploy
uses: actions/deploy-pages@d6db90164ac5ed86f2b6aed7e0febac5b3c0c03e # v4.0.5
# ── Snapshot (versioned) ────────────────────────────────────────────────────
# Triggered by a major release tag (e.g. v2.0.0).
# 1. Builds docs with a versioned base URL (/github-code-search/v2/).
# 2. Stores the output in the gh-pages branch under /v2/ using plain git
# (no third-party action). The gh-pages branch is storage only — Pages
# still points to GitHub Actions; the deploy job merges snapshots in.
# 3. Opens a PR with the versions.json + blog stub changes against main —
# main requires signed commits and passing status checks, so a direct
# push is rejected. Since GITHUB_TOKEN-authored events don't trigger new
# workflow runs, ci.yaml is explicitly dispatched (workflow_dispatch is
# exempt from that restriction) for this branch/SHA and awaited before
# merging. The merge itself is also GITHUB_TOKEN-authored, so it won't
# trigger this workflow's own `push` trigger either — this workflow is
# explicitly dispatched for main right after merging instead.
#
# Convention: only tags matching vX.0.0 (major bumps) trigger a snapshot.
# Patch and minor releases update the main docs in-place via the deploy job.
snapshot:
name: Snapshot versioned docs
if: startsWith(github.ref, 'refs/tags/')
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
actions: write
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
# Full history needed to push to gh-pages and commit versions.json to main.
fetch-depth: 0
- name: Extract major version from tag
id: ver
run: |
# Validate that the tag strictly matches vX.0.0 before proceeding.
# The workflow trigger filter (v[0-9]*.0.0) is the primary guard, but
# this ensures the script fails fast if triggered with an unexpected ref.
if ! echo "$GITHUB_REF_NAME" | grep -Eq '^v[0-9]+\.0\.0$'; then
echo "Error: '$GITHUB_REF_NAME' does not match expected pattern vX.0.0" >&2
exit 1
fi
MAJOR="${GITHUB_REF_NAME%%.*}" # e.g. v2 from v2.0.0
echo "major=$MAJOR" >> "$GITHUB_OUTPUT"
- name: Setup Bun
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
with:
bun-version: latest
- name: Install dependencies
run: bun install --frozen-lockfile
- name: Build versioned snapshot
env:
# config.mts reads VITEPRESS_BASE when set; falls back to /github-code-search/
VITEPRESS_BASE: /github-code-search/${{ steps.ver.outputs.major }}/
run: bun run docs:build
- name: Store snapshot in gh-pages branch
run: |
set -euo pipefail
MAJOR="${{ steps.ver.outputs.major }}"
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
# Set up the gh-pages worktree (create orphan branch if it doesn't exist yet).
if git ls-remote --heads origin gh-pages | grep -q gh-pages; then
git fetch origin gh-pages
git worktree add /tmp/gh-pages-storage origin/gh-pages
else
git worktree add --orphan -b gh-pages /tmp/gh-pages-storage
touch /tmp/gh-pages-storage/.nojekyll # prevent Jekyll processing
fi
# Copy the built snapshot into its versioned directory.
rm -rf "/tmp/gh-pages-storage/$MAJOR"
cp -r docs/.vitepress/dist "/tmp/gh-pages-storage/$MAJOR"
# Commit and push.
cd /tmp/gh-pages-storage
git add .
git diff --staged --quiet || git commit -m "docs: store snapshot $MAJOR [skip ci]"
git push origin gh-pages
# Explicit cleanup — belt-and-suspenders even on ephemeral runners.
git worktree remove /tmp/gh-pages-storage
- name: Prepend new version entry to versions.json
run: |
MAJOR="${{ steps.ver.outputs.major }}"
LINK="/${MAJOR}/"
# Idempotent — skip if the entry already exists. Otherwise, prepend
# the new version tagged "(latest)" and strip that suffix from any
# previous entry, since it's now superseded.
jq --arg text "$MAJOR (latest)" --arg link "$LINK" \
'if any(.[]; .link == $link) then .
else [{"text": $text, "link": $link}] + (map(.text |= sub(" [(]latest[)]$"; "")))
end' \
docs/public/versions.json > /tmp/versions_new.json
mv /tmp/versions_new.json docs/public/versions.json
- name: Generate blog post stub for new major version
run: |
set -euo pipefail
# Convert vX.0.0 → v<maj>-0-0 for the file name (e.g. v3.0.0 → v3-0-0)
TAG="$GITHUB_REF_NAME"
SLUG="${TAG//./-}" # v3.0.0 → v3-0-0
BLOG_FILE="docs/blog/release-${SLUG}.md"
RELEASE_DATE="$(date -u +%Y-%m-%d)"
# Idempotent — skip creation if the file already exists (manually authored).
if [ -f "$BLOG_FILE" ]; then
echo "Blog post $BLOG_FILE already exists — skipping stub generation."
else
# Fix: use Python to write the file so heredoc indentation never
# leaks into the generated Markdown (which would break frontmatter).
python3 - <<PY
import pathlib
tag = "$TAG"
release_date = "$RELEASE_DATE"
blog_file = "$BLOG_FILE"
content = (
f'---\ntitle: "What\'s new in {tag}"\n'
f'description: "Highlights of github-code-search {tag}"\n'
f'date: {release_date}\n---\n\n'
f'# What\'s new in github-code-search {tag}\n\n'
f'> Full release notes: <https://github.com/fulll/github-code-search/releases/tag/{tag}>\n\n'
'<!-- TODO: fill in feature highlights, usage examples and screenshots. -->\n'
)
pathlib.Path(blog_file).write_text(content, encoding="utf-8")
print(f"Created blog stub: {blog_file}")
PY
fi
- name: Update blog/index.md table with new major version
run: |
set -euo pipefail
TAG="$GITHUB_REF_NAME"
SLUG="${TAG//./-}"
# Add a row to the blog index table only if the version isn't already listed.
if grep -qF "release-${SLUG}" docs/blog/index.md; then
echo "Blog index already contains ${TAG} — skipping."
else
python3 - <<PY
import re, pathlib
path = pathlib.Path("docs/blog/index.md")
content = path.read_text()
tag = "$TAG"
slug = "$SLUG"
new_row = f"| [{tag}](./release-{slug}) | <!-- TODO: add summary --> |"
# Insert the new row after the last markdown table row (line ending with |).
updated = re.sub(
r"(\|[^\n]+\|)(\s*\Z)",
lambda m: m.group(1) + "\n" + new_row + m.group(2),
content,
count=1,
flags=re.DOTALL,
)
path.write_text(updated)
print(f"Inserted row for {tag} into blog/index.md")
PY
fi
- name: Open PR with versions.json and blog stub, run CI, merge, and deploy
# main is protected by a ruleset requiring signed commits and passing
# status checks — a direct push (previously via
# stefanzweifel/git-auto-commit-action) is rejected. Open a PR instead:
# GitHub's own merge-via-API produces a Verified commit.
#
# `gh pr merge --auto` alone is NOT enough here: this PR/branch is
# created using the default GITHUB_TOKEN, and events authored by that
# token don't cascade into new workflow runs — ci.yaml's `pull_request`
# trigger never fires, the required checks stay "expected" forever,
# and auto-merge would wait indefinitely. workflow_dispatch is
# exempted from that restriction, so we explicitly dispatch ci.yaml
# for this branch/SHA, wait for it, then merge directly once it's
# green — see review on PR #215. The merge push is ALSO
# GITHUB_TOKEN-authored, so this workflow's own `push` trigger won't
# fire either — dispatch it explicitly for main afterwards.
env:
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
MAJOR="${{ steps.ver.outputs.major }}"
BRANCH="docs/${MAJOR}-snapshot-metadata"
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git add docs/public/versions.json docs/blog/
if git diff --staged --quiet; then
echo "No changes to commit — skipping."
exit 0
fi
# Re-base the pending changes onto a fresh branch off origin/main —
# the tag checkout and main's tip are normally the same commit, but
# this is robust even if main advanced in between.
git stash
git fetch origin main
git checkout -B "$BRANCH" origin/main
git stash pop
git add docs/public/versions.json docs/blog/
# The pre-rebase guard above doesn't cover this state: origin/main
# may already carry this exact metadata (e.g. a retry after it was
# applied manually), in which case there's nothing left to commit.
if git diff --staged --quiet; then
echo "No changes remaining after rebasing onto origin/main — already up to date, skipping."
exit 0
fi
git commit -m "docs: add ${MAJOR} to versions.json and blog"
git push --force origin "$BRANCH"
SHA="$(git rev-parse HEAD)"
PR_URL=$(gh pr create --base main --head "$BRANCH" \
--title "docs: add ${MAJOR} to versions.json and blog" \
--body "Automated versioned-docs snapshot metadata for \`${GITHUB_REF_NAME}\`." \
2>&1) || PR_URL=$(gh pr view "$BRANCH" --json url -q .url)
echo "Opened $PR_URL"
gh workflow run ci.yaml --ref "$BRANCH"
RUN_ID=""
for _ in $(seq 1 10); do
# Filter by headSha, not just the newest run for this branch — a
# retry (or an earlier attempt reusing this branch name) can leave
# a stale, already-green run around whose SHA no longer matches
# what was just pushed. See review on PR #215.
# Note: gh's --jq takes a single expression string — it has no
# --arg support like the jq binary, so interpolate $SHA directly
# (safe: it's the output of `git rev-parse HEAD`, not user input).
RUN_ID=$(gh run list --workflow=ci.yaml --branch="$BRANCH" \
--event=workflow_dispatch --limit=10 --json databaseId,headSha \
--jq "[.[] | select(.headSha == \"${SHA}\")] | .[0].databaseId // empty")
[ -n "$RUN_ID" ] && break
sleep 3
done
if [ -z "$RUN_ID" ]; then
echo "::error::Could not find the dispatched CI run for $BRANCH at $SHA" >&2
exit 1
fi
gh run watch "$RUN_ID" --exit-status
gh pr merge "$BRANCH" --squash
# This merge is itself GITHUB_TOKEN-authored, so the resulting push
# to main won't trigger docs.yaml's `push` trigger either — dispatch
# it explicitly so the deploy job picks up the new snapshot now,
# instead of waiting for the next unrelated docs change to main.
gh workflow run docs.yaml --ref main