Skip to content

Commit 4e5aeaf

Browse files
committed
Merge branch 'dev' into main — release v3.11.1
CI infrastructure fix to unblock arm64 image builds. - ci(release): native arm64 runners + split-build manifest; drop OTel variant Fixes the QEMU-pandas compile timeout that cancelled v3.10.0 and v3.11.0 release runs at the 6h ceiling. arm64 builds now run on ubuntu-24.04-arm natively, completing in ~15min instead of >6h.
2 parents c4bf2ed + ef06bd4 commit 4e5aeaf

2 files changed

Lines changed: 186 additions & 53 deletions

File tree

.github/workflows/release.yaml

Lines changed: 184 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ env:
2222
jobs:
2323
release:
2424
runs-on: ubuntu-latest
25+
timeout-minutes: 5
2526
outputs:
2627
version: ${{ steps.version.outputs.version }}
2728
released: ${{ steps.version.outputs.version != '' }}
@@ -33,7 +34,6 @@ jobs:
3334
- name: Resolve version from tag
3435
id: version
3536
run: |
36-
# Priority: workflow_dispatch input > pushed tag > latest tag on HEAD
3737
TAG="${{ inputs.tag || github.ref_name }}"
3838
if [[ -z "$TAG" || "$TAG" == "main" || "$TAG" == "dev" ]]; then
3939
TAG=$(git describe --tags --abbrev=0 HEAD 2>/dev/null || echo "")
@@ -65,7 +65,9 @@ jobs:
6565
needs: release
6666
if: needs.release.outputs.released == 'true'
6767
runs-on: ubuntu-latest
68+
timeout-minutes: 30
6869
strategy:
70+
fail-fast: false
6971
matrix:
7072
include:
7173
- goos: linux
@@ -119,6 +121,7 @@ jobs:
119121
needs: [release, build-binaries]
120122
if: needs.release.outputs.released == 'true'
121123
runs-on: ubuntu-latest
124+
timeout-minutes: 10
122125
steps:
123126
- name: Download all binary assets
124127
env:
@@ -146,46 +149,106 @@ jobs:
146149
--repo "${{ github.repository }}" \
147150
--clobber
148151
149-
# Build and push Docker images to GHCR + Docker Hub
150-
docker-images:
152+
# Build Docker images per (variant × platform) on native runners (no QEMU).
153+
# Each job pushes a digest-only image; the merge job combines them into a
154+
# multi-arch manifest and pushes the final tags to both GHCR and Docker Hub.
155+
# Variants: base (backend only), latest (+ web UI + Python), full (+ all skills).
156+
# OTel and Tailscale variants are not built — users build from source if needed.
157+
docker-build:
151158
needs: release
152159
if: needs.release.outputs.released == 'true'
153-
runs-on: ubuntu-latest
160+
timeout-minutes: 90
154161
strategy:
162+
fail-fast: false
155163
matrix:
164+
variant: [base, latest, full]
165+
platform: [linux/amd64, linux/arm64]
156166
include:
157-
# Base: backend API-only, no web UI, no runtimes
158167
- variant: base
159-
suffix: "-base"
160-
enable_otel: "false"
161168
enable_embedui: "false"
162169
enable_python: "false"
163170
enable_full_skills: "false"
164-
# Latest: backend + embedded web UI + Python
165171
- variant: latest
166-
suffix: ""
167-
enable_otel: "false"
168172
enable_embedui: "true"
169173
enable_python: "true"
170174
enable_full_skills: "false"
171-
# Full: all runtimes + skills pre-installed
172175
- variant: full
173-
suffix: "-full"
174-
enable_otel: "false"
175176
enable_embedui: "true"
176177
enable_python: "true"
177178
enable_full_skills: "true"
178-
# OTel: latest + OpenTelemetry tracing
179-
- variant: otel
180-
suffix: "-otel"
181-
enable_otel: "true"
182-
enable_embedui: "true"
183-
enable_python: "true"
184-
enable_full_skills: "false"
179+
- platform: linux/amd64
180+
runner: ubuntu-latest
181+
arch: amd64
182+
- platform: linux/arm64
183+
runner: ubuntu-24.04-arm
184+
arch: arm64
185+
runs-on: ${{ matrix.runner }}
185186
steps:
186187
- uses: actions/checkout@v4
187188

