soft3/.github/workflows/candidate.yml

name: Shared soft3 candidate

on:
  workflow_call:
    inputs:
      component:
        required: true
        type: string
      manager_ref:
        required: true
        type: string
      candidate:
        default: ''
        type: string
      cut:
        default: false
        type: boolean

permissions:
  contents: read

jobs:
  snapshot:
    runs-on: ubuntu-24.04
    outputs:
      candidate: ${{ steps.capture.outputs.candidate }}
      matrix: ${{ steps.capture.outputs.matrix }}
    defaults:
      run:
        shell: nu {0}
    steps:
      - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
        with:
          repository: cyberia-to/soft3
          ref: ${{ inputs.manager_ref }}
          persist-credentials: false
      - uses: hustcer/setup-nu@9215c80adb545a85c419d5085b76eb6d082f49e7
        with:
          version: '0.112.2'
      - name: Capture default branches and immutable inputs
        id: capture
        env:
          GH_TOKEN: ${{ github.token }}
          COMPONENT: ${{ inputs.component }}
          CANDIDATE: ${{ inputs.candidate }}
        run: |
          let name = if ($env.CANDIDATE | is-empty) { $"candidate-(date now | format date '%Y%m%d').1" } else { $env.CANDIDATE }
          python3 release/train.py snapshot --manager . --component $env.COMPONENT --candidate $name --output snapshot
          if $env.LAST_EXIT_CODE != 0 { error make {msg: "source snapshot failed"} }
          let rows = [
            {runner: ubuntu-24.04, target: stack}
            {runner: ubuntu-24.04, target: x86_64-unknown-linux-gnu}
            {runner: ubuntu-24.04-arm, target: aarch64-unknown-linux-gnu}
            {runner: macos-14, target: aarch64-apple-darwin}
            {runner: macos-15-intel, target: x86_64-apple-darwin}
          ]
          let rows = if $env.COMPONENT == cyb { $rows | append {runner: ubuntu-24.04, target: aarch64-linux-android} } else { $rows }
          $"candidate=($name)\n" | save --append $env.GITHUB_OUTPUT
          $"matrix=({include: $rows} | to json --raw)\n" | save --append $env.GITHUB_OUTPUT
      - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a
        with:
          name: source-snapshot
          path: snapshot/sources.json
          if-no-files-found: error

  qualify:
    needs: snapshot
    strategy:
      fail-fast: false
      matrix: ${{ fromJSON(needs.snapshot.outputs.matrix) }}
    runs-on: ${{ matrix.runner }}
    timeout-minutes: 90
    defaults:
      run:
        shell: nu {0}
    steps:
      - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
        with:
          repository: cyberia-to/soft3
          ref: ${{ inputs.manager_ref }}
          persist-credentials: false
      - uses: hustcer/setup-nu@9215c80adb545a85c419d5085b76eb6d082f49e7
        with:
          version: '0.112.2'
      - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c
        with:
          name: source-snapshot
          path: snapshot
      - name: Install Rust
        run: |
          rustup toolchain install 1.95.0 --profile minimal --component rustfmt
          if $env.LAST_EXIT_CODE != 0 { error make {msg: "Rust installation failed"} }
          rustup default 1.95.0
      - name: Desktop Linux libraries
        if: inputs.component == 'cyb' && startsWith(matrix.runner, 'ubuntu') && matrix.target != 'stack'
        run: |
          sudo apt-get update
          if $env.LAST_EXIT_CODE != 0 { error make {msg: "apt update failed"} }
          sudo apt-get install -y libasound2-dev libudev-dev libx11-dev libxrandr-dev libxi-dev libgl1-mesa-dev libwayland-dev libxkbcommon-dev libgtk-3-dev libayatana-appindicator3-dev mesa-vulkan-drivers xvfb
      - name: Android toolchain
        if: matrix.target == 'aarch64-linux-android'
        run: |
          rustup target add aarch64-linux-android
          if $env.LAST_EXIT_CODE != 0 { error make {msg: "Android Rust target unavailable"} }
          sdkmanager 'platforms;android-35' 'build-tools;35.0.0' 'ndk;27.2.12479018'
      - name: Qualify stack and product
        env:
          TARGET: ${{ matrix.target }}
          RUSTUP_TOOLCHAIN: '1.95.0'
          CARGO_BUILD_JOBS: '2'
          PYTHONUNBUFFERED: '1'
        run: |
          let args = if $env.TARGET == stack { [--stack] } else { [] }
          python3 release/train.py build --snapshot snapshot/sources.json --checkout ($env.RUNNER_TEMP | path join sources) --output ($env.RUNNER_TEMP | path join receipt) --target $env.TARGET ...$args
          if $env.LAST_EXIT_CODE != 0 { error make {msg: "qualification engine failed; receipts retained"} }
      - name: Retain every result
        if: always()
        uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a
        with:
          name: candidate-${{ matrix.target }}
          path: |
            ${{ runner.temp }}/*.tar.gz
            ${{ runner.temp }}/receipt/
          if-no-files-found: warn
      - name: Report the verdict
        run: |
          let receipt = (open ($env.RUNNER_TEMP | path join receipt release-validation.json))
          if $receipt.result != green { error make {msg: "RED candidate gate; see retained release-validation.json"} }

  collect:
    if: always() && needs.snapshot.result == 'success'
    needs: [snapshot, qualify]
    runs-on: ubuntu-24.04
    permissions:
      contents: write
    defaults:
      run:
        shell: nu {0}
    steps:
      - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
        with:
          repository: cyberia-to/soft3
          ref: ${{ inputs.manager_ref }}
          persist-credentials: false
      - uses: hustcer/setup-nu@9215c80adb545a85c419d5085b76eb6d082f49e7
        with:
          version: '0.112.2'
      - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c
        with:
          name: source-snapshot
          path: snapshot
      - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c
        continue-on-error: true
        with:
          pattern: candidate-*
          path: artifacts
      - name: Assemble green and red evidence
        run: python3 release/train.py collect --snapshot snapshot/sources.json --artifacts artifacts --output assembled
      - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a
        with:
          name: assembled-candidate
          path: assembled/
          if-no-files-found: error
          retention-days: 90
      - name: Create draft candidate
        if: inputs.cut && github.ref == format('refs/heads/{0}', github.event.repository.default_branch)
        env:
          GH_TOKEN: ${{ github.token }}
        run: python3 release/train.py draft --output assembled

Graph