NIST P-256/384/521, ML-KEM/ML-DSA parameter sets, and key interoperability formats #2066
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
| # Copyright (C) 2025-2026 Steel Security Advisors LLC | |
| # SPDX-License-Identifier: Apache-2.0 | |
| name: CI - Build and Test | |
| on: | |
| push: | |
| branches: [ main, develop, 'feature/**', 'fix/**' ] | |
| pull_request: | |
| branches: [ main, develop ] | |
| workflow_dispatch: | |
| # Collapse overlapping runs on the same ref; feature branches and PR | |
| # heads cancel in-flight runs, main is preserved. | |
| concurrency: | |
| group: ci-build-test-${{ github.workflow }}-${{ github.ref }} | |
| cancel-in-progress: ${{ github.ref != 'refs/heads/main' && github.event_name != 'schedule' }} | |
| jobs: | |
| # C Library Build and Test | |
| c-library: | |
| name: C Library (${{ matrix.os }}, ${{ matrix.compiler }}) | |
| runs-on: ${{ matrix.os }} | |
| timeout-minutes: 15 | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| os: [ubuntu-latest, macos-latest] | |
| compiler: [gcc, clang] | |
| include: | |
| - os: ubuntu-latest | |
| compiler: gcc | |
| cc: gcc-13 | |
| cxx: g++-13 | |
| - os: ubuntu-latest | |
| compiler: clang | |
| cc: clang-18 | |
| cxx: clang++-18 | |
| - os: macos-latest | |
| compiler: gcc | |
| cc: gcc-12 | |
| cxx: g++-12 | |
| - os: macos-latest | |
| compiler: clang | |
| cc: clang | |
| cxx: clang++ | |
| steps: | |
| - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 | |
| - name: Install dependencies (Ubuntu) | |
| if: matrix.os == 'ubuntu-latest' | |
| run: | | |
| sudo rm -f /etc/apt/sources.list.d/microsoft-prod.list /etc/apt/sources.list.d/azure-cli.list || true | |
| sudo apt-get update | |
| sudo apt-get install -y cmake libssl-dev ${{ matrix.cc }} ${{ matrix.cxx }} | |
| - name: Install dependencies (macOS) | |
| if: matrix.os == 'macos-latest' | |
| run: | | |
| brew install cmake gcc@12 | |
| - name: Configure CMake | |
| run: | | |
| mkdir -p build | |
| cd build | |
| # Set compiler path | |
| if [ "$RUNNER_OS" = "macOS" ]; then | |
| # For GCC on macOS, use full path | |
| if [ "${{ matrix.compiler }}" = "gcc" ]; then | |
| CC_PATH=$(brew --prefix gcc@12)/bin/gcc-12 | |
| CXX_PATH=$(brew --prefix gcc@12)/bin/g++-12 | |
| else | |
| CC_PATH=${{ matrix.cc }} | |
| CXX_PATH=${{ matrix.cxx }} | |
| fi | |
| else | |
| CC_PATH=${{ matrix.cc }} | |
| CXX_PATH=${{ matrix.cxx }} | |
| fi | |
| cmake .. \ | |
| -DCMAKE_C_COMPILER=${CC_PATH} \ | |
| -DCMAKE_CXX_COMPILER=${CXX_PATH} \ | |
| -DCMAKE_BUILD_TYPE=Release \ | |
| -DAMA_BUILD_SHARED=ON \ | |
| -DAMA_BUILD_STATIC=ON \ | |
| -DAMA_BUILD_TESTS=ON \ | |
| -DAMA_ENABLE_AVX2=ON | |
| - name: Build | |
| # PR #326 follow-up: re-emit per-command verbose output on | |
| # failure so the actual compiler diagnostic surfaces in the | |
| # job log without needing local repro. The parallel-j4 form | |
| # is the fast happy path; the serial-verbose form runs only | |
| # if the fast path returned non-zero AND propagates that | |
| # non-zero exit so the step still fails. Diagnoses the | |
| # otherwise opaque "Process completed with exit code 2" | |
| # surfaced on the macos-latest-clang lane that's been red | |
| # since `58e7a2d` introduced the dispatch cache code. | |
| shell: bash | |
| run: | | |
| set -o pipefail | |
| if cmake --build build --config Release -j4; then | |
| exit 0 | |
| fi | |
| rc=$? | |
| echo "::group::Re-running build with --verbose to surface the failing command" | |
| # `--clean-first` discards the partial build state from the | |
| # failed parallel pass so the verbose pass actually re-runs | |
| # the failing compile (otherwise cmake/ninja/make caches the | |
| # failure and just exits non-zero without re-emitting it). | |
| cmake --build build --config Release --verbose --clean-first -j1 \ | |
| 2>&1 | tee build-verbose.log || true | |
| echo "::endgroup::" | |
| # First few lines of the verbose log are the most useful — | |
| # they show the failing TU and the diagnostic text immediately, | |
| # without the reviewer having to scroll through every | |
| # successful compile that ran before it. | |
| if [ -s build-verbose.log ]; then | |
| echo "::group::First failure block from verbose log" | |
| grep -m1 -B0 -A40 'error:' build-verbose.log || tail -80 build-verbose.log | |
| echo "::endgroup::" | |
| fi | |
| exit "$rc" | |
| - name: Test | |
| # `--output-on-failure` + `--verbose` together print the full | |
| # stdout/stderr of every failing test directly into the CI log, | |
| # plus a one-line per-passing-test trace. Without `--verbose` | |
| # ctest swallows the diagnostic output of an exit-code-only | |
| # failure, which is how the `C Library (*, clang)` lanes | |
| # previously reported only "Process completed with exit code 8" | |
| # without ever revealing which 8 tests failed. PR #326 | |
| # follow-up: every failing C test now self-diagnoses in the | |
| # job log. | |
| run: cd build && ctest --output-on-failure --verbose | |
| # --------------------------------------------------------------------- | |
| # AMA_USE_NATIVE_PQC=OFF configuration guard | |
| # --------------------------------------------------------------------- | |
| # ON is and remains the default (top-level CMakeLists.txt) and the only | |
| # configuration setup.py will build — INVARIANT-7 forbids a cryptographic | |
| # fallback, so the shipped wheel always carries the native PQC backends. | |
| # OFF is nonetheless a supported CMake configuration for downstream | |
| # packagers who take PQC from elsewhere, and it is exactly the kind of | |
| # cell that silently rots when nothing builds it: before this job existed, | |
| # `-DAMA_USE_NATIVE_PQC=OFF` left dispatch code referencing four Kyber/ | |
| # Dilithium NTT reference symbols defined only in the gated PQC source | |
| # group. | |
| # | |
| # The observable failure is toolchain-dependent, and the guard catches it | |
| # either way. On this Linux lane GNU ld does NOT set --no-undefined on the | |
| # shared library, so libama_cryptography.so links with those four symbols | |
| # left UNDEFINED; what actually fails is every EXECUTABLE that links it — | |
| # the always-built test binaries and the non-PQC examples — with | |
| # "undefined reference to ama_dilithium_ntt_generic_ref". On macOS/Windows | |
| # the shared-library link itself would fail. The build step below fails on | |
| # the first of these, and the ctest step then proves the tests that survive | |
| # the gate (including test_agent_binding, which needs no PQC symbol) still | |
| # pass rather than merely compile. Fail-closed: no continue-on-error. | |
| c-library-no-native-pqc: | |
| name: C Library (AMA_USE_NATIVE_PQC=OFF configuration guard) | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 15 | |
| steps: | |
| - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 | |
| - name: Install dependencies | |
| run: | | |
| sudo rm -f /etc/apt/sources.list.d/microsoft-prod.list /etc/apt/sources.list.d/azure-cli.list || true | |
| sudo apt-get update | |
| sudo apt-get install -y cmake gcc-13 g++-13 | |
| - name: Configure CMake (native PQC disabled) | |
| run: | | |
| cmake -B build-nopqc \ | |
| -DCMAKE_C_COMPILER=gcc-13 \ | |
| -DCMAKE_CXX_COMPILER=g++-13 \ | |
| -DCMAKE_BUILD_TYPE=Release \ | |
| -DAMA_USE_NATIVE_PQC=OFF \ | |
| -DAMA_BUILD_SHARED=ON \ | |
| -DAMA_BUILD_STATIC=ON \ | |
| -DAMA_BUILD_TESTS=ON \ | |
| -DAMA_BUILD_EXAMPLES=ON | |
| - name: Build | |
| run: cmake --build build-nopqc --config Release -j4 | |
| - name: Test | |
| run: cd build-nopqc && ctest --output-on-failure | |
| - name: Assert the default is still ON | |
| # A regression that flipped the default would make the job above green | |
| # for the wrong reason (it would test the default path twice). Assert | |
| # CMake's RESOLVED value from a default configure (no -D override), not | |
| # a regex over the source line: a source regex both false-passes on a | |
| # trailing '... ON)' comment and false-fails on CMake-valid ON spellings | |
| # (TRUE / 1 / lowercase on). This reads the value CMake actually | |
| # computed, so it is immune to how the option line is written. | |
| shell: bash | |
| run: | | |
| set -o pipefail | |
| cmake -B build-default-probe \ | |
| -DCMAKE_C_COMPILER=gcc-13 \ | |
| -DCMAKE_CXX_COMPILER=g++-13 \ | |
| -DCMAKE_BUILD_TYPE=Release >/dev/null | |
| resolved=$(cmake -LA -N build-default-probe | grep '^AMA_USE_NATIVE_PQC:' || true) | |
| echo "resolved default: ${resolved:-<absent>}" | |
| if [ "$resolved" != "AMA_USE_NATIVE_PQC:BOOL=ON" ]; then | |
| echo "::error::AMA_USE_NATIVE_PQC no longer defaults to ON (resolved: ${resolved:-<absent>})" | |
| exit 1 | |
| fi | |
| echo "AMA_USE_NATIVE_PQC default is ON." | |
| # Python Package Build and Test | |
| python-package: | |
| name: Python ${{ matrix.python-version }} on ${{ matrix.os }} | |
| runs-on: ${{ matrix.os }} | |
| timeout-minutes: 20 | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| os: [ubuntu-latest, macos-latest, windows-latest] | |
| python-version: ['3.10', '3.11', '3.12', '3.13', '3.14'] | |
| steps: | |
| - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 | |
| - name: Set up Python ${{ matrix.python-version }} | |
| id: setup-python | |
| uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0 | |
| continue-on-error: true | |
| with: | |
| python-version: ${{ matrix.python-version }} | |
| # First-party retry path: actions/setup-python intermittently fails on | |
| # GitHub-hosted Windows runners (toolcache / cache-restore flake; see | |
| # PR #306). Only runs on the first miss, so happy path is unchanged. | |
| - name: Set up Python ${{ matrix.python-version }} (retry) | |
| if: steps.setup-python.outcome == 'failure' | |
| uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0 | |
| with: | |
| python-version: ${{ matrix.python-version }} | |
| - name: Install system dependencies (Ubuntu) | |
| if: matrix.os == 'ubuntu-latest' | |
| run: | | |
| sudo rm -f /etc/apt/sources.list.d/microsoft-prod.list /etc/apt/sources.list.d/azure-cli.list || true | |
| sudo apt-get update | |
| sudo apt-get install -y build-essential python3-dev cmake libssl-dev | |
| - name: Install system dependencies (macOS) | |
| if: matrix.os == 'macos-latest' | |
| run: brew install cmake | |
| - name: Install system dependencies (Windows) | |
| if: matrix.os == 'windows-latest' | |
| shell: pwsh | |
| run: | | |
| # CMake is pre-installed on GitHub-hosted Windows runners. | |
| # Only install via Chocolatey (with retries) if it is missing, | |
| # so a transient Chocolatey CDN outage cannot break the build. | |
| if (Get-Command cmake -ErrorAction SilentlyContinue) { | |
| Write-Host "CMake already available: $(cmake --version | Select-Object -First 1)" | |
| } else { | |
| Write-Host "CMake not found; installing via Chocolatey with retries..." | |
| $maxAttempts = 5 | |
| for ($attempt = 1; $attempt -le $maxAttempts; $attempt++) { | |
| Write-Host "Attempt ${attempt}/${maxAttempts}: choco install cmake..." | |
| choco install cmake --installargs 'ADD_CMAKE_TO_PATH=System' -y 2>&1 | |
| if ($LASTEXITCODE -eq 0) { | |
| Write-Host "Successfully installed cmake" | |
| break | |
| } | |
| if ($attempt -lt $maxAttempts) { | |
| $delay = $attempt * 15 | |
| Write-Host "Install failed, waiting ${delay}s before retry..." | |
| Start-Sleep -Seconds $delay | |
| } | |
| } | |
| if ($LASTEXITCODE -ne 0) { | |
| Write-Error "All ${maxAttempts} attempts to install cmake via Chocolatey failed" | |
| exit 1 | |
| } | |
| # Refresh PATH so cmake is available to subsequent steps | |
| Import-Module "$env:ChocolateyInstall\helpers\chocolateyProfile.psm1" | |
| refreshenv | |
| } | |
| - name: Install Python build dependencies | |
| run: | | |
| python -m pip install --upgrade pip setuptools wheel | |
| pip install "Cython>=3.0.0" "numpy>=1.24.0,<3.0.0" | |
| # Pre-build the native C library so the install step can rely on | |
| # an already-populated ``build/`` directory. Mirrors the | |
| # proven-working pattern from ``ci.yml::test`` and avoids the | |
| # latent setup.py CMakeBuild fragility on Windows / macOS that | |
| # surfaces as "Python X.Y on {os} -- exit 1/2" without the | |
| # pre-build. | |
| - name: Build native C library (Linux / macOS) | |
| if: matrix.os != 'windows-latest' | |
| run: | | |
| cmake -B build -DAMA_USE_NATIVE_PQC=ON -DCMAKE_BUILD_TYPE=Release -DAMA_BUILD_TESTS=OFF -DAMA_BUILD_EXAMPLES=OFF | |
| # Use Python's os.cpu_count() for portability — `nproc` is GNU | |
| # coreutils and not guaranteed on macos-latest. Bare `-j` would | |
| # unbound make's parallelism and OOM the runner. | |
| cmake --build build -j"$(python -c 'import os; print(os.cpu_count() or 1)')" | |
| - name: Build native C library (Windows) | |
| if: matrix.os == 'windows-latest' | |
| run: | | |
| cmake -B build -DAMA_USE_NATIVE_PQC=ON -DCMAKE_BUILD_TYPE=Release -DAMA_BUILD_TESTS=OFF -DAMA_BUILD_EXAMPLES=OFF | |
| cmake --build build --config Release --parallel | |
| - name: Install Python dependencies + package (with [dev] extras) | |
| # Use the [dev] extras so hypothesis / pytest-cov / mypy / etc. | |
| # come in via the canonical pyproject.toml declaration rather | |
| # than the legacy requirements-dev.txt. Matches ci.yml::test. | |
| run: pip install -e ".[dev]" | |
| - name: Install differential/interop reference libraries (test-only) | |
| # Independent third-party implementations that let the cross-library | |
| # differential tests (tests/test_differential.py and the PyCA-interop | |
| # tests) actually EXECUTE rather than skip — previously they were dead | |
| # in every CI lane because these libs were never installed. They are | |
| # test-only references, NOT a runtime dependency (INVARIANT-1 forbids | |
| # product code importing them; the invariant lint enforces that only | |
| # ama_cryptography/*.py stays clean). Linux-gated to avoid cross-OS | |
| # wheel flakiness on the primary differential lane. | |
| if: runner.os == 'Linux' | |
| run: pip install cryptography pynacl pycryptodome | |
| - name: Verify module integrity | |
| run: python -m ama_cryptography.integrity --verify | |
| - name: Run tests | |
| env: | |
| # Force backend availability check to fail loudly if the | |
| # native C library didn't build / didn't load — otherwise | |
| # tests silently skip and mask backend regressions. | |
| AMA_CI_REQUIRE_BACKENDS: "1" | |
| # UTF-8 encoding for Windows to support Unicode symbols in output | |
| PYTHONIOENCODING: ${{ matrix.os == 'windows-latest' && 'utf-8' || '' }} | |
| PYTHONUTF8: ${{ matrix.os == 'windows-latest' && '1' || '' }} | |
| run: pytest tests/ -v --tb=short --cov=ama_cryptography --cov-report=xml | |
| - name: Upload coverage | |
| if: matrix.os == 'ubuntu-latest' && matrix.python-version == '3.11' | |
| uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0 | |
| with: | |
| token: ${{ secrets.CODECOV_TOKEN }} | |
| files: ./coverage.xml | |
| flags: python | |
| name: python-${{ matrix.python-version }} | |
| # Linting and Code Quality | |
| lint: | |
| name: Lint and Format Check | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 10 | |
| steps: | |
| - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 | |
| - name: Set up Python | |
| uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0 | |
| with: | |
| python-version: '3.11' | |
| - name: Install dependencies | |
| run: | | |
| # Pinned to requirements-lock.txt so this gate matches the dev | |
| # toolchain and pre-commit (no unpinned ruff, no mypy 1.x/2.x split). | |
| # types-PyYAML keeps tools/check_workflow_commands.py inside | |
| # `mypy --strict`; tests/ imports it, so mypy follows into it. | |
| pip install "black==26.5.1" "ruff==0.15.20" "mypy==2.1.0" \ | |
| "PyYAML==6.0.3" "types-PyYAML==6.0.12.20260724" | |
| - name: Lint (ruff) | |
| run: ruff check . | |
| - name: Check formatting (black) | |
| run: black --check --diff . | |
| - name: Type check (mypy --strict) | |
| run: mypy --strict ama_cryptography/ tests/ | |
| continue-on-error: false | |
| # Security Audit | |
| security: | |
| name: Security Audit | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 10 | |
| steps: | |
| - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 | |
| - name: Set up Python | |
| uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0 | |
| with: | |
| python-version: '3.11' | |
| - name: Install dependencies | |
| run: | | |
| python -m pip install --upgrade pip | |
| pip install pip-audit bandit | |
| # Use the audit-time dependency closure so pip-audit's strict | |
| # mode (below) is reproducible across runner image versions. | |
| pip install -r requirements-lock.txt | |
| # FAIL-CLOSED on the LOCK FILE (INVARIANT-2 / audit Issue 8 + | |
| # Copilot review #322): --strict gates merge on any vulnerable | |
| # package; `--requirement requirements-lock.txt` scopes the audit | |
| # to the pinned dependency closure rather than whatever the | |
| # `pip install pip-audit bandit` step transitively pulled in, | |
| # so the gate is reproducible across reruns. CVE ignores must | |
| # be declared in INVARIANT-14's table with the canonical | |
| # comment, never silently here. | |
| - name: Audit dependencies (strict, lock-file scoped) | |
| run: pip-audit --strict --desc --requirement requirements-lock.txt | |
| # FAIL-CLOSED on Bandit Medium+: any unjustified Medium/High | |
| # finding fails the build. Justified suppressions must carry an | |
| # inline `# nosec` with a tracking ID, validated by | |
| # tools/check_suppression_hygiene.py per INVARIANT-13. | |
| - name: Security scan (bandit, fail on Medium+) | |
| run: | | |
| bandit -r ama_cryptography/ \ | |
| --severity-level medium \ | |
| --confidence-level medium \ | |
| --exit-zero \ | |
| > bandit-medium-plus.txt | |
| echo "=== Bandit Medium/High findings ===" | |
| cat bandit-medium-plus.txt | |
| if grep -E '^\s*(Medium|High):\s*[1-9]' bandit-medium-plus.txt; then | |
| echo "::error::Bandit reports unjustified Medium/High findings — see report above." | |
| exit 1 | |
| fi | |
| # Benchmark Regression Detection (Wheel Install) | |
| # Required job — fails the workflow on detected regression. | |
| # Environment variance on shared CI runners (25-50%) means baselines | |
| # must account for ctypes-fallback throughput, not Cython-optimized peaks. | |
| # | |
| # NAME DISAMBIGUATION: ci.yml::benchmark-regression also produces a | |
| # "Benchmark Regression Detection" status check via an editable install | |
| # (`pip install -e ".[dev]"`). This job is deliberately the wheel-install | |
| # variant — it builds a real wheel via PEP 517 (`python -m build --wheel`), | |
| # installs that wheel, and runs the benchmark harness from a directory | |
| # outside the source tree so the install under test cannot be shadowed | |
| # by `./ama_cryptography/`. Packaging-time regressions (missing | |
| # MANIFEST.in entries, missing package_data, broken setup.py CMakeBuild | |
| # paths, incomplete pyproject.toml [build-system].requires) only surface | |
| # in this variant — they're invisible to an editable install. | |
| # The two checks must remain independently required in branch rulesets. | |
| benchmark-regression: | |
| name: Benchmark Regression Detection (Wheel Install) | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 25 | |
| needs: [c-library] | |
| steps: | |
| - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 | |
| - name: Set up Python | |
| uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0 | |
| with: | |
| python-version: '3.11' | |
| - name: Install system dependencies | |
| run: | | |
| sudo rm -f /etc/apt/sources.list.d/microsoft-prod.list /etc/apt/sources.list.d/azure-cli.list || true | |
| sudo apt-get update | |
| sudo apt-get install -y build-essential cmake libssl-dev | |
| - name: Build wheel via PEP 517 (isolated build env, signed integrity) | |
| # `python -m build --wheel` runs the build in an isolated env that | |
| # only sees pyproject.toml's [build-system].requires — so a missing | |
| # build-time dep (e.g., Cython, cmake, numpy) fails closed here | |
| # rather than silently falling through on a workstation that | |
| # happened to have it. setup.py's CMakeBuild compiles the native | |
| # C library inside the build, so the resulting wheel ships the | |
| # compiled libama_cryptography artifact. | |
| # | |
| # AMA_BUILD_PIPELINE=1 enables setup.py's post-build integrity | |
| # signer (CMakeBuild._run_integrity_signer) so the wheel ships an | |
| # Ed25519-signed integrity artefact, not just the digest-only | |
| # fallback. The signer uses the in-tree ama_ed25519 C kernel via | |
| # ctypes (INVARIANT-1: no PyCA dependency). Tagged releases can | |
| # additionally set AMA_INTEGRITY_TRUST_ANCHOR_PUBKEY_HEX + | |
| # AMA_INTEGRITY_REQUIRE_TRUST_ANCHOR=1 in the release workflow to | |
| # produce trust-anchored wheels — this PR-level job uses a | |
| # per-build ephemeral pubkey (still verified at import time). | |
| env: | |
| AMA_BUILD_PIPELINE: "1" | |
| run: | | |
| python -m pip install --upgrade pip | |
| pip install build | |
| python -m build --wheel --outdir dist/ | |
| ls -lh dist/ | |
| - name: Install built wheel (no source-tree shadow) | |
| # Plain `pip install dist/*.whl` — no `-e`, no fallback to source. | |
| # numpy is only needed to import the optional math_engine extension | |
| # transitively touched by some benchmarks; it's not declared in the | |
| # base wheel's runtime deps because INVARIANT-1 keeps the runtime | |
| # closure dependency-free. | |
| run: | | |
| pip install dist/*.whl | |
| pip install "numpy>=1.24.0,<3.0.0" | |
| - name: Run benchmark regression detection (from temp dir, wheel-only) | |
| # cd OUT of $GITHUB_WORKSPACE so `import ama_cryptography` cannot | |
| # resolve to ./ama_cryptography (the working-tree shadow that | |
| # would silently turn this job into ci.yml's editable variant). | |
| # The site-packages assertion makes the shadow regression noisy: | |
| # if a future commit accidentally re-introduces the editable | |
| # install, this assertion fails the job before benchmarks run. | |
| env: | |
| SRCDIR: ${{ github.workspace }} | |
| run: | | |
| mkdir -p "$RUNNER_TEMP/wheel-bench" | |
| cd "$RUNNER_TEMP/wheel-bench" | |
| python -c " | |
| import ama_cryptography | |
| loc = ama_cryptography.__file__ | |
| assert 'site-packages' in loc, ( | |
| f'wheel install not active — ama_cryptography loaded from {loc!r}; ' | |
| 'editable / source-tree shadow regression. Re-check the install step.' | |
| ) | |
| print(f'OK: ama_cryptography loaded from {loc}') | |
| " | |
| python "$SRCDIR/benchmarks/benchmark_runner.py" \ | |
| --verbose \ | |
| --baseline "$SRCDIR/benchmarks/baseline.json" \ | |
| --output "$SRCDIR/benchmarks/regression_results.json" \ | |
| --markdown "$SRCDIR/benchmarks/regression_report.md" | |
| - name: Upload regression report | |
| if: always() | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| with: | |
| name: benchmark-regression-report | |
| path: | | |
| benchmarks/regression_results.json | |
| benchmarks/regression_report.md | |
| dist/*.whl | |
| # Docker Build — gating: the smoke tests at the bottom of this job | |
| # exercise the exact path a downstream consumer hits on `docker pull`, | |
| # so a build/runtime regression here is a customer-visible regression | |
| # and must block merge. Transient Docker Hub auth/rate-limit failures | |
| # are mitigated in-job via: | |
| # * Pre-pulling the BuildKit image with 8 retries + capped exponential | |
| # backoff (~340s of tolerance) | |
| # * Optional Docker Hub login (gated on both DOCKERHUB_USERNAME and | |
| # DOCKERHUB_TOKEN being set, so a half-configured fork degrades | |
| # cleanly rather than failing noisily) | |
| # If you find yourself wanting to re-add continue-on-error here, | |
| # diagnose the underlying flake instead — silencing this gate masks | |
| # exactly the class of regression it exists to catch. | |
| docker: | |
| name: Docker Build | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 20 | |
| # Job-level env so the optional-login step can gate on env.* in `if:`. | |
| # `secrets.*` is not allowed in step-level `if:` expressions, which | |
| # otherwise causes a workflow-validation error before any job runs. | |
| env: | |
| DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }} | |
| DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }} | |
| steps: | |
| - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 | |
| - name: Pre-pull BuildKit image (with retries) | |
| run: | | |
| # Pre-pull the BuildKit image used by setup-buildx-action to | |
| # survive transient Docker Hub outages (504, network timeouts). | |
| # | |
| # Widened from 5 attempts / ~150s to 8 attempts / ~340s after | |
| # Docker Hub timeouts blocked PR #370 three times in one day. | |
| # The backoff is capped rather than doubling without bound: past | |
| # ~90s per gap the extra wait buys little and the job's 20-minute | |
| # timeout starts to bind. | |
| # | |
| # This is a retry, not a bypass. If the image genuinely cannot be | |
| # fetched the job still proceeds to buildx setup and fails there | |
| # on a real error. Do NOT add continue-on-error here (INVARIANT-2) | |
| # — the smoke tests below exercise the exact path a downstream | |
| # consumer hits on `docker pull`, so silencing this job hides a | |
| # customer-visible regression. | |
| IMAGE="moby/buildkit:buildx-stable-1" | |
| # Try the mirror BEFORE Docker Hub on each attempt. mirror.gcr.io is | |
| # a pull-through cache that serves library and official images | |
| # without a token, so it is not subject to Docker Hub's | |
| # unauthenticated rate limit — which is the actual failure mode this | |
| # ladder exists for. Retagging makes the pulled image satisfy | |
| # setup-buildx-action's reference either way. | |
| MIRROR="mirror.gcr.io/moby/buildkit:buildx-stable-1" | |
| BACKOFF="10 20 40 60 60 60 90" | |
| attempt=1 | |
| for delay in ${BACKOFF} final; do | |
| echo "Attempt ${attempt}/8: pulling ${IMAGE} via mirror..." | |
| if docker pull "${MIRROR}"; then | |
| docker tag "${MIRROR}" "${IMAGE}" | |
| echo "Successfully pulled ${IMAGE} from the mirror on attempt ${attempt}" | |
| exit 0 | |
| fi | |
| echo "Mirror miss; falling back to Docker Hub for attempt ${attempt}..." | |
| if docker pull "${IMAGE}"; then | |
| echo "Successfully pulled ${IMAGE} from Docker Hub on attempt ${attempt}" | |
| exit 0 | |
| fi | |
| if [ "${delay}" = "final" ]; then | |
| break | |
| fi | |
| echo "Pull failed, waiting ${delay}s before retry..." | |
| sleep "${delay}" | |
| attempt=$((attempt + 1)) | |
| done | |
| echo "::warning::All 8 pull attempts for ${IMAGE} failed over ~340s; buildx setup will retry internally" | |
| - name: Set up Docker Buildx | |
| uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4 | |
| with: | |
| # The base-image pulls (ubuntu:22.04, alpine:3.18) happen INSIDE the | |
| # BuildKit container, which does not share the runner's local image | |
| # cache — so the host-side pre-pull above cannot cover them, and | |
| # they were the part of this job still exposed to unauthenticated | |
| # Docker Hub. Docker Hub timeouts blocked PR #370 three times in one | |
| # day, all of them here. | |
| # | |
| # Routing docker.io through mirror.gcr.io takes those pulls off the | |
| # unauthenticated Docker Hub path entirely. This is a parameter of an | |
| # action already in use, not a new dependency. | |
| # | |
| # Strictly additive: BuildKit falls back to the source registry when | |
| # a mirror does not have an image or does not answer, so this cannot | |
| # make a pull fail that would otherwise have succeeded. It also does | |
| # not weaken the gate — the images are content-addressed, so a mirror | |
| # cannot serve different bytes under the same digest. | |
| buildkitd-config-inline: | | |
| [registry."docker.io"] | |
| mirrors = ["mirror.gcr.io"] | |
| - name: Log in to Docker Hub (optional, avoids rate limits) | |
| # Gate on BOTH secrets: a half-configured fork (e.g. username | |
| # set but token missing) would otherwise reach docker/login-action | |
| # and fail noisily, defeating the "optional" intent of the step. | |
| if: env.DOCKERHUB_USERNAME != '' && env.DOCKERHUB_TOKEN != '' | |
| uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0 | |
| with: | |
| username: ${{ env.DOCKERHUB_USERNAME }} | |
| password: ${{ env.DOCKERHUB_TOKEN }} | |
| - name: Build Ubuntu image | |
| uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7 | |
| with: | |
| context: . | |
| file: docker/Dockerfile | |
| push: false | |
| load: true | |
| tags: ama-cryptography:ubuntu | |
| cache-from: type=gha,scope=ubuntu | |
| cache-to: ${{ github.event_name == 'push' && 'type=gha,scope=ubuntu,mode=max' || '' }} | |
| - name: Build Alpine image | |
| uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7 | |
| with: | |
| context: . | |
| file: docker/Dockerfile.alpine | |
| push: false | |
| load: true | |
| tags: ama-cryptography:alpine | |
| cache-from: type=gha,scope=alpine | |
| cache-to: ${{ github.event_name == 'push' && 'type=gha,scope=alpine,mode=max' || '' }} | |
| - name: Test Ubuntu image | |
| run: | | |
| docker run --rm ama-cryptography:ubuntu python3 -c "import ama_cryptography; print('OK')" | |
| - name: Test Alpine image | |
| run: | | |
| docker run --rm ama-cryptography:alpine python3 -c "import ama_cryptography; print('OK')" | |
| ci-gate: | |
| name: Build and Test Gate | |
| # Single aggregating status check for this workflow (see the matching gate | |
| # in ci.yml). `needs:` is workflow-local, so this gate covers only the jobs | |
| # defined in ci-build-test.yml. Branch protection should require this | |
| # context instead of the individual job names. | |
| # | |
| # This list must name EVERY other job in this workflow. A job absent from | |
| # it still runs and still reports its own red X, but branch protection | |
| # only evaluates the gate context — so an absent job cannot block a merge. | |
| # `c-library-no-native-pqc` was exactly that: it guards the | |
| # AMA_USE_NATIVE_PQC=OFF configuration, the build that commit f3dd0c2 on | |
| # this branch had to repair after it broke undetected. It ran green-or-red | |
| # into the void while the gate stayed green. tools/check_gate_coverage.py | |
| # (INVARIANT-31) now fails the PR that omits a job here. | |
| if: always() | |
| needs: | |
| - c-library | |
| - c-library-no-native-pqc | |
| - python-package | |
| - lint | |
| - security | |
| - benchmark-regression | |
| - docker | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 3 | |
| steps: | |
| - name: Fail unless every required job succeeded | |
| # STRICT. Every job in this workflow runs unconditionally — none carries | |
| # a job-level `if:` — so on the authoritative head-commit run each MUST | |
| # be `success`. `skipped` now fails the gate too: on this workflow it can | |
| # only mean a `needs:` dependency failed (already red) or a future job's | |
| # `if:` drifted so it silently stopped running — exactly the gap a | |
| # lenient gate hides. `cancelled` also fails, but only ever appears on a | |
| # run superseded by a newer push or a manual abort, neither of which | |
| # gates the latest head commit. Matrix jobs are fail-fast:false and | |
| # timeouts report as `failure`. | |
| if: ${{ contains(needs.*.result, 'failure') || contains(needs.*.result, 'skipped') || contains(needs.*.result, 'cancelled') }} | |
| run: | | |
| echo "::error::Build and Test Gate failed — a required job did not succeed." | |
| echo "Dependency results: ${{ join(needs.*.result, ', ') }}" | |
| exit 1 | |
| - name: All required jobs succeeded | |
| run: echo "Build and Test Gate passed — all required jobs are green." |