From 349aa191ae05aee6d53db4bcba37bd6980e812fe Mon Sep 17 00:00:00 2001 From: Chiranjib Swain Date: Thu, 22 Oct 2026 03:23:54 +0530 Subject: [PATCH] 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 --- .github/workflows/e2e-tests.yml | 21 ++++++ README.md | 15 ++++ __tests__/installer.test.ts | 129 ++++++++++++++++++++++++++++++++ __tests__/setup-dotnet.test.ts | 38 ++++++++++ action.yml | 4 + dist/setup/index.js | 47 +++++++----- src/installer.ts | 50 +++++++------ src/setup-dotnet.ts | 5 +- 8 files changed, 267 insertions(+), 42 deletions(-) diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index 20f4f0d..db2730d 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -769,3 +769,24 @@ jobs: throw "Expected 9.0.100 to be reused, got '${{ steps.reuse.outputs.dotnet-version }}'." } __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$" diff --git a/README.md b/README.md index 47667f9..68ae8ac 100644 --- a/README.md +++ b/README.md @@ -86,6 +86,21 @@ steps: 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, 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). diff --git a/__tests__/installer.test.ts b/__tests__/installer.test.ts index 734a9e7..7666064 100644 --- a/__tests__/installer.test.ts +++ b/__tests__/installer.test.ts @@ -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', () => { diff --git a/__tests__/setup-dotnet.test.ts b/__tests__/setup-dotnet.test.ts index 1e1f149..27f6ed0 100644 --- a/__tests__/setup-dotnet.test.ts +++ b/__tests__/setup-dotnet.test.ts @@ -315,6 +315,44 @@ describe('setup-dotnet tests', () => { 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", () => { const captureCheckLatest = async () => { inputs['dotnet-version'] = ['10.0.101']; diff --git a/action.yml b/action.yml index 191393b..3a55f28 100644 --- a/action.yml +++ b/action.yml @@ -14,6 +14,10 @@ inputs: 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".' 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: description: 'Optional global.json location, if your global.json isn''t located in the root of the repo.' required: false diff --git a/dist/setup/index.js b/dist/setup/index.js index 2e18523..578aa2d 100644 --- a/dist/setup/index.js +++ b/dist/setup/index.js @@ -45382,11 +45382,12 @@ class DotnetCoreInstaller { checkLatest; minimumVersion; rollForward; + skipLtsPrepass; static FeatureBandSyntax = /^(\d+)\.(\d+)\.(\d)xx$/; static { 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.quality = quality; this.architecture = architecture; @@ -45394,6 +45395,7 @@ class DotnetCoreInstaller { this.checkLatest = checkLatest; this.minimumVersion = minimumVersion; this.rollForward = rollForward; + this.skipLtsPrepass = skipLtsPrepass; this.version = version.trim(); } getInstalledSdkVersions() { @@ -45594,24 +45596,30 @@ class DotnetCoreInstaller { : []; /** * 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. */ - const runtimeInstallOutput = await new DotnetInstallScript() - .useArchitecture(this.architecture) - // If dotnet CLI is already installed - avoid overwriting it - .useArguments(utils_IS_WINDOWS ? '-SkipNonVersionedFiles' : '--skip-non-versioned-files') - // Install only runtime + CLI - .useArguments(utils_IS_WINDOWS ? '-Runtime' : '--runtime', 'dotnet') - // Use latest stable version - .useArguments(utils_IS_WINDOWS ? '-Channel' : '--channel', 'LTS') - .useArguments(...architectureArguments) - .execute(); - if (runtimeInstallOutput.exitCode) { - /** - * dotnetInstallScript will install CLI and runtime even if previous script haven't succeded, - * so at this point it's too early to throw an error - */ - warning(`Failed to install dotnet runtime + cli, exit code: ${runtimeInstallOutput.exitCode}. ${runtimeInstallOutput.stderr}`); + if (!this.skipLtsPrepass) { + const runtimeInstallOutput = await new DotnetInstallScript() + .useArchitecture(this.architecture) + // If dotnet CLI is already installed - avoid overwriting it + .useArguments(utils_IS_WINDOWS ? '-SkipNonVersionedFiles' : '--skip-non-versioned-files') + // Install only runtime + CLI + .useArguments(utils_IS_WINDOWS ? '-Runtime' : '--runtime', 'dotnet') + // Use latest stable version + .useArguments(utils_IS_WINDOWS ? '-Channel' : '--channel', 'LTS') + .useArguments(...architectureArguments) + .execute(); + if (runtimeInstallOutput.exitCode) { + /** + * dotnetInstallScript will install CLI and runtime even if previous script haven't succeded, + * so at this point it's too early to throw an error + */ + 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 @@ -106872,11 +106880,12 @@ async function run() { if (quality && !qualityOptions.includes(quality)) { 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; const uniqueVersions = new Set(versions.map(v => (v.toLowerCase() === 'latest' ? 'latest' : v))); for (const version of uniqueVersions) { 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(); installedDotnetVersions.push(installedVersion); } diff --git a/src/installer.ts b/src/installer.ts index 3b1e36b..60291c1 100644 --- a/src/installer.ts +++ b/src/installer.ts @@ -421,7 +421,8 @@ export class DotnetCoreInstaller { private dotnetChannel?: string, private checkLatest: boolean = true, private minimumVersion?: string, - private rollForward?: string + private rollForward?: string, + private skipLtsPrepass: boolean = false ) { this.version = version.trim(); } @@ -720,29 +721,34 @@ export class DotnetCoreInstaller { : []; /** * 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. */ - const runtimeInstallOutput = await new DotnetInstallScript() - .useArchitecture(this.architecture) - // If dotnet CLI is already installed - avoid overwriting it - .useArguments( - IS_WINDOWS ? '-SkipNonVersionedFiles' : '--skip-non-versioned-files' - ) - // Install only runtime + CLI - .useArguments(IS_WINDOWS ? '-Runtime' : '--runtime', 'dotnet') - // Use latest stable version - .useArguments(IS_WINDOWS ? '-Channel' : '--channel', 'LTS') - .useArguments(...architectureArguments) - .execute(); + if (!this.skipLtsPrepass) { + const runtimeInstallOutput = await new DotnetInstallScript() + .useArchitecture(this.architecture) + // If dotnet CLI is already installed - avoid overwriting it + .useArguments( + IS_WINDOWS ? '-SkipNonVersionedFiles' : '--skip-non-versioned-files' + ) + // Install only runtime + CLI + .useArguments(IS_WINDOWS ? '-Runtime' : '--runtime', 'dotnet') + // Use latest stable version + .useArguments(IS_WINDOWS ? '-Channel' : '--channel', 'LTS') + .useArguments(...architectureArguments) + .execute(); - if (runtimeInstallOutput.exitCode) { - /** - * dotnetInstallScript will install CLI and runtime even if previous script haven't succeded, - * so at this point it's too early to throw an error - */ - core.warning( - `Failed to install dotnet runtime + cli, exit code: ${runtimeInstallOutput.exitCode}. ${runtimeInstallOutput.stderr}` - ); + if (runtimeInstallOutput.exitCode) { + /** + * dotnetInstallScript will install CLI and runtime even if previous script haven't succeded, + * so at this point it's too early to throw an error + */ + core.warning( + `Failed to install dotnet runtime + cli, exit code: ${runtimeInstallOutput.exitCode}. ${runtimeInstallOutput.stderr}` + ); + } + } else { + core.info('Skipping LTS runtime pre-pass (skip-lts-prepass: true).'); } /** diff --git a/src/setup-dotnet.ts b/src/setup-dotnet.ts index 34faf77..2479f9a 100644 --- a/src/setup-dotnet.ts +++ b/src/setup-dotnet.ts @@ -130,6 +130,8 @@ export async function run() { ); } + const skipLtsPrepass = core.getBooleanInput('skip-lts-prepass'); + let dotnetInstaller: DotnetCoreInstaller; const uniqueVersions = new Set( versions.map(v => (v.toLowerCase() === 'latest' ? 'latest' : v)) @@ -143,7 +145,8 @@ export async function run() { version.toLowerCase() === 'latest' ? dotnetChannel : undefined, checkLatest, constraint?.minimumVersion, - constraint?.rollForward + constraint?.rollForward, + skipLtsPrepass ); const installedVersion = await dotnetInstaller.installDotnet(); installedDotnetVersions.push(installedVersion);