name: Release Linux

on:
  workflow_call:
    inputs:
      target:
        description: Release target label.
        required: true
        type: string
      runner:
        description: Architecture-native GitHub Actions runner label.
        required: true
        type: string
      manylinux_image:
        description: Digest-pinned manylinux 2.28 build image.
        required: true
        type: string
      arch:
        description: Release architecture label.
        required: true
        type: string
      agentd_artifact:
        description: Matching musl agentd artifact.
        required: true
        type: string
      firmware_artifact:
        description: Matching generated kernel.c artifact.
        required: true
        type: string
      libkrunfw_file:
        description: Canonical runtime library filename.
        required: true
        type: string
      libkrunfw_asset:
        description: Public release asset name for libkrunfw.
        required: true
        type: string
      napi_target:
        description: napi-rs target triple.
        required: true
        type: string
      node_file:
        description: Platform Node native binding filename.
        required: true
        type: string
      npm_dir:
        description: Platform npm package directory.
        required: true
        type: string
      go_ffi_file:
        description: Cargo output filename for the Go FFI library.
        required: true
        type: string
      go_bundle_name:
        description: Embedded Go SDK bundle filename.
        required: true
        type: string

permissions:
  contents: read

env:
  CARGO_TERM_COLOR: always
  CARGO_NET_RETRY: "10"
  CARGO_HTTP_TIMEOUT: "120"
  CARGO_HTTP_MULTIPLEXING: "false"
  CARGO_INCREMENTAL: "0"
  LIBKRUNFW_VERSION: "5.6.1"
  LIBKRUNFW_ABI: "5"
  LINUX_GLIBC_BASELINE: "2.28"

