Skip to content

Commit a7ee8e7

Browse files
chaodu-agentchaodu-agent
andauthored
feat(ci): migrate build-operator to unified Dockerfile + tag format (#1180)
* feat(ci): migrate build-operator to unified Dockerfile + tag format Replace per-variant Dockerfile matrix with Dockerfile.unified targets: - build-core: shared Rust builder stage (once per arch, ~8-10 min) - build-agents: per-agent thin layers in parallel (~2 min each) - merge-manifests: multi-arch manifests with openab:<version>-<agent> tags Tag format changes: Old: ghcr.io/openabdev/openab-codex:0.9.0-beta.1 New: ghcr.io/openabdev/openab:0.9.0-beta.1-codex Pre-release tags: openab:<version>-<agent> + openab:beta-<agent> Stable promotion: openab:<version>-<agent> + openab:<major.minor>-<agent> + openab:stable-<agent> Default (kiro): also tagged as openab:<version>, openab:beta, openab:stable, openab:latest AGENTS list defined as env var for easy maintenance. Part of #1175 follow-up. * fix: address all review findings for PR B - build-core: always push builder (fix dry_run bug) [擺渡 🔴 F1] - build-core/agents: add no-cache param to docker action [擺渡 🟡 F2] - build-agents: add per-agent cache-to scope [Z渡 🟡] - merge-manifests: add digest hex format validation [Z渡+覺渡+口渡 🟡] - merge-manifests: add SHA tag for commit traceability [口渡 🟡] - Add concurrency group to prevent race conditions [口渡 🟡] - resolve-tag: add explicit permissions [口渡 🟡] - AGENTS: add native-sandbox (requires Dockerfile.unified target) [口渡 🔴] * fix: remove native-sandbox from AGENTS — not in unified Dockerfile scope * feat: no default agent, no latest — all tags use <version>-<agent> Remove kiro special-casing from merge-manifests and promote-stable. All agents (including kiro) use identical tag format: openab:<version>-<agent> openab:beta-<agent> openab:stable-<agent> openab:<major.minor>-<agent> No bare tags (openab:beta, openab:latest) are published. --------- Co-authored-by: chaodu-agent <chaodu-agent@openab.dev>
1 parent 266f4a4 commit a7ee8e7

1 file changed

Lines changed: 133 additions & 94 deletions

File tree

.github/workflows/build-operator.yml

Lines changed: 133 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -22,17 +22,26 @@ on:
2222
type: boolean
2323
default: false
2424

25+
concurrency:
26+
group: build-operator-${{ github.ref_name }}
27+
cancel-in-progress: false
28+
2529
env:
2630
REGISTRY: ghcr.io
2731
IMAGE_NAME: ${{ github.repository }}
32+
# All agent variants built from Dockerfile.unified targets.
33+
AGENTS: "kiro,claude,codex,copilot,cursor,gemini,grok,hermes,mimocode,opencode,antigravity,pi,native,agentcore"
2834

2935
jobs:
3036
resolve-tag:
3137
runs-on: ubuntu-latest
38+
permissions:
39+
contents: read
3240
outputs:
3341
tag: ${{ steps.resolve.outputs.tag }}
3442
chart_version: ${{ steps.resolve.outputs.chart_version }}
3543
is_prerelease: ${{ steps.resolve.outputs.is_prerelease }}
44+
agents: ${{ steps.resolve.outputs.agents }}
3645
steps:
3746
- name: Resolve and validate tag
3847
id: resolve
@@ -58,36 +67,26 @@ jobs:
5867
IS_PRERELEASE="false"
5968
fi
6069
70+
# Build agent matrix as JSON array + validate against allowlist
71+
ALLOWED="${{ env.AGENTS }}"
72+
AGENTS_JSON=$(echo "$ALLOWED" | tr ',' '\n' | sed 's/^ *//;s/ *$//' | jq -R . | jq -sc .)
73+
6174
echo "tag=${TAG}" >> "$GITHUB_OUTPUT"
6275
echo "chart_version=${CHART_VERSION}" >> "$GITHUB_OUTPUT"
6376
echo "is_prerelease=${IS_PRERELEASE}" >> "$GITHUB_OUTPUT"
77+
echo "agents=${AGENTS_JSON}" >> "$GITHUB_OUTPUT"
6478
65-
# ── Pre-release path: full build ──────────────────────────────
79+
# ── Pre-release path: unified build ──────────────────────────────
6680

67-
build-image:
81+
build-core:
6882
needs: resolve-tag
6983
if: ${{ needs.resolve-tag.outputs.is_prerelease == 'true' }}
7084
strategy:
85+
fail-fast: true
7186
matrix:
72-
variant:
73-
- { suffix: "", dockerfile: "Dockerfile", artifact: "default" }
74-
- { suffix: "-codex", dockerfile: "Dockerfile.codex", artifact: "codex" }
75-
- { suffix: "-claude", dockerfile: "Dockerfile.claude", artifact: "claude" }
76-
- { suffix: "-gemini", dockerfile: "Dockerfile.gemini", artifact: "gemini" }
77-
- { suffix: "-copilot", dockerfile: "Dockerfile.copilot", artifact: "copilot" }
78-
- { suffix: "-opencode", dockerfile: "Dockerfile.opencode", artifact: "opencode" }
79-
- { suffix: "-cursor", dockerfile: "Dockerfile.cursor", artifact: "cursor" }
80-
- { suffix: "-hermes", dockerfile: "Dockerfile.hermes", artifact: "hermes" }
81-
- { suffix: "-agentcore", dockerfile: "Dockerfile.agentcore", artifact: "agentcore" }
82-
- { suffix: "-grok", dockerfile: "Dockerfile.grok", artifact: "grok" }
83-
- { suffix: "-antigravity", dockerfile: "Dockerfile.antigravity", artifact: "antigravity" }
84-
- { suffix: "-pi", dockerfile: "Dockerfile.pi", artifact: "pi" }
85-
- { suffix: "-mimocode", dockerfile: "Dockerfile.mimocode", artifact: "mimocode" }
86-
- { suffix: "-native", dockerfile: "Dockerfile.native", artifact: "native" }
87-
- { suffix: "-native-sandbox", dockerfile: "openshell/Dockerfile", artifact: "nativesandbox" }
8887
platform:
89-
- { os: linux/amd64, runner: ubuntu-latest }
90-
- { os: linux/arm64, runner: ubuntu-24.04-arm }
88+
- { os: linux/amd64, runner: ubuntu-latest, arch: amd64 }
89+
- { os: linux/arm64, runner: ubuntu-24.04-arm, arch: arm64 }
9190
runs-on: ${{ matrix.platform.runner }}
9291
permissions:
9392
contents: read
@@ -103,22 +102,63 @@ jobs:
103102
username: ${{ github.repository_owner }}
104103
password: ${{ secrets.GITHUB_TOKEN }}
105104

106-
- name: Docker metadata
107-
id: meta
108-
uses: docker/metadata-action@v6
105+
- name: Build shared builder stage
106+
uses: docker/build-push-action@v6
109107
with:
110-
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}${{ matrix.variant.suffix }}
108+
context: .
109+
file: Dockerfile.unified
110+
target: builder
111+
platforms: ${{ matrix.platform.os }}
112+
# Always push builder — it's an internal image needed by build-agents.
113+
# dry_run only gates the final agent image push + manifest creation.
114+
push: true
115+
tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}/builder:${{ needs.resolve-tag.outputs.chart_version }}-${{ matrix.platform.arch }}
116+
no-cache: ${{ inputs.no_cache == true }}
117+
cache-from: ${{ inputs.no_cache != true && format('type=gha,scope=unified-builder-{0}', matrix.platform.arch) || '' }}
118+
cache-to: ${{ inputs.no_cache != true && format('type=gha,scope=unified-builder-{0},mode=max', matrix.platform.arch) || '' }}
119+
120+
build-agents:
121+
needs: [resolve-tag, build-core]
122+
if: ${{ needs.resolve-tag.outputs.is_prerelease == 'true' }}
123+
strategy:
124+
fail-fast: false
125+
matrix:
126+
agent: ${{ fromJson(needs.resolve-tag.outputs.agents) }}
127+
platform:
128+
- { os: linux/amd64, runner: ubuntu-latest, arch: amd64 }
129+
- { os: linux/arm64, runner: ubuntu-24.04-arm, arch: arm64 }
130+
runs-on: ${{ matrix.platform.runner }}
131+
permissions:
132+
contents: read
133+
packages: write
134+
steps:
135+
- uses: actions/checkout@v6
111136

