chore(validation): enforce package graph boundaries

Reject internal package version drift, dependency cycles, and runtime-to-integration reverse dependencies before launching the external UPM consumer validation.
This commit is contained in:
2026-08-26 01:18:02 +08:00
parent 724e0bc8d8
commit 3e612b975a
2 changed files with 69 additions and 8 deletions
+12 -3
View File
@@ -1,14 +1,17 @@
# UPM Consumer Validation
`Validate-UpmConsumer.ps1` verifies the package graph from outside the main Unity
project. It performs three checks:
project. It performs five checks:
1. Every dependency on another local `com.cneicy.*` package must use that
package's current `package.json` version.
2. A temporary Unity project is created under `Temp/UpmConsumerValidation`, all
2. The local package graph must be acyclic.
3. A normal package must not depend on a `*-integration-*` package. Integration
packages may depend on each other, and starter packages may aggregate them.
4. A temporary Unity project is created under `Temp/UpmConsumerValidation`, all
local packages are installed through `file:` UPM dependencies, package tests
are enabled, and Unity must load every package and asmdef.
3. The same temporary UPM consumer project runs all package EditMode tests.
5. The same temporary UPM consumer project runs all package EditMode tests.
Run with the Unity version declared by this repository:
@@ -16,6 +19,12 @@ Run with the Unity version declared by this repository:
./Tools/UpmConsumerValidation/Validate-UpmConsumer.ps1
```
Run only the package graph checks without starting Unity:
```powershell
./Tools/UpmConsumerValidation/Validate-UpmConsumer.ps1 -GraphOnly
```
Use `-UnityEditorPath` when the matching editor is not installed in a standard
location. Pass `-KeepProject` to retain the temporary project, reports, and Unity logs for
investigation. `-SkipTests` is only for a quick package-resolution diagnostic and is not the
@@ -3,7 +3,8 @@ param(
[string]$UnityEditorPath,
[string]$ProjectRoot,
[switch]$KeepProject,
[switch]$SkipTests
[switch]$SkipTests,
[switch]$GraphOnly
)
$ErrorActionPreference = 'Stop'
@@ -101,8 +102,12 @@ foreach ($record in $packageRecords) {
$packageByName[$record.Name] = $record
}
$versionErrors = @()
$graphErrors = @()
$localDependencies = @{}
foreach ($record in $packageRecords) {
$dependencies = [System.Collections.Generic.HashSet[string]]::new(
[System.StringComparer]::Ordinal)
$localDependencies[$record.Name] = $dependencies
if (-not $record.Manifest.dependencies) {
continue
}
@@ -112,14 +117,60 @@ foreach ($record in $packageRecords) {
continue
}
[void]$dependencies.Add($dependency.Name)
$actualVersion = $packageByName[$dependency.Name].Version
if ([string]$dependency.Value -ne $actualVersion) {
$versionErrors += "$($record.Name) requires $($dependency.Name) $($dependency.Value), local version is $actualVersion"
$graphErrors += "$($record.Name) requires $($dependency.Name) $($dependency.Value), local version is $actualVersion"
}
$dependsOnIntegration = $dependency.Name -like '*-integration-*'
$isIntegrationOrStarter =
$record.Name -like '*-integration-*' -or
$record.Name -like '*-starter-*'
if ($dependsOnIntegration -and -not $isIntegrationOrStarter) {
$graphErrors += "$($record.Name) must not depend on integration package $($dependency.Name)"
}
}
}
if ($versionErrors.Count -gt 0) {
throw "Internal package versions are inconsistent:`n$($versionErrors -join "`n")"
if ($graphErrors.Count -gt 0) {
throw "Internal package graph is invalid:`n$($graphErrors -join "`n")"
}
$resolvedPackages = [System.Collections.Generic.HashSet[string]]::new(
[System.StringComparer]::Ordinal)
do {
$madeProgress = $false
foreach ($packageName in $localDependencies.Keys | Sort-Object) {
if ($resolvedPackages.Contains($packageName)) {
continue
}
$unresolvedDependencies = @($localDependencies[$packageName] |
Where-Object { -not $resolvedPackages.Contains($_) })
if ($unresolvedDependencies.Count -eq 0) {
[void]$resolvedPackages.Add($packageName)
$madeProgress = $true
}
}
} while ($madeProgress)
if ($resolvedPackages.Count -ne $packageRecords.Count) {
$cycleDetails = $localDependencies.Keys |
Where-Object { -not $resolvedPackages.Contains($_) } |
Sort-Object |
ForEach-Object {
$packageName = $_
$blockedBy = @($localDependencies[$packageName] |
Where-Object { -not $resolvedPackages.Contains($_) } |
Sort-Object)
"$packageName -> $($blockedBy -join ', ')"
}
throw "Circular internal package dependencies detected:`n$($cycleDetails -join "`n")"
}
Write-Host "Package graph PASS packages=$($packageRecords.Count)"
if ($GraphOnly) {
return
}
if (Test-Path -LiteralPath $consumerRoot) {
@@ -133,6 +184,7 @@ $dependencies = [ordered]@{
'com.unity.test-framework' = '1.1.33'
'com.unity.textmeshpro' = '3.0.7'
'com.unity.ugui' = '1.0.0'
'com.unity.modules.uielements' = '1.0.0'
}
foreach ($record in $packageRecords | Sort-Object Name) {
$dependencies[$record.Name] = Convert-ToFileDependency $record.Directory