diff --git a/.github/workflows/unity-xref-maps.yml b/.github/workflows/unity-xref-maps.yml index 0fd864f..5fc699a 100644 --- a/.github/workflows/unity-xref-maps.yml +++ b/.github/workflows/unity-xref-maps.yml @@ -94,7 +94,7 @@ jobs: shell: pwsh id: set-unity-releases-versions run: | - $url = 'https://services.api.unity.com/unity/editor/release/v1/releases?' + ` + $baseUrl = 'https://services.api.unity.com/unity/editor/release/v1/releases?' + ` 'architecture=X86_64&' + ` 'platform=WINDOWS&' + ` 'limit=25&' + ` @@ -102,9 +102,17 @@ jobs: 'stream=SUPPORTED&' + ` 'stream=TECH' - $response = Invoke-RestMethod $url + $allResults = @() + $offset = 0 - $versions = $response.results + do + { + $response = Invoke-RestMethod "$baseUrl&offset=$offset" + $allResults += $response.results + $offset += 25 + } while ($response.results.Count -gt 0) + + $versions = $allResults | Select-Object -ExpandProperty version | ForEach-Object { $match = [regex]::Match($_, '(?(?(\d+)\.(\d+))\.(\d+))') @@ -214,6 +222,8 @@ jobs: --docFxConfigurationFilePath '${{ runner.temp }}/Work/docfx.json' ` --xrefMapsPath "${{ runner.temp }}/_site/$($version.ShortVersion)/xrefmap.yml" + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + - name: Upload artifact uses: actions/upload-artifact@v5 with: diff --git a/UnityXrefMaps.Tests/UtilsTests.cs b/UnityXrefMaps.Tests/UtilsTests.cs new file mode 100644 index 0000000..88b78dc --- /dev/null +++ b/UnityXrefMaps.Tests/UtilsTests.cs @@ -0,0 +1,24 @@ +using System.Threading.Tasks; + +namespace UnityXrefMaps.Tests; + +public class UtilsTests +{ + [Fact] + public async Task RunCommand_SuccessfulProcess_ReturnsZero() + { + // `true` always exits with code 0. + // RunCommand must surface the exit code so callers can detect failures. + int exitCode = await Utils.RunCommand("true", "", _ => { }, _ => { }); + Assert.Equal(0, exitCode); + } + + [Fact] + public async Task RunCommand_FailingProcess_ReturnsNonZero() + { + // `false` always exits with code 1. + // Before the fix, RunCommand returned void so exit codes were silently discarded. + int exitCode = await Utils.RunCommand("false", "", _ => { }, _ => { }); + Assert.Equal(1, exitCode); + } +} diff --git a/UnityXrefMaps.Tests/XrefMapServiceTests.cs b/UnityXrefMaps.Tests/XrefMapServiceTests.cs new file mode 100644 index 0000000..396371e --- /dev/null +++ b/UnityXrefMaps.Tests/XrefMapServiceTests.cs @@ -0,0 +1,68 @@ +using System.IO; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging.Abstractions; + +namespace UnityXrefMaps.Tests; + +public class XrefMapServiceTests +{ + private readonly XrefMapService _service = new(NullLogger.Instance); + + [Fact] + public async Task Load_ZeroColonAtWordBoundary_IsStripped() + { + // `0: value` in a YAML string value causes the parser to treat it as a nested + // mapping, crashing with a type-mismatch. The regex must strip `0:` before parsing. + string yaml = """ + sorted: false + references: + - uid: SomeType + name: SomeType + href: SomeType.html + commentId: T:SomeType + nameWithType: 0: orphan + """; + + string filePath = Path.GetTempFileName(); + try + { + await File.WriteAllTextAsync(filePath, yaml); + XrefMap result = await _service.Load(filePath); + Assert.NotNull(result); + Assert.Single(result.References!); + } + finally + { + File.Delete(filePath); + } + } + + [Fact] + public async Task Load_NonZeroDigitColon_IsPreserved() + { + // The old regex `(\d):` stripped the colon from every digit-colon sequence, corrupting + // legitimate values such as `data1:field` → `data1field` in href or nameWithType fields. + // Only `0:` at a word boundary should be stripped. + string yaml = """ + sorted: false + references: + - uid: SomeType + name: SomeType + href: SomeType.html + commentId: T:SomeType + nameWithType: data1:field + """; + + string filePath = Path.GetTempFileName(); + try + { + await File.WriteAllTextAsync(filePath, yaml); + XrefMap result = await _service.Load(filePath); + Assert.Equal("data1:field", result.References![0].NameWithType); + } + finally + { + File.Delete(filePath); + } + } +} diff --git a/UnityXrefMaps/Commands/BuildCommand.cs b/UnityXrefMaps/Commands/BuildCommand.cs index ee73ba6..a32520d 100644 --- a/UnityXrefMaps/Commands/BuildCommand.cs +++ b/UnityXrefMaps/Commands/BuildCommand.cs @@ -106,8 +106,8 @@ public BuildCommand(ILoggerFactory loggerFactory) string docFxFileDirectoryPath = Path.GetDirectoryName(docFxFilePath)!; string generatedDocsPath = Path.Combine(docFxFileDirectoryPath, docFxConfiguration!.Build!.Destination!); - string generatedXrefMapPath = Path.Combine(generatedDocsPath, Constants.DefaultXrefMapFileName!); - + string generatedXrefMapPath = Path.Combine(generatedDocsPath, Constants.DefaultXrefMapFileName!); + ILogger logger = loggerFactory.CreateLogger(); if (logger.IsEnabled(LogLevel.Information)) @@ -124,9 +124,9 @@ public BuildCommand(ILoggerFactory loggerFactory) docFxArguments += ' ' + docFxAdditionalArguments; } - if (repositoryTags == null || repositoryTags.Length == 0) + if (repositoryTags == null || repositoryTags.Length == 0) { - repositoryTags = [.. repository.GetLatestVersions().Select(v => v.release)]; + repositoryTags = [.. repository.GetLatestVersions().Select(v => v.release)]; } foreach (string repositoryTag in repositoryTags) @@ -149,7 +149,7 @@ public BuildCommand(ILoggerFactory loggerFactory) logger.LogInformation("Running DocFX on '{RepositoryTag}'", repositoryTag); } - await Utils.RunCommand( + int exitCode = await Utils.RunCommand( "docfx", docFxArguments, value => { @@ -173,6 +173,18 @@ await Utils.RunCommand( }, cancellationToken); + if (exitCode != 0) + { + result = false; + + if (logger.IsEnabled(LogLevel.Error)) + { + logger.LogError("DocFX exited with code {ExitCode} for Unity '{RepositoryTag}'", exitCode, repositoryTag); + } + + continue; + } + if (!File.Exists(generatedXrefMapPath)) { result = false; @@ -192,8 +204,8 @@ await Utils.RunCommand( await Utils.CopyFile(generatedXrefMapPath, xrefMapPath, cancellationToken); - XrefMap xrefMap = await xrefMapService.Load(xrefMapPath, cancellationToken); - + XrefMap xrefMap = await xrefMapService.Load(xrefMapPath, cancellationToken); + xrefMap.References = [.. xrefMapService.Process(string.Format(apiUrl!, shortVersion), xrefMap.References!, trimNamespaces!, isPackage)]; await xrefMapService.Save(xrefMapPath, xrefMap, cancellationToken); diff --git a/UnityXrefMaps/Utils.cs b/UnityXrefMaps/Utils.cs index 8b88a27..6e49723 100644 --- a/UnityXrefMaps/Utils.cs +++ b/UnityXrefMaps/Utils.cs @@ -39,7 +39,7 @@ public static async Task CopyFile(string sourcePath, string destPath, Cancellati /// The arguments of the command. /// The function to call with the output data of the command. /// The function to call with the error data of the command. - public static async Task RunCommand(string command, string arguments, Action output, Action error, CancellationToken cancellationToken = default) + public static async Task RunCommand(string command, string arguments, Action output, Action error, CancellationToken cancellationToken = default) { using var process = new Process(); process.StartInfo = new ProcessStartInfo(command, arguments) @@ -58,6 +58,8 @@ public static async Task RunCommand(string command, string arguments, Action diff --git a/UnityXrefMaps/XrefMapService.cs b/UnityXrefMaps/XrefMapService.cs index 8aeacf1..9a09b34 100644 --- a/UnityXrefMaps/XrefMapService.cs +++ b/UnityXrefMaps/XrefMapService.cs @@ -31,7 +31,7 @@ public async Task Load(string filePath, CancellationToken cancellationT string xrefMapText = await File.ReadAllTextAsync(filePath, cancellationToken); // Remove `0:` strings on the xrefmap that make crash Deserializer - xrefMapText = ZeroStringsRegex().Replace(xrefMapText, "$1"); + xrefMapText = ZeroStringsRegex().Replace(xrefMapText, "0"); return _deserializer.Deserialize(xrefMapText); } @@ -77,7 +77,7 @@ public async Task Save(string filePath, XrefMap xrefMap, CancellationToken cance await File.WriteAllTextAsync(filePath, xrefMapText, cancellationToken); } - [GeneratedRegex(@"(\d):")] + [GeneratedRegex(@"\b0:")] private static partial Regex ZeroStringsRegex(); } }