Repository: gardener/cc-utils
Pull request #1174: add oci-ocm-multiarch workflow
Author: login: ccwienk
Labels: name: needs/second-opinion
description: Needs second review by someone else
name: status/closed
description: Issue is closed (either delivered or triaged)
name: size/L
description: Denotes a PR that changes 100-499 lines, ignoring generated files.
Created: 2025-02-27T11:57:30Z | Merged: 2025-03-05T06:56:51Z


**Release note**:
<!--  Write your release note:
1. Enter your release note in the below block.
2. If no release note is required, just write "NONE" within the block.

Format of block header: <category> <target_group>
Possible values:
- category:       breaking|feature|bugfix|doc|other
- target_group:   user|operator|developer|dependency
-->
```feature developer
add reusable workflow `oci-ocm-multiarch` that will allow building multi-arch OCI Images, running the build-jobs on runners w/ matching platform, if available.
```



Diff:
diff --git a/.github/workflows/build-and-test.yaml b/.github/workflows/build-and-test.yaml
index 36014a5697..852162f140 100644
--- a/.github/workflows/build-and-test.yaml
+++ b/.github/workflows/build-and-test.yaml
@@ -45,7 +45,7 @@ jobs:
     outputs:
       ocm_repository: ${{ steps.params.outputs.ocm_repository }}
       ocm_releases_repository: ${{ steps.params.outputs.ocm_releases_repository }}
-      oci_repository: ${{ steps.params.outputs.oci_repository }}
+      oci-registry: ${{ steps.params.outputs.oci-registry }}
       oci_platforms: ${{ steps.params.outputs.oci_platforms }}
     steps:
       - uses: actions/checkout@v4
@@ -79,19 +79,19 @@ jobs:
 
           if ${{ inputs.release || false }}; then
             ocm_repository=${releases_repo}
-            oci_repository=${releases_repo}
+            oci_registry=${releases_repo}
           else
             ocm_repository=${snapshots_repo}
-            oci_repository=${snapshots_repo}
+            oci_registry=${snapshots_repo}
           fi
 
           echo "ocm_releases_repository=${releases_repo}"
           echo "ocm_repository=${ocm_repository}"
-          echo "oci_repository=${oci_repository}"
+          echo "oci-registry=${oci_registry}"
 
           echo "ocm_releases_repository=${releases_repo}" >> "${GITHUB_OUTPUT}"
           echo "ocm_repository=${ocm_repository}" >> "${GITHUB_OUTPUT}"
-          echo "oci_repository=${oci_repository}" >> "${GITHUB_OUTPUT}"
+          echo "oci-registry=${oci_registry}" >> "${GITHUB_OUTPUT}"
           echo "oci_platforms=linux/amd64,linux/arm64" >> "${GITHUB_OUTPUT}"
 
   version:
@@ -220,10 +220,11 @@ jobs:
         cp "${resources_file}" dist/ocm_resources.yaml
         echo "ocm_resources=dist/ocm_resources.yaml" >> "${GITHUB_OUTPUT}"
         find "${pkg_dir}"
+        tar czf distribution-packages.tar.gz dist
     - uses: actions/upload-artifact@v4
       with:
         name: distribution-packages
-        path: dist/
+        path: distribution-packages.tar.gz
 
   base-component-descriptor:
     name: Generate Base-OCM-Component-Descriptor
@@ -289,7 +290,6 @@ jobs:
         uses: actions/download-artifact@v4
         with:
           name: distribution-packages
-          path: /tmp/dist
       - name: Retrieve Linting Logs
         uses: actions/download-artifact@v4
         with:
@@ -307,6 +307,9 @@ jobs:
           set -eu
           version=${{ needs.version.outputs.version }}
           ocm_repo=${{ needs.params.outputs.ocm_repository }}
+
+          tar xf distribution-packages.tar.gz -C /tmp
+
           echo "importing base-component-descriptor"
           echo "${{ needs.base-component-descriptor.outputs.component-descriptor }}" \
             > component-descriptor.yaml
@@ -352,7 +355,7 @@ jobs:
             --file component-descriptor.yaml
 
           echo "adding image-resource"
-          echo "${{ needs.images.outputs.ocm_resources }} " | \
+          echo "${{ needs.images.outputs.ocm-resource }} " | \
           python3 -m ocm append resource \
             --file component-descriptor.yaml
 
@@ -379,7 +382,6 @@ jobs:
       uses: actions/download-artifact@v4
       with:
         name: distribution-packages
-        path: /tmp/dist
     - name: lint
       run: |
         # debug
@@ -392,6 +394,7 @@ jobs:
           cat /tmp/apk.log
           exit 1
         fi
+        tar xf distribution-packages.tar.gz -C /tmp
         echo "installing linters"
         export CFLAGS='-Wno-int-conversion'
         if ! pip3 install --upgrade --break-system-packages \
@@ -440,13 +443,13 @@ jobs:
       uses: actions/download-artifact@v4
       with:
         name: distribution-packages
-        path: /tmp/dist
     - name: run-tests
       run: |
         set -eu
         echo "install dependencies for python-packages"
         apk add --no-cache $(cat gardener-cicd-libs.apk-packages)
         echo "install packages"
+        tar xf distribution-packages.tar.gz -C /tmp
         export CFLAGS='-Wno-int-conversion'
         if ! pip3 install --break-system-packages \
           --find-links /tmp/dist \
@@ -471,65 +474,40 @@ jobs:
         .ci/test
 
   images:
-    name: Build OCI Images
+    name: Build Job-Image
     needs:
       - version
-      - package
+      - package # needed indirectly (distribution-packages-artefact)
       - params
-    outputs:
-      oci_image_ref: ${{ steps.image-build.outputs.image_reference }}
-      ocm_resources: ${{ steps.image-build.outputs.ocm_resource }}
-    runs-on: ubuntu-latest
-    environment: build
     permissions:
       contents: read
       packages: write
       id-token: write
