Build and CI

Warning

Some operational workflows on this page remain proposed contracts. The release verification command documented below is implemented and covered by local contract tests.

Packaging, dependency management, CI workflows, Docker images, and release process.

pyproject.toml

PEP 621 metadata; setuptools build backend; Python 3.11+ only.

[build-system]
requires = ["setuptools>=68", "wheel"]
build-backend = "setuptools.build_meta"

[project]
name = "rfgen"
description = "RF synthetic data generation framework"
readme = "README.md"
requires-python = ">=3.11"
license = {file = "LICENSE"}
authors = [{name = "Superpose"}]
dynamic = ["version"]

# ─── Core (always installed) ────────────────────────────────────────────────
dependencies = [
    "numpy>=2.0,<3.0",                      # TorchSig 2.1+ requires numpy 2
    "torch>=2.4,<3.0",
    "torchaudio>=2.4",                      # for resample_poly
    "scipy>=1.13",
    "pydantic>=2.5,<3.0",
    "hydra-core>=1.3,<2.0",
    "omegaconf>=2.3",
    "zarr>=2.16,<4.0",
    "fsspec>=2024.6",
    "blosc>=1.11",
    "typer>=0.12",                          # CLI
    "rich>=13.0",                           # CLI rendering
    "pyyaml>=6.0",
    "h5py>=3.10",
    "sigmf>=1.10.0",                        # SigMF fromarray/fromfile APIs
]

[project.optional-dependencies]
# ─── Backend extras ─────────────────────────────────────────────────────────
torchsig = [
    "torchsig>=2.1.1,<2.2",                 # pinned per lit review §12
]
sionna = [
    "sionna>=2.0.1,<2.1",
    "torch>=2.9,<3.0",                      # Sionna 2.0 PHY/SYS require PyTorch 2.9+
    "mitsuba>=3.5",                         # required by sionna.rt
]
lora = [
    # gr-lora_sdr is GPL; not bundled. We ship a verification helper that
    # invokes it via subprocess if the user installs it separately.
]
cellular = [
    "pyzmq>=26.0",                          # srsRAN_4G ZMQ wrapper
]
radar = [
    "radarsimpy>=10.0",                     # GPL; optional. Basic radar works without.
]

# ─── Infra extras ───────────────────────────────────────────────────────────
spark = [
    "pyspark>=3.5,<4.0",
    "google-cloud-dataproc>=5.10",
    "gcsfs>=2024.6",
    "google-cloud-storage>=2.14",
]
annotator = [
    "google-cloud-aiplatform>=1.40",        # Vertex AI Batch Prediction
    "anthropic>=0.100",
    "openai>=1.50",
]
hil = [
    "uhd>=4.6",                             # USRP
    "pyusrp>=1.0; python_version<'3.12'",   # platform-dependent
    "pyhackrf>=0.0.5",
]

# ─── Dev / test extras ──────────────────────────────────────────────────────
test = [
    "pytest>=8.0",
    "pytest-cov>=5.0",
    "pytest-xdist>=3.5",                    # parallel test execution
    "pytest-mock>=3.12",
    "pytest-asyncio>=0.23",
    "hypothesis>=6.100",                    # property-based testing
]
lint = [
    "ruff>=0.6",
    "mypy>=1.11",
    "pre-commit>=3.7",
]
docs = [
    "sphinx>=8.1",
    "myst-parser>=4.0",
    "furo>=2024.8",
    "sphinx-design>=0.6",
    "sphinx-copybutton>=0.5",
    "sphinx-autobuild>=2024.10",
    "linkify-it-py>=2.0",
]
all = [
    "rfgen[torchsig,sionna,cellular,spark,annotator,test,lint,docs]",
]

[project.scripts]
rfgen = "rfgen.cli.main:app"

[project.entry-points."rfgen.emitters"]
# (Empty by default; first-party emitters register via decorator. Third-party
# emitters declare their own entry-points block in their own pyproject.toml.)

# ─── Build config ───────────────────────────────────────────────────────────
[tool.setuptools.dynamic]
version = {attr = "rfgen._version.__version__"}

