1- name : Build and Deploy
1+ name : Build, Deploy, and On-Demand Preview
22
3- on :
3+ " on " :
44 push :
55 branches :
66 - main
77 pull_request :
88 branches :
99 - main
10+ issue_comment :
11+ types :
12+ - created
1013
11- permissions :
12- contents : read
13- pages : write
14- id-token : write
15-
16- concurrency :
17- group : " pages"
18- cancel-in-progress : false
14+ env :
15+ PREVIEW_ROOT : pr-preview
1916
2017jobs :
21- build :
18+ build-pr :
19+ if : github.event_name == 'pull_request'
20+ runs-on : ubuntu-latest
21+ permissions :
22+ contents : read
23+ steps :
24+ - name : Checkout
25+ uses : actions/checkout@v4
26+ with :
27+ persist-credentials : false
28+
29+ - name : Install uv
30+ uses : astral-sh/setup-uv@v2
31+
32+ - name : Install dependencies
33+ run : uv sync
34+
35+ - name : Build site
36+ run : uv run mkdocs build --strict
37+
38+ deploy-main :
39+ if : github.event_name == 'push' && github.ref == 'refs/heads/main'
2240 runs-on : ubuntu-latest
41+ permissions :
42+ contents : write
43+ concurrency :
44+ group : gh-pages
45+ cancel-in-progress : false
2346 steps :
2447 - name : Checkout
2548 uses : actions/checkout@v4
49+ with :
50+ persist-credentials : false
51+
52+ - name : Install uv
53+ uses : astral-sh/setup-uv@v2
54+
55+ - name : Install dependencies
56+ run : uv sync
57+
58+ - name : Build site
59+ run : uv run mkdocs build --strict
60+
61+ - name : Checkout gh-pages
62+ env :
63+ GH_TOKEN : ${{ github.token }}
64+ run : |
65+ set -euo pipefail
66+
67+ git init gh-pages
68+ cd gh-pages
69+ git remote add origin "https://x-access-token:${GH_TOKEN}@github.com/${GITHUB_REPOSITORY}.git"
70+
71+ if git ls-remote --exit-code --heads origin gh-pages > /dev/null; then
72+ git fetch --depth=1 origin gh-pages
73+ git checkout -B gh-pages FETCH_HEAD
74+ else
75+ git checkout --orphan gh-pages
76+ fi
77+
78+ - name : Deploy main site to gh-pages
79+ run : |
80+ set -euo pipefail
81+
82+ find gh-pages -mindepth 1 -maxdepth 1 \
83+ ! -name .git \
84+ ! -name "${PREVIEW_ROOT}" \
85+ -exec rm -rf {} +
86+ cp -a site/. gh-pages/
87+
88+ cd gh-pages
89+ git config user.name "github-actions[bot]"
90+ git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
91+ git add --all
92+
93+ if git diff --cached --quiet; then
94+ echo "No gh-pages changes to deploy."
95+ else
96+ git commit -m "Deploy main site"
97+ git push origin HEAD:gh-pages
98+ fi
99+
100+ preview-command :
101+ if : >-
102+ github.event_name == 'issue_comment' &&
103+ github.event.issue.pull_request &&
104+ contains(fromJSON('["OWNER", "MEMBER", "COLLABORATOR"]'), github.event.comment.author_association) &&
105+ (
106+ startsWith(github.event.comment.body, '/preview') ||
107+ startsWith(github.event.comment.body, '/unpreview')
108+ )
109+ runs-on : ubuntu-latest
110+ permissions :
111+ contents : read
112+ pull-requests : read
113+ outputs :
114+ action : ${{ steps.preview.outputs.action }}
115+ head_sha : ${{ steps.preview.outputs.head_sha }}
116+ pr_number : ${{ steps.preview.outputs.pr_number }}
117+ preview_url : ${{ steps.preview.outputs.preview_url }}
118+ steps :
119+ - name : Read command and PR metadata
120+ id : preview
121+ env :
122+ GH_TOKEN : ${{ github.token }}
123+ PR_NUMBER : ${{ github.event.issue.number }}
124+ run : |
125+ set -euo pipefail
126+
127+ body="$(jq -r '.comment.body' "$GITHUB_EVENT_PATH" | tr -d '\r')"
128+ body="$(printf '%s' "$body" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')"
129+
130+ case "$body" in
131+ /preview)
132+ action=deploy
133+ ;;
134+ /unpreview)
135+ action=remove
136+ ;;
137+ *)
138+ action=ignore
139+ ;;
140+ esac
141+
142+ repo_name="${GITHUB_REPOSITORY#*/}"
143+ if [ "$repo_name" = "${GITHUB_REPOSITORY_OWNER}.github.io" ]; then
144+ preview_url="https://${repo_name}/${PREVIEW_ROOT}/pr-${PR_NUMBER}/"
145+ else
146+ preview_url="https://${GITHUB_REPOSITORY_OWNER}.github.io/${repo_name}/${PREVIEW_ROOT}/pr-${PR_NUMBER}/"
147+ fi
148+ {
149+ echo "action=${action}"
150+ echo "pr_number=${PR_NUMBER}"
151+ echo "preview_url=${preview_url}"
152+ } >> "$GITHUB_OUTPUT"
153+
154+ if [ "$action" != "deploy" ]; then
155+ exit 0
156+ fi
157+
158+ pr_json="$(gh api "repos/${GITHUB_REPOSITORY}/pulls/${PR_NUMBER}")"
159+ head_repo="$(printf '%s' "$pr_json" | jq -r '.head.repo.full_name')"
160+ base_repo="$(printf '%s' "$pr_json" | jq -r '.base.repo.full_name')"
161+ head_sha="$(printf '%s' "$pr_json" | jq -r '.head.sha')"
162+
163+ if [ "$head_repo" != "$base_repo" ]; then
164+ echo "::error::PR previews are only supported for branches in this repository."
165+ exit 1
166+ fi
167+
168+ if [ -z "$head_sha" ] || [ "$head_sha" = "null" ]; then
169+ echo "::error::Could not determine the PR head SHA."
170+ exit 1
171+ fi
172+
173+ echo "head_sha=${head_sha}" >> "$GITHUB_OUTPUT"
174+
175+ build-preview :
176+ if : needs.preview-command.outputs.action == 'deploy'
177+ needs : preview-command
178+ runs-on : ubuntu-latest
179+ permissions :
180+ contents : read
181+ steps :
182+ - name : Checkout PR head
183+ uses : actions/checkout@v4
184+ with :
185+ ref : ${{ needs.preview-command.outputs.head_sha }}
186+ persist-credentials : false
26187
27188 - name : Install uv
28189 uses : astral-sh/setup-uv@v2
@@ -33,19 +194,143 @@ jobs:
33194 - name : Build site
34195 run : uv run mkdocs build --strict
35196
36- - name : Upload artifact
37- uses : actions/upload-pages- artifact@v3
197+ - name : Upload preview artifact
198+ uses : actions/upload-artifact@v4
38199 with :
200+ name : pr-preview-${{ needs.preview-command.outputs.pr_number }}
39201 path : site
202+ if-no-files-found : error
40203
41- deploy :
42- if : github.event_name == 'push' && github.ref == 'refs/heads/main'
43- needs : build
204+ deploy-preview :
205+ if : needs.preview-command.outputs.action == 'deploy'
206+ needs :
207+ - preview-command
208+ - build-preview
209+ runs-on : ubuntu-latest
210+ permissions :
211+ contents : write
212+ issues : write
213+ pull-requests : write
214+ concurrency :
215+ group : gh-pages
216+ cancel-in-progress : false
217+ steps :
218+ - name : Checkout gh-pages
219+ env :
220+ GH_TOKEN : ${{ github.token }}
221+ run : |
222+ set -euo pipefail
223+
224+ git init gh-pages
225+ cd gh-pages
226+ git remote add origin "https://x-access-token:${GH_TOKEN}@github.com/${GITHUB_REPOSITORY}.git"
227+
228+ if git ls-remote --exit-code --heads origin gh-pages > /dev/null; then
229+ git fetch --depth=1 origin gh-pages
230+ git checkout -B gh-pages FETCH_HEAD
231+ else
232+ git checkout --orphan gh-pages
233+ fi
234+
235+ - name : Download preview artifact
236+ uses : actions/download-artifact@v4
237+ with :
238+ name : pr-preview-${{ needs.preview-command.outputs.pr_number }}
239+ path : preview-site
240+
241+ - name : Deploy PR preview
242+ env :
243+ PR_NUMBER : ${{ needs.preview-command.outputs.pr_number }}
244+ run : |
245+ set -euo pipefail
246+
247+ preview_dir="gh-pages/${PREVIEW_ROOT}/pr-${PR_NUMBER}"
248+ rm -rf "$preview_dir"
249+ mkdir -p "$preview_dir"
250+ cp -a preview-site/. "$preview_dir"/
251+
252+ cd gh-pages
253+ git config user.name "github-actions[bot]"
254+ git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
255+ git add --all "${PREVIEW_ROOT}"
256+
257+ if git diff --cached --quiet; then
258+ echo "No preview changes to deploy."
259+ else
260+ git commit -m "Deploy PR ${PR_NUMBER} preview"
261+ git push origin HEAD:gh-pages
262+ fi
263+
264+ - name : Comment with preview URL
265+ env :
266+ GH_TOKEN : ${{ github.token }}
267+ PR_NUMBER : ${{ needs.preview-command.outputs.pr_number }}
268+ PREVIEW_URL : ${{ needs.preview-command.outputs.preview_url }}
269+ run : |
270+ set -euo pipefail
271+
272+ marker="<!-- pr-preview:${PR_NUMBER} -->"
273+ body="$(printf '%s\n%s\n' "$marker" "Preview published: ${PREVIEW_URL}")"
274+ gh pr comment "$PR_NUMBER" --repo "$GITHUB_REPOSITORY" --body "$body"
275+
276+ remove-preview :
277+ if : needs.preview-command.outputs.action == 'remove'
278+ needs : preview-command
44279 runs-on : ubuntu-latest
45- environment :
46- name : github-pages
47- url : ${{ steps.deployment.outputs.page_url }}
280+ permissions :
281+ contents : write
282+ issues : write
283+ pull-requests : write
284+ concurrency :
285+ group : gh-pages
286+ cancel-in-progress : false
48287 steps :
49- - name : Deploy to GitHub Pages
50- id : deployment
51- uses : actions/deploy-pages@v4
288+ - name : Checkout gh-pages
289+ env :
290+ GH_TOKEN : ${{ github.token }}
291+ run : |
292+ set -euo pipefail
293+
294+ git init gh-pages
295+ cd gh-pages
296+ git remote add origin "https://x-access-token:${GH_TOKEN}@github.com/${GITHUB_REPOSITORY}.git"
297+
298+ if git ls-remote --exit-code --heads origin gh-pages > /dev/null; then
299+ git fetch --depth=1 origin gh-pages
300+ git checkout -B gh-pages FETCH_HEAD
301+ else
302+ git checkout --orphan gh-pages
303+ fi
304+
305+ - name : Remove PR preview
306+ env :
307+ PR_NUMBER : ${{ needs.preview-command.outputs.pr_number }}
308+ run : |
309+ set -euo pipefail
310+
311+ preview_dir="gh-pages/${PREVIEW_ROOT}/pr-${PR_NUMBER}"
312+ rm -rf "$preview_dir"
313+ mkdir -p "gh-pages/${PREVIEW_ROOT}"
314+
315+ cd gh-pages
316+ git config user.name "github-actions[bot]"
317+ git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
318+ git add --all "${PREVIEW_ROOT}"
319+
320+ if git diff --cached --quiet; then
321+ echo "No preview changes to remove."
322+ else
323+ git commit -m "Remove PR ${PR_NUMBER} preview"
324+ git push origin HEAD:gh-pages
325+ fi
326+
327+ - name : Comment that preview was removed
328+ env :
329+ GH_TOKEN : ${{ github.token }}
330+ PR_NUMBER : ${{ needs.preview-command.outputs.pr_number }}
331+ run : |
332+ set -euo pipefail
333+
334+ marker="<!-- pr-preview:${PR_NUMBER} -->"
335+ body="$(printf '%s\n%s\n' "$marker" "Preview removed for PR #${PR_NUMBER}.")"
336+ gh pr comment "$PR_NUMBER" --repo "$GITHUB_REPOSITORY" --body "$body"
0 commit comments