Files
UnityAI/hooks/workflow-check.ps1

312 lines
10 KiB
PowerShell
Raw Normal View History

2026-07-10 17:50:20 +08:00
param(
[string]$UnityProjectRoot = '',
[string]$DocsRoot = ''
)
$ErrorActionPreference = 'Stop'
function Get-FullPath {
param([string]$Path, [string]$BasePath)
if ([string]::IsNullOrWhiteSpace($Path)) {
return $null
}
if (-not [System.IO.Path]::IsPathRooted($Path)) {
$Path = Join-Path $BasePath $Path
}
return [System.IO.Path]::GetFullPath($Path)
}
function Test-UnityProject {
param([string]$Path)
if ([string]::IsNullOrWhiteSpace($Path)) {
return $false
}
return (Test-Path -LiteralPath (Join-Path $Path 'Assets')) -and
(Test-Path -LiteralPath (Join-Path $Path 'ProjectSettings/ProjectVersion.txt'))
}
function Resolve-UnityProject {
param([string]$ExplicitPath, [string]$DocumentationRoot)
if (-not [string]::IsNullOrWhiteSpace($ExplicitPath)) {
$resolved = Get-FullPath $ExplicitPath (Get-Location).Path
if (-not (Test-UnityProject $resolved)) {
throw "Unity project was not found: $resolved"
}
return $resolved
}
if (-not [string]::IsNullOrWhiteSpace($env:UNITY_PROJECT_ROOT)) {
$resolved = Get-FullPath $env:UNITY_PROJECT_ROOT (Get-Location).Path
if (-not (Test-UnityProject $resolved)) {
throw "UNITY_PROJECT_ROOT is not a Unity project: $resolved"
}
return $resolved
}
$current = [System.IO.Path]::GetFullPath((Get-Location).Path)
if (Test-UnityProject $current) {
return $current
}
$parent = Split-Path -Parent $DocumentationRoot
$candidates = @()
if (Test-Path -LiteralPath $parent) {
$directories = Get-ChildItem -LiteralPath $parent -Directory -ErrorAction SilentlyContinue
foreach ($directory in $directories) {
if (Test-UnityProject $directory.FullName) {
$candidates += $directory.FullName
}
}
}
if ($candidates.Count -eq 1) {
return [System.IO.Path]::GetFullPath($candidates[0])
}
if ($candidates.Count -gt 1) {
Write-Warning 'Multiple sibling Unity projects were found. Use -UnityProjectRoot to select one.'
}
return $null
}
function Get-GitRoot {
param([string]$Path)
$result = & git -C $Path rev-parse --show-toplevel 2>$null
if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($result)) {
return $null
}
return [System.IO.Path]::GetFullPath(($result | Select-Object -First 1))
}
function Get-GitChanges {
param([string]$RepositoryRoot, [string]$RepositoryName)
$result = @()
if ([string]::IsNullOrWhiteSpace($RepositoryRoot)) {
return $result
}
$lines = & git -C $RepositoryRoot -c core.quotepath=false status --porcelain=v1 --untracked-files=all
if ($LASTEXITCODE -ne 0) {
throw "Could not read git status: $RepositoryRoot"
}
foreach ($line in $lines) {
if ([string]::IsNullOrWhiteSpace($line) -or $line.Length -lt 4) {
continue
}
$path = $line.Substring(3).Trim()
if ($path.Contains(' -> ')) {
$path = ($path -split ' -> ')[-1]
}
$result += [pscustomobject]@{
Repository = $RepositoryName
Root = $RepositoryRoot
Status = $line.Substring(0, 2)
Path = $path.Replace('\', '/')
}
}
return $result
}
function Show-Changes {
param([string]$Title, [object[]]$Changes)
Write-Output ""
Write-Output "[$Title]"
if ($Changes.Count -eq 0) {
Write-Output ' Clean'
return
}
foreach ($change in $Changes) {
Write-Output (" {0} {1}" -f $change.Status, $change.Path)
}
}
function Get-MarkdownLinkFailures {
param([string]$DocumentationRoot, [object[]]$Changes)
$failures = @()
$markdownChanges = $Changes | Where-Object { $_.Path -like '*.md' -and $_.Status -notmatch 'D' }
foreach ($change in $markdownChanges) {
$fullPath = Join-Path $DocumentationRoot $change.Path
if (-not (Test-Path -LiteralPath $fullPath)) {
continue
}
$content = [System.IO.File]::ReadAllText($fullPath, [System.Text.Encoding]::UTF8)
$matches = [regex]::Matches($content, '!?(?:\[[^\]]*\])\((?<path>[^\)]+)\)')
foreach ($match in $matches) {
$link = $match.Groups['path'].Value.Trim()
if ([string]::IsNullOrWhiteSpace($link) -or $link.StartsWith('#') -or
$link -match '^[a-zA-Z][a-zA-Z0-9+.-]*://' -or $link -match '^mailto:') {
continue
}
if ($link.StartsWith('<') -and $link.EndsWith('>')) {
$link = $link.Substring(1, $link.Length - 2)
}
$pathOnly = ($link -split '#')[0]
if ([string]::IsNullOrWhiteSpace($pathOnly)) {
continue
}
$decoded = [System.Uri]::UnescapeDataString($pathOnly)
$target = Get-FullPath $decoded (Split-Path -Parent $fullPath)
if (-not (Test-Path -LiteralPath $target)) {
$failures += [pscustomobject]@{
Source = $change.Path
Link = $link
}
}
}
}
return $failures
}
$scriptRoot = Split-Path -Parent $MyInvocation.MyCommand.Path
if ([string]::IsNullOrWhiteSpace($DocsRoot)) {
$DocsRoot = Split-Path -Parent $scriptRoot
}
$DocsRoot = Get-FullPath $DocsRoot (Get-Location).Path
$docsGitRoot = Get-GitRoot $DocsRoot
if ([string]::IsNullOrWhiteSpace($docsGitRoot)) {
throw "Documentation git repository was not found: $DocsRoot"
}
$DocsRoot = $docsGitRoot
$resolvedUnityRoot = Resolve-UnityProject $UnityProjectRoot $DocsRoot
$unityGitRoot = $null
if (-not [string]::IsNullOrWhiteSpace($resolvedUnityRoot)) {
$unityGitRoot = Get-GitRoot $resolvedUnityRoot
if ([string]::IsNullOrWhiteSpace($unityGitRoot)) {
throw "Unity project is not a git repository: $resolvedUnityRoot"
}
}
$docsChanges = @(Get-GitChanges $DocsRoot 'UnityAI')
$unityChanges = @()
if (-not [string]::IsNullOrWhiteSpace($unityGitRoot)) {
$unityChanges = @(Get-GitChanges $unityGitRoot (Split-Path -Leaf $resolvedUnityRoot))
}
Write-Output '[workflow-check] Read-only StrayFog workflow check'
Write-Output ("Documentation repository: {0}" -f $DocsRoot)
if ($resolvedUnityRoot) {
Write-Output ("Unity project: {0}" -f $resolvedUnityRoot)
$versionFile = Join-Path $resolvedUnityRoot 'ProjectSettings/ProjectVersion.txt'
$versionLine = Get-Content -LiteralPath $versionFile -Encoding UTF8 |
Where-Object { $_ -match '^m_EditorVersion:' } | Select-Object -First 1
if ($versionLine) {
Write-Output ("Unity version: {0}" -f (($versionLine -split ':', 2)[1].Trim()))
}
}
else {
Write-Warning 'Unity project was not found. Running documentation-only checks.'
}
Show-Changes 'UnityAI changes' $docsChanges
Show-Changes 'Unity project changes' $unityChanges
$unityPaths = @($unityChanges | ForEach-Object { $_.Path })
$copyRelationships = @(
[pscustomobject]@{
Name = 'CopyToHF'
SourcePattern = '^Assets/StrayFog/Editor/CopyToX/CopyToHF/'
TargetPattern = '^Assets/Game/GameHFScripte/CopyToHF/'
GeneratedName = 'HF'
},
[pscustomobject]@{
Name = 'CopyToGeneral'
SourcePattern = '^Assets/StrayFog/Editor/CopyToX/CopyToGeneral/'
TargetPattern = '^Assets/Game/GameGeneralScripte/CopyToGeneral/'
GeneratedName = 'General'
}
)
Write-Output ''
Write-Output '[Source and generation boundary]'
$relationshipDetected = $false
foreach ($relationship in $copyRelationships) {
$hasSource = @($unityPaths | Where-Object {
$_ -match $relationship.SourcePattern
}).Count -gt 0
$hasTarget = @($unityPaths | Where-Object {
$_ -match $relationship.TargetPattern
}).Count -gt 0
if (-not $hasSource -and -not $hasTarget) {
continue
}
$relationshipDetected = $true
if ($hasSource -and -not $hasTarget) {
Write-Warning ("{0} source changed but generated target did not change. Run CopyToX and inspect the generated diff." -f
$relationship.Name)
}
elseif ($hasTarget -and -not $hasSource) {
Write-Warning ("Generated {0} files changed without {1} source changes. Confirm that generated files were not edited directly." -f
$relationship.GeneratedName, $relationship.Name)
}
else {
Write-Output (" Source and generated target changes were both detected for {0}." -f
$relationship.Name)
}
}
if (-not $relationshipDetected) {
Write-Output ' No CopyToX source/target relationship was detected.'
}
$mappingPath = Join-Path $scriptRoot 'workflow-path-mappings.json'
$mapping = Get-Content -LiteralPath $mappingPath -Raw -Encoding UTF8 | ConvertFrom-Json
$documents = @{}
$verifications = @{}
foreach ($path in $unityPaths) {
foreach ($entry in $mapping.mappings) {
if ($path -match $entry.pattern) {
$documents[$entry.document] = $true
$verifications[$entry.verification] = $true
}
}
}
Write-Output ''
Write-Output '[Suggested verification]'
if ($verifications.Count -eq 0) {
Write-Output ' No Unity verification suggestions for the current diff.'
}
else {
foreach ($item in $verifications.Keys | Sort-Object) {
Write-Output (" - {0}" -f $item)
}
}
Write-Output ''
Write-Output '[Documentation impact]'
if ($documents.Count -eq 0) {
Write-Output ' No module document mapping matched the Unity diff.'
}
else {
foreach ($document in $documents.Keys | Sort-Object) {
$changed = @($docsChanges | Where-Object { $_.Path -eq $document }).Count -gt 0
$marker = if ($changed) { 'updated' } else { 'review if long-term behavior changed' }
Write-Output (" - {0} ({1})" -f $document, $marker)
}
}
$linkFailures = @(Get-MarkdownLinkFailures $DocsRoot $docsChanges)
Write-Output ''
Write-Output '[Markdown links]'
if ($linkFailures.Count -eq 0) {
Write-Output ' Changed Markdown links are valid.'
}
else {
foreach ($failure in $linkFailures) {
Write-Error ("Broken link in {0}: {1}" -f $failure.Source, $failure.Link)
}
exit 1
}
Write-Output ''
Write-Output '[workflow-check] Completed without writing files.'
exit 0