Skip to content

Commit 88ce036

Browse files
chaodu-agent超渡法師
andauthored
feat(ci): unified image repo with shared builder and tag-based variants (#1175)
* feat(ci): unified image repo with shared builder and tag-based variants Consolidate all Docker images into ghcr.io/openabdev/openab with tag-based variants (<version>-<agent>). - Add Dockerfile.unified with shared builder stage (compile once, unified mode) and per-agent targets as thin runtime layers - Add build-images.yml workflow: - Job 1: build-core (shared builder, 1x per arch) - Job 2: build-agents (14x parallel, thin layers) - Job 3: merge-manifests (multi-arch per variant) - Tag strategy: <version>-<agent>, <version> (default=kiro), latest Agents: kiro, claude, codex, copilot, cursor, gemini, grok, hermes, mimocode, opencode, antigravity, pi, native, agentcore Estimated build time: ~12 min total (vs ~140 min today) Closes #1174 * fix: address reviewer feedback on unified image build - Dockerfile.unified: add BUILDER_IMAGE ARG for registry-based builder reuse - Dockerfile.unified: move openab-agent/agy-acp COPY after main build (cache-bust fix) - Dockerfile.unified: fix .gemini → .agy dir in antigravity target - build-images.yml: push builder to GHCR, pass via BUILDER_IMAGE build-arg - build-images.yml: remove artifact upload/download flow (no longer needed) - build-images.yml: add branch guard — only push :latest from main - build-images.yml: add concurrency group to prevent tag race conditions - build-images.yml: add input validation (tag format + variant allowlist) - build-images.yml: remove agent cache-to (thin layers don't need caching) - build-images.yml: add digest count/format validation in merge-manifests * fix: add git to base-node, remove dead ALL_VARIANTS env var * fix: address script injection, use arch-based tags, registry cache-from - Fix script injection: pass inputs via env vars instead of direct interpolation - Use arch names (amd64/arm64) in builder tags instead of runner names - Add cache-from type=registry for builder reuse (ensures no recompilation even on GHA cache miss) - Use env vars for branch checks in merge-manifests (injection hardening) * ci: add PR-triggered smoke test for Dockerfile.unified targets Covers all 14 unified targets with: - docker build --target <variant> validation - openab binary existence check - agent CLI existence check Triggered on PRs that touch Dockerfile.unified, src/, crates/, or the openab-agent/agy-acp directories. * fix(ci): use GHA cache for unified smoke test to avoid 14x recompilation Build the shared builder stage once and cache it via GHA cache. Agent target smoke tests pull cached builder layers instead of recompiling the Rust workspace independently. * fix: add [workspace] marker to openab-agent/agy-acp during build Cargo walks up to find /build/Cargo.toml workspace root and errors because openab-agent/agy-acp are not workspace members. Adding an empty [workspace] table makes each sub-crate its own workspace root, matching how Dockerfile.native handles this. * fix: cursor target symlink path (mv dist-package → cursor-agent) --------- Co-authored-by: 超渡法師 <kiro-bot@openab.dev>
1 parent f9a2849 commit 88ce036

3 files changed

Lines changed: 711 additions & 0 deletions

File tree

.github/workflows/build-images.yml

Lines changed: 270 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,270 @@
1+
name: Build Images
2+
3+
on:
4+
workflow_dispatch:
5+
inputs:
6+
branch:
7+
description: 'Branch to build (default: main)'
8+
required: false
9+
type: string
10+
default: 'main'
11+
variants:
12+
description: 'Variants to build (comma-separated, or "all")'
13+
required: false
14+
type: string
15+
default: 'all'
16+
tag:
17+
description: 'Image tag version prefix (default: snapshot-<short-sha>)'
18+
required: false
19+
type: string
20+
21+
concurrency:
22+
group: build-images-${{ github.event.inputs.tag || 'snapshot' }}
23+
cancel-in-progress: false
24+
25+
env:
26+
REGISTRY: ghcr.io
27+
IMAGE_NAME: ${{ github.repository }}
28+
29+
jobs:
30+
# ─────────────────────────────────────────────────────────────────────────────
31+
# Job 1: Resolve matrix and tag, validate inputs
32+
# ─────────────────────────────────────────────────────────────────────────────
33+
matrix:
34+
runs-on: ubuntu-latest
35+
outputs:
36+
variants: ${{ steps.resolve.outputs.variants }}
37+
tag: ${{ steps.resolve.outputs.tag }}
38+
steps:
39+
- uses: actions/checkout@v6
40+
with:
41+
ref: ${{ inputs.branch }}
42+
43+
- name: Validate inputs and resolve matrix
44+
id: resolve
45+
env:
46+
TAG_INPUT: ${{ inputs.tag }}
47+
VARIANTS_INPUT: ${{ inputs.variants }}
48+
BRANCH_INPUT: ${{ inputs.branch }}
49+
run: |
50+
set -euo pipefail
51+
52+
# Validate tag format (alphanumeric, dots, dashes only)
53+
if [ -n "$TAG_INPUT" ]; then
54+
if ! echo "$TAG_INPUT" | grep -qE '^[a-zA-Z0-9._-]+$'; then
55+
echo "::error::Invalid tag format: $TAG_INPUT (allowed: alphanumeric, dots, dashes)"
56+
exit 1
57+
fi
58+
if [ "$TAG_INPUT" = "latest" ]; then
59+
echo "::error::Cannot use 'latest' as tag input — latest is managed automatically for main branch builds"
60+
exit 1
61+
fi
62+
fi
63+
64+
# Resolve variants
65+
ALLOWED="kiro,claude,codex,copilot,cursor,gemini,grok,hermes,mimocode,opencode,antigravity,pi,native,agentcore"
66+
if [ "$VARIANTS_INPUT" = "all" ] || [ -z "$VARIANTS_INPUT" ]; then
67+
VARIANTS="$ALLOWED"
68+
else
69+
VARIANTS="$VARIANTS_INPUT"
70+
for v in $(echo "$VARIANTS" | tr ',' ' '); do
71+
v=$(echo "$v" | xargs)
72+
if ! echo ",$ALLOWED," | grep -q ",$v,"; then
73+
echo "::error::Unknown variant: $v (allowed: $ALLOWED)"
74+
exit 1
75+
fi
76+
done
77+
fi
78+
79+
# Convert to JSON array
80+
JSON=$(echo "$VARIANTS" | tr ',' '\n' | sed 's/^ *//;s/ *$//' | jq -R . | jq -sc .)
81+
echo "variants=${JSON}" >> "$GITHUB_OUTPUT"
82+
83+
# Resolve tag
84+
if [ -n "$TAG_INPUT" ]; then
85+
echo "tag=$TAG_INPUT" >> "$GITHUB_OUTPUT"
86+
else
87+
SHA=$(git rev-parse --short HEAD)
88+
echo "tag=snapshot-${SHA}" >> "$GITHUB_OUTPUT"
89+
fi
90+
91+
# ─────────────────────────────────────────────────────────────────────────────
92+
# Job 2: Build shared builder and push to registry (once per arch)
93+
# ─────────────────────────────────────────────────────────────────────────────
94+
build-core:
95+
needs: matrix
96+
strategy:
97+
fail-fast: true
98+
matrix:
99+
platform:
100+
- { os: linux/amd64, runner: ubuntu-latest, arch: amd64 }
101+
- { os: linux/arm64, runner: ubuntu-24.04-arm, arch: arm64 }
102+
runs-on: ${{ matrix.platform.runner }}
103+
permissions:
104+
contents: read
105+
packages: write
106+
steps:
107+
- uses: actions/checkout@v6
108+
with:
109+
ref: ${{ inputs.branch }}
110+
111+
- uses: docker/setup-buildx-action@v3
112+
113+
- uses: docker/login-action@v4
114+
with:
115+
registry: ${{ env.REGISTRY }}
116+
username: ${{ github.repository_owner }}
117+
password: ${{ secrets.GITHUB_TOKEN }}
118+
119+
- name: Build builder stage and push to registry
120+
uses: docker/build-push-action@v6
121+
with:
122+
context: .
123+
file: Dockerfile.unified
124+
target: builder
125+
platforms: ${{ matrix.platform.os }}
126+
push: true
127+
tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}/builder:${{ needs.matrix.outputs.tag }}-${{ matrix.platform.arch }}
128+
cache-from: type=gha,scope=builder-${{ matrix.platform.arch }}
129+
cache-to: type=gha,scope=builder-${{ matrix.platform.arch }},mode=max
130+
131+
# ─────────────────────────────────────────────────────────────────────────────
132+
# Job 3: Build each agent variant (thin layers, parallel)
133+
# ─────────────────────────────────────────────────────────────────────────────
134+
build-agents:
135+
needs: [matrix, build-core]
136+
strategy:
137+
fail-fast: false
138+
matrix:
139+
variant: ${{ fromJson(needs.matrix.outputs.variants) }}
140+
platform:
141+
- { os: linux/amd64, runner: ubuntu-latest, arch: amd64 }
142+
- { os: linux/arm64, runner: ubuntu-24.04-arm, arch: arm64 }
143+
runs-on: ${{ matrix.platform.runner }}
144+
permissions:
145+
contents: read
146+
packages: write
147+
steps:
148+
- uses: actions/checkout@v6
149+
with:
150+
ref: ${{ inputs.branch }}
151+
152+
- uses: docker/setup-buildx-action@v3
153+
154+
- uses: docker/login-action@v4
155+
with:
156+
registry: ${{ env.REGISTRY }}
157+
username: ${{ github.repository_owner }}
158+
password: ${{ secrets.GITHUB_TOKEN }}
159+
160+
- name: Build and push by digest
161+
id: build
162+
uses: docker/build-push-action@v6
163+
with:
164+
context: .
165+
file: Dockerfile.unified
166+
target: ${{ matrix.variant }}
167+
platforms: ${{ matrix.platform.os }}
168+
build-args: |
169+
BUILDER_IMAGE=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}/builder:${{ needs.matrix.outputs.tag }}-${{ matrix.platform.arch }}
170+
outputs: type=image,name=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }},push-by-digest=true,name-canonical=true,push=true
171+
cache-from: |
172+
type=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}/builder:${{ needs.matrix.outputs.tag }}-${{ matrix.platform.arch }}
173+
type=gha,scope=builder-${{ matrix.platform.arch }}
174+
175+
- name: Export digest
176+
run: |
177+
mkdir -p /tmp/digests
178+
digest="${{ steps.build.outputs.digest }}"
179+
touch "/tmp/digests/${digest#sha256:}"
180+
181+
- name: Upload digest
182+
uses: actions/upload-artifact@v4
183+
with:
184+
name: digests-${{ matrix.variant }}-${{ matrix.platform.arch }}
185+
path: /tmp/digests/*
186+
retention-days: 1
187+
188+
# ─────────────────────────────────────────────────────────────────────────────
189+
# Job 4: Create multi-arch manifest for each variant
190+
# ─────────────────────────────────────────────────────────────────────────────
191+
merge-manifests:
192+
needs: [matrix, build-agents]
193+
strategy:
194+
matrix:
195+
variant: ${{ fromJson(needs.matrix.outputs.variants) }}
196+
runs-on: ubuntu-latest
197+
permissions:
198+
contents: read
199+
packages: write
200+
steps:
201+
- name: Download digests
202+
uses: actions/download-artifact@v4
203+
with:
204+
path: /tmp/digests
205+
pattern: digests-${{ matrix.variant }}-*
206+
merge-multiple: true
207+
208+
- name: Validate digest count
209+
run: |
210+
COUNT=$(ls -1 /tmp/digests/ | wc -l)
211+
if [ "$COUNT" -ne 2 ]; then
212+
echo "::error::Expected 2 digests (amd64 + arm64), got $COUNT"
213+
exit 1
214+
fi
215+
for f in /tmp/digests/*; do
216+
name=$(basename "$f")
217+
if ! echo "$name" | grep -qE '^[a-f0-9]{64}$'; then
218+
echo "::error::Invalid digest format: $name"
219+
exit 1
220+
fi
221+
done
222+
223+
- uses: docker/setup-buildx-action@v3
224+
225+
- uses: docker/login-action@v4
226+
with:
227+
registry: ${{ env.REGISTRY }}
228+
username: ${{ github.repository_owner }}
229+
password: ${{ secrets.GITHUB_TOKEN }}
230+
231+
- name: Create manifest list
232+
env:
233+
BRANCH_INPUT: ${{ inputs.branch }}
234+
run: |
235+
IMAGE="${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}"
236+
TAG="${{ needs.matrix.outputs.tag }}"
237+
VARIANT="${{ matrix.variant }}"
238+
239+
# Tag: <version>-<agent>
240+
docker buildx imagetools create \
241+
-t "${IMAGE}:${TAG}-${VARIANT}" \
242+
$(printf "${IMAGE}@sha256:%s " $(ls /tmp/digests/))
243+
244+
# For kiro (default): also tag as <version> and latest (only from main)
245+
if [ "${VARIANT}" = "kiro" ]; then
246+
docker buildx imagetools create \
247+
-t "${IMAGE}:${TAG}" \
248+
$(printf "${IMAGE}@sha256:%s " $(ls /tmp/digests/))
249+
250+
if [ "$BRANCH_INPUT" = "main" ]; then
251+
docker buildx imagetools create \
252+
-t "${IMAGE}:latest" \
253+
$(printf "${IMAGE}@sha256:%s " $(ls /tmp/digests/))
254+
fi
255+
fi
256+
257+
- name: Summary
258+
env:
259+
BRANCH_INPUT: ${{ inputs.branch }}
260+
run: |
261+
IMAGE="${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}"
262+
TAG="${{ needs.matrix.outputs.tag }}"
263+
VARIANT="${{ matrix.variant }}"
264+
echo "### 📦 \`${IMAGE}:${TAG}-${VARIANT}\`" >> "$GITHUB_STEP_SUMMARY"
265+
if [ "${VARIANT}" = "kiro" ]; then
266+
echo "### 📦 \`${IMAGE}:${TAG}\` (default)" >> "$GITHUB_STEP_SUMMARY"
267+
if [ "$BRANCH_INPUT" = "main" ]; then
268+
echo "### 📦 \`${IMAGE}:latest\`" >> "$GITHUB_STEP_SUMMARY"
269+
fi
270+
fi
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
name: Docker Smoke Test (Unified)
2+
3+
on:
4+
pull_request:
5+
paths:
6+
- 'Dockerfile.unified'
7+
- 'src/**'
8+
- 'crates/**'
9+
- 'Cargo.*'
10+
- 'openab-agent/**'
11+
- 'agy-acp/**'
12+
13+
jobs:
14+
# Build the shared builder stage once, then test agent targets using cache
15+
build-builder:
16+
runs-on: ubuntu-latest
17+
steps:
18+
- uses: actions/checkout@v6
19+
20+
- uses: docker/setup-buildx-action@v3
21+
22+
- name: Build builder stage
23+
uses: docker/build-push-action@v6
24+
with:
25+
context: .
26+
file: Dockerfile.unified
27+
target: builder
28+
load: true
29+
tags: openab-builder:local
30+
cache-to: type=gha,scope=unified-smoke-builder,mode=max
31+
32+
smoke-test-unified:
33+
needs: build-builder
34+
strategy:
35+
fail-fast: false
36+
matrix:
37+
target:
38+
- { name: kiro, agent: "kiro-cli" }
39+
- { name: claude, agent: "claude" }
40+
- { name: codex, agent: "codex-acp" }
41+
- { name: copilot, agent: "copilot" }
42+
- { name: cursor, agent: "cursor-agent" }
43+
- { name: gemini, agent: "gemini" }
44+
- { name: grok, agent: "grok" }
45+
- { name: hermes, agent: "hermes-acp" }
46+
- { name: mimocode, agent: "mimo" }
47+
- { name: opencode, agent: "opencode" }
48+
- { name: antigravity, agent: "agy-acp" }
49+
- { name: pi, agent: "pi-acp" }
50+
- { name: native, agent: "openab-agent" }
51+
- { name: agentcore, agent: "openab" }
52+
runs-on: ubuntu-latest
53+
steps:
54+
- uses: actions/checkout@v6
55+
56+
- uses: docker/setup-buildx-action@v3
57+
58+
- name: Build unified target
59+
uses: docker/build-push-action@v6
60+
with:
61+
context: .
62+
file: Dockerfile.unified
63+
target: ${{ matrix.target.name }}
64+
load: true
65+
tags: openab-unified-${{ matrix.target.name }}:test
66+
cache-from: type=gha,scope=unified-smoke-builder
67+
68+
- name: Verify openab binary exists
69+
run: docker run --rm --entrypoint which openab-unified-${{ matrix.target.name }}:test openab
70+
71+
- name: Verify agent CLI exists
72+
run: docker run --rm --entrypoint which openab-unified-${{ matrix.target.name }}:test ${{ matrix.target.agent }}

0 commit comments

Comments
 (0)