From 4720699624cd42e9e56568bd2c3c341336e0af19 Mon Sep 17 00:00:00 2001 From: mxj711 Date: Sat, 10 Oct 2026 12:49:15 +0800 Subject: [PATCH] build(release): prepare synchronized PyPI publishing Prepare public metadata for motrix-env-core, motrix-deploy, and motrix-rl. Pin cross-workspace dependencies to the shared workspace version and enforce the contract in tests. Use Commitizen, reviewed release pull requests, and Trusted Publishing to automate package releases. Refs #108 --- .github/PULL_REQUEST_TEMPLATE.md | 2 +- .github/REPOSITORY_SETTINGS.md | 45 +- .github/scripts/validate_release_version.py | 67 +++ .github/workflows/README.md | 111 ++-- .github/workflows/RELEASE.md | 344 ++++++++++++ .github/workflows/branch-policy.yml | 4 +- .github/workflows/codeql.yml | 6 +- .github/workflows/docker-build.yml | 24 +- .github/workflows/pr-checks.yml | 6 +- .github/workflows/pypi-publish.yml | 176 +++++++ .github/workflows/release.yml | 521 +++++++++++++++++++ .github/workflows/tests.yml | 8 +- motrix_deploy/LICENSE | 204 ++++++++ motrix_deploy/pyproject.toml | 30 +- motrix_deploy/src/motrix_deploy/py.typed | 0 motrix_deploy_motrixsim/pyproject.toml | 4 +- motrix_deploy_mujoco/pyproject.toml | 4 +- motrix_deploy_tasks/pyproject.toml | 6 +- motrix_deploy_unitree/pyproject.toml | 4 +- motrix_env_core/LICENSE | 204 ++++++++ motrix_env_core/pyproject.toml | 28 +- motrix_env_core/src/motrix_env_core/py.typed | 0 motrix_env_motrixsim/pyproject.toml | 2 +- motrix_env_mujoco/pyproject.toml | 2 +- motrix_envs/pyproject.toml | 8 +- motrix_rl/LICENSE | 204 ++++++++ motrix_rl/pyproject.toml | 32 +- motrix_rl/src/motrix_rl/py.typed | 0 motrix_rl_builtin/pyproject.toml | 2 +- motrix_rl_rslrl/pyproject.toml | 2 +- motrix_rl_skrl/pyproject.toml | 2 +- motrix_robots/pyproject.toml | 2 +- pyproject.toml | 43 +- tests/test_release_version_validation.py | 39 ++ tests/test_workspace_versions.py | 101 ++++ uv.lock | 125 ++++- 36 files changed, 2235 insertions(+), 127 deletions(-) create mode 100644 .github/scripts/validate_release_version.py create mode 100644 .github/workflows/RELEASE.md create mode 100644 .github/workflows/pypi-publish.yml create mode 100644 .github/workflows/release.yml create mode 100644 motrix_deploy/LICENSE create mode 100644 motrix_deploy/src/motrix_deploy/py.typed create mode 100644 motrix_env_core/LICENSE create mode 100644 motrix_env_core/src/motrix_env_core/py.typed create mode 100644 motrix_rl/LICENSE create mode 100644 motrix_rl/src/motrix_rl/py.typed create mode 100644 tests/test_release_version_validation.py create mode 100644 tests/test_workspace_versions.py diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index bef944b9..7c5d18a3 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -3,7 +3,7 @@ - [ ] This is a normal PR from a fork targeting upstream `main`. - [ ] This is a `hotfix/*` PR from a fork targeting upstream `stable`. - [ ] This is a maintainer release PR from `main` to `stable`. -- [ ] This is a maintainer post-hotfix back-merge from upstream `stable` to +- [ ] This is a maintainer stable-patch back-merge from upstream `stable` to `main`. - [ ] I selected `Motphys/MotrixLab` as the base repository and the correct base branch in GitHub's compare-across-forks UI. diff --git a/.github/REPOSITORY_SETTINGS.md b/.github/REPOSITORY_SETTINGS.md index 0b6b09e5..981cc346 100644 --- a/.github/REPOSITORY_SETTINGS.md +++ b/.github/REPOSITORY_SETTINGS.md @@ -52,8 +52,9 @@ single source of truth. branch protection rules. Protect both long-lived branches. - Require pull requests, at least one maintainer review, and dismissal of stale approvals. -- Require the `Branch policy / validate`, `Tests / pytest`, `PR checks / prek`, - and `CodeQL / analyze` status checks. +- Require the `Branch policy / validate`, `Tests / pytest (fast)`, + `Tests / pytest (numba)`, `PR checks / prek`, and `CodeQL / analyze` status + checks. - Require approval of all review conversations before merge. - Require linear history or squash merging; disable merge commits for ordinary contributor PRs. @@ -63,22 +64,38 @@ single source of truth. `hotfix/*` PR; if a ruleset bypass is unavoidable, record the incident and immediately follow up with the required hotfix PR. +## Temporary release branch rulesets + +- Add branch rulesets targeting `release/**` and `hotfix/*`. +- Block force-pushes and deletion for both patterns. +- Do not require pull requests on the temporary branches themselves. The release + App must push the validated patch bump to `hotfix/*`, and governance remains + at the `hotfix/* → stable` review; `release/**` is governed by its PR into + `main`. +- Restrict direct writes where the ruleset supports actor conditions, but keep + the release App installation allowed for the automated patch bump. +- Require generated `release/v* → main` version PRs to use squash or rebase so + the resulting `main` head remains the detectable `chore(release):` commit. If + that trigger is missed, maintainers can use the documented + `open_release_pr=true` manual fallback. + ## Stable branch and releases - Configure normal pull requests from forks to target `main`. - Require a release pull request from `main` to `stable`. -- Allow `hotfix/*` pull requests to target `stable` directly. +- Allow `hotfix/*` stable patch pull requests to target `stable` directly. - Allow only the upstream maintainer back-merge `stable` → `main` after a - hotfix release; reject `stable` → `main` PRs from forks. -- Require every hotfix to be merged back into `main` after release. + stable patch release; reject `stable` → `main` PRs from forks. +- Require every stable patch release to be merged back into `main` after + release. - Restrict tag creation for `v*` to maintainers and create tags from `stable`. -- Configure the Docker/package publishing workflow to run only for reviewed - tags on `stable`. +- Keep PyPI publishing automated from reviewed `stable` release tags, but keep + Docker publishing as a manual dispatch from `stable`. - Restrict who can create, update, or delete `v*` tags. -- Add a tag ruleset for `v*` that allows creation only by maintainers and - prevents deletion or updates. -- Remember that Git tags do not record their source branch; the Docker - workflow's ancestry check is a second guard, while the tag ruleset and +- Add a tag ruleset for `v*` that allows creation only by maintainers and the + release App, and prevents deletion or updates. +- Remember that Git tags do not record their source branch; the PyPI and Docker + workflow ancestry checks are second guards, while the tag ruleset and maintainer permissions enforce the `stable`-only release process. ## Security and automation @@ -104,6 +121,12 @@ single source of truth. third-party asset audit is complete. - Configure the package/container publishing secrets only in the main repository, never in forks. +- Create a release GitHub App, install it only on this repository with Contents + read/write and Pull requests read/write, and store its client ID/private key as + `RELEASE_APP_CLIENT_ID` / `RELEASE_APP_PRIVATE_KEY`. The workflow exchanges + those credentials for short-lived installation tokens; generated PRs, tags, + and GitHub Releases therefore trigger the downstream workflows and required + checks normally. - Check Git LFS bandwidth/storage usage before publishing large releases. GitHub references: [about rulesets](https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/managing-rulesets/about-rulesets), diff --git a/.github/scripts/validate_release_version.py b/.github/scripts/validate_release_version.py new file mode 100644 index 00000000..b0231170 --- /dev/null +++ b/.github/scripts/validate_release_version.py @@ -0,0 +1,67 @@ +# Copyright Motphys Technology Co., Ltd. 2025, 2026 +# SPDX-License-Identifier: Apache-2.0 + +"""Validate a release target selected by the release workflow.""" + +from argparse import ArgumentParser +from typing import Literal + +from packaging.version import InvalidVersion, Version + +ReleaseKind = Literal["normal", "stable-patch"] + + +def parse_version(value: str, label: str) -> Version: + try: + return Version(value) + except InvalidVersion as error: + message = f"{label} version {value!r} is not a valid PEP 440 version" + raise ValueError(message) from error + + +def validate(current_value: str, target_value: str, release_kind: ReleaseKind) -> Version: + current = parse_version(current_value, "current") + target = parse_version(target_value, "target") + + if current.local is not None or current.dev is not None: + message = f"current release version must not be a dev or local version: {current}" + raise ValueError(message) + if target.local is not None or target.dev is not None: + message = f"target release version must not be a dev or local version: {target}" + raise ValueError(message) + if target <= current: + message = f"target version {target} must be newer than current version {current}" + raise ValueError(message) + + if release_kind == "stable-patch": + release = current.release + if len(release) != 3: + message = f"stable branch patch releases require a three-part current version: {current}" + raise ValueError(message) + expected = Version(f"{release[0]}.{release[1]}.{release[2] + 1}") + if target != expected or target.pre is not None or target.post is not None: + message = ( + "stable branch patch releases must use the exact next final patch version: " + f"expected {expected}, got {target}" + ) + raise ValueError(message) + + return target + + +def main() -> None: + parser = ArgumentParser(description=__doc__) + parser.add_argument("--current", required=True) + parser.add_argument("--target", required=True) + parser.add_argument("--release-kind", choices=("normal", "stable-patch"), required=True) + arguments = parser.parse_args() + + try: + target = validate(arguments.current, arguments.target, arguments.release_kind) + except ValueError as error: + parser.exit(2, f"error: {error}\n") + print(target) + + +if __name__ == "__main__": + main() diff --git a/.github/workflows/README.md b/.github/workflows/README.md index cdbb0de1..1a953755 100644 --- a/.github/workflows/README.md +++ b/.github/workflows/README.md @@ -89,36 +89,27 @@ scheduled scan. Review results under the repository's **Security** tab. ### File: `branch-policy.yml` Checks pull request metadata without checking out contributor code. Normal PRs -must target `main`; a post-hotfix back-merge may use upstream `stable → main`. -Only a release PR from the upstream `main` branch or a `hotfix/*` PR may target -`stable`. Add `Branch policy / validate` as a required status check on both +must target `main`; a post-patch back-merge may use upstream `stable → main`. +Only a release PR from the upstream `main` branch or a `hotfix/*` stable patch +PR may target `stable`. Add `Branch policy / validate` as a required status check on both protected branches. ## Docker Image Build Workflow ### File: `docker-build.yml` -Automatically builds and pushes Docker images to Docker Hub when a new version -tag pointing to the `stable` release branch is pushed. +Manually builds and pushes Docker images to Docker Hub after a release. #### Trigger Conditions -The workflow is triggered when you push a git tag that matches the pattern `v*`. -The job then verifies that the tagged commit is reachable from `stable`: - -```bash -git switch stable -git pull --ff-only upstream stable -git tag -a v0.3.0 -m "Release v0.3.0" -git push upstream v0.3.0 -``` - -Tags whose commit is not reachable from `stable` are rejected by the -verification step and are not published. Git tags do not retain the branch on -which they were created, so the tag ruleset and maintainer-only permissions -remain the authoritative control for requiring that tags are created from +The workflow is triggered only by manual dispatch. Run it from the `stable` +branch after the release workflow has created the matching `v*` tag and GitHub +Release. The job verifies that the dispatched commit is reachable from `stable`. +Commits that are not reachable from `stable` are rejected by the verification +step and are not published. + #### What It Does 1. **Extracts Version**: Reads the version from `pyproject.toml` (currently `0.3.0`) @@ -145,21 +136,10 @@ To add secrets: #### Usage Example -```bash -# 1. Update version in pyproject.toml if needed -# 2. Commit your changes -git add . -git commit -m "Release v0.3.0" - -# 3. From the upstream stable commit, create and push an annotated version tag -git switch stable -git pull --ff-only upstream stable -git tag -a v0.3.0 -m "Release v0.3.0" -git push upstream v0.3.0 - -# 4. The workflow will automatically build and push the Docker image -# 5. Monitor the build at: https://github.com/Motphys/MotrixLab/actions -``` +1. Complete the release pipeline and wait for the GitHub Release. +2. Open **Actions → Build and Push Docker Image → Run workflow**. +3. Select the `stable` branch and run the workflow. +4. Monitor the run at . #### Built Image Tags @@ -182,7 +162,7 @@ docker pull motphys/motrixlab:v0.3 - ✅ **Multi-tag Support**: Automatically tags with version, major.minor, and latest - ✅ **Version Extraction**: Automatically reads version from pyproject.toml - ✅ **Docker Layer Caching**: Uses UV cache mounts for faster dependency installation -- ✅ **Tag-based Trigger**: Only builds on version tags, not on every commit +- ✅ **Manual Trigger**: Does not run automatically when a release tag is pushed #### Docker Image Contents @@ -217,14 +197,67 @@ docker run --gpus all motphys/motrixlab:test scripts/view.py env=cartpole - Ensure `pyproject.toml` has a valid `version = "x.y.z"` line - Check the workflow logs for the exact extraction command output -**Tag not triggering the workflow:** +**Workflow rejecting the dispatched commit:** -- Ensure the tag starts with `v` (e.g., `v0.3.0`, not `0.3.0`) -- Confirm that the tag points to a commit on `stable`, then push it from the - upstream repository (for example, `git push upstream v0.3.0`) +- Dispatch the workflow from the upstream `stable` branch after the release + pull request has been merged. +- Confirm that the selected commit is reachable from `stable`; the ancestry + check intentionally rejects manual runs from unreleased `main` commits. #### See Also - [Docker Hub Repository](https://hub.docker.com/r/motphys/motrixlab) - [Container Deployment Documentation](../../docs/source/zh_CN/user_guide/getting_started/container_deployment.md) - [Dockerfile](../../docker/Dockerfile) + +# Python Package Publishing + +### File: `pypi-publish.yml` + +Publishes the `motrix-env-core`, `motrix-deploy`, and `motrix-rl` distributions +(the dependency closure of the public packages) to a Python package index. + +- **Release trigger**: publishing a GitHub Release whose `v`-prefixed tag matches the + workspace package version uploads all three packages to pypi.org. +- **Manual trigger**: `workflow_dispatch` uploads to TestPyPI by default, or to + pypi.org with `index=pypi`. Use a TestPyPI run to verify the full install + closure before creating the production release. A manual formal-PyPI run has + the same stable ancestry, exact-tag, and workspace-version guards as a Release + event. + +For production publication, the selected commit must be an ancestor of `stable` +and the matching immutable tag must point exactly at that commit. Every workspace +package must carry the same version before any distribution is built. A dedicated +build job uploads one wheel per package as a workflow artifact, and one publishing +job per package then uploads them in dependency order (env-core → deploy → rl). +Existing immutable distributions are skipped during recovery, so a partial +failure can be retried without replacing an already-published package. + +Authentication uses PyPI Trusted Publishing (OIDC) — no API tokens are stored +in repository secrets. Before the first run, a maintainer must register a +pending publisher for each of the three project names on both pypi.org and +test.pypi.org, pointing at `Motphys/MotrixLab` with the workflow file +`pypi-publish.yml` and the matching job environment (`pypi-env-core`, +`pypi-deploy`, or `pypi-rl`). The distinct environments are required because +PyPI binds at most one project to a repository/workflow/environment +combination. + +# Release Pipeline + +### File: `release.yml` + +Automates version bumps, release pull requests, stable tags, and GitHub +Releases while preserving human review at both governance points: the version +bump merge into `main` and the release merge into `stable`. + +See the dedicated [Release Workflow Guide](RELEASE.md) for: + +- one-time setup, +- normal release instructions, +- stable branch patch release instructions, +- version selection rules, +- release App setup, +- TestPyPI validation, +- package publishing order, +- automatic stable-patch back-merge, +- failure recovery. diff --git a/.github/workflows/RELEASE.md b/.github/workflows/RELEASE.md new file mode 100644 index 00000000..99a8fb79 --- /dev/null +++ b/.github/workflows/RELEASE.md @@ -0,0 +1,344 @@ +# Release Workflow Guide + +This guide explains how maintainers use the release pipeline to select versions, +review release changes, create tags and GitHub Releases, and publish Python +wheels. Docker publishing remains a separate manual operation. + +## Scope + +| Workflow | Purpose | Trigger | +| --- | --- | --- | +| `release.yml` | Bump workspace versions, open release PRs, tag `stable`, create GitHub Releases, and open stable-patch back-merge PRs | Manual dispatch and pushes to `main`/`stable` | +| `pypi-publish.yml` | Build and publish wheels for `motrix-env-core`, `motrix-deploy`, and `motrix-rl` | GitHub Release publication or manual dispatch | +| `docker-build.yml` | Build and push the Docker image | Manual dispatch from `stable` | + +**Stable branch patch release** is the preferred name for a patch cut from +`stable`. Its branch prefix remains `hotfix/*`; the prefix is a Git convention, +not a separate version class. + +## One-time setup + +1. Create the protected `stable` branch from a reviewed `main` commit. +2. Create a GitHub App for release automation and install it only on + `Motphys/MotrixLab`. Grant the installation: + - **Contents: read and write** + - **Pull requests: read and write** +3. Store the App client ID as the `RELEASE_APP_CLIENT_ID` repository variable + and its private key as the `RELEASE_APP_PRIVATE_KEY` secret. +4. Register PyPI Trusted Publishers for all three publishable projects on both + pypi.org and test.pypi.org: + - `motrix-env-core` with the environment `pypi-env-core` + - `motrix-deploy` with the environment `pypi-deploy` + - `motrix-rl` with the environment `pypi-rl` + + Use repository `Motphys/MotrixLab` and workflow `pypi-publish.yml`. The + distinct GitHub environments are required because PyPI binds at most one + project to a repository/workflow/environment combination. +5. For Docker publishing, configure `DOCKER_USERNAME` and `DOCKER_PASSWORD`. + +The workflows exchange the App credentials for a short-lived installation +token. Besides avoiding a long-lived personal access token, this is required +because resources created with a job's `GITHUB_TOKEN` do not trigger downstream +workflows. The App token makes generated PRs run normal required checks and +makes tag/GitHub Release creation trigger package publishing. + +## Inputs and version rules + +Run `release.yml` on `main` for a normal release or on `hotfix/*` for a stable +branch patch release. + +| Input | Meaning | +| --- | --- | +| `mode` | `auto`, `patch`, `minor`, `major`, `beta`, or `stable`; defaults to `auto` | +| `version` | Exact PEP 440 version; when non-empty, it overrides `mode` | +| `open_release_pr` | Manual fallback that opens the current `main → stable` release PR without creating a version bump | + +Version selection priority is: + +```text +explicit version > mode > Conventional Commits +``` + +### Normal releases + +With `mode=auto`, Commitizen examines commits after the most recent `v*` tag: + +| Commit type | Bump | +| --- | --- | +| `fix`, `refactor`, `perf` | PATCH | +| `feat` | MINOR | +| Breaking change | MINOR while the project is `0.x` | +| `docs`, `chore`, `test`, `ci`, `build`, `style` | No bump | + +`--major-version-zero` is added dynamically only while the current major +version is zero. It keeps automatic breaking changes below `1.0.0` during +initial development and is omitted automatically once the project reaches +`1.0.0`. + +Manual modes have the same meanings as before: + +| Mode | Result | +| --- | --- | +| `patch` | `X.Y.Z → X.Y.Z+1` | +| `minor` | `X.Y.Z → X.Y+1.0` | +| `major` | `X.Y.Z → X+1.0.0` | +| `beta` | Increment the beta number, such as `0.4.0b0 → 0.4.0b1` | +| `stable` | Remove a pending pre-release suffix, such as `0.4.0b0 → 0.4.0` | + +Pre-release versions may be published to formal PyPI. They are still marked as +GitHub pre-releases when their version contains a PEP 440 beta segment. + +Every explicit normal-release target must: + +- parse as a PEP 440 version, +- be strictly newer than the current version, +- contain no development or local version segment. + +Pre-release versions such as `0.5.0b0` are valid explicit targets. + +### Stable branch patch releases + +A `hotfix/*` branch must have `origin/stable` as its merge base and must be +current with the stable tip. If `stable` advances, merge `origin/stable` into +the patch branch or recreate the branch before dispatching the workflow. + +Stable branch patch releases are hard-limited to PATCH increments: + +- without `version`, only `mode=auto` and `mode=patch` are accepted, and both + resolve as PATCH; +- with `version`, the target must be the exact next **final** patch version, + for example `0.4.0 → 0.4.1`; +- beta, dev, post-release, local, minor, and major targets are rejected. + +If a patch branch already contains a release bump, that bump must be its latest +commit. Re-running preparation then validates the existing exact patch version +and is otherwise a no-op. + +### Synchronized files and guards + +Before opening a reviewed PR or pushing a patch bump, the workflow: + +1. validates the selected version against the current version; +2. verifies that `vX.Y.Z` does not already exist; +3. runs Commitizen once as a dry run and once to modify files; +4. updates the root and every workspace member `pyproject.toml`; +5. updates internal `motrix-*` dependencies to the same exact pin; +6. runs `uv lock`; +7. verifies every workspace package has the selected version; +8. runs `tests/test_workspace_versions.py`. + +The dry-run result must equal the version written to the files. The stable tag +job repeats the all-workspace version checks and the version-contract test +before it creates a Git tag or GitHub Release. + +## Normal release + +```mermaid +flowchart TD + A[Dispatch Release on main] --> B[Commitizen computes vX] + B --> C[Create release/vX branch] + C --> D[PR: release/vX to main] + D --> E[Human review] + E --> F[Merge with squash or rebase] + F --> G[Open Release PR: main to stable] + G --> H[Human review] + H --> I[Validate all workspace versions] + I --> J[Create vX tag and GitHub Release] + J --> K[Publish Python wheels] + K --> L[Optional manual Docker dispatch] +``` + +### Steps + +1. Confirm that checks on `main` are green. +2. Open **Actions → Release → Run workflow**, select `main`, and choose a mode + or exact version: + + ```bash + gh workflow run release.yml --ref main --field mode=auto + ``` + +3. Wait for **Prepare version bump pull request** to finish. +4. Review the generated `release/vX.Y.Z → main` PR. +5. Merge that PR with **squash or rebase** and preserve a head-commit subject + beginning with: + + ```text + chore(release): bump version to vX.Y.Z + ``` + + Do not use an ordinary merge commit: the push trigger recognizes the release + bump by the new `main` head subject. + +6. Review and merge the generated **Release vX.Y.Z** PR from `main` to + `stable`. +7. The stable push validates all workspace versions and release contracts, + creates the tag and GitHub Release, and starts wheel publishing. +8. Optionally run **Build and Push Docker Image** from `stable`. + +If the automatic `main → stable` PR was not created—for example, because the +version-bump PR was merged with an unexpected strategy—run `release.yml` on +`main` with `open_release_pr=true`: + +```bash +gh workflow run release.yml \ + --ref main \ + --field open_release_pr=true +``` + +## Stable branch patch release + +```mermaid +flowchart TD + A[stable] --> B[Create current hotfix/* branch] + B --> C[Commit fixes] + C --> D[Dispatch Release as patch] + D --> E[Validate exact next patch version] + E --> F[Push chore(release) to hotfix/*] + F --> G[PR: hotfix/* to stable] + G --> H[Human review] + H --> I[Create vX.Y.Z+1 tag and GitHub Release] + I --> J[Publish Python wheels] + I --> K[Open PR: stable to main] + K --> L[Human back-merge review] +``` + +### Steps + +1. Create the patch branch from the current upstream `stable` tip: + + ```bash + git fetch origin + git switch -c hotfix/fix-description origin/stable + ``` + +2. Commit the fix with a conventional commit, preferably: + + ```text + fix: describe the corrected behavior + ``` + +3. Push the branch: + + ```bash + git push -u origin HEAD + ``` + +4. Run the release workflow on that branch: + + ```bash + branch="$(git branch --show-current)" + gh workflow run release.yml --ref "$branch" --field mode=patch + ``` + +5. The workflow pushes `chore(release): bump version to vX.Y.Z+1` to the patch + branch and opens the `hotfix/* → stable` PR. +6. Review and merge that PR into `stable`. +7. Wait for tag creation, GitHub Release publication, and Python publishing. +8. Review and merge the automatically created `stable → main` back-merge PR. + +Do not replace the final back-merge with a direct `hotfix/* → main` PR. The +release tag points at the reviewed merge result on `stable`; merging +`stable → main` carries that exact released state and version history back to +development. + +The back-merge PR is opened after the tag job succeeds. Package publishing is +event-driven and runs asynchronously, so verify that all publishing jobs and the +published artifacts are healthy before merging it. + +## Python wheel publishing + +Publication is automatic when a GitHub Release is published. The build job +verifies that: + +- the release commit is reachable from `origin/stable`; +- `v` plus the workspace version exists and points exactly at the selected + commit; +- the release tag matches the workspace version; +- every workspace package has the same version. + +Only wheels are built. The three public distributions are published in +dependency order: + +```text +motrix-env-core → motrix-deploy → motrix-rl +``` + +Publishing uses `skip-existing: true`: if one package job succeeds and a later +job fails, re-running the failed path skips immutable distributions that are +already present and publishes only the missing package. It never replaces a +previously uploaded version. + +### TestPyPI validation + +Before a production release, manually run `pypi-publish.yml` on a reviewed +commit and select `index=testpypi`: + +```bash +gh workflow run pypi-publish.yml \ + --ref main \ + --field index=testpypi +``` + +TestPyPI validation is intentionally more permissive so pre-release commits can +be exercised before a stable tag exists. + +The `index=pypi` manual option is retained for recovery, but it performs the +same production checks as a Release event: the selected commit must be reachable +from `stable`, and the matching immutable `v*` tag must point exactly at that +commit. Normal production publishing should still occur through the reviewed +GitHub Release event. + +## Automation and human gates + +| Stage | Automation | +| --- | --- | +| Version calculation and validation | Automatic after manual dispatch | +| Normal version bump PR | Automatic | +| Merge version bump into `main` | Human review | +| Open `main → stable` release PR | Automatic, with manual fallback | +| Merge release into `stable` | Human review | +| Workspace release checks | Automatic before tagging | +| Tag and GitHub Release | Automatic | +| Python wheel publishing | Automatic | +| Stable patch `stable → main` PR | Automatic | +| Stable patch back-merge review | Human | +| Docker image publishing | Manual dispatch | + +## Failure recovery + +| Symptom | Action | +| --- | --- | +| `vX.Y.Z` already exists | Select a new version; Git tags and PyPI uploads are immutable | +| `release/vX.Y.Z` already exists | Inspect its open PR first. Close and delete it only if the version was never tagged or published | +| Commitizen finds no bumpable commits | Use an explicit mode or exact version | +| Explicit version is rejected | Use a strictly newer PEP 440 version without dev or local segments | +| Stable patch version is rejected | Use the exact next final patch version and update the branch from `stable` | +| Workspace versions or internal pins diverge | Fix the `pyproject.toml` files and regenerate `uv.lock` | +| Release App token cannot be created | Check `RELEASE_APP_CLIENT_ID`, `RELEASE_APP_PRIVATE_KEY`, installation scope, and permissions | +| Tag exists at the same stable commit but the GitHub Release is missing | Re-run the stable release job; tag creation is idempotent | +| Tag exists at a different commit | Add a new version bump and merge it to `stable`; never move the tag | +| Only part of PyPI publication succeeded | Re-run the failed publishing jobs; unchanged existing distributions are skipped | +| `main` push did not open a release PR | Use the `open_release_pr=true` manual fallback and enforce squash/rebase for future release PRs | +| Back-merge PR was not created | Confirm the stable job completed and the stable head is associated with a merged `hotfix/* → stable` PR | + +## Operational checklist + +Before merging anything into `stable`: + +- [ ] Required PR checks, tests, and CodeQL are green. +- [ ] The release PR contains the intended version bump and lockfile changes. +- [ ] Internal `motrix-*` dependencies are exact pins to the workspace version. +- [ ] TestPyPI validation has succeeded for a nontrivial release. +- [ ] PyPI Trusted Publishers are configured. +- [ ] The release GitHub App is installed with Contents and Pull requests + write permissions. + +After publication: + +- [ ] Verify the GitHub Release and immutable tag. +- [ ] Verify all three Python publishing jobs succeeded. +- [ ] Install or inspect the published wheels. +- [ ] Run Docker publishing manually if this release needs a new image. +- [ ] For a stable branch patch release, review and merge the automatic + `stable → main` back-merge PR. diff --git a/.github/workflows/branch-policy.yml b/.github/workflows/branch-policy.yml index 3f12dca3..da3963f1 100644 --- a/.github/workflows/branch-policy.yml +++ b/.github/workflows/branch-policy.yml @@ -22,11 +22,11 @@ jobs: case "$BASE_BRANCH" in main) if [[ "$HEAD_BRANCH" == hotfix/* ]]; then - echo "Hotfix branches must target stable first; back-merge stable into main after release." + echo "Stable patch branches must target stable first; back-merge stable into main after release." exit 1 fi if [[ "$HEAD_BRANCH" == stable && "$HEAD_REPOSITORY" != "Motphys/MotrixLab" ]]; then - echo "The post-hotfix stable-to-main back-merge must come from the upstream repository." + echo "The stable-patch back-merge must come from the upstream repository." exit 1 fi ;; diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index c6d3afea..7a28c743 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -17,12 +17,12 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Initialize CodeQL - uses: github/codeql-action/init@6f5948dfacef28e207b48d0905cf90c03365536d # v3 + uses: github/codeql-action/init@24c54180a607b1449ed407dd24f251e4e9147c8d # v4.38.3 with: languages: python - name: Perform CodeQL analysis - uses: github/codeql-action/analyze@6f5948dfacef28e207b48d0905cf90c03365536d # v3 + uses: github/codeql-action/analyze@24c54180a607b1449ed407dd24f251e4e9147c8d # v4.38.3 diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml index bffdfe1f..2216e1f5 100644 --- a/.github/workflows/docker-build.yml +++ b/.github/workflows/docker-build.yml @@ -1,9 +1,7 @@ name: Build and Push Docker Image on: - push: - tags: - - "v*" + workflow_dispatch: env: DOCKER_IMAGE: motphys/motrixlab @@ -44,7 +42,7 @@ jobs: df -h - name: Checkout repository - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: fetch-depth: 0 @@ -57,31 +55,35 @@ jobs: id: version run: | VERSION=$(grep '^version' pyproject.toml | head -1 | sed 's/version = "\(.*\)"/\1/' | tr -d '" ') - echo "version=${VERSION}" >> $GITHUB_OUTPUT + MAJOR_MINOR="${VERSION%%.*}.${VERSION#*.}" + MAJOR_MINOR="${MAJOR_MINOR%.*}" + echo "version=${VERSION}" >> "$GITHUB_OUTPUT" + echo "major_minor=${MAJOR_MINOR}" >> "$GITHUB_OUTPUT" echo "Extracted version: ${VERSION}" + echo "Extracted major.minor version: ${MAJOR_MINOR}" - name: Set up Docker Buildx - uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3 + uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0 - name: Log in to Docker Hub - uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3 + uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0 with: username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_PASSWORD }} - name: Extract metadata for Docker id: meta - uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5 + uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5.10.0 with: images: ${{ env.DOCKER_IMAGE }} tags: | - type=semver,pattern={{version}},prefix=v - type=semver,pattern={{major}}.{{minor}},prefix=v + type=raw,value=v${{ steps.version.outputs.version }} + type=raw,value=v${{ steps.version.outputs.major_minor }} type=raw,value=${{ steps.version.outputs.version }} type=raw,value=latest - name: Build and push Docker image - uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6 + uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2 with: context: . file: ${{ env.DOCKERFILE_PATH }}/Dockerfile diff --git a/.github/workflows/pr-checks.yml b/.github/workflows/pr-checks.yml index 5e806ef8..89c1ddbc 100644 --- a/.github/workflows/pr-checks.yml +++ b/.github/workflows/pr-checks.yml @@ -16,16 +16,16 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: fetch-depth: 0 - name: Setup uv - uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1 + uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0 with: version: "0.9.14" - name: Run prek - uses: j178/prek-action@4e14d07f9231acabce116ccfca13b13dd9755ece # v3.0.0 + uses: j178/prek-action@1382ad72dea72f9d53832f2bcb65b3e87a947204 # v3.0.1 with: prek-version: "0.5.2" diff --git a/.github/workflows/pypi-publish.yml b/.github/workflows/pypi-publish.yml new file mode 100644 index 00000000..5b3cf1d3 --- /dev/null +++ b/.github/workflows/pypi-publish.yml @@ -0,0 +1,176 @@ +name: Publish Python Packages + +on: + release: + types: [published] + workflow_dispatch: + inputs: + index: + description: Target package index for manual runs + type: choice + options: [testpypi, pypi] + default: testpypi + +permissions: {} + +env: + PYPI_URL: https://upload.pypi.org/legacy/ + TESTPYPI_URL: https://test.pypi.org/legacy/ + +jobs: + build: + name: Build release distributions + # Trusted Publishing credentials are bound to this repository; never + # build or publish package distributions from a fork. + if: github.repository == 'Motphys/MotrixLab' + runs-on: ubuntu-latest + timeout-minutes: 15 + permissions: + contents: read + steps: + - name: Checkout repository + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + # Required to verify that the release commit is on stable. + fetch-depth: 0 + + - name: Verify the production release commit is on stable + if: > + github.event_name == 'release' || + (github.event_name == 'workflow_dispatch' && inputs.index == 'pypi') + env: + EVENT_NAME: ${{ github.event_name }} + run: | + git fetch origin stable --tags + if ! git merge-base --is-ancestor "$GITHUB_SHA" origin/stable; then + echo "Production publishing requires $GITHUB_SHA to be reachable from origin/stable." >&2 + exit 1 + fi + + version=$(sed -n 's/^version = "\(.*\)"$/\1/p' pyproject.toml | head -1) + tag="v$version" + if ! tagged=$(git rev-parse -q --verify "refs/tags/$tag^{commit}" 2>/dev/null); then + echo "Production publishing requires tag $tag to exist." >&2 + exit 1 + fi + if [ "$tagged" != "$GITHUB_SHA" ]; then + echo "Tag $tag points at $tagged, but the selected production commit is $GITHUB_SHA." >&2 + exit 1 + fi + if [ "$EVENT_NAME" = "release" ]; then + echo "Release event and stable tag checks passed for $tag." + else + echo "Manual production recovery checks passed for $tag." + fi + + - name: Verify package versions + env: + EVENT_NAME: ${{ github.event_name }} + REF_NAME: ${{ github.ref_name }} + run: | + versions=$( + for f in pyproject.toml */pyproject.toml; do + sed -n 's/^version = "\(.*\)"$/\1/p' "$f" | head -1 + done | sort -u + ) + if [ "$(printf '%s\n' "$versions" | wc -l)" != 1 ] || [ -z "$versions" ]; then + echo "Workspace package versions diverged: $versions" >&2 + exit 1 + fi + core=$(printf '%s\n' "$versions" | head -1) + if [ "$EVENT_NAME" = "release" ] && [ "v$core" != "$REF_NAME" ]; then + echo "Release tag '$REF_NAME' does not match package version 'v$core'." >&2 + exit 1 + fi + + - name: Set up uv + uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0 + with: + version: "0.9.14" + + - name: Set up Python + run: uv python install 3.10 + + - name: Build distribution wheels + run: | + uv build --wheel motrix_env_core --out-dir dist/env-core + uv build --wheel motrix_deploy --out-dir dist/deploy + uv build --wheel motrix_rl --out-dir dist/rl + + - name: Upload distribution packages + uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 + with: + name: release-dists + path: dist/ + if-no-files-found: error + + publish-env-core: + name: Publish motrix-env-core + needs: build + if: github.repository == 'Motphys/MotrixLab' + runs-on: ubuntu-latest + # Distinct GitHub environments keep the three Trusted Publisher registrations + # unique: PyPI binds at most one project to a repository/workflow/environment. + environment: pypi-env-core + timeout-minutes: 10 + permissions: + contents: read + id-token: write # PyPI Trusted Publishing OIDC token. + steps: + - name: Download distribution packages + uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2 + with: + name: release-dists + path: dist/ + - name: Publish to the target index + uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2 + with: + packages-dir: dist/env-core + repository-url: ${{ (github.event_name == 'release' || inputs.index == 'pypi') && env.PYPI_URL || env.TESTPYPI_URL }} + skip-existing: true + + publish-deploy: + name: Publish motrix-deploy + needs: publish-env-core + if: github.repository == 'Motphys/MotrixLab' + runs-on: ubuntu-latest + environment: pypi-deploy + timeout-minutes: 10 + permissions: + contents: read + id-token: write # PyPI Trusted Publishing OIDC token. + steps: + - name: Download distribution packages + uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2 + with: + name: release-dists + path: dist/ + - name: Publish to the target index + uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2 + with: + packages-dir: dist/deploy + repository-url: ${{ (github.event_name == 'release' || inputs.index == 'pypi') && env.PYPI_URL || env.TESTPYPI_URL }} + skip-existing: true + + publish-rl: + name: Publish motrix-rl + needs: publish-deploy + if: github.repository == 'Motphys/MotrixLab' + runs-on: ubuntu-latest + environment: pypi-rl + timeout-minutes: 10 + permissions: + contents: read + id-token: write # PyPI Trusted Publishing OIDC token. + steps: + - name: Download distribution packages + uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2 + with: + name: release-dists + path: dist/ + - name: Publish to the target index + uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2 + with: + packages-dir: dist/rl + repository-url: ${{ (github.event_name == 'release' || inputs.index == 'pypi') && env.PYPI_URL || env.TESTPYPI_URL }} + skip-existing: true diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 00000000..e03c0b53 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,521 @@ +name: Release + +on: + workflow_dispatch: + inputs: + mode: + description: Version selection for the release + type: choice + options: [auto, patch, minor, major, beta, stable] + default: auto + version: + description: Explicit version (overrides mode); e.g. 0.5.0b0 + type: string + open_release_pr: + description: Open the current main-to-stable release PR without preparing a bump + type: boolean + default: false + push: + branches: [main, stable] + +permissions: {} + +concurrency: + group: release-automation + cancel-in-progress: false + +jobs: + prepare: + name: Prepare version bump pull request + # The job commits to temporary release branches and opens reviewed pull + # requests; it must never run anywhere else. + if: > + github.repository == 'Motphys/MotrixLab' && + github.event_name == 'workflow_dispatch' && + inputs.open_release_pr != true + runs-on: ubuntu-latest + timeout-minutes: 15 + permissions: + contents: write + pull-requests: write + steps: + - name: Create release app token + id: app-token + uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 + with: + client-id: ${{ vars.RELEASE_APP_CLIENT_ID }} + private-key: ${{ secrets.RELEASE_APP_PRIVATE_KEY }} + permission-contents: write + permission-pull-requests: write + + - name: Troubleshoot app token failure + if: failure() + run: | + cat >&2 <<'EOF' + ❌ FAILED: Could not create release app token + + Troubleshooting checklist: + 1. Verify RELEASE_APP_CLIENT_ID is set in repo variables + Settings → Variables and secrets → Repository variables + 2. Verify RELEASE_APP_PRIVATE_KEY is set in repo secrets + Settings → Secrets and variables → Repository secrets + 3. Verify the GitHub App is installed on this repository + Settings → GitHub Apps → Installed GitHub Apps + 4. Verify the App has the following permissions: + - Contents: read & write + - Pull requests: read & write + + For setup details, see: .github/workflows/RELEASE.md + EOF + exit 1 + + - name: Checkout repository + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + # Commitizen derives the next version from the full history and the + # most recent v-prefixed release tag. Full history is required. + fetch-depth: 0 + token: ${{ steps.app-token.outputs.token }} + + - name: Resolve the release branch + env: + REF_NAME: ${{ github.ref_name }} + run: | + case "$REF_NAME" in + main) + echo "BASE=main" >> "$GITHUB_ENV" + echo "PATCH_RELEASE=false" >> "$GITHUB_ENV" + echo "ALREADY_PREPARED=false" >> "$GITHUB_ENV" + ;; + hotfix/*) + # A stable branch patch must be based on the current stable tip. + # Requiring merge-base equality permits fix commits and a merge + # from updated stable, but rejects a stale patch branch. + git fetch origin stable --no-tags + stable_sha=$(git rev-parse origin/stable) + base_sha=$(git merge-base HEAD origin/stable) + if [ "$base_sha" != "$stable_sha" ]; then + cat >&2 <&2 + exit 1 + fi + if [ "$release_commits" -eq 1 ]; then + if ! git log -1 --format=%s | grep -q '^chore(release): bump version to v'; then + echo "The release bump on $REF_NAME must be its latest commit." >&2 + echo "Add a new patch branch and bump instead of modifying a prepared release." >&2 + exit 1 + fi + echo "ALREADY_PREPARED=true" >> "$GITHUB_ENV" + else + echo "ALREADY_PREPARED=false" >> "$GITHUB_ENV" + fi + + echo "BASE=stable" >> "$GITHUB_ENV" + echo "PATCH_RELEASE=true" >> "$GITHUB_ENV" + ;; + *) + echo "Release preparation must run on main or a hotfix/* branch, got '$REF_NAME'." >&2 + exit 1 + ;; + esac + + - name: Set up uv + uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0 + with: + version: "0.9.14" + + - name: Set up Python + run: uv python install 3.10 + + - name: Select and validate the release version + env: + MODE: ${{ inputs.mode }} + EXPLICIT_VERSION: ${{ inputs.version }} + PATCH_RELEASE: ${{ env.PATCH_RELEASE }} + ALREADY_PREPARED: ${{ env.ALREADY_PREPARED }} + run: | + uv sync --frozen --only-group dev --no-install-project --no-install-workspace + + if [ "$PATCH_RELEASE" = true ]; then + CURRENT_VERSION=$(git show origin/stable:pyproject.toml | + sed -n 's/^version = "\(.*\)"$/\1/p' | head -1) + else + CURRENT_VERSION=$(.venv/bin/cz version -p) + fi + if [ -z "$CURRENT_VERSION" ]; then + echo "Unable to resolve the current version." >&2 + exit 1 + fi + + args=(--yes --version-files-only --check-consistency) + if [ "$ALREADY_PREPARED" = true ]; then + prepared_version=$(.venv/bin/cz version -p) + VERSION=$(.venv/bin/python .github/scripts/validate_release_version.py \ + --current "$CURRENT_VERSION" \ + --target "$prepared_version" \ + --release-kind stable-patch) + elif [ -n "$EXPLICIT_VERSION" ]; then + VERSION=$(.venv/bin/python .github/scripts/validate_release_version.py \ + --current "$CURRENT_VERSION" \ + --target "$EXPLICIT_VERSION" \ + --release-kind "$([ "$PATCH_RELEASE" = true ] && echo stable-patch || echo normal)") + args+=("$VERSION") + else + if [ "$PATCH_RELEASE" = true ]; then + case "$MODE" in + auto|patch) MODE="patch" ;; + *) + echo "Stable branch patch releases only accept mode=auto or mode=patch." >&2 + exit 1 + ;; + esac + fi + + current_major="${CURRENT_VERSION%%.*}" + if [ "$current_major" = 0 ]; then + # Keep breaking changes below 1.0 while the project is in initial + # development. Commitizen does not allow this option to be + # combined with an explicit target version. + args+=(--major-version-zero) + fi + case "$MODE" in + auto) ;; + patch) args+=(--increment PATCH) ;; + minor) args+=(--increment MINOR) ;; + major) args+=(--increment MAJOR) ;; + beta) args+=(--prerelease beta) ;; + stable) + # Commitizen's linear prerelease behavior stabilizes a pending + # release, for example 0.4.0b0 → 0.4.0. + args+=(--increment PATCH) + ;; + *) + echo "Unsupported release mode: $MODE" >&2 + exit 1 + ;; + esac + + candidate=$(.venv/bin/cz bump --dry-run --get-next "${args[@]}") + if [ -z "$candidate" ]; then + echo "Commitizen did not produce a version." >&2 + exit 1 + fi + VERSION=$(.venv/bin/python .github/scripts/validate_release_version.py \ + --current "$CURRENT_VERSION" \ + --target "$candidate" \ + --release-kind "$([ "$PATCH_RELEASE" = true ] && echo stable-patch || echo normal)") + fi + + if git ls-remote --exit-code --tags origin "refs/tags/v$VERSION" >/dev/null 2>&1; then + echo "Tag v$VERSION already exists." >&2 + exit 1 + fi + echo "VERSION=$VERSION" >> "$GITHUB_ENV" + + if [ "$ALREADY_PREPARED" != true ]; then + .venv/bin/cz bump "${args[@]}" + uv lock + + bumped=$(.venv/bin/cz version -p) + if [ "$bumped" != "$VERSION" ]; then + echo "Commitizen predicted v$VERSION but bumped v$bumped." >&2 + exit 1 + fi + fi + + - name: Verify workspace version consistency + run: | + versions=$( + { + sed -n 's/^version = "\(.*\)"$/\1/p' pyproject.toml | head -1 + for f in */pyproject.toml; do + sed -n 's/^version = "\(.*\)"$/\1/p' "$f" | head -1 + done + } | sort -u + ) + if [ "$(printf '%s\n' "$versions" | wc -l)" != 1 ] || [ -z "$versions" ]; then + echo "Workspace package versions diverged: $versions" >&2 + exit 1 + fi + version=$(printf '%s\n' "$versions" | head -1) + if [ "$version" != "$VERSION" ]; then + echo "Workspace version $version does not match the selected version $VERSION." >&2 + exit 1 + fi + + - name: Verify workspace dependency pins + run: .venv/bin/python -m pytest tests/test_workspace_versions.py + + - name: Publish the version bump + if: env.ALREADY_PREPARED != 'true' + env: + GH_TOKEN: ${{ steps.app-token.outputs.token }} + REF_NAME: ${{ github.ref_name }} + BASE: ${{ env.BASE }} + run: | + git config user.name "Motrix Release Bot" + git config user.email "noreply@motphys.com" + git add pyproject.toml ./*/pyproject.toml uv.lock + git commit -m "chore(release): bump version to v$VERSION" + if [ "$BASE" = main ]; then + branch="release/v$VERSION" + if git ls-remote --exit-code --heads origin "$branch" >/dev/null 2>&1; then + echo "Branch $branch already exists; inspect its pull request before retrying." >&2 + exit 1 + fi + git checkout -b "$branch" + git push origin "$branch" + gh pr create \ + --base main \ + --head "$branch" \ + --title "chore(release): bump version to v$VERSION" \ + --body "Bumps every workspace package and \`uv.lock\` to **v$VERSION**. + + Merge this pull request with squash or rebase, then merge the automatically created \`main → stable\` release pull request to cut the release." + else + git push origin "HEAD:$REF_NAME" + if [ -z "$(gh pr list --head "$REF_NAME" --base stable --state open --json number --jq '.[].number')" ]; then + gh pr create \ + --base stable \ + --head "$REF_NAME" \ + --title "chore(release): bump version to v$VERSION" \ + --body "Stable branch patch release **v$VERSION**: fixes plus the workspace version bump." + fi + fi + + open-release-pr: + name: Open main to stable release pull request + if: > + github.repository == 'Motphys/MotrixLab' && ( + ( + github.event_name == 'push' && + github.ref == 'refs/heads/main' && + startsWith(github.event.head_commit.message, 'chore(release):') + ) || ( + github.event_name == 'workflow_dispatch' && + github.ref_name == 'main' && + inputs.open_release_pr + ) + ) + runs-on: ubuntu-latest + timeout-minutes: 5 + permissions: + contents: read + pull-requests: write + steps: + - name: Create release app token + id: app-token + uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 + with: + client-id: ${{ vars.RELEASE_APP_CLIENT_ID }} + private-key: ${{ secrets.RELEASE_APP_PRIVATE_KEY }} + permission-contents: read + permission-pull-requests: write + + - name: Checkout repository + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + # Full history and tags are required to test whether the version + # has already been released. + fetch-depth: 0 + token: ${{ steps.app-token.outputs.token }} + + - name: Open or refresh the release pull request + env: + GH_TOKEN: ${{ steps.app-token.outputs.token }} + run: | + VERSION=$(sed -n 's/^version = "\(.*\)"$/\1/p' pyproject.toml | head -1) + if [ -z "$VERSION" ]; then + echo "No version found in the root pyproject.toml." >&2 + exit 1 + fi + if git rev-parse -q --verify "refs/tags/v$VERSION" >/dev/null 2>&1; then + echo "v$VERSION has already been tagged; nothing to release." + exit 0 + fi + + title="Release v$VERSION" + for number in $(gh pr list --base stable --head main --state open --json number --jq '.[].number'); do + gh pr close "$number" --comment "Superseded by $title." + done + gh pr create \ + --base stable \ + --head main \ + --title "$title" \ + --body "Merging this pull request publishes **v$VERSION**: the release workflow tags the reviewed commit, creates the GitHub release, and triggers PyPI publishing. Run the Docker workflow manually from **stable** after this release is complete. + + Review the changelog since the previous release before merging." + + tag-release: + name: Tag the stable release commit + if: > + github.repository == 'Motphys/MotrixLab' && + github.event_name == 'push' && + github.ref == 'refs/heads/stable' + runs-on: ubuntu-latest + timeout-minutes: 15 + permissions: + contents: write + steps: + - name: Create release app token + id: app-token + uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 + with: + client-id: ${{ vars.RELEASE_APP_CLIENT_ID }} + private-key: ${{ secrets.RELEASE_APP_PRIVATE_KEY }} + permission-contents: write + + - name: Checkout repository + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + fetch-depth: 0 + token: ${{ steps.app-token.outputs.token }} + + - name: Verify all workspace package versions + run: | + VERSION=$(sed -n 's/^version = "\(.*\)"$/\1/p' pyproject.toml | head -1) + if [ -z "$VERSION" ]; then + echo "No version found in the root pyproject.toml." >&2 + exit 1 + fi + versions=$( + for f in pyproject.toml */pyproject.toml; do + sed -n 's/^version = "\(.*\)"$/\1/p' "$f" | head -1 + done | sort -u + ) + if [ "$(printf '%s\n' "$versions" | wc -l)" != 1 ] || [ -z "$versions" ]; then + echo "Workspace package versions diverged: $versions" >&2 + exit 1 + fi + if [ "$(printf '%s\n' "$versions" | head -1)" != "$VERSION" ]; then + echo "Workspace packages do not match the root version $VERSION." >&2 + exit 1 + fi + echo "VERSION=$VERSION" >> "$GITHUB_ENV" + + - name: Set up uv + uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0 + with: + version: "0.9.14" + + - name: Set up Python + run: uv python install 3.10 + + - name: Verify workspace release contracts + run: | + uv sync --frozen --only-group dev --no-install-project --no-install-workspace + .venv/bin/python -m pytest tests/test_workspace_versions.py + + - name: Create the release tag + env: + GH_TOKEN: ${{ steps.app-token.outputs.token }} + run: | + tag="v$VERSION" + if git rev-parse -q --verify "refs/tags/$tag^{commit}" >/dev/null 2>&1; then + existing_sha=$(git rev-parse "refs/tags/$tag^{commit}") + if [ "$existing_sha" = "$GITHUB_SHA" ]; then + echo "$tag already exists at this commit; nothing to do." + exit 0 + else + echo "Tag $tag exists at $existing_sha, but the current commit is $GITHUB_SHA." >&2 + echo "A stable patch or release merge reached stable without a version bump." >&2 + exit 1 + fi + fi + + gh auth setup-git + git config user.name "Motrix Release Bot" + git config user.email "noreply@motphys.com" + git tag "$tag" "$GITHUB_SHA" + git push origin "refs/tags/$tag" + + - name: Create the GitHub release + env: + GH_TOKEN: ${{ steps.app-token.outputs.token }} + run: | + tag="v$VERSION" + if gh release view "$tag" >/dev/null 2>&1; then + echo "Release $tag already exists; skipping creation." + exit 0 + fi + args=(--target "$GITHUB_SHA" --title "$tag" --generate-notes) + case "$VERSION" in + *b[0-9]*) args+=(--prerelease) ;; + esac + gh release create "$tag" "${args[@]}" + + backmerge-stable-patch: + name: Open the stable patch back-merge pull request + needs: tag-release + if: > + github.repository == 'Motphys/MotrixLab' && + github.event_name == 'push' && + github.ref == 'refs/heads/stable' + runs-on: ubuntu-latest + timeout-minutes: 5 + permissions: + contents: read + pull-requests: write + steps: + - name: Create release app token + id: app-token + uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 + with: + client-id: ${{ vars.RELEASE_APP_CLIENT_ID }} + private-key: ${{ secrets.RELEASE_APP_PRIVATE_KEY }} + permission-contents: read + permission-pull-requests: write + + - name: Checkout repository + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + fetch-depth: 0 + token: ${{ steps.app-token.outputs.token }} + + - name: Open the back-merge pull request + env: + GH_TOKEN: ${{ steps.app-token.outputs.token }} + run: | + is_patch_release=$( + gh api "repos/${GITHUB_REPOSITORY}/commits/${GITHUB_SHA}/pulls" \ + --jq 'map(select(.base.ref == "stable" and (.head.ref | startswith("hotfix/")))) | length > 0' + ) + if [ "$is_patch_release" != true ]; then + echo "This stable release was not merged from hotfix/*; no back-merge is needed." + exit 0 + fi + + VERSION=$(sed -n 's/^version = "\(.*\)"$/\1/p' pyproject.toml | head -1) + tag="v$VERSION" + tagged=$(git rev-parse -q --verify "refs/tags/$tag^{commit}" 2>/dev/null || true) + if [ -z "$tagged" ] || [ "$tagged" != "$GITHUB_SHA" ]; then + echo "Refusing to open the back-merge before $tag points at $GITHUB_SHA." >&2 + exit 1 + fi + + existing=$(gh pr list --base main --head stable --state open --json number --jq '.[].number') + if [ -n "$existing" ]; then + echo "Stable-to-main pull request #$existing already exists." + exit 0 + fi + gh pr create \ + --base main \ + --head stable \ + --title "Back-merge stable v$VERSION to main" \ + --body "Synchronizes the released stable patch and version bump to main. + + This pull request was opened automatically after **v$VERSION** was tagged on stable." diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 9705065e..5a14ec56 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -36,7 +36,7 @@ jobs: # via $HOME here; the cache action below reads the same variable. echo "NUMBA_CACHE_DIR=$HOME/.cache/motrixlab/numba-manager" >> "$GITHUB_ENV" - name: Restore numba kernel cache - uses: actions/cache@v4 + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: ~/.cache/motrixlab key: numba-kernel-${{ runner.os }}-${{ matrix.suite }}-${{ github.sha }} @@ -44,12 +44,12 @@ jobs: numba-kernel-${{ runner.os }}-${{ matrix.suite }}- numba-kernel-${{ runner.os }}- - name: Checkout repository - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: lfs: true - name: Set up uv - uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5.4.2 + uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0 with: version: "0.9.14" enable-cache: true @@ -70,7 +70,7 @@ jobs: - name: Upload test results if: always() - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 with: name: pytest-${{ matrix.suite }} path: test-results/ diff --git a/motrix_deploy/LICENSE b/motrix_deploy/LICENSE new file mode 100644 index 00000000..34bead94 --- /dev/null +++ b/motrix_deploy/LICENSE @@ -0,0 +1,204 @@ +Copyright Motphys Technology Co., Ltd. 2025, 2026 + + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + +Copyright [yyyy] [name of copyright owner] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/motrix_deploy/pyproject.toml b/motrix_deploy/pyproject.toml index 0eadbb75..45abe52f 100644 --- a/motrix_deploy/pyproject.toml +++ b/motrix_deploy/pyproject.toml @@ -7,14 +7,34 @@ name = "motrix-deploy" version = "0.4.0b0" description = "Framework-independent policy deployment runtime for MotrixLab." authors = [{ name = "Motphys", email = "developers@motphys.com" }] -requires-python = "==3.10.*" +requires-python = ">=3.10,<3.14" readme = "README.md" license = "Apache-2.0" +license-files = ["LICENSE"] +keywords = [ + "reinforcement-learning", + "robotics", + "deployment", + "onnx", +] +classifiers = [ + "Development Status :: 4 - Beta", + "Intended Audience :: Developers", + "Intended Audience :: Science/Research", + "Operating System :: OS Independent", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Topic :: Scientific/Engineering :: Artificial Intelligence", + "Typing :: Typed", +] # onnxruntime is a core dependency: deployed policies are ONNX artifacts, so # every deployment runtime needs it for inference. dependencies = [ "hydra-core>=1.3,<1.4", - "motrix-env-core", + "motrix-env-core==0.4.0b0", "numpy>=1.26", "onnxruntime==1.23.2", "pydantic===2.12.5", @@ -22,3 +42,9 @@ dependencies = [ [project.scripts] motrix-deploy = "motrix_deploy.cli:main" + +[project.urls] +Homepage = "https://github.com/Motphys/MotrixLab" +Repository = "https://github.com/Motphys/MotrixLab" +Issues = "https://github.com/Motphys/MotrixLab/issues" +Changelog = "https://github.com/Motphys/MotrixLab/releases" diff --git a/motrix_deploy/src/motrix_deploy/py.typed b/motrix_deploy/src/motrix_deploy/py.typed new file mode 100644 index 00000000..e69de29b diff --git a/motrix_deploy_motrixsim/pyproject.toml b/motrix_deploy_motrixsim/pyproject.toml index 1e006bc1..72f192f5 100644 --- a/motrix_deploy_motrixsim/pyproject.toml +++ b/motrix_deploy_motrixsim/pyproject.toml @@ -11,8 +11,8 @@ requires-python = "==3.10.*" readme = "README.md" license = "Apache-2.0" dependencies = [ - "motrix-deploy", - "motrix-env-motrixsim", + "motrix-deploy==0.4.0b0", + "motrix-env-motrixsim==0.4.0b0", "numpy>=1.26", ] diff --git a/motrix_deploy_mujoco/pyproject.toml b/motrix_deploy_mujoco/pyproject.toml index 28e79efa..fc95700e 100644 --- a/motrix_deploy_mujoco/pyproject.toml +++ b/motrix_deploy_mujoco/pyproject.toml @@ -11,8 +11,8 @@ requires-python = "==3.10.*" readme = "README.md" license = "Apache-2.0" dependencies = [ - "motrix-deploy", - "motrix-env-mujoco", + "motrix-deploy==0.4.0b0", + "motrix-env-mujoco==0.4.0b0", "hydra-core>=1.3", "numpy>=1.26", ] diff --git a/motrix_deploy_tasks/pyproject.toml b/motrix_deploy_tasks/pyproject.toml index 8285dba6..79101f2b 100644 --- a/motrix_deploy_tasks/pyproject.toml +++ b/motrix_deploy_tasks/pyproject.toml @@ -11,9 +11,9 @@ requires-python = "==3.10.*" readme = "README.md" license = "Apache-2.0" dependencies = [ - "motrix-deploy", - "motrix-env-core", - "motrix-robots", + "motrix-deploy==0.4.0b0", + "motrix-env-core==0.4.0b0", + "motrix-robots==0.4.0b0", "hydra-core>=1.3,<1.4", "numpy>=1.26", ] diff --git a/motrix_deploy_unitree/pyproject.toml b/motrix_deploy_unitree/pyproject.toml index 253c8870..aa661037 100644 --- a/motrix_deploy_unitree/pyproject.toml +++ b/motrix_deploy_unitree/pyproject.toml @@ -11,9 +11,9 @@ requires-python = "==3.10.*" readme = "README.md" license = "Apache-2.0" dependencies = [ + "motrix-deploy==0.4.0b0", + "motrix-env-core==0.4.0b0", "hydra-core>=1.3,<1.4", - "motrix-deploy", - "motrix-env-core", "numpy>=1.26", "unitree-sdk2py", ] diff --git a/motrix_env_core/LICENSE b/motrix_env_core/LICENSE new file mode 100644 index 00000000..34bead94 --- /dev/null +++ b/motrix_env_core/LICENSE @@ -0,0 +1,204 @@ +Copyright Motphys Technology Co., Ltd. 2025, 2026 + + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + +Copyright [yyyy] [name of copyright owner] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/motrix_env_core/pyproject.toml b/motrix_env_core/pyproject.toml index 47391952..787e271f 100644 --- a/motrix_env_core/pyproject.toml +++ b/motrix_env_core/pyproject.toml @@ -7,9 +7,29 @@ name = "motrix-env-core" version = "0.4.0b0" description = "Backend-neutral framework APIs and runtime facilities for building reinforcement learning environments." authors = [{ name = "Motphys", email = "developers@motphys.com" }] -requires-python = "==3.10.*" +requires-python = ">=3.10,<3.14" readme = "README.md" license = "Apache-2.0" +license-files = ["LICENSE"] +keywords = [ + "reinforcement-learning", + "robotics", + "simulation", + "environment", +] +classifiers = [ + "Development Status :: 4 - Beta", + "Intended Audience :: Developers", + "Intended Audience :: Science/Research", + "Operating System :: OS Independent", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Topic :: Scientific/Engineering :: Artificial Intelligence", + "Typing :: Typed", +] # Heavy runtime deps (numba/scipy for manager kernel compilation, imageio for # video recording) are optional extras so deployment-side installs # (motrix-deploy and its backend plugins) only pull the lightweight config, @@ -46,3 +66,9 @@ all = [ # short kernels. The wheel drops libtbb.so.12 into /lib, so the runtime # preloads it by full path (see motrix_env_core.numba.threading). tbb = ["tbb==2021.13.0"] + +[project.urls] +Homepage = "https://github.com/Motphys/MotrixLab" +Repository = "https://github.com/Motphys/MotrixLab" +Issues = "https://github.com/Motphys/MotrixLab/issues" +Changelog = "https://github.com/Motphys/MotrixLab/releases" diff --git a/motrix_env_core/src/motrix_env_core/py.typed b/motrix_env_core/src/motrix_env_core/py.typed new file mode 100644 index 00000000..e69de29b diff --git a/motrix_env_motrixsim/pyproject.toml b/motrix_env_motrixsim/pyproject.toml index 51b41b5a..50059c81 100644 --- a/motrix_env_motrixsim/pyproject.toml +++ b/motrix_env_motrixsim/pyproject.toml @@ -11,7 +11,7 @@ requires-python = "==3.10.*" readme = "README.md" license = "Apache-2.0" dependencies = [ - "motrix-env-core", + "motrix-env-core==0.4.0b0", "motrixsim==0.10.1", "numpy>=1.26", ] diff --git a/motrix_env_mujoco/pyproject.toml b/motrix_env_mujoco/pyproject.toml index 7d91718d..b698eff8 100644 --- a/motrix_env_mujoco/pyproject.toml +++ b/motrix_env_mujoco/pyproject.toml @@ -11,7 +11,7 @@ requires-python = "==3.10.*" readme = "README.md" license = "Apache-2.0" dependencies = [ - "motrix-env-core", + "motrix-env-core==0.4.0b0", "mujoco==3.9.0", "numpy>=1.26", ] diff --git a/motrix_envs/pyproject.toml b/motrix_envs/pyproject.toml index 55ad3b86..1ebc438e 100644 --- a/motrix_envs/pyproject.toml +++ b/motrix_envs/pyproject.toml @@ -12,12 +12,12 @@ readme = "README.md" license = "Apache-2.0" dependencies = [ "gymnasium==1.1.1", - "motrix-env-core[numba,video]", - "motrix-env-motrixsim", - "motrix-robots", + "motrix-env-core[numba,video]==0.4.0b0", + "motrix-env-motrixsim==0.4.0b0", + "motrix-robots==0.4.0b0", "numpy>=1.26", "omegaconf>=2.3,<2.4", ] [project.optional-dependencies] -deploy = ["motrix-deploy", "motrix-deploy-tasks"] +deploy = ["motrix-deploy==0.4.0b0", "motrix-deploy-tasks==0.4.0b0"] diff --git a/motrix_rl/LICENSE b/motrix_rl/LICENSE new file mode 100644 index 00000000..34bead94 --- /dev/null +++ b/motrix_rl/LICENSE @@ -0,0 +1,204 @@ +Copyright Motphys Technology Co., Ltd. 2025, 2026 + + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + +Copyright [yyyy] [name of copyright owner] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/motrix_rl/pyproject.toml b/motrix_rl/pyproject.toml index 309655f0..a2c8b9fb 100644 --- a/motrix_rl/pyproject.toml +++ b/motrix_rl/pyproject.toml @@ -7,16 +7,34 @@ name = "motrix-rl" version = "0.4.0b0" description = "Reinforcement learning training framework with multi-backend unified training interface." authors = [{ name = "Motphys", email = "developers@motphys.com" }] -requires-python = "==3.10.*" +requires-python = ">=3.10,<3.14" readme = "README.md" license = "Apache-2.0" +license-files = ["LICENSE"] +keywords = [ + "reinforcement-learning", + "robotics", + "training", +] +classifiers = [ + "Development Status :: 4 - Beta", + "Intended Audience :: Developers", + "Intended Audience :: Science/Research", + "Operating System :: OS Independent", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Topic :: Scientific/Engineering :: Artificial Intelligence", + "Typing :: Typed", +] dependencies = [ "gymnasium==1.1.1", - "python-abc>=0.2.0", - "motrix-env-core[numba,video]", + "motrix-env-core[numba,video]==0.4.0b0", "rich>=13", "hydra-core>=1.3,<1.4", - "motrix-deploy", + "motrix-deploy==0.4.0b0", "numpy>=1.26", "omegaconf>=2.3,<2.4", "nvidia-ml-py>=13.610.43", @@ -28,3 +46,9 @@ dependencies = [ # extras (see the root pyproject.toml tool.uv.sources). "torch>=2.7,<3", ] + +[project.urls] +Homepage = "https://github.com/Motphys/MotrixLab" +Repository = "https://github.com/Motphys/MotrixLab" +Issues = "https://github.com/Motphys/MotrixLab/issues" +Changelog = "https://github.com/Motphys/MotrixLab/releases" diff --git a/motrix_rl/src/motrix_rl/py.typed b/motrix_rl/src/motrix_rl/py.typed new file mode 100644 index 00000000..e69de29b diff --git a/motrix_rl_builtin/pyproject.toml b/motrix_rl_builtin/pyproject.toml index 5041e9ac..8b03febf 100644 --- a/motrix_rl_builtin/pyproject.toml +++ b/motrix_rl_builtin/pyproject.toml @@ -11,7 +11,7 @@ requires-python = "==3.10.*" readme = "README.md" license = "Apache-2.0" dependencies = [ - "motrix-rl>=0.4.0b0", + "motrix-rl==0.4.0b0", "gymnasium==1.1.1", "numpy>=1.26", "torch>=2.7,<3", diff --git a/motrix_rl_rslrl/pyproject.toml b/motrix_rl_rslrl/pyproject.toml index 17f8042d..2752cdad 100644 --- a/motrix_rl_rslrl/pyproject.toml +++ b/motrix_rl_rslrl/pyproject.toml @@ -11,7 +11,7 @@ requires-python = "==3.10.*" readme = "README.md" license = "Apache-2.0" dependencies = [ - "motrix-rl>=0.4.0b0", + "motrix-rl==0.4.0b0", "torch>=2.7,<3", "rsl-rl-lib>=4.0,<5.0", ] diff --git a/motrix_rl_skrl/pyproject.toml b/motrix_rl_skrl/pyproject.toml index 6f4690e3..07894cc5 100644 --- a/motrix_rl_skrl/pyproject.toml +++ b/motrix_rl_skrl/pyproject.toml @@ -11,7 +11,7 @@ requires-python = "==3.10.*" readme = "README.md" license = "Apache-2.0" dependencies = [ - "motrix-rl>=0.4.0b0", + "motrix-rl==0.4.0b0", "skrl>=2.1,<2.2", "torch>=2.7,<3", ] diff --git a/motrix_robots/pyproject.toml b/motrix_robots/pyproject.toml index 75e76db4..3e3d1cad 100644 --- a/motrix_robots/pyproject.toml +++ b/motrix_robots/pyproject.toml @@ -10,7 +10,7 @@ authors = [{ name = "Motphys", email = "developers@motphys.com" }] requires-python = "==3.10.*" readme = "README.md" license = "Apache-2.0" -dependencies = ["motrix-env-core"] +dependencies = ["motrix-env-core==0.4.0b0"] [project.entry-points."motrix_env_core.robots"] builtin = "motrix_robots.registration:register" diff --git a/pyproject.toml b/pyproject.toml index 588d4c24..ee7d9cd1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,25 +3,25 @@ name = "motrix-lab" version = "0.4.0b0" description = "A general-purpose machine learning architecture designed for robot training" authors = [{ name = "Motphys", email = "developers@motphys.com" }] -requires-python = "==3.10.*" +requires-python = ">=3.10,<3.14" dependencies = [ - "motrix-deploy", - "motrix-deploy-mujoco", - "motrix-deploy-motrixsim", - "motrix-deploy-tasks", - "motrix-env-motrixsim", - "motrix-env-mujoco", - "motrix-envs", - "motrix-rl", - "motrix-rl-builtin", - "motrix-rl-skrl", + "motrix-deploy==0.4.0b0", + "motrix-deploy-mujoco==0.4.0b0", + "motrix-deploy-motrixsim==0.4.0b0", + "motrix-deploy-tasks==0.4.0b0", + "motrix-env-motrixsim==0.4.0b0", + "motrix-env-mujoco==0.4.0b0", + "motrix-envs==0.4.0b0", + "motrix-rl==0.4.0b0", + "motrix-rl-builtin==0.4.0b0", + "motrix-rl-skrl==0.4.0b0", ] [project.optional-dependencies] -rslrl = ["motrix-rl-rslrl"] -skrl-jax = ["motrix-rl-skrl[jax]"] +rslrl = ["motrix-rl-rslrl==0.4.0b0"] +skrl-jax = ["motrix-rl-skrl[jax]==0.4.0b0"] unitree = [ - "motrix-deploy-unitree", + "motrix-deploy-unitree==0.4.0b0", ] docs = [ "sphinx", @@ -50,7 +50,7 @@ cuda = [ # machines (see motrix-env-core's tbb extra); optional, combines with any # GPU profile. tbb = [ - "motrix-env-core[tbb]", + "motrix-env-core[tbb]==0.4.0b0", ] # ROCm wheels only exist for Linux x86_64; the markers keep other platforms # from resolving these requirements against the default (PyPI) index. @@ -62,6 +62,7 @@ rocm = [ [dependency-groups] dev = [ + "commitizen>=4,<5", "dprint-py>=0.57.0.0", "mypy>=1.15,<2", "pytest>=8.3.3,<8.4.0", @@ -70,7 +71,7 @@ dev = [ test = [ { include-group = "dev" }, "hydra-core>=1.3,<1.4", - "motrix-deploy-unitree", + "motrix-deploy-unitree==0.4.0b0", ] [tool.uv.workspace] @@ -151,6 +152,16 @@ triton-rocm = [ { index = "pytorch-rocm72", marker = "sys_platform == 'linux' and platform_machine == 'x86_64'" }, ] +[tool.commitizen] +name = "cz_conventional_commits" +version_provider = "uv" +version_scheme = "pep440" +tag_format = "v$version" +version_files = [ + 'pyproject.toml:"motrix-[a-z0-9-]+(?:\[[^"\]]+\])?==', + '*/pyproject.toml:^version = |"motrix-[a-z0-9-]+(?:\[[^"\]]+\])?==', +] + [tool.pytest.ini_options] markers = [ "slow: tests with substantial compilation or runtime cost", diff --git a/tests/test_release_version_validation.py b/tests/test_release_version_validation.py new file mode 100644 index 00000000..c7b2bee8 --- /dev/null +++ b/tests/test_release_version_validation.py @@ -0,0 +1,39 @@ +# Copyright Motphys Technology Co., Ltd. 2025, 2026 +# SPDX-License-Identifier: Apache-2.0 + +"""Release workflow version-selection contracts.""" + +from importlib.util import module_from_spec, spec_from_file_location +from pathlib import Path + +import pytest + +SCRIPT = Path(__file__).resolve().parents[1] / ".github/scripts/validate_release_version.py" +SPEC = spec_from_file_location("validate_release_version", SCRIPT) +assert SPEC is not None and SPEC.loader is not None +validate_release_version = module_from_spec(SPEC) +SPEC.loader.exec_module(validate_release_version) + + +def test_normal_release_accepts_strictly_newer_prerelease() -> None: + assert str(validate_release_version.validate("0.4.0", "0.5.0b0", "normal")) == "0.5.0b0" + + +def test_stable_patch_requires_exact_next_final_patch() -> None: + assert str(validate_release_version.validate("0.4.0", "0.4.1", "stable-patch")) == "0.4.1" + + +@pytest.mark.parametrize( + ("current", "target", "release_kind"), + [ + ("0.4.0", "0.4.0", "normal"), + ("0.4.0", "0.4.0.dev1", "normal"), + ("0.4.0", "0.5.0+local", "normal"), + ("0.4.0", "0.4.0b0", "stable-patch"), + ("0.4.0", "0.4.2", "stable-patch"), + ("0.4.0", "0.5.0", "stable-patch"), + ], +) +def test_invalid_release_targets_are_rejected(current: str, target: str, release_kind: str) -> None: + with pytest.raises(ValueError): + validate_release_version.validate(current, target, release_kind) diff --git a/tests/test_workspace_versions.py b/tests/test_workspace_versions.py new file mode 100644 index 00000000..627601bd --- /dev/null +++ b/tests/test_workspace_versions.py @@ -0,0 +1,101 @@ +# Copyright Motphys Technology Co., Ltd. 2025, 2026 +# SPDX-License-Identifier: Apache-2.0 + +"""Workspace release version contracts.""" + +import re +from pathlib import Path + +from packaging.requirements import Requirement +from packaging.utils import canonicalize_name + +ROOT = Path(__file__).resolve().parents[1] + + +def _project_field(path: Path, field: str) -> str: + match = re.search(rf'^{field} = "(.+)"$', path.read_text(), re.MULTILINE) + assert match is not None, f"Missing {field} in {path}" + return match.group(1) + + +def _workspace_pyprojects() -> list[tuple[Path, str, str]]: + root_pyproject = ROOT / "pyproject.toml" + members_match = re.search( + r"\[tool\.uv\.workspace\]\s*members\s*=\s*\[(.*?)\]", + root_pyproject.read_text(), + re.DOTALL, + ) + assert members_match is not None, "Missing [tool.uv.workspace] members in root pyproject.toml" + members = re.findall(r'"([^"]+)"', members_match.group(1)) + projects = [ + ( + root_pyproject, + _project_field(root_pyproject, "name"), + _project_field(root_pyproject, "version"), + ) + ] + for member in members: + pyproject = ROOT / member / "pyproject.toml" + projects.append((pyproject, _project_field(pyproject, "name"), _project_field(pyproject, "version"))) + return projects + + +def _workspace_projects() -> dict[str, str]: + return {name: version for _, name, version in _workspace_pyprojects()} + + +def _lock_versions() -> dict[str, str]: + lock = (ROOT / "uv.lock").read_text() + return dict(re.findall(r'\[\[package\]\]\nname = "([^"]+)"\nversion = "([^"]+)"', lock)) + + +def _toml(path: Path) -> dict: + try: + import tomllib + except ModuleNotFoundError: # Python 3.10 + import tomlkit + + return tomlkit.loads(path.read_text()) + with path.open("rb") as file: + return tomllib.load(file) + + +def test_workspace_packages_share_one_version(): + versions = set(_workspace_projects().values()) + assert len(versions) == 1, f"Workspace package versions diverged: {sorted(versions)}" + + +def test_lockfile_records_workspace_versions(): + lock_versions = _lock_versions() + for name, version in _workspace_projects().items(): + assert lock_versions.get(name) == version, ( + f"uv.lock has {name} {lock_versions.get(name)!r}, pyproject.toml declares {version!r}; run `uv lock`" + ) + + +def test_workspace_member_dependencies_are_pinned_to_workspace_version(): + projects = _workspace_projects() + workspace_names = {canonicalize_name(name) for name in projects} + + for pyproject, project_name, version in _workspace_pyprojects(): + document = _toml(pyproject) + project = document["project"] + requirements = list(project.get("dependencies", [])) + for optional in project.get("optional-dependencies", {}).values(): + requirements.extend(optional) + for group in document.get("dependency-groups", {}).values(): + requirements.extend(item for item in group if isinstance(item, str)) + + for requirement_text in requirements: + requirement = Requirement(requirement_text) + requirement_name = canonicalize_name(requirement.name) + if requirement_name == canonicalize_name(project_name): + # Self-dependencies only activate another extra of the same + # distribution and do not need a version specifier. + continue + if requirement_name not in workspace_names: + continue + assert str(requirement.specifier) == f"=={version}", ( + f"{pyproject} must pin {requirement.name} to the workspace version " + f"{version}; got {requirement.specifier}" + ) diff --git a/uv.lock b/uv.lock index bfd2c129..bad9b3d2 100644 --- a/uv.lock +++ b/uv.lock @@ -89,6 +89,15 @@ version = "4.9.3" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/3e/38/7859ff46355f76f8d19459005ca000b6e7012f2f1ca597746cbcd1fbfe5e/antlr4-python3-runtime-4.9.3.tar.gz", hash = "sha256:f224469b4168294902bb1efa80a8bf7855f24c99aef99cbefc1bcd3cce77881b", size = 117034, upload-time = "2021-11-06T17:52:23.524Z" } +[[package]] +name = "argcomplete" +version = "3.7.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/87/6f/5a73f04007ca950701765949209f068da628bd11f9c2da287278ce91e0ee/argcomplete-3.7.2.tar.gz", hash = "sha256:aad8b69a0b9969edb62db0d1752354c0d50717b10e0cbb00e2a958381b9fc6b9", size = 74473, upload-time = "2026-08-06T04:53:21.662Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/46/bd/551ee6af426af84ca33e02622be722925c196608e9127d731ef17c47f06e/argcomplete-3.7.2-py3-none-any.whl", hash = "sha256:6029205678bdd9c1c728a155f5f9ecf5812393f969eef58807641a2bc2aa5b19", size = 43294, upload-time = "2026-08-06T04:53:20.246Z" }, +] + [[package]] name = "array-api-compat" version = "1.15.0" @@ -238,6 +247,30 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a7/06/3d6badcf13db419e25b07041d9c7b4a2c331d3f4e7134445ec5df57714cd/coloredlogs-15.0.1-py2.py3-none-any.whl", hash = "sha256:612ee75c546f53e92e70049c9dbfcc18c935a2b9a53b66085ce9ef6a6e5c0934", size = 46018, upload-time = "2021-06-11T10:22:42.561Z" }, ] +[[package]] +name = "commitizen" +version = "4.19.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "argcomplete" }, + { name = "charset-normalizer" }, + { name = "colorama" }, + { name = "decli" }, + { name = "deprecated" }, + { name = "jinja2" }, + { name = "packaging" }, + { name = "prompt-toolkit" }, + { name = "pyyaml" }, + { name = "questionary" }, + { name = "termcolor" }, + { name = "tomlkit" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c4/ad/ad4e6fafa0627f38ebd3c65ad63767f3f07e0b33b72cbff0d3cadddf6ecb/commitizen-4.19.2.tar.gz", hash = "sha256:af0fb25f2fb60956b9d64eecd6e3ec7b710c2f9b012ce8728cb32c728c2c5a8a", size = 69721, upload-time = "2026-10-07T07:31:13.903Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7c/95/904e5c6b231038c1f66f75dceb7600db010631eb657d5c1b75ef26d0d42c/commitizen-4.19.2-py3-none-any.whl", hash = "sha256:178505ff30a9740b33e4d8e6a01854306213d88f5068fbc88554dbcb1a39fd4b", size = 91930, upload-time = "2026-10-07T07:31:12.221Z" }, +] + [[package]] name = "coverage" version = "7.16.0" @@ -346,6 +379,27 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f1/e7/3bbb2d3e6eb7aa29e2edc95ea5062458a0c92dd3e87640f83feed537e555/cyclonedds-0.10.2-cp310-cp310-win_amd64.whl", hash = "sha256:a5be989f69103954c6041b80e3ea1ae9973d41b1b0e437fb71b671417c450ad6", size = 2274046, upload-time = "2022-09-09T07:04:30.987Z" }, ] +[[package]] +name = "decli" +version = "0.6.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0c/59/d4ffff1dee2c8f6f2dd8f87010962e60f7b7847504d765c91ede5a466730/decli-0.6.3.tar.gz", hash = "sha256:87f9d39361adf7f16b9ca6e3b614badf7519da13092f2db3c80ca223c53c7656", size = 7564, upload-time = "2025-06-01T15:23:41.25Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d8/fa/ec878c28bc7f65b77e7e17af3522c9948a9711b9fa7fc4c5e3140a7e3578/decli-0.6.3-py3-none-any.whl", hash = "sha256:5152347c7bb8e3114ad65db719e5709b28d7f7f45bdb709f70167925e55640f3", size = 7989, upload-time = "2025-06-01T15:23:40.228Z" }, +] + +[[package]] +name = "deprecated" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "wrapt" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/49/85/12f0a49a7c4ffb70572b6c2ef13c90c88fd190debda93b23f026b25f9634/deprecated-1.3.1.tar.gz", hash = "sha256:b1b50e0ff0c1fddaa5708a2c6b0a6588bb09b892825ab2b214ac9ea9d92a5223", size = 2932523, upload-time = "2025-10-30T08:19:02.757Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/84/d0/205d54408c08b13550c733c4b85429e7ead111c7f0014309637425520a9a/deprecated-1.3.1-py2.py3-none-any.whl", hash = "sha256:597bfef186b6f60181535a29fbe44865ce137a5079f295b479886c82729d5f3f", size = 11298, upload-time = "2025-10-30T08:19:00.758Z" }, +] + [[package]] name = "docutils" version = "0.21.2" @@ -1182,12 +1236,14 @@ unitree = [ [package.dev-dependencies] dev = [ + { name = "commitizen" }, { name = "dprint-py" }, { name = "mypy" }, { name = "pytest" }, { name = "pytest-cov" }, ] test = [ + { name = "commitizen" }, { name = "dprint-py" }, { name = "hydra-core" }, { name = "motrix-deploy-unitree" }, @@ -1233,12 +1289,14 @@ provides-extras = ["rslrl", "skrl-jax", "unitree", "docs", "cuda", "tbb", "rocm" [package.metadata.requires-dev] dev = [ + { name = "commitizen", specifier = ">=4,<5" }, { name = "dprint-py", specifier = ">=0.57.0.0" }, { name = "mypy", specifier = ">=1.15,<2" }, { name = "pytest", specifier = ">=8.3.3,<8.4.0" }, { name = "pytest-cov" }, ] test = [ + { name = "commitizen", specifier = ">=4,<5" }, { name = "dprint-py", specifier = ">=0.57.0.0" }, { name = "hydra-core", specifier = ">=1.3,<1.4" }, { name = "motrix-deploy-unitree", editable = "motrix_deploy_unitree" }, @@ -1260,7 +1318,6 @@ dependencies = [ { name = "nvidia-ml-py" }, { name = "omegaconf" }, { name = "onnx" }, - { name = "python-abc" }, { name = "rich" }, { name = "torch", version = "2.7.0+cu128", source = { registry = "https://download.pytorch.org/whl/cu128" }, marker = "(sys_platform == 'linux' and extra == 'extra-10-motrix-lab-cuda') or (sys_platform == 'win32' and extra == 'extra-10-motrix-lab-cuda') or (extra == 'extra-10-motrix-lab-cuda' and extra == 'extra-10-motrix-lab-rocm')" }, { name = "torch", version = "2.11.0", source = { registry = "https://pypi.org/simple" }, marker = "(platform_machine != 'x86_64' and sys_platform == 'linux' and extra != 'extra-10-motrix-lab-cuda') or (sys_platform != 'linux' and sys_platform != 'win32') or (sys_platform == 'win32' and extra == 'extra-10-motrix-lab-rocm') or (sys_platform == 'win32' and extra != 'extra-10-motrix-lab-cuda') or (sys_platform == 'linux' and extra == 'extra-10-motrix-lab-cuda' and extra == 'extra-10-motrix-lab-rocm') or (sys_platform == 'linux' and extra != 'extra-10-motrix-lab-cuda' and extra != 'extra-10-motrix-lab-rocm')" }, @@ -1277,7 +1334,6 @@ requires-dist = [ { name = "nvidia-ml-py", specifier = ">=13.610.43" }, { name = "omegaconf", specifier = ">=2.3,<2.4" }, { name = "onnx", specifier = "==1.20.1" }, - { name = "python-abc", specifier = ">=0.2.0" }, { name = "rich", specifier = ">=13" }, { name = "torch", specifier = ">=2.7,<3" }, ] @@ -2082,11 +2138,11 @@ wheels = [ [[package]] name = "packaging" -version = "25.0" +version = "26.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/fa/3944b40b07da9ce895c0e6303a5ab7d53da063554f534556b134a54d6093/packaging-26.3.tar.gz", hash = "sha256:94edc256424af38762eb31306eed28beb9f0efc50a8837492c9d6fd6004aed79", size = 313412, upload-time = "2026-08-04T18:15:28.737Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, + { url = "https://files.pythonhosted.org/packages/63/34/ba1c580383c9eada3711951fef0795c80b829a078d72188184bcab9dd527/packaging-26.3-py3-none-any.whl", hash = "sha256:d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c", size = 129956, upload-time = "2026-08-04T18:15:27.159Z" }, ] [[package]] @@ -2126,6 +2182,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, ] +[[package]] +name = "prompt-toolkit" +version = "3.0.53" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "wcwidth" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7d/ea/39b988c938f75cb75d7045b5c69f8bfed47ee2152c8837fb403de29d6fb8/prompt_toolkit-3.0.53.tar.gz", hash = "sha256:9ec8a0ad96d5c56148b3f914aa79c1564c3fde5d2e6b876e7bc327e353cf8fa6", size = 435492, upload-time = "2026-07-26T20:56:14.758Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/6f/84908cad2d6aa5144abcf7b42709fe4fdb459bc640ec7ac5786e7693dabc/prompt_toolkit-3.0.53-py3-none-any.whl", hash = "sha256:01c0891d7f9237d5e339f7d3e42cdae80b7534abb1c7c0e3352efba6231492f2", size = 392288, upload-time = "2026-07-26T20:56:12.512Z" }, +] + [[package]] name = "protobuf" version = "6.33.1" @@ -2264,12 +2332,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/9d/7a/d968e294073affff457b041c2be9868a40c1c71f4a35fcc1e45e5493067b/pytest_cov-7.1.0-py3-none-any.whl", hash = "sha256:a0461110b7865f9a271aa1b51e516c9a95de9d696734a2f71e3e78f46e1d4678", size = 22876, upload-time = "2026-03-21T20:11:14.438Z" }, ] -[[package]] -name = "python-abc" -version = "0.2.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/17/e3/5973ccea7302dfe994331e2824bfc8cc4e4c8ba7f0b9d1f16bcda5ef26a3/python-abc-0.2.0.tar.gz", hash = "sha256:90017d09fbac7bde4b64b2c7e1b5d22da9055b64b821d1a2b4dc805b450b251a", size = 7423, upload-time = "2022-06-26T21:36:06.156Z" } - [[package]] name = "pyvers" version = "0.2.2" @@ -2296,6 +2358,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/2a/fa/926c003379b19fca39dd4634818b00dec6c62d87faf628d1394e137354d4/pyyaml-6.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c", size = 158561, upload-time = "2025-09-25T21:31:57.406Z" }, ] +[[package]] +name = "questionary" +version = "2.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "prompt-toolkit" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f6/45/eafb0bba0f9988f6a2520f9ca2df2c82ddfa8d67c95d6625452e97b204a5/questionary-2.1.1.tar.gz", hash = "sha256:3d7e980292bb0107abaa79c68dd3eee3c561b83a0f89ae482860b181c8bd412d", size = 25845, upload-time = "2025-08-28T19:00:20.851Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3c/26/1062c7ec1b053db9e499b4d2d5bc231743201b74051c973dadeac80a8f43/questionary-2.1.1-py3-none-any.whl", hash = "sha256:a51af13f345f1cdea62347589fbb6df3b290306ab8930713bfae4d475a7d4a59", size = 36753, upload-time = "2025-08-28T19:00:19.56Z" }, +] + [[package]] name = "requests" version = "2.32.5" @@ -2753,6 +2827,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/77/b8/0135fadc89e73be292b473cb820b4f5a08197779206b33191e801feeae40/tomli-2.3.0-py3-none-any.whl", hash = "sha256:e95b1af3c5b07d9e643909b5abbec77cd9f1217e6d0bca72b0234736b9fb1f1b", size = 14408, upload-time = "2025-10-08T22:01:46.04Z" }, ] +[[package]] +name = "tomlkit" +version = "0.15.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/96/e07752635b98536177fa1f37671c8f3cdde2e724c6bcf6034b2cfb571565/tomlkit-0.15.1.tar.gz", hash = "sha256:e25bbf38843005246210a12982776f27f99cb9be67160e14434d0c0d21ee1e97", size = 180129, upload-time = "2026-07-17T01:48:04.562Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/13/bc/8c13eb66537dce1d2bd3a57132902f38d0e7f5bb46fa9f4daed9fe9d76ee/tomlkit-0.15.1-py3-none-any.whl", hash = "sha256:177a05aece5a8ca5266fd3c448abb47b8d352f09d477d3ca8332db4d89b24304", size = 49449, upload-time = "2026-07-17T01:48:05.728Z" }, +] + [[package]] name = "toolz" version = "1.1.0" @@ -3061,6 +3144,26 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc", size = 129795, upload-time = "2025-06-18T14:07:40.39Z" }, ] +[[package]] +name = "wcwidth" +version = "0.9.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f0/b4/7830542634bb2d3e62aa3b586a72d5b3b6c91c3168929e7000ef3fed041d/wcwidth-0.9.2.tar.gz", hash = "sha256:ae0ef90b90f6af38b54f1fe6d58662ec33b3cb4b8391958a62416d654231727b", size = 955039, upload-time = "2026-10-05T00:24:05.521Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/59/1e/4532a81fb9dfbf4114a816775e0a36c3a64ee1d1f4bba2094e2da50be5dc/wcwidth-0.9.2-cp310-abi3-macosx_10_9_x86_64.whl", hash = "sha256:7ef5a940bd5e30bac6e721f1a48fce0cd7bb3ece19e9c5d139e72c76c35cfd07", size = 605343, upload-time = "2026-10-05T00:23:22.649Z" }, + { url = "https://files.pythonhosted.org/packages/a0/07/cb6940e81134b7ed25fa312ee9ab536a63db0793b149f88a90e603ceace9/wcwidth-0.9.2-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:ae0800c5339423cc53d33a266ad264b42ba8aaa16d4464f6e6b1bee607f50b17", size = 609960, upload-time = "2026-10-05T00:23:27.049Z" }, + { url = "https://files.pythonhosted.org/packages/a4/80/15ad05d40bfa99155639fb9e13b3d77083aa0fab893c816db2543d29005c/wcwidth-0.9.2-cp310-abi3-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:9e542f1f8475b78452a295495d7a5bc3ead565112e9446a64dc93462a41c2a79", size = 762566, upload-time = "2026-10-05T00:23:38.322Z" }, + { url = "https://files.pythonhosted.org/packages/bc/f0/b8ef7758003d66b60f093695831a86dcc726aac01ee6446ffcbda27b61e3/wcwidth-0.9.2-cp310-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:674b518af28d38ee645ff97b74f5760abee5fad4bac74413bfc4b881ef2ce724", size = 771632, upload-time = "2026-10-05T00:23:32.448Z" }, + { url = "https://files.pythonhosted.org/packages/db/6c/f940133c71427c208575910e981942bd78c98b1f7cd0d1425ca4b7457c04/wcwidth-0.9.2-cp310-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:751bef0ab404b6a1dc028b56b4b85d46486be1c55833f80da533e42dc691f389", size = 769763, upload-time = "2026-10-05T00:23:40.175Z" }, + { url = "https://files.pythonhosted.org/packages/92/8f/285f862826f721964ec7c42f81dc53d23afbd723a0f4cd989651f8218e25/wcwidth-0.9.2-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:c3d80f39ba4653a595edae9aa46a509d14883790a8fc23c5db221ceb207f64b7", size = 782134, upload-time = "2026-10-05T00:23:33.926Z" }, + { url = "https://files.pythonhosted.org/packages/c2/2d/64aa54882a5d556d3654c1f926d9118b797461033e23a158409941a37c8f/wcwidth-0.9.2-cp310-abi3-musllinux_1_2_i686.whl", hash = "sha256:0a47e03d8293590ecce66c45dc20ff7b4b885e3c78093722239585eca0d77ab2", size = 782851, upload-time = "2026-10-05T00:23:41.974Z" }, + { url = "https://files.pythonhosted.org/packages/59/39/52389f6de7fe2e9c14ceb8253dd99034bd86e1c87847ea3c100a97dded9a/wcwidth-0.9.2-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:67d901a4ad99249eb775b4ee4769ca97fa405d35a75f46e83166910a47003f04", size = 784621, upload-time = "2026-10-05T00:23:43.449Z" }, + { url = "https://files.pythonhosted.org/packages/b3/8b/20225500a076ace27bbcc8a6fd7c55125133c57a618816c7b7b8b73070b1/wcwidth-0.9.2-cp310-abi3-win32.whl", hash = "sha256:ee1fd0db9d9fd711a70f3e7765e0e04c05d26982fa05361456163062549d7da4", size = 593123, upload-time = "2026-10-05T00:23:55.953Z" }, + { url = "https://files.pythonhosted.org/packages/5a/d6/b0690f55ea0483530a18bac917fbadbf54f35122510446fc370f5f1c2453/wcwidth-0.9.2-cp310-abi3-win_amd64.whl", hash = "sha256:2a9746de704242bd4fdaabb31dd46b82f694a56a8d21081ad89b679a89da9fec", size = 597827, upload-time = "2026-10-05T00:23:57.489Z" }, + { url = "https://files.pythonhosted.org/packages/e5/11/6ecf4e9e268ab1a4ec617ffcccc2ee4a71301625f5490912dbaba462fa9c/wcwidth-0.9.2-cp310-abi3-win_arm64.whl", hash = "sha256:b9c6ab615e03723b7f8760ea2f27758d656e7e13b51515c9dca5c3e8b04612fa", size = 598415, upload-time = "2026-10-05T00:23:51.517Z" }, + { url = "https://files.pythonhosted.org/packages/9c/b4/0bfa065af506540d9d558e3e5548cff00bc1f9b24e6e2a8512498e8628de/wcwidth-0.9.2-py3-none-any.whl", hash = "sha256:89ca642c5bf0101157a09366be69fad0379db1f700ae39a920e103234573670e", size = 301667, upload-time = "2026-10-05T00:23:21.097Z" }, +] + [[package]] name = "werkzeug" version = "3.1.3"