From 22a751eae6f6f190ba25ac19ce6bff3d6c271015 Mon Sep 17 00:00:00 2001 From: ooctipus Date: Tue, 22 Sep 2026 15:41:51 -0700 Subject: [PATCH 1/4] Clarify cloning without physics replication Adapt #7129 to the release clone-context API. The retired template cloning and copy_from_source guidance is already absent on this branch. Preserve its current examples and PhysX collision-filtering guidance. (cherry picked from commit 354168560f6fd25a3983126fab05f632d858c5d1) --- docs/source/how-to/cloning.rst | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/source/how-to/cloning.rst b/docs/source/how-to/cloning.rst index c1b462701b48..d31f8b548941 100644 --- a/docs/source/how-to/cloning.rst +++ b/docs/source/how-to/cloning.rst @@ -327,6 +327,11 @@ roots as a global-only plan. This keeps their existing native physics initializa USD runs before native physics contexts so the destination topology exists when they consume it. No fallback context is constructed during dispatch. +Setting ``replicate_physics=False`` skips the active physics replication context. +Other declared contexts still run, including USD replication when configured, so +they can create the per-environment prims without replicating the physics model. +The physics backend can then parse those prims independently. + Collision Filtering ------------------- From c1f3baa49ae503a4bdf59c70a52a411f3f347eb4 Mon Sep 17 00:00:00 2001 From: hujc Date: Sun, 20 Sep 2026 23:47:44 -0700 Subject: [PATCH 2/4] [Fix] Separate SimulationContext construction from lookup (#7722) Separate `SimulationContext` construction from lookup. A second construction previously returned the live singleton and silently discarded its configuration, including a requested device change (NVBug 6677911). - `SimulationContext(cfg)` constructs a context and raises if one already exists, including no-argument construction or re-passing the same cfg. - `SimulationContext.instance()` retrieves the live context. - `SimulationContext.clear_instance()` releases it before constructing a replacement. Removed the singleton-reusing `__new__`, configuration/device comparison helper, and warn-and-ignore path. Native-backend sharing remains the registry's responsibility; this change does not alter it or introduce concurrent simulation contexts. The follow-up removes 49 production lines compared with the previous PR revision. Thirteen existing test suites now release the contexts they construct instead of accidentally borrowing a previous test's context. No production caller migration was needed. **Breaking:** callers using repeated `SimulationContext(...)` as lookup must switch to `SimulationContext.instance()`. To apply a new configuration, tear down with `clear_instance()` before constructing again. The class documentation and major changelog fragment include this migration. - Verified the revised singleton regression test fails on the previous implementation and passes with the fix. - GPU 0 / CPU: `test_simulation_context.py`: **51 passed**; `test_build_simulation_context_headless.py`: **15 passed**, run in separate processes because each launches Kit. - Nine schema/fragment suites: **165 passed**. - Four manager suites: **38 passed**. Total: **269 passed**. - `uv run --no-sync isaaclab -f`: passed, including changelog validation against the PR merge base. - Bug fix - Breaking change with migration guidance - [ ] Backport this pull request to the active release branch after it merges into `develop` - [x] Read the contribution guidelines - [x] Ran formatting and lint checks - [x] Updated public API documentation and migration guidance - [x] Added no new runtime warnings - [x] Verified the regression test fails before the fix and passes afterward - [x] Added a changelog fragment for the changed package - [x] Contributor already listed in `CONTRIBUTORS.md` --------- Co-authored-by: Octi Zhang (cherry picked from commit 4cf439dfeba72acb08699d0f5a02c1a6dcfeb479) --- ...jichuanh-simcontext-device-guard.major.rst | 8 ++++ .../isaaclab/sim/simulation_context.py | 17 +++++---- .../test/managers/test_event_manager.py | 3 +- .../test/managers/test_observation_manager.py | 4 +- .../test/managers/test_reward_manager.py | 3 +- .../test/managers/test_termination_manager.py | 3 +- .../test/sim/test_articulation_fragments.py | 7 ++++ .../test/sim/test_collision_fragments.py | 7 ++++ .../test/sim/test_joint_drive_fragments.py | 7 ++++ .../isaaclab/test/sim/test_mass_fragments.py | 7 ++++ .../test/sim/test_material_fragments.py | 8 ++++ .../test/sim/test_mesh_collision_fragments.py | 7 ++++ .../test/sim/test_schema_fragments.py | 7 ++++ .../sim/test_schema_writer_nested_targets.py | 7 ++++ .../test/sim/test_simulation_context.py | 38 ++++++++++++------- .../test/sim/test_tendon_fragments.py | 7 ++++ 16 files changed, 114 insertions(+), 26 deletions(-) create mode 100644 source/isaaclab/changelog.d/jichuanh-simcontext-device-guard.major.rst diff --git a/source/isaaclab/changelog.d/jichuanh-simcontext-device-guard.major.rst b/source/isaaclab/changelog.d/jichuanh-simcontext-device-guard.major.rst new file mode 100644 index 000000000000..604a385edf66 --- /dev/null +++ b/source/isaaclab/changelog.d/jichuanh-simcontext-device-guard.major.rst @@ -0,0 +1,8 @@ +Changed +^^^^^^^ + +* **Breaking:** Changed :class:`~isaaclab.sim.SimulationContext` to reject construction while a + context already exists, instead of returning it and discarding the requested configuration. + This includes no-argument construction and reusing the same configuration. Use + :meth:`~isaaclab.sim.SimulationContext.instance` to retrieve the live context, or call + :meth:`~isaaclab.sim.SimulationContext.clear_instance` before constructing a replacement. diff --git a/source/isaaclab/isaaclab/sim/simulation_context.py b/source/isaaclab/isaaclab/sim/simulation_context.py index 12219a681512..1f294f61b65a 100644 --- a/source/isaaclab/isaaclab/sim/simulation_context.py +++ b/source/isaaclab/isaaclab/sim/simulation_context.py @@ -95,19 +95,14 @@ class SimulationContext: * Simulation state (play, pause, step, stop) * Rendering and visualization - The singleton instance can be accessed using the ``instance()`` class method. + Use :meth:`instance` to retrieve the live context. Construction always creates a new context + and raises if one already exists; call :meth:`clear_instance` before constructing a replacement. """ # SINGLETON PATTERN _instance: SimulationContext | None = None - def __new__(cls, cfg: SimulationCfg | None = None): - """Enforce singleton pattern.""" - if cls._instance is not None: - return cls._instance - return super().__new__(cls) - @classmethod def instance(cls) -> SimulationContext | None: """Get the singleton instance, or None if not created.""" @@ -118,9 +113,15 @@ def __init__(self, cfg: SimulationCfg | None = None): Args: cfg: Simulation configuration. Defaults to None (uses default config). + + Raises: + RuntimeError: If a simulation context already exists. """ if type(self)._instance is not None: - return # Already initialized + raise RuntimeError( + "A SimulationContext already exists. Use SimulationContext.instance() to retrieve it," + " or call SimulationContext.clear_instance() before constructing a replacement." + ) from pxr import UsdUtils # noqa: PLC0415 diff --git a/source/isaaclab/test/managers/test_event_manager.py b/source/isaaclab/test/managers/test_event_manager.py index cd3cde703a55..762d2bcb4e42 100644 --- a/source/isaaclab/test/managers/test_event_manager.py +++ b/source/isaaclab/test/managers/test_event_manager.py @@ -94,7 +94,8 @@ def env(): # create sim sim = SimulationContext() # create dummy environment - return DummyEnv(num_envs, 0.01, device, sim, dummy1, dummy2) + yield DummyEnv(num_envs, 0.01, device, sim, dummy1, dummy2) + SimulationContext.clear_instance() def test_str(env): diff --git a/source/isaaclab/test/managers/test_observation_manager.py b/source/isaaclab/test/managers/test_observation_manager.py index c839f7b7b473..4cd0a14ea27c 100644 --- a/source/isaaclab/test/managers/test_observation_manager.py +++ b/source/isaaclab/test/managers/test_observation_manager.py @@ -118,7 +118,9 @@ def setup_env(): # let the simulation play (we need this for observation manager to compute obs dims) env.sim._app_control_on_stop_handle = None env.sim.reset() - return env + yield env + sim.stop() + sim_utils.SimulationContext.clear_instance() def test_str(setup_env): diff --git a/source/isaaclab/test/managers/test_reward_manager.py b/source/isaaclab/test/managers/test_reward_manager.py index 57b9fd57f02f..d3cc9fd74187 100644 --- a/source/isaaclab/test/managers/test_reward_manager.py +++ b/source/isaaclab/test/managers/test_reward_manager.py @@ -43,7 +43,8 @@ def grilled_chicken_with_yoghurt(env, hot: bool, bland: float): @pytest.fixture def env(): sim = SimulationContext() - return namedtuple("ManagerBasedRLEnv", ["num_envs", "dt", "device", "sim"])(20, 0.1, "cpu", sim) + yield namedtuple("ManagerBasedRLEnv", ["num_envs", "dt", "device", "sim"])(20, 0.1, "cpu", sim) + SimulationContext.clear_instance() def test_str(env): diff --git a/source/isaaclab/test/managers/test_termination_manager.py b/source/isaaclab/test/managers/test_termination_manager.py index 095a95d69db2..43e066e1b211 100644 --- a/source/isaaclab/test/managers/test_termination_manager.py +++ b/source/isaaclab/test/managers/test_termination_manager.py @@ -52,7 +52,8 @@ def fail_every_3_steps(env) -> torch.Tensor: @pytest.fixture def env(): sim = SimulationContext() - return DummyEnv(num_envs=20, device="cpu", sim=sim) + yield DummyEnv(num_envs=20, device="cpu", sim=sim) + SimulationContext.clear_instance() def test_initial_state_and_shapes(env): diff --git a/source/isaaclab/test/sim/test_articulation_fragments.py b/source/isaaclab/test/sim/test_articulation_fragments.py index 57cfa0f86dbe..3b1f68cdffdc 100644 --- a/source/isaaclab/test/sim/test_articulation_fragments.py +++ b/source/isaaclab/test/sim/test_articulation_fragments.py @@ -22,6 +22,13 @@ from isaaclab.sim import SimulationCfg, SimulationContext +@pytest.fixture(autouse=True) +def cleanup_simulation_context(): + """Release the simulation context after each test.""" + yield + SimulationContext.clear_instance() + + def _make_xform(stage, path="/World/Art"): UsdGeom.Xform.Define(stage, path) return stage.GetPrimAtPath(path) diff --git a/source/isaaclab/test/sim/test_collision_fragments.py b/source/isaaclab/test/sim/test_collision_fragments.py index f5da64c8b255..b4ae604a71c6 100644 --- a/source/isaaclab/test/sim/test_collision_fragments.py +++ b/source/isaaclab/test/sim/test_collision_fragments.py @@ -22,6 +22,13 @@ pytestmark = pytest.mark.integration +@pytest.fixture(autouse=True) +def cleanup_simulation_context(): + """Release the simulation context after each test.""" + yield + SimulationContext.clear_instance() + + def _make_xform(stage, path="/World/Body"): UsdGeom.Xform.Define(stage, path) return stage.GetPrimAtPath(path) diff --git a/source/isaaclab/test/sim/test_joint_drive_fragments.py b/source/isaaclab/test/sim/test_joint_drive_fragments.py index be4c19de6424..2c4069a5b589 100644 --- a/source/isaaclab/test/sim/test_joint_drive_fragments.py +++ b/source/isaaclab/test/sim/test_joint_drive_fragments.py @@ -24,6 +24,13 @@ pytestmark = pytest.mark.integration +@pytest.fixture(autouse=True) +def cleanup_simulation_context(): + """Release the simulation context after each test.""" + yield + SimulationContext.clear_instance() + + def _make_revolute_joint(stage, path="/World/Articulation/joint_0"): UsdGeom.Xform.Define(stage, "/World/Articulation") UsdGeom.Cube.Define(stage, "/World/Articulation/body0") diff --git a/source/isaaclab/test/sim/test_mass_fragments.py b/source/isaaclab/test/sim/test_mass_fragments.py index b57808d501ae..e5effb5f38e4 100644 --- a/source/isaaclab/test/sim/test_mass_fragments.py +++ b/source/isaaclab/test/sim/test_mass_fragments.py @@ -22,6 +22,13 @@ pytestmark = pytest.mark.integration +@pytest.fixture(autouse=True) +def cleanup_simulation_context(): + """Release the simulation context after each test.""" + yield + SimulationContext.clear_instance() + + def _make_xform(stage, path="/World/Body"): UsdGeom.Xform.Define(stage, path) return stage.GetPrimAtPath(path) diff --git a/source/isaaclab/test/sim/test_material_fragments.py b/source/isaaclab/test/sim/test_material_fragments.py index fbea81b5bc91..d08c0d2cf362 100644 --- a/source/isaaclab/test/sim/test_material_fragments.py +++ b/source/isaaclab/test/sim/test_material_fragments.py @@ -21,6 +21,14 @@ pytestmark = pytest.mark.integration + +@pytest.fixture(autouse=True) +def cleanup_simulation_context(): + """Release the simulation context after each test.""" + yield + SimulationContext.clear_instance() + + # ------------------------------------------------------------------------------------- # RigidBodyMaterialFragment marker + metadata # ------------------------------------------------------------------------------------- diff --git a/source/isaaclab/test/sim/test_mesh_collision_fragments.py b/source/isaaclab/test/sim/test_mesh_collision_fragments.py index 0d8af815d02c..4ecbcf8fcdb8 100644 --- a/source/isaaclab/test/sim/test_mesh_collision_fragments.py +++ b/source/isaaclab/test/sim/test_mesh_collision_fragments.py @@ -22,6 +22,13 @@ pytestmark = pytest.mark.integration +@pytest.fixture(autouse=True) +def cleanup_simulation_context(): + """Release the simulation context after each test.""" + yield + SimulationContext.clear_instance() + + def _make_xform(stage, path="/World/Mesh"): UsdGeom.Xform.Define(stage, path) return stage.GetPrimAtPath(path) diff --git a/source/isaaclab/test/sim/test_schema_fragments.py b/source/isaaclab/test/sim/test_schema_fragments.py index 7055976f8c5d..9f2615e3f3e0 100644 --- a/source/isaaclab/test/sim/test_schema_fragments.py +++ b/source/isaaclab/test/sim/test_schema_fragments.py @@ -22,6 +22,13 @@ pytestmark = pytest.mark.integration +@pytest.fixture(autouse=True) +def cleanup_simulation_context(): + """Release the simulation context after each test.""" + yield + SimulationContext.clear_instance() + + def _make_xform(stage, path="/World/Body"): UsdGeom.Xform.Define(stage, path) return stage.GetPrimAtPath(path) diff --git a/source/isaaclab/test/sim/test_schema_writer_nested_targets.py b/source/isaaclab/test/sim/test_schema_writer_nested_targets.py index ca084c45bfed..fc3cb37513b7 100644 --- a/source/isaaclab/test/sim/test_schema_writer_nested_targets.py +++ b/source/isaaclab/test/sim/test_schema_writer_nested_targets.py @@ -30,6 +30,13 @@ """Link prim paths relative to the robot root; ``link2/link3`` is a nested child link.""" +@pytest.fixture(autouse=True) +def cleanup_simulation_context(): + """Release the simulation context after each test.""" + yield + SimulationContext.clear_instance() + + def _author_robot_usd(path: str) -> None: """Author a minimal articulated-robot USD layout. diff --git a/source/isaaclab/test/sim/test_simulation_context.py b/source/isaaclab/test/sim/test_simulation_context.py index 95b663f25578..2617f1078cd8 100644 --- a/source/isaaclab/test/sim/test_simulation_context.py +++ b/source/isaaclab/test/sim/test_simulation_context.py @@ -169,20 +169,30 @@ def test_instance_before_creation(): @pytest.mark.isaacsim_ci def test_singleton(): - """Tests that the singleton is working.""" - sim1 = SimulationContext() - sim2 = SimulationContext() - assert sim1 is sim2 - - # try to delete the singleton - sim2.clear_instance() - assert sim1.instance() is None - # create new instance - sim3 = SimulationContext() - assert sim1 is not sim3 - assert sim1.instance() is sim3.instance() - # clear instance - sim3.clear_instance() + """Construction creates a context; only instance() retrieves the live context.""" + sim = SimulationContext(SimulationCfg(dt=0.01)) + live_device, live_dt = sim.cfg.device, sim.cfg.dt + other_device = "cpu" if live_device.startswith("cuda") else "cuda:0" + for args in ( + (), + (None,), + (sim.cfg,), + (sim.cfg.copy(),), + (sim.cfg.replace(dt=2.0 * live_dt),), + (sim.cfg.replace(device=other_device),), + ): + with pytest.raises(RuntimeError, match=r"SimulationContext\.instance\(\)"): + SimulationContext(*args) + assert SimulationContext.instance() is sim + assert sim.cfg.dt == live_dt + assert sim.cfg.device == sim.device == live_device + + SimulationContext.clear_instance() + assert SimulationContext.instance() is None + replacement = SimulationContext(SimulationCfg(dt=2.0 * live_dt)) + assert replacement is not sim + assert SimulationContext.instance() is replacement + assert replacement.cfg.dt == 2.0 * live_dt """ diff --git a/source/isaaclab/test/sim/test_tendon_fragments.py b/source/isaaclab/test/sim/test_tendon_fragments.py index 0afd370b4c4a..390d186e6f79 100644 --- a/source/isaaclab/test/sim/test_tendon_fragments.py +++ b/source/isaaclab/test/sim/test_tendon_fragments.py @@ -40,6 +40,13 @@ pytestmark = pytest.mark.integration +@pytest.fixture(autouse=True) +def cleanup_simulation_context(): + """Release the simulation context after each test.""" + yield + SimulationContext.clear_instance() + + def _new_sim(): sim_utils.create_new_stage() SimulationContext(SimulationCfg(dt=0.01)) From 5b55caa4b1dc0b73f10649b549e0dd04229769c5 Mon Sep 17 00:00:00 2001 From: Zihan Xu <2024010904024@std.uestc.edu.cn> Date: Tue, 22 Sep 2026 15:44:11 +0800 Subject: [PATCH 3/4] Fix DelayBuffer state mutation on invalid lag (#7893) Fixes #7793 DelayBuffer.set_time_lag currently writes the requested lag into the live tensor before validating the resulting range. If validation raises, the invalid value remains in the buffer and later compute() calls use it. This validates the requested scalar or tensor delays before assigning them to the live tensor. Invalid inputs raise immediately without changing the lag tensor or its cached bounds. Valid updates retain the existing tensor identity, without cloning the full buffer state. - [x] Bug fix (non-breaking change which fixes an existing issue) - [x] I have run the pre-commit checks - [x] I have added regression coverage for the rejected scalar and tensor updates - [x] I have added a changelog fragment Validation: all 7 tests in test_delay_buffer.py passed. The added early-validation regression failed against the previous clone-based implementation and passed with this change. Additional CPU and CUDA checks covered valid full/subset updates, empty selections, rejected inputs, tensor identity, cached bounds, and subsequent delayed output. Full pre-commit checks and git diff --check passed. --------- Signed-off-by: Xu Zihan <2024010904024@std.uestc.edu.cn> Co-authored-by: Octi Zhang (cherry picked from commit 22fcd71bc4c21d361a02c4004cab5d66bde8d68b) --- .../delay-buffer-invalid-state.rst | 5 ++++ .../isaaclab/utils/buffers/delay_buffer.py | 27 +++++++++---------- .../isaaclab/test/utils/test_delay_buffer.py | 25 +++++++++++++++++ 3 files changed, 42 insertions(+), 15 deletions(-) create mode 100644 source/isaaclab/changelog.d/delay-buffer-invalid-state.rst diff --git a/source/isaaclab/changelog.d/delay-buffer-invalid-state.rst b/source/isaaclab/changelog.d/delay-buffer-invalid-state.rst new file mode 100644 index 000000000000..787145138c99 --- /dev/null +++ b/source/isaaclab/changelog.d/delay-buffer-invalid-state.rst @@ -0,0 +1,5 @@ +Fixed +^^^^^ + +* Fixed ``DelayBuffer.set_time_lag`` leaving invalid per-batch delays in the buffer by validating requested + values before assignment, without cloning the live lag tensor. diff --git a/source/isaaclab/isaaclab/utils/buffers/delay_buffer.py b/source/isaaclab/isaaclab/utils/buffers/delay_buffer.py index 56086922613d..c74bab9fb079 100644 --- a/source/isaaclab/isaaclab/utils/buffers/delay_buffer.py +++ b/source/isaaclab/isaaclab/utils/buffers/delay_buffer.py @@ -125,29 +125,26 @@ def set_time_lag(self, time_lag: int | torch.Tensor, batch_ids: Sequence[int] | if batch_ids is None: batch_ids = slice(None) - # parse requested time_lag + # Validate the requested values before changing the live configuration. if isinstance(time_lag, int): - # set the time lags across provided batch indices - self._time_lags[batch_ids] = time_lag + min_time_lag = max_time_lag = time_lag elif isinstance(time_lag, torch.Tensor): # check valid dtype for time_lag: must be int or long if time_lag.dtype not in [torch.int, torch.long]: raise TypeError(f"Invalid dtype for time_lag: {time_lag.dtype}. Expected torch.int or torch.long.") - # set the time lags - self._time_lags[batch_ids] = time_lag.to(device=self.device) + min_time_lag = int(time_lag.min().item()) if time_lag.numel() else 0 + max_time_lag = int(time_lag.max().item()) if time_lag.numel() else 0 else: raise TypeError(f"Invalid type for time_lag: {type(time_lag)}. Expected int or integer tensor.") - # compute the min and max time lag - self._min_time_lag = int(torch.min(self._time_lags).item()) - self._max_time_lag = int(torch.max(self._time_lags).item()) - # check that time_lag is feasible - if self._min_time_lag < 0: - raise ValueError(f"The minimum time lag cannot be negative. Received: {self._min_time_lag}") - if self._max_time_lag > self._history_length: - raise ValueError( - f"The maximum time lag cannot be larger than the history length. Received: {self._max_time_lag}" - ) + if min_time_lag < 0: + raise ValueError(f"The minimum time lag cannot be negative. Received: {min_time_lag}") + if max_time_lag > self._history_length: + raise ValueError(f"The maximum time lag cannot be larger than the history length. Received: {max_time_lag}") + + self._time_lags[batch_ids] = time_lag.to(device=self.device) if isinstance(time_lag, torch.Tensor) else time_lag + self._min_time_lag = int(self._time_lags.min().item()) + self._max_time_lag = int(self._time_lags.max().item()) def reset(self, batch_ids: Sequence[int] | None = None): """Reset the data in the delay buffer at the specified batch indices. diff --git a/source/isaaclab/test/utils/test_delay_buffer.py b/source/isaaclab/test/utils/test_delay_buffer.py index 354281c07a24..203bf4fb15a8 100644 --- a/source/isaaclab/test/utils/test_delay_buffer.py +++ b/source/isaaclab/test/utils/test_delay_buffer.py @@ -93,6 +93,31 @@ def test_random_time_lags(delay_buffer): assert torch.all(error == 0) +@pytest.mark.parametrize( + ("time_lag", "batch_ids"), + [ + (5, [2]), + (-1, [2]), + (torch.tensor([5, 1], dtype=torch.int), [2, 3]), + ], +) +def test_invalid_time_lag_does_not_mutate_state(delay_buffer, time_lag, batch_ids, monkeypatch): + """Reject invalid inputs before copying or changing the live lag configuration.""" + initial_lags = torch.arange(delay_buffer.batch_size, dtype=torch.int) % 5 + delay_buffer.set_time_lag(initial_lags) + expected_lags = delay_buffer.time_lags + expected_values = expected_lags.clone() + monkeypatch.setattr(expected_lags, "clone", lambda: pytest.fail("Validate the requested lag before copying state.")) + + with pytest.raises(ValueError): + delay_buffer.set_time_lag(time_lag, batch_ids) + + assert delay_buffer.time_lags is expected_lags + assert torch.equal(delay_buffer.time_lags, expected_values) + assert delay_buffer.min_time_lag == 0 + assert delay_buffer.max_time_lag == 4 + + def test_compute_result_independent_of_internal_buffer(delay_buffer): """``compute()``'s returned tensor must not alias the internal circular buffer storage. From 1433b614b63c3c7209db8de280d725af0635e212 Mon Sep 17 00:00:00 2001 From: Octi Zhang Date: Wed, 23 Sep 2026 09:52:48 -0700 Subject: [PATCH 4/4] Make v3.0.0-EA the default documentation version (#7947) Adapt the release redirect recipe so local multi-version builds and the deployment workflow share the EA default and reject missing target pages. (cherry picked from commit 6e9ef17f594a3c5ccf2472ba7946e734e53e8bb8) --- .github/workflows/docs.yaml | 15 ++++----------- docs/Makefile | 5 ++++- 2 files changed, 8 insertions(+), 12 deletions(-) diff --git a/.github/workflows/docs.yaml b/.github/workflows/docs.yaml index 09e355ee513f..27f13d198e29 100644 --- a/.github/workflows/docs.yaml +++ b/.github/workflows/docs.yaml @@ -85,6 +85,8 @@ jobs: needs: [doc-build-type] # run on deploy branches to create multi-version docs if: needs.doc-build-type.outputs.trigger-deploy == 'true' + env: + DOCS_DEFAULT_REF: v3.0.0-EA steps: - name: Checkout code @@ -116,18 +118,9 @@ jobs: git for-each-ref --format="%(refname:short)" refs/heads/ | xargs -r git branch -D make multi-docs - - name: Set default docs version + - name: Verify default docs version working-directory: ./docs - env: - DOCS_DEFAULT_REF: v3.0.0-beta2 - run: | - test -n "$DOCS_DEFAULT_REF" - test -f "_build/$DOCS_DEFAULT_REF/index.html" || { - echo "::error::Default docs ref '$DOCS_DEFAULT_REF' was not built" - exit 1 - } - sed "s|url=\./[^\"]*/index.html|url=./$DOCS_DEFAULT_REF/index.html|" \ - _redirect/index.html > _build/index.html + run: grep -Fq "url=./$DOCS_DEFAULT_REF/index.html" _build/index.html - name: Upload GitHub Pages artifact uses: actions/upload-pages-artifact@v4 diff --git a/docs/Makefile b/docs/Makefile index 82f4c708b7f0..96daebd61d5b 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -7,11 +7,14 @@ SPHINXOPTS ?= SPHINXBUILD ?= sphinx-build SOURCEDIR = . BUILDDIR = _build +DOCS_DEFAULT_REF ?= v3.0.0-EA .PHONY: multi-docs multi-docs: @sphinx-multiversion "$(SOURCEDIR)" "$(BUILDDIR)" --jobs=auto $(SPHINXOPTS) - @cp _redirect/index.html $(BUILDDIR)/index.html + @test -f "$(BUILDDIR)/$(DOCS_DEFAULT_REF)/index.html" || \ + (echo "Default docs ref '$(DOCS_DEFAULT_REF)' was not built" && exit 1) + @sed 's|url=\./[^"]*/index.html|url=./$(DOCS_DEFAULT_REF)/index.html|' _redirect/index.html > $(BUILDDIR)/index.html .PHONY: current-docs current-docs: