Merge d41b67df8de628d465a92e94cadaaba938871fc3 into 2016bd2012dba4e32de620c46fe006a3ac9f0602

This commit is contained in:
Justin Lampe 2025-11-25 00:00:02 +05:30 committed by GitHub
commit 333e72c1a6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 332 additions and 17 deletions

View File

@ -49,20 +49,42 @@ steps:
9.0.x
- run: dotnet build <my project>
```
## Installing additional runtimes
The `dotnet-runtime` input allows you to install .NET runtimes separately from SDKs. This is useful for multi-targeting scenarios where you need one SDK version but multiple runtime versions.
When `dotnet-runtime` is specified, both the .NET Runtime (Microsoft.NETCore.App) and the ASP.NET Core Runtime (Microsoft.AspNetCore.App) are installed for each specified version.
**Example: Install SDK 10 with runtimes 8 and 9**:
```yml
steps:
- uses: actions/checkout@v5
- name: Setup dotnet
uses: actions/setup-dotnet@v5
with:
dotnet-version: '10.0.x'
dotnet-runtime: |
8.0.x
9.0.x
- run: dotnet build <my project>
- run: dotnet test <my project>
```
## Supported version syntax
The `dotnet-version` input supports following syntax:
The `dotnet-version` and `dotnet-runtime` inputs support following syntax:
- **A.B.C** (e.g 6.0.400, 7.0.100-preview.7.22377.5) - installs exact version of .NET SDK
- **A.B** or **A.B.x** (e.g. 8.0, 8.0.x) - installs the latest patch version of .NET SDK on the channel `8.0`, including prerelease versions (preview, rc)
- **A.B.C** (e.g 6.0.400, 7.0.100-preview.7.22377.5) - installs exact version of .NET SDK or runtime
- **A.B** or **A.B.x** (e.g. 8.0, 8.0.x) - installs the latest patch version on the channel `8.0`, including prerelease versions (preview, rc)
- **A** or **A.x** (e.g. 8, 8.x) - installs the latest minor version of the specified major tag, including prerelease versions (preview, rc)
- **A.B.Cxx** (e.g. 8.0.4xx) - available since `.NET 5.0` release. Installs the latest version of the specific SDK release, including prerelease versions (preview, rc).
- **A.B.Cxx** (e.g. 8.0.4xx) - available since `.NET 5.0` release. Installs the latest version of the specific release, including prerelease versions (preview, rc).
## Using the `dotnet-quality` input
This input sets up the action to install the latest build of the specified quality in the channel. The possible values of `dotnet-quality` are: **daily**, **signed**, **validated**, **preview**, **ga**.
> **Note**: `dotnet-quality` input can be used only with .NET SDK version in 'A.B', 'A.B.x', 'A', 'A.x' and 'A.B.Cxx' formats where the major version is higher than 5. In other cases, `dotnet-quality` input will be ignored.
> **Note**: `dotnet-quality` input can be used only with .NET SDK or runtime version in 'A.B', 'A.B.x', 'A', 'A.x' and 'A.B.Cxx' formats where the major version is higher than 5. In other cases, `dotnet-quality` input will be ignored. The quality setting applies to both SDK and runtime installations.
```yml
steps:

View File