-    steps:
-      - uses: actions/checkout@v4
-      - name: Retrieve Distribution Packages
-        uses: actions/download-artifact@v4
-        with:
-          name: distribution-packages
-          path: /tmp/dist
-
-      - name: prepare build-filesystem
-        id: prepare
-        run: |
-          cp -r /tmp/dist .
-          ls -lta
-
-          setuptools_version=${{ needs.version.outputs.setuptools-version }}
-          # workaround: set repository-version to setuptools-version so installation of
-          #             packages will succeed
-          echo "${setuptools_version}" | .ci/write-version
-      - name: Authenticate against OCI-Registry
-        uses: ./.github/actions/oci-auth
-        with:
-          gh-token: ${{ secrets.GITHUB_TOKEN }}
-          oci-image-reference: ${{ needs.params.outputs.oci_repository }}
-      - name: Build OCI Image (using ocm-oci-build-action)
-        uses: ./.github/actions/ocm-oci-build
-        id: image-build
-        with:
-          name: job-image
-          repository: ${{ needs.params.outputs.oci_repository }}/cicd/job-image
-          version: ${{ needs.version.outputs.version }}
-          oci_platforms: ${{ needs.params.outputs.oci_platforms }}
-          context: . # pass modified path rather than clean checkout
-          ocm_labels: |
-            - name: cloud.gardener.cnudie/dso/scanning-hints/package-versions
-              value:
-                - name: containerd
-                  version: v1.6.15
-            - name: gardener.cloud/cve-categorisation
-              value:
-                authentication_enforced: true
-                availability_requirement: low
-                confidentiality_requirement: high
-                integrity_requirement: high
-                network_exposure: protected
-                user_interaction: gardener-operator
+    uses: ./.github/workflows/oci-ocm-multiarch.yaml
+    with:
+      name: job-image
+      oci-registry: ${{ needs.params.outputs.oci-registry }}
+      oci-repository: cicd/job-image
+      oci-platforms: ${{ needs.params.outputs.oci_platforms }}
+      version: ${{ needs.version.outputs.version }}
+      # workaround: we need to use a deviating version for setuptools
+      prebuild-hook: |
+        echo "${{ needs.version.outputs.setuptools-version }}" | .ci/write-version
+      build-ctx-artefact: distribution-packages
+      untar-build-ctx-artefact: distribution-packages.tar.gz
+      ocm-labels: |
+        - name: cloud.gardener.cnudie/dso/scanning-hints/package-versions
+          value:
+            - name: containerd
+              version: v1.6.15
+        - name: gardener.cloud/cve-categorisation
+          value:
+            authentication_enforced: true
+            availability_requirement: low
+            confidentiality_requirement: high
+            integrity_requirement: high
+            network_exposure: protected
+            user_interaction: gardener-operator
 
   documentation:
     name: Generate Documentation
@@ -545,7 +523,7 @@ jobs:
       uses: docker/setup-buildx-action@v3
     - name: Generate Documentation
       run: |
-        image_ref=${{ needs.images.outputs.oci_image_ref }}
+        image_ref=${{ needs.images.outputs.oci-reference }}
         mkdir documentation-out.d
         docker run -v$PWD:/src \
           -e GH_PAGES_PATH=/src/documentation-out.d \
