name: Docker Build on: workflow_call: inputs: component: description: "Component to build (gateway, supervisor, cli)" required: true type: string timeout-minutes: description: "Per-arch Docker image job timeout in minutes" required: false type: number default: 20 push: description: "Push image to registry" required: false type: boolean default: true platform: description: "Target platform(s) for Docker build" required: false type: string default: "linux/amd64,linux/arm64" runner: description: "Deprecated; per-arch native runners are selected automatically" required: false type: string default: "linux-amd64-cpu8" cargo-version: description: "Pre-computed cargo version (skips internal git-based computation)" required: false type: string default: "" image-tag: description: "Image tag base to build/push (defaults to the GitHub SHA)" required: false type: string default: "" checkout-ref: description: "Git ref to check out for build inputs (defaults to the workflow SHA)" required: false type: string default: "" publish-manifest: description: "Push the bare-SHA manifest. Set false for single-arch branch workflows." required: false type: boolean default: true env: MISE_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} permissions: contents: read packages: write defaults: run: shell: bash jobs: resolve: name: Resolve build plan runs-on: ubuntu-latest outputs: matrix: ${{ steps.resolve.outputs.matrix }} platform_count: ${{ steps.resolve.outputs.platform_count }} arches: ${{ steps.resolve.outputs.arches }} binary_component: ${{ steps.resolve.outputs.binary_component }} binary_name: ${{ steps.resolve.outputs.binary_name }} artifact_prefix: ${{ steps.resolve.outputs.artifact_prefix }} image_tag_base: ${{ steps.resolve.outputs.image_tag_base }} features: ${{ steps.resolve.outputs.features }} has_image: ${{ steps.resolve.outputs.has_image }} steps: - name: Resolve component and platform matrix id: resolve run: | set -euo pipefail component="${{ inputs.component }}" case "$component" in gateway) binary_component=gateway binary_name=openshell-gateway features="bundled-z3" has_image=true ;; supervisor) binary_component=sandbox binary_name=openshell-sandbox features="" has_image=true ;; cli) binary_component=cli binary_name=openshell features="" has_image=false ;; *) echo "unsupported component: $component" >&2 exit 1 ;; esac image_tag_base="${{ inputs['image-tag'] }}" if [[ -z "$image_tag_base" ]]; then image_tag_base="$GITHUB_SHA" fi platform_input="${{ inputs.platform }}" platform_input="${platform_input//[[:space:]]/}" if [[ -z "$platform_input" ]]; then echo "platform input must not be empty" >&2 exit 1 fi IFS=',' read -r -a platforms <<< "$platform_input" matrix='{"include":[' arches=() count=0 for platform in "${platforms[@]}"; do case "$platform" in linux/amd64) arch=amd64 runner=linux-amd64-cpu8 ;; linux/arm64) arch=arm64 runner=linux-arm64-cpu8 ;; *) echo "unsupported platform: $platform" >&2 echo "supported platforms: linux/amd64, linux/arm64" >&2 exit 1 ;; esac if [[ $count -gt 0 ]]; then matrix+=',' fi matrix+='{"platform":"'"$platform"'","arch":"'"$arch"'","runner":"'"$runner"'"}' arches+=("$arch") count=$((count + 1)) done matrix+=']}' { echo "matrix=$matrix" echo "platform_count=$count" echo "arches=${arches[*]}" echo "binary_component=$binary_component" echo "binary_name=$binary_name" echo "artifact_prefix=rust-binary-${component}" echo "image_tag_base=$image_tag_base" echo "features=$features" echo "has_image=$has_image" } >> "$GITHUB_OUTPUT" rust-binary: name: Rust ${{ needs.resolve.outputs.binary_component }} (${{ matrix.arch }}) needs: resolve permissions: contents: read packages: read strategy: fail-fast: false matrix: ${{ fromJSON(needs.resolve.outputs.matrix) }} uses: ./.github/workflows/rust-native-build.yml with: component: ${{ needs.resolve.outputs.binary_component }} arch: ${{ matrix.arch }} cargo-version: ${{ inputs['cargo-version'] }} image-tag: ${{ needs.resolve.outputs.image_tag_base }} checkout-ref: ${{ inputs['checkout-ref'] }} features: ${{ needs.resolve.outputs.features }} artifact-name: ${{ needs.resolve.outputs.artifact_prefix }}-linux-${{ matrix.arch }} secrets: inherit build: name: Build ${{ inputs.component }} (${{ matrix.arch }}) needs: [resolve, rust-binary] if: needs.resolve.outputs.has_image == 'true' runs-on: ${{ matrix.runner }} timeout-minutes: ${{ inputs['timeout-minutes'] }} strategy: fail-fast: false matrix: ${{ fromJSON(needs.resolve.outputs.matrix) }} container: image: ghcr.io/nvidia/openshell/ci:latest credentials: username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} options: --privileged volumes: - /var/run/docker.sock:/var/run/docker.sock # Expose the nv-gha-runners buildkitd.toml registry mirror config # inside the container so setup-buildx can read it. - /etc/buildkit:/etc/buildkit:ro env: IMAGE_TAG: ${{ format('{0}-{1}', needs.resolve.outputs.image_tag_base, matrix.arch) }} IMAGE_REGISTRY: ghcr.io/nvidia/openshell DOCKER_PUSH: ${{ inputs.push && '1' || '0' }} DOCKER_PLATFORM: ${{ matrix.platform }} steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: ref: ${{ inputs['checkout-ref'] || github.sha }} fetch-depth: 0 - name: Mark workspace safe for git run: git config --global --add safe.directory "$GITHUB_WORKSPACE" - name: Install tools run: mise install --locked - name: Log in to GHCR if: ${{ inputs.push }} run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u "${{ github.actor }}" --password-stdin - name: Set up buildx (local driver) uses: ./.github/actions/setup-buildx with: buildkitd-config: /etc/buildkit/buildkitd.toml - name: Download Rust binary artifact uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: name: ${{ needs.resolve.outputs.artifact_prefix }}-linux-${{ matrix.arch }} path: prebuilt-rust-binary - name: Stage Rust binary in Docker build context run: | set -euo pipefail binary="${{ needs.resolve.outputs.binary_name }}" download_dir="prebuilt-rust-binary" stage="deploy/docker/.build/prebuilt-binaries/${{ matrix.arch }}" found="$(find "$download_dir" -type f -name "$binary" -print -quit)" if [[ -z "$found" ]]; then echo "missing downloaded artifact file: $binary" >&2 find "$download_dir" -maxdepth 4 -type f -print >&2 || true exit 1 fi mkdir -p "$stage" install -m 0755 "$found" "$stage/$binary" ls -lh "$stage/" - name: Build ${{ inputs.component }} image env: DOCKER_BUILDER: openshell run: | set -euo pipefail mise exec -- tasks/scripts/docker-build-image.sh "${{ inputs.component }}" \ --cache-from "type=gha,scope=${{ inputs.component }}-${{ matrix.arch }}" \ --cache-to "type=gha,mode=max,scope=${{ inputs.component }}-${{ matrix.arch }}" - name: Smoke check ${{ inputs.component }} image run: | set -euo pipefail image="${IMAGE_REGISTRY}/${{ inputs.component }}:${IMAGE_TAG}" case "${{ inputs.component }}" in gateway) output="$(docker run --rm --platform "${{ matrix.platform }}" "$image" --version)" echo "$output" grep -q '^openshell-gateway ' <<<"$output" ;; supervisor) output="$(docker run --rm --platform "${{ matrix.platform }}" --entrypoint /openshell-sandbox "$image" --version)" echo "$output" grep -q '^openshell-sandbox ' <<<"$output" ;; esac merge: name: Merge ${{ inputs.component }} manifest needs: [resolve, build] if: ${{ inputs.push && inputs['publish-manifest'] && needs.resolve.outputs.has_image == 'true' }} runs-on: linux-amd64-cpu8 timeout-minutes: 10 container: image: ghcr.io/nvidia/openshell/ci:latest credentials: username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} volumes: - /var/run/docker.sock:/var/run/docker.sock steps: - name: Log in to GHCR run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u "${{ github.actor }}" --password-stdin - name: Create multi-arch manifest run: | set -euo pipefail image="ghcr.io/nvidia/openshell/${{ inputs.component }}" refs=() for arch in ${{ needs.resolve.outputs.arches }}; do refs+=("${image}:${{ needs.resolve.outputs.image_tag_base }}-${arch}") done docker buildx imagetools create \ --prefer-index=false \ -t "${image}:${{ needs.resolve.outputs.image_tag_base }}" \ "${refs[@]}"