ADR-10: Cross-repo release train

Status: accepted (2026-05-28) Blocks: workstream C (release process), workstream D (Python packaging).

Context

dag-ml depends on dag-ml-data; nirs4all depends on dag-ml + dag-ml-data (eventually). Without a scripted release order, version pinning drifts (Codex flagged this as the “cross-repo release coordination” risk).

Decision

The release train is a fixed five-step sequence, scripted and CI-gated:

  1. dag-ml-data releases first. cargo-release cuts a tag, publishes the publishable workspace crates to crates.io, builds the Python wheel target (dag-ml-data-py) for the configured PyPI publisher, and runs the cross-header parity test against dag-ml at HEAD.

  2. dag-ml’s pinned dag-ml-data version bumps. A scripted PR (opened by the release workflow) updates the workspace Cargo.toml’s dag-ml-data = { version = "X.Y.Z" } constraint, runs the green gate, and merges automatically if the gate is green.

  3. dag-ml releases. Same scripted path: cargo-release → crates.io for the publishable workspace crates → Python wheel target for dag-ml-py through the configured PyPI publisher → CHANGELOG entry referencing ADR-10.

  4. nirs4all’s pinned dag-ml + dag-ml-data versions bump. Same scripted-PR pattern; nirs4all’s CI gates on dag-ml ≥ X.Y.Z AND dag-ml-data ≥ X.Y.Z.

  5. nirs4all releases (independent cadence; usually one release per train run).

CI gates

  • Each release tag triggers release-crates.yml in the repo being released. The workflow validates release metadata, validates the Cargo publish plan, checks that the v* tag matches the workspace version and publishes crates in dependency order.

  • release-crates.yml accepts final SemVer tags such as v0.2.0 and still supports prerelease SemVer tags for future alpha/beta/rc trains when explicitly needed.

  • cargo publish --dry-run runs in CI through scripts/release/check_publish_plan.py --dry-run.

  • A “release ready” PR check verifies CHANGELOG, ADR delta, and ABI snapshot are present.

Hotfix path

For a single-repo hotfix (e.g. dag-ml only, leaving dag-ml-data unchanged), the release script accepts --skip-data and runs steps 3–5 directly. The CHANGELOG entry must justify the skip.

Consequences

  • scripts/release/ lands in both repos with per-repo release-crates.yml GitHub Actions workflows for crates.io publication.

  • CONTRIBUTING.md documents the train and the hotfix exception.

  • Version pinning becomes a hard CI constraint; manual edits are reverted by the bot.

Risk

  • A breaking change in dag-ml-data that dag-ml cannot adopt blocks the whole train. The release script surfaces this with a clear “dag-ml does not compile against dag-ml-data X.Y.Z” error and refuses to proceed. Resolution: either revert the breaking change or land the dag-ml-side migration first.