diff --git a/.github/workflows/oci-ocm-multiarch.yaml b/.github/workflows/oci-ocm-multiarch.yaml
new file mode 100644
index 0000000000..935f6ba5ce
--- /dev/null
+++ b/.github/workflows/oci-ocm-multiarch.yaml
@@ -0,0 +1,323 @@
+name: OCI-Build (multiarch)
+description: |
+  Reusable Workflow for building multi-platform OCI Images (grouped with an OCI Image Index),
+  accompanied by an OCM Component Descriptor Resource fragment.
+
+  The workflow will spawn a separate job for each specified platform, trying to use matching runners.
+  Currently, native runners for either `linux/arm64`. and `linux/amd64` are used. Other platforms
+  will be built on runners on `linux/amd64`, using emulation.
+on:
+  workflow_call:
+    inputs:
+      oci-registry:
+        description: |
+          The (optional) oci-registry as part of image-push-target.
+          If both oci-registry, and oci-repository are present, push-target is calculated as:
+          {oci-registry}/{oci-repository}
+        type: string
+        required: false
+      oci-repository:
+        description: |
+          The oci-repository as part of image-push-target.
+          If both oci-registry, and oci-repository are present, push-target is calculated as:
+          {oci-registry}/{oci-repository}
+          If only one of both is passed, the missing parameter will be ignored.
+        type: string
+        required: true
+      oci-platforms:
+        description: |
+          The (comma-separated) OCI Platforms to build for. Only linux/arm64 and linux/amd64 are
+          currently supported for being run natively on arm/x86_64-runners respectively.
+        type: string
+        default: 'linux/arm64,linux/amd64'
+      tag:
+        description: |
+          The image-tag to set. Occurrences of `{version}` will be replaced by `version` input
+          (which is the default).
+        type: string
+        default: '{version}'
+        required: false
+      target:
+        description: |
+          The (optional) target to build (passed to `docker build` as --target).
+        type: string
+        required: false
+      dockerfile:
+        type: string
+        required: false
+      prebuild-hook:
+        description: |
+          if set, value will be executed as a bash-expression _after_ repository-checkout and
+          (if configured) artefact-import, but _before_ running build.
+
+          This can be useful, e.g. to conveniently pass-in some dynamic values from other build-jobs,
+          or to run some pre-generation steps.
+
+          The value will _not_ be quoted, so any valid bash-script can be passed.
+        type: string
+        required: false
+      build-ctx-artefact:
+        description: |
+          If passed, this value is interpreted as an GitHub Actions Artifact name. It is downloaded
+          into the default working directory after doing a repository-checkout.
+
+          Note that thus-imported files will overwrite existing ones from repository-checkout.
+          Contained files will be merged recursively into existing directories.
+        type: string
+        required: false
+      untar-build-ctx-artefact:
+        description: |
+          If passed, along w/ build-ctx-artefact, the downloaded artefact is assumed to contain
+          a tarfile, and will be extracted. This parameter's name is used to specify the
+          filename of said tarfile. This is useful, as GitHubActions choose to use ZIP by default,
+          so wrapping in TAR is a recommended practise, e.g. to preserve executable bits.
+        type: string
+        required: false
+      name:
+        description: |
+          The Image's name (used as OCM-Resource-Name)
+        type: string
+        required: true
+      version:
+        description: |
+          The Image's version (used as OCM-Resource-Version). Will by default also used as `tag`.
+          Must be a valid "relaxed semver".
+        type: string
+        required: true
+      ocm-labels:
+        description: |
+          optional OCM-Labels to add to the OCM Resource fragment. May either be a single label,
+          or a list of labels in either YAML or JSON format.
+
+          Example (single label):
+            ```
+            name: my-label
+            value: my-label-value
+            ```
+          Example (list of labels):
+            ```
+            - name: first-label
+              value: value1
+            - name: second-label
+              value: value2
+            ```
+        type: string
+
+    outputs:
+      ocm-resource:
+        description: |
+          OCM Resource fragment describing the published image.
+        value: ${{ jobs.collect-images.outputs.ocm-resource }}
+      oci-reference:
+        description: |
+          The OCI-Image-Reference to which the built Image-Index was pushed to
+        value: ${{ jobs.collect-images.outputs.oci-reference }}
+
+jobs:
+  preprocess:
+    runs-on: ubuntu-latest
+    outputs:
+      args: ${{ steps.preprocess.outputs.args }}
+      target-image-ref: ${{ steps.preprocess.outputs.target-image-ref }}
+    steps:
+      - name: preprocess
+        shell: python
+        id: preprocess
+        run: |
+          import json
+          import os
+          import pprint
+
+          platform_names = '${{ inputs.oci-platforms }}'.split(',')
+          ref_parts = []
+          if (oci_registry := '${{ inputs.oci-registry }}'):
+            ref_parts.append(oci_registry.strip('/'))
+          if (oci_repository := '${{ inputs.oci-repository }}'):
+            ref_parts.append(oci_repository.strip('/'))
+
+          image_ref_base = '/'.join(ref_parts)
+
+          # todo: validate version
+          version = '${{ inputs.version }}'
+
+          # note: `version` must be in scope to resolve occurrences of `{version}`
+          tag = f'${{ inputs.tag }}'
+
+          runners_by_platform = {
+            'linux/arm64': 'ubuntu-24.04-arm',
+            'linux/amd64': 'ubuntu-latest',
+          }
+
+          args = []
+
+          for platform_name in platform_names:
+            if not platform_name in runners_by_platform:
+              print(f'WARNING: no runner for {platform_name=}')
+            platform_tag = f'${{ inputs.tag }}-{platform_name.replace("/", "_")}'
+            args.append({
+              'platform-name': platform_name,
+              'runner': runners_by_platform.get(platform_name, 'ubuntu-latest'),
+              'tag': platform_tag,
+              'image-reference': f'{image_ref_base}:{platform_tag}',
+            })
+
+          pprint.pprint(args)
+
+          with open(os.environ['GITHUB_OUTPUT'], 'a') as f:
+            f.write(f'args={json.dumps(args)}')
+            f.write('\n')
+
+            f.write(f'target-image-ref={image_ref_base}:${{ inputs.tag }}')
+            f.write('\n')
+
+  build-images:
+    needs: preprocess
+    runs-on: ${{ matrix.args.runner }}
+    permissions:
+      contents: read
+      packages: write
+      id-token: write
+    strategy:
+      matrix:
+        args: ${{ fromJSON(needs.preprocess.outputs.args) }}
+    steps:
+      - uses: docker/setup-buildx-action@v3
+      - uses: gardener/cc-utils/.github/actions/oci-auth@master
+        with:
+          oci-image-reference: ${{ matrix.args.image-reference }}
+          gh-token: ${{ secrets.GITHUB_TOKEN }}
+      - uses: actions/checkout@v4
+      - name: retrieve build-ctx-artefact
+        if: ${{ inputs.build-ctx-artefact }}
+        uses: actions/download-artifact@v4
+        with:
+          name: ${{ inputs.build-ctx-artefact }}
+          merge-multiple: true
+      - name: untar build-ctx-artefact
+        if: ${{ inputs.build-ctx-artefact && inputs.untar-build-ctx-artefact }}
+        run: |
+          fname="${{ inputs.untar-build-ctx-artefact }}"
+          echo "untarring ${fname}"
+          tar xf "${fname}"
+      - name: prebuild-hook
+        if: ${{ inputs.prebuild-hook }}
+        run: |
+          ${{ inputs.prebuild-hook }}
+      - name: 'build ${{ inputs.name }} / ${{ matrix.args.platform-name }}'
+        uses: docker/build-push-action@v6
+        id: build
+        with:
+          push: true
+          tags: ${{ matrix.args.image-reference }}
+          context: '.'
+          target: ${{ inputs.target }}
+          file: ${{ inputs.dockerfile }}
+
+  collect-images:
+    needs:
+      - build-images
+      - preprocess
+    outputs:
+      ocm-resource: ${{ steps.collect-images.outputs.ocm-resource }}
+      oci-reference: ${{ steps.collect-images.outputs.oci-reference }}
+    runs-on: ubuntu-latest
+    permissions:
+      contents: read
+      packages: write
+      id-token: write
+    steps:
+      - uses: gardener/cc-utils/.github/actions/install-gardener-gha-libs@master
+      - uses: gardener/cc-utils/.github/actions/oci-auth@master
+        with:
+          oci-image-reference: ${{ needs.preprocess.outputs.target-image-ref }}
+          gh-token: ${{ secrets.GITHUB_TOKEN }}
+      - name: collect-images
+        id: collect-images
+        shell: python
+        run: |
+          import dataclasses
+          import json
+          import os
+          import pprint
+          import textwrap
+
+          import yaml
+
+          import oci.auth
+          import oci.client
+          import oci.merge
+          import oci.model
+          import ocm
+
+          args = json.loads('''
+            ${{ needs.preprocess.outputs.args }}
+          ''')
+          pprint.pprint(args)
+
+          src_image_refs = [
+            a['image-reference'] for a in args
+          ]
+
+          tgt_image_ref = '${{ needs.preprocess.outputs.target-image-ref }}'
+
+          oci_client = oci.client.Client(
+            credentials_lookup=oci.auth.docker_credentials_lookup(),
+          )
+
+          print(f'merging {src_image_refs=} into {tgt_image_ref=}')
+          oci.merge.into_image_index(
+            src_image_refs=src_image_refs,
+            tgt_image_ref=tgt_image_ref,
+            oci_client=oci_client,
+          )
+
+          print(f'published to {tgt_image_ref=}')
+
+          labels_str = '''${{ inputs.ocm-labels }}'''
+          if labels_str:
+            labels = yaml.safe_load(labels_str)
+            if isinstance(labels, dict):
+              labels = [labels]
+            elif isinstance(labels, list):
+              pass
+            else:
+              raise ValueError(f'label must either be a dict or a list - saw: {type(labels)=}')
+          else:
+            labels = []
+
+          resource = ocm.Resource(
+            name='${{ inputs.name }}',
+            version='${{ inputs.version }}',
+            type=ocm.ArtefactType.OCI_IMAGE,
+            access=ocm.OciAccess(
+              imageReference=tgt_image_ref,
+            ),
+            relation=ocm.ResourceRelation.LOCAL,
+            labels=labels,
+          )
+
+          resource_str = yaml.dump(
+            data=dataclasses.asdict(resource),
+            Dumper=ocm.EnumValueYamlDumper,
+          )
+
+          print('ocm-resource:')
+          print(resource_str)
+
+          with open(os.environ['GITHUB_OUTPUT'], 'a') as f:
+            f.write(f'oci-reference={tgt_image_ref}\n')
+            f.write('ocm-resource<<EOF\n')
+            f.write(resource_str)
+            f.write('EOF\n')
+
+          with open(os.environ['GITHUB_STEP_SUMMARY'], 'a') as f:
+            f.write(textwrap.dedent(f'''\
+            ## OCM OCI Image Build Summary
+            pushed to: `{tgt_image_ref}`
+            platforms: `${{ inputs.oci-platforms }}`
+
+            *OCM Resource Fragment*
+            ```
+            {resource_str}
+            ```
+            '''))