@ -344,6 +344,171 @@ describe('installer tests', () => {
expect(path).toContain(process.env['DOTNET_INSTALL_DIR']);
});
});
describe('installRuntime() tests', () => {
beforeAll(() => {
whichSpy.mockImplementation(() => Promise.resolve('PathToShell'));
chmodSyncSpy.mockImplementation(() => {});
readdirSpy.mockImplementation(() => Promise.resolve([]));
});
afterAll(() => {
jest.resetAllMocks();
});
it('should throw the error in case of non-zero exit code of the .NET runtime installation script. The error message should contain logs.', async () => {
const inputVersion = '8.0.402';
const inputQuality = '' as QualityOptions;
const errorMessage = 'fictitious error message!';
getExecOutputSpy.mockImplementation(() => {
return Promise.resolve({
exitCode: 1,
stdout: '',
stderr: errorMessage
});
});
const dotnetInstaller = new installer.DotnetCoreInstaller(
inputVersion,
inputQuality
);
await expect(dotnetInstaller.installRuntime()).rejects.toThrow(
`Failed to install dotnet runtime, exit code: 1. ${errorMessage}`
);
});
it('should throw the error in case of non-zero exit code of the ASP.NET Core runtime installation script. The error message should contain logs.', async () => {
const inputVersion = '8.0.402';
const inputQuality = '' as QualityOptions;
const errorMessage = 'fictitious aspnetcore error message!';
getExecOutputSpy
.mockImplementationOnce(() => {
return Promise.resolve({
exitCode: 0,
stdout: `Fictitious dotnet runtime version ${inputVersion} is installed`,
stderr: ''
});
})
.mockImplementationOnce(() => {
return Promise.resolve({
exitCode: 1,
stdout: '',
stderr: errorMessage
});
});
const dotnetInstaller = new installer.DotnetCoreInstaller(
inputVersion,
inputQuality
);
await expect(dotnetInstaller.installRuntime()).rejects.toThrow(
`Failed to install aspnetcore runtime, exit code: 1. ${errorMessage}`
);
});
it('should return version of .NET runtime after installation complete', async () => {
const inputVersion = '8.0.402';
const inputQuality = '' as QualityOptions;
const stdout = `Fictitious dotnet runtime version ${inputVersion} is installed`;
getExecOutputSpy.mockImplementation(() => {
return Promise.resolve({
exitCode: 0,
stdout: `${stdout}`,
stderr: ''
});
});
maxSatisfyingSpy.mockImplementation(() => inputVersion);
const dotnetInstaller = new installer.DotnetCoreInstaller(
inputVersion,
inputQuality
);
const installedVersion = await dotnetInstaller.installRuntime();
expect(installedVersion).toBe(inputVersion);
});
it(`should supply '--runtime dotnet' and '--runtime aspnetcore' arguments to the installation script`, async () => {
const inputVersion = '8.0.402';
const inputQuality = '' as QualityOptions;
const stdout = `Fictitious dotnet runtime version ${inputVersion} is installed`;
getExecOutputSpy.mockImplementation(() => {
return Promise.resolve({
exitCode: 0,
stdout: `${stdout}`,
stderr: ''
});
});
maxSatisfyingSpy.mockImplementation(() => inputVersion);
const dotnetInstaller = new installer.DotnetCoreInstaller(
inputVersion,
inputQuality
);
await dotnetInstaller.installRuntime();
// Check first call installs .NET runtime
const dotnetScriptArguments = (
getExecOutputSpy.mock.calls[0][1] as string[]
).join(' ');
const expectedDotnetArgument = IS_WINDOWS
? `-Runtime dotnet`
: `--runtime dotnet`;
expect(dotnetScriptArguments).toContain(expectedDotnetArgument);
// Check second call installs ASP.NET Core runtime
const aspnetcoreScriptArguments = (
getExecOutputSpy.mock.calls[1][1] as string[]
).join(' ');
const expectedAspnetcoreArgument = IS_WINDOWS
? `-Runtime aspnetcore`
: `--runtime aspnetcore`;
expect(aspnetcoreScriptArguments).toContain(expectedAspnetcoreArgument);
});
it(`should supply 'version' argument to both runtime installation scripts if supplied version is in A.B.C syntax`, async () => {
const inputVersion = '8.0.402';
const inputQuality = '' as QualityOptions;
const stdout = `Fictitious dotnet runtime version ${inputVersion} is installed`;
getExecOutputSpy.mockImplementation(() => {
return Promise.resolve({
exitCode: 0,
stdout: `${stdout}`,
stderr: ''
});
});
maxSatisfyingSpy.mockImplementation(() => inputVersion);
const dotnetInstaller = new installer.DotnetCoreInstaller(
inputVersion,
inputQuality
);
await dotnetInstaller.installRuntime();
const expectedArgument = IS_WINDOWS
? `-Version ${inputVersion}`
: `--version ${inputVersion}`;
// Check both calls contain version argument
const dotnetScriptArguments = (
getExecOutputSpy.mock.calls[0][1] as string[]
).join(' ');
expect(dotnetScriptArguments).toContain(expectedArgument);
const aspnetcoreScriptArguments = (
getExecOutputSpy.mock.calls[1][1] as string[]
).join(' ');
expect(aspnetcoreScriptArguments).toContain(expectedArgument);
});
});
});
describe('DotnetVersionResolver tests', () => {

View File

@ -7,6 +7,8 @@ branding:
inputs:
dotnet-version:
description: 'Optional SDK version(s) to use. If not provided, will install global.json version when available. Examples: 2.2.104, 3.1, 3.1.x, 3.x, 6.0.2xx'
dotnet-runtime:
description: 'Optional runtime version(s) to install. Supports same version syntax as dotnet-version. Examples: 8.0.x, 9.0.x, 8.0.402'
dotnet-quality:
description: 'Optional quality of the build. The possible values are: daily, signed, validated, preview, ga.'
global-json-file:

60
dist/setup/index.js vendored
View File

@ -100716,6 +100716,42 @@ class DotnetCoreInstaller {
}
return this.parseInstalledVersion(dotnetInstallOutput.stdout);
}
async installRuntime() {
const versionResolver = new DotnetVersionResolver(this.version);
const dotnetVersion = await versionResolver.createDotnetVersion();
/**
* Install .NET runtime (Microsoft.NETCore.App)
* Skip non-versioned files to avoid overwriting CLI
*/
const dotnetRuntimeOutput = await new DotnetInstallScript()
// If dotnet CLI is already installed - avoid overwriting it
.useArguments(utils_1.IS_WINDOWS ? '-SkipNonVersionedFiles' : '--skip-non-versioned-files')
// Install .NET runtime (Microsoft.NETCore.App)
.useArguments(utils_1.IS_WINDOWS ? '-Runtime' : '--runtime', 'dotnet')
// Use version provided by user
.useVersion(dotnetVersion, this.quality)
.execute();
if (dotnetRuntimeOutput.exitCode) {
throw new Error(`Failed to install dotnet runtime, exit code: ${dotnetRuntimeOutput.exitCode}. ${dotnetRuntimeOutput.stderr}`);
}
/**
* Install ASP.NET Core runtime (Microsoft.AspNetCore.App)
* Skip non-versioned files to avoid overwriting CLI
*/
const aspnetcoreRuntimeOutput = await new DotnetInstallScript()
// If dotnet CLI is already installed - avoid overwriting it
.useArguments(utils_1.IS_WINDOWS ? '-SkipNonVersionedFiles' : '--skip-non-versioned-files')
// Install ASP.NET Core runtime (Microsoft.AspNetCore.App)
.useArguments(utils_1.IS_WINDOWS ? '-Runtime' : '--runtime', 'aspnetcore')
// Use version provided by user
.useVersion(dotnetVersion, this.quality)
.execute();
if (aspnetcoreRuntimeOutput.exitCode) {
throw new Error(`Failed to install aspnetcore runtime, exit code: ${aspnetcoreRuntimeOutput.exitCode}. ${aspnetcoreRuntimeOutput.stderr}`);
}
// Return the .NET runtime version (both should be the same version)
return this.parseInstalledVersion(dotnetRuntimeOutput.stdout);
}
parseInstalledVersion(stdout) {
const regex = /(?<version>\d+\.\d+\.\d+[a-z0-9._-]*)/gm;
const matchedResult = regex.exec(stdout);
@ -100796,6 +100832,7 @@ async function run() {
//
// dotnet-version is optional, but needs to be provided for most use cases.
// If supplied, install / use from the tool cache.
// dotnet-runtime is optional and allows installing runtime-only versions.
// global-version-file may be specified to point to a specific global.json
// and will be used to install an additional version.
// If not supplied, look for version in ./global.json.
@ -100803,7 +100840,9 @@ async function run() {
// Proxy, auth, (etc) are still set up, even if no version is identified
//
const versions = core.getMultilineInput('dotnet-version');
const runtimeVersions = core.getMultilineInput('dotnet-runtime');
const installedDotnetVersions = [];
const installedRuntimeVersions = [];
const globalJsonFileInput = core.getInput('global-json-file');
if (globalJsonFileInput) {
const globalJsonPath = path_1.default.resolve(process.cwd(), globalJsonFileInput);
@ -100823,11 +100862,11 @@ async function run() {
core.info(`The global.json wasn't found in the root directory. No .NET version will be installed.`);
}
}
const quality = core.getInput('dotnet-quality');
if (quality && !qualityOptions.includes(quality)) {
throw new Error(`Value '${quality}' is not supported for the 'dotnet-quality' option. Supported values are: daily, signed, validated, preview, ga.`);
}
if (versions.length) {
const quality = core.getInput('dotnet-quality');
if (quality && !qualityOptions.includes(quality)) {
throw new Error(`Value '${quality}' is not supported for the 'dotnet-quality' option. Supported values are: daily, signed, validated, preview, ga.`);
}
let dotnetInstaller;
const uniqueVersions = new Set(versions);
for (const version of uniqueVersions) {
@ -100837,6 +100876,19 @@ async function run() {
}
installer_1.DotnetInstallDir.addToPath();
}
if (runtimeVersions.length) {
let dotnetInstaller;
const uniqueRuntimeVersions = new Set(runtimeVersions);
for (const runtimeVersion of uniqueRuntimeVersions) {
dotnetInstaller = new installer_1.DotnetCoreInstaller(runtimeVersion, quality);
const installedRuntimeVersion = await dotnetInstaller.installRuntime();
installedRuntimeVersions.push(installedRuntimeVersion);
}
// Ensure PATH is set (may have been set already by SDK installation)
if (!versions.length) {
installer_1.DotnetInstallDir.addToPath();
}
}
const sourceUrl = core.getInput('source-url');
const configFile = core.getInput('config-file');
if (sourceUrl) {

7
package-lock.json generated
View File

@ -360,6 +360,7 @@
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.23.3.tgz",
"integrity": "sha512-Jg+msLuNuCJDyBvFv5+OKOUjWMZgd85bKjbICd3zWrKAo+bJ49HJufi7CQE0q0uR8NGyO6xkCACScNqyjHSZew==",
"dev": true,
"peer": true,
"dependencies": {
"@ampproject/remapping": "^2.2.0",
"@babel/code-frame": "^7.22.13",
@ -1416,6 +1417,7 @@
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/@octokit/core/-/core-5.0.1.tgz",
"integrity": "sha512-lyeeeZyESFo+ffI801SaBKmCfsvarO+dgV8/0gD8u1d87clbEdWsP5yC+dSj3zLhb2eIf5SJrn6vDz9AheETHw==",
"peer": true,
"dependencies": {
"@octokit/auth-token": "^4.0.0",
"@octokit/graphql": "^7.0.0",
@ -2521,6 +2523,7 @@
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz",
"integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==",
"dev": true,
"peer": true,
"bin": {
"acorn": "bin/acorn"
},
@ -2792,6 +2795,7 @@
"url": "https://github.com/sponsors/ai"
}
],
"peer": true,
"dependencies": {
"caniuse-lite": "^1.0.30001541",
"electron-to-chromium": "^1.4.535",
@ -3248,6 +3252,7 @@
"resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz",
"integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==",
"dev": true,
"peer": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.2.0",
"@eslint-community/regexpp": "^4.6.1",
@ -4269,6 +4274,7 @@
"resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz",
"integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==",
"dev": true,
"peer": true,
"dependencies": {
"@jest/core": "^29.7.0",
"@jest/types": "^29.6.3",
@ -6038,6 +6044,7 @@
"integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==",
"dev": true,
"license": "Apache-2.0",
"peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"

View File

@ -311,6 +311,56 @@ export class DotnetCoreInstaller {
return this.parseInstalledVersion(dotnetInstallOutput.stdout);
}
public async installRuntime(): Promise<string | null> {
const versionResolver = new DotnetVersionResolver(this.version);
const dotnetVersion = await versionResolver.createDotnetVersion();
/**
* Install .NET runtime (Microsoft.NETCore.App)
* Skip non-versioned files to avoid overwriting CLI
*/
const dotnetRuntimeOutput = await new DotnetInstallScript()
// If dotnet CLI is already installed - avoid overwriting it
.useArguments(
IS_WINDOWS ? '-SkipNonVersionedFiles' : '--skip-non-versioned-files'
)
// Install .NET runtime (Microsoft.NETCore.App)
.useArguments(IS_WINDOWS ? '-Runtime' : '--runtime', 'dotnet')
// Use version provided by user
.useVersion(dotnetVersion, this.quality)
.execute();
if (dotnetRuntimeOutput.exitCode) {
throw new Error(
`Failed to install dotnet runtime, exit code: ${dotnetRuntimeOutput.exitCode}. ${dotnetRuntimeOutput.stderr}`
);
}
/**
* Install ASP.NET Core runtime (Microsoft.AspNetCore.App)
* Skip non-versioned files to avoid overwriting CLI
*/
const aspnetcoreRuntimeOutput = await new DotnetInstallScript()
// If dotnet CLI is already installed - avoid overwriting it
.useArguments(
IS_WINDOWS ? '-SkipNonVersionedFiles' : '--skip-non-versioned-files'
)
// Install ASP.NET Core runtime (Microsoft.AspNetCore.App)
.useArguments(IS_WINDOWS ? '-Runtime' : '--runtime', 'aspnetcore')
// Use version provided by user
.useVersion(dotnetVersion, this.quality)
.execute();
if (aspnetcoreRuntimeOutput.exitCode) {
throw new Error(
`Failed to install aspnetcore runtime, exit code: ${aspnetcoreRuntimeOutput.exitCode}. ${aspnetcoreRuntimeOutput.stderr}`
);
}
// Return the .NET runtime version (both should be the same version)
return this.parseInstalledVersion(dotnetRuntimeOutput.stdout);
}
private parseInstalledVersion(stdout: string): string | null {
const regex = /(?<version>\d+\.\d+\.\d+[a-z0-9._-]*)/gm;
const matchedResult = regex.exec(stdout);

View File

@ -24,6 +24,7 @@ export async function run() {
//
// dotnet-version is optional, but needs to be provided for most use cases.
// If supplied, install / use from the tool cache.
// dotnet-runtime is optional and allows installing runtime-only versions.
// global-version-file may be specified to point to a specific global.json
// and will be used to install an additional version.
// If not supplied, look for version in ./global.json.
@ -31,7 +32,9 @@ export async function run() {
// Proxy, auth, (etc) are still set up, even if no version is identified
//
const versions = core.getMultilineInput('dotnet-version');
const runtimeVersions = core.getMultilineInput('dotnet-runtime');
const installedDotnetVersions: (string | null)[] = [];
const installedRuntimeVersions: (string | null)[] = [];
const globalJsonFileInput = core.getInput('global-json-file');
if (globalJsonFileInput) {
@ -57,15 +60,15 @@ export async function run() {
}
}
const quality = core.getInput('dotnet-quality') as QualityOptions;
if (quality && !qualityOptions.includes(quality)) {
throw new Error(
`Value '${quality}' is not supported for the 'dotnet-quality' option. Supported values are: daily, signed, validated, preview, ga.`
);
}
if (versions.length) {
const quality = core.getInput('dotnet-quality') as QualityOptions;
if (quality && !qualityOptions.includes(quality)) {
throw new Error(
`Value '${quality}' is not supported for the 'dotnet-quality' option. Supported values are: daily, signed, validated, preview, ga.`
);
}
let dotnetInstaller: DotnetCoreInstaller;
const uniqueVersions = new Set<string>(versions);
for (const version of uniqueVersions) {
@ -76,6 +79,20 @@ export async function run() {
DotnetInstallDir.addToPath();
}
if (runtimeVersions.length) {
let dotnetInstaller: DotnetCoreInstaller;
const uniqueRuntimeVersions = new Set<string>(runtimeVersions);
for (const runtimeVersion of uniqueRuntimeVersions) {
dotnetInstaller = new DotnetCoreInstaller(runtimeVersion, quality);
const installedRuntimeVersion = await dotnetInstaller.installRuntime();
installedRuntimeVersions.push(installedRuntimeVersion);
}
// Ensure PATH is set (may have been set already by SDK installation)
if (!versions.length) {
DotnetInstallDir.addToPath();
}
}
const sourceUrl: string = core.getInput('source-url');
const configFile: string = core.getInput('config-file');
if (sourceUrl) {