[tool.setuptools.packages.find]
where = ["src"]
include = ["rfgen*"]

[tool.setuptools.package-data]
"rfgen" = ["py.typed"]                      # mypy marker (PEP 561)
"rfgen.presets" = ["*.yaml"]
"rfgen.configs" = ["**/*.yaml"]

# ─── Tooling ────────────────────────────────────────────────────────────────
[tool.ruff]
line-length = 100
target-version = "py311"
src = ["src", "tests"]

[tool.ruff.lint]
select = [
    "E", "W", "F",          # pyflakes / pycodestyle
    "I",                    # isort
    "UP",                   # pyupgrade
    "B",                    # bugbear
    "SIM",                  # simplify
    "RUF",                  # ruff-specific
]
ignore = [
    "E501",                 # line too long (formatter handles)
]

[tool.mypy]
python_version = "3.11"
strict = true
warn_unreachable = true
plugins = ["pydantic.mypy"]
exclude = ["build/", "docs/_build/"]

[tool.pytest.ini_options]
minversion = "8.0"
testpaths = ["tests"]
python_files = ["test_*.py"]
pythonpath = ["src"]
markers = [
    "slow: marks tests as slow (skipped on PR; run on main)",
    "gpu: requires GPU",
    "spark: requires Spark cluster (skipped locally)",
    "hil: requires SDR hardware (USRP/HackRF)",
    "annotator: hits LLM APIs (skipped on PR; run weekly)",
    "golden: golden-set verification (slow, full coverage)",
]
filterwarnings = [
    "error",                                # warnings are errors in tests
    "ignore::DeprecationWarning:torchsig.*",
]

[tool.coverage.run]
source = ["src/rfgen"]
omit = ["src/rfgen/_version.py", "src/rfgen/cli/*"]

[tool.coverage.report]
fail_under = 80
show_missing = true
exclude_lines = [
    "pragma: no cover",
    "if TYPE_CHECKING:",
    "raise NotImplementedError",
    "@abstractmethod",
]

>=2.0.1,<2.1 is both the tested CI/runtime range and the declared package extra. This keeps installation aligned with the Sionna PHY and RT APIs used by the shipped propagation adapters.

Version pinning policy

Pinned upstream versions track the Background / Literature Review § Versions and Pinning table. Update frequency:

Dependency

Pin tightness

Update rule

TorchSig

>=2.1.1,<2.2 (patch range)

Pin patch; v2.2 is a major bump (re-verify all integrations)

Sionna

>=2.0.1,<2.1 (tested patch range)

Pin patch; re-verify all propagation integrations before v2.1

PyTorch

>=2.4,<3.0 (major range)

Track latest minor

NumPy

>=2.0,<3.0

TorchSig 2.1 requires numpy 2

Pydantic

>=2.5,<3.0

v3 will be a major bump

Hydra

>=1.3,<2.0

v2 in development; pin 1.x

Zarr

>=2.16,<4.0

v3 is supported but defaults to v2 layout

Sigstore

==4.4.0

Exact DSSE API pin; changing it requires adapter and real-fixture revalidation

CI runs against the lowest-supported versions in addition to latest; both pass before merge.

Attestation dependency boundary

Offline supply-chain evidence verification

Release qualification is deliberately split into two boundaries. External, release-owned tooling produces and verifies the evidence; rfgen’s supply_chain_evidence module then makes a deterministic offline decision from parsed facts and hash commitments. The module does not read release artifacts, invoke Syft, Trivy, or Cosign, reach a registry, or call any network-backed verification service.

For every release artifact, retain the actual artifact and these immutable, content-addressed artifacts:

Release artifact kind

Required evidence

OCI_AMD64

SBOM, vulnerability scan, signature bundle, and provenance

OCI_ARM64

SBOM, vulnerability scan, signature bundle, and provenance

SDIST

SBOM, vulnerability scan, signature bundle, and provenance

WHEEL

SBOM, vulnerability scan, signature bundle, and provenance

