main
yml 189 lines 8.17 KB
Raw
1 name: Site Preview (redesign PRs)
2
3 # Preview gate for web / look-and-feel PRs (issue #354).
4 # Builds the Hugo site the same way as deploy-site.yml, then publishes the
5 # rendered site as a downloadable artifact and posts a PR comment explaining
6 # how reviewers and the operator can inspect the look-and-feel before merge.
7 #
8 # Security notes:
9 # - Triggered by `pull_request` (NOT pull_request_target).
10 # - The build job, which executes untrusted PR templates/layouts, runs with
11 # `contents: read` only and has no access to secrets.
12 # - The comment job does NOT check out PR code; it only posts a comment with
13 # `pull-requests: write` plus `issues: write` (PR comments go through the
14 # Issues API), keeping the privileged token away from PR content.
15 # - All actions are pinned to a full commit SHA; checkout uses
16 # persist-credentials: false; no `${{ }}` interpolation inside run blocks.
17
18 on:
19 pull_request:
20 paths:
21 - 'layouts/**'
22 - 'assets/**'
23 - 'themes/**'
24 - 'content/**'
25 - 'static/**'
26 - 'config/**'
27 - 'hugo.toml'
28 - '.github/workflows/site-preview.yml'
29
30 # Least privilege at the top level; jobs widen only what they need.
31 permissions:
32 contents: read # only needed to read repository contents for the build
33
34 concurrency:
35 group: site-preview-${{ github.event.pull_request.number }}
36 cancel-in-progress: true
37
38 jobs:
39 build-preview:
40 name: Build preview site
41 runs-on: ubuntu-latest
42 permissions:
43 contents: read # checkout PR sources; no secrets exposed to PR templates
44 env:
45 HUGO_VERSION: 0.161.1
46 # Relative root base URL so the bundle is portable; reviewers serve it
47 # from a local web server (see the PR comment for instructions).
48 PREVIEW_BASEURL: "/"
49 steps:
50 - name: Check out repository
51 uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
52 with:
53 fetch-depth: 0
54 submodules: recursive
55 persist-credentials: false
56
57 - name: Set up Node.js
58 uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
59 with:
60 node-version: '24'
61
62 - name: Install Hugo
63 run: |
64 set -euo pipefail
65 RELEASE_URL="https://github.com/gohugoio/hugo/releases/download/v${HUGO_VERSION}"
66 TARBALL="hugo_extended_${HUGO_VERSION}_linux-amd64.tar.gz"
67 CHECKSUM_FILE="hugo_${HUGO_VERSION}_checksums.txt"
68 curl --fail --silent --show-error --location --retry 10 --retry-delay 5 --retry-max-time 300 --retry-all-errors \
69 --output "${TARBALL}" "${RELEASE_URL}/${TARBALL}"
70
71 # Download and verify checksums (same hardening as deploy-site.yml).
72 curl --fail --silent --show-error --location --retry 10 --retry-delay 5 --retry-max-time 300 --retry-all-errors \
73 --output "${CHECKSUM_FILE}" "${RELEASE_URL}/${CHECKSUM_FILE}"
74 checksum_line="$(awk -v file="${TARBALL}" '$NF == file {print; found=1} END {if (!found) exit 1}' "${CHECKSUM_FILE}")" || {
75 echo "Error: No checksum entry for ${TARBALL} found in ${CHECKSUM_FILE}" >&2
76 exit 1
77 }
78 if ! printf '%s\n' "${checksum_line}" | sha256sum --check; then
79 echo "Error: Checksum verification failed for ${TARBALL}" >&2
80 exit 1
81 fi
82 rm "${CHECKSUM_FILE}"
83
84 mkdir -p "${HOME}/.local/hugo"
85 tar -C "${HOME}/.local/hugo" -xf "${TARBALL}"
86 rm "${TARBALL}"
87 echo "${HOME}/.local/hugo" >> "${GITHUB_PATH}"
88 export PATH="${HOME}/.local/hugo:${PATH}"
89 hugo version
90
91 - name: Build preview site
92 # Same minify flag as production, but with a preview baseURL and with
93 # drafts + future-dated content included so reviewers see everything.
94 run: |
95 set -euo pipefail
96 hugo --gc --minify --buildDrafts --buildFuture --baseURL "${PREVIEW_BASEURL}"
97
98 - name: Build Pagefind search index
99 run: npx pagefind --site public/
100
101 - name: Upload preview site artifact
102 uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
103 with:
104 name: site-preview
105 path: ./public
106 retention-days: 14
107 if-no-files-found: error
108
109 - name: Write preview summary
110 run: |
111 set -euo pipefail
112 {
113 echo "## 🔎 Site preview built"
114 echo ""
115 echo "Hugo extended **${HUGO_VERSION}** build succeeded (drafts + future content included)."
116 echo ""
117 echo "Download the **\`site-preview\`** artifact from this run, unzip it, then serve it:"
118 echo ""
119 echo '```bash'
120 echo "unzip site-preview.zip -d site-preview && cd site-preview"
121 echo "python3 -m http.server 8080 # then open http://localhost:8080/"
122 echo '```'
123 echo ""
124 echo "### Reviewer checklist"
125 echo "- [ ] Homepage renders correctly"
126 echo "- [ ] Latest weekly article renders correctly"
127 echo "- [ ] Representative archive / rollup (monthly, yearly, topics) pages render"
128 echo "- [ ] Navigation menu, search page, and footer look correct"
129 echo "- [ ] Light + dark themes both look correct"
130 echo "- [ ] Mobile widths checked: 320 / 360 / 390 / 414 / 768 px"
131 echo "- [ ] Screenshots attached to the PR for the operator / Fry / Calculon review"
132 } >> "${GITHUB_STEP_SUMMARY}"
133
134 comment:
135 name: Post preview instructions
136 needs: build-preview
137 runs-on: ubuntu-latest
138 permissions:
139 pull-requests: write # post/update the preview instructions comment
140 issues: write # PR comments are created via the Issues API (listComments/createComment/updateComment)
141 steps:
142 - name: Post or update preview comment
143 uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
144 env:
145 RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
146 with:
147 script: |
148 const marker = '<!-- site-preview-bot -->';
149 const runUrl = process.env.RUN_URL;
150 const prNumber = context.payload.pull_request.number;
151 const body = [
152 marker,
153 '## 🔎 Site preview ready',
154 '',
155 'A rendered preview of this PR was built with Hugo extended (drafts + future content included).',
156 '',
157 `**Download it:** open the [workflow run](${runUrl}) → **Artifacts** → \`site-preview\`.`,
158 '',
159 'Then serve and open it locally:',
160 '',
161 '```bash',
162 'unzip site-preview.zip -d site-preview && cd site-preview',
163 'python3 -m http.server 8080 # then open http://localhost:8080/',
164 '```',
165 '',
166 '_(Links are root-relative, so open it via a local web server rather than `file://`.)_',
167 '',
168 '### Reviewer checklist',
169 '- [ ] Homepage renders correctly',
170 '- [ ] Latest weekly article renders correctly',
171 '- [ ] Representative archive / rollup (monthly, yearly, topics) pages render',
172 '- [ ] Navigation menu, search page, and footer look correct',
173 '- [ ] Light + dark themes both look correct',
174 '- [ ] Mobile widths checked: 320 / 360 / 390 / 414 / 768 px',
175 '- [ ] Screenshots attached for the operator / Fry / Calculon review',
176 ].join('\n');
177
178 const { owner, repo } = context.repo;
179 const comments = await github.paginate(github.rest.issues.listComments, {
180 owner, repo, issue_number: prNumber, per_page: 100,
181 });
182 const existing = comments.find(c => c.body && c.body.includes(marker));
183 if (existing) {
184 await github.rest.issues.updateComment({ owner, repo, comment_id: existing.id, body });
185 core.info(`Updated preview comment ${existing.id}`);
186 } else {
187 await github.rest.issues.createComment({ owner, repo, issue_number: prNumber, body });
188 core.info('Created preview comment');
189 }