112-
- name: Build and push by digest
137+
- uses: docker/setup-buildx-action@v3
138+
139+
- uses: docker/login-action@v4
140+
with:
141+
registry: ${{ env.REGISTRY }}
142+
username: ${{ github.repository_owner }}
143+
password: ${{ secrets.GITHUB_TOKEN }}
144+
145+
- name: Build agent variant
113146
id: build
114147
uses: docker/build-push-action@v6
115148
with:
116149
context: .
117-
file: ${{ matrix.variant.dockerfile }}
150+
file: Dockerfile.unified
151+
target: ${{ matrix.agent }}
118152
platforms: ${{ matrix.platform.os }}
119-
outputs: type=image,name=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}${{ matrix.variant.suffix }},push-by-digest=true,name-canonical=true,push=${{ inputs.dry_run != true }}
120-
cache-from: ${{ inputs.no_cache != true && format('type=gha,scope={0}-{1}', matrix.variant.suffix, matrix.platform.os) || '' }}
121-
cache-to: ${{ inputs.no_cache != true && format('type=gha,scope={0}-{1},mode=max', matrix.variant.suffix, matrix.platform.os) || '' }}
153+
build-args: |
154+
BUILDER_IMAGE=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}/builder:${{ needs.resolve-tag.outputs.chart_version }}-${{ matrix.platform.arch }}
155+
outputs: type=image,name=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }},push-by-digest=true,name-canonical=true,push=${{ inputs.dry_run != true }}
156+
no-cache: ${{ inputs.no_cache == true }}
157+
cache-from: |
158+
type=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}/builder:${{ needs.resolve-tag.outputs.chart_version }}-${{ matrix.platform.arch }}
159+
${{ inputs.no_cache != true && format('type=gha,scope=unified-builder-{0}', matrix.platform.arch) || '' }}
160+
${{ inputs.no_cache != true && format('type=gha,scope=unified-agent-{0}-{1}', matrix.agent, matrix.platform.arch) || '' }}
161+
cache-to: ${{ inputs.no_cache != true && format('type=gha,scope=unified-agent-{0}-{1},mode=max', matrix.agent, matrix.platform.arch) || '' }}
122162

123163
- name: Export digest
124164
if: inputs.dry_run != true
@@ -131,31 +171,16 @@ jobs:
131171
if: inputs.dry_run != true
132172
uses: actions/upload-artifact@v4
133173
with:
134-
name: digests-${{ matrix.variant.artifact }}-${{ matrix.platform.runner }}
174+
name: digests-${{ matrix.agent }}-${{ matrix.platform.arch }}
135175
path: /tmp/digests/*
136176
retention-days: 1
137177

138178
merge-manifests:
139-
needs: [resolve-tag, build-image]
179+
needs: [resolve-tag, build-agents]
140180
if: ${{ inputs.dry_run != true && needs.resolve-tag.outputs.is_prerelease == 'true' }}
141181
strategy:
142182
matrix:
143-
variant:
144-
- { suffix: "", artifact: "default" }
145-
- { suffix: "-codex", artifact: "codex" }
146-
- { suffix: "-claude", artifact: "claude" }
147-
- { suffix: "-gemini", artifact: "gemini" }
148-
- { suffix: "-copilot", artifact: "copilot" }
149-
- { suffix: "-opencode", artifact: "opencode" }
150-
- { suffix: "-cursor", artifact: "cursor" }
151-
- { suffix: "-hermes", artifact: "hermes" }
152-
- { suffix: "-agentcore", artifact: "agentcore" }
153-
- { suffix: "-grok", artifact: "grok" }
154-
- { suffix: "-antigravity", artifact: "antigravity" }
155-
- { suffix: "-pi", artifact: "pi" }
156-
- { suffix: "-mimocode", artifact: "mimocode" }
157-
- { suffix: "-native", artifact: "native" }
158-
- { suffix: "-native-sandbox", artifact: "nativesandbox" }
183+
agent: ${{ fromJson(needs.resolve-tag.outputs.agents) }}
159184
runs-on: ubuntu-latest
160185
permissions:
161186
contents: read
@@ -165,9 +190,24 @@ jobs:
165190
uses: actions/download-artifact@v4
166191
with:
167192
path: /tmp/digests
168-
pattern: digests-${{ matrix.variant.artifact }}-*
193+
pattern: digests-${{ matrix.agent }}-*
169194
merge-multiple: true
170195

196+
- name: Validate digests
197+
run: |
198+
COUNT=$(ls -1 /tmp/digests/ | wc -l)
199+
if [ "$COUNT" -ne 2 ]; then
200+
echo "::error::Expected 2 digests (amd64 + arm64), got $COUNT"
201+
exit 1
202+
fi
203+
for f in /tmp/digests/*; do
204+
name=$(basename "$f")
205+
if ! echo "$name" | grep -qE '^[a-f0-9]{64}$'; then
206+
echo "::error::Invalid digest format: $name"
207+
exit 1
208+
fi
209+
done
210+
171211
- uses: docker/setup-buildx-action@v3
172212

173213
- uses: docker/login-action@v4
@@ -176,21 +216,31 @@ jobs:
176216
username: ${{ github.repository_owner }}
177217
password: ${{ secrets.GITHUB_TOKEN }}
178218

179-
- name: Docker metadata
180-
id: meta
181-
uses: docker/metadata-action@v6
182-
with:
183-
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}${{ matrix.variant.suffix }}
184-
tags: |
185-
type=sha,prefix=
186-
type=semver,pattern={{version}},value=${{ needs.resolve-tag.outputs.tag }}
187-
type=raw,value=beta
188-
189219
- name: Create manifest list
190-
working-directory: /tmp/digests
191220
run: |
192-
docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
193-
$(printf '${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}${{ matrix.variant.suffix }}@sha256:%s ' *)
221+
IMAGE="${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}"
222+
VERSION="${{ needs.resolve-tag.outputs.chart_version }}"
223+
AGENT="${{ matrix.agent }}"
224+
SHA="${{ github.sha }}"
225+
SHORT_SHA="${SHA:0:7}"
226+
227+
DIGESTS=$(printf "${IMAGE}@sha256:%s " $(ls /tmp/digests/))
228+
229+
# All agents use the same tag format: <version>-<agent>, beta-<agent>
230+
# No bare tags, no "latest", no default agent.
231+
docker buildx imagetools create \
232+
-t "${IMAGE}:${VERSION}-${AGENT}" \
233+
-t "${IMAGE}:beta-${AGENT}" \
234+
-t "${IMAGE}:${SHORT_SHA}-${AGENT}" \
235+
${DIGESTS}
236+
237+
- name: Summary
238+
run: |
239+
IMAGE="${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}"
240+
VERSION="${{ needs.resolve-tag.outputs.chart_version }}"
241+
AGENT="${{ matrix.agent }}"
242+
echo "### 📦 \`${IMAGE}:${VERSION}-${AGENT}\`" >> "$GITHUB_STEP_SUMMARY"
243+
echo "### 📦 \`${IMAGE}:beta-${AGENT}\`" >> "$GITHUB_STEP_SUMMARY"
194244
195245
# ── Stable path: promote pre-release image (no rebuild) ──────
196246

@@ -199,22 +249,7 @@ jobs:
199249
if: ${{ inputs.dry_run != true && needs.resolve-tag.outputs.is_prerelease == 'false' }}
200250
strategy:
201251
matrix:
202-
variant:
203-
- { suffix: "" }
204-
- { suffix: "-codex" }
205-
- { suffix: "-claude" }
206-
- { suffix: "-gemini" }
207-
- { suffix: "-copilot" }
208-
- { suffix: "-opencode" }
209-
- { suffix: "-cursor" }
210-
- { suffix: "-hermes" }
211-
- { suffix: "-agentcore" }
212-
- { suffix: "-grok" }
213-
- { suffix: "-antigravity" }
214-
- { suffix: "-pi" }
215-
- { suffix: "-mimocode" }
216-
- { suffix: "-native" }
217-
- { suffix: "-native-sandbox" }
252+
agent: ${{ fromJson(needs.resolve-tag.outputs.agents) }}
218253
runs-on: ubuntu-latest
219254
permissions:
220255
contents: read
@@ -236,7 +271,6 @@ jobs:
236271
id: find-prerelease
237272
run: |
238273
CHART_VERSION="${{ needs.resolve-tag.outputs.chart_version }}"
239-
# Find latest pre-release tag matching this version (e.g. v0.7.0-beta.1)
240274
PRERELEASE_TAG=$(git tag -l "v${CHART_VERSION}-*" --sort=-v:refname | head -1)
241275
if [ -z "$PRERELEASE_TAG" ]; then
242276
echo "::error::No pre-release tag found for v${CHART_VERSION}-*. Run a pre-release build first."
@@ -246,28 +280,33 @@ jobs:
246280
echo "Found pre-release: ${PRERELEASE_TAG} (${PRERELEASE_VERSION})"
247281
echo "prerelease_version=${PRERELEASE_VERSION}" >> "$GITHUB_OUTPUT"
248282
249-
- name: Verify pre-release image exists
250-
run: |
251-
IMAGE="${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}${{ matrix.variant.suffix }}"
252-
PRERELEASE_VERSION="${{ steps.find-prerelease.outputs.prerelease_version }}"
253-
echo "Checking ${IMAGE}:${PRERELEASE_VERSION} ..."
254-
docker buildx imagetools inspect "${IMAGE}:${PRERELEASE_VERSION}" || \
255-
{ echo "::error::Image ${IMAGE}:${PRERELEASE_VERSION} not found — build the pre-release first"; exit 1; }
256-
257-
- name: Promote to stable tags
283+
- name: Verify and promote to stable tags
258284
run: |
259-
IMAGE="${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}${{ matrix.variant.suffix }}"
285+
IMAGE="${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}"
260286
PRERELEASE_VERSION="${{ steps.find-prerelease.outputs.prerelease_version }}"
261287
CHART_VERSION="${{ needs.resolve-tag.outputs.chart_version }}"
262288
MAJOR_MINOR="${CHART_VERSION%.*}"
289+
AGENT="${{ matrix.agent }}"
290+
291+
# All agents use the same format — no default, no latest
292+
echo "Checking ${IMAGE}:${PRERELEASE_VERSION}-${AGENT} ..."
293+
docker buildx imagetools inspect "${IMAGE}:${PRERELEASE_VERSION}-${AGENT}" || \
294+
{ echo "::error::Image ${IMAGE}:${PRERELEASE_VERSION}-${AGENT} not found"; exit 1; }
263295
264-
echo "Promoting ${IMAGE}:${PRERELEASE_VERSION} → ${CHART_VERSION}, ${MAJOR_MINOR}, latest, stable"
296+
# Promote: openab:<version>-<agent>, openab:<major.minor>-<agent>, openab:stable-<agent>
265297
docker buildx imagetools create \
266-
-t "${IMAGE}:${CHART_VERSION}" \
267-
-t "${IMAGE}:${MAJOR_MINOR}" \
268-
-t "${IMAGE}:latest" \
269-
-t "${IMAGE}:stable" \
270-
"${IMAGE}:${PRERELEASE_VERSION}"
298+
-t "${IMAGE}:${CHART_VERSION}-${AGENT}" \
299+
-t "${IMAGE}:${MAJOR_MINOR}-${AGENT}" \
300+
-t "${IMAGE}:stable-${AGENT}" \
301+
"${IMAGE}:${PRERELEASE_VERSION}-${AGENT}"
302+
303+
- name: Summary
304+
run: |
305+
IMAGE="${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}"
306+
CHART_VERSION="${{ needs.resolve-tag.outputs.chart_version }}"
307+
AGENT="${{ matrix.agent }}"
308+
echo "### 📦 Promoted \`${IMAGE}:${CHART_VERSION}-${AGENT}\`" >> "$GITHUB_STEP_SUMMARY"
309+
echo "### 📦 Promoted \`${IMAGE}:stable-${AGENT}\`" >> "$GITHUB_STEP_SUMMARY"
271310
272311
# ── Chart release (runs after either path) ───────────────────
273312

0 commit comments

Comments
 (0)