The qualification report stores each artifact SHA-256 plus the SHA-256 of its SBOM, scan, signature bundle, and provenance. A passing report contains all four kinds exactly once, is sorted canonically, has no causes, and carries its own SHA-256 over RFC 8785 canonical JSON excluding that hash field. Retain the source revision, exact signing identity and issuer, parsed tool versions, and the original evidence bytes with the report so an offline verifier can compare their recorded hashes.

Tool and evidence policy

The evidence producer, outside this module, must use supported inputs:

  • Syft 1.x to produce the SBOM, represented as CycloneDX 1.6.

  • Trivy 0.58.0 or newer to produce the vulnerability scan.

  • Cosign 2.x to verify the signature bundle and provenance, including the required identity and issuer.

  • SLSA provenance schema 1.0.

The producer parses those real outputs into ParsedArtifactEvidenceV1 and provides them through SupplyChainEvidenceAdapter. This adapter is only a typed input seam. It must not substitute synthetic output for a missing tool, claim that a scan, signature, or provenance verified when it did not, or invent hashes, findings, identities, issuers, signatures, scans, or provenance. Missing evidence, mismatched hashes or policy identity, false verification flags, and unexcepted HIGH or CRITICAL CVEs fail qualification.

An exception is permitted only for one matching HIGH or CRITICAL CVE. It must name an owner and mitigation, bind to the scan’s SHA-256, and expire within 30 days of creation. Offline release verification supplies an explicit as_of date when reproducible expiry evaluation is needed.

Example release gate, after the external evidence collection and verification steps have completed:

from datetime import date

from rfgen.supply_chain_evidence import ParsedEvidenceSupplyChainVerifier

result = ParsedEvidenceSupplyChainVerifier(
    adapter=parsed_evidence_adapter,
    policy=release_policy,
).verify(report, as_of=date(2026, 7, 15))
if result.status != "PASS":
    raise RuntimeError("release supply-chain qualification failed: " + "; ".join(result.causes))

This gate verifies supplied evidence only. It cannot make absent evidence trustworthy, and it must fail rather than fabricate or infer a successful external tool result.

Clean-wheel product gate

tests/golden/test_clean_wheel_workflow.py is a slow and golden test, so the default local selection excludes it. Run it deliberately from a dev environment that includes the declared build tool:

python -m pytest -q -m golden tests/golden/test_clean_wheel_workflow.py

The gate builds both an sdist and wheel, creates an isolated venv outside the checkout, copies only the resolved marker-selected wheel dependency closure into that venv, then installs the built rfgen[torch] wheel with networking disabled. It rejects any external site-packages path and requires every loaded rfgen module to originate in the venv. The fixture writes and reopens 32 deterministic seed-1337 QPSK records, checks a persisted offline caption, publishes a manifest, and verifies fresh-process reproduction. Its complete workflow deadline is 120 seconds and all direct-child, driver, and reproduction RSS evidence must remain at or below 2 GiB.

This is a local merge gate while GitHub Actions are intentionally disabled; it does not enable or wait for remote CI.

The installed package pins sigstore==4.4.0 exactly. Its verified wheel SHA-256 is 80c36d08b02479e2a282d0bea93de68fe0d43a93b0d58e4d9ac6bb9f8425957c; SIGSTORE_VERSION and SIGSTORE_WHEEL_SHA256 expose the same contract to callers.

Local attestation evidence exercises the real offline Verifier.staging(offline=True).verify_dsse(...) path with the upstream Sigstore v4.4.0 staging fixture retained under tests/fixtures/sigstore/ and its recorded provenance hashes. The rfgen adapter requires canonical RFC 8785 DSSE statement bytes and verifies the exact Sigstore identity and issuer before it parses the typed statement. This is a signing and trust-root verification boundary only: it neither publishes an attestation nor makes a domain-policy or release-admission decision.

Repository layout for build artifacts

rf-data-generation/
├── pyproject.toml
├── src/rfgen/                  # package source
├── tests/                      # test suite
├── docs/                       # Sphinx sources
├── docker/
│   ├── Dockerfile.spark        # production Spark executor image
│   ├── Dockerfile.dev          # local dev container
│   └── entrypoint.sh
├── scripts/
│   ├── release.sh              # tags, builds wheels, publishes
│   ├── update-deps.sh          # pip-compile-driven lockfile refresh
│   └── migrations/             # one-shot schema migrations
├── .github/
│   └── workflows/
│       ├── test.yml            # unit + integration on every PR
│       ├── lint.yml            # ruff + mypy on every PR
│       ├── build.yml           # wheel build sanity-check
│       ├── docs.yml            # sphinx build + GitHub Pages deploy
│       ├── golden.yml          # nightly golden-set verification
│       ├── spark-image.yml     # Docker image build + push to GCR
│       ├── release.yml         # tagged release → PyPI publish
│       └── nightly.yml         # full integration suite + LLM tests
└── .pre-commit-config.yaml

CI workflows

.github/workflows/test.yml: runs on every PR

name: Test

on:
  pull_request:
    branches: [main]
  push:
    branches: [main]

permissions:
  contents: read

concurrency:
  group: test-${{ github.ref }}
  cancel-in-progress: true

jobs:
  test:
    runs-on: ubuntu-latest
    strategy:
      fail-fast: false
      matrix:
        python: ["3.11", "3.12"]
        deps: ["lowest", "latest"]

    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-python@v5
        with:
          python-version: ${{ matrix.python }}
          cache: pip

      - name: Install (lowest deps)
        if: matrix.deps == 'lowest'
        run: |
          pip install -e ".[torchsig,test]" \
            --constraint constraints/lowest.txt

      - name: Install (latest deps)
        if: matrix.deps == 'latest'
        run: pip install -e ".[torchsig,test]"

      - name: Run unit + contract tests
        run: pytest -m "not slow and not gpu and not spark and not hil and not annotator" \
                    -n auto \
                    --cov=rfgen \
                    --cov-report=xml

      - name: Upload coverage
        if: matrix.python == '3.11' && matrix.deps == 'latest'
        uses: codecov/codecov-action@v4
        with:
          files: ./coverage.xml

  test-sionna:
    runs-on: ubuntu-latest        # GPU jobs need self-hosted; gated separately
    if: contains(github.event.pull_request.labels.*.name, 'sionna')
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: "3.11"
      - name: Install sionna extras
        run: pip install -e ".[sionna,test]"
      - name: Run sionna tests (CPU mode)
        run: pytest tests/integration/test_sionna_*.py

.github/workflows/lint.yml

name: Lint

on:
  pull_request:
    branches: [main]

jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with: { python-version: "3.11", cache: pip }

      - name: Install lint deps
        run: pip install -e ".[lint]"

      - name: Ruff (format check)
        run: ruff format --check src tests

      - name: Ruff (lint)
        run: ruff check src tests

      - name: Mypy
        run: mypy src/rfgen

.github/workflows/golden.yml: nightly golden-set verification

name: Golden-set verification

on:
  schedule:
    - cron: "0 5 * * *"           # 05:00 UTC daily
  workflow_dispatch:

jobs:
  golden:
    runs-on: ubuntu-latest
    timeout-minutes: 60
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with: { python-version: "3.11", cache: pip }

      - name: Install
        run: pip install -e ".[torchsig,sionna,test]"

      - name: Generate reproducibility baseline
        run: |
          rfgen generate +preset=narrowband_classifier_baseline_xs \
              run.seed=42 \
              storage.path=./golden-current

      - name: Diff against baseline
        run: |
          gsutil -m rsync -r gs://rfgen-ci-golden/v0/baseline ./golden-baseline
          # Compare IQ + structured labels byte-by-byte; fail on any diff
          python scripts/diff_zarr.py ./golden-baseline ./golden-current \
              --strict-iq --strict-labels

      - name: Run golden-set tests
        run: pytest -m golden tests/integration/

.github/workflows/spark-image.yml

name: Spark image

on:
  push:
    branches: [main]
    paths:
      - "docker/Dockerfile.spark"
      - "src/rfgen/**"
      - "pyproject.toml"
  workflow_dispatch:

