2222 type : boolean
2323 default : false
2424
25+ concurrency :
26+ group : build-operator-${{ github.ref_name }}
27+ cancel-in-progress : false
28+
2529env :
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
2935jobs :
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