Add skip-lts-prepass input to optimize .NET SDK installation process (#791)

- Introduced `skip-lts-prepass` input to allow users to skip the LTS runtime pre-pass during installation.
- Updated documentation to explain the new input and its usage.
- Enhanced tests to verify the behavior of the `skip-lts-prepass` functionality.

Co-authored-by: chiranjib-swain <chiranjib-swain@github.com>
This commit is contained in:
Chiranjib Swain
2026-10-22 03:23:54 +05:30
committed by GitHub
parent e2cf7c663b
commit 349aa191ae
8 changed files with 267 additions and 42 deletions

View File

@@ -769,3 +769,24 @@ jobs:
throw "Expected 9.0.100 to be reused, got '${{ steps.reuse.outputs.dotnet-version }}'." throw "Expected 9.0.100 to be reused, got '${{ steps.reuse.outputs.dotnet-version }}'."
} }
__tests__/verify-dotnet.ps1 -Patterns "^9\.0\.100$" __tests__/verify-dotnet.ps1 -Patterns "^9\.0\.100$"
test-skip-lts-prepass:
runs-on: ${{ matrix.operating-system }}
strategy:
fail-fast: false
matrix:
operating-system: [ubuntu-latest, windows-latest]
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Clear toolcache
shell: pwsh
run: __tests__/clear-toolcache.ps1 ${{ runner.os }}
- name: Setup dotnet 9.0.308 with skip-lts-prepass
uses: ./
with:
dotnet-version: 9.0.308
skip-lts-prepass: true
- name: Verify dotnet
shell: pwsh
run: __tests__/verify-dotnet.ps1 -Patterns "^9\.0\.308$"

View File

@@ -86,6 +86,21 @@ steps:
dotnet-channel: LTS dotnet-channel: LTS
``` ```
## Using the `skip-lts-prepass` input
By default, `setup-dotnet` installs a minimal LTS runtime before installing the requested `dotnet-version`, to guarantee a stable `dotnet` executable is available. If your workflow doesn't need this safeguard (for example, the runner image already has a `dotnet` executable, or you're on a clean self-hosted/container environment and want to avoid the extra download), set `skip-lts-prepass: true` to install only the requested version.
```yaml
steps:
- uses: actions/checkout@v7
- uses: actions/setup-dotnet@v6
with:
dotnet-version: '9.0.x'
skip-lts-prepass: true
```
> **Note**: Setting skip-lts-prepass: true skips the LTS runtime pre-pass, so the action won't proactively install the latest LTS runtime. The existing dotnet executable and host components will be used if present; on a clean machine, they will come from the requested SDK installation. Consequently, dotnet --info may report a different Host: Version than when the pre-pass runs.
## Using the `architecture` input ## Using the `architecture` input
Using the architecture input, it is possible to specify the required .NET SDK architecture. Possible values: `x64`, `x86`, `arm64`, `amd64`, `arm`, `s390x`, `ppc64le`, `riscv64`. If the input is not specified, the architecture defaults to the host OS architecture (not all of the architectures are available on all platforms). Using the architecture input, it is possible to specify the required .NET SDK architecture. Possible values: `x64`, `x86`, `arm64`, `amd64`, `arm`, `s390x`, `ppc64le`, `riscv64`. If the input is not specified, the architecture defaults to the host OS architecture (not all of the architectures are available on all platforms).

View File