jobs:
  build:
    runs-on: ubuntu-latest
    permissions:
      id-token: write
      contents: read
    steps:
      - uses: actions/checkout@v4

      - id: gcp-auth
        uses: google-github-actions/auth@v2
        with:
          workload_identity_provider: ${{ secrets.GCP_WIF_PROVIDER }}
          service_account: rfgen-ci@${{ secrets.GCP_PROJECT }}.iam.gserviceaccount.com

      - uses: google-github-actions/setup-gcloud@v2

      - name: Configure Docker for GCR
        run: gcloud auth configure-docker us-central1-docker.pkg.dev

      - name: Build + push image
        run: |
          IMAGE=us-central1-docker.pkg.dev/$GCP_PROJECT/rfgen/spark:$(git rev-parse --short HEAD)
          docker build -f docker/Dockerfile.spark -t $IMAGE .
          docker push $IMAGE
          # Tag latest after push
          docker tag $IMAGE us-central1-docker.pkg.dev/$GCP_PROJECT/rfgen/spark:latest
          docker push us-central1-docker.pkg.dev/$GCP_PROJECT/rfgen/spark:latest
        env:
          GCP_PROJECT: ${{ secrets.GCP_PROJECT }}

.github/workflows/release.yml

name: Release

on:
  push:
    tags: ["v*"]

permissions:
  contents: write
  id-token: write               # for trusted PyPI publishing

