Skip to content

fix: stop retrying artifact upload on non-retryable status codes#6449

Merged
thomhurst merged 1 commit into
mainfrom
fix/artifact-upload-non-retryable-codes
Jul 19, 2026
Merged

fix: stop retrying artifact upload on non-retryable status codes#6449
thomhurst merged 1 commit into
mainfrom
fix/artifact-upload-non-retryable-codes

Conversation

@thomhurst

Copy link
Copy Markdown
Owner

Description

Follow-up to discussion #6448 (GitHub Enterprise Server users waiting ~30s for the HTML report artifact upload to fail).

GHES doesn't implement the Actions Results artifact API that actions/upload-artifact@v4+ — and TUnit's in-process uploader — depend on, so CreateArtifact returns 404. That should fail immediately, but on netstandard2.0 it didn't:

// HttpRequestException has no StatusCode on netstandard2.0, so we guessed from the message
if (msg.Contains("401") || msg.Contains("403") || ...) return false;
return true;   // <-- 404 lands here

A 404 was therefore treated as retryable and burned the whole 5-attempt backoff schedule (3s → 4.5s → 6.75s → 10.1s, plus jitter ≈ 25-30s) before surfacing the identical error.

Changes

  • Added ArtifactUploadException, which carries the HTTP status code on every target framework. EnsureSuccessAsync throws it instead of HttpRequestException, removing both the #if NET split and the string heuristic.
  • IsRetryable(int?) is now an allowlist of genuinely transient codes: 408, 429, 500, 502, 503, 504 (408 is new). Everything else — 404, 401, 403, 400, 422 — fails on the first attempt.
  • 404 responses include a hint that the artifact API is unavailable on this host, which is expected on GitHub Enterprise Server.
  • Non-retryable failures still propagate out of UploadAsync, so the CreateArtifact name-dedup loop doesn't re-issue the same doomed request three times. HtmlReporter catches and warns exactly as before.

Complements #6447 rather than replacing it — that flag is still the way to skip the upload entirely; this makes the un-flagged path cost one request instead of five.

Type of Change

  • Bug fix (non-breaking change that fixes an issue)

Testing

14 new IsRetryable cases in TUnit.Engine.Tests/GitHubArtifactUploaderTests.cs covering the transient allowlist, the non-retryable codes (incl. the GHES 404), and the no-status case. 20/20 pass on net10.0.

No source generator or public API surface touched, so no snapshot updates required.

On netstandard2.0 HttpRequestException carries no StatusCode, so
IsRetryable fell back to a message heuristic that only excluded
401/403. A GitHub Enterprise Server 404 (the Actions Results artifact
API does not exist there) therefore burned the full 5-attempt backoff
schedule, adding roughly 25-30s to every run.

Introduce ArtifactUploadException carrying the status code on every
target framework, so retry decisions are code-driven rather than
string-driven. IsRetryable is now an allowlist of transient codes
(408, 429, 500, 502, 503, 504); everything else fails on the first
attempt. 404 responses gain a hint that the artifact API is
unavailable on this host.

Non-retryable failures still propagate out of UploadAsync so the
CreateArtifact name-dedup loop does not re-issue the same doomed
request three times.
@thomhurst
thomhurst enabled auto-merge (squash) July 19, 2026 11:53
@thomhurst
thomhurst merged commit ca3a49d into main Jul 19, 2026
12 checks passed
@thomhurst
thomhurst deleted the fix/artifact-upload-non-retryable-codes branch July 19, 2026 12:09
This was referenced Jul 19, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant