@@ -177,17 +177,60 @@ jobs:
177177 file : operator/reporting/cover.out
178178 format : golang
179179
180- # Build multi-platform container image and push to registry
181- build-and-push-operator :
180+ # Compute image tags and version metadata once for reuse
181+ compute-metadata :
182182 runs-on : ubuntu-latest
183- needs : [tests] # Don't run the build and push if tests fail
184- # Sets the permissions granted to the `GITHUB_TOKEN` for the actions in this job.
183+ needs : [tests]
184+ outputs :
185+ git-sha : ${{ steps.meta.outputs.git-sha }}
186+ version : ${{ steps.meta.outputs.version }}
187+ tags : ${{ steps.meta.outputs.tags }}
188+ steps :
189+ - uses : actions/checkout@v4
190+ - name : Fetch all tags
191+ run : git fetch --tags --force
192+ - name : Compute metadata
193+ id : meta
194+ run : |
195+ export GIT_SHA=$(git rev-parse --short ${{ github.sha }})
196+ echo "git-sha=${GIT_SHA}" >> $GITHUB_OUTPUT
197+
198+ case ${{ github.ref_type }} in
199+ branch)
200+ export VERSION=$(git tag --list 'operator*' --sort=-v:refname | head -n 1 | cut -d/ -f2)+${GIT_SHA}
201+ TAGS="${GIT_SHA} $(echo "${VERSION}" | tr + -)"
202+ ;;
203+ tag)
204+ export VERSION=$(echo "${{ github.ref_name }}" | cut -f 2 -d /)
205+ TAGS="${GIT_SHA} ${VERSION} latest"
206+ ;;
207+ *)
208+ echo "Unknown ref type: ${{ github.ref_type }}"
209+ exit 1
210+ ;;
211+ esac
212+
213+ echo "version=${VERSION}" >> $GITHUB_OUTPUT
214+ echo "tags=${TAGS}" >> $GITHUB_OUTPUT
215+ echo "📦 Version: ${VERSION}"
216+ echo "🏷️ Tags: ${TAGS}"
217+
218+ # Build container images on native architecture runners (much faster than QEMU)
219+ build-operator :
220+ runs-on : ${{ matrix.runner }}
221+ needs : [compute-metadata]
222+ strategy :
223+ matrix :
224+ include :
225+ - platform : linux/amd64
226+ runner : ubuntu-latest
227+ - platform : linux/arm64
228+ runner : ubuntu-24.04-arm
185229 permissions :
186230 contents : read
187231 packages : write
188232 attestations : write
189233 id-token : write
190- #
191234 steps :
192235 - name : Checkout repository
193236 uses : actions/checkout@v4
@@ -199,60 +242,106 @@ jobs:
199242 registry : ${{ env.REGISTRY }}
200243 username : ${{ github.actor }}
201244 password : ${{ secrets.GITHUB_TOKEN }}
202-
203- # Setup for multi-platform builds (linux/amd64, linux/arm64)
204- - name : Set up QEMU
205- uses : docker/setup-qemu-action@v3
206245
207246 - name : Set up Docker Buildx
208247 uses : docker/setup-buildx-action@v3
209248
210- # Build and tag container image based on git ref type
211- - name : Build the operator container image
249+ # Build and tag container image for single platform on native hardware
250+ - name : Build the operator container image (${{ matrix.platform }})
212251 id : build
213252 env :
214- platforms : ${{ env.PLATFORMS }}
253+ GIT_SHA : ${{ needs.compute-metadata.outputs.git-sha }}
254+ VERSION : ${{ needs.compute-metadata.outputs.version }}
215255 run : |
216- apt-get update && apt-get install -y make git jq
256+ sudo apt-get update && sudo apt-get install -y jq
217257 cd operator
218- # if this is a tag build, use the tag as the version, otherwise use the sha
219- git fetch --all
220- export GIT_SHA=$(git rev-parse --short ${{ github.sha }})
221- TAGS="-t ${REGISTRY@L}/${{env.IMAGE_NAME}}/operator:${GIT_SHA}"
222- case ${{ github.ref_type }} in
223- branch)
224- # The last tag + current git sha
225- export OPERATOR_VERSION=$(git tag --list 'operator*' --sort=-v:refname | head -n 1 | cut -d/ -f2)+${GIT_SHA}
226- TAGS="$TAGS -t ${REGISTRY@L}/${{env.IMAGE_NAME}}/operator:$(echo "${OPERATOR_VERSION}" | tr + -)"
227- ;;
228- tag)
229- # The version part of the tag
230- export OPERATOR_VERSION=$(echo "${{ github.ref_name }}" | cut -f 2 -d /)
231- TAGS="$TAGS -t ${REGISTRY@L}/${{env.IMAGE_NAME}}/operator:${OPERATOR_VERSION} -t ${REGISTRY@L}/${{env.IMAGE_NAME}}/operator:latest"
232- ;;
233- *)
234- echo "Unkown type ${{ github.ref_type }}"
235- exit 1
236- ;;
237- esac
258+ PLATFORM_TAG=$(echo "${{ matrix.platform }}" | tr '/' '-')
259+
260+ # Lowercase for Docker compliance
261+ IMAGE_NAME=$(echo "${{env.IMAGE_NAME}}" | tr '[:upper:]' '[:lower:]')
262+ REGISTRY=$(echo "${{env.REGISTRY}}" | tr '[:upper:]' '[:lower:]')
263+
264+ # Build platform-specific tags for all target tags
265+ TAGS=""
266+ for TAG in ${{ needs.compute-metadata.outputs.tags }}; do
267+ TAGS="$TAGS -t ${REGISTRY}/${IMAGE_NAME}/operator:${TAG}-${PLATFORM_TAG}"
268+ done
269+
238270 set -x
239271 docker buildx build \
240272 --build-arg GIT_SHA=${GIT_SHA} \
241- --build-arg VERSION=${OPERATOR_VERSION } \
242- --build-arg GO_VERSION=${GO_VERSION} \
273+ --build-arg VERSION=${VERSION } \
274+ --build-arg GO_VERSION=${{ env. GO_VERSION } } \
243275 --push \
244- --platform ${{ env.PLATFORMS }} \
276+ --platform ${{ matrix.platform }} \
277+ --provenance=false \
245278 ${TAGS@L} \
246279 --metadata-file=metadata.json \
247280 -f ../containers/operator.Dockerfile .
248- cat metadata.json
281+
249282 echo "digest=$(cat metadata.json | jq -r .\"containerimage.digest\")" >> $GITHUB_OUTPUT
250- cat $GITHUB_OUTPUT
283+
284+ # Create multi-platform manifest from individual architecture builds
285+ create-manifest :
286+ runs-on : ubuntu-latest
287+ needs : [compute-metadata, build-operator]
288+ permissions :
289+ contents : read
290+ packages : write
291+ attestations : write
292+ id-token : write
293+ steps :
294+ - name : Log in to the Container registry
295+ uses : docker/login-action@v3
296+ with :
297+ registry : ${{ env.REGISTRY }}
298+ username : ${{ github.actor }}
299+ password : ${{ secrets.GITHUB_TOKEN }}
300+
301+ # Create and push multi-platform manifests, then delete platform-specific tags
302+ - name : Create manifests and cleanup
303+ id : manifest
304+ run : |
305+ sudo apt-get update && sudo apt-get install -y jq
306+
307+ # Lowercase for Docker compliance
308+ IMAGE_NAME=$(echo "${{env.IMAGE_NAME}}" | tr '[:upper:]' '[:lower:]')
309+ REGISTRY=$(echo "${{env.REGISTRY}}" | tr '[:upper:]' '[:lower:]')
310+
311+ # Create manifest for each tag combining amd64 and arm64 images
312+ for TAG in ${{ needs.compute-metadata.outputs.tags }}; do
313+ FULL_TAG="${REGISTRY}/${IMAGE_NAME}/operator:${TAG}"
314+ echo "📦 Creating manifest for $FULL_TAG"
315+ docker manifest create $FULL_TAG \
316+ ${FULL_TAG}-linux-amd64 \
317+ ${FULL_TAG}-linux-arm64
318+ docker manifest push $FULL_TAG
319+ echo "✅ Pushed $FULL_TAG"
320+ done
321+
322+ # Get digest of the main tag (git sha) for attestation
323+ MAIN_TAG="${REGISTRY}/${IMAGE_NAME}/operator:${{ needs.compute-metadata.outputs.git-sha }}"
324+ DIGEST=$(docker manifest inspect $MAIN_TAG | jq -r '.manifests[0].digest')
325+ echo "digest=$DIGEST" >> $GITHUB_OUTPUT
326+ echo "subject-name=${REGISTRY}/${IMAGE_NAME}/operator" >> $GITHUB_OUTPUT
327+
328+ # Clean up platform-specific tags to avoid confusion
329+ echo "🗑️ Cleaning up platform-specific tags..."
330+ for TAG in ${{ needs.compute-metadata.outputs.tags }}; do
331+ for PLATFORM in linux-amd64 linux-arm64; do
332+ PLATFORM_TAG="${REGISTRY}/${IMAGE_NAME}/operator:${TAG}-${PLATFORM}"
333+ echo "Deleting ${PLATFORM_TAG}"
334+ docker run --rm quay.io/skopeo/stable:latest delete \
335+ --creds ${{ github.actor }}:${{ secrets.GITHUB_TOKEN }} \
336+ docker://${PLATFORM_TAG} 2>&1 || echo " (tag may not exist)"
337+ done
338+ done
339+ echo "✅ Cleanup complete"
251340
252- # Generate supply chain security attestation
341+ # Generate supply chain security attestation for the multi-platform manifest
253342 - name : Generate artifact attestation
254343 uses : actions/attest-build-provenance@v2
255344 with :
256- subject-name : ${{ env.REGISTRY }}/${{env.IMAGE_NAME}}/operator
257- subject-digest : ${{ steps.build .outputs.digest }}
345+ subject-name : ${{ steps.manifest.outputs.subject-name }}
346+ subject-digest : ${{ steps.manifest .outputs.digest }}
258347 push-to-registry : true
0 commit comments