jobs:
  release:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with: { fetch-depth: 0 }

      - uses: actions/setup-python@v5
        with: { python-version: "3.11" }

      - name: Verify tag matches version
        run: |
          TAG=${GITHUB_REF#refs/tags/v}
          PKG_VER=$(python -c "from rfgen._version import __version__; print(__version__)")
          [ "$TAG" = "$PKG_VER" ] || { echo "Tag $TAG != package $PKG_VER"; exit 1; }

      - name: Build wheel + sdist
        run: |
          pip install build
          python -m build

      - name: Publish to PyPI (trusted publishing)
        uses: pypa/gh-action-pypi-publish@release/v1

      - name: Create GitHub release
        uses: softprops/action-gh-release@v2
        with:
          generate_release_notes: true
          files: dist/*

.github/workflows/nightly.yml

name: Nightly full integration

on:
  schedule:
    - cron: "0 4 * * *"
  workflow_dispatch:

jobs:
  full:
    runs-on: ubuntu-latest-large            # 16 vCPU runner
    timeout-minutes: 180
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with: { python-version: "3.11" }
      - run: pip install -e ".[all]"
      - name: Run slow + integration suite
        run: pytest -m "slow or integration" -n 4
      - name: Run annotator suite
        run: pytest -m annotator
        env:
          GOOGLE_APPLICATION_CREDENTIALS: /tmp/sa.json
          ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
          OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}

Pre-commit hooks

.pre-commit-config.yaml:

repos:
  - repo: https://github.com/astral-sh/ruff-pre-commit
    rev: v0.6.9
    hooks:
      - id: ruff
        args: [--fix]
      - id: ruff-format

  - repo: https://github.com/pre-commit/pre-commit-hooks
    rev: v5.0.0
    hooks:
      - id: trailing-whitespace
      - id: end-of-file-fixer
      - id: check-yaml
        args: [--unsafe]                    # Hydra YAML uses !!python/name
      - id: check-merge-conflict
      - id: check-added-large-files
        args: [--maxkb=500]
      - id: detect-private-key

  - repo: local
    hooks:
      - id: no-non-deterministic-seeds
        name: Ban time.time() and os.urandom in src/rfgen/
        entry: scripts/lint/check_no_random_seeds.sh
        language: system
        files: ^src/rfgen/.*\.py$

Docker images

docker/Dockerfile.spark

Production Spark serverless executor image:

FROM us-central1-docker.pkg.dev/cloud-dataproc/dataproc-templates/python:1.0.0

# System deps for Sionna RT (Mitsuba)
RUN apt-get update && apt-get install -y --no-install-recommends \
    libgl1 libxrender1 libxext6 \
    && rm -rf /var/lib/apt/lists/*

# Python deps
COPY pyproject.toml /opt/rfgen/
COPY src/ /opt/rfgen/src/
WORKDIR /opt/rfgen
RUN pip install --no-cache-dir -e ".[torchsig,sionna,spark]"

# Determinism guard
ENV PYTHONHASHSEED=0
ENV TF_DETERMINISTIC_OPS=1
ENV CUBLAS_WORKSPACE_CONFIG=":4096:8"

ENTRYPOINT ["/opt/rfgen/docker/entrypoint.sh"]

docker/Dockerfile.dev

Local dev container (smaller, no Spark base):

FROM python:3.11-slim

RUN apt-get update && apt-get install -y --no-install-recommends \
    git build-essential libgl1 libxrender1 \
    && rm -rf /var/lib/apt/lists/*

WORKDIR /workspace
COPY pyproject.toml ./
RUN pip install --no-cache-dir -e ".[all]"

Release process

Release support inventory publication

Before tagging a release, collect the installed managed-provider and HIL entry points together with the packaged production configuration. Feed the resulting AdvertisedInventoryV1, the Layer 11 execution-evidence records, the claim-source index, scientific coverage report, and retained support evidence to build_release_support_inventory. The builder is an offline policy step: it does not run cloud jobs or tests, and it returns no inventory if an input is missing, stale, contradictory, or non-passing.

Use the returned ReleaseSupportInventoryV1.inventory_sha256 as the release matrix identity. Retain the advertised inventory, claim index, scientific coverage report, row evidence URIs, and the exact source revision alongside that value. Local validation is the current merge gate while hosted CI is disabled; see Release Support Inventory for the exact model and failure-code contract.

Benchmark qualification

After support-inventory publication, retain five timing and five peak-memory observations for each release benchmark on the signed reference host. Create or verify BenchmarkBaselineSetV1 through the Layer 14 DSSE boundary, retaining the PASS support-evidence ID for every baseline row. Candidate qualification never updates this baseline set.

Pass the complete Chapter 7 Layer 11 evidence context to qualify_benchmarks. It requires a Wave 4 manifest whose base commit is the support-inventory source revision and one real, passing BENCHMARKS check. The check environment hash binds the reference host; its retained artifact hashes remain independent evidence identities. A performance regression produces a self-hashed FAIL report with all causes; malformed input produces no report.

Only a self-hash-verified PASS report bound to the exact support inventory and baseline set may be passed to seal_benchmark_wave. That operation creates a rehash candidate for Wave 5 and immediately gives it back to Layer 11 for manifest validation. Retain the baseline set, DSSE bundle, support inventory, report, benchmark artifacts, and Wave 5 manifest together.

The inclusive p95 rule intentionally preserves fractional byte values. For example, (1, 2, 3, 4, 5) bytes has inclusive p95 4.8 bytes. Do not round that stored statistic before comparison or publication.

Semantic versioning: MAJOR.MINOR.PATCH. Release cadence:

Bump

Triggers

Patch (0.5.00.5.1)

Bug fix; no schema or API change

Minor (0.5.00.6.0)

New emitter family; new annotation type; additive label-schema field; new config option (with sensible default)

Major (0.x1.0.0)

Breaking ABC change; renamed config field; removed feature; major label-schema bump (requires migration)

Step-by-step

# 1. Update version
echo '__version__ = "0.5.1"' > src/rfgen/_version.py

# 2. Update CHANGELOG
$EDITOR CHANGELOG.md

# 3. Run full local tests
pytest -m "not gpu and not spark and not hil"

# 4. Verify docs build
sphinx-build docs docs/_build/html

# 5. Verify reproducibility smoke test
rfgen generate +preset=narrowband_classifier_baseline_xs run.seed=42 \
    storage.path=./reproduce-check
diff -r ./out/baseline ./reproduce-check    # must be empty

# 6. Commit, tag, push
git add src/rfgen/_version.py CHANGELOG.md
git commit -m "Release 0.5.1"
git tag v0.5.1
git push origin main --tags

The release.yml workflow then publishes to PyPI and creates a GitHub Release. The spark-image.yml workflow builds and pushes a tagged Docker image.

Release rehearsal

Before Wave 6 promotion, run the fixed release rehearsal with run_release_rehearsal(spec, deadline=...). The self-hashed RehearsalSpecV1 fixes the release revision, reference environment, provider rows, command selectors, and a four-hour global budget. Its steps are ordered: INSTALL, GOLDEN, EXPORT, MIGRATE, CONCURRENT_COMMIT, ANNOTATION_COMMIT, KILL_RESUME, MANAGED_CANARY, RESTORE, SIGNATURE_VERIFY, and ROLLBACK.

Each ordinary step depends on its predecessor. A failure marks descendants SKIPPED with DEPENDENCY; ROLLBACK always runs after the last attempted step. Only KILL_RESUME and MANAGED_CANARY may use two attempts. Every step timeout is one through 3,600 seconds. Process execution is capped by the step timeout, the remaining four-hour monotonic budget, and the caller deadline. A global or caller-deadline cap produces GLOBAL_TIMEOUT; rollback has its own bounded cleanup attempt.

The rehearsal first validates the Chapter 7 manifest and gate through the Layer 11 API. Wave 5 must be the exact release_revision, and the manifest must have no active writers. A passing post-run REHEARSAL gate binds the exact ordered command selectors, including required provider selectors, the environment digest, and immutable PROCESS evidence. Missing, mock, skipped, or unavailable required evidence fails the rehearsal.

run_release_rehearsal returns only RehearsalReportV1. It does not promote a wave. After a PASS report and its validated Chapter 7 gate receipt, seal_release_rehearsal appends one Wave 6 commit to the retained Wave 5 manifest, rehashes it through Layer 11, and validates it again. A report or gate receipt from another spec, manifest, or run cannot seal Wave 6.

from datetime import UTC, datetime

from rfgen.release_rehearsal import run_release_rehearsal, seal_release_rehearsal

report = run_release_rehearsal(
    rehearsal_spec,
    deadline=datetime(2026, 7, 15, 20, 0, tzinfo=UTC),
)
if report.overall.value != "PASS":
    raise RuntimeError("release rehearsal failed")

sealed_manifest = seal_release_rehearsal(
    report, rehearsal_spec, chapter_context, post_run_chapter_gate, wave_six, validator
)

Release-evidence assembly and manifest signing

After the Wave 6 rehearsal is sealed, assemble the release input with assemble_unsigned_release_manifest(inputs, created_at=..., validator=..., artifact_reader=...). The operation is an offline validation boundary. It does not publish an artifact, call an external gate, or sign anything. It returns UnsignedReleaseManifestV1 only when every required benchmark, rehearsal, scientific-coverage, HIL, disaster-recovery, and supply-chain report is PASS, the rehearsal cleanup is PASS, and the supplied Layer 11 bundle validates as one exact record universe.

The unsigned manifest is RFC 8785 canonical JSON. Its SHA-256 is calculated only from that unsigned object. Assembly copies release version, distribution version, source revision, support lifecycle, migration range, plugin range, limitations, and trust-policy hash from the support inventory. It also requires the retained artifacts to hash to their exact persisted bytes. The release includes every authoritative acceptance gate and module status through Layer 51, final Chapter 1 through 6 manifests and passing chapter gates, the Chapter 7 Wave 6 checkpoint, and every scientific handoff/result pair.

The execution-evidence schema and validation report are control artifacts, not distributable payloads. Their IDs are respectively execution-evidence-schema-v1 and execution-validation-report-v1; the schema must match both its tracked raw bytes and canonical JSON hash, while the report artifact must equal the RFC 8785 serialization excluding its own report_sha256. Neither control artifact may carry an SBOM, provenance, or artifact-signature row. A distributable wheel, source distribution, OCI image, Helm chart, Terraform package, or docs artifact instead requires exactly one SBOM, provenance record bound to its hash, and artifact signature.

Finalize only the validated unsigned object with finalize_release_manifest(unsigned, adapter=..., policy=..., bundle_writer=...). It creates an in-toto Statement v1 whose only subject is release-manifest.json with the unsigned hash and whose predicate is RELEASE_MANIFEST_V1. The Layer 14 SigstoreDSSEAdapter signs and immediately verifies that exact typed predicate before the injected writer receives the bundle bytes and SHA-256. A writer or verification failure returns no final manifest.

Consumers use verify_release_manifest_hash(final, adapter=..., policy=..., bundle_reader=...). It constant-time compares the recomputed unsigned hash, verifies the persisted bundle hash, parses the Sigstore bundle, enforces the identity, issuer, subject, and predicate policy, and constant-time compares the reconstructed unsigned manifest bytes. Any mismatch fails closed with ReleaseEvidenceValidationError; assembly or verification must not be treated as a release admission for a later Wave 7 or Layer 52 decision.

Release evidence verification

After signed-manifest assembly, use the implemented command to make the final release-qualification decision:

rfgen release verify MANIFEST [--offline-bundle PATH] [--json] [--timeout 900]

MANIFEST is the persisted signed release manifest. --timeout is a global deadline in seconds and accepts integers from 1 through 3600. --json emits the self-hashed verification report, the Chapter 7 receipt when one exists, and any promoted module-status records. Without --json, the command prints the report outcome, exit code, and number of promotions.

The CLI does not resolve release evidence by itself. The embedding application must first call configure_release_verification_runtime(factory), where the factory contract is:

def factory(
    manifest: ReleaseManifestV1,
    offline_bundle: Path | None,
    timeout: int,
) -> ReleasePromotionOutcomeV1: ...

The CLI parses MANIFEST into ReleaseManifestV1, passes the optional offline directory path unchanged, and passes the bounded integer timeout. The application owns conversion of that path into OfflineBundleIndexV1, assembly of ReleaseVerificationInputsV1, artifact reading, trust-policy selection, and construction of the verifier. It must not assume the CLI discovers an index or derives release inputs from a directory. If no factory was installed, the command writes the redacted JSON object {"error": "RUNTIME_UNCONFIGURED", "exit_code": 6} to standard error and exits 6.

The verifier rechecks the signed manifest through the Layer 14 DSSE boundary, the complete Layer 11 execution-evidence record universe, retained control artifact bytes, support rows, and Chapter 7 sealing lineage. It emits one sorted check row per verification stage. A report contains its RFC 8785 canonical JSON SHA-256 excluding only report_sha256; consumers validate that self-hash before using a report or promotion.

All independent post-parse stages are collected before precedence is applied. The command exits with the earliest applicable code:

Exit code

Meaning

0

Every required check passed; only an offline success with a valid Chapter 7 receipt can create release-qualified status records.

2

Manifest invocation or schema failure before artifact verification.

3

Trust, signature, or transparency failure.

4

Artifact or execution-evidence hash failure.

5

Support or release-gate failure.

6

Global deadline or redacted internal failure.

Use --offline-bundle to pass a self-contained verification directory to the application-owned factory. The resulting index requires normalized relative paths, no duplicate paths or symlinks, exact sizes and SHA-256 values, and at most 8 GiB of declared files. Offline mode requires every listed evidence object, including the pinned Sigstore trusted root and transparency material. It constructs verification from that pinned root and does not refresh TUF metadata or perform DNS or network lookup. A missing or changed offline object fails closed; the verifier does not fall back to an online trust source.

See Manifest verification for the signed manifest boundary, Execution evidence for the record universe, and Release support inventory for support-lifecycle inputs.

Major release additional steps

  • Write a migration script under scripts/migrations/v{from}_to_v{to}.py

  • Update manifest.json["schema_changes"] with a description of the change

  • Update Background / Roadmap with the new milestone state

  • Cross-check Reference / Label Schema § Schema versioning for the new policy

Pre-release tags

v0.5.0rc1, v0.5.0rc2, etc. published to PyPI as pre-releases (not picked up by pip install rfgen without --pre).


Reproducibility lockfile

For exact-byte reproducibility across users / CI / Spark, we maintain a constraints lockfile:

pip-compile --extra=all pyproject.toml --output-file=constraints/main.txt
pip-compile --extra=all pyproject.toml --output-file=constraints/lowest.txt --lower-bound

The Spark Docker image is built against constraints/main.txt. CI’s “lowest” matrix uses constraints/lowest.txt. Both pass before merging to main.


See Also