ci(platform): enforce uipath.platform layering with import-linter#1788
ci(platform): enforce uipath.platform layering with import-linter#1788andreiancuta-uipath wants to merge 6 commits into
Conversation
Add an import-linter layers contract over uipath.platform, enforced as a CI step in the lint-uipath-platform job. The contract statically forbids import cycles and upward imports between domain packages — the class of bug behind the 0.1.89 identity/common incident (#1787). Type-checking imports are excluded; the contract is exhaustive so new subpackages must be assigned a layer explicitly. The contract is intentionally committed without ignores: lint-imports fails at this commit on the pre-existing upward imports from common/interrupt_models.py, fixed in the next commit. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
common/interrupt_models.py imported action_center, attachments, context_grounding, documents, and orchestrator at runtime, forming a cycle cluster around common — the same layering inversion class that broke 0.1.89 (only masked by import ordering). The models are HITL suspend/resume payloads consumed solely by resume_triggers, so they move into that package; all former upward edges now point downward and the import-linter contract passes with zero ignores. The models are importable from uipath.platform.resume_triggers. No re-export shim in common — an eager one recreates the cycle and a lazy one still violates the layering contract. Known external usages (uipath-langchain-python docs/samples/test mocks, POC repos) import from uipath.platform.common and need a coordinated update; none are published package sources. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Adds CI enforcement for a layered architecture within uipath.platform using import-linter, and resolves an existing layering inversion by relocating HITL interrupt/resume models from common to resume_triggers (with corresponding import path updates and version bumps).
Changes:
- Add an
import-linterlayerscontract to statically forbid upward imports and cycles withinuipath.platform, enforced in CI. - Move interrupt/HITL models to
uipath.platform.resume_triggersand update imports/exports/tests accordingly (breaking pre-1.0 re-export removal fromcommon). - Bump
uipath-platformanduipathversions and lockfiles; addimport-linterdev dependency and ignore its cache.
Reviewed changes
Copilot reviewed 8 out of 12 changed files in this pull request and generated no comments.
Show a summary per file
| File | Description |
|---|---|
| packages/uipath/uv.lock | Bumps uipath and uipath-platform versions; adds import-linter to dev metadata. |
| packages/uipath/pyproject.toml | Bumps uipath version and raises minimum uipath-platform version. |
| packages/uipath-platform/uv.lock | Bumps uipath-platform version; locks import-linter and its transitive deps. |
| packages/uipath-platform/tests/services/test_hitl.py | Updates tests to import interrupt models from resume_triggers instead of common. |
| packages/uipath-platform/src/uipath/platform/resume_triggers/interrupt_models.py | Introduces relocated interrupt/HITL models under resume_triggers. |
| packages/uipath-platform/src/uipath/platform/resume_triggers/_protocol.py | Switches protocol implementation to import models from local resume_triggers. |
| packages/uipath-platform/src/uipath/platform/resume_triggers/init.py | Re-exports interrupt models from resume_triggers as the new public import path. |
| packages/uipath-platform/src/uipath/platform/common/init.py | Removes re-exports of interrupt models from common (breaking change as intended). |
| packages/uipath-platform/pyproject.toml | Adds import-linter dev dependency and defines the platform-layers contract. |
| packages/uipath-platform/CLAUDE.md | Documents the new lint-imports command for local validation. |
| .gitignore | Ignores .import_linter_cache/. |
| .github/workflows/lint-packages.yml | Adds CI step to run uv run lint-imports for uipath-platform. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
c2d23ff to
9984c7e
Compare
🚨 Heads up:
|
|



Why
Follow-up to #1787.
This PR adds an import-linter
layerscontract overuipath.platform, enforced in CI.exhaustive— new subpackages must be explicitly assigned a layerWhat changed
lint-importsstep inlint-packages.yml; contract inpyproject.toml.resume_triggers:interrupt_models.py(HITL suspend/resume payloads) moved fromcommon/toresume_triggers/, its only consumer. All seven formerly-upward edges now point downward; models no longer re-exported fromuipath.platform.common. Breaking changeguardrails: decoupled from the facade.GuardrailValidatorBase.runno longer lazily constructsUiPath()— it depends on aGuardrailEvaluatorprotocol, withDefaultGuardrailEvaluatorbuilding aGuardrailsServicedirectly from env config.common.auth.build_api_config: pydantic-to-typed-error translation extracted from_uipath.py, shared by the facade and the new evaluator.resume_triggerskeeps the deliberate call-back-into-facade pattern, so the top layer is_uipath : resume_triggers;guardrailssits in the plain services layer.How the dependencies were refactored
resume_triggers: interrupt models move out ofcommonEvery service package imports
common(base service, config, errors) — those downward edges are normal. Butinterrupt_models.pylived insidecommonand imported five service packages back, closing a package-level cyclecommon ⇄ services(masked only by import ordering incommon/__init__, the same class as the 0.1.89 incident). Moving the models intoresume_triggersremoves the upward half of the loop.flowchart TB subgraph BEFORE["❌ Before — cycle: common ⇄ service layers"] direction TB B_RT["resume_triggers/_protocol.py"] subgraph B_SVC["service layers"] B_ORC["orchestrator"] B_AC["action_center"] B_AT["attachments"] B_CG["context_grounding"] B_DOC["documents"] end subgraph B_COMMON["common (bottom layer)"] B_IM["interrupt_models.py"] B_BASE["_base_service.py, _config.py, errors, …"] end B_RT --> B_IM B_IM -. "upward ⚠️" .-> B_ORC B_IM -. "upward ⚠️" .-> B_AC B_IM -. "upward ⚠️" .-> B_AT B_IM -. "upward ⚠️" .-> B_CG B_IM -. "upward ⚠️" .-> B_DOC B_SVC -- "normal downward imports<br/>(closes the cycle)" --> B_BASE end subgraph AFTER["✅ After — interrupt models in resume_triggers"] direction TB subgraph A_RT["resume_triggers (top layer)"] A_P["_protocol.py"] A_IM["interrupt_models.py"] end subgraph A_SVC["service layers"] A_ORC["orchestrator"] A_AC["action_center"] A_AT["attachments"] A_CG["context_grounding"] A_DOC["documents"] end A_COMMON["common (bottom layer)"] A_P --> A_IM A_IM --> A_ORC A_IM --> A_AC A_IM --> A_AT A_IM --> A_CG A_IM --> A_DOC A_SVC --> A_COMMON end style B_IM fill:#ffdddd,stroke:#cc0000,color:#000 style A_IM fill:#ddffdd,stroke:#00aa00,color:#000guardrails: facade cycle broken with an evaluator protocolThe facade imports
GuardrailsServiceat module top, andGuardrailValidatorBase.runlazily imported the facade back — a static two-package cycle_uipath ⇄ guardrails. The evaluator protocol removes the upward edge, leaving only the one-way_uipath → guardrails.flowchart TB subgraph GBEFORE["❌ Before — cycle: _uipath ⇄ guardrails"] direction TB GB_U["_uipath.py (UiPath facade)"] GB_G["guardrails<br/>(GuardrailsService, GuardrailValidatorBase)"] GB_U -- "imports GuardrailsService" --> GB_G GB_G -. "run() lazily imports UiPath ⚠️" .-> GB_U end subgraph GAFTER["✅ After — one-way, facade-free"] direction TB GA_U["_uipath.py (UiPath facade)"] subgraph GA_G["guardrails"] GA_BASE["GuardrailValidatorBase.run()"] GA_PROTO["GuardrailEvaluator (Protocol)"] GA_DEF["DefaultGuardrailEvaluator"] GA_SVC["GuardrailsService"] end subgraph GA_COMMON["common"] GA_AUTH["auth.build_api_config() + resolve_config_from_env()"] end GA_U --> GA_SVC GA_U --> GA_AUTH GA_BASE --> GA_PROTO GA_DEF -- "implements" --> GA_PROTO GA_BASE -- "lazy default" --> GA_DEF GA_DEF --> GA_SVC GA_DEF --> GA_AUTH end style GB_G fill:#ffdddd,stroke:#cc0000,color:#000 style GA_PROTO fill:#ddffdd,stroke:#00aa00,color:#000 style GA_DEF fill:#ddffdd,stroke:#00aa00,color:#000Breaking change (0.1.x -> 0.2.0)
uipath.platform.resume_triggers(no longer re-exported fromuipath.platform.common)UiPath/uipath-langchain-python—docs/human_in_the_loop.md, severalsamples/,tests/hitl/mocks/job_trigger_hitl.pyUiPath/prior-auth-poc-fe,UiPath/fins-vertical-solution— coded-agent POC scriptsUiPath/skills/UiPath/AgenticProductSupport— markdown API referencesValidation
from ..identity import IdentityServicetocommon/_base_service.pybreaks the contract (reproduces the 0.1.89 class)uipath.platform.identity,uipath.platform.resume_triggers, and the facade all cleanuipath-platform0.2.0,uipath2.13.0🤖 Generated with Claude Code