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
github-actions Bot pushed a commit to IntelliTect/CodingGuidelines that referenced this pull request Jul 20, 2026
Updated [TUnit.Core](https://github.com/thomhurst/TUnit) from 1.59.0 to
1.61.15.

<details>
<summary>Release notes</summary>

_Sourced from [TUnit.Core's
releases](https://github.com/thomhurst/TUnit/releases)._

## 1.61.15

<!-- Release notes generated using configuration in .github/release.yml
at v1.61.15 -->

## What's Changed
### Other Changes
* feat: Allow disabling the automatic html report upload to
upload-artifacts@​v7 by @​Crashdummyy in
thomhurst/TUnit#6447
* fix: stop retrying artifact upload on non-retryable status codes by
@​thomhurst in thomhurst/TUnit#6449
### Dependencies
* chore(deps): update tunit to 1.61.0 by @​thomhurst in
thomhurst/TUnit#6437
* chore(deps): update verify to 31.24.3 by @​thomhurst in
thomhurst/TUnit#6438
* chore(deps): update dependency
opentelemetry.instrumentation.aspnetcore to 1.17.0 by @​thomhurst in
thomhurst/TUnit#6440
* chore(deps): update verify to 31.25.0 by @​thomhurst in
thomhurst/TUnit#6442
* chore(deps): update dependency docusaurus-plugin-llms to ^0.5.0 by
@​thomhurst in thomhurst/TUnit#6443
* chore(deps): update opentelemetry to 1.17.0 by @​thomhurst in
thomhurst/TUnit#6441

## New Contributors
* @​Crashdummyy made their first contribution in
thomhurst/TUnit#6447

**Full Changelog**:
thomhurst/TUnit@v1.61.0...v1.61.15

## 1.61.0

<!-- Release notes generated using configuration in .github/release.yml
at v1.61.0 -->

## What's Changed
### Other Changes
* docs: revamp README with feature showcase and auto-updating benchmarks
by @​thomhurst in thomhurst/TUnit#6430
* docs: align docs homepage messaging with revamped README by
@​thomhurst in thomhurst/TUnit#6431
* fix(engine): emit ECMA-335 metadata-format type names in
TestMethodIdentifierProperty by @​thomhurst in
thomhurst/TUnit#6433
* fix(benchmarks): make SetupTeardownTests JSON tests Native AOT
compatible by @​thomhurst in
thomhurst/TUnit#6434
* Organize repository into conventional .NET layout by @​thomhurst in
thomhurst/TUnit#6435
### Dependencies
* chore(deps): update dependency polyfill to v11 by @​thomhurst in
thomhurst/TUnit#6418
* chore(deps): update dependency tunit.aspire to 1.6* by @​thomhurst in
thomhurst/TUnit#6419
* chore(deps): update tunit to 1.6* by @​thomhurst in
thomhurst/TUnit#6420
* chore(deps): update dependency polyfill to 11.0.1 by @​thomhurst in
thomhurst/TUnit#6421
* chore(deps): update dependency polyfill to 11.0.1 by @​thomhurst in
thomhurst/TUnit#6422
* chore(deps): update verify to 31.24.2 by @​thomhurst in
thomhurst/TUnit#6424
* chore(deps): update microsoft.build to 18.8.2 by @​thomhurst in
thomhurst/TUnit#6426
* chore(deps): update dependency microsoft.net.stringtools to 18.8.2 by
@​thomhurst in thomhurst/TUnit#6425
* chore(deps): update actions/setup-dotnet action to v6 by @​thomhurst
in thomhurst/TUnit#6427
* chore(deps): bump websocket-driver from 0.7.4 to 0.7.5 in /docs by
@​dependabot[bot] in thomhurst/TUnit#6429
* chore(deps): update opentelemetry to 1.17.0 by @​thomhurst in
thomhurst/TUnit#6432


**Full Changelog**:
thomhurst/TUnit@v1.60.0...v1.61.0

## 1.60.0

<!-- Release notes generated using configuration in .github/release.yml
at v1.60.0 -->

## What's Changed
### Other Changes
* feat: cross-process test report aggregation — merged HTML report +
single GitHub summary by @​thomhurst in
thomhurst/TUnit#6415
### Dependencies
* chore(deps): update tunit to 1.59.0 by @​thomhurst in
thomhurst/TUnit#6398
* chore(deps): update verify to 31.24.1 by @​thomhurst in
thomhurst/TUnit#6399
* chore(deps): update microsoft.testing to 2.3.2 by @​thomhurst in
thomhurst/TUnit#6401
* chore(deps): update mstest to 4.3.2 by @​thomhurst in
thomhurst/TUnit#6402
* chore(deps): update actions/setup-node action to v7 by @​thomhurst in
thomhurst/TUnit#6403
* chore(deps): update dependency microsoft.net.test.sdk to 18.8.0 by
@​thomhurst in thomhurst/TUnit#6404
* chore(deps): update dependency microsoft.net.test.sdk to 18.8.1 by
@​thomhurst in thomhurst/TUnit#6405
* chore(deps): update dependency fsharp.core to 10.1.302 by @​thomhurst
in thomhurst/TUnit#6406
* chore(deps): update dependency microsoft.templateengine.authoring.cli
to v10.0.302 by @​thomhurst in
thomhurst/TUnit#6407
* chore(deps): update dependency microsoft.entityframeworkcore to
10.0.10 by @​thomhurst in thomhurst/TUnit#6410
* chore(deps): update dependency dotnet-sdk to v10.0.302 by @​thomhurst
in thomhurst/TUnit#6409
* chore(deps): update dependency system.commandline to 2.0.10 by
@​thomhurst in thomhurst/TUnit#6412
* chore(deps): update dependency
microsoft.templateengine.authoring.templateverifier to 10.0.302 by
@​thomhurst in thomhurst/TUnit#6411
* chore(deps): update microsoft.aspnetcore to 10.0.10 by @​thomhurst in
thomhurst/TUnit#6413
* chore(deps): update microsoft.extensions to 10.0.10 by @​thomhurst in
thomhurst/TUnit#6414
* chore(deps): update microsoft.extensions to 10.8.0 by @​thomhurst in
thomhurst/TUnit#6416
* chore(deps): update dependency polyfill to v11 by @​thomhurst in
thomhurst/TUnit#6417


**Full Changelog**:
thomhurst/TUnit@v1.59.0...v1.60.0

Commits viewable in [compare
view](thomhurst/TUnit@v1.59.0...v1.61.15).
</details>

[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=TUnit.Core&package-manager=nuget&previous-version=1.59.0&new-version=1.61.15)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
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