ci(platform): enforce uipath.platform layering with import-linter#1788
ci(platform): enforce uipath.platform layering with import-linter#1788andreiancuta-uipath wants to merge 2 commits into
Conversation
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:
|
69c4bfe to
b5babed
Compare
🚨 Heads up:
|
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>
b5babed to
e441141
Compare
|



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.uipath.platform.commontemporarily so consumers can migrate without restoring the eager cycle.guardrails: split the public decorator API from the lower-level service and platform-model implementation.GuardrailsServiceand its platform models now live in_guardrails_service, which the facade can import without loading the publicguardrailspackage.guardrailspackage remains the public API, sits above_uipath, and retains its service/model exports.UiPath, leaving an acyclicguardrails → _uipath → _guardrails_servicedependency chain.uipath-platformfrom 0.2.9 to 0.2.10 and refreshed the lockfiles.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).The models now live canonically in
resume_triggers.flowchart TB subgraph BEFORE["❌ Before — cycle: common ⇄ service layers"] direction TB B_RT["resume_triggers/_protocol.py"] subgraph B_SVC["service layers"] direction LR B_ORC["orchestrator"] B_AC["action_center"] B_AT["attachments"] B_CG["context_grounding"] B_DOC["documents"] B_ORC ~~~ B_AC ~~~ B_AT ~~~ B_CG ~~~ B_DOC 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 imports to five packages ⚠️" .-> B_SVC B_SVC -- "normal downward imports<br/>(closes the cycle)" --> B_BASE end subgraph AFTER["✅ After — canonical 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"] direction LR A_ORC["orchestrator"] A_AC["action_center"] A_AT["attachments"] A_CG["context_grounding"] A_DOC["documents"] A_ORC ~~~ A_AC ~~~ A_AT ~~~ A_CG ~~~ A_DOC end A_COMMON["common (bottom layer)<br/>temporary lazy aliases"] A_P --> A_IM A_IM -- "model dependencies" --> A_SVC A_SVC --> A_COMMON A_COMMON -. "deprecated compatibility<br/>(temporary exception)" .-> A_IM end BEFORE ~~~ AFTER style B_IM fill:#ffdddd,stroke:#cc0000,color:#000 style A_IM fill:#ddffdd,stroke:#00aa00,color:#000 style A_COMMON fill:#fff3cd,stroke:#b8860b,color:#000guardrails: public API split from the service layerguardrailspackage forGuardrailsService.UiPathback through the platform package, producing_uipath ⇄ guardrails._guardrails_servicepackage.guardrailspackage sits above the facade and retains its public exports._uipathnow depends only on_guardrails_service.guardrails → _uipath → _guardrails_service → common, plusguardrails → _guardrails_service.flowchart TB subgraph GBEFORE["❌ Before — cycle: _uipath ⇄ guardrails"] direction TB GB_U["_uipath.py (UiPath facade)"] GB_G["guardrails<br/>(service, models, decorators)"] GB_U -- "imports GuardrailsService" --> GB_G GB_G -. "run() lazily imports UiPath ⚠️" .-> GB_U end subgraph GAFTER["✅ After — split public API from service layer"] direction TB GA_G["guardrails<br/>(public API + decorators, top layer)"] GA_U["_uipath.py (UiPath facade)"] GA_INTERNAL["_guardrails_service<br/>(service + platform models)"] GA_COMMON["common"] GA_G -- "validators lazily create UiPath" --> GA_U GA_G -- "re-exports service/models" --> GA_INTERNAL GA_U -- "constructs GuardrailsService" --> GA_INTERNAL GA_INTERNAL --> GA_COMMON end GBEFORE ~~~ GAFTER style GB_G fill:#ffdddd,stroke:#cc0000,color:#000 style GA_INTERNAL fill:#ddffdd,stroke:#00aa00,color:#000Compatibility and migration
uipath.platform.resume_triggers.uipath.platform.commonanduipath.platform.common.interrupt_modelsremain as deprecated lazy aliases and resolve to the canonical objects.uipath.platform.guardrails;NumberParameterValuealso remains temporarily available from its existinguipath.platform.guardrails.guardrailspath.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
_guardrails_servicedoes not load the publicguardrailspackage.