Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docker/runtime-python/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ name = "odoo-runtime-support"
version = "0.0.0"
requires-python = ">=3.13"
dependencies = [
"hatchling==1.27.0",
"hatchling==1.31.0",
"passlib>=1.7.4",
"pydantic>=2.13.4",
"pydantic-settings>=2.14.2",
Expand Down
8 changes: 4 additions & 4 deletions docker/runtime-python/uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 5 additions & 1 deletion docs/tooling/artifact-inputs.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,11 @@ Before `platform runtime publish`, run `platform dependencies check` and commit
the exact tenant, devkit, and shared-addon inputs. Publish fails closed for
dirty source repos, nonordinary index flags or Git replacement refs, untracked
or symlinked staged files, stale/missing tenant lock pairs, mutable VCS refs,
source-supplied `.odoo-python-source.json` markers, or staged-byte changes.
source-supplied `.odoo-python-source.json` markers, staged-byte changes, or an
addon build backend that is absent at the exact version from both the devkit
support lock and tenant lock catalog. When the manifest includes the devkit
repo, `platform dependencies check` reports that build-tool mismatch before the
publish workflow reaches Buildx.
Devkit alone writes those markers from the verified Git snapshots used for the
build. Each recorded source commit must also be advertised by a ref in its
normalized GitHub origin; changing only `.git/config` cannot reattribute a
Expand Down
22 changes: 22 additions & 0 deletions odoo_devkit/dependency_workspace.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,9 +120,15 @@ def inspect_dependency_workspace(*, manifest: WorkspaceManifest) -> DependencyWo
raise DependencyWorkspaceError(
"Shared addons repo must be materialized before dependency inspection. Run `platform workspace sync` first."
)
devkit_repo_path = _resolve_devkit_repo_path(manifest)
if manifest.devkit_repo is not None and (devkit_repo_path is None or not devkit_repo_path.is_dir()):
raise DependencyWorkspaceError(
"Devkit repo must be materialized before dependency inspection. Run `platform workspace sync` first."
)

tenant_repo_path = tenant_repo_path.resolve()
shared_addons_repo_path = shared_addons_repo_path.resolve() if shared_addons_repo_path is not None else None
devkit_repo_path = devkit_repo_path.resolve() if devkit_repo_path is not None else None
project_inputs = _discover_project_inputs(
tenant_repo_path=tenant_repo_path,
shared_addons_repo_path=shared_addons_repo_path,
Expand Down Expand Up @@ -198,6 +204,11 @@ def inspect_dependency_workspace(*, manifest: WorkspaceManifest) -> DependencyWo
root_payload = _load_pyproject(staged_root / "pyproject.toml")
root_runtime_dependencies = _validate_root_pyproject(payload=root_payload)
requires_tenant_lock = requires_tenant_lock or bool(root_runtime_dependencies)
if devkit_repo_path is not None:
require_staged_build_requirements_supplied(
support_root=devkit_repo_path / "docker" / "runtime-python",
tenant_root=staged_root,
)
workspace_member_set = _workspace_members(root=staged_root, payload=root_payload)
workspace_members = tuple(sorted(path.as_posix() for path in workspace_member_set))
expected_members = {
Expand Down Expand Up @@ -342,6 +353,17 @@ def _resolve_shared_addons_repo_path(manifest: WorkspaceManifest) -> Path | None
)


def _resolve_devkit_repo_path(manifest: WorkspaceManifest) -> Path | None:
from .workspace import resolve_optional_repo_path_with_managed_checkout, resolve_workspace_path

workspace_path = resolve_workspace_path(manifest)
return resolve_optional_repo_path_with_managed_checkout(
manifest.devkit_repo,
manifest=manifest,
managed_checkout_path=workspace_path / "sources" / "devkit",
)


def _discover_project_inputs(*, tenant_repo_path: Path, shared_addons_repo_path: Path | None) -> tuple[_ProjectInput, ...]:
projects: list[_ProjectInput] = []
tenant_addons_root = tenant_repo_path / "addons"
Expand Down
59 changes: 57 additions & 2 deletions tests/test_dependency_workspace.py
Original file line number Diff line number Diff line change
Expand Up @@ -428,6 +428,55 @@ def test_addon_build_requirements_must_be_supplied_by_a_lock_catalog(self) -> No
tenant_root=tenant_root,
)

def test_dependency_inspection_checks_devkit_build_requirement_catalog(self) -> None:
with tempfile.TemporaryDirectory() as temporary_directory_name:
temp_root = Path(temporary_directory_name)
tenant_repo_path = temp_root / "tenant"
devkit_repo_path = temp_root / "devkit"
self._write_member_pyproject(
tenant_repo_path / "addons" / "tenant_addon",
build_requirement="hatchling==1.31.0",
)
self._write_root_workspace(
tenant_repo_path=tenant_repo_path,
members=("addons/*",),
)
support_root = devkit_repo_path / "docker" / "runtime-python"
support_root.mkdir(parents=True)
support_pyproject = support_root / "pyproject.toml"
support_pyproject.write_text(
'[project]\nname = "runtime-support"\nversion = "0.0.0"\n'
'dependencies = ["hatchling==1.27.0"]\n\n[tool.uv]\npackage = false\n',
encoding="utf-8",
)
manifest = self._write_manifest(
temp_root=temp_root,
tenant_repo_path=tenant_repo_path,
devkit_repo_path=devkit_repo_path,
)
self._commit_repo(tenant_repo_path)

with mock.patch("odoo_devkit.dependency_workspace._uv_lock_is_current", return_value=True):
inspection = inspect_dependency_workspace(manifest=manifest)

self.assertFalse(inspection.current)
self.assertIn(
"Addon build requirements must be supplied by the support/runtime or tenant lock catalog: hatchling==1.31.0",
inspection.findings,
)

support_pyproject.write_text(
support_pyproject.read_text(encoding="utf-8").replace(
"hatchling==1.27.0",
"hatchling==1.31.0",
),
encoding="utf-8",
)
with mock.patch("odoo_devkit.dependency_workspace._uv_lock_is_current", return_value=True):
inspection = inspect_dependency_workspace(manifest=manifest)

self.assertTrue(inspection.current, inspection.findings)

def test_owned_requirements_file_fails(self) -> None:
with tempfile.TemporaryDirectory() as temporary_directory_name:
temp_root = Path(temporary_directory_name)
Expand Down Expand Up @@ -470,14 +519,15 @@ def _write_member_pyproject(
*,
project_name: str = "tenant_addon",
dependencies: tuple[str, ...] = (),
build_requirement: str = "hatchling==1.27.0",
) -> None:
project_root.mkdir(parents=True, exist_ok=True)
dependency_lines = ",\n".join(f" {json.dumps(dependency)}" for dependency in dependencies)
if dependency_lines:
dependency_lines += ",\n"
(project_root / "pyproject.toml").write_text(
"[build-system]\n"
'requires = ["hatchling==1.27.0"]\n'
f"requires = [{json.dumps(build_requirement)}]\n"
'build-backend = "hatchling.build"\n\n'
"[project]\n"
f'name = "{project_name}"\n'
Expand Down Expand Up @@ -526,12 +576,17 @@ def _write_manifest(
temp_root: Path,
tenant_repo_path: Path,
shared_repo_path: Path | None = None,
devkit_repo_path: Path | None = None,
) -> WorkspaceManifest:
tenant_repo_path.mkdir(parents=True, exist_ok=True)
shared_repo_table = ""
if shared_repo_path is not None:
shared_repo_path.mkdir(parents=True, exist_ok=True)
shared_repo_table = f'\n[repos.shared_addons]\nname = "shared-addons"\npath = "{shared_repo_path}"\nref = "main"\n'
devkit_repo_table = ""
if devkit_repo_path is not None:
devkit_repo_path.mkdir(parents=True, exist_ok=True)
devkit_repo_table = f'\n[repos.devkit]\nname = "odoo-devkit"\npath = "{devkit_repo_path}"\nref = "main"\n'
manifest_path = tenant_repo_path / "workspace.toml"
manifest_path.write_text(
"schema_version = 1\n"
Expand All @@ -544,7 +599,7 @@ def _write_manifest(
'name = "tenant"\n'
'path = "."\n'
'ref = "main"\n'
f"{shared_repo_table}\n"
f"{shared_repo_table}{devkit_repo_table}\n"
"[runtime]\n"
'context = "test"\n'
'instance = "local"\n'
Expand Down