-
Notifications
You must be signed in to change notification settings - Fork 21
300 lines (281 loc) · 11.7 KB
/
Copy pathrelease.yml
File metadata and controls
300 lines (281 loc) · 11.7 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
name: Release
# Triggered ONLY on annotated tag push matching `v*.*.*`. The
# expected sequence is:
#
# 1. Commit + push CHANGELOG bump on main (manual).
# 2. `git tag -a vX.Y.Z -m "..."` against that commit (manual).
# 3. `git push origin vX.Y.Z` — this workflow fires.
#
# We do NOT re-run unit tests here; the tag must already point at a
# commit that's green on the `ci.yml` workflow. Re-testing would
# double the wall-clock for tag bumps without catching anything CI
# wouldn't.
on:
push:
tags: ['v*.*.*']
permissions:
contents: write
packages: write
# `actions/*` and `docker/*` shipped on Node 20 still work today, but
# GitHub's deprecation timer fires 2026-06-02 — they'll be force-
# migrated to Node 24 on the runner anyway. Opt in early so a
# mid-release runner update doesn't break a tag push, and so the
# workflow log stops emitting the annotation noise that drowned the
# v1.0.0 run.
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: 'true'
jobs:
# -------------------------------------------------------------------
# Per-platform Docker image build. 2×2 matrix (component × platform);
# arm64 runs on a NATIVE arm64 runner instead of QEMU emulation —
# the v1.0.0 release spent 1h24m on a Rust+LTO+QEMU arm64 link
# which native runners drop to ~10 min. Each job pushes BY DIGEST
# only (no human-readable tag); the merge job below combines the
# digests into the final `:version` + `:latest` manifest list.
# -------------------------------------------------------------------
build-images:
name: Build · ${{ matrix.component }} · ${{ matrix.platform }}
runs-on: ${{ matrix.runner }}
strategy:
fail-fast: false
matrix:
component: [server, web]
platform: [linux/amd64, linux/arm64]
include:
- platform: linux/amd64
runner: ubuntu-latest
- platform: linux/arm64
runner: ubuntu-24.04-arm
steps:
- uses: actions/checkout@v4
- name: Reclaim runner disk
# `Dockerfile.server` links the entire workspace under LTO +
# codegen-units=1 + strip — the default runner ~14 GiB
# freespace fills mid-link. Same trimming as ci.yml's rust job.
run: |
sudo rm -rf /usr/share/dotnet /usr/local/lib/android /opt/ghc \
/opt/hostedtoolcache/CodeQL /usr/local/.ghcup
df -h /
- name: Set up Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to GHCR
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Resolve image name + platform pair
id: meta
# `platform_pair` is the platform name munged for use as an
# artifact suffix — `actions/upload-artifact@v4` rejects `/`.
# `image` lowercases the owner because GHCR is case-sensitive
# on path segments and the Helm values default to lowercase.
run: |
set -eu
OWNER="$(echo "${{ github.repository_owner }}" | tr '[:upper:]' '[:lower:]')"
echo "image=ghcr.io/$OWNER/think-watch-${{ matrix.component }}" >> "$GITHUB_OUTPUT"
PLATFORM_PAIR="$(echo '${{ matrix.platform }}' | tr '/' '-')"
echo "platform_pair=$PLATFORM_PAIR" >> "$GITHUB_OUTPUT"
- name: Build & push by digest
id: build
uses: docker/build-push-action@v6
with:
context: .
file: deploy/docker/Dockerfile.${{ matrix.component }}
platforms: ${{ matrix.platform }}
# `push-by-digest=true,name-canonical=true` is the documented
# multi-arch split: each platform job pushes an UNTAGGED
# blob whose digest the merge job glues into a manifest list
# below. Without push-by-digest, the second platform's push
# would overwrite the first's `:version` tag.
outputs: |
type=image,name=${{ steps.meta.outputs.image }},push-by-digest=true,name-canonical=true,push=true
labels: |
org.opencontainers.image.source=https://github.com/${{ github.repository }}
org.opencontainers.image.version=${{ github.ref_name }}
org.opencontainers.image.revision=${{ github.sha }}
org.opencontainers.image.licenses=BUSL-1.1
cache-from: type=gha,scope=${{ matrix.component }}-${{ steps.meta.outputs.platform_pair }}
cache-to: type=gha,scope=${{ matrix.component }}-${{ steps.meta.outputs.platform_pair }},mode=max
- name: Export digest
run: |
mkdir -p "${{ runner.temp }}/digests"
digest="${{ steps.build.outputs.digest }}"
touch "${{ runner.temp }}/digests/${digest#sha256:}"
- name: Upload digest
uses: actions/upload-artifact@v4
with:
name: digests-${{ matrix.component }}-${{ steps.meta.outputs.platform_pair }}
path: ${{ runner.temp }}/digests/*
if-no-files-found: error
retention-days: 1
# -------------------------------------------------------------------
# Merge per-platform digests into a single multi-arch manifest list
# tagged `:version` (always) and `:latest` (stable releases only).
# -------------------------------------------------------------------
merge-manifests:
name: Manifest · ${{ matrix.component }}
runs-on: ubuntu-latest
needs: build-images
strategy:
fail-fast: false
matrix:
component: [server, web]
steps:
- name: Download digests
uses: actions/download-artifact@v4
with:
path: ${{ runner.temp }}/digests
pattern: digests-${{ matrix.component }}-*
merge-multiple: true
- name: Set up Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to GHCR
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Resolve tags
id: tags
# Reuse the existing version + prerelease detection — keeps
# the "0.x and -rc/-beta/-alpha don't get :latest" rule in
# exactly one place across both job stages.
run: |
set -eu
VERSION="${GITHUB_REF_NAME#v}"
OWNER="$(echo "${{ github.repository_owner }}" | tr '[:upper:]' '[:lower:]')"
IMAGE="ghcr.io/$OWNER/think-watch-${{ matrix.component }}"
echo "image=$IMAGE" >> "$GITHUB_OUTPUT"
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
TAG_ARGS="-t $IMAGE:$VERSION"
case "$GITHUB_REF_NAME" in
v0.*|*-rc.*|*-beta.*|*-alpha.*) ;;
*) TAG_ARGS="$TAG_ARGS -t $IMAGE:latest" ;;
esac
echo "tag_args=$TAG_ARGS" >> "$GITHUB_OUTPUT"
- name: Create manifest list and push
# `docker buildx imagetools create` is the canonical primitive
# for assembling a multi-arch manifest list from individually-
# pushed per-platform digests. It writes the manifest to GHCR
# under each `-t` tag and resolves the children by their
# already-pushed sha256 references.
working-directory: ${{ runner.temp }}/digests
run: |
set -eu
REFS=""
for d in *; do
REFS="$REFS ${{ steps.tags.outputs.image }}@sha256:$d"
done
docker buildx imagetools create ${{ steps.tags.outputs.tag_args }} $REFS
- name: Inspect resulting manifest
# Cheap sanity check — `imagetools inspect` lists the platforms
# in the manifest, so a regression where only one platform
# landed (silent push failure on the other matrix entry) is
# caught here instead of in a user's `helm install`.
run: |
docker buildx imagetools inspect \
${{ steps.tags.outputs.image }}:${{ steps.tags.outputs.version }}
# -------------------------------------------------------------------
# Helm chart — packaged with the same version as the tag
# -------------------------------------------------------------------
package-helm:
name: Helm chart
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: azure/setup-helm@v4
with:
version: latest
- name: Resolve version
id: meta
run: echo "version=${GITHUB_REF_NAME#v}" >> "$GITHUB_OUTPUT"
- name: Package chart
# `--app-version` is the image tag the chart's deployment
# templates reach for by default (via `tw.imageTag`). Pinning
# it here keeps a fresh `helm install think-watch-X.Y.Z.tgz`
# pointing at the matching image bundle from the build-images
# job above.
run: |
helm package deploy/helm/think-watch \
--version "${{ steps.meta.outputs.version }}" \
--app-version "${{ steps.meta.outputs.version }}" \
--destination .
- name: Upload chart artifact
uses: actions/upload-artifact@v4
with:
name: helm-chart
path: think-watch-*.tgz
retention-days: 30
# -------------------------------------------------------------------
# GitHub Release — attaches the Helm chart, links the images
# -------------------------------------------------------------------
create-release:
name: GitHub Release
needs: [merge-manifests, package-helm]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Download Helm chart artifact
uses: actions/download-artifact@v4
with:
name: helm-chart
path: .
- name: Extract release notes from CHANGELOG
id: notes
# awk-based section extraction so we don't pull in another
# dep just for `## [X.Y.Z]` markdown chunking. If the
# CHANGELOG doesn't carry an entry for this tag, fail loudly
# — releasing without notes hides what changed from operators.
run: |
set -eu
VERSION="${GITHUB_REF_NAME#v}"
awk -v ver="$VERSION" '
/^## \[/{
if ($0 ~ "^## \\[" ver "\\]") { found=1; next }
else if (found) { exit }
}
found
' CHANGELOG.md > release-notes.md
if [ ! -s release-notes.md ]; then
echo "::error::No CHANGELOG entry for $VERSION — refusing to publish a release without notes."
exit 1
fi
# Add a leading reference to the GHCR images so users on the
# Release page can copy-paste them without hunting through
# the chart values.
OWNER="$(echo "${{ github.repository_owner }}" | tr '[:upper:]' '[:lower:]')"
{
echo "## Images"
echo ""
echo "- \`ghcr.io/$OWNER/think-watch-server:$VERSION\`"
echo "- \`ghcr.io/$OWNER/think-watch-web:$VERSION\`"
echo ""
echo "## Helm"
echo ""
echo '```bash'
echo "helm install think-watch ./think-watch-$VERSION.tgz \\"
echo " --namespace thinkwatch --create-namespace"
echo '```'
echo ""
echo "---"
echo ""
cat release-notes.md
} > release-body.md
- name: Detect pre-release flag
id: meta
run: |
PRERELEASE=false
case "$GITHUB_REF_NAME" in
v0.*|*-rc.*|*-beta.*|*-alpha.*) PRERELEASE=true ;;
esac
echo "prerelease=$PRERELEASE" >> "$GITHUB_OUTPUT"
- name: Create release
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ github.ref_name }}
name: ThinkWatch ${{ github.ref_name }}
body_path: release-body.md
prerelease: ${{ steps.meta.outputs.prerelease }}
files: |
think-watch-*.tgz