1- # SPDX-License-Identifier: MPL-2.0
21# This workflow is managed by gh actions-lock.
2+ # SPDX-License-Identifier: MPL-2.0
33# This workflow is managed by gh actions-lock.
44name : GitHub Pages
55
66on :
77 push :
88 branches : [main, master]
9+ pull_request :
910 workflow_dispatch :
1011
1112permissions :
12- actions : read
1313 contents : read
14- pages : write
15- id-token : write
1614
1715concurrency :
18- group : " pages"
19- cancel-in-progress : false
16+ group : " pages-${{ github.event_name }}-${{ github.ref }} "
17+ cancel-in-progress : ${{ github.event_name == 'pull_request' }}
2018
2119jobs :
2220 build :
23- runs-on : ubuntu-latest
21+ name : Build Pages artifact
22+ runs-on : ubuntu-24.04
2423 timeout-minutes : 30
24+ permissions :
25+ contents : read
2526 steps :
2627 - name : Checkout
2728 uses : actions/checkout@v7.0.1
29+ with :
30+ persist-credentials : false
2831
2932 - name : Checkout casket-ssg
3033 uses : actions/checkout@v7.0.1
3134 with :
3235 repository : hyperpolymath/casket-ssg
36+ ref : cec3c20d80ea1dc93660b69a4e7b38aa49f2a56b # standalone build; optional liblol bridge
3337 path : .casket-ssg
34-
35- - name : Setup GHCup
36- uses : haskell-actions/setup@v2.12.0
37- with :
38- ghc-version : ' 9.8.2'
39- cabal-version : ' 3.10'
38+ persist-credentials : false
4039
4140 - name : Cache Cabal
4241 uses : actions/cache@v6.1.0
@@ -47,12 +46,28 @@ jobs:
4746 .casket-ssg/dist-newstyle
4847 key : ${{ runner.os }}-casket-${{ hashFiles('.casket-ssg/casket-ssg.cabal') }}
4948
49+ # Explicitly allowed by repository policy; actions.lock pins its commit.
50+ - name : Select tested Haskell toolchain
51+ uses : haskell-actions/setup@v2.12.0
52+ with :
53+ ghc-version : ' 9.6.6'
54+ cabal-version : ' 3.10.3.0'
55+
56+ - name : Prepare runner Haskell toolchain
57+ run : |
58+ set -euo pipefail
59+ ghc --version
60+ cabal --version
61+ cabal update
62+
5063 - name : Build casket-ssg
5164 working-directory : .casket-ssg
52- run : cabal build
65+ run : cabal build --index-state=2026-09-06T00:00:00Z
5366
5467 - name : Prepare site source
5568 shell : bash
69+ env :
70+ CONTENT_REF : ${{ github.event_name == 'pull_request' && github.sha || github.ref_name }}
5671 run : |
5772 set -euo pipefail
5873 rm -rf .site-src _site
6782 README_URL=""
6883
6984 if [ -f README.md ]; then
70- README_URL="${REPO_URL}/blob/${{ github.ref_name } }/README.md"
85+ README_URL="${REPO_URL}/blob/${CONTENT_REF }/README.md"
7186 elif [ -f README.adoc ]; then
72- README_URL="${REPO_URL}/blob/${{ github.ref_name } }/README.adoc"
87+ README_URL="${REPO_URL}/blob/${CONTENT_REF }/README.adoc"
7388 fi
7489
7590 {
87102 echo "- README: [project README](${README_URL})"
88103 fi
89104 if [ -d docs ]; then
90- echo "- Docs directory: [docs/](${REPO_URL}/tree/${{ github.ref_name } }/docs)"
105+ echo "- Docs directory: [docs/](${REPO_URL}/tree/${CONTENT_REF }/docs)"
91106 fi
92107 echo
93108 echo "Project-specific site content can be added later under site/."
97112 - name : Build site
98113 run : |
99114 mkdir -p _site
100- cd .casket-ssg && cabal run casket-ssg -- build ../.site-src ../_site
115+ cd .casket-ssg && cabal run --index-state=2026-09-06T00:00:00Z casket-ssg -- build ../.site-src ../_site
101116 touch ../_site/.nojekyll
102117
103118 - name : Setup Pages
@@ -108,13 +123,83 @@ jobs:
108123 with :
109124 path : ' _site'
110125
126+ preview :
127+ name : Validate Pages artifact
128+ if : github.event_name == 'pull_request'
129+ runs-on : ubuntu-24.04
130+ needs : build
131+ timeout-minutes : 10
132+ permissions :
133+ actions : read
134+ contents : read
135+ steps :
136+ - name : Download Pages artifact
137+ uses : actions/download-artifact@v8.0.1
138+ with :
139+ name : github-pages
140+ path : .pages-preview
141+
142+ - name : Validate deployable artifact
143+ shell : bash
144+ run : |
145+ set -euo pipefail
146+
147+ artifact=".pages-preview/artifact.tar"
148+ entries_file="${RUNNER_TEMP}/pages-preview-entries.txt"
149+
150+ if [ ! -s "${artifact}" ]; then
151+ echo "::error::Pages artifact is absent or empty"
152+ exit 1
153+ fi
154+
155+ tar -tf "${artifact}" > "${entries_file}"
156+
157+ entry_count=0
158+ has_index=0
159+ while IFS= read -r entry; do
160+ entry_count=$((entry_count + 1))
161+ case "${entry}" in
162+ /*|../*|*/../*|*/..)
163+ echo "::error::Pages artifact contains an unsafe path: ${entry}"
164+ exit 1
165+ ;;
166+ index.html|./index.html)
167+ has_index=1
168+ ;;
169+ esac
170+ done < "${entries_file}"
171+
172+ if [ "${entry_count}" -eq 0 ]; then
173+ echo "::error::Pages artifact contains no files"
174+ exit 1
175+ fi
176+
177+ if [ "${has_index}" -ne 1 ]; then
178+ echo "::error::Pages artifact contains no index.html"
179+ exit 1
180+ fi
181+
182+ {
183+ echo "### Pages preview artifact"
184+ echo
185+ echo "- Files: ${entry_count}"
186+ echo "- SHA-256: \`$(sha256sum "${artifact}" | awk '{print $1}')\`"
187+ echo "- Production deployment: intentionally deferred until merge"
188+ } >> "${GITHUB_STEP_SUMMARY}"
189+
111190 deploy :
191+ name : Deploy production Pages site
192+ if : github.event_name != 'pull_request'
112193 environment :
113194 name : github-pages
114195 url : ${{ steps.deployment.outputs.page_url }}
115- runs-on : ubuntu-latest
196+ runs-on : ubuntu-24.04
116197 needs : build
117198 timeout-minutes : 10
199+ permissions :
200+ contents : read
201+ pages : write
202+ id-token : write
118203 steps :
119204 - name : Deploy to GitHub Pages
120205 id : deployment
0 commit comments