Skip to content

Commit 5775936

Browse files
committed
chore(ci): ci: optimize docker builds with native arm64 runners
1 parent bd28c62 commit 5775936

5 files changed

Lines changed: 136 additions & 63 deletions

File tree

.github/workflows/docker.yml

Lines changed: 107 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,12 @@ permissions:
2828
id-token: write
2929

3030
jobs:
31-
docker:
32-
runs-on: ubuntu-latest
31+
build:
3332
strategy:
33+
fail-fast: false
3434
matrix:
35+
image: [server, agent, web, mcp]
36+
platform: [linux/amd64, linux/arm64]
3537
include:
3638
- image: server
3739
dockerfile: docker/Dockerfile.server
@@ -41,6 +43,11 @@ jobs:
4143
dockerfile: docker/Dockerfile.web
4244
- image: mcp
4345
dockerfile: docker/Dockerfile.mcp
46+
- platform: linux/amd64
47+
runner: ubuntu-latest
48+
- platform: linux/arm64
49+
runner: ubuntu-24.04-arm64
50+
runs-on: ${{ matrix.runner }}
4451
steps:
4552
- name: Checkout
4653
uses: actions/checkout@v4
@@ -52,26 +59,23 @@ jobs:
5259
images: |
5360
memohai/${{ matrix.image }}
5461
ghcr.io/${{ github.repository_owner }}/${{ matrix.image }}
55-
tags: |
56-
type=raw,value=dev,enable=${{ github.ref == 'refs/heads/main' }}
57-
type=raw,value=latest,enable=${{ github.event_name == 'release' || (startsWith(github.ref, 'refs/tags/') && !contains(github.ref_name, '-')) }}
58-
type=ref,event=pr
59-
type=semver,pattern={{version}}
60-
type=semver,pattern={{major}}.{{minor}}
61-
type=semver,pattern={{major}}
62-
type=sha
63-
labels: |
64-
org.opencontainers.image.title=memoh-${{ matrix.image }}
65-
org.opencontainers.image.description=Memoh ${{ matrix.image }} - Multi-member AI agent platform
66-
org.opencontainers.image.vendor=memohai
67-
68-
- name: Set up QEMU
69-
if: startsWith(github.ref, 'refs/tags/')
70-
uses: docker/setup-qemu-action@v3
7162
7263
- name: Set up Docker Buildx
7364
uses: docker/setup-buildx-action@v3
7465

66+
- name: Set up Go
67+
if: matrix.image == 'server' || matrix.image == 'mcp'
68+
uses: actions/setup-go@v5
69+
with:
70+
go-version: '1.25'
71+
cache: true
72+
73+
- name: Pre-warm Go mod cache
74+
if: matrix.image == 'server' || matrix.image == 'mcp'
75+
run: |
76+
mkdir -p .go-cache
77+
GOMODCACHE=$(pwd)/.go-cache go mod download
78+
7579
- name: Login to Docker Hub
7680
if: github.event_name != 'pull_request'
7781
uses: docker/login-action@v3
@@ -87,22 +91,97 @@ jobs:
8791
username: ${{ github.actor }}
8892
password: ${{ secrets.GITHUB_TOKEN }}
8993

