From b768575ddcd6e6158ba75693805c5cf98d9eaa77 Mon Sep 17 00:00:00 2001 From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com> Date: Wed, 11 Mar 2026 01:27:01 +0000 Subject: [PATCH 1/3] fix: use unique artifact names to avoid collisions in matrix builds Append a short job backend ID suffix to the artifact name so each matrix job produces a distinct artifact. Also track which artifact name was accepted by CreateArtifact and use it in FinalizeArtifact, fixing a bug where a conflict-deduped name was not carried forward. --- .../Reporters/Html/GitHubArtifactUploader.cs | 29 +++++++++++++++++-- 1 file changed, 26 insertions(+), 3 deletions(-) diff --git a/TUnit.Engine/Reporters/Html/GitHubArtifactUploader.cs b/TUnit.Engine/Reporters/Html/GitHubArtifactUploader.cs index 6b60c2e37f..e56fd9b7d0 100644 --- a/TUnit.Engine/Reporters/Html/GitHubArtifactUploader.cs +++ b/TUnit.Engine/Reporters/Html/GitHubArtifactUploader.cs @@ -31,15 +31,19 @@ internal static class GitHubArtifactUploader var origin = new Uri(resultsUrl).GetLeftPart(UriPartial.Authority); var fileName = Path.GetFileName(filePath); + // Build a unique artifact name using the job backend ID to avoid collisions in matrix builds + var baseFileName = BuildUniqueArtifactName(fileName, workflowJobRunBackendId); + // Step 1: CreateArtifact (deduplicate name on 409 conflict) var createUrl = $"{origin}/twirp/github.actions.results.api.v1.ArtifactService/CreateArtifact"; string? signedUploadUrl = null; + string? acceptedArtifactName = null; for (var nameAttempt = 0; nameAttempt < 3 && signedUploadUrl is null; nameAttempt++) { var artifactName = nameAttempt == 0 - ? fileName - : $"{Path.GetFileNameWithoutExtension(fileName)}-{nameAttempt + 1}{Path.GetExtension(fileName)}"; + ? baseFileName + : $"{Path.GetFileNameWithoutExtension(baseFileName)}-{nameAttempt + 1}{Path.GetExtension(baseFileName)}"; var createBody = BuildCreateArtifactJson(workflowRunBackendId, workflowJobRunBackendId, artifactName); @@ -66,6 +70,11 @@ internal static class GitHubArtifactUploader using var doc = JsonDocument.Parse(json); return doc.RootElement.GetProperty("signed_upload_url").GetString(); }, cancellationToken); + + if (signedUploadUrl is not null) + { + acceptedArtifactName = artifactName; + } } if (signedUploadUrl is null) @@ -105,7 +114,7 @@ internal static class GitHubArtifactUploader // Step 3: FinalizeArtifact var finalizeUrl = $"{origin}/twirp/github.actions.results.api.v1.ArtifactService/FinalizeArtifact"; - var finalizeBody = BuildFinalizeArtifactJson(workflowRunBackendId, workflowJobRunBackendId, fileName, fileBytes.Length, sha256Hash); + var finalizeBody = BuildFinalizeArtifactJson(workflowRunBackendId, workflowJobRunBackendId, acceptedArtifactName!, fileBytes.Length, sha256Hash); var artifactId = await RetryAsync(async () => { @@ -127,6 +136,20 @@ internal static class GitHubArtifactUploader return artifactId; } + private static string BuildUniqueArtifactName(string fileName, string jobRunBackendId) + { + // Use the first 8 characters of the job backend ID as a short unique suffix. + // This ensures each matrix job gets a distinct artifact name without relying + // on environment variables that may not differentiate matrix combinations. + var shortId = jobRunBackendId.Length > 8 + ? jobRunBackendId.Substring(0, 8) + : jobRunBackendId; + + var nameWithoutExt = Path.GetFileNameWithoutExtension(fileName); + var ext = Path.GetExtension(fileName); + return $"{nameWithoutExt}-{shortId}{ext}"; + } + private static string BuildCreateArtifactJson(string runId, string jobId, string fileName) { using var ms = new MemoryStream(); From 709df332d7ffa97f90339c26fcc0dc38291cd950 Mon Sep 17 00:00:00 2001 From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com> Date: Wed, 11 Mar 2026 01:37:58 +0000 Subject: [PATCH 2/3] refactor: only append job ID suffix on 409 conflict Keep the original artifact name for the first attempt so non-matrix builds get a clean name. Only fall back to the job backend ID suffix when a conflict is detected. --- .../Reporters/Html/GitHubArtifactUploader.cs | 27 +++++++++---------- 1 file changed, 12 insertions(+), 15 deletions(-) diff --git a/TUnit.Engine/Reporters/Html/GitHubArtifactUploader.cs b/TUnit.Engine/Reporters/Html/GitHubArtifactUploader.cs index e56fd9b7d0..34ece53b42 100644 --- a/TUnit.Engine/Reporters/Html/GitHubArtifactUploader.cs +++ b/TUnit.Engine/Reporters/Html/GitHubArtifactUploader.cs @@ -31,19 +31,23 @@ internal static class GitHubArtifactUploader var origin = new Uri(resultsUrl).GetLeftPart(UriPartial.Authority); var fileName = Path.GetFileName(filePath); - // Build a unique artifact name using the job backend ID to avoid collisions in matrix builds - var baseFileName = BuildUniqueArtifactName(fileName, workflowJobRunBackendId); - // Step 1: CreateArtifact (deduplicate name on 409 conflict) var createUrl = $"{origin}/twirp/github.actions.results.api.v1.ArtifactService/CreateArtifact"; string? signedUploadUrl = null; string? acceptedArtifactName = null; + var nameWithoutExt = Path.GetFileNameWithoutExtension(fileName); + var ext = Path.GetExtension(fileName); for (var nameAttempt = 0; nameAttempt < 3 && signedUploadUrl is null; nameAttempt++) { - var artifactName = nameAttempt == 0 - ? baseFileName - : $"{Path.GetFileNameWithoutExtension(baseFileName)}-{nameAttempt + 1}{Path.GetExtension(baseFileName)}"; + var artifactName = nameAttempt switch + { + 0 => fileName, + // On first conflict, append the job backend ID to uniquely identify this matrix job + 1 => $"{nameWithoutExt}-{GetShortJobId(workflowJobRunBackendId)}{ext}", + // On further conflicts, add an extra numeric suffix + _ => $"{nameWithoutExt}-{GetShortJobId(workflowJobRunBackendId)}-{nameAttempt}{ext}", + }; var createBody = BuildCreateArtifactJson(workflowRunBackendId, workflowJobRunBackendId, artifactName); @@ -136,18 +140,11 @@ internal static class GitHubArtifactUploader return artifactId; } - private static string BuildUniqueArtifactName(string fileName, string jobRunBackendId) + private static string GetShortJobId(string jobRunBackendId) { - // Use the first 8 characters of the job backend ID as a short unique suffix. - // This ensures each matrix job gets a distinct artifact name without relying - // on environment variables that may not differentiate matrix combinations. - var shortId = jobRunBackendId.Length > 8 + return jobRunBackendId.Length > 8 ? jobRunBackendId.Substring(0, 8) : jobRunBackendId; - - var nameWithoutExt = Path.GetFileNameWithoutExtension(fileName); - var ext = Path.GetExtension(fileName); - return $"{nameWithoutExt}-{shortId}{ext}"; } private static string BuildCreateArtifactJson(string runId, string jobId, string fileName) From 61a49303c7c4e4ed75ab1c211eb5ac64f18df9ba Mon Sep 17 00:00:00 2001 From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com> Date: Wed, 11 Mar 2026 01:38:30 +0000 Subject: [PATCH 3/3] style: use range syntax instead of Substring --- TUnit.Engine/Reporters/Html/GitHubArtifactUploader.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/TUnit.Engine/Reporters/Html/GitHubArtifactUploader.cs b/TUnit.Engine/Reporters/Html/GitHubArtifactUploader.cs index 34ece53b42..a06af4f6ef 100644 --- a/TUnit.Engine/Reporters/Html/GitHubArtifactUploader.cs +++ b/TUnit.Engine/Reporters/Html/GitHubArtifactUploader.cs @@ -143,7 +143,7 @@ internal static class GitHubArtifactUploader private static string GetShortJobId(string jobRunBackendId) { return jobRunBackendId.Length > 8 - ? jobRunBackendId.Substring(0, 8) + ? jobRunBackendId[..8] : jobRunBackendId; }