188-
- uses: docker/setup-qemu-action@v3
189+
- uses: docker/setup-buildx-action@v3
190+
191+
- name: Log in to GHCR
192+
uses: docker/login-action@v3
193+
with:
194+
registry: ghcr.io
195+
username: ${{ github.actor }}
196+
password: ${{ secrets.GITHUB_TOKEN }}
197+
198+
- name: Build and push by digest
199+
id: build
200+
uses: docker/build-push-action@v6
201+
with:
202+
context: .
203+
platforms: ${{ matrix.platform }}
204+
outputs: type=image,name=${{ env.GHCR_IMAGE }},push-by-digest=true,name-canonical=true,push=true
205+
build-args: |
206+
ENABLE_OTEL=false
207+
ENABLE_EMBEDUI=${{ matrix.enable_embedui }}
208+
ENABLE_PYTHON=${{ matrix.enable_python }}
209+
ENABLE_FULL_SKILLS=${{ matrix.enable_full_skills }}
210+
VERSION=v${{ needs.release.outputs.version }}
211+
cache-from: type=gha,scope=${{ matrix.variant }}-${{ matrix.arch }}
212+
cache-to: type=gha,mode=max,scope=${{ matrix.variant }}-${{ matrix.arch }}
213+
provenance: false
214+
215+
- name: Export digest
216+
run: |
217+
mkdir -p /tmp/digests
218+
digest="${{ steps.build.outputs.digest }}"
219+
touch "/tmp/digests/${digest#sha256:}"
220+
221+
- uses: actions/upload-artifact@v4
222+
with:
223+
name: digests-${{ matrix.variant }}-${{ matrix.arch }}
224+
path: /tmp/digests/*
225+
if-no-files-found: error
226+
retention-days: 1
227+
228+
# Combine per-platform digests into multi-arch manifests and push final tags
229+
# to both GHCR and Docker Hub. One job per variant runs in parallel.
230+
docker-merge:
231+
needs: [release, docker-build]
232+
if: needs.release.outputs.released == 'true'
233+
runs-on: ubuntu-latest
234+
timeout-minutes: 15
235+
strategy:
236+
fail-fast: false
237+
matrix:
238+
include:
239+
- variant: base
240+
suffix: "-base"
241+
- variant: latest
242+
suffix: ""
243+
- variant: full
244+
suffix: "-full"
245+
steps:
246+
- name: Download digests
247+
uses: actions/download-artifact@v4
248+
with:
249+
path: /tmp/digests
250+
pattern: digests-${{ matrix.variant }}-*
251+
merge-multiple: true
189252

190253
- uses: docker/setup-buildx-action@v3
191254

@@ -214,32 +277,92 @@ jobs:
214277
type=raw,value=latest,enable=${{ matrix.suffix == '' }},suffix=
215278
type=raw,value=${{ matrix.variant }},enable=${{ matrix.suffix != '' }}
216279
217-
- name: Build and push
218-
uses: docker/build-push-action@v6
219-
with:
220-
context: .
221-
platforms: linux/amd64,linux/arm64
222-
push: true
223-
tags: ${{ steps.meta.outputs.tags }}
224-
labels: ${{ steps.meta.outputs.labels }}
225-
build-args: |
226-
ENABLE_OTEL=${{ matrix.enable_otel }}
227-
ENABLE_EMBEDUI=${{ matrix.enable_embedui }}
228-
ENABLE_PYTHON=${{ matrix.enable_python }}
229-
ENABLE_FULL_SKILLS=${{ matrix.enable_full_skills }}
230-
VERSION=v${{ needs.release.outputs.version }}
231-
cache-from: type=gha,scope=${{ matrix.variant }}
232-
cache-to: type=gha,mode=max,scope=${{ matrix.variant }}
280+
- name: Create manifest list and push
281+
working-directory: /tmp/digests
282+
run: |
283+
# Compose -t flags from metadata-action tags (newline-separated)
284+
TAG_ARGS=()
285+
while IFS= read -r tag; do
286+
[ -n "$tag" ] && TAG_ARGS+=("-t" "$tag")
287+
done <<< "${{ steps.meta.outputs.tags }}"
288+
# Compose source digest refs (pushed to GHCR by docker-build)
289+
DIGEST_ARGS=()
290+
for f in *; do
291+
DIGEST_ARGS+=("${{ env.GHCR_IMAGE }}@sha256:$f")
292+
done
293+
set -x
294+
docker buildx imagetools create "${TAG_ARGS[@]}" "${DIGEST_ARGS[@]}"
295+
296+
- name: Inspect manifest
297+
run: |
298+
docker buildx imagetools inspect \
299+
"${{ env.GHCR_IMAGE }}:v${{ needs.release.outputs.version }}${{ matrix.suffix }}"
233300
234-
# Build and push web UI Docker image
235-
docker-web:
301+
# Build web UI Docker image per platform on native runners.
302+
docker-web-build:
236303
needs: release
237304
if: needs.release.outputs.released == 'true'
238-
runs-on: ubuntu-latest
305+
timeout-minutes: 30
306+
strategy:
307+
fail-fast: false
308+
matrix:
309+
platform: [linux/amd64, linux/arm64]
310+
include:
311+
- platform: linux/amd64
312+
runner: ubuntu-latest
313+
arch: amd64
314+
- platform: linux/arm64
315+
runner: ubuntu-24.04-arm
316+
arch: arm64
317+
runs-on: ${{ matrix.runner }}
239318
steps:
240319
- uses: actions/checkout@v4
241320

242-
- uses: docker/setup-qemu-action@v3
321+
- uses: docker/setup-buildx-action@v3
322+
323+
- name: Log in to GHCR
324+
uses: docker/login-action@v3
325+
with:
326+
registry: ghcr.io
327+
username: ${{ github.actor }}
328+
password: ${{ secrets.GITHUB_TOKEN }}
329+
330+
- name: Build and push by digest
331+
id: build
332+
uses: docker/build-push-action@v6
333+
with:
334+
context: ui/web
335+
platforms: ${{ matrix.platform }}
336+
outputs: type=image,name=${{ env.GHCR_IMAGE }}-web,push-by-digest=true,name-canonical=true,push=true
337+
cache-from: type=gha,scope=web-${{ matrix.arch }}
338+
cache-to: type=gha,mode=max,scope=web-${{ matrix.arch }}
339+
provenance: false
340+
341+
- name: Export digest
342+
run: |
343+
mkdir -p /tmp/digests
344+
digest="${{ steps.build.outputs.digest }}"
345+
touch "/tmp/digests/${digest#sha256:}"
346+
347+
- uses: actions/upload-artifact@v4
348+
with:
349+
name: digests-web-${{ matrix.arch }}
350+
path: /tmp/digests/*
351+
if-no-files-found: error
352+
retention-days: 1
353+
354+
docker-web-merge:
355+
needs: [release, docker-web-build]
356+
if: needs.release.outputs.released == 'true'
357+
runs-on: ubuntu-latest
358+
timeout-minutes: 15
359+
steps:
360+
- name: Download digests
361+
uses: actions/download-artifact@v4
362+
with:
363+
path: /tmp/digests
364+
pattern: digests-web-*
365+
merge-multiple: true
243366

244367
- uses: docker/setup-buildx-action@v3
245368

@@ -267,22 +390,31 @@ jobs:
267390
type=raw,value=v${{ needs.release.outputs.version }}
268391
type=raw,value=latest
269392
270-
- name: Build and push
271-
uses: docker/build-push-action@v6
272-
with:
273-
context: ui/web
274-
platforms: linux/amd64,linux/arm64
275-
push: true
276-
tags: ${{ steps.meta.outputs.tags }}
277-
labels: ${{ steps.meta.outputs.labels }}
278-
cache-from: type=gha,scope=web
279-
cache-to: type=gha,mode=max,scope=web
393+
- name: Create manifest list and push
394+
working-directory: /tmp/digests
395+
run: |
396+
TAG_ARGS=()
397+
while IFS= read -r tag; do
398+
[ -n "$tag" ] && TAG_ARGS+=("-t" "$tag")
399+
done <<< "${{ steps.meta.outputs.tags }}"
400+
DIGEST_ARGS=()
401+
for f in *; do
402+
DIGEST_ARGS+=("${{ env.GHCR_IMAGE }}-web@sha256:$f")
403+
done
404+
set -x
405+
docker buildx imagetools create "${TAG_ARGS[@]}" "${DIGEST_ARGS[@]}"
406+
407+
- name: Inspect manifest
408+
run: |
409+
docker buildx imagetools inspect \
410+
"${{ env.GHCR_IMAGE }}-web:v${{ needs.release.outputs.version }}"
280411
281412
# Notify Discord on new release (runs even if docker jobs fail)
282413
notify-discord:
283-
needs: [release, build-binaries, docker-images, docker-web]
414+
needs: [release, build-binaries, docker-merge, docker-web-merge]
284415
if: always() && needs.release.outputs.released == 'true' && !cancelled()
285416
runs-on: ubuntu-latest
417+
timeout-minutes: 5
286418
steps:
287419
- name: Send Discord notification
288420
env:

CLAUDE.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -155,10 +155,11 @@ Published to GHCR (`ghcr.io/nextlevelbuilder/goclaw`) and Docker Hub (`digitop/g
155155
| latest | `:latest`, `:vX.Y.Z` | Backend + web UI + Python |
156156
| base | `:base`, `:vX.Y.Z-base` | Backend only, no UI/runtimes |
157157
| full | `:full`, `:vX.Y.Z-full` | All runtimes + skills pre-installed |
158-
| otel | `:otel`, `:vX.Y.Z-otel` | Latest + OpenTelemetry tracing |
159158
| web | `-web:latest` | Standalone web UI (Nginx) |
160159
| beta | `:beta`, `:vX.Y.Z-beta.N` | Beta builds from dev |
161160

161+
OTel and Tailscale variants are not pre-built — build from source with the appropriate `--build-arg ENABLE_OTEL=true` or `-tags tsnet` flag if needed.
162+
162163
### Tag Pattern Safety
163164

164165
- `release.yaml`: tag-triggered (`v[0-9]+.[0-9]+.[0-9]+`) — clean semver only, no beta/rc

0 commit comments

Comments
 (0)