-
-
Notifications
You must be signed in to change notification settings - Fork 71
230 lines (210 loc) · 9.47 KB
/
Copy pathbuild-production-site.yml
File metadata and controls
230 lines (210 loc) · 9.47 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
---
# Build the production website and publish it as a checksummed tarball on a
# per-deploy GitHub release, for deployment on grass.osgeo.org.
#
# The production server does not build the site. Its cronjob
# (grass-addons: utils/cronjobs_osgeo_lxd/hugo_clean_and_update_job.sh)
# downloads the tarball from the release marked "latest", verifies the
# checksum, and rsyncs it into the web root. This keeps the Hugo Extended +
# Dart Sass + Node toolchain pinned in CI only, so nothing has to be installed
# or upgraded on the server. Old releases are pruned to a bounded history, and
# a bad build can be rolled back by re-marking an older release as latest.
#
# Two jobs: "build" produces the tarball and hands it over as a workflow
# artifact; "deploy" (needs: build) publishes it to the release. A failed
# build therefore never touches the published assets, and only the deploy
# job holds a writable token.
name: Build production site artifact
on:
# Runs on pushes targeting the default branch
push:
branches: ["master"]
# Runs the build job on pull requests as a merge gate; the deploy job is
# skipped for pull requests (see its `if` below).
pull_request:
branches: ["master"]
# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:
# Each job declares exactly what it needs.
permissions: {}
concurrency:
group: >-
production-site-${{ github.event_name == 'pull_request' &&
github.head_ref || github.ref }}
# Cancel in progress in pull requests.
# Otherwise, limit to one in progress and one queued for each ref.
# Only the latest queued run per ref is kept, older ones are cancelled.
# The already running publish is always allowed to complete, so a deploy is
# never killed mid-upload; the freshness gate in the deploy job then makes
# sure only the newest commit publishes.
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
defaults:
run:
shell: bash
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: read
env:
# Toolchain version pins, kept current by Renovate (see the custom
# manager in renovate.json that matches these annotations).
# renovate: datasource=github-releases depName=gohugoio/hugo
HUGO_VERSION: 0.113.0
# renovate: datasource=github-releases depName=sass/dart-sass
DART_SASS_VERSION: 1.101.0
steps:
- name: Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- name: Install Hugo CLI (extended)
env:
TEMP_DIR: ${{ runner.temp }}
run: |
# Hugo release assets have used two naming schemes; try both.
base="https://github.com/gohugoio/hugo/releases/download/v${HUGO_VERSION}"
wget -O "$TEMP_DIR/hugo.deb" "$base/hugo_extended_${HUGO_VERSION}_Linux-64bit.deb" \
|| wget -O "$TEMP_DIR/hugo.deb" "$base/hugo_extended_${HUGO_VERSION}_linux-amd64.deb"
sudo dpkg -i "$TEMP_DIR/hugo.deb"
# Pinned for reproducibility. Note: with Hugo 0.113 the site compiles via
# Hugo Extended's built-in libsass (head.html sets no "transpiler"
# option), so this binary is not exercised yet; it is pinned now so the
# planned Hugo/Dart Sass upgrade inherits a reproducible toolchain.
- name: Install Dart Sass
env:
TEMP_DIR: ${{ runner.temp }}
run: |
curl -fsSL -o "$TEMP_DIR/dart-sass.tar.gz" \
"https://github.com/sass/dart-sass/releases/download/${DART_SASS_VERSION}/dart-sass-${DART_SASS_VERSION}-linux-x64.tar.gz"
tar -xzf "$TEMP_DIR/dart-sass.tar.gz" -C "$TEMP_DIR"
echo "$TEMP_DIR/dart-sass" >> "$GITHUB_PATH"
- name: Setup Node.js
if: ${{ hashFiles('.nvmrc') != '' }}
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version-file: .nvmrc
# Skipped when no lockfile exists (pre-migration master); fails the job
# if the install fails, so a broken artifact is never published.
- name: Install Node.js dependencies
if: ${{ hashFiles('package-lock.json', 'npm-shrinkwrap.json') != '' }}
run: npm ci
- name: Build with Hugo
env:
# For maximum backward compatibility with Hugo modules
HUGO_ENVIRONMENT: production
HUGO_ENV: production
run: hugo --minify
- name: Record build info
run: |
{
echo "commit: ${GITHUB_SHA}"
echo "built: $(date -u +%Y-%m-%dT%H:%M:%SZ)"
echo "run: ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}"
} > public/BUILD_INFO
- name: Package and checksum
run: |
tar -czf grass-website.tar.gz -C public .
sha256sum grass-website.tar.gz > grass-website.tar.gz.sha256
# Handoff to the deploy job; short retention, this is not the published
# copy (the release assets are).
- name: Upload build for the deploy job
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: production-site
path: |
grass-website.tar.gz
grass-website.tar.gz.sha256
retention-days: 1
if-no-files-found: error
deploy:
# Pull requests only validate the build; publishing happens only from the
# default branch (pushes or manual dispatch on master). The environment
# adds platform-level enforcement on top: its deployment branch policy
# should allow master only, and protection rules (required reviewers) can
# gate publishes with approvals; both are configured in the repository
# settings under Environments.
if: >-
${{ github.event_name != 'pull_request' &&
github.ref == 'refs/heads/master' }}
runs-on: ubuntu-latest
needs: build
environment:
name: production
url: https://grass.osgeo.org/
permissions:
contents: write
steps:
# Only the newest commit of the ref may publish. If the ref moved while
# this run was building or queued, a newer run owns the publish and this
# one skips (as a success, not a failure).
- name: Check this run is still the newest commit
id: freshness
env:
GH_TOKEN: ${{ github.token }}
run: |
head_sha="$(gh api "repos/$GITHUB_REPOSITORY/git/ref/heads/$GITHUB_REF_NAME" --jq '.object.sha')"
if [[ "$head_sha" == "$GITHUB_SHA" ]]; then
echo "current=true" >> "$GITHUB_OUTPUT"
else
echo "current=false" >> "$GITHUB_OUTPUT"
echo "::notice::Skipping publish: $GITHUB_REF_NAME moved to ${head_sha};" \
"a newer run owns the publish (this run built ${GITHUB_SHA})."
fi
- name: Download build
if: ${{ steps.freshness.outputs.current == 'true' }}
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: production-site
- name: Verify checksum
if: ${{ steps.freshness.outputs.current == 'true' }}
run: sha256sum -c grass-website.tar.gz.sha256
# Each deploy is its own immutable, calendar-dated release marked as
# "latest". The server downloads from the releases/latest/download
# shortcut, so a rollback is just re-marking an older release as latest
# ("gh release edit <tag> --latest"). Asset names are stable so the
# server always requests the same files.
- name: Publish a versioned production release
if: ${{ steps.freshness.outputs.current == 'true' }}
env:
GH_TOKEN: ${{ github.token }}
run: |
short_sha="${GITHUB_SHA::7}"
tag="site-$(date -u +'%Y%m%d-%H%M%S')-${short_sha}"
title="Production site $(date -u +'%Y-%m-%d') (${short_sha})"
{
echo "Automated production build of grass.osgeo.org from ${short_sha}."
echo
echo "The server deploys the release marked \"latest\"; see BUILD_INFO"
echo "inside the tarball for provenance. Add \`[keep]\` to a release"
echo "title to exempt it from automatic pruning."
} > release-notes.md
gh release create "$tag" \
--repo "$GITHUB_REPOSITORY" \
--target "$GITHUB_SHA" \
--title "$title" \
--notes-file release-notes.md \
--latest \
grass-website.tar.gz grass-website.tar.gz.sha256
# Bounded retention: keep the newest $KEEP builds plus any pinned with
# [keep] in the title, delete the rest (with their tags). This answers
# "how do old release assets get cleaned up".
- name: Prune old production releases
if: ${{ steps.freshness.outputs.current == 'true' }}
env:
GH_TOKEN: ${{ github.token }}
KEEP: "10"
run: |
mapfile -t stale < <(
gh release list --repo "$GITHUB_REPOSITORY" --limit 500 \
--json tagName,name,createdAt \
| jq -r --argjson keep "$KEEP" '
[.[] | select(.tagName | startswith("site-"))]
| sort_by(.createdAt) | reverse | .[$keep:]
| .[] | select(.name | test("\\[keep\\]") | not) | .tagName'
)
for tag in "${stale[@]}"; do
echo "Pruning old release $tag"
gh release delete "$tag" --repo "$GITHUB_REPOSITORY" --cleanup-tag --yes
done
echo "Pruned ${#stale[@]} release(s); kept newest $KEEP plus any [keep]."