Compare commits

..
Author SHA1 Message Date
Priya Gupta 6ec5c6b286 Add fallback install directory when default is not writable (#785)
* Add fallback install directory when default is not writable

* Refactor macOS install directory handling to warn when default is not writable
2026-10-30 08:13:43 -05:00
dependabot[bot]andHarithaVattikuti 62573f4b60 Bump brace-expansion (#790)
* Bump brace-expansion

Bumps  and [brace-expansion](https://github.com/juliangruber/brace-expansion). These dependencies needed to be updated together.

Updates `brace-expansion` from 1.1.18 to 1.1.21
- [Release notes](https://github.com/juliangruber/brace-expansion/releases)
- [Commits](https://github.com/juliangruber/brace-expansion/compare/v1.1.18...v1.1.21)

Updates `brace-expansion` from 5.0.9 to 5.0.12
- [Release notes](https://github.com/juliangruber/brace-expansion/releases)
- [Commits](https://github.com/juliangruber/brace-expansion/compare/v1.1.18...v1.1.21)

Updates `brace-expansion` from 2.1.4 to 2.1.7
- [Release notes](https://github.com/juliangruber/brace-expansion/releases)
- [Commits](https://github.com/juliangruber/brace-expansion/compare/v1.1.18...v1.1.21)

---
updated-dependencies:
- dependency-name: brace-expansion
  dependency-version: 1.1.21
  dependency-type: indirect
- dependency-name: brace-expansion
  dependency-version: 5.0.12
  dependency-type: indirect
- dependency-name: brace-expansion
  dependency-version: 2.1.7
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

* Fix license failures

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: HarithaVattikuti <73516759+HarithaVattikuti@users.noreply.github.com>
2026-10-27 11:40:29 -05:00
dependabot[bot] 67290a9d23 Bump handlebars from 4.7.9 to 4.7.10 (#792)
Bumps [handlebars](https://github.com/handlebars-lang/handlebars.js) from 4.7.9 to 4.7.10.
- [Release notes](https://github.com/handlebars-lang/handlebars.js/releases)
- [Changelog](https://github.com/handlebars-lang/handlebars.js/blob/v4.7.10/release-notes.md)
- [Commits](https://github.com/handlebars-lang/handlebars.js/compare/v4.7.9...v4.7.10)

---
updated-dependencies:
- dependency-name: handlebars
  dependency-version: 4.7.10
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-27 11:34:40 -05:00
dependabot[bot] 0072421cf6 Bump actions/checkout from 7.0.0 to 7.0.1 (#788)
Bumps [actions/checkout](https://github.com/actions/checkout) from 7.0.0 to 7.0.1.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v7...3d3c42e5aac5ba805825da76410c181273ba90b1)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: 7.0.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-27 11:23:00 -05:00
dependabot[bot]andHarithaVattikuti 9ead3175a9 Bump undici from 6.28.0 to 6.29.1 (#784)
* Bump undici from 6.28.0 to 6.29.1

Bumps [undici](https://github.com/nodejs/undici) from 6.28.0 to 6.29.1.
- [Release notes](https://github.com/nodejs/undici/releases)
- [Commits](https://github.com/nodejs/undici/compare/v6.28.0...v6.29.1)

---
updated-dependencies:
- dependency-name: undici
  dependency-version: 6.29.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

* Fix license failures

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: HarithaVattikuti <73516759+HarithaVattikuti@users.noreply.github.com>
2026-10-27 11:18:06 -05:00
Chiranjib Swainandchiranjib-swain 349aa191ae 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>
2026-10-21 16:53:54 -05:00
14 changed files with 2135 additions and 740 deletions

No files matched your search

+22 -1
View File
@@ -747,7 +747,7 @@ jobs:
operating-system: [ubuntu-latest, windows-latest, macos-latest]
steps:
- name: Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Clear toolcache
shell: pwsh
run: __tests__/clear-toolcache.ps1 ${{ runner.os }}
@@ -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$"
@@ -1,6 +1,6 @@
---
name: brace-expansion
version: 1.1.18
version: 1.1.21
type: npm
summary: Brace expansion as known from sh/bash
homepage: https://github.com/juliangruber/brace-expansion
@@ -1,6 +1,6 @@
---
name: brace-expansion
version: 5.0.9
version: 5.0.12
type: npm
summary: Brace expansion as known from sh/bash
homepage:
+1 -1
View File
@@ -1,6 +1,6 @@
---
name: undici
version: 6.28.0
version: 6.29.1
type: npm
summary: An HTTP/1.1 client, written from scratch for Node.js
homepage: https://undici.nodejs.org
+22 -1
View File
@@ -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).
@@ -437,7 +452,13 @@ You can also set `DOTNET_INSTALL_DIR` to a value based on runtime variables, suc
- name: Set DOTNET_INSTALL_DIR
run: echo "DOTNET_INSTALL_DIR=$HOME/.dotnet" >> $GITHUB_ENV
```
> **Note**: On some self-hosted or large Linux runners, installing .NET under the default `/usr/share/dotnet` location may fail due to insufficient permissions. To ensure successful installation, set `DOTNET_INSTALL_DIR` to a user-writable path.
> **Note**: On some self-hosted, larger or containerized runners the default location is not writable by the current user, and the installation fails with a permission error. The action resolves the install directory in this order:
>
> 1. `DOTNET_INSTALL_DIR`, when set. It is always honored and never probed.
> 2. The default location for the runner OS, when it is writable.
> 3. `.dotnet` in the current user's home directory, for example `/home/runner/.dotnet` or `C:\Users\runneradmin\.dotnet`.
>
> Runners that can write to the default location are unaffected. When the fallback is used, `DOTNET_ROOT` and `PATH` point at the home directory, so .NET preinstalled under the system location is no longer picked up. Set `DOTNET_INSTALL_DIR` explicitly to pin the location. If no writable location is found, the action warns and keeps the default, so the installation is likely to fail.
## Recommended permissions
+395 -10
View File
@@ -11,6 +11,7 @@ import {
import each from 'jest-each';
import semver from 'semver';
import fspromises from 'fs/promises';
import type {Stats} from 'fs';
import os from 'os';
import path from 'path';
@@ -35,26 +36,47 @@ jest.unstable_mockModule('@actions/io', () => ({
jest.unstable_mockModule('fs', () => {
const actual = jest.requireActual('fs') as typeof import('fs');
const chmodSync = jest.fn();
const lstatSync = jest.fn(actual.lstatSync);
const mkdtempSync = jest.fn(actual.mkdtempSync);
const rmSync = jest.fn(actual.rmSync);
const readdirSync = jest.fn();
const existsSync = jest.fn(() => true);
const statSync = jest.fn(() => ({isFile: () => true}));
const accessSync = jest.fn();
return {
const mocked = {
...actual,
chmodSync,
lstatSync,
mkdtempSync,
rmSync,
readdirSync,
existsSync,
statSync,
accessSync,
default: {
...actual,
chmodSync,
readdirSync,
existsSync,
statSync,
accessSync
}
accessSync
};
return {
...mocked,
default: mocked
};
});
jest.unstable_mockModule('os', () => {
const actual = jest.requireActual('os') as typeof import('os');
// os.homedir() reads the real environment, not jest's process.env.
const homedir = jest.fn(actual.homedir);
// Resolved per call, so spies placed on the real module still take effect.
const arch = () => actual.arch();
return {...actual, homedir, arch, default: {...actual, homedir, arch}};
});
// Allows tests to re-import the installer as if running on another OS.
let platformOverride: 'windows' | 'linux' | 'mac' | undefined;
jest.unstable_mockModule('../src/utils.js', () => {
const actual = jest.requireActual(
'../src/utils.js'
) as typeof import('../src/utils.js');
const platform = platformOverride ?? actual.PLATFORM;
return {...actual, PLATFORM: platform, IS_WINDOWS: platform === 'windows'};
});
const exec = await import('@actions/exec');
@@ -1218,6 +1240,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', () => {
@@ -1235,6 +1386,240 @@ describe('installer tests', () => {
expect(path).toContain(process.env['DOTNET_INSTALL_DIR']);
});
});
describe('install directory fallback tests', () => {
// Mock factories re-run on resetModules, so the installer sees new
// instances; configure those, not the ones captured at file load.
const importInstallerFor = async (
platform: 'windows' | 'linux' | 'mac',
isWritable: (dir: string) => boolean
) => {
platformOverride = platform;
jest.resetModules();
const freshFs = await import('fs');
(freshFs.lstatSync as jest.Mock).mockReturnValue({} as Stats);
(freshFs.mkdtempSync as jest.Mock).mockImplementation(
(...args: unknown[]) => {
const prefix = String(args[0]);
if (!isWritable(path.dirname(prefix))) {
throw Object.assign(new Error('permission denied'), {
code: 'EACCES'
});
}
return `${prefix}abc123`;
}
);
(freshFs.rmSync as jest.Mock).mockImplementation(() => {});
const freshCore = await import('@actions/core');
const freshOs = await import('os');
const {DotnetInstallDir} = await import('../src/installer.js');
return {DotnetInstallDir, core: freshCore, fs: freshFs, os: freshOs};
};
const probedDirs = (freshFs: typeof import('fs')) =>
(freshFs.mkdtempSync as jest.Mock).mock.calls.map(call =>
path.dirname(String(call[0]))
);
beforeEach(() => {
delete process.env['DOTNET_INSTALL_DIR'];
});
afterEach(() => {
platformOverride = undefined;
});
it(`should use the default location when it is writable`, async () => {
const {DotnetInstallDir} = await importInstallerFor(
'linux',
() => true
);
expect(DotnetInstallDir.dirPath).toBe('/usr/share/dotnet');
});
it(`should fall back to the home directory when the default location is not writable`, async () => {
const fallbackPath = path.join(os.homedir(), '.dotnet');
const {DotnetInstallDir, core: freshCore} = await importInstallerFor(
'linux',
dir => dir === fallbackPath
);
expect(DotnetInstallDir.dirPath).toBe(fallbackPath);
expect(freshCore.warning).toHaveBeenCalled();
});
it(`should reject a dangling symlink instead of probing its writable parent`, async () => {
// Resolved, because that is the form the probe receives on every platform.
const linkPath = path.resolve('/usr/share/dotnet');
const {DotnetInstallDir, fs: freshFs} = await importInstallerFor(
'linux',
() => true
);
// A dangling link exists for lstat, but nothing can be created under it.
(freshFs.mkdtempSync as jest.Mock).mockImplementation(
(...args: unknown[]) => {
if (path.dirname(String(args[0])) === linkPath) {
throw Object.assign(new Error('no such file or directory'), {
code: 'ENOENT'
});
}
return `${args[0]}abc123`;
}
);
expect(DotnetInstallDir.dirPath).toBe(
path.join(os.homedir(), '.dotnet')
);
const probed = probedDirs(freshFs);
expect(probed).toContain(linkPath);
expect(probed).not.toContain(path.dirname(linkPath));
});
it(`should probe the nearest existing parent when the directory is missing`, async () => {
const systemPath = path.resolve('/usr/share/dotnet');
const parentPath = path.dirname(systemPath);
const {DotnetInstallDir, fs: freshFs} = await importInstallerFor(
'linux',
() => true
);
// Only the parent exists, so the walk has to climb before probing.
(freshFs.lstatSync as jest.Mock).mockImplementation(
(...args: unknown[]) =>
path.resolve(String(args[0])) === parentPath
? ({} as Stats)
: undefined
);
expect(DotnetInstallDir.dirPath).toBe('/usr/share/dotnet');
const probed = probedDirs(freshFs);
expect(probed).toContain(parentPath);
expect(probed).not.toContain(systemPath);
});
it(`should give up at the filesystem root when no ancestor exists`, async () => {
const systemPath = path.resolve('/usr/share/dotnet');
const homePath = path.join(os.homedir(), '.dotnet');
const {DotnetInstallDir, fs: freshFs} = await importInstallerFor(
'linux',
() => true
);
// Nothing on the system path exists, all the way up to the root.
(freshFs.lstatSync as jest.Mock).mockImplementation(
(...args: unknown[]) =>
systemPath.startsWith(path.resolve(String(args[0])))
? undefined
: ({} as Stats)
);
expect(DotnetInstallDir.dirPath).toBe(homePath);
// The walk hit the root and returned without ever probing.
expect(probedDirs(freshFs)).toEqual([homePath]);
});
it(`should not probe a relative default location`, async () => {
// An unset HOME makes the macOS default relative, e.g. 'undefined/.dotnet'.
delete process.env['HOME'];
const homePath = path.join(os.homedir(), '.dotnet');
const {DotnetInstallDir, fs: freshFs} = await importInstallerFor(
'mac',
() => true
);
expect(DotnetInstallDir.dirPath).toBe(homePath);
expect(probedDirs(freshFs)).not.toContain(process.cwd());
});
it(`should not probe a relative default location on Windows`, async () => {
// An unset PROGRAMFILES makes the Windows default relative.
delete process.env['PROGRAMFILES'];
const homePath = path.join(os.homedir(), '.dotnet');
const {DotnetInstallDir, fs: freshFs} = await importInstallerFor(
'windows',
() => true
);
expect(DotnetInstallDir.dirPath).toBe(homePath);
expect(probedDirs(freshFs)).not.toContain(process.cwd());
});
it(`should not treat the filesystem root as a home directory`, async () => {
const root = path.parse(process.cwd()).root;
const rootDotnet = path.join(root, '.dotnet');
// Only the root candidate is writable, so an unguarded run would pick it.
const {
DotnetInstallDir,
os: freshOs,
core: freshCore
} = await importInstallerFor('linux', dir => dir === rootDotnet);
(freshOs.homedir as jest.Mock).mockReturnValue(root);
expect(DotnetInstallDir.dirPath).toBe('/usr/share/dotnet');
expect(freshCore.warning).toHaveBeenCalled();
});
it(`should still report writable when the probe cannot be removed`, async () => {
const {DotnetInstallDir, fs: freshFs} = await importInstallerFor(
'linux',
() => true
);
(freshFs.rmSync as jest.Mock).mockImplementation(() => {
throw Object.assign(new Error('device or resource busy'), {
code: 'EBUSY'
});
});
expect(DotnetInstallDir.dirPath).toBe('/usr/share/dotnet');
});
it(`should keep the shared macOS location and warn when it is not writable`, async () => {
// The mac default derives from HOME, homeInstallPath() from os.homedir();
// aligning them is what makes both candidates the same path.
process.env['HOME'] = os.homedir();
const homePath = path.join(os.homedir(), '.dotnet');
const {
DotnetInstallDir,
fs: freshFs,
core: freshCore
} = await importInstallerFor('mac', () => false);
expect(DotnetInstallDir.dirPath).toBe(homePath);
// One candidate, so it is probed once rather than twice.
expect(probedDirs(freshFs)).toEqual([homePath]);
expect(freshCore.warning).toHaveBeenCalled();
});
it(`should prefer DOTNET_INSTALL_DIR env.var without probing`, async () => {
process.env['DOTNET_INSTALL_DIR'] = path.join(path.sep, 'custom');
const {DotnetInstallDir, fs: freshFs} = await importInstallerFor(
'linux',
() => false
);
expect(DotnetInstallDir.dirPath).toBe(path.join(path.sep, 'custom'));
expect(freshFs.mkdtempSync as jest.Mock).not.toHaveBeenCalled();
});
it(`should resolve once, and not before the directory is needed`, async () => {
const {DotnetInstallDir, fs: freshFs} = await importInstallerFor(
'linux',
() => true
);
const probe = freshFs.mkdtempSync as jest.Mock;
expect(probe).not.toHaveBeenCalled();
void DotnetInstallDir.dirPath;
const afterFirstRead = probe.mock.calls.length;
void DotnetInstallDir.dirPath;
expect(afterFirstRead).toBeGreaterThan(0);
expect(probe.mock.calls.length).toBe(afterFirstRead);
});
});
});
describe('normalizeArch() tests', () => {
+38
View File
@@ -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'];
+4
View File
@@ -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
+656 -230
View File
File diff suppressed because it is too large. Load diff
+832 -279
View File
File diff suppressed because it is too large. Load diff
+34 -184
View File
@@ -36,7 +36,7 @@
"husky": "^9.1.7",
"jest": "^30.4.2",
"jest-circus": "^30.4.2",
"jest-each": "^30.5.2",
"jest-each": "^30.0.0",
"prettier": "^3.8.4",
"ts-jest": "^29.4.11",
"typescript": "^6.0.3",
@@ -1425,22 +1425,6 @@
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
}
},
"node_modules/@jest/react-is-18": {
"name": "react-is",
"version": "18.3.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz",
"integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==",
"dev": true,
"license": "MIT"
},
"node_modules/@jest/react-is-19": {
"name": "react-is",
"version": "19.3.0",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-19.3.0.tgz",
"integrity": "sha512-UpMYezM4v5/18F28aC66AEsjXIgE02kyEMH6yLdgLXu/UTfa1Ntwck/nNLrbqJsEXW7gPb0coNO9FQse9WTovA==",
"dev": true,
"license": "MIT"
},
"node_modules/@jest/reporters": {
"version": "30.4.1",
"resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-30.4.1.tgz",
@@ -2904,9 +2888,9 @@
"license": "Apache-2.0"
},
"node_modules/brace-expansion": {
"version": "5.0.9",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz",
"integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==",
"version": "5.0.12",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.12.tgz",
"integrity": "sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==",
"license": "MIT",
"dependencies": {
"balanced-match": "^4.0.2"
@@ -4051,9 +4035,9 @@
"license": "MIT"
},
"node_modules/glob/node_modules/brace-expansion": {
"version": "2.1.4",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.4.tgz",
"integrity": "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==",
"version": "2.1.7",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.7.tgz",
"integrity": "sha512-uZbew1NqdmPDTMJ8ah1y+b+9QEJrfkXFk3RcTQw3X0jW/xRUvFKsg1CfQdSYGdTbXZWExtU3J3ccxtnfw1Fi0g==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -4103,13 +4087,13 @@
"dev": true
},
"node_modules/handlebars": {
"version": "4.7.9",
"resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.9.tgz",
"integrity": "sha512-4E71E0rpOaQuJR2A3xDZ+GM1HyWYv1clR58tC8emQNeQe3RH7MAzSbat+V0wG78LQBo6m6bzSG/L4pBuCsgnUQ==",
"version": "4.7.10",
"resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.10.tgz",
"integrity": "sha512-P5VJMVM7qgBn6vjXMw8WG9uVI+ncf2pi72j4de4yz5ZULLj2RGqLYaKOYGsgyrViQ0tePOVlN1tDCCXXtFqXKg==",
"dev": true,
"license": "MIT",
"dependencies": {
"minimist": "^1.2.5",
"minimist": "^1.2.8",
"neo-async": "^2.6.2",
"source-map": "^0.6.1",
"wordwrap": "^1.0.0"
@@ -4124,6 +4108,16 @@
"uglify-js": "^3.1.4"
}
},
"node_modules/handlebars/node_modules/minimist": {
"version": "1.2.8",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
"integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
"dev": true,
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/has-flag": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
@@ -4509,23 +4503,6 @@
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
}
},
"node_modules/jest-circus/node_modules/jest-each": {
"version": "30.4.1",
"resolved": "https://registry.npmjs.org/jest-each/-/jest-each-30.4.1.tgz",
"integrity": "sha512-/8MJbH6fuj48TstjrMf+u/pd06Qezz5xOXvZA6442heNOWr8bdeoGZX2d9fCn028CoMgYmroH9//zky5GfyYmA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jest/get-type": "30.1.0",
"@jest/types": "30.4.1",
"chalk": "^4.1.2",
"jest-util": "30.4.1",
"pretty-format": "30.4.1"
},
"engines": {
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
}
},
"node_modules/jest-cli": {
"version": "30.4.2",
"resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-30.4.2.tgz",
@@ -4640,144 +4617,17 @@
}
},
"node_modules/jest-each": {
"version": "30.5.2",
"resolved": "https://registry.npmjs.org/jest-each/-/jest-each-30.5.2.tgz",
"integrity": "sha512-GRrW+7fmmgmkvyfEBqc1QFdWnF1Ex5yG6Y6AWL4TTxnkWlKIT98dsKh3P3UuWm6v4XsQwVE6DEzXIPMzWJ3ZbA==",
"version": "30.4.1",
"resolved": "https://registry.npmjs.org/jest-each/-/jest-each-30.4.1.tgz",
"integrity": "sha512-/8MJbH6fuj48TstjrMf+u/pd06Qezz5xOXvZA6442heNOWr8bdeoGZX2d9fCn028CoMgYmroH9//zky5GfyYmA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jest/get-type": "30.5.0",
"@jest/types": "30.5.1",
"@jest/get-type": "30.1.0",
"@jest/types": "30.4.1",
"chalk": "^4.1.2",
"jest-regex-util": "30.5.0",
"jest-util": "30.5.1",
"pretty-format": "30.5.1"
},
"engines": {
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
}
},
"node_modules/jest-each/node_modules/@jest/get-type": {
"version": "30.5.0",
"resolved": "https://registry.npmjs.org/@jest/get-type/-/get-type-30.5.0.tgz",
"integrity": "sha512-9/2VUPitAjmBzbvDvqrxmvB7BzWsBW0WmkkojX1ODuxX1NLGxx9gfaZpHB0z8DtJ9uhGNmZG/VXBhf8uO0OV8Q==",
"dev": true,
"license": "MIT",
"engines": {
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
}
},
"node_modules/jest-each/node_modules/@jest/pattern": {
"version": "30.5.0",
"resolved": "https://registry.npmjs.org/@jest/pattern/-/pattern-30.5.0.tgz",
"integrity": "sha512-HdNQYSdRTEBNrginaqzQtTjG0HRMfrra/z6Ok7uL3S87vSlarIVohEsJsSj5edu3MiHoHjAkvPROz5ZjoKai+w==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/node": "*",
"jest-regex-util": "30.5.0"
},
"engines": {
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
}
},
"node_modules/jest-each/node_modules/@jest/schemas": {
"version": "30.5.0",
"resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.5.0.tgz",
"integrity": "sha512-/hunigyNpc4RCjC0VaW3f5RCUZVM2+WQ65qP7z083Gmvac7or2LI50XVNOtE4YPgBpV0yxYiAgorAPGniCoJmg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@sinclair/typebox": "^0.34.0"
},
"engines": {
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
}
},
"node_modules/jest-each/node_modules/@jest/types": {
"version": "30.5.1",
"resolved": "https://registry.npmjs.org/@jest/types/-/types-30.5.1.tgz",
"integrity": "sha512-LvVYn83nnXPl+Rg98nvcFgjx6nRMTArhSn6RAX/w3ELn54S8A42TYZvsCMdGUqTM8S0wyXbtlQdU6Hi6dykj9g==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jest/pattern": "30.5.0",
"@jest/schemas": "30.5.0",
"@types/istanbul-lib-coverage": "^2.0.6",
"@types/istanbul-reports": "^3.0.4",
"@types/node": "*",
"@types/yargs": "^17.0.33",
"chalk": "^4.1.2"
},
"engines": {
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
}
},
"node_modules/jest-each/node_modules/ansi-styles": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz",
"integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/jest-each/node_modules/jest-regex-util": {
"version": "30.5.0",
"resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-30.5.0.tgz",
"integrity": "sha512-Mg0WK7A6xRHLSA1udJ8y9f3lM0uUhFTBnLKzwPmqB9AylvpleJ6BLemR8K9dK27DY+cesDryoA7yLZCAHsPG1A==",
"dev": true,
"license": "MIT",
"engines": {
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
}
},
"node_modules/jest-each/node_modules/jest-util": {
"version": "30.5.1",
"resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.5.1.tgz",
"integrity": "sha512-yKuxmNy2rSbTXw+3SIPanJo+nV4/BS1p26v44IYBFMsswSQySfMMcPHErnOncda7i9HEz0q605rIhSTBVgrZTg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jest/types": "30.5.1",
"@types/node": "*",
"chalk": "^4.1.2",
"ci-info": "^4.2.0",
"graceful-fs": "^4.2.11",
"picomatch": "^4.0.3"
},
"engines": {
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
}
},
"node_modules/jest-each/node_modules/picomatch": {
"version": "4.0.7",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz",
"integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/jest-each/node_modules/pretty-format": {
"version": "30.5.1",
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.5.1.tgz",
"integrity": "sha512-byhRAPguVKMQIj4kjJwJ5lAskVhfuiSdiYl/aLTWpgkGEmic2jYhJh1yE9ih8Ox44Xg9ccCT11/S6QrzSJuNrg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jest/react-is-18": "npm:react-is@^18.3.1",
"@jest/react-is-19": "npm:react-is@^19.2.5",
"@jest/schemas": "30.5.0",
"ansi-styles": "^5.2.0"
"jest-util": "30.4.1",
"pretty-format": "30.4.1"
},
"engines": {
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
@@ -5406,9 +5256,9 @@
"license": "MIT"
},
"node_modules/minimatch/node_modules/brace-expansion": {
"version": "1.1.18",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz",
"integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==",
"version": "1.1.21",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.21.tgz",
"integrity": "sha512-9zeA+KLZNNzglF2TPKRQEDyx6Yby7daAkuy8MiPzpXPsYDWi/DRM8jmwUDxokQjYqBpv5DgPiwD4h4ZZSy1Ujw==",
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0",
@@ -6516,9 +6366,9 @@
}
},
"node_modules/undici": {
"version": "6.28.0",
"resolved": "https://registry.npmjs.org/undici/-/undici-6.28.0.tgz",
"integrity": "sha512-LIY910g9TI13YS95lrMFrs8Rm/u/irgHeTWoKCoteeJ04CUJ92eEfj0rVn+7VKMPBpUPiUoBKfhNyLI23EE/KA==",
"version": "6.29.1",
"resolved": "https://registry.npmjs.org/undici/-/undici-6.29.1.tgz",
"integrity": "sha512-EvRlUt/QqAEvXdQeRMYUfwOQsfzpDztykrG2owGEE/rBOBoYv9uF39ZzsNDh4lTFHhTr2XOZjKkYgueD12ZmGA==",
"license": "MIT",
"engines": {
"node": ">=18.17"
+1 -1
View File
@@ -57,7 +57,7 @@
"husky": "^9.1.7",
"jest": "^30.4.2",
"jest-circus": "^30.4.2",
"jest-each": "^30.5.2",
"jest-each": "^30.0.0",
"prettier": "^3.8.4",
"ts-jest": "^29.4.11",
"typescript": "^6.0.3",
+124 -30
View File
@@ -7,7 +7,10 @@ import {
accessSync,
chmodSync,
constants as fsConstants,
lstatSync,
mkdtempSync,
readdirSync,
rmSync,
statSync
} from 'fs';
import path from 'path';
@@ -347,11 +350,97 @@ export abstract class DotnetInstallDir {
windows: path.join(process.env['PROGRAMFILES'] + '', 'dotnet')
};
public static readonly dirPath = process.env['DOTNET_INSTALL_DIR']
? DotnetInstallDir.convertInstallPathToAbsolute(
private static resolvedDirPath: string | undefined;
// Resolved on first use so a job that installs nothing never touches the disk.
public static get dirPath(): string {
DotnetInstallDir.resolvedDirPath ??= DotnetInstallDir.resolveDirPath();
return DotnetInstallDir.resolvedDirPath;
}
private static resolveDirPath(): string {
if (process.env['DOTNET_INSTALL_DIR']) {
return DotnetInstallDir.convertInstallPathToAbsolute(
process.env['DOTNET_INSTALL_DIR']
)
: DotnetInstallDir.default[PLATFORM];
);
}
const systemPath = DotnetInstallDir.default[PLATFORM];
const homePath = DotnetInstallDir.homeInstallPath();
// A relative default (unset HOME/PROGRAMFILES) would probe the cwd.
if (
path.isAbsolute(systemPath) &&
DotnetInstallDir.isWritableLocation(systemPath)
) {
return systemPath;
}
// On macOS both candidates are the same path, already probed above.
const hasFallback = !!homePath && homePath !== systemPath;
if (hasFallback && DotnetInstallDir.isWritableLocation(homePath!)) {
core.warning(
`The default .NET install directory '${systemPath}' is not writable by the current user. Falling back to '${homePath}'; .NET preinstalled in the default location will no longer be used. Set the DOTNET_INSTALL_DIR environment variable to override this location.`
);
return homePath!;
}
// Not setFailed: the install may still succeed, so let it report the error.
core.warning(
hasFallback
? `Neither the default .NET install directory '${systemPath}' nor '${homePath}' is writable by the current user. Keeping '${systemPath}', but the installation is likely to fail. Set the DOTNET_INSTALL_DIR environment variable to a writable location.`
: `The default .NET install directory '${systemPath}' is not writable by the current user. Keeping it, but the installation is likely to fail. Set the DOTNET_INSTALL_DIR environment variable to a writable location.`
);
return systemPath;
}
private static homeInstallPath(): string | undefined {
try {
const home = os.homedir();
// An empty HOME resolves against the cwd; a root HOME gives '/.dotnet'.
if (!path.isAbsolute(home) || home === path.parse(home).root) {
return undefined;
}
return path.join(home, '.dotnet');
} catch {
return undefined;
}
}
// Writability is tested by creating a directory. accessSync is not enough: it
// ignores Windows ACLs, so it reports success where the install would fail.
private static isWritableLocation(installDir: string): boolean {
let existingPath = path.resolve(installDir);
try {
// lstat also matches a broken symlink. existsSync does not, and the walk
// would skip past it to a writable parent and wrongly report success.
while (!lstatSync(existingPath, {throwIfNoEntry: false})) {
const parentPath = path.dirname(existingPath);
if (parentPath === existingPath) return false;
existingPath = parentPath;
}
} catch {
return false;
}
let probeDir: string | undefined;
try {
probeDir = mkdtempSync(path.join(existingPath, '.setup-dotnet-probe-'));
return true;
} catch {
return false;
} finally {
if (probeDir) {
try {
rmSync(probeDir, {recursive: true, force: true});
} catch {
// Throwing here would discard the result already returned above.
}
}
}
}
private static convertInstallPathToAbsolute(installDir: string): string {
if (path.isAbsolute(installDir)) return path.normalize(installDir);
@@ -410,10 +499,6 @@ function isExecutableFile(filePath: string): boolean {
export class DotnetCoreInstaller {
private static readonly FeatureBandSyntax = /^(\d+)\.(\d+)\.(\d)xx$/;
static {
DotnetInstallDir.setEnvironmentVariable();
}
constructor(
private version: string,
private quality: QualityOptions,
@@ -421,7 +506,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();
}
@@ -686,6 +772,9 @@ export class DotnetCoreInstaller {
}
public async installDotnet(): Promise<string | null> {
// Before findLocalSdkVersion(), which looks under the resolved directory.
DotnetInstallDir.setEnvironmentVariable();
const isCrossArch =
!!this.architecture &&
normalizeArch(this.architecture) !== normalizeArch(os.arch());
@@ -720,29 +809,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).');
}
/**
+4 -1
View File
@@ -130,6 +130,8 @@ export async function run() {
);
}
const skipLtsPrepass = core.getBooleanInput('skip-lts-prepass');
let dotnetInstaller: DotnetCoreInstaller;
const uniqueVersions = new Set<string>(
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);