Skip to content

Qualification

Qualification #140

Workflow file for this run

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