Add dotnet-version: latest support with dotnet-channel input (#730)

* feat: add dotnet-version: latest keyword with dotnet-channel support (#497)

* restore test-proxy container image

* update e2e-tests.yml and documentation

* fix(tests): correct release-type and support-phase values in latest-version test mocks
This commit is contained in:
mahabaleshwars
2026-04-27 23:54:18 +05:30
committed by GitHub
parent df991aeaf2
commit af9211b136
9 changed files with 687 additions and 50 deletions

View File

@@ -9,7 +9,6 @@ import * as io from '@actions/io';
import * as installer from '../src/installer';
import {IS_WINDOWS} from '../src/utils';
import {QualityOptions} from '../src/setup-dotnet';
describe('installer tests', () => {
const env = process.env;
@@ -40,7 +39,7 @@ describe('installer tests', () => {
it('should throw the error in case of non-zero exit code of the installation script. The error message should contain logs.', async () => {
const inputVersion = '10.0.101';
const inputQuality = '' as QualityOptions;
const inputQuality = '';
const errorMessage = 'fictitious error message!';
getExecOutputSpy.mockImplementation(() => {
@@ -62,7 +61,7 @@ describe('installer tests', () => {
it('should return version of .NET SDK after installation complete', async () => {
const inputVersion = '10.0.101';
const inputQuality = '' as QualityOptions;
const inputQuality = '';
const stdout = `Fictitious dotnet version ${inputVersion} is installed`;
getExecOutputSpy.mockImplementation(() => {
return Promise.resolve({
@@ -84,7 +83,7 @@ describe('installer tests', () => {
it(`should supply 'version' argument to the installation script if supplied version is in A.B.C syntax`, async () => {
const inputVersion = '10.0.101';
const inputQuality = '' as QualityOptions;
const inputQuality = '';
const stdout = `Fictitious dotnet version ${inputVersion} is installed`;
getExecOutputSpy.mockImplementation(() => {
@@ -122,7 +121,7 @@ describe('installer tests', () => {
it(`should warn if the 'quality' input is set and the supplied version is in A.B.C syntax`, async () => {
const inputVersion = '10.0.101';
const inputQuality = 'ga' as QualityOptions;
const inputQuality = 'ga';
const stdout = `Fictitious dotnet version ${inputVersion} is installed`;
getExecOutputSpy.mockImplementation(() => {
return Promise.resolve({
@@ -147,7 +146,7 @@ describe('installer tests', () => {
it(`should warn if the 'quality' input is set and version isn't in A.B.C syntax but major tag is lower then 6`, async () => {
const inputVersion = '3.1';
const inputQuality = 'ga' as QualityOptions;
const inputQuality = 'ga';
const stdout = `Fictitious dotnet version ${inputVersion} is installed`;
getExecOutputSpy.mockImplementation(() => {
@@ -174,7 +173,7 @@ describe('installer tests', () => {
each(['10', '10.0', '10.0.x', '10.0.*', '10.0.X']).test(
`should supply 'quality' argument to the installation script if quality input is set and version (%s) is not in A.B.C syntax`,
async inputVersion => {
const inputQuality = 'ga' as QualityOptions;
const inputQuality = 'ga';
const exitCode = 0;
const stdout = `Fictitious dotnet version ${inputVersion} is installed`;
getExecOutputSpy.mockImplementation(() => {
@@ -214,7 +213,7 @@ describe('installer tests', () => {
each(['10', '10.0', '10.0.x', '10.0.*', '10.0.X']).test(
`should supply 'channel' argument to the installation script if version (%s) isn't in A.B.C syntax`,
async inputVersion => {
const inputQuality = '' as QualityOptions;
const inputQuality = '';
const exitCode = 0;
const stdout = `Fictitious dotnet version ${inputVersion} is installed`;
getExecOutputSpy.mockImplementation(() => {
@@ -255,7 +254,7 @@ describe('installer tests', () => {
it(`should supply '-ProxyAddress' argument to the installation script if env.variable 'https_proxy' is set`, async () => {
process.env['https_proxy'] = 'https://proxy.com';
const inputVersion = '10.0.101';
const inputQuality = '' as QualityOptions;
const inputQuality = '';
const stdout = `Fictitious dotnet version ${inputVersion} is installed`;
getExecOutputSpy.mockImplementation(() => {
@@ -293,7 +292,7 @@ describe('installer tests', () => {
it(`should supply '-ProxyBypassList' argument to the installation script if env.variable 'no_proxy' is set`, async () => {
process.env['no_proxy'] = 'first.url,second.url';
const inputVersion = '10.0.101';
const inputQuality = '' as QualityOptions;
const inputQuality = '';
const stdout = `Fictitious dotnet version ${inputVersion} is installed`;
getExecOutputSpy.mockImplementation(() => {
@@ -331,7 +330,7 @@ describe('installer tests', () => {
it(`should supply 'architecture' argument to the installation script when architecture is provided`, async () => {
const inputVersion = '10.0.101';
const inputQuality = '' as QualityOptions;
const inputQuality = '';
const inputArchitecture = 'x64';
const stdout = `Fictitious dotnet version ${inputVersion} is installed`;
@@ -365,7 +364,7 @@ describe('installer tests', () => {
it(`should NOT supply 'architecture' argument when architecture is not provided`, async () => {
const inputVersion = '10.0.101';
const inputQuality = '' as QualityOptions;
const inputQuality = '';
const stdout = `Fictitious dotnet version ${inputVersion} is installed`;
getExecOutputSpy.mockImplementation(() => {
@@ -395,7 +394,7 @@ describe('installer tests', () => {
it(`should supply 'install-dir' with arch subdirectory for cross-arch install`, async () => {
const inputVersion = '10.0.101';
const inputQuality = '' as QualityOptions;
const inputQuality = '';
const inputArchitecture = 'x64';
const stdout = `Fictitious dotnet version ${inputVersion} is installed`;
@@ -436,7 +435,7 @@ describe('installer tests', () => {
it(`should NOT supply 'install-dir' when architecture matches runner's native arch`, async () => {
const inputVersion = '10.0.101';
const inputQuality = '' as QualityOptions;
const inputQuality = '';
const nativeArch = os.arch().toLowerCase();
const stdout = `Fictitious dotnet version ${inputVersion} is installed`;

View File

@@ -0,0 +1,223 @@
import {DotnetVersionResolver} from '../src/installer';
import * as hc from '@actions/http-client';
import * as core from '@actions/core';
// Mock http-client
jest.mock('@actions/http-client');
describe('DotnetVersionResolver with latest', () => {
let getJsonMock: jest.Mock;
let warningSpy: jest.SpyInstance;
beforeEach(() => {
getJsonMock = jest.fn();
(hc.HttpClient as any).mockImplementation(() => {
return {
getJson: getJsonMock
};
});
warningSpy = jest.spyOn(core, 'warning').mockImplementation(() => {});
});
afterEach(() => {
jest.clearAllMocks();
jest.restoreAllMocks();
});
const mockReleases = {
'releases-index': [
{
'channel-version': '10.0',
'support-phase': 'preview',
'release-type': 'lts'
},
{
'channel-version': '9.0',
'support-phase': 'active',
'release-type': 'sts'
},
{
'channel-version': '8.0',
'support-phase': 'active',
'release-type': 'lts'
},
{
'channel-version': '7.0',
'support-phase': 'eol',
'release-type': 'sts'
}
]
};
it('should resolve "latest" to highest stable version by default', async () => {
getJsonMock.mockResolvedValue({result: mockReleases});
const resolver = new DotnetVersionResolver('latest');
const version = await resolver.createDotnetVersion();
expect(version.value).toBe('9.0');
expect(version.type.toLowerCase()).toContain('channel');
expect(version.qualityFlag).toBe(true);
});
it('should resolve "LATEST" (uppercase) to highest stable version', async () => {
getJsonMock.mockResolvedValue({result: mockReleases});
const resolver = new DotnetVersionResolver('LATEST');
const version = await resolver.createDotnetVersion();
expect(version.value).toBe('9.0');
expect(version.type.toLowerCase()).toContain('channel');
expect(version.qualityFlag).toBe(true);
});
it('should resolve "latest" to highest preview version if quality is preview', async () => {
getJsonMock.mockResolvedValue({result: mockReleases});
const resolver = new DotnetVersionResolver('latest', 'preview');
const version = await resolver.createDotnetVersion();
expect(version.value).toBe('10.0');
});
it('should resolve "latest" with channel filter LTS', async () => {
getJsonMock.mockResolvedValue({result: mockReleases});
const resolver = new DotnetVersionResolver('latest', '', 'LTS');
const version = await resolver.createDotnetVersion();
expect(version.value).toBe('8.0');
});
it('should resolve "latest" with channel filter STS', async () => {
getJsonMock.mockResolvedValue({result: mockReleases});
const resolver = new DotnetVersionResolver('latest', '', 'STS');
const version = await resolver.createDotnetVersion();
expect(version.value).toBe('9.0');
});
it('should resolve "latest" with channel filter STS and preview quality', async () => {
getJsonMock.mockResolvedValue({result: mockReleases});
const resolver = new DotnetVersionResolver('latest', 'preview', 'STS');
const version = await resolver.createDotnetVersion();
// preview quality includes all support-phases; STS filter → 9.0 (active, sts)
expect(version.value).toBe('9.0');
});
it('should warn if channel is provided but version is not latest', async () => {
getJsonMock.mockResolvedValue({result: mockReleases});
const resolver = new DotnetVersionResolver('8.0', '', 'LTS');
await resolver.createDotnetVersion();
expect(warningSpy).toHaveBeenCalledWith(
`The 'dotnet-channel' input is only supported when 'dotnet-version' is set to 'latest'.`
);
});
it('should throw when releases-index API returns empty active releases', async () => {
const emptyReleases = {
'releases-index': [
{
'channel-version': '7.0',
'support-phase': 'eol',
'release-type': 'sts'
}
]
};
getJsonMock.mockResolvedValue({result: emptyReleases});
const resolver = new DotnetVersionResolver('latest');
await expect(resolver.createDotnetVersion()).rejects.toThrow(
/Could not find any active releases/
);
});
it('should throw when releases-index response has unexpected format', async () => {
getJsonMock.mockResolvedValue({result: {}});
const resolver = new DotnetVersionResolver('latest');
await expect(resolver.createDotnetVersion()).rejects.toThrow(
/Unexpected response format/
);
});
it('should throw when releases-index response is null', async () => {
getJsonMock.mockResolvedValue({result: null});
const resolver = new DotnetVersionResolver('latest');
await expect(resolver.createDotnetVersion()).rejects.toThrow(
/Unexpected response format/
);
});
it('should resolve "latest" with ga quality same as default (no previews)', async () => {
getJsonMock.mockResolvedValue({result: mockReleases});
const resolver = new DotnetVersionResolver('latest', 'ga');
const version = await resolver.createDotnetVersion();
// ga should behave like no quality — skip preview (10.0), pick 9.0
expect(version.value).toBe('9.0');
});
it('should resolve "latest" with LTS channel and daily quality', async () => {
getJsonMock.mockResolvedValue({result: mockReleases});
const resolver = new DotnetVersionResolver('latest', 'daily', 'LTS');
const version = await resolver.createDotnetVersion();
// daily allows previews, LTS filter applies — 10.0 (preview, lts) is the highest LTS
expect(version.value).toBe('10.0');
expect(version.qualityFlag).toBe(true);
});
it('should resolve "latest" with A.B channel directly without API call', async () => {
const resolver = new DotnetVersionResolver('latest', '', '8.0');
const version = await resolver.createDotnetVersion();
expect(version.value).toBe('8.0');
expect(version.type.toLowerCase()).toContain('channel');
expect(version.qualityFlag).toBe(true);
// Should NOT call the API
expect(getJsonMock).not.toHaveBeenCalled();
});
it('should resolve "latest" with A.B.Cxx channel directly without API call', async () => {
const resolver = new DotnetVersionResolver('latest', '', '8.0.1xx');
const version = await resolver.createDotnetVersion();
expect(version.value).toBe('8.0.1xx');
expect(version.type.toLowerCase()).toContain('channel');
expect(version.qualityFlag).toBe(true);
// Should NOT call the API
expect(getJsonMock).not.toHaveBeenCalled();
});
it('should resolve "latest" with A.B channel for older version with qualityFlag false', async () => {
const resolver = new DotnetVersionResolver('latest', '', '3.1');
const version = await resolver.createDotnetVersion();
expect(version.value).toBe('3.1');
expect(version.type.toLowerCase()).toContain('channel');
// major 3 < 6 → qualityFlag false
expect(version.qualityFlag).toBe(false);
expect(getJsonMock).not.toHaveBeenCalled();
});
it('should resolve "latest" with A.B.Cxx channel and quality', async () => {
const resolver = new DotnetVersionResolver('latest', 'ga', '8.0.2xx');
const version = await resolver.createDotnetVersion();
expect(version.value).toBe('8.0.2xx');
expect(version.qualityFlag).toBe(true);
expect(getJsonMock).not.toHaveBeenCalled();
});
});

View File

@@ -84,7 +84,7 @@ describe('setup-dotnet tests', () => {
inputs['dotnet-version'] = ['10.0'];
inputs['dotnet-quality'] = 'fictitiousQuality';
const expectedErrorMessage = `Value '${inputs['dotnet-quality']}' is not supported for the 'dotnet-quality' option. Supported values are: daily, signed, validated, preview, ga.`;
const expectedErrorMessage = `Value '${inputs['dotnet-quality']}' is not supported for the 'dotnet-quality' option. Supported values are: daily, preview, ga.`;
await setup.run();
expect(setFailedSpy).toHaveBeenCalledWith(expectedErrorMessage);
@@ -256,5 +256,95 @@ describe('setup-dotnet tests', () => {
await setup.run();
expect(setFailedSpy).toHaveBeenCalledWith(expectedErrorMessage);
});
it('should fail the action if unsupported dotnet-channel value is provided with latest', async () => {
inputs['dotnet-version'] = ['latest'];
inputs['dotnet-quality'] = '';
inputs['dotnet-channel'] = 'invalid';
inputs['architecture'] = '';
const expectedErrorMessage = `Value 'invalid' is not supported for the 'dotnet-channel' option. Supported values are: LTS, STS, A.B (e.g. 8.0), A.B.Cxx (e.g. 8.0.1xx).`;
await setup.run();
expect(setFailedSpy).toHaveBeenCalledWith(expectedErrorMessage);
});
it('should warn but not fail if unsupported dotnet-channel value is provided with a specific version', async () => {
inputs['dotnet-version'] = ['8.0.x'];
inputs['dotnet-quality'] = '';
inputs['dotnet-channel'] = 'invalid';
inputs['architecture'] = '';
installDotnetSpy.mockImplementation(() => Promise.resolve(''));
await setup.run();
expect(setFailedSpy).not.toHaveBeenCalled();
expect(warningSpy).toHaveBeenCalledWith(
`Value 'invalid' is not supported for the 'dotnet-channel' option and will be ignored because 'dotnet-version' is not set to 'latest'. Supported values are: LTS, STS, A.B (e.g. 8.0), A.B.Cxx (e.g. 8.0.1xx).`
);
});
it('should pass valid dotnet-channel value through without error', async () => {
inputs['dotnet-version'] = ['latest'];
inputs['dotnet-quality'] = '';
inputs['dotnet-channel'] = 'LTS';
inputs['architecture'] = '';
installDotnetSpy.mockImplementation(() => Promise.resolve(''));
await setup.run();
expect(setFailedSpy).not.toHaveBeenCalled();
});
it('should pass A.B channel value through without error when used with latest', async () => {
inputs['dotnet-version'] = ['latest'];
inputs['dotnet-quality'] = '';
inputs['dotnet-channel'] = '8.0';
inputs['architecture'] = '';
installDotnetSpy.mockImplementation(() => Promise.resolve(''));
await setup.run();
expect(setFailedSpy).not.toHaveBeenCalled();
});
it('should pass A.B.Cxx channel value through without error when used with latest', async () => {
inputs['dotnet-version'] = ['latest'];
inputs['dotnet-quality'] = '';
inputs['dotnet-channel'] = '8.0.1xx';
inputs['architecture'] = '';
installDotnetSpy.mockImplementation(() => Promise.resolve(''));
await setup.run();
expect(setFailedSpy).not.toHaveBeenCalled();
});
it('should fail with A.B.Cxx channel if major version is below 5', async () => {
inputs['dotnet-version'] = ['latest'];
inputs['dotnet-quality'] = '';
inputs['dotnet-channel'] = '3.1.1xx';
inputs['architecture'] = '';
const expectedErrorMessage = `Value '3.1.1xx' is not supported for the 'dotnet-channel' option. Supported values are: LTS, STS, A.B (e.g. 8.0), A.B.Cxx (e.g. 8.0.1xx).`;
await setup.run();
expect(setFailedSpy).toHaveBeenCalledWith(expectedErrorMessage);
});
it('should warn and not fail if valid dotnet-channel is provided with a non-latest version', async () => {
inputs['dotnet-version'] = ['8.0.x'];
inputs['dotnet-quality'] = '';
inputs['dotnet-channel'] = 'LTS';
inputs['architecture'] = '';
installDotnetSpy.mockImplementation(() => Promise.resolve(''));
await setup.run();
expect(setFailedSpy).not.toHaveBeenCalled();
expect(warningSpy).toHaveBeenCalledWith(
`The 'dotnet-channel' input is only supported when 'dotnet-version' is set to 'latest'.`
);
});
});
});