Description:
On self-hosted runners without outbound internet access, actions/setup-dotnet fails to set up a .NET SDK when a floating version (e.g. 8.0.x, or a bare channel like 8.0) is requested, even when a satisfying SDK is already installed on the runner. Two separate steps in the install path make outbound requests before any check of what is already present locally:
-
Unconditional runtime pre-install. DotnetCoreInstaller.installDotnet() always runs the install script with --runtime dotnet --channel LTS first ("to get the latest stable version of the dotnet CLI"). This resolves Runtime/LTS/latest.version from builds.dotnet.microsoft.com (falling back to ci.dot.net) on every run, regardless of the requested version. On an air-gapped runner this fails immediately.
-
Online latest.version resolution for floating/channel versions. A floating input such as 8.0.x is turned into a channel argument (8.0) rather than a concrete version. The install script then fetches Sdk/8.0/latest.version over the network to resolve the exact patch, before it checks whether a matching SDK is already on disk. On an air-gapped runner this also fails.
Representative failing log (floating versions 8.0.x/9.0.x/10.0.x, no outbound access):
install-dotnet.sh --skip-non-versioned-files --runtime dotnet --channel LTS
dotnet-install: Download attempt #1 has failed: Unable to download https://builds.dotnet.microsoft.com/dotnet/Runtime/LTS/latest.version.
... (retries, then falls back to https://ci.dot.net/public/Runtime/LTS/latest.version) ...
Warning: Failed to install dotnet runtime + cli, exit code: 1. dotnet_install: Error: Failed to resolve the exact version number.
install-dotnet.sh --skip-non-versioned-files --channel 8.0
dotnet-install: Download attempt #1 has failed: Unable to download https://builds.dotnet.microsoft.com/dotnet/Sdk/8.0/latest.version.
... (retries, then falls back to https://ci.dot.net/public/Sdk/8.0/latest.version) ...
Error: Failed to install dotnet, exit code: 1. dotnet_install: Error: Failed to resolve the exact version number.
Clarifying the "it already reuses the cache" behavior
When a fully pinned version (e.g. 8.0.422) is requested, the input becomes a concrete --version argument, no latest.version fetch is needed for the SDK, and the install script reports .NET Core SDK with version 'X' is already installed when it finds it on disk. This is why pinned-version runs succeed and appear to "reuse" an existing install.
However, note two things:
- This reuse is the install script detecting an SDK already present at the install directory —
setup-dotnet does not consult the GitHub Actions tool cache (@actions/tool-cache) in its install path. So "preload the tool cache" (as works for actions/setup-java) does not apply here.
- Even for a pinned version, the unconditional runtime pre-install (point 1) still makes an outbound
latest.version request, which fails on a fully air-gapped runner.
For floating/channel versions, the online resolution (point 2) happens before the on-disk check, so an already-satisfying local SDK is never given the chance to short-circuit the network call.
Justification:
This blocks any workflow that runs setup-dotnet with floating versions on air-gapped self-hosted runners. Notably, GitHub's Automatic Dependency Submission (ADS) for NuGet uses floating versions (8.0.x, 9.0.x, 10.0.x) and therefore always takes the online-resolution path, so it cannot be made to work offline by preinstalling SDKs or preloading the tool cache. This came up via a customer support escalation; the report is intentionally scoped to the general capability and contains no customer- or environment-specific details.
For comparison, actions/setup-java is usable in preloaded/offline scenarios because a satisfying local toolchain short-circuits the download.
Proposed behavior:
- Before performing online version resolution for a floating/channel request, check whether an already-installed SDK under the install directory (
DOTNET_ROOT / DotnetInstallDir) satisfies the requested version; if so, use it and skip the network call.
- Make the runtime pre-install (point 1) tolerant of a runner with no outbound access — e.g. skip it when a suitable CLI/runtime is already present, or don't hard-require the online
latest.version resolution for it.
- (Secondary) Provide a supported way to point resolution/download at an internal mirror/feed (the install scripts already support an Azure feed override), so environments that maintain a mirror can resolve floating versions offline.
Are you willing to submit a PR?
Yes — happy to collaborate on this if the maintainers agree on the approach.
Description:
On self-hosted runners without outbound internet access,
actions/setup-dotnetfails to set up a .NET SDK when a floating version (e.g.8.0.x, or a bare channel like8.0) is requested, even when a satisfying SDK is already installed on the runner. Two separate steps in the install path make outbound requests before any check of what is already present locally:Unconditional runtime pre-install.
DotnetCoreInstaller.installDotnet()always runs the install script with--runtime dotnet --channel LTSfirst ("to get the latest stable version of the dotnet CLI"). This resolvesRuntime/LTS/latest.versionfrombuilds.dotnet.microsoft.com(falling back toci.dot.net) on every run, regardless of the requested version. On an air-gapped runner this fails immediately.src/installer.ts#L410-L417Online
latest.versionresolution for floating/channel versions. A floating input such as8.0.xis turned into a channel argument (8.0) rather than a concrete version. The install script then fetchesSdk/8.0/latest.versionover the network to resolve the exact patch, before it checks whether a matching SDK is already on disk. On an air-gapped runner this also fails.src/installer.ts#L120-L143externals/install-dotnet.shget_version_from_latestversion_fileRepresentative failing log (floating versions
8.0.x/9.0.x/10.0.x, no outbound access):Clarifying the "it already reuses the cache" behavior
When a fully pinned version (e.g.
8.0.422) is requested, the input becomes a concrete--versionargument, nolatest.versionfetch is needed for the SDK, and the install script reports.NET Core SDK with version 'X' is already installedwhen it finds it on disk. This is why pinned-version runs succeed and appear to "reuse" an existing install.However, note two things:
setup-dotnetdoes not consult the GitHub Actions tool cache (@actions/tool-cache) in its install path. So "preload the tool cache" (as works foractions/setup-java) does not apply here.latest.versionrequest, which fails on a fully air-gapped runner.For floating/channel versions, the online resolution (point 2) happens before the on-disk check, so an already-satisfying local SDK is never given the chance to short-circuit the network call.
Justification:
This blocks any workflow that runs
setup-dotnetwith floating versions on air-gapped self-hosted runners. Notably, GitHub's Automatic Dependency Submission (ADS) for NuGet uses floating versions (8.0.x,9.0.x,10.0.x) and therefore always takes the online-resolution path, so it cannot be made to work offline by preinstalling SDKs or preloading the tool cache. This came up via a customer support escalation; the report is intentionally scoped to the general capability and contains no customer- or environment-specific details.For comparison,
actions/setup-javais usable in preloaded/offline scenarios because a satisfying local toolchain short-circuits the download.Proposed behavior:
DOTNET_ROOT/DotnetInstallDir) satisfies the requested version; if so, use it and skip the network call.latest.versionresolution for it.Are you willing to submit a PR?
Yes — happy to collaborate on this if the maintainers agree on the approach.