90-
- name: Build and push
94+
- name: Build and push by digest
95+
id: build
9196
uses: docker/build-push-action@v6
9297
with:
9398
context: .
9499
file: ${{ matrix.dockerfile }}
95-
push: ${{ github.event_name != 'pull_request' }}
96-
tags: ${{ steps.meta.outputs.tags }}
97-
labels: ${{ steps.meta.outputs.labels }}
98-
platforms: ${{ startsWith(github.ref, 'refs/tags/') && 'linux/amd64,linux/arm64' || 'linux/amd64' }}
100+
platforms: ${{ matrix.platform }}
101+
# 仅在 main 分支、tag 推送或 release 发布时才真正推送
102+
push: ${{ github.event_name != 'pull_request' && (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/') || github.event_name == 'release') }}
103+
outputs: ${{ (github.event_name != 'pull_request' && (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/') || github.event_name == 'release')) && 'type=image,name-canonical=true,push-by-digest=true' || 'type=image,push=false' }}
104+
build-contexts: |
105+
gomodcache=${{ github.workspace }}/.go-cache
99106
build-args: |
100107
VERSION=${{ steps.meta.outputs.version }}
101108
COMMIT_HASH=${{ github.sha }}
102-
BUILD_TIME=${{ fromJson(steps.meta.outputs.json).labels['org.opencontainers.image.created'] }}
103109
VITE_API_URL=/api
104110
VITE_AGENT_URL=/agent
105-
provenance: ${{ startsWith(github.ref, 'refs/tags/') }}
106-
sbom: ${{ startsWith(github.ref, 'refs/tags/') }}
107-
cache-from: type=gha,scope=${{ matrix.image }}
108-
cache-to: type=gha,scope=${{ matrix.image }},mode=max
111+
cache-from: type=gha,scope=${{ matrix.image }}-${{ matrix.platform }}
112+
cache-to: type=gha,scope=${{ matrix.image }}-${{ matrix.platform }},mode=max
113+
114+
- name: Export digest
115+
if: github.event_name != 'pull_request' && (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/') || github.event_name == 'release')
116+
run: |
117+
mkdir -p /tmp/digests
118+
digest="${{ steps.build.outputs.digest }}"
119+
touch "/tmp/digests/${digest#sha256:}"
120+
121+
- name: Upload digest
122+
if: github.event_name != 'pull_request' && (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/') || github.event_name == 'release')
123+
uses: actions/upload-artifact@v4
124+
with:
125+
name: digests-${{ matrix.image }}-${{ strategy.job-index }}
126+
path: /tmp/digests/*
127+
if-no-files-found: error
128+
retention-days: 1
129+
130+
merge:
131+
runs-on: ubuntu-latest
132+
if: github.event_name != 'pull_request' && (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/') || github.event_name == 'release')
133+
needs: build
134+
strategy:
135+
matrix:
136+
image: [server, agent, web, mcp]
137+
steps:
138+
- name: Download digests
139+
uses: actions/download-artifact@v4
140+
with:
141+
path: /tmp/digests
142+
pattern: digests-${{ matrix.image }}-*
143+
merge-multiple: true
144+
145+
- name: Set up Docker Buildx
146+
uses: docker/setup-buildx-action@v3
147+
148+
- name: Login to Docker Hub
149+
uses: docker/login-action@v3
150+
with:
151+
username: ${{ secrets.DOCKERHUB_USERNAME }}
152+
password: ${{ secrets.DOCKERHUB_TOKEN }}
153+
154+
- name: Login to GitHub Container Registry
155+
uses: docker/login-action@v3
156+
with:
157+
registry: ghcr.io
158+
username: ${{ github.actor }}
159+
password: ${{ secrets.GITHUB_TOKEN }}
160+
161+
- name: Docker meta
162+
id: meta
163+
uses: docker/metadata-action@v5
164+
with:
165+
images: |
166+
memohai/${{ matrix.image }}
167+
ghcr.io/${{ github.repository_owner }}/${{ matrix.image }}
168+
tags: |
169+
type=raw,value=dev,enable=${{ github.ref == 'refs/heads/main' }}
170+
type=raw,value=latest,enable=${{ github.event_name == 'release' || (startsWith(github.ref, 'refs/tags/') && !contains(github.ref_name, '-')) }}
171+
type=ref,event=pr
172+
type=semver,pattern={{version}}
173+
type=semver,pattern={{major}}.{{minor}}
174+
type=semver,pattern={{major}}
175+
176+
- name: Create manifest list and push
177+
working-directory: /tmp/digests
178+
run: |
179+
# 提取所有标签并格式化为 -t tag1 -t tag2
180+
TAG_ARGS=$(echo "${{ steps.meta.outputs.tags }}" | xargs -I {} echo "-t {}")
181+
182+
# 使用 Docker Hub 的引用作为源来合并 manifest (因为 build 阶段已经推送到所有仓库)
183+
# printf 会遍历当前目录下所有的 digest 文件名
184+
SOURCES=$(printf "memohai/${{ matrix.image }}@sha256:%s " *)
185+
186+
echo "Creating manifest for ${{ matrix.image }} with tags: $TAG_ARGS"
187+
docker buildx imagetools create $TAG_ARGS $SOURCES

docker/Dockerfile.agent

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
FROM oven/bun:1 AS builder
1+
FROM --platform=$BUILDPLATFORM oven/bun:1 AS builder
22

33
WORKDIR /build
44

docker/Dockerfile.mcp

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,16 @@
11
# syntax=docker/dockerfile:1
2-
FROM golang:1.25-alpine AS build
2+
FROM scratch AS gomodcache
3+
4+
FROM --platform=$BUILDPLATFORM golang:1.25-alpine AS build
35

46
WORKDIR /src
57
COPY go.mod go.sum ./
68
RUN --mount=type=cache,target=/go/pkg/mod \
9+
--mount=type=bind,from=gomodcache,target=/tmp/gomodcache \
10+
set -eux; \
11+
if [ -d /tmp/gomodcache/cache/download ]; then \
12+
cp -a /tmp/gomodcache/. /go/pkg/mod/; \
13+
fi; \
714
go mod download
815

916
COPY . .

docker/Dockerfile.server

Lines changed: 19 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,61 +1,47 @@
11
# syntax=docker/dockerfile:1
22

3-
# ---- Stage 1: Build server binary ----
4-
FROM golang:1.25-alpine AS server-builder
3+
# ---- Stage 0: Cache Context Fallback ----
4+
FROM scratch AS gomodcache
55

6+
# ---- Stage 1: Build base with dependencies ----
7+
FROM --platform=$BUILDPLATFORM golang:1.25-alpine AS build-base
68
WORKDIR /build
79
RUN apk add --no-cache git make
8-
910
COPY go.mod go.sum ./
1011
RUN --mount=type=cache,target=/go/pkg/mod \
12+
--mount=type=bind,from=gomodcache,target=/tmp/gomodcache \
13+
set -eux; \
14+
if [ -d /tmp/gomodcache/cache/download ]; then \
15+
cp -a /tmp/gomodcache/. /go/pkg/mod/; \
16+
fi; \
1117
go mod download
12-
1318
COPY . .
1419

20+
# ---- Stage 2: Build server binary ----
21+
FROM build-base AS server-builder
1522
ARG VERSION=dev
1623
ARG COMMIT_HASH=unknown
1724
ARG BUILD_TIME=unknown
1825
ARG TARGETOS
1926
ARG TARGETARCH
20-
2127
RUN --mount=type=cache,target=/go/pkg/mod \
2228
--mount=type=cache,target=/root/.cache/go-build \
2329
set -eux; \
24-
build_os="${TARGETOS:-linux}"; \
25-
build_arch="${TARGETARCH:-$(uname -m)}"; \
26-
case "$build_arch" in \
27-
x86_64) build_arch="amd64" ;; \
28-
aarch64) build_arch="arm64" ;; \
29-
esac; \
30-
case "$build_arch" in \
31-
amd64|arm64) ;; \
32-
*) echo "unsupported TARGETARCH: $build_arch (only amd64/arm64)"; exit 1 ;; \
33-
esac; \
34-
CGO_ENABLED=0 GOOS="$build_os" GOARCH="$build_arch" \
30+
CGO_ENABLED=0 GOOS=${TARGETOS:-linux} GOARCH=${TARGETARCH:-amd64} \
3531
go build -trimpath \
3632
-ldflags "-s -w \
3733
-X github.com/memohai/memoh/internal/version.Version=${VERSION} \
3834
-X github.com/memohai/memoh/internal/version.CommitHash=${COMMIT_HASH} \
3935
-X github.com/memohai/memoh/internal/version.BuildTime=${BUILD_TIME}" \
4036
-o memoh-server ./cmd/agent/main.go
4137

42-
# ---- Stage 2: Build MCP binary ----
43-
FROM golang:1.25-alpine AS mcp-builder
44-
45-
WORKDIR /src
46-
RUN apk add --no-cache ca-certificates git
47-
48-
COPY go.mod go.sum ./
49-
RUN --mount=type=cache,target=/go/pkg/mod \
50-
go mod download
51-
52-
COPY . .
53-
54-
ARG TARGETARCH=amd64
38+
# ---- Stage 3: Build MCP binary ----
39+
FROM build-base AS mcp-builder
40+
ARG TARGETARCH
5541
ARG COMMIT_HASH=unknown
5642
RUN --mount=type=cache,target=/go/pkg/mod \
5743
--mount=type=cache,target=/root/.cache/go-build \
58-
CGO_ENABLED=0 GOOS=linux GOARCH=${TARGETARCH} \
44+
CGO_ENABLED=0 GOOS=linux GOARCH=${TARGETARCH:-amd64} \
5945
go build -trimpath \
6046
-ldflags "-s -w -X github.com/memohai/memoh/internal/version.CommitHash=${COMMIT_HASH}" \
6147
-o /out/mcp ./cmd/mcp
@@ -88,14 +74,15 @@ FROM alpine:latest AS oci-exporter
8874

8975
COPY --from=mcp-rootfs /tmp/rootfs.tar /tmp/layer.tar
9076
ARG MCP_IMAGE_TAG=docker.io/library/memoh-mcp:latest
77+
ARG TARGETARCH
9178

9279
RUN set -e \
9380
&& LAYER_SHA=$(sha256sum /tmp/layer.tar | awk '{print $1}') \
9481
&& LAYER_SIZE=$(wc -c < /tmp/layer.tar) \
9582
&& mkdir -p "/tmp/image/${LAYER_SHA}" /out \
9683
&& mv /tmp/layer.tar "/tmp/image/${LAYER_SHA}/layer.tar" \
97-
&& printf '{"architecture":"amd64","os":"linux","created":"1970-01-01T00:00:00Z","config":{"Entrypoint":["/opt/entrypoint.sh"],"WorkingDir":"/app","Env":["PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"]},"rootfs":{"type":"layers","diff_ids":["sha256:%s"]},"history":[{"created":"1970-01-01T00:00:00Z","comment":"memoh-mcp image"}]}' \
98-
"${LAYER_SHA}" > /tmp/config.json \
84+
&& printf '{"architecture":"%s","os":"linux","created":"1970-01-01T00:00:00Z","config":{"Entrypoint":["/opt/entrypoint.sh"],"WorkingDir":"/app","Env":["PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"]},"rootfs":{"type":"layers","diff_ids":["sha256:%s"]},"history":[{"created":"1970-01-01T00:00:00Z","comment":"memoh-mcp image"}]}' \
85+
"${TARGETARCH:-amd64}" "${LAYER_SHA}" > /tmp/config.json \
9986
&& CONFIG_SHA=$(sha256sum /tmp/config.json | awk '{print $1}') \
10087
&& mv /tmp/config.json "/tmp/image/${CONFIG_SHA}.json" \
10188
&& printf '[{"Config":"%s.json","RepoTags":["%s"],"Layers":["%s/layer.tar"]}]' \

docker/Dockerfile.web

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# syntax=docker/dockerfile:1
2-
FROM node:25-alpine AS builder
2+
FROM --platform=$BUILDPLATFORM node:25-alpine AS builder
33

44
WORKDIR /build
55

0 commit comments

Comments
 (0)