diff --git a/.github/release-gates.json b/.github/release-gates.json index f3c2afdd8..9b7c50d6b 100644 --- a/.github/release-gates.json +++ b/.github/release-gates.json @@ -2,7 +2,7 @@ "pending_migrations": [ { "pull_request": "https://github.com/ai-dynamo/dynamo/pull/14065", - "requirement": "Refresh and merge Dynamo's Planner and wheel-smoke migration to best_available(config), with required worker_type, nested estimator_config, and the released-wheel compatibility bridge. Validate against the canonical wheel before clearing this gate and releasing the API." + "requirement": "Publish a canonical nightly wheel, then validate and merge Dynamo's Planner and wheel-smoke migration to best_available(config), with required worker_type, nested estimator_config, and the released-wheel compatibility bridge. Clear this gate before a stable API release; it must not block development nightly publication." } ] } diff --git a/.github/workflows/nightly-ci.yml b/.github/workflows/nightly-ci.yml index 19d82cbed..91ab6401b 100644 --- a/.github/workflows/nightly-ci.yml +++ b/.github/workflows/nightly-ci.yml @@ -130,20 +130,8 @@ jobs: } } core.setFailed('commit_sha is not on main or any release/* branch'); - # Both scheduled and approved manual publication require completed migrations. - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 - with: - ref: ${{ steps.target.outputs.sha }} - persist-credentials: false - # Keep the checker/current policy trusted while also checking the exact - # publication target's migration declarations. Neither can bypass the other. - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 - with: - ref: ${{ github.sha }} - path: release-policy - persist-credentials: false - - name: Require completed downstream API migrations - run: python3 release-policy/scripts/check_release_migrations.py --target-gates .github/release-gates.json + # Downstream migrations consume this development wheel before they can + # merge. Their completion is a stable-release prerequisite, not a nightly gate. # Computed once here so both arch legs stamp the same date even if the # run crosses a UTC midnight. - name: Compute unique nightly version diff --git a/docs/ci.md b/docs/ci.md index 2c08deb22..1539f8569 100644 --- a/docs/ci.md +++ b/docs/ci.md @@ -356,6 +356,14 @@ by its zero-padded ten-digit workflow run number, for example retries retain the same version, and later dates sort after earlier dates. Builds use pinned tooling and record checksums and provenance. +Development nightlies must be available before downstream consumers can validate +and merge an API migration. Pending entries in +[the stable-release migration checklist](../.github/release-gates.json) therefore +do not block scheduled or approved manual nightlies. Build, compliance, wheel-smoke, +FPE qualification, and security requirements continue to apply. Publish the nightly, +validate and merge the downstream migration against that wheel, then complete the +migration checklist before a stable release. + Python dependency licenses are checked in isolated jobs on both architectures before building or staging. Artifacts are then staged directly to internal Artifactory through the protected `automated-release` environment. Each wheel diff --git a/docs/core-api.md b/docs/core-api.md index 26c304e20..f69610193 100644 --- a/docs/core-api.md +++ b/docs/core-api.md @@ -328,18 +328,14 @@ past-KV coordinate rather than the op-level mean-context coordinate. role/options signatures and separate estimator constructors. This is a source migration: use `ForwardPassPerfModelConfig::new(...)` in Rust or the SDK config class in Python, and use the explicit migration helper for saved EngineConfig - values. Downstream Dynamo callers must migrate before this API is released; + values. Downstream Dynamo callers must migrate before this API's stable release; keep the crate and wheel versions aligned at the coordinated minor release. -- Publication is blocked by [the release gate](../.github/release-gates.json) - until [Dynamo #14065](https://github.com/ai-dynamo/dynamo/pull/14065) is refreshed, - merged, and its Planner/wheel smoke validated against this API. Both scheduled - nightly CI and approved manual dispatch run `scripts/check_release_migrations.py` - before staging and the downstream publish trigger. Manual dispatch may select - a main/release commit, but both the workflow revision's policy and the selected - commit's migration declarations must pass before publication. The checker runs - from the workflow revision; missing or malformed target gates fail closed. - Clear the pending entry in a reviewed change only after the migration evidence - is available. +- [The migration checklist](../.github/release-gates.json) and + `scripts/check_release_migrations.py` apply before stable publication. Clear the + pending entry in a reviewed change after downstream validation and merge. + There is currently no standalone stable-publication workflow in this repository; + that release process must invoke the checker for both its policy and target + declarations (`--target-gates`). Missing or malformed declarations fail closed. - The raw PyO3 class and ergonomic SDK wrapper intentionally share the name `RustForwardPassPerfModel`; callers should import from `aisimulate_core.sdk` unless they specifically need the JSON-oriented native binding. diff --git a/scripts/check_release_migrations.py b/scripts/check_release_migrations.py index ec395b9d4..4b6b415e1 100644 --- a/scripts/check_release_migrations.py +++ b/scripts/check_release_migrations.py @@ -2,7 +2,7 @@ # SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 -"""Block release staging while source-controlled downstream migrations remain.""" +"""Check downstream migrations before stable publication; not a nightly gate.""" from __future__ import annotations @@ -30,7 +30,7 @@ def require_completed_migrations(path: Path = GATES) -> None: raise ValueError("each release gate needs a pull_request and requirement") if pending: details = "\n".join(f"- {item['pull_request']}: {item['requirement']}" for item in pending) - raise RuntimeError(f"Release publication is blocked by pending downstream migrations:\n{details}") + raise RuntimeError(f"Stable release publication is blocked by pending downstream migrations:\n{details}") def main(argv: list[str] | None = None) -> int: diff --git a/tests/test_ci_workflow_contracts.py b/tests/test_ci_workflow_contracts.py index d8c9bc1ae..ae095562d 100644 --- a/tests/test_ci_workflow_contracts.py +++ b/tests/test_ci_workflow_contracts.py @@ -40,7 +40,7 @@ ACTION_ROOT = REPOSITORY_ROOT / ".github" / "actions" -def test_release_migration_gate_blocks_publication_until_reviewed_clearance(tmp_path): +def test_stable_release_migrations_require_reviewed_clearance(tmp_path): from scripts.check_release_migrations import GATES, require_completed_migrations with pytest.raises(RuntimeError, match="dynamo/pull/14065"): @@ -56,25 +56,29 @@ def test_release_migration_gate_blocks_publication_until_reviewed_clearance(tmp_ with pytest.raises(FileNotFoundError): require_completed_migrations(path) - jobs = _workflow("nightly-ci.yml")["jobs"] + +def test_nightly_can_publish_the_wheel_needed_by_pending_downstream_migrations(): + workflow = _workflow("nightly-ci.yml") + serialized_workflow = json.dumps(workflow) + assert "check_release_migrations.py" not in serialized_workflow + assert "release-gates.json" not in serialized_workflow + jobs = workflow["jobs"] guard = jobs["changes-guard"] - index = next(i for i, step in enumerate(guard["steps"]) if "check_release_migrations.py" in step.get("run", "")) - assert next(i for i, step in enumerate(guard["steps"]) if step.get("id") == "target") < index - assert index < next(i for i, step in enumerate(guard["steps"]) if step.get("id") == "decide") - assert "if" not in guard["steps"][index] - checkouts = [step for step in guard["steps"][:index] if step.get("uses", "").startswith("actions/checkout@")] - assert len(checkouts) == 2 - assert checkouts[0]["with"]["ref"] == "${{ steps.target.outputs.sha }}" - assert "path" not in checkouts[0]["with"] - assert checkouts[1]["with"]["ref"] == "${{ github.sha }}" - assert checkouts[1]["with"]["path"] == "release-policy" - assert all("if" not in step and step["with"]["persist-credentials"] == "false" for step in checkouts) - assert guard["steps"][index]["run"] == ( - "python3 release-policy/scripts/check_release_migrations.py --target-gates .github/release-gates.json" - ) - assert "build-artifacts" in jobs["trigger-gitlab-security"]["needs"] - assert "changes-guard" in jobs["build-artifacts"]["needs"] - assert "needs.changes-guard.outputs.should-build == 'true'" in jobs["build-artifacts"]["if"] + commands = "\n".join(_run_commands(job) for job in jobs.values() if "steps" in job) + assert "check_release_migrations.py" not in commands + assert "release-gates.json" not in commands + assert not any(step.get("uses", "").startswith("actions/checkout@") for step in guard["steps"]) + steps = [step.get("id") for step in guard["steps"]] + assert steps.index("target") < steps.index("version") < steps.index("decide") + assert guard["outputs"]["dev-version"] == "${{ steps.version.outputs.dev-version }}" + build = jobs["build-artifacts"] + assert "scripts/apply_dev_version.py" in _run_commands(build) + assert {"changes-guard", "manual-approval", "python-compliance"} <= set(build["needs"]) + assert "needs.changes-guard.outputs.should-build == 'true'" in build["if"] + publish = jobs["trigger-gitlab-security"] + assert {"build-artifacts", "manual-approval", "fpe-support-matrix", "license-evidence"} <= set(publish["needs"]) + for name in ("build-artifacts", "fpe-support-matrix", "license-evidence"): + assert f"needs.{name}.result == 'success'" in publish["if"] @pytest.mark.parametrize( @@ -87,7 +91,7 @@ def test_release_migration_gate_blocks_publication_until_reviewed_clearance(tmp_ ("clear", "malformed", 1), ], ) -def test_publication_checks_current_policy_and_selected_target(tmp_path, monkeypatch, current, target, expected): +def test_stable_publication_checks_current_policy_and_selected_target(tmp_path, monkeypatch, current, target, expected): from scripts import check_release_migrations as checker paths = {}