From 3050343cb0f65a5550862c37d9de787b77ec0f2e Mon Sep 17 00:00:00 2001 From: Timothy Willard <9395586+TimothyWillard@users.noreply.github.com> Date: Mon, 27 Apr 2026 10:12:37 -0400 Subject: [PATCH] ci: co-release `op_engine` w/ connector version --- .github/workflows/release.yaml | 4 +-- docs/development/creating-a-release.md | 6 ++-- flepimop2-op_engine/pyproject.toml | 18 +++++------ justfile | 17 ++++++++--- pyproject.toml | 2 +- scripts/release_validate.py | 42 ++++++++++++++++++++++++-- 6 files changed, 66 insertions(+), 23 deletions(-) diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 6c8d8f2..8f34dd6 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -68,8 +68,8 @@ jobs: run: | mkdir -p release-dist/flepimop2-op_engine cd flepimop2-op_engine - uv run --with build --with twine python -m build --outdir ../release-dist/flepimop2-op_engine - uv run --with twine python -m twine check --strict ../release-dist/flepimop2-op_engine/* + uv run --no-project --with build --with twine python -m build --outdir ../release-dist/flepimop2-op_engine + uv run --no-project --with twine python -m twine check --strict ../release-dist/flepimop2-op_engine/* - name: 'Upload core package artifacts' uses: 'actions/upload-artifact@v7' with: diff --git a/docs/development/creating-a-release.md b/docs/development/creating-a-release.md index 14303f9..8db708e 100644 --- a/docs/development/creating-a-release.md +++ b/docs/development/creating-a-release.md @@ -27,6 +27,8 @@ Today that means these two files must contain the same semantic version: If any of them differ, the `validate` job fails immediately. +The workflow also validates that `flepimop2-op_engine` depends on the exact matching `op-engine` release version. For example, release `0.2.0` must declare `op-engine==0.2.0`. + ## 2. Run The Local Release Preflight Use the local pre-release target before dispatching the release workflow: @@ -124,6 +126,6 @@ For `publish-target=pypi`, configure the PyPI trusted publisher entry for the sa Because this repository publishes two distributions from one workflow, the same trusted publisher setup must be allowed to publish both package names on the selected index. -## 6. Current Packaging Note +## 6. Package Metadata -The release workflow now handles the mechanics of validating, building, and sequencing both distributions together. Package-index acceptance still depends on the metadata inside each built distribution, so publishing `flepimop2-op_engine` to a public index may still require follow-up packaging changes outside the workflow itself. +Published package metadata must use registry-compatible dependencies. The provider package must not declare direct URL dependencies, because PyPI rejects distributions that require them. diff --git a/flepimop2-op_engine/pyproject.toml b/flepimop2-op_engine/pyproject.toml index 2f16c21..add7862 100644 --- a/flepimop2-op_engine/pyproject.toml +++ b/flepimop2-op_engine/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "flepimop2-op_engine" -version = "0.1.0" +version = "0.1.1" readme = { file = "README.md", content-type = "text/markdown" } description = "flepimop2 engine provider package for op_engine" requires-python = ">=3.11,<3.15" @@ -12,12 +12,8 @@ authors = [ ] keywords = ["flepimop2", "op_engine", "engine", "provider"] dependencies = [ - # op-engine is not in a registry yet, so keep it as a direct reference. - "op-engine @ git+https://github.com/ACCIDDA/op_engine.git@main", - - # flepimop2 is not in a registry, so keep it as a direct reference. - "flepimop2 @ git+https://github.com/ACCIDDA/flepimop2.git@main", - + "op-engine==0.1.1", + "flepimop2>=0.1.0", "pydantic>=2.0,<3", "numpy>=1.26", ] @@ -44,10 +40,6 @@ Issues = "https://github.com/ACCIDDA/op_engine/issues" requires = ["hatchling"] build-backend = "hatchling.build" -# REQUIRED for hatchling to accept git/direct URL dependencies -[tool.hatch.metadata] -allow-direct-references = true - # Ensure hatchling can build from src/ layout and include the namespace package [tool.hatch.build.targets.wheel] packages = ["src/flepimop2"] @@ -57,6 +49,10 @@ testpaths = ["tests"] addopts = "-q" +[tool.uv.sources] +op-engine = { path = "..", editable = true } + + [tool.ruff.format] docstring-code-format = true docstring-code-line-length = 72 diff --git a/justfile b/justfile index 6ad3dee..e3a2b0a 100644 --- a/justfile +++ b/justfile @@ -33,7 +33,15 @@ pytest-core: # Assumes `flepimop2-op_engine/.venv` already exists (run `just provider-sync` or `just ci` first). pytest-provider: provider-sync - cd flepimop2-op_engine && .venv/bin/python -m pytest --doctest-modules + #!/usr/bin/env bash + set -euo pipefail + CORE_DIST="$(mktemp -d)" + trap 'rm -rf "${CORE_DIST}"' EXIT + uv run --with build python -m build --wheel --outdir "${CORE_DIST}" + cd flepimop2-op_engine + export PATH="${PWD}/.venv/bin:${PATH}" + export PIP_FIND_LINKS="${CORE_DIST}" + .venv/bin/python -m pytest --doctest-modules pytest: pytest-core pytest-provider @@ -77,8 +85,8 @@ build-check-core: build-check-provider: cd flepimop2-op_engine && rm -rf dist - cd flepimop2-op_engine && uv run --with build --with twine python -m build --wheel - cd flepimop2-op_engine && uv run --with twine python -m twine check --strict dist/* + cd flepimop2-op_engine && uv run --no-project --with build --with twine python -m build --wheel + cd flepimop2-op_engine && uv run --no-project --with twine python -m twine check --strict dist/* build-check: build-check-core build-check-provider @@ -104,7 +112,7 @@ build-test-provider: trap 'rm -rf "${CLEANROOM}"' EXIT cd flepimop2-op_engine uv export --frozen --only-group dev --no-emit-project --format requirements.txt --no-hashes --output-file "${CLEANROOM}/dev-requirements.txt" >/dev/null - uv run --with build python -m build --wheel --outdir "${CLEANROOM}/provider-dist" + uv run --no-project --with build python -m build --wheel --outdir "${CLEANROOM}/provider-dist" cd .. uv run --with build python -m build --wheel --outdir "${CLEANROOM}/core-dist" uv venv --python "${UV_PYTHON_VERSION:-3.12}" "${CLEANROOM}/venv" @@ -119,6 +127,7 @@ build-test-provider: cp -R flepimop2-op_engine/tests "${CLEANROOM}/tests" cd "${CLEANROOM}" export PATH="${CLEANROOM}/venv/bin:${PATH}" + export PIP_FIND_LINKS="${CLEANROOM}/core-dist" "${CLEANROOM}/venv/bin/pytest" --import-mode=importlib tests --quiet --exitfirst build-test: build-test-core build-test-provider diff --git a/pyproject.toml b/pyproject.toml index fafda1f..ae2f33e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "op_engine" -version = "0.1.0" +version = "0.1.1" description = "Multiphysics integration engine for ODEs and PDEs using implicit-explicit operator splitting" readme = { file = "README.md", content-type = "text/markdown" } requires-python = ">=3.11,<3.15" diff --git a/scripts/release_validate.py b/scripts/release_validate.py index abeb54a..57a6aa1 100644 --- a/scripts/release_validate.py +++ b/scripts/release_validate.py @@ -11,6 +11,9 @@ REPO_ROOT: Final[pathlib.Path] = pathlib.Path(__file__).resolve().parents[1] SEMVER_PATTERN: Final[re.Pattern[str]] = re.compile(r"^\d+\.\d+\.\d+$") +PROVIDER_PYPROJECT: Final[pathlib.Path] = ( + REPO_ROOT / "flepimop2-op_engine" / "pyproject.toml" +) def get_declared_versions() -> dict[str, str]: @@ -22,9 +25,9 @@ def get_declared_versions() -> dict[str, str]: core_version = tomllib.loads((REPO_ROOT / "pyproject.toml").read_text("utf-8"))[ "project" ]["version"] - provider_version = tomllib.loads( - (REPO_ROOT / "flepimop2-op_engine" / "pyproject.toml").read_text("utf-8") - )["project"]["version"] + provider_version = tomllib.loads(PROVIDER_PYPROJECT.read_text("utf-8"))["project"][ + "version" + ] return { "pyproject.toml": str(core_version), @@ -56,6 +59,38 @@ def validate_release_version() -> str: return version +def validate_provider_dependencies(version: str) -> None: + """ + Validate provider dependencies that must match release metadata. + + Raises: + SystemExit: If any provider dependencies are direct references. + SystemExit: If the provider does not depend on the exact shared `op-engine` + release version. + """ + provider_project = tomllib.loads(PROVIDER_PYPROJECT.read_text("utf-8"))["project"] + dependencies = [str(dependency) for dependency in provider_project["dependencies"]] + + direct_references = [ + dependency + for dependency in dependencies + if " @ " in dependency or "://" in dependency or "git+" in dependency + ] + if direct_references: + rendered = ", ".join(direct_references) + msg = f"Provider package dependencies must be publishable to PyPI: {rendered}" + raise SystemExit(msg) + + expected_op_engine = f"op-engine=={version}" + if expected_op_engine not in dependencies: + rendered = ", ".join(dependencies) + msg = ( + "Provider package must depend on the exact shared op-engine release " + f"version {expected_op_engine!r}; got: {rendered}" + ) + raise SystemExit(msg) + + def write_github_outputs(version: str, output_path: pathlib.Path) -> None: """Write workflow outputs for downstream GitHub Actions jobs.""" docs_version = ".".join(version.split(".")[:2]) @@ -78,6 +113,7 @@ def main() -> None: args = parser.parse_args() version = validate_release_version() + validate_provider_dependencies(version) print(f"Validated release version: {version}") output_path = args.github_output