@@ -1218,6 +1218,135 @@ describe('installer tests', () => {
} }
); );
}); });
describe('skip-lts-prepass tests', () => {
it('should run the LTS pre-pass by default (skip-lts-prepass not set)', async () => {
const inputVersion = '9.0.100';
const inputQuality = '';
const stdout = `Fictitious dotnet version ${inputVersion} is installed`;
getExecOutputSpy.mockImplementation(() => {
return Promise.resolve({exitCode: 0, stdout, stderr: ''});
});
maxSatisfyingSpy.mockImplementation(() => inputVersion);
const dotnetInstaller = new installer.DotnetCoreInstaller(
inputVersion,
inputQuality
);
await dotnetInstaller.installDotnet();
expect(getExecOutputSpy).toHaveBeenCalledTimes(2);
const ltsCallArguments = (
getExecOutputSpy.mock.calls[0][1] as string[]
).join(' ');
expect(ltsCallArguments).toContain(
IS_WINDOWS ? '-Channel LTS' : '--channel LTS'
);
});
it('should skip the LTS pre-pass and install only the requested version when skip-lts-prepass is true', async () => {
const inputVersion = '9.0.100';
const inputQuality = '';
const stdout = `Fictitious dotnet version ${inputVersion} is installed`;
getExecOutputSpy.mockImplementation(() => {
return Promise.resolve({exitCode: 0, stdout, stderr: ''});
});
maxSatisfyingSpy.mockImplementation(() => inputVersion);
const dotnetInstaller = new installer.DotnetCoreInstaller(
inputVersion,
inputQuality,
undefined,
undefined,
true,
undefined,
undefined,
true
);
const installedVersion = await dotnetInstaller.installDotnet();
expect(getExecOutputSpy).toHaveBeenCalledTimes(1);
const scriptArguments = (
getExecOutputSpy.mock.calls[0][1] as string[]
).join(' ');
expect(scriptArguments).not.toContain('LTS');
expect(scriptArguments).toContain(
IS_WINDOWS
? `-Version ${inputVersion}`
: `--version ${inputVersion}`
);
expect(installedVersion).toBe(inputVersion);
});
it('should skip the LTS pre-pass for a cross-architecture install', async () => {
const inputVersion = '9.0.100';
const inputQuality = '';
const inputArchitecture =
installer.normalizeArch(os.arch()) === 'x64' ? 'arm64' : 'x64';
const stdout = `Fictitious dotnet version ${inputVersion} is installed`;
getExecOutputSpy.mockImplementation(() => {
return Promise.resolve({exitCode: 0, stdout, stderr: ''});
});
maxSatisfyingSpy.mockImplementation(() => inputVersion);
const dotnetInstaller = new installer.DotnetCoreInstaller(
inputVersion,
inputQuality,
inputArchitecture,
undefined,
true,
undefined,
undefined,
true
);
await dotnetInstaller.installDotnet();
expect(getExecOutputSpy).toHaveBeenCalledTimes(1);
const scriptArguments = (
getExecOutputSpy.mock.calls[0][1] as string[]
).join(' ');
expect(scriptArguments).not.toContain('LTS');
expect(scriptArguments).toContain(
IS_WINDOWS ? '-Architecture' : '--architecture'
);
});
it('should skip the LTS pre-pass for each requested version', async () => {
const inputQuality = '';
getExecOutputSpy.mockImplementation(() => {
return Promise.resolve({
exitCode: 0,
stdout: `Fictitious dotnet version installed`,
stderr: ''
});
});
maxSatisfyingSpy.mockImplementation(() => '9.0.100');
for (const version of ['8.0.100', '9.0.100']) {
const dotnetInstaller = new installer.DotnetCoreInstaller(
version,
inputQuality,
undefined,
undefined,
true,
undefined,
undefined,
true
);
await dotnetInstaller.installDotnet();
}
// 2 versions x 1 call each (no LTS pre-pass call for either)
expect(getExecOutputSpy).toHaveBeenCalledTimes(2);
for (const call of getExecOutputSpy.mock.calls) {
expect((call[1] as string[]).join(' ')).not.toContain('LTS');
}
});
});
}); });
describe('addToPath() tests', () => { describe('addToPath() tests', () => {

View File

@@ -315,6 +315,44 @@ describe('setup-dotnet tests', () => {
expect(capturedCheckLatest).toBe(false); expect(capturedCheckLatest).toBe(false);
}); });
it("should read the 'skip-lts-prepass' input and pass it to DotnetCoreInstaller", async () => {
inputs['dotnet-version'] = ['10.0.101'];
inputs['dotnet-quality'] = '';
inputs['architecture'] = '';
inputs['skip-lts-prepass'] = 'true';
let capturedSkipLtsPrepass: boolean | undefined;
installDotnetSpy.mockImplementation(function (this: any) {
capturedSkipLtsPrepass = this.skipLtsPrepass;
return Promise.resolve('');
});
await setup.run();
expect(getBooleanInputSpy).toHaveBeenCalledWith('skip-lts-prepass');
expect(installDotnetSpy).toHaveBeenCalledTimes(1);
expect(capturedSkipLtsPrepass).toBe(true);
});
it("should default 'skip-lts-prepass' to false when the input isn't set", async () => {
inputs['dotnet-version'] = ['10.0.101'];
inputs['dotnet-quality'] = '';
inputs['architecture'] = '';
inputs['skip-lts-prepass'] = '';
let capturedSkipLtsPrepass: boolean | undefined;
installDotnetSpy.mockImplementation(function (this: any) {
capturedSkipLtsPrepass = this.skipLtsPrepass;
return Promise.resolve('');
});
await setup.run();
expect(getBooleanInputSpy).toHaveBeenCalledWith('skip-lts-prepass');
expect(installDotnetSpy).toHaveBeenCalledTimes(1);
expect(capturedSkipLtsPrepass).toBe(false);
});
describe("'check-latest' resolution", () => { describe("'check-latest' resolution", () => {
const captureCheckLatest = async () => { const captureCheckLatest = async () => {
inputs['dotnet-version'] = ['10.0.101']; inputs['dotnet-version'] = ['10.0.101'];

View File

@@ -14,6 +14,10 @@ inputs:
dotnet-channel: dotnet-channel:
description: 'Optional channel for the installation. The possible values are: STS, LTS, A.B (e.g. 8.0), A.B.Cxx (e.g. 8.0.1xx, available since 5.0). To be used with "dotnet-version: latest".' description: 'Optional channel for the installation. The possible values are: STS, LTS, A.B (e.g. 8.0), A.B.Cxx (e.g. 8.0.1xx, available since 5.0). To be used with "dotnet-version: latest".'
required: false required: false
skip-lts-prepass:
description: 'Optional. When true, skips the LTS runtime pre-pass and installs only the requested dotnet-version. Default: false.'
required: false
default: false
global-json-file: global-json-file:
description: 'Optional global.json location, if your global.json isn''t located in the root of the repo.' description: 'Optional global.json location, if your global.json isn''t located in the root of the repo.'
required: false required: false

15
dist/setup/index.js vendored
View File

@@ -45382,11 +45382,12 @@ class DotnetCoreInstaller {
checkLatest; checkLatest;
minimumVersion; minimumVersion;
rollForward; rollForward;
skipLtsPrepass;
static FeatureBandSyntax = /^(\d+)\.(\d+)\.(\d)xx$/; static FeatureBandSyntax = /^(\d+)\.(\d+)\.(\d)xx$/;
static { static {
DotnetInstallDir.setEnvironmentVariable(); DotnetInstallDir.setEnvironmentVariable();
} }
constructor(version, quality, architecture, dotnetChannel, checkLatest = true, minimumVersion, rollForward) { constructor(version, quality, architecture, dotnetChannel, checkLatest = true, minimumVersion, rollForward, skipLtsPrepass = false) {
this.version = version; this.version = version;
this.quality = quality; this.quality = quality;
this.architecture = architecture; this.architecture = architecture;
@@ -45394,6 +45395,7 @@ class DotnetCoreInstaller {
this.checkLatest = checkLatest; this.checkLatest = checkLatest;
this.minimumVersion = minimumVersion; this.minimumVersion = minimumVersion;
this.rollForward = rollForward; this.rollForward = rollForward;
this.skipLtsPrepass = skipLtsPrepass;
this.version = version.trim(); this.version = version.trim();
} }
getInstalledSdkVersions() { getInstalledSdkVersions() {
@@ -45594,8 +45596,10 @@ class DotnetCoreInstaller {
: []; : [];
/** /**
* Install dotnet runtime first in order to get * Install dotnet runtime first in order to get
* the latest stable version of dotnet CLI * the latest stable version of dotnet CLI.
* Opt-out via skip-lts-prepass when this safeguard isn't needed.
*/ */
if (!this.skipLtsPrepass) {
const runtimeInstallOutput = await new DotnetInstallScript() const runtimeInstallOutput = await new DotnetInstallScript()
.useArchitecture(this.architecture) .useArchitecture(this.architecture)
// If dotnet CLI is already installed - avoid overwriting it // If dotnet CLI is already installed - avoid overwriting it
@@ -45613,6 +45617,10 @@ class DotnetCoreInstaller {
*/ */
warning(`Failed to install dotnet runtime + cli, exit code: ${runtimeInstallOutput.exitCode}. ${runtimeInstallOutput.stderr}`); warning(`Failed to install dotnet runtime + cli, exit code: ${runtimeInstallOutput.exitCode}. ${runtimeInstallOutput.stderr}`);
} }
}
else {
info('Skipping LTS runtime pre-pass (skip-lts-prepass: true).');
}
/** /**
* Install dotnet over the latest version of * Install dotnet over the latest version of
* dotnet CLI * dotnet CLI
@@ -106872,11 +106880,12 @@ async function run() {
if (quality && !qualityOptions.includes(quality)) { if (quality && !qualityOptions.includes(quality)) {
throw new Error(`Value '${quality}' is not supported for the 'dotnet-quality' option. Supported values are: daily, preview, ga.`); throw new Error(`Value '${quality}' is not supported for the 'dotnet-quality' option. Supported values are: daily, preview, ga.`);
} }
const skipLtsPrepass = getBooleanInput('skip-lts-prepass');
let dotnetInstaller; let dotnetInstaller;
const uniqueVersions = new Set(versions.map(v => (v.toLowerCase() === 'latest' ? 'latest' : v))); const uniqueVersions = new Set(versions.map(v => (v.toLowerCase() === 'latest' ? 'latest' : v)));
for (const version of uniqueVersions) { for (const version of uniqueVersions) {
const constraint = globalJsonConstraints.get(version); const constraint = globalJsonConstraints.get(version);
dotnetInstaller = new DotnetCoreInstaller(version, quality, architecture, version.toLowerCase() === 'latest' ? dotnetChannel : undefined, checkLatest, constraint?.minimumVersion, constraint?.rollForward); dotnetInstaller = new DotnetCoreInstaller(version, quality, architecture, version.toLowerCase() === 'latest' ? dotnetChannel : undefined, checkLatest, constraint?.minimumVersion, constraint?.rollForward, skipLtsPrepass);
const installedVersion = await dotnetInstaller.installDotnet(); const installedVersion = await dotnetInstaller.installDotnet();
installedDotnetVersions.push(installedVersion); installedDotnetVersions.push(installedVersion);
} }

View File

@@ -421,7 +421,8 @@ export class DotnetCoreInstaller {
private dotnetChannel?: string, private dotnetChannel?: string,
private checkLatest: boolean = true, private checkLatest: boolean = true,
private minimumVersion?: string, private minimumVersion?: string,
private rollForward?: string private rollForward?: string,
private skipLtsPrepass: boolean = false
) { ) {
this.version = version.trim(); this.version = version.trim();
} }
@@ -720,8 +721,10 @@ export class DotnetCoreInstaller {
: []; : [];
/** /**
* Install dotnet runtime first in order to get * Install dotnet runtime first in order to get
* the latest stable version of dotnet CLI * the latest stable version of dotnet CLI.
* Opt-out via skip-lts-prepass when this safeguard isn't needed.
*/ */
if (!this.skipLtsPrepass) {
const runtimeInstallOutput = await new DotnetInstallScript() const runtimeInstallOutput = await new DotnetInstallScript()
.useArchitecture(this.architecture) .useArchitecture(this.architecture)
// If dotnet CLI is already installed - avoid overwriting it // If dotnet CLI is already installed - avoid overwriting it
@@ -744,6 +747,9 @@ export class DotnetCoreInstaller {
`Failed to install dotnet runtime + cli, exit code: ${runtimeInstallOutput.exitCode}. ${runtimeInstallOutput.stderr}` `Failed to install dotnet runtime + cli, exit code: ${runtimeInstallOutput.exitCode}. ${runtimeInstallOutput.stderr}`
); );
} }
} else {
core.info('Skipping LTS runtime pre-pass (skip-lts-prepass: true).');
}
/** /**
* Install dotnet over the latest version of * Install dotnet over the latest version of

View File

@@ -130,6 +130,8 @@ export async function run() {
); );
} }
const skipLtsPrepass = core.getBooleanInput('skip-lts-prepass');
let dotnetInstaller: DotnetCoreInstaller; let dotnetInstaller: DotnetCoreInstaller;
const uniqueVersions = new Set<string>( const uniqueVersions = new Set<string>(
versions.map(v => (v.toLowerCase() === 'latest' ? 'latest' : v)) versions.map(v => (v.toLowerCase() === 'latest' ? 'latest' : v))
@@ -143,7 +145,8 @@ export async function run() {
version.toLowerCase() === 'latest' ? dotnetChannel : undefined, version.toLowerCase() === 'latest' ? dotnetChannel : undefined,
checkLatest, checkLatest,
constraint?.minimumVersion, constraint?.minimumVersion,
constraint?.rollForward constraint?.rollForward,
skipLtsPrepass
); );
const installedVersion = await dotnetInstaller.installDotnet(); const installedVersion = await dotnetInstaller.installDotnet();
installedDotnetVersions.push(installedVersion); installedDotnetVersions.push(installedVersion);