Qualification #140
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Qualification | |
| on: | |
| schedule: | |
| - cron: "0 3 * * 0" | |
| workflow_dispatch: | |
| inputs: | |
| mode: | |
| description: "Assurance is routine coverage; release adds release-grade evidence." | |
| required: true | |
| type: choice | |
| options: | |
| - assurance | |
| - release | |
| default: assurance | |
| concurrency: | |
| group: ${{ github.workflow }}-${{ github.ref }}-${{ github.event_name == 'workflow_dispatch' && inputs.mode == 'release' && 'release' || 'assurance' }} | |
| cancel-in-progress: true | |
| env: | |
| RUST_BACKTRACE: 1 | |
| CARGO_TERM_COLOR: always | |
| RSCRYPTO_TEST_MODE: weekly | |
| CARGO_INCREMENTAL: 0 | |
| permissions: | |
| contents: read | |
| jobs: | |
| mode: | |
| name: Resolve Qualification Mode | |
| runs-on: ubuntu-latest | |
| outputs: | |
| mode: ${{ steps.mode.outputs.mode }} | |
| steps: | |
| - name: Resolve mode | |
| id: mode | |
| shell: bash | |
| env: | |
| EVENT_NAME: ${{ github.event_name }} | |
| REQUESTED_MODE: ${{ inputs.mode }} | |
| run: | | |
| set -euo pipefail | |
| case "$EVENT_NAME" in | |
| schedule) | |
| mode=assurance | |
| ;; | |
| workflow_dispatch) | |
| case "$REQUESTED_MODE" in | |
| assurance | release) mode="$REQUESTED_MODE" ;; | |
| *) echo "Unknown Qualification mode: $REQUESTED_MODE" >&2; exit 2 ;; | |
| esac | |
| ;; | |
| *) | |
| echo "Unsupported Qualification event: $EVENT_NAME" >&2 | |
| exit 2 | |
| ;; | |
| esac | |
| echo "mode=$mode" >> "$GITHUB_OUTPUT" | |
| echo "Qualification mode: $mode" | |
| rail-plan: | |
| name: Cargo Rail Full Plan | |
| needs: mode | |
| runs-on: ubuntu-latest | |
| outputs: | |
| matrix: ${{ steps.matrix.outputs.matrix }} | |
| identity: ${{ steps.rail.outputs.plan-identity }} | |
| head-commit: ${{ steps.rail.outputs.head-commit }} | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| fetch-depth: 0 | |
| persist-credentials: false | |
| - name: Build full exact work plan | |
| id: rail | |
| uses: loadingalias/cargo-rail-action@78ad385a85627484a5b634cf1bc3caa1de872c6a # v8.2.0 | |
| with: | |
| version: "0.25.0" | |
| components: surface | |
| all: true | |
| - name: Materialize full CI matrix | |
| id: matrix | |
| shell: bash | |
| env: | |
| PLAN_FILE: ${{ steps.rail.outputs.plan-file }} | |
| PLAN_READER: ${{ steps.rail.outputs.plan-reader }} | |
| run: scripts/ci/materialize-rail-plan.sh "$PLAN_FILE" "$PLAN_READER" "$GITHUB_OUTPUT" | |
| - name: Check public source surface | |
| if: contains(fromJSON(steps.rail.outputs.required-work), 'surface') | |
| shell: bash | |
| run: | | |
| mkdir -p target/cargo-rail | |
| cargo rail surface --check --json > target/cargo-rail/surface-result.json | |
| - name: Upload exact work plan | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| with: | |
| name: cargo-rail-plan-${{ github.run_id }} | |
| path: | | |
| ${{ steps.rail.outputs.plan-file }} | |
| ${{ steps.rail.outputs.plan-reader }} | |
| retention-days: 1 | |
| if-no-files-found: error | |
| - name: Upload Surface evidence | |
| if: ${{ !cancelled() && contains(fromJSON(steps.rail.outputs.required-work), 'surface') }} | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| with: | |
| name: cargo-rail-surface-${{ github.run_id }} | |
| path: target/cargo-rail/surface-result.json | |
| retention-days: ${{ needs.mode.outputs.mode == 'release' && 90 || 14 }} | |
| if-no-files-found: error | |
| # Both modes run the full planned platform, graph, and feature suite. | |
| # Release mode adds full supply-chain policy and longer evidence retention. | |
| suite: | |
| name: CI Suite (${{ needs.mode.outputs.mode }}) | |
| needs: [mode, rail-plan] | |
| permissions: | |
| contents: read | |
| uses: ./.github/workflows/_ci-suite.yaml | |
| with: | |
| matrix: ${{ needs.rail-plan.outputs.matrix }} | |
| plan_artifact: cargo-rail-plan-${{ github.run_id }} | |
| plan_identity: ${{ needs.rail-plan.outputs.identity }} | |
| plan_head_commit: ${{ needs.rail-plan.outputs.head-commit }} | |
| supply_chain_mode: ${{ needs.mode.outputs.mode == 'release' && 'full' || 'light' }} | |
| test_mode: weekly | |
| artifact_retention_days: ${{ needs.mode.outputs.mode == 'release' && 90 || 14 }} | |
| secrets: | |
| cache_access_key_id: ${{ secrets.CARGO_RAIL_R2_READ_ACCESS_KEY_ID }} | |
| cache_secret_access_key: ${{ secrets.CARGO_RAIL_R2_READ_SECRET_ACCESS_KEY }} | |
| # ─── Miri memory-safety (stacked borrows). ── | |
| miri: | |
| name: Miri (${{ matrix.target.name }}) | |
| permissions: | |
| contents: read | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| target: | |
| - name: x86_64 | |
| runner: runs-on=${{ github.run_id }}/runner=linux-x64-ci | |
| - name: aarch64 | |
| runner: runs-on=${{ github.run_id }}/runner=linux-arm64-ci | |
| uses: ./.github/workflows/_rust-job.yaml | |
| with: | |
| runner: ${{ matrix.target.runner }} | |
| timeout_minutes: 60 | |
| tools_mode: minimal | |
| toolchain_contract: nightly | |
| toolchain_components: "miri, rust-src" | |
| operation: miri | |
| secrets: | |
| cache_access_key_id: ${{ secrets.CARGO_RAIL_R2_READ_ACCESS_KEY_ID }} | |
| cache_secret_access_key: ${{ secrets.CARGO_RAIL_R2_READ_SECRET_ACCESS_KEY }} | |
| # ─── Miri memory-safety (tree borrows). ── | |
| miri-tree-borrows: | |
| name: Miri (Tree Borrows) | |
| permissions: | |
| contents: read | |
| uses: ./.github/workflows/_rust-job.yaml | |
| with: | |
| runner: runs-on=${{ github.run_id }}/runner=linux-x64-ci | |
| timeout_minutes: 60 | |
| tools_mode: minimal | |
| toolchain_contract: nightly | |
| toolchain_components: "miri, rust-src" | |
| operation: miri | |
| miri_tree_borrows: true | |
| secrets: | |
| cache_access_key_id: ${{ secrets.CARGO_RAIL_R2_READ_ACCESS_KEY_ID }} | |
| cache_secret_access_key: ${{ secrets.CARGO_RAIL_R2_READ_SECRET_ACCESS_KEY }} | |
| # ─── Fuzzing (libFuzzer full + scoped packages). ── | |
| fuzzing: | |
| name: Fuzzing (${{ matrix.target.name }}) | |
| needs: mode | |
| permissions: | |
| contents: read | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| target: | |
| - name: x86_64 | |
| runner: runs-on=${{ github.run_id }}/runner=linux-x64-ci | |
| - name: aarch64 | |
| runner: runs-on=${{ github.run_id }}/runner=linux-arm64-ci | |
| uses: ./.github/workflows/_rust-job.yaml | |
| with: | |
| runner: ${{ matrix.target.runner }} | |
| timeout_minutes: 120 | |
| tools_mode: fuzz | |
| toolchain_contract: nightly | |
| operation: fuzz | |
| artifact_name: fuzz-output-weekly-${{ matrix.target.name }} | |
| artifact_path: | | |
| fuzz-output/corpus.tar.gz | |
| fuzz/artifacts/ | |
| fuzz-packages/*/artifacts/ | |
| artifact_always: true | |
| artifact_retention_days: ${{ needs.mode.outputs.mode == 'release' && 90 || 14 }} | |
| secrets: | |
| cache_access_key_id: ${{ secrets.CARGO_RAIL_R2_READ_ACCESS_KEY_ID }} | |
| cache_secret_access_key: ${{ secrets.CARGO_RAIL_R2_READ_SECRET_ACCESS_KEY }} | |
| asan-fuzzing: | |
| name: ASan Fuzz Corpus (${{ matrix.target.name }}) | |
| permissions: | |
| contents: read | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| target: | |
| - name: x86_64 | |
| runner: runs-on=${{ github.run_id }}/runner=linux-x64-ci | |
| - name: aarch64 | |
| runner: runs-on=${{ github.run_id }}/runner=linux-arm64-ci | |
| uses: ./.github/workflows/_rust-job.yaml | |
| with: | |
| runner: ${{ matrix.target.runner }} | |
| timeout_minutes: 120 | |
| tools_mode: fuzz | |
| toolchain_contract: nightly | |
| toolchain_components: "rust-src" | |
| operation: fuzz-asan | |
| secrets: | |
| cache_access_key_id: ${{ secrets.CARGO_RAIL_R2_READ_ACCESS_KEY_ID }} | |
| cache_secret_access_key: ${{ secrets.CARGO_RAIL_R2_READ_SECRET_ACCESS_KEY }} | |
| # --- Native ML-KEM promotion gate on the physical AWS targets. --- | |
| mlkem-graviton: | |
| name: ML-KEM Graviton Gate (${{ matrix.display_name }}) | |
| needs: mode | |
| permissions: | |
| contents: read | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - platform: graviton3 | |
| display_name: AWS Graviton3 | |
| - platform: graviton4 | |
| display_name: AWS Graviton4 | |
| uses: ./.github/workflows/_rust-job.yaml | |
| with: | |
| runner: runs-on=${{ github.run_id }}/runner=${{ matrix.platform }} | |
| timeout_minutes: 180 | |
| tools_mode: minimal | |
| toolchain_components: "clippy, rustfmt, rust-src" | |
| operation: mlkem-aarch64 | |
| platform: ${{ matrix.platform }} | |
| artifact_name: mlkem-aarch64-gate-${{ matrix.platform }} | |
| artifact_path: mlkem-aarch64-gate/ | |
| artifact_always: true | |
| artifact_retention_days: ${{ needs.mode.outputs.mode == 'release' && 90 || 14 }} | |
| secrets: | |
| cache_access_key_id: ${{ secrets.CARGO_RAIL_R2_READ_ACCESS_KEY_ID }} | |
| cache_secret_access_key: ${{ secrets.CARGO_RAIL_R2_READ_SECRET_ACCESS_KEY }} | |
| # ─── Constant-time evidence on benchmark-published physical lanes. ── | |
| ct: | |
| name: Constant-Time Evidence (${{ needs.mode.outputs.mode }}) | |
| needs: mode | |
| permissions: | |
| contents: read | |
| uses: ./.github/workflows/ct.yaml | |
| with: | |
| platforms: all | |
| dudect_timeout: 1800 | |
| binsec_timeout: 900 | |
| upload_raw_artifacts: ${{ needs.mode.outputs.mode == 'release' }} | |
| artifact_retention_days: ${{ needs.mode.outputs.mode == 'release' && 90 || 14 }} | |
| secrets: | |
| CARGO_RAIL_R2_READ_ACCESS_KEY_ID: ${{ secrets.CARGO_RAIL_R2_READ_ACCESS_KEY_ID }} | |
| CARGO_RAIL_R2_READ_SECRET_ACCESS_KEY: ${{ secrets.CARGO_RAIL_R2_READ_SECRET_ACCESS_KEY }} | |
| rsa: | |
| name: RSA Evidence (${{ needs.mode.outputs.mode }}) | |
| needs: mode | |
| permissions: | |
| contents: read | |
| uses: ./.github/workflows/rsa.yaml | |
| with: | |
| artifact_retention_days: ${{ needs.mode.outputs.mode == 'release' && 90 || 14 }} | |
| secrets: | |
| CARGO_RAIL_R2_READ_ACCESS_KEY_ID: ${{ secrets.CARGO_RAIL_R2_READ_ACCESS_KEY_ID }} | |
| CARGO_RAIL_R2_READ_SECRET_ACCESS_KEY: ${{ secrets.CARGO_RAIL_R2_READ_SECRET_ACCESS_KEY }} | |
| # RISC-V is a target/evidence variant in the same qualification graph, not | |
| # a second release ritual with an independently selected source tree. | |
| riscv-native: | |
| name: RISC-V Native Evidence | |
| permissions: | |
| contents: read | |
| uses: ./.github/workflows/_rust-job.yaml | |
| with: | |
| runner: ubuntu-24.04-riscv | |
| timeout_minutes: 240 | |
| tools_mode: none | |
| toolchain_contract: nightly | |
| toolchain_components: "clippy, rustfmt" | |
| operation: native-riscv | |
| secrets: | |
| cache_access_key_id: ${{ secrets.CARGO_RAIL_R2_READ_ACCESS_KEY_ID }} | |
| cache_secret_access_key: ${{ secrets.CARGO_RAIL_R2_READ_SECRET_ACCESS_KEY }} | |
| riscv-ct: | |
| name: RISC-V CT Evidence (${{ needs.mode.outputs.mode }}) | |
| needs: mode | |
| permissions: | |
| contents: read | |
| uses: ./.github/workflows/ct.yaml | |
| with: | |
| platforms: rise-riscv | |
| dudect_timeout: 1800 | |
| binsec_timeout: 900 | |
| upload_raw_artifacts: ${{ needs.mode.outputs.mode == 'release' }} | |
| artifact_retention_days: ${{ needs.mode.outputs.mode == 'release' && 90 || 14 }} | |
| secrets: | |
| CARGO_RAIL_R2_READ_ACCESS_KEY_ID: ${{ secrets.CARGO_RAIL_R2_READ_ACCESS_KEY_ID }} | |
| CARGO_RAIL_R2_READ_SECRET_ACCESS_KEY: ${{ secrets.CARGO_RAIL_R2_READ_SECRET_ACCESS_KEY }} | |
| # ─── Coverage (nextest + fuzz corpus → codecov). ── | |
| coverage: | |
| name: Coverage | |
| needs: [mode, suite, fuzzing] | |
| permissions: | |
| contents: read | |
| runs-on: runs-on=${{ github.run_id }}/runner=linux-x64-ci | |
| timeout-minutes: 120 | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| fetch-depth: 0 | |
| persist-credentials: false | |
| - name: Setup | |
| id: setup | |
| uses: ./.github/actions/setup | |
| with: | |
| tools-mode: coverage | |
| toolchain-components: "rust-src" | |
| cache-url: ${{ secrets.CARGO_RAIL_R2_READ_ACCESS_KEY_ID != '' && secrets.CARGO_RAIL_R2_READ_SECRET_ACCESS_KEY != '' && vars.CARGO_RAIL_CACHE_URL || '' }} | |
| cache-mode: read | |
| cache-access-key-id: ${{ secrets.CARGO_RAIL_R2_READ_ACCESS_KEY_ID }} | |
| cache-secret-access-key: ${{ secrets.CARGO_RAIL_R2_READ_SECRET_ACCESS_KEY }} | |
| - name: Download fuzz corpus artifacts | |
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 | |
| with: | |
| pattern: fuzz-output-weekly-* | |
| path: fuzz-output-artifacts | |
| - name: Restore fuzz corpora | |
| shell: bash | |
| run: | | |
| shopt -s nullglob | |
| archives=(fuzz-output-artifacts/*/fuzz-output/corpus.tar.gz) | |
| if [ "${#archives[@]}" -eq 0 ]; then | |
| echo "No fuzz corpus archives found" | |
| exit 0 | |
| fi | |
| for archive in "${archives[@]}"; do | |
| echo "Extracting $archive" | |
| tar -xzf "$archive" | |
| done | |
| - name: Total Coverage (nextest + fuzz corpus replay) | |
| run: just test-coverage | |
| - name: Capture Cargo Rail Cache Status | |
| id: cache-status | |
| if: ${{ !cancelled() && steps.setup.outputs.cache-enabled == 'true' }} | |
| shell: bash | |
| env: | |
| RSCRYPTO_CI_OPERATION: coverage | |
| RSCRYPTO_CI_RUNNER: linux-x64-ci | |
| run: scripts/ci/capture-cache-status.sh | |
| - name: Upload Cargo Rail Cache Status | |
| if: ${{ !cancelled() && steps.setup.outputs.cache-enabled == 'true' }} | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| with: | |
| name: ${{ steps.cache-status.outputs.artifact_name }} | |
| path: target/cargo-rail/cache-status.json | |
| retention-days: 14 | |
| if-no-files-found: error | |
| - name: Install authenticated Codecov CLI | |
| id: codecov | |
| run: scripts/ci/install-codecov.sh | |
| - name: Upload to Codecov | |
| uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0 | |
| with: | |
| binary: ${{ steps.codecov.outputs.binary }} | |
| token: ${{ secrets.CODECOV_TOKEN }} | |
| files: coverage/total.lcov | |
| flags: total | |
| name: total | |
| fail_ci_if_error: true | |
| - name: Upload Coverage Artifacts | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| with: | |
| name: coverage-reports | |
| path: | | |
| coverage/total.lcov | |
| coverage/SUMMARY.md | |
| retention-days: ${{ needs.mode.outputs.mode == 'release' && 90 || 14 }} | |
| if-no-files-found: warn | |
| # The mode-specific terminal name is part of the release qualification contract. | |
| complete: | |
| name: Complete (${{ needs.mode.outputs.mode }}) | |
| needs: | |
| [ | |
| mode, | |
| suite, | |
| miri, | |
| miri-tree-borrows, | |
| fuzzing, | |
| asan-fuzzing, | |
| mlkem-graviton, | |
| ct, | |
| rsa, | |
| riscv-native, | |
| riscv-ct, | |
| coverage, | |
| ] | |
| if: ${{ !cancelled() }} | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Assert all lanes passed | |
| run: | | |
| failed=0 | |
| for lane in \ | |
| "mode=${{ needs.mode.result }}" \ | |
| "suite=${{ needs.suite.result }}" \ | |
| "miri=${{ needs.miri.result }}" \ | |
| "miri-tree-borrows=${{ needs.miri-tree-borrows.result }}" \ | |
| "fuzzing=${{ needs.fuzzing.result }}" \ | |
| "asan-fuzzing=${{ needs.asan-fuzzing.result }}" \ | |
| "mlkem-graviton=${{ needs.mlkem-graviton.result }}" \ | |
| "ct=${{ needs.ct.result }}" \ | |
| "rsa=${{ needs.rsa.result }}" \ | |
| "riscv-native=${{ needs.riscv-native.result }}" \ | |
| "riscv-ct=${{ needs.riscv-ct.result }}" \ | |
| "coverage=${{ needs.coverage.result }}" | |
| do | |
| name="${lane%%=*}" | |
| result="${lane#*=}" | |
| if [ "$result" != "success" ]; then | |
| echo "FAIL: $name ($result)" | |
| failed=1 | |
| else | |
| echo " OK: $name" | |
| fi | |
| done | |
| if [ "$failed" -eq 1 ]; then | |
| echo "" | |
| echo "One or more required lanes failed." | |
| exit 1 | |
| fi |