# Every shipped Linux ELF is compiled or linked inside the architecture-native
# manylinux container. Building the kernel remains outside this workflow; only
# the generated kernel.c bundle is linked here so libkrunfw shares the baseline.
jobs:
  runtime:
    name: Runtime (${{ inputs.target }})
    runs-on: ${{ inputs.runner }}
    container:
      image: ${{ inputs.manylinux_image }}
    timeout-minutes: 60
    steps:
      - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1

      - uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable

      - uses: Swatinem/rust-cache@f0d9c3887740aee45f6153b24b3a6b815192ec16 # v2
        with:
          cache-bin: false
          cache-targets: true
          shared-key: release-runtime-${{ inputs.target }}

      - name: Install build dependencies
        run: dnf install -y libcap-ng-devel

      - name: Download agentd
        uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
        with:
          name: ${{ inputs.agentd_artifact }}
          path: build/

      - name: Download firmware bundle
        uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
        with:
          name: ${{ inputs.firmware_artifact }}
          path: firmware/

      - name: Link libkrunfw against the baseline
        run: |
          mkdir -p build
          cc -fPIC -DABI_VERSION=${{ env.LIBKRUNFW_ABI }} -shared \
            -Wl,-soname,libkrunfw.so.${{ env.LIBKRUNFW_ABI }} \
            -o build/${{ inputs.libkrunfw_file }} firmware/kernel.c
          strip build/${{ inputs.libkrunfw_file }}
          cd build
          ln -sf ${{ inputs.libkrunfw_file }} libkrunfw.so.${{ env.LIBKRUNFW_ABI }}
          ln -sf libkrunfw.so.${{ env.LIBKRUNFW_ABI }} libkrunfw.so

      - name: Build msb
        run: |
          cargo build --release --no-default-features --features net,ssh -p microsandbox-cli
          cp target/release/msb build/msb

      - name: Verify runtime and glibc baseline
        run: |
          build/msb --version
          python3 scripts/ci/validate_linux_glibc.py \
            --max-version ${{ env.LINUX_GLIBC_BASELINE }} \
            build/agentd build/msb build/${{ inputs.libkrunfw_file }}

      - name: Upload runtime artifacts
        uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
        with:
          name: runtime-${{ inputs.target }}
          # upload-artifact dereferences symlinks. Listing only the canonical
          # library prevents SONAME aliases from becoming duplicate real files.
          path: |
            build/agentd
            build/msb
            build/${{ inputs.libkrunfw_file }}
          compression-level: 0

  metrics:
    name: Metrics (${{ inputs.target }})
    runs-on: ${{ inputs.runner }}
    container:
      image: ${{ inputs.manylinux_image }}
    timeout-minutes: 45
    steps:
      - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1

      - uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable

      - uses: Swatinem/rust-cache@f0d9c3887740aee45f6153b24b3a6b815192ec16 # v2
        with:
          cache-bin: false
          cache-targets: true
          shared-key: release-metrics-${{ inputs.target }}

      - name: Build msb-metrics
        run: cargo build --release -p microsandbox-metrics-collector

      - name: Verify glibc baseline
        run: |
          python3 scripts/ci/validate_linux_glibc.py \
            --max-version ${{ env.LINUX_GLIBC_BASELINE }} \
            target/release/msb-metrics

      - name: Upload metrics artifact
        uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
        with:
          name: metrics-${{ inputs.target }}
          path: target/release/msb-metrics
          compression-level: 0

  go-ffi:
    name: Go FFI (${{ inputs.target }})
    needs: runtime
    runs-on: ${{ inputs.runner }}
    container:
      image: ${{ inputs.manylinux_image }}
    timeout-minutes: 60
    steps:
      - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1

      - uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable

      - uses: Swatinem/rust-cache@f0d9c3887740aee45f6153b24b3a6b815192ec16 # v2
        with:
          cache-bin: false
          cache-targets: true
          shared-key: release-go-${{ inputs.target }}

      - name: Install build dependencies
        run: dnf install -y libcap-ng-devel

      - name: Download runtime artifacts
        uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
        with:
          name: runtime-${{ inputs.target }}
          path: build/

      - name: Build and validate Go FFI
        run: |
          cargo build --release -p microsandbox-go
          python3 scripts/ci/validate_linux_glibc.py \
            --max-version ${{ env.LINUX_GLIBC_BASELINE }} \
            target/release/${{ inputs.go_ffi_file }}

      - name: Upload Go FFI artifact
        uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
        with:
          name: go-ffi-${{ inputs.target }}
          path: target/release/${{ inputs.go_ffi_file }}
          compression-level: 0

  node:
    name: Node SDK (${{ inputs.target }})
    needs: runtime
    runs-on: ${{ inputs.runner }}
    container:
      image: ${{ inputs.manylinux_image }}
    timeout-minutes: 60
    steps:
      - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1

      - uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable

      - uses: Swatinem/rust-cache@f0d9c3887740aee45f6153b24b3a6b815192ec16 # v2
        with:
          cache-bin: false
          cache-targets: true
          shared-key: release-node-${{ inputs.target }}

      - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
        with:
          node-version: 22
          cache: npm
          cache-dependency-path: sdk/node-ts/package-lock.json

      - name: Install build dependencies
        run: dnf install -y libcap-ng-devel

      - name: Download runtime artifacts
        uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
        with:
          name: runtime-${{ inputs.target }}
          path: build/

      - name: Build Node native binding
        working-directory: sdk/node-ts
        run: |
          node scripts/prune-platform-optional-deps.mjs
          npm install --package-lock=false
          npm run build:native -- --target ${{ inputs.napi_target }}
          node -e 'require("./native/index.cjs")'

      - name: Prepare and validate Node packages
        working-directory: sdk/node-ts
        run: |
          npm run build:ts
          node scripts/prepare-platform-package.mjs ${{ inputs.npm_dir }}
          python3 ../../scripts/ci/validate_linux_glibc.py \
            --max-version ${{ env.LINUX_GLIBC_BASELINE }} \
            native/${{ inputs.node_file }} npm/${{ inputs.npm_dir }}

          # Install exactly the tarballs that will be published, without relying
          # on a source-tree override or an already-published optional package.
          pack_dir="${RUNNER_TEMP}/microsandbox-packs"
          consumer_dir="${RUNNER_TEMP}/microsandbox-consumer"
          mkdir -p "$pack_dir" "$consumer_dir"
          main_tarball=$(npm pack --silent --pack-destination "$pack_dir")
          platform_tarball=$(
            cd npm/${{ inputs.npm_dir }}
            npm pack --silent --pack-destination "$pack_dir"
          )
          cd "$consumer_dir"
          npm init --yes >/dev/null
          npm install --ignore-scripts --omit=optional \
            "$pack_dir/$main_tarball" "$pack_dir/$platform_tarball"
          node --input-type=module -e 'await import("microsandbox")'

      - name: Upload Node SDK artifacts
        uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
        with:
          name: node-sdk-${{ inputs.npm_dir }}
          path: |
            sdk/node-ts/native/${{ inputs.node_file }}
            sdk/node-ts/native/index.cjs
            sdk/node-ts/native/index.d.ts
            sdk/node-ts/npm/${{ inputs.npm_dir }}/${{ inputs.node_file }}
            sdk/node-ts/npm/${{ inputs.npm_dir }}/bin/msb
            sdk/node-ts/npm/${{ inputs.npm_dir }}/lib/*
          compression-level: 0

  python:
    name: Python SDK (${{ inputs.target }})
    needs: runtime
    runs-on: ${{ inputs.runner }}
    timeout-minutes: 60
    steps:
      - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1

      - uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable

      - uses: Swatinem/rust-cache@f0d9c3887740aee45f6153b24b3a6b815192ec16 # v2
        with:
          cache-bin: false
          cache-targets: true
          shared-key: release-python-${{ inputs.target }}

      - name: Install build dependencies
        run: sudo apt-get update && sudo apt-get install -y libcap-ng-dev

      - name: Download runtime artifacts
        uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
        with:
          name: runtime-${{ inputs.target }}
          path: build/

      - name: Stage runtime bundle
        run: |
          mkdir -p sdk/python/microsandbox/_bundled/bin sdk/python/microsandbox/_bundled/lib
          # GitHub artifact archives normalize every file to 0644. Restore the
          # runtime mode explicitly before maturin records it in the wheel.
          install -m 755 build/msb sdk/python/microsandbox/_bundled/bin/msb
          install -m 644 build/${{ inputs.libkrunfw_file }} sdk/python/microsandbox/_bundled/lib/

      - name: Build Python wheel
        uses: PyO3/maturin-action@e83996d129638aa358a18fbd1dfb82f0b0fb5d3b # v1
        with:
          working-directory: sdk/python
          command: build
          args: --release --out dist
          manylinux: 2_28
          before-script-linux: dnf install -y libcap-ng-devel

      - name: Verify wheel glibc baseline
        run: |
          python3 scripts/ci/validate_linux_glibc.py \
            --max-version ${{ env.LINUX_GLIBC_BASELINE }} \
            sdk/python/dist

      - name: Upload Python SDK wheel
        uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
        with:
          name: python-sdk-${{ inputs.target }}
          path: sdk/python/dist/*.whl
          compression-level: 0

  assemble-platform:
    name: Assemble (${{ inputs.target }})
    needs: [runtime, metrics, go-ffi]
    runs-on: ubuntu-latest
    steps:
      - name: Download runtime artifacts
        uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
        with:
          name: runtime-${{ inputs.target }}
          path: bundle/runtime/

      - name: Download metrics artifact
        uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
        with:
          name: metrics-${{ inputs.target }}
          path: bundle/metrics/

      - name: Download Go FFI artifact
        uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
        with:
          name: go-ffi-${{ inputs.target }}
          path: bundle/go/

      - name: Stage platform artifacts
        run: |
          set -euo pipefail
          mkdir -p artifacts
          # Preserve an executable msb entry in the release tarball after the
          # runtime artifact round trip normalized it to 0644.
          chmod 755 bundle/runtime/msb
          cp bundle/runtime/msb artifacts/msb-${{ inputs.target }}
          cp bundle/metrics/msb-metrics artifacts/msb-metrics-${{ inputs.target }}
          cp bundle/runtime/${{ inputs.libkrunfw_file }} artifacts/${{ inputs.libkrunfw_asset }}
          cp bundle/runtime/agentd artifacts/agentd-${{ inputs.arch }}
          tar -czf artifacts/microsandbox-${{ inputs.target }}.tar.gz \
            -C bundle/runtime msb ${{ inputs.libkrunfw_file }}
          cp bundle/go/${{ inputs.go_ffi_file }} artifacts/${{ inputs.go_bundle_name }}

      - name: Upload platform artifacts
        uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
        with:
          name: release-${{ inputs.target }}
          path: artifacts/
          compression-level: 0
