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 |
|
Pin patch; v2.2 is a major bump (re-verify all integrations) |
Sionna |
|
Pin patch; re-verify all propagation integrations before v2.1 |
PyTorch |
|
Track latest minor |
NumPy |
|
TorchSig 2.1 requires numpy 2 |
Pydantic |
|
v3 will be a major bump |
Hydra |
|
v2 in development; pin 1.x |
Zarr |
|
v3 is supported but defaults to v2 layout |
Sigstore |
|
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 |
|---|---|
|
SBOM, vulnerability scan, signature bundle, and provenance |
|
SBOM, vulnerability scan, signature bundle, and provenance |
|
SBOM, vulnerability scan, signature bundle, and provenance |
|
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 ( |
Bug fix; no schema or API change |
Minor ( |
New emitter family; new annotation type; additive label-schema field; new config option (with sensible default) |
Major ( |
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}.pyUpdate
manifest.json["schema_changes"]with a description of the changeUpdate Background / Roadmap with the new milestone state
Cross-check
Reference / Label Schema § Schema versioningfor the new policy
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¶
Reference / Project Layout: repo structure
Reference / Determinism: what reproducibility means
How-to / Run on Spark: how the Docker image is consumed
Background / Roadmap: milestone-driven release cadence