Reviews:
user: login: 8R0WNI3
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-02-27T17:12:50Z
user: login: ccwienk
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-02-27T21:43:49Z
user: login: ccwienk
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-02-27T21:49:01Z
user: login: ccwienk
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-02-27T21:50:53Z
user: login: 8R0WNI3
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-04T19:33:56Z
user: login: ccwienk
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-04T21:05:17Z
user: login: ccwienk
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-04T21:08:43Z
user: login: 8R0WNI3
body: /lgtm
state: APPROVED
author_association: MEMBER
submitted_at: 2025-03-05T05:11:28Z

Review comments:
diff_hunk: @@ -0,0 +1,300 @@
+name: OCI-Build (multiarch)
+description: |
+  Reusable Workflow for building multi-platform OCI Images (grouped with an OCI Image Index),
+  accompanied by an OCM Component Descriptor Resource fragment.
+
+  The workflow will spawn a separate job for each specified platform, trying to use matching runners.
+  Currently, native runners for either `linux/arm64`. and `linux/amd64` are used. Other platforms
+  will be built on runners on `linux/amd64`, using emulation.
+on:
+  workflow_call:
+    inputs:
+      oci-registry:
+        description: |
+          The (optional) oci-registry as part of image-push-target.
+          If both oci-registry, and oci-repository are present, push-target is calculated as:
+          {oci-registry}/{oci-repository}
+          If only one of both is passed, the missing parameter will be ignored.
+        type: string
+        required: true
path: .github/workflows/oci-ocm-multiarch.yaml
user: login: 8R0WNI3
body: The description states this property is optional, but this flag says differently (ditto for `oci-repository` input).
created_at: 2025-02-27T17:01:00Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,300 @@
+name: OCI-Build (multiarch)
+description: |
+  Reusable Workflow for building multi-platform OCI Images (grouped with an OCI Image Index),
+  accompanied by an OCM Component Descriptor Resource fragment.
+
+  The workflow will spawn a separate job for each specified platform, trying to use matching runners.
+  Currently, native runners for either `linux/arm64`. and `linux/amd64` are used. Other platforms
+  will be built on runners on `linux/amd64`, using emulation.
+on:
+  workflow_call:
+    inputs:
+      oci-registry:
+        description: |
+          The (optional) oci-registry as part of image-push-target.
+          If both oci-registry, and oci-repository are present, push-target is calculated as:
+          {oci-registry}/{oci-repository}
+          If only one of both is passed, the missing parameter will be ignored.
+        type: string
+        required: true
+      oci-repository:
+        description: |
+          The (optional) oci-repository as part of image-push-target.
+          If both oci-registry, and oci-repository are present, push-target is calculated as:
+          {oci-registry}/{oci-repository}
+          If only one of both is passed, the missing parameter will be ignored.
+        type: string
+        required: true
+      oci-platforms:
+        description: |
+          The (comma-separated) OCI Platforms to build for. Only linux/arm64 and linux/amd64 are
+          currently supported for being run natively on arm/x86_64-runners respectively.
+        type: string
+        default: 'linux/arm64,linux/amd64'
+      tag:
+        description: |
+          The image-tag to set. Occurrences of `{version}` will be replaced by `version` input
+          (which is the default).
+        type: string
+        default: '{version}'
+        required: false
+      target:
path: .github/workflows/oci-ocm-multiarch.yaml
user: login: 8R0WNI3
body: nit: Add description
created_at: 2025-02-27T17:02:12Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,300 @@
+name: OCI-Build (multiarch)
+description: |
+  Reusable Workflow for building multi-platform OCI Images (grouped with an OCI Image Index),
+  accompanied by an OCM Component Descriptor Resource fragment.
+
+  The workflow will spawn a separate job for each specified platform, trying to use matching runners.
+  Currently, native runners for either `linux/arm64`. and `linux/amd64` are used. Other platforms
+  will be built on runners on `linux/amd64`, using emulation.
+on:
+  workflow_call:
+    inputs:
+      oci-registry:
+        description: |
+          The (optional) oci-registry as part of image-push-target.
+          If both oci-registry, and oci-repository are present, push-target is calculated as:
+          {oci-registry}/{oci-repository}
+          If only one of both is passed, the missing parameter will be ignored.
+        type: string
+        required: true
+      oci-repository:
+        description: |
+          The (optional) oci-repository as part of image-push-target.
+          If both oci-registry, and oci-repository are present, push-target is calculated as:
+          {oci-registry}/{oci-repository}
+          If only one of both is passed, the missing parameter will be ignored.
+        type: string
+        required: true
+      oci-platforms:
+        description: |
+          The (comma-separated) OCI Platforms to build for. Only linux/arm64 and linux/amd64 are
+          currently supported for being run natively on arm/x86_64-runners respectively.
+        type: string
+        default: 'linux/arm64,linux/amd64'
+      tag:
+        description: |
+          The image-tag to set. Occurrences of `{version}` will be replaced by `version` input
+          (which is the default).
+        type: string
+        default: '{version}'
+        required: false
+      target:
+        type: string
+        required: false
+      dockerfile:
+        type: string
+        required: false
+      build-ctx-artefact:
+        description: |
+          If passed, this value is interpreted as an GitHub Actions Artifact name. It is downloaded
+          into the default working directory after doing a repository-checkout.
+
+          Note that thus-imported files will overwrite existing ones from repository-checkout.
+          Contained files will be merged recursively into existing directories.
+        type: string
+        required: false
+      untar-build-ctx-artefact:
+        description: |
+          If passed, along w/ build-ctx-artefact, the downloaded artefact is assumed to contain
+          a tarfile, and will be extracted. This parameter's name is used to specify the
+          filename of said tarfile. This is useful, as GitHubActions choose to use ZIP by default,
+          so wrapping in TAR is a recommended practise, e.g. to preserve executable bits.
+        type: string
+        required: false
+      name:
+        description: |
+          The Image's name (used as OCM-Resource-Name)
+        type: string
+        required: true
+      version:
+        description: |
+          The Image's version (used as OCM-Resource-Version). Will by default also used as `tag`.
+          Must be a valid "relaxed semver".
+        type: string
+        required: true
+      ocm-labels:
+        description: |
+          optional OCM-Labels to add to the OCM Resource fragment. May either be a single label,
+          or a list of labels in either YAML or JSON format.
+
+          Example (single label):
+            ```
+            name: my-label
+            value: my-label-value
+            ```
+          Example (list of labels):
+            ```
+            - name: first-label
+              value: value1
+            - name: second-label
+              value: value2
+            ```
+        type: string
+
+    outputs:
+      ocm-resource:
+        description: |
+          OCM Resource fragment describing the published image.
+        value: ${{ jobs.collect-images.outputs.ocm-resource }}
+
+jobs:
+  preprocess:
+    runs-on: ubuntu-latest
+    outputs:
+      args: ${{ steps.preprocess.outputs.args }}
+      target-image-ref: ${{ steps.preprocess.outputs.target-image-ref }}
+    steps:
+      - name: preprocess
+        shell: python
+        id: preprocess
+        run: |
+          import json
+          import os
+          import pprint
+
+          platform_names = '${{ inputs.oci-platforms }}'.split(',')
+          ref_parts = []
+          if (oci_registry := '${{ inputs.oci-registry }}'):
+            ref_parts.append(oci_registry.strip('/'))
+          if (oci_repository := '${{ inputs.oci-repository }}'):
+            ref_parts.append(oci_repository.strip('/'))
+
+          image_ref_base = '/'.join(ref_parts)
+
+          # todo: validate version
+          version = '${{ inputs.version }}'
+
+          # note: `version` must be in scope to resolve occurrences of `{version}`
+          tag = f'${{ inputs.tag }}'
+
+          runners_by_platform = {
+            'linux/arm64': 'ubuntu-24.04-arm',
+            'linux/amd64': 'ubuntu-latest',
+          }
+
+          args = []
+
+          for platform_name in platform_names:
+            if not platform_name in runners_by_platform:
+              print(f'WARNING: no runner for {platform_name=}')
+            platform_tag = f'${{ inputs.tag }}-{platform_name.replace("/", "_")}'
+            args.append({
+              'platform-name': platform_name,
+              'runner': runners_by_platform.get(platform_name, 'ubuntu-latest'),
+              'tag': f'${{ inputs.tag }}-{platform_name.replace("/", "_")}',
path: .github/workflows/oci-ocm-multiarch.yaml
user: login: 8R0WNI3
body: nit: Reuse already defined `platform_tag`
created_at: 2025-02-27T17:08:21Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,300 @@
+name: OCI-Build (multiarch)
+description: |
+  Reusable Workflow for building multi-platform OCI Images (grouped with an OCI Image Index),
+  accompanied by an OCM Component Descriptor Resource fragment.
+
+  The workflow will spawn a separate job for each specified platform, trying to use matching runners.
+  Currently, native runners for either `linux/arm64`. and `linux/amd64` are used. Other platforms
+  will be built on runners on `linux/amd64`, using emulation.
+on:
+  workflow_call:
+    inputs:
+      oci-registry:
+        description: |
+          The (optional) oci-registry as part of image-push-target.
+          If both oci-registry, and oci-repository are present, push-target is calculated as:
+          {oci-registry}/{oci-repository}
+          If only one of both is passed, the missing parameter will be ignored.
+        type: string
+        required: true
+      oci-repository:
+        description: |
+          The (optional) oci-repository as part of image-push-target.
+          If both oci-registry, and oci-repository are present, push-target is calculated as:
+          {oci-registry}/{oci-repository}
+          If only one of both is passed, the missing parameter will be ignored.
+        type: string
+        required: true
+      oci-platforms:
+        description: |
+          The (comma-separated) OCI Platforms to build for. Only linux/arm64 and linux/amd64 are
+          currently supported for being run natively on arm/x86_64-runners respectively.
+        type: string
+        default: 'linux/arm64,linux/amd64'
+      tag:
+        description: |
+          The image-tag to set. Occurrences of `{version}` will be replaced by `version` input
+          (which is the default).
+        type: string
+        default: '{version}'
+        required: false
+      target:
path: .github/workflows/oci-ocm-multiarch.yaml
user: login: ccwienk
body: done
created_at: 2025-02-27T21:43:49Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,300 @@
+name: OCI-Build (multiarch)
+description: |
+  Reusable Workflow for building multi-platform OCI Images (grouped with an OCI Image Index),
+  accompanied by an OCM Component Descriptor Resource fragment.
+
+  The workflow will spawn a separate job for each specified platform, trying to use matching runners.
+  Currently, native runners for either `linux/arm64`. and `linux/amd64` are used. Other platforms
+  will be built on runners on `linux/amd64`, using emulation.
+on:
+  workflow_call:
+    inputs:
+      oci-registry:
+        description: |
+          The (optional) oci-registry as part of image-push-target.
+          If both oci-registry, and oci-repository are present, push-target is calculated as:
+          {oci-registry}/{oci-repository}
+          If only one of both is passed, the missing parameter will be ignored.
+        type: string
+        required: true
path: .github/workflows/oci-ocm-multiarch.yaml
user: login: ccwienk
body: you are correct, of course. However, I wonder: _should_ we actually make both inputs optional?

My rationale behind splitting this into two is to follow common practise we see within gardener-org (probably not only there), where registry and repository are often split. this will also work well w/ our approach of using different registries (snapshots vs releases).

If we keep those inputs (both) as mandatory, GHA will do a (much) earlier validation of wrong usage (we can fail only later, at runtime, otherwise). GHA unfortunately does not allow specification of complex conditions ("at least one of both must be given, and non-empty").

So I would currently tend (different than orginally stated in documentation) to make both inputs mandatory. WDYT?

Edit: I think it will make sense to make `oci-repository` mandatory, and `oci-registry` optional
created_at: 2025-02-27T21:49:01Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,300 @@
+name: OCI-Build (multiarch)
+description: |
+  Reusable Workflow for building multi-platform OCI Images (grouped with an OCI Image Index),
+  accompanied by an OCM Component Descriptor Resource fragment.
+
+  The workflow will spawn a separate job for each specified platform, trying to use matching runners.
+  Currently, native runners for either `linux/arm64`. and `linux/amd64` are used. Other platforms
+  will be built on runners on `linux/amd64`, using emulation.
+on:
+  workflow_call:
+    inputs:
+      oci-registry:
+        description: |
+          The (optional) oci-registry as part of image-push-target.
+          If both oci-registry, and oci-repository are present, push-target is calculated as:
+          {oci-registry}/{oci-repository}
+          If only one of both is passed, the missing parameter will be ignored.
+        type: string
+        required: true
+      oci-repository:
+        description: |
+          The (optional) oci-repository as part of image-push-target.
+          If both oci-registry, and oci-repository are present, push-target is calculated as:
+          {oci-registry}/{oci-repository}
+          If only one of both is passed, the missing parameter will be ignored.
+        type: string
+        required: true
+      oci-platforms:
+        description: |
+          The (comma-separated) OCI Platforms to build for. Only linux/arm64 and linux/amd64 are
+          currently supported for being run natively on arm/x86_64-runners respectively.
+        type: string
+        default: 'linux/arm64,linux/amd64'
+      tag:
+        description: |
+          The image-tag to set. Occurrences of `{version}` will be replaced by `version` input
+          (which is the default).
+        type: string
+        default: '{version}'
+        required: false
+      target:
+        type: string
+        required: false
+      dockerfile:
+        type: string
+        required: false
+      build-ctx-artefact:
+        description: |
+          If passed, this value is interpreted as an GitHub Actions Artifact name. It is downloaded
+          into the default working directory after doing a repository-checkout.
+
+          Note that thus-imported files will overwrite existing ones from repository-checkout.
+          Contained files will be merged recursively into existing directories.
+        type: string
+        required: false
+      untar-build-ctx-artefact:
+        description: |
+          If passed, along w/ build-ctx-artefact, the downloaded artefact is assumed to contain
+          a tarfile, and will be extracted. This parameter's name is used to specify the
+          filename of said tarfile. This is useful, as GitHubActions choose to use ZIP by default,
+          so wrapping in TAR is a recommended practise, e.g. to preserve executable bits.
+        type: string
+        required: false
+      name:
+        description: |
+          The Image's name (used as OCM-Resource-Name)
+        type: string
+        required: true
+      version:
+        description: |
+          The Image's version (used as OCM-Resource-Version). Will by default also used as `tag`.
+          Must be a valid "relaxed semver".
+        type: string
+        required: true
+      ocm-labels:
+        description: |
+          optional OCM-Labels to add to the OCM Resource fragment. May either be a single label,
+          or a list of labels in either YAML or JSON format.
+
+          Example (single label):
+            ```
+            name: my-label
+            value: my-label-value
+            ```
+          Example (list of labels):
+            ```
+            - name: first-label
+              value: value1
+            - name: second-label
+              value: value2
+            ```
+        type: string
+
+    outputs:
+      ocm-resource:
+        description: |
+          OCM Resource fragment describing the published image.
+        value: ${{ jobs.collect-images.outputs.ocm-resource }}
+
+jobs:
+  preprocess:
+    runs-on: ubuntu-latest
+    outputs:
+      args: ${{ steps.preprocess.outputs.args }}
+      target-image-ref: ${{ steps.preprocess.outputs.target-image-ref }}
+    steps:
+      - name: preprocess
+        shell: python
+        id: preprocess
+        run: |
+          import json
+          import os
+          import pprint
+
+          platform_names = '${{ inputs.oci-platforms }}'.split(',')
+          ref_parts = []
+          if (oci_registry := '${{ inputs.oci-registry }}'):
+            ref_parts.append(oci_registry.strip('/'))
+          if (oci_repository := '${{ inputs.oci-repository }}'):
+            ref_parts.append(oci_repository.strip('/'))
+
+          image_ref_base = '/'.join(ref_parts)
+
+          # todo: validate version
+          version = '${{ inputs.version }}'
+
+          # note: `version` must be in scope to resolve occurrences of `{version}`
+          tag = f'${{ inputs.tag }}'
+
+          runners_by_platform = {
+            'linux/arm64': 'ubuntu-24.04-arm',
+            'linux/amd64': 'ubuntu-latest',
+          }
+
+          args = []
+
+          for platform_name in platform_names:
+            if not platform_name in runners_by_platform:
+              print(f'WARNING: no runner for {platform_name=}')
+            platform_tag = f'${{ inputs.tag }}-{platform_name.replace("/", "_")}'
+            args.append({
+              'platform-name': platform_name,
+              'runner': runners_by_platform.get(platform_name, 'ubuntu-latest'),
+              'tag': f'${{ inputs.tag }}-{platform_name.replace("/", "_")}',
path: .github/workflows/oci-ocm-multiarch.yaml
user: login: ccwienk
body: yet again: good catch - done
created_at: 2025-02-27T21:50:52Z
author_association: MEMBER
diff_hunk: @@ -467,65 +470,40 @@ jobs:
         .ci/test
 
   images:
-    name: Build OCI Images
+    name: Build Job-Image
     needs:
       - version
-      - package
+      - package # needed indirectly (distribution-packages-artefact)
       - params
-    outputs:
-      oci_image_ref: ${{ steps.image-build.outputs.image_reference }}
-      ocm_resources: ${{ steps.image-build.outputs.ocm_resource }}
-    runs-on: ubuntu-latest
-    environment: build
     permissions:
       contents: read
       packages: write
       id-token: write
-    steps:
-      - uses: actions/checkout@v4
-      - name: Retrieve Distribution Packages
-        uses: actions/download-artifact@v4
-        with:
-          name: distribution-packages
-          path: /tmp/dist
-
-      - name: prepare build-filesystem
-        id: prepare
-        run: |
-          cp -r /tmp/dist .
-          ls -lta
-
-          setuptools_version=${{ needs.version.outputs.setuptools-version }}
-          # workaround: set repository-version to setuptools-version so installation of
-          #             packages will succeed
-          echo "${setuptools_version}" | .ci/write-version
-      - name: Authenticate against OCI-Registry
-        uses: ./.github/actions/oci-auth
-        with:
-          gh-token: ${{ secrets.GITHUB_TOKEN }}
-          oci-image-reference: ${{ needs.params.outputs.oci_repository }}
-      - name: Build OCI Image (using ocm-oci-build-action)
-        uses: ./.github/actions/ocm-oci-build
-        id: image-build
-        with:
-          name: job-image
-          repository: ${{ needs.params.outputs.oci_repository }}/cicd/job-image
-          version: ${{ needs.version.outputs.version }}
-          oci_platforms: ${{ needs.params.outputs.oci_platforms }}
-          context: . # pass modified path rather than clean checkout
-          ocm_labels: |
-            - name: cloud.gardener.cnudie/dso/scanning-hints/package-versions
-              value:
-                - name: containerd
-                  version: v1.6.15
-            - name: gardener.cloud/cve-categorisation
-              value:
-                authentication_enforced: true
-                availability_requirement: low
-                confidentiality_requirement: high
-                integrity_requirement: high
-                network_exposure: protected
-                user_interaction: gardener-operator
+    uses: ./.github/workflows/oci-ocm-multiarch.yaml
+    with:
+      name: job-image
+      oci-registry: ${{ needs.params.outputs.oci_repository }}
path: .github/workflows/build-and-test.yaml
user: login: 8R0WNI3
body: Hmm, can we consistently use `oci-registry` != `oci-repository`? I don't like that `oci_repository` has to be passed here to `oci-registry` although there is another input called `oci-repository` which expects `cicd/job-image`...
created_at: 2025-03-04T19:20:32Z
author_association: MEMBER
diff_hunk: @@ -467,65 +470,40 @@ jobs:
         .ci/test
 
   images:
-    name: Build OCI Images
+    name: Build Job-Image
     needs:
       - version
-      - package
+      - package # needed indirectly (distribution-packages-artefact)
       - params
-    outputs:
-      oci_image_ref: ${{ steps.image-build.outputs.image_reference }}
-      ocm_resources: ${{ steps.image-build.outputs.ocm_resource }}
-    runs-on: ubuntu-latest
-    environment: build
     permissions:
       contents: read
       packages: write
       id-token: write
-    steps:
-      - uses: actions/checkout@v4
-      - name: Retrieve Distribution Packages
-        uses: actions/download-artifact@v4
-        with:
-          name: distribution-packages
-          path: /tmp/dist
-
-      - name: prepare build-filesystem
-        id: prepare
-        run: |
-          cp -r /tmp/dist .
-          ls -lta
-
-          setuptools_version=${{ needs.version.outputs.setuptools-version }}
-          # workaround: set repository-version to setuptools-version so installation of
-          #             packages will succeed
-          echo "${setuptools_version}" | .ci/write-version
-      - name: Authenticate against OCI-Registry
-        uses: ./.github/actions/oci-auth
-        with:
-          gh-token: ${{ secrets.GITHUB_TOKEN }}
-          oci-image-reference: ${{ needs.params.outputs.oci_repository }}
-      - name: Build OCI Image (using ocm-oci-build-action)
-        uses: ./.github/actions/ocm-oci-build
-        id: image-build
-        with:
-          name: job-image
-          repository: ${{ needs.params.outputs.oci_repository }}/cicd/job-image
-          version: ${{ needs.version.outputs.version }}
-          oci_platforms: ${{ needs.params.outputs.oci_platforms }}
-          context: . # pass modified path rather than clean checkout
-          ocm_labels: |
-            - name: cloud.gardener.cnudie/dso/scanning-hints/package-versions
-              value:
-                - name: containerd
-                  version: v1.6.15
-            - name: gardener.cloud/cve-categorisation
-              value:
-                authentication_enforced: true
-                availability_requirement: low
-                confidentiality_requirement: high
-                integrity_requirement: high
-                network_exposure: protected
-                user_interaction: gardener-operator
+    uses: ./.github/workflows/oci-ocm-multiarch.yaml
+    with:
+      name: job-image
+      oci-registry: ${{ needs.params.outputs.oci_repository }}
+      oci-repository: cicd/job-image
+      oci-platforms: 'linux/arm64,linux/amd64'
path: .github/workflows/build-and-test.yaml
user: login: 8R0WNI3
body: Shouldn't this be read from `needs.params.outputs.oci_platforms`?
created_at: 2025-03-04T19:22:09Z
author_association: MEMBER
diff_hunk: @@ -467,65 +470,40 @@ jobs:
         .ci/test
 
   images:
-    name: Build OCI Images
+    name: Build Job-Image
     needs:
       - version
-      - package
+      - package # needed indirectly (distribution-packages-artefact)
       - params
-    outputs:
path: .github/workflows/build-and-test.yaml
user: login: 8R0WNI3
body: Does this jobnot have to define any outputs anymore? Like `oci-reference` or`ocm-resource`?
created_at: 2025-03-04T19:27:55Z
author_association: MEMBER
diff_hunk: @@ -467,65 +470,40 @@ jobs:
         .ci/test
 
   images:
-    name: Build OCI Images
+    name: Build Job-Image
     needs:
       - version
-      - package
+      - package # needed indirectly (distribution-packages-artefact)
       - params
-    outputs:
-      oci_image_ref: ${{ steps.image-build.outputs.image_reference }}
-      ocm_resources: ${{ steps.image-build.outputs.ocm_resource }}
-    runs-on: ubuntu-latest
-    environment: build
     permissions:
       contents: read
       packages: write
       id-token: write
-    steps:
-      - uses: actions/checkout@v4
-      - name: Retrieve Distribution Packages
-        uses: actions/download-artifact@v4
-        with:
-          name: distribution-packages
-          path: /tmp/dist
-
-      - name: prepare build-filesystem
-        id: prepare
-        run: |
-          cp -r /tmp/dist .
-          ls -lta
-
-          setuptools_version=${{ needs.version.outputs.setuptools-version }}
-          # workaround: set repository-version to setuptools-version so installation of
-          #             packages will succeed
-          echo "${setuptools_version}" | .ci/write-version
-      - name: Authenticate against OCI-Registry
-        uses: ./.github/actions/oci-auth
-        with:
-          gh-token: ${{ secrets.GITHUB_TOKEN }}
-          oci-image-reference: ${{ needs.params.outputs.oci_repository }}
-      - name: Build OCI Image (using ocm-oci-build-action)
-        uses: ./.github/actions/ocm-oci-build
-        id: image-build
-        with:
-          name: job-image
-          repository: ${{ needs.params.outputs.oci_repository }}/cicd/job-image
-          version: ${{ needs.version.outputs.version }}
-          oci_platforms: ${{ needs.params.outputs.oci_platforms }}
-          context: . # pass modified path rather than clean checkout
-          ocm_labels: |
-            - name: cloud.gardener.cnudie/dso/scanning-hints/package-versions
-              value:
-                - name: containerd
-                  version: v1.6.15
-            - name: gardener.cloud/cve-categorisation
-              value:
-                authentication_enforced: true
-                availability_requirement: low
-                confidentiality_requirement: high
-                integrity_requirement: high
-                network_exposure: protected
-                user_interaction: gardener-operator
+    uses: ./.github/workflows/oci-ocm-multiarch.yaml
+    with:
+      name: job-image
+      oci-registry: ${{ needs.params.outputs.oci_repository }}
path: .github/workflows/build-and-test.yaml
user: login: ccwienk
body: yep, I also did not like this whilst creating this diff (but did not yet add a refactoring-commit). will do so
edit: done
created_at: 2025-03-04T21:05:17Z
author_association: MEMBER
diff_hunk: @@ -467,65 +470,40 @@ jobs:
         .ci/test
 
   images:
-    name: Build OCI Images
+    name: Build Job-Image
     needs:
       - version
-      - package
+      - package # needed indirectly (distribution-packages-artefact)
       - params
-    outputs:
path: .github/workflows/build-and-test.yaml
user: login: ccwienk
body: If I am not mistaken, outputs do not need to be "re-declared" in case of a job that calls a reusable workflow (will double-check)

update: As you can see [here](https://github.com/ccwienk/cc-utils/actions/runs/13576326000/job/37953407580#step:7:289), the ocm-resource-fragment as conveyed via output (`images.outputs.ocm-resource`) was correctly passed to component-descriptor-job. 😌 
created_at: 2025-03-04T21:08:43Z
author_association: MEMBER

PR comments:
user: login: ccwienk
created_at: 2025-02-27T12:19:20Z
body: there are still a couple of open TODOs (which I plan to add to this pullrequest):
- [x] re-use in cc-utils + concourse-repository
- [x] add prebuild-hook (required by both cc-utils, and concourse-repository)
- [ ] upload ocm-resource-fragments as artefacts (to support more than one image - thanks GHA for making such simple things so cumbersome)
- [ ] maybe: add option to import commit
- [ ] maybe (probably separate pullrequest): export/merge-ocm-fragment-actions
author_association: MEMBER
user: login: gardener-robot
created_at: 2025-03-03T03:05:34Z
body: @zkdev, @tuananh17n You have pull request review open invite, please check
author_association: NONE
user: login: ccwienk
created_at: 2025-03-04T18:59:53Z
body: > there are still a couple of open TODOs (which I plan to add to this pullrequest):
> 
> * [x]  re-use in cc-utils + concourse-repository
> * [x]  add prebuild-hook (required by both cc-utils, and concourse-repository)
> * [ ]  upload ocm-resource-fragments as artefacts (to support more than one image - thanks GHA for making such simple things so cumbersome)
> * [ ]  maybe: add option to import commit
> * [ ]  maybe (probably separate pullrequest): export/merge-ocm-fragment-actions

I think it will make more sense to work on the third and fifth bullet-point in a separate pullrequest (+ to drop the fourth until actually needed)
author_association: MEMBER
user: login: ccwienk
created_at: 2025-03-04T21:26:47Z
body: note to self: remember to autosquash prior to merging
author_association: MEMBER