mirror of
https://github.com/actions/setup-dotnet.git
synced 2026-09-29 17:55:07 +00:00
Add check-latest input to reuse a locally installed .NET SDK and skip network calls (#774)
* check-latest-input feature implemented * e2e test update * e2e-tests.yml updated * update documentation * fix: align local SDK matching with the online resolver * implement review comments * implement review comments * implement review comments * implement review comments * implement review comments * implement review comments * implement review comments --------- Co-authored-by: mahabaleshwars <147705296+mahabaleshwars@users.noreply.github.com>
This commit is contained in:
@@ -738,3 +738,34 @@ jobs:
|
|||||||
- name: Verify dotnet
|
- name: Verify dotnet
|
||||||
shell: pwsh
|
shell: pwsh
|
||||||
run: __tests__/verify-dotnet.ps1 -Patterns "^9\.0\.1\d{2}"
|
run: __tests__/verify-dotnet.ps1 -Patterns "^9\.0\.1\d{2}"
|
||||||
|
|
||||||
|
test-check-latest-false:
|
||||||
|
runs-on: ${{ matrix.operating-system }}
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
operating-system: [ubuntu-latest, windows-latest, macos-latest]
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
|
- name: Clear toolcache
|
||||||
|
shell: pwsh
|
||||||
|
run: __tests__/clear-toolcache.ps1 ${{ runner.os }}
|
||||||
|
# Oldest 9.0.1xx SDK: resolving '9.0.1xx' online would return a newer patch.
|
||||||
|
- name: Setup dotnet 9.0.100
|
||||||
|
uses: ./
|
||||||
|
with:
|
||||||
|
dotnet-version: '9.0.100'
|
||||||
|
- name: Setup dotnet 9.0.1xx with check-latest false
|
||||||
|
id: reuse
|
||||||
|
uses: ./
|
||||||
|
with:
|
||||||
|
dotnet-version: '9.0.1xx'
|
||||||
|
check-latest: false
|
||||||
|
- name: Verify dotnet
|
||||||
|
shell: pwsh
|
||||||
|
run: |
|
||||||
|
if ('${{ steps.reuse.outputs.dotnet-version }}' -ne '9.0.100') {
|
||||||
|
throw "Expected 9.0.100 to be reused, got '${{ steps.reuse.outputs.dotnet-version }}'."
|
||||||
|
}
|
||||||
|
__tests__/verify-dotnet.ps1 -Patterns "^9\.0\.100$"
|
||||||
|
|||||||
@@ -323,6 +323,29 @@ steps:
|
|||||||
|
|
||||||
> **Note**: Ensure workloads are compatible with your runner's OS, architecture, and .NET SDK version before enabling workload installation. Some workloads may require additional installation time due to large toolchain downloads.
|
> **Note**: Ensure workloads are compatible with your runner's OS, architecture, and .NET SDK version before enabling workload installation. Some workloads may require additional installation time due to large toolchain downloads.
|
||||||
|
|
||||||
|
## Using the `check-latest` input
|
||||||
|
|
||||||
|
By default (`check-latest: true`) the action resolves and installs the latest version matching the request online, which is the historical behavior. With `check-latest: false` it first looks for an SDK already installed under [`DOTNET_INSTALL_DIR`](#environment-variables) and reuses it, skipping SDK resolution and download — useful for air-gapped or preloaded runners (the `cache` and `workloads` inputs still use the network). If none matches, it installs online as usual.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v7
|
||||||
|
- name: Reuse a preinstalled SDK when available
|
||||||
|
uses: actions/setup-dotnet@v6
|
||||||
|
with:
|
||||||
|
dotnet-version: '8.0.x'
|
||||||
|
check-latest: false
|
||||||
|
- run: dotnet build <my project>
|
||||||
|
```
|
||||||
|
|
||||||
|
An installed SDK is reused only when it matches the requested spec (`A.B.C`, `A.B`, `A.B.x`, `A.B.Cxx`, `A`, `A.x` or `latest`; `x`, `X` and `*` interchange except in a feature band, which is lowercase `Cxx`), satisfies `dotnet-quality`, is not older than the `sdk.version` declared in `global.json`, and sits next to a runnable `dotnet` executable. As online, `dotnet-quality` is ignored for a full `A.B.C` request, so such a request always resolves to a GA SDK. Cross-architecture requests, `latest` with an `LTS` or `STS` channel, and a bare wildcard always install online.
|
||||||
|
|
||||||
|
When the request comes from `global.json`, the SDK is picked the way the .NET resolver picks one: `latestPatch`, `latestFeature`, `latestMinor` and `latestMajor` take the **highest** SDK the policy allows, while `feature`, `minor` and `major` roll forward to the **nearest** higher feature band, minor or major and then take its latest patch. `patch` — the default when `rollForward` is omitted — prefers the declared version, otherwise the latest patch in its band. `disable` and a prerelease `sdk.version` require an exact match, and `allowPrerelease` is not used: `dotnet-quality` is the only prerelease control.
|
||||||
|
|
||||||
|
### Setting `check-latest` from the environment
|
||||||
|
|
||||||
|
When the input is not set, the action reads the `DOTNET_CHECK_LATEST` environment variable (`true` or `false`, case-insensitive; an unsupported value warns and is ignored), which covers workflows you cannot edit. Setting it on a self-hosted runner alongside SDKs preinstalled under [`DOTNET_INSTALL_DIR`](#environment-variables) lets those workflows run without reaching `aka.ms`, `builds.dotnet.microsoft.com` or `ci.dot.net`. It applies to every job on that runner, so a dedicated runner is recommended; any workflow can still override it with an explicit `check-latest` input.
|
||||||
|
|
||||||
# Outputs and environment variables
|
# Outputs and environment variables
|
||||||
|
|
||||||
## Outputs
|
## Outputs
|
||||||
@@ -381,6 +404,7 @@ Some environment variables may be necessary for your particular case or to impro
|
|||||||
| **Env.variable** | **Description** | **Default value** |
|
| **Env.variable** | **Description** | **Default value** |
|
||||||
| ----------- | ----------- | ----------- |
|
| ----------- | ----------- | ----------- |
|
||||||
| DOTNET_INSTALL_DIR |Specifies a directory where .NET SDKs should be installed by the action.|*default value for each OS* |
|
| DOTNET_INSTALL_DIR |Specifies a directory where .NET SDKs should be installed by the action.|*default value for each OS* |
|
||||||
|
| DOTNET_CHECK_LATEST |Fallback for the [`check-latest`](#setting-check-latest-from-the-environment) input, for workflows that cannot set it. Ignored when the input is set.|*true*|
|
||||||
| DOTNET_NOLOGO |Removes logo and telemetry message from first run of dotnet cli|*false*|
|
| DOTNET_NOLOGO |Removes logo and telemetry message from first run of dotnet cli|*false*|
|
||||||
| DOTNET_CLI_TELEMETRY_OPTOUT |Opt-out of telemetry being sent to Microsoft|*false*|
|
| DOTNET_CLI_TELEMETRY_OPTOUT |Opt-out of telemetry being sent to Microsoft|*false*|
|
||||||
| DOTNET_MULTILEVEL_LOOKUP |Configures whether the global install location is used as a fall-back|*true*|
|
| DOTNET_MULTILEVEL_LOOKUP |Configures whether the global install location is used as a fall-back|*true*|
|
||||||
|
|||||||
+707
-1
@@ -1,5 +1,6 @@
|
|||||||
import {
|
import {
|
||||||
afterAll,
|
afterAll,
|
||||||
|
afterEach,
|
||||||
beforeAll,
|
beforeAll,
|
||||||
beforeEach,
|
beforeEach,
|
||||||
describe,
|
describe,
|
||||||
@@ -34,10 +35,25 @@ jest.unstable_mockModule('@actions/io', () => ({
|
|||||||
jest.unstable_mockModule('fs', () => {
|
jest.unstable_mockModule('fs', () => {
|
||||||
const actual = jest.requireActual('fs') as typeof import('fs');
|
const actual = jest.requireActual('fs') as typeof import('fs');
|
||||||
const chmodSync = jest.fn();
|
const chmodSync = jest.fn();
|
||||||
|
const readdirSync = jest.fn();
|
||||||
|
const existsSync = jest.fn(() => true);
|
||||||
|
const statSync = jest.fn(() => ({isFile: () => true}));
|
||||||
|
const accessSync = jest.fn();
|
||||||
return {
|
return {
|
||||||
...actual,
|
...actual,
|
||||||
chmodSync,
|
chmodSync,
|
||||||
default: {...actual, chmodSync}
|
readdirSync,
|
||||||
|
existsSync,
|
||||||
|
statSync,
|
||||||
|
accessSync,
|
||||||
|
default: {
|
||||||
|
...actual,
|
||||||
|
chmodSync,
|
||||||
|
readdirSync,
|
||||||
|
existsSync,
|
||||||
|
statSync,
|
||||||
|
accessSync
|
||||||
|
}
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -514,6 +530,696 @@ describe('installer tests', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('check-latest: false (local SDK reuse) tests', () => {
|
||||||
|
const readdirSyncSpy = fs.readdirSync as unknown as jest.Mock;
|
||||||
|
const statSyncSpy = fs.statSync as unknown as jest.Mock;
|
||||||
|
const accessSyncSpy = fs.accessSync as unknown as jest.Mock;
|
||||||
|
|
||||||
|
const asFile = {isFile: () => true};
|
||||||
|
const muxerName = IS_WINDOWS ? 'dotnet.exe' : 'dotnet';
|
||||||
|
const isMuxer = (target: unknown) => String(target).endsWith(muxerName);
|
||||||
|
const throwEnoent = () => {
|
||||||
|
throw new Error('ENOENT');
|
||||||
|
};
|
||||||
|
|
||||||
|
const makeDirents = (names: string[]): any =>
|
||||||
|
names.map(name => ({
|
||||||
|
name,
|
||||||
|
isDirectory: () => true,
|
||||||
|
isSymbolicLink: () => false
|
||||||
|
}));
|
||||||
|
|
||||||
|
const makeSymlinks = (names: string[]): any =>
|
||||||
|
names.map(name => ({
|
||||||
|
name,
|
||||||
|
isDirectory: () => false,
|
||||||
|
isSymbolicLink: () => true
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
getExecOutputSpy.mockClear();
|
||||||
|
getExecOutputSpy.mockImplementation(() =>
|
||||||
|
Promise.resolve({
|
||||||
|
exitCode: 0,
|
||||||
|
stdout: 'Fictitious dotnet version 1.2.3 is installed',
|
||||||
|
stderr: ''
|
||||||
|
})
|
||||||
|
);
|
||||||
|
statSyncSpy.mockReturnValue(asFile);
|
||||||
|
accessSyncSpy.mockImplementation(() => undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
readdirSyncSpy.mockReset();
|
||||||
|
statSyncSpy.mockReset();
|
||||||
|
accessSyncSpy.mockReset();
|
||||||
|
});
|
||||||
|
|
||||||
|
interface LocalReuseCase {
|
||||||
|
name: string;
|
||||||
|
version: string;
|
||||||
|
quality?: string;
|
||||||
|
dotnetChannel?: string;
|
||||||
|
minimumVersion?: string;
|
||||||
|
installed: string[];
|
||||||
|
expected: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const localReuseCases: LocalReuseCase[] = [
|
||||||
|
{
|
||||||
|
name: 'reuses a locally installed pinned SDK and skips all install scripts',
|
||||||
|
version: '8.0.422',
|
||||||
|
installed: ['8.0.100', '8.0.422'],
|
||||||
|
expected: '8.0.422'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'falls back to online install when no local SDK matches the pinned request',
|
||||||
|
version: '8.0.422',
|
||||||
|
installed: ['8.0.100'],
|
||||||
|
expected: null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'reuses the highest matching patch for a floating A.B.x request',
|
||||||
|
version: '8.0.x',
|
||||||
|
installed: ['8.0.100', '8.0.412', '8.0.205'],
|
||||||
|
expected: '8.0.412'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'matches the correct feature band for an A.B.Cxx request',
|
||||||
|
version: '8.0.1xx',
|
||||||
|
installed: ['8.0.100', '8.0.105', '8.0.203'],
|
||||||
|
expected: '8.0.105'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'picks the highest installed SDK for a channel-less latest request',
|
||||||
|
version: 'latest',
|
||||||
|
installed: ['8.0.412', '9.0.101'],
|
||||||
|
expected: '9.0.101'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'ignores prerelease SDKs when quality is not preview/daily',
|
||||||
|
version: '8.0.x',
|
||||||
|
installed: ['8.0.100-preview.1', '8.0.100'],
|
||||||
|
expected: '8.0.100'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'reuses the highest installed SDK for a major-only request',
|
||||||
|
version: '8',
|
||||||
|
installed: ['8.0.100', '8.0.412', '8.0.205'],
|
||||||
|
expected: '8.0.412'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'reuses a prerelease SDK when quality is preview',
|
||||||
|
version: '8.0.x',
|
||||||
|
quality: 'preview',
|
||||||
|
installed: ['8.0.100-preview.1', '8.0.100-preview.2'],
|
||||||
|
expected: '8.0.100-preview.2'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'falls back to online install when no local SDK matches a floating request',
|
||||||
|
version: '9.0.x',
|
||||||
|
installed: ['8.0.412'],
|
||||||
|
expected: null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'installs online for a latest request with an LTS channel',
|
||||||
|
version: 'latest',
|
||||||
|
dotnetChannel: 'LTS',
|
||||||
|
installed: ['8.0.412', '9.0.101'],
|
||||||
|
expected: null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'reuses the SDK of the requested channel for a latest request',
|
||||||
|
version: 'latest',
|
||||||
|
dotnetChannel: '8.0',
|
||||||
|
installed: ['8.0.412', '9.0.101'],
|
||||||
|
expected: '8.0.412'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'installs online for a wildcard request instead of guessing',
|
||||||
|
version: 'x',
|
||||||
|
installed: ['8.0.412', '9.0.101'],
|
||||||
|
expected: null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'does not reuse a local SDK below the global.json minimum version',
|
||||||
|
version: '8.0',
|
||||||
|
minimumVersion: '8.0.400',
|
||||||
|
installed: ['8.0.100', '8.0.205'],
|
||||||
|
expected: null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'reuses a local SDK at or above the global.json minimum version',
|
||||||
|
version: '8.0',
|
||||||
|
minimumVersion: '8.0.400',
|
||||||
|
installed: ['8.0.100', '8.0.412'],
|
||||||
|
expected: '8.0.412'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'installs online when quality is preview but only GA SDKs are local',
|
||||||
|
version: '8.0.x',
|
||||||
|
quality: 'preview',
|
||||||
|
installed: ['8.0.412'],
|
||||||
|
expected: null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'ignores the quality input for majors below 6 when matching locally',
|
||||||
|
version: '3.1',
|
||||||
|
quality: 'preview',
|
||||||
|
installed: ['3.1.426', '3.1.500-preview.1'],
|
||||||
|
expected: '3.1.426'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
each(localReuseCases).it(
|
||||||
|
'$name',
|
||||||
|
async ({
|
||||||
|
version,
|
||||||
|
quality = '',
|
||||||
|
dotnetChannel,
|
||||||
|
minimumVersion,
|
||||||
|
installed,
|
||||||
|
expected
|
||||||
|
}: LocalReuseCase) => {
|
||||||
|
readdirSyncSpy.mockReturnValue(makeDirents(installed));
|
||||||
|
maxSatisfyingSpy.mockImplementation(() => '8.0.412');
|
||||||
|
|
||||||
|
const installedVersion = await new installer.DotnetCoreInstaller(
|
||||||
|
version,
|
||||||
|
quality as any,
|
||||||
|
undefined,
|
||||||
|
dotnetChannel,
|
||||||
|
false,
|
||||||
|
minimumVersion
|
||||||
|
).installDotnet();
|
||||||
|
|
||||||
|
if (expected === null) {
|
||||||
|
expect(getExecOutputSpy).toHaveBeenCalledTimes(2);
|
||||||
|
} else {
|
||||||
|
expect(installedVersion).toBe(expected);
|
||||||
|
expect(getExecOutputSpy).not.toHaveBeenCalled();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
it('rejects an A.B.Cxx request below .NET 5 instead of reusing a local SDK', async () => {
|
||||||
|
readdirSyncSpy.mockReturnValue(makeDirents(['3.1.100']));
|
||||||
|
|
||||||
|
const dotnetInstaller = new installer.DotnetCoreInstaller(
|
||||||
|
'3.1.1xx',
|
||||||
|
'',
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
false
|
||||||
|
);
|
||||||
|
|
||||||
|
await expect(dotnetInstaller.installDotnet()).rejects.toThrow(
|
||||||
|
`The 'dotnet-version' was supplied in invalid format: 3.1.1xx! The A.B.Cxx syntax is available since the .NET 5.0 release.`
|
||||||
|
);
|
||||||
|
expect(getExecOutputSpy).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
each(['preview', 'daily'] as const).it(
|
||||||
|
"reuses the highest local prerelease SDK for a channel-less latest request with '%s' quality",
|
||||||
|
async quality => {
|
||||||
|
readdirSyncSpy.mockReturnValue(
|
||||||
|
makeDirents(['8.0.412', '9.0.100-preview.1', '9.0.100-preview.2'])
|
||||||
|
);
|
||||||
|
|
||||||
|
const dotnetInstaller = new installer.DotnetCoreInstaller(
|
||||||
|
'latest',
|
||||||
|
quality,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
false
|
||||||
|
);
|
||||||
|
const installedVersion = await dotnetInstaller.installDotnet();
|
||||||
|
|
||||||
|
expect(installedVersion).toBe('9.0.100-preview.2');
|
||||||
|
expect(getExecOutputSpy).not.toHaveBeenCalled();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
it('does NOT reuse a local SDK for a cross-architecture request', async () => {
|
||||||
|
readdirSyncSpy.mockReturnValue(makeDirents(['8.0.412']));
|
||||||
|
maxSatisfyingSpy.mockImplementation(() => '8.0.412');
|
||||||
|
const archSpy = jest.spyOn(os, 'arch').mockReturnValue('arm64');
|
||||||
|
|
||||||
|
const dotnetInstaller = new installer.DotnetCoreInstaller(
|
||||||
|
'8.0.x',
|
||||||
|
'',
|
||||||
|
'x64',
|
||||||
|
undefined,
|
||||||
|
false
|
||||||
|
);
|
||||||
|
await dotnetInstaller.installDotnet();
|
||||||
|
|
||||||
|
expect(getExecOutputSpy).toHaveBeenCalledTimes(2);
|
||||||
|
archSpy.mockRestore();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('installs online when check-latest defaults to true even if SDK is local', async () => {
|
||||||
|
readdirSyncSpy.mockReturnValue(makeDirents(['8.0.422']));
|
||||||
|
maxSatisfyingSpy.mockImplementation(() => '8.0.422');
|
||||||
|
|
||||||
|
const dotnetInstaller = new installer.DotnetCoreInstaller(
|
||||||
|
'8.0.422',
|
||||||
|
''
|
||||||
|
);
|
||||||
|
await dotnetInstaller.installDotnet();
|
||||||
|
|
||||||
|
expect(getExecOutputSpy).toHaveBeenCalledTimes(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('installs online when the dotnet muxer is missing', async () => {
|
||||||
|
readdirSyncSpy.mockReturnValue(makeDirents(['8.0.412']));
|
||||||
|
statSyncSpy.mockImplementation((target: string) => {
|
||||||
|
if (isMuxer(target)) {
|
||||||
|
throwEnoent();
|
||||||
|
}
|
||||||
|
return asFile;
|
||||||
|
});
|
||||||
|
maxSatisfyingSpy.mockImplementation(() => '8.0.412');
|
||||||
|
|
||||||
|
const dotnetInstaller = new installer.DotnetCoreInstaller(
|
||||||
|
'8.0.x',
|
||||||
|
'',
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
false
|
||||||
|
);
|
||||||
|
await dotnetInstaller.installDotnet();
|
||||||
|
|
||||||
|
expect(getExecOutputSpy).toHaveBeenCalledTimes(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
const itOnPosix = IS_WINDOWS ? it.skip : it;
|
||||||
|
|
||||||
|
itOnPosix(
|
||||||
|
'installs online when the dotnet muxer is not executable',
|
||||||
|
async () => {
|
||||||
|
readdirSyncSpy.mockReturnValue(makeDirents(['8.0.412']));
|
||||||
|
accessSyncSpy.mockImplementation((target: string) => {
|
||||||
|
if (isMuxer(target)) {
|
||||||
|
throw new Error('EACCES');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
maxSatisfyingSpy.mockImplementation(() => '8.0.412');
|
||||||
|
|
||||||
|
const dotnetInstaller = new installer.DotnetCoreInstaller(
|
||||||
|
'8.0.x',
|
||||||
|
'',
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
false
|
||||||
|
);
|
||||||
|
await dotnetInstaller.installDotnet();
|
||||||
|
|
||||||
|
expect(getExecOutputSpy).toHaveBeenCalledTimes(2);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
it('installs online when the dotnet muxer path is a directory', async () => {
|
||||||
|
readdirSyncSpy.mockReturnValue(makeDirents(['8.0.412']));
|
||||||
|
statSyncSpy.mockImplementation((target: string) =>
|
||||||
|
isMuxer(target) ? {isFile: () => false} : asFile
|
||||||
|
);
|
||||||
|
maxSatisfyingSpy.mockImplementation(() => '8.0.412');
|
||||||
|
|
||||||
|
const dotnetInstaller = new installer.DotnetCoreInstaller(
|
||||||
|
'8.0.x',
|
||||||
|
'',
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
false
|
||||||
|
);
|
||||||
|
await dotnetInstaller.installDotnet();
|
||||||
|
|
||||||
|
expect(getExecOutputSpy).toHaveBeenCalledTimes(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('reuses an SDK exposed as a symbolic link', async () => {
|
||||||
|
readdirSyncSpy.mockReturnValue(makeSymlinks(['8.0.412']));
|
||||||
|
|
||||||
|
const dotnetInstaller = new installer.DotnetCoreInstaller(
|
||||||
|
'8.0.x',
|
||||||
|
'',
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
false
|
||||||
|
);
|
||||||
|
const installedVersion = await dotnetInstaller.installDotnet();
|
||||||
|
|
||||||
|
expect(installedVersion).toBe('8.0.412');
|
||||||
|
expect(getExecOutputSpy).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('matches the online channel mapping for legacy major-only requests', async () => {
|
||||||
|
readdirSyncSpy.mockReturnValue(makeDirents(['3.0.103', '3.1.426']));
|
||||||
|
|
||||||
|
const dotnetInstaller = new installer.DotnetCoreInstaller(
|
||||||
|
'3',
|
||||||
|
'',
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
false
|
||||||
|
);
|
||||||
|
const installedVersion = await dotnetInstaller.installDotnet();
|
||||||
|
|
||||||
|
expect(installedVersion).toBe('3.1.426');
|
||||||
|
expect(getExecOutputSpy).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
each(['8.0.X', '8.0.x', '8.0.*', '8.0']).it(
|
||||||
|
'reuses a local SDK for the floating request %s',
|
||||||
|
async (version: string) => {
|
||||||
|
readdirSyncSpy.mockReturnValue(makeDirents(['8.0.100', '8.0.412']));
|
||||||
|
|
||||||
|
const dotnetInstaller = new installer.DotnetCoreInstaller(
|
||||||
|
version,
|
||||||
|
'',
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
false
|
||||||
|
);
|
||||||
|
const installedVersion = await dotnetInstaller.installDotnet();
|
||||||
|
|
||||||
|
expect(installedVersion).toBe('8.0.412');
|
||||||
|
expect(getExecOutputSpy).not.toHaveBeenCalled();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
each(['8.X', '8.x', '8.*', '8']).it(
|
||||||
|
'reuses a local SDK for the major-only request %s',
|
||||||
|
async (version: string) => {
|
||||||
|
readdirSyncSpy.mockReturnValue(makeDirents(['8.0.100', '8.0.412']));
|
||||||
|
|
||||||
|
const dotnetInstaller = new installer.DotnetCoreInstaller(
|
||||||
|
version,
|
||||||
|
'',
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
false
|
||||||
|
);
|
||||||
|
const installedVersion = await dotnetInstaller.installDotnet();
|
||||||
|
|
||||||
|
expect(installedVersion).toBe('8.0.412');
|
||||||
|
expect(getExecOutputSpy).not.toHaveBeenCalled();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
it('does not reuse a local SDK for an uppercase feature band request', async () => {
|
||||||
|
readdirSyncSpy.mockReturnValue(makeDirents(['8.0.105']));
|
||||||
|
|
||||||
|
const dotnetInstaller = new installer.DotnetCoreInstaller(
|
||||||
|
'8.0.1XX',
|
||||||
|
'',
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
false
|
||||||
|
);
|
||||||
|
|
||||||
|
await expect(dotnetInstaller.installDotnet()).rejects.toThrow(
|
||||||
|
`The 'dotnet-version' was supplied in invalid format: 8.0.1XX!`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
each(['08.0.x', '8.00.x', '08']).it(
|
||||||
|
'does not reuse a local SDK for the invalid request %s',
|
||||||
|
async (version: string) => {
|
||||||
|
readdirSyncSpy.mockReturnValue(makeDirents(['8.0.412']));
|
||||||
|
|
||||||
|
const dotnetInstaller = new installer.DotnetCoreInstaller(
|
||||||
|
version,
|
||||||
|
'',
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
false
|
||||||
|
);
|
||||||
|
|
||||||
|
await expect(dotnetInstaller.installDotnet()).rejects.toThrow(
|
||||||
|
`The 'dotnet-version' was supplied in invalid format: ${version}!`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
it('ignores an sdk folder that does not contain an SDK', async () => {
|
||||||
|
readdirSyncSpy.mockReturnValue(makeDirents(['8.0.412']));
|
||||||
|
statSyncSpy.mockImplementation((target: string) => {
|
||||||
|
if (String(target).includes('dotnet.dll')) {
|
||||||
|
throwEnoent();
|
||||||
|
}
|
||||||
|
return asFile;
|
||||||
|
});
|
||||||
|
maxSatisfyingSpy.mockImplementation(() => '8.0.412');
|
||||||
|
|
||||||
|
const dotnetInstaller = new installer.DotnetCoreInstaller(
|
||||||
|
'8.0.x',
|
||||||
|
'',
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
false
|
||||||
|
);
|
||||||
|
await dotnetInstaller.installDotnet();
|
||||||
|
|
||||||
|
expect(getExecOutputSpy).toHaveBeenCalledTimes(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('global.json rollForward tests', () => {
|
||||||
|
interface RollForwardCase {
|
||||||
|
name: string;
|
||||||
|
version?: string;
|
||||||
|
quality?: string;
|
||||||
|
minimumVersion?: string;
|
||||||
|
rollForward?: string;
|
||||||
|
installed: string[];
|
||||||
|
expected: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const rollForwardCases: RollForwardCase[] = [
|
||||||
|
{
|
||||||
|
name: 'latestMajor reuses the highest local SDK across majors',
|
||||||
|
minimumVersion: '8.0.100',
|
||||||
|
rollForward: 'latestMajor',
|
||||||
|
installed: ['8.0.412', '9.0.101'],
|
||||||
|
expected: '9.0.101'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'latestMajor ignores local SDKs below the declared version',
|
||||||
|
minimumVersion: '8.0.412',
|
||||||
|
rollForward: 'latestMajor',
|
||||||
|
installed: ['8.0.100', '7.0.400'],
|
||||||
|
expected: null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'latestMajor keeps requiring GA builds when quality is preview',
|
||||||
|
quality: 'preview',
|
||||||
|
minimumVersion: '8.0.100',
|
||||||
|
rollForward: 'latestMajor',
|
||||||
|
installed: ['9.0.200-preview.1'],
|
||||||
|
expected: null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'latestMinor reuses a higher local minor of the same major',
|
||||||
|
version: '8',
|
||||||
|
minimumVersion: '8.1.100',
|
||||||
|
rollForward: 'latestMinor',
|
||||||
|
installed: ['8.1.200', '8.2.300'],
|
||||||
|
expected: '8.2.300'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'latestMinor does not cross into another major',
|
||||||
|
version: '8',
|
||||||
|
minimumVersion: '8.1.100',
|
||||||
|
rollForward: 'latestMinor',
|
||||||
|
installed: ['9.0.101'],
|
||||||
|
expected: null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'latestMinor ignores local SDKs below the declared version',
|
||||||
|
version: '8',
|
||||||
|
minimumVersion: '8.1.100',
|
||||||
|
rollForward: 'latestMinor',
|
||||||
|
installed: ['8.0.412'],
|
||||||
|
expected: null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'latestFeature takes the highest band of the declared major.minor',
|
||||||
|
version: '8.0',
|
||||||
|
minimumVersion: '8.0.100',
|
||||||
|
rollForward: 'latestFeature',
|
||||||
|
installed: ['8.0.105', '8.0.400'],
|
||||||
|
expected: '8.0.400'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'latestPatch stays inside the declared feature band',
|
||||||
|
version: '8.0.2xx',
|
||||||
|
minimumVersion: '8.0.200',
|
||||||
|
rollForward: 'latestPatch',
|
||||||
|
installed: ['8.0.205', '8.0.412'],
|
||||||
|
expected: '8.0.205'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'latestPatch takes the highest patch even when the declared version is installed',
|
||||||
|
version: '8.0.1xx',
|
||||||
|
minimumVersion: '8.0.100',
|
||||||
|
rollForward: 'latestPatch',
|
||||||
|
installed: ['8.0.100', '8.0.105'],
|
||||||
|
expected: '8.0.105'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'patch prefers the exact declared version',
|
||||||
|
version: '8.0.100',
|
||||||
|
minimumVersion: '8.0.100',
|
||||||
|
rollForward: 'patch',
|
||||||
|
installed: ['8.0.100', '8.0.105'],
|
||||||
|
expected: '8.0.100'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'patch stays inside the declared feature band',
|
||||||
|
version: '8.0.100',
|
||||||
|
minimumVersion: '8.0.100',
|
||||||
|
rollForward: 'patch',
|
||||||
|
installed: ['8.0.105', '8.0.200'],
|
||||||
|
expected: '8.0.105'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'feature prefers the declared band over a higher band',
|
||||||
|
version: '8.0.100',
|
||||||
|
minimumVersion: '8.0.100',
|
||||||
|
rollForward: 'feature',
|
||||||
|
installed: ['8.0.105', '8.0.400'],
|
||||||
|
expected: '8.0.105'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'feature rolls to the next higher band, not the highest',
|
||||||
|
version: '8.0.100',
|
||||||
|
minimumVersion: '8.0.100',
|
||||||
|
rollForward: 'feature',
|
||||||
|
installed: ['8.0.200', '8.0.300', '8.0.400'],
|
||||||
|
expected: '8.0.200'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'feature takes the latest patch of the selected band',
|
||||||
|
version: '8.0.100',
|
||||||
|
minimumVersion: '8.0.100',
|
||||||
|
rollForward: 'feature',
|
||||||
|
installed: ['8.0.201', '8.0.205', '8.0.400'],
|
||||||
|
expected: '8.0.205'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'feature does not cross into another minor',
|
||||||
|
version: '8.0.100',
|
||||||
|
minimumVersion: '8.0.100',
|
||||||
|
rollForward: 'feature',
|
||||||
|
installed: ['8.1.100'],
|
||||||
|
expected: null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'minor prefers a higher band of the declared minor over a higher minor',
|
||||||
|
version: '8.0.100',
|
||||||
|
minimumVersion: '8.0.100',
|
||||||
|
rollForward: 'minor',
|
||||||
|
installed: ['8.0.400', '8.1.100'],
|
||||||
|
expected: '8.0.400'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'minor rolls to the next higher minor, not the highest',
|
||||||
|
version: '8.0.100',
|
||||||
|
minimumVersion: '8.0.100',
|
||||||
|
rollForward: 'minor',
|
||||||
|
installed: ['8.1.100', '8.2.100'],
|
||||||
|
expected: '8.1.100'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'major prefers the declared major over a higher major',
|
||||||
|
version: '8.0.100',
|
||||||
|
minimumVersion: '8.0.100',
|
||||||
|
rollForward: 'major',
|
||||||
|
installed: ['8.0.400', '9.0.100'],
|
||||||
|
expected: '8.0.400'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'major rolls to the next higher major, not the highest',
|
||||||
|
version: '8.0.100',
|
||||||
|
minimumVersion: '8.0.100',
|
||||||
|
rollForward: 'major',
|
||||||
|
installed: ['9.0.100', '10.0.100'],
|
||||||
|
expected: '9.0.100'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'non-latest policies ignore dotnet-quality, as the online path does',
|
||||||
|
version: '8.0.100',
|
||||||
|
quality: 'preview',
|
||||||
|
minimumVersion: '8.0.100',
|
||||||
|
rollForward: 'patch',
|
||||||
|
installed: ['8.0.105'],
|
||||||
|
expected: '8.0.105'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'non-latest policies never prefer a prerelease over a GA build',
|
||||||
|
version: '8.0.100',
|
||||||
|
quality: 'preview',
|
||||||
|
minimumVersion: '8.0.100',
|
||||||
|
rollForward: 'major',
|
||||||
|
installed: ['8.0.100', '9.0.100-rc.2'],
|
||||||
|
expected: '8.0.100'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'roll-forward policies never select below the declared version',
|
||||||
|
version: '8.0.200',
|
||||||
|
minimumVersion: '8.0.200',
|
||||||
|
rollForward: 'major',
|
||||||
|
installed: ['8.0.100'],
|
||||||
|
expected: null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'disable requires the exact declared version',
|
||||||
|
version: '8.0.100',
|
||||||
|
installed: ['8.0.200'],
|
||||||
|
expected: null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'installs online for an empty version without a rollForward floor',
|
||||||
|
installed: ['8.0.412'],
|
||||||
|
expected: null
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
each(rollForwardCases).it(
|
||||||
|
'$name',
|
||||||
|
async ({
|
||||||
|
version = '',
|
||||||
|
quality = '',
|
||||||
|
minimumVersion,
|
||||||
|
rollForward,
|
||||||
|
installed,
|
||||||
|
expected
|
||||||
|
}: RollForwardCase) => {
|
||||||
|
readdirSyncSpy.mockReturnValue(makeDirents(installed));
|
||||||
|
maxSatisfyingSpy.mockImplementation(() => '8.0.412');
|
||||||
|
|
||||||
|
const installedVersion = await new installer.DotnetCoreInstaller(
|
||||||
|
version,
|
||||||
|
quality as any,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
false,
|
||||||
|
minimumVersion,
|
||||||
|
rollForward
|
||||||
|
).installDotnet();
|
||||||
|
|
||||||
|
if (expected === null) {
|
||||||
|
expect(getExecOutputSpy).toHaveBeenCalledTimes(2);
|
||||||
|
} else {
|
||||||
|
expect(installedVersion).toBe(expected);
|
||||||
|
expect(getExecOutputSpy).not.toHaveBeenCalled();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('addToPath() tests', () => {
|
describe('addToPath() tests', () => {
|
||||||
it(`should export DOTNET_ROOT env.var with value from DOTNET_INSTALL_DIR env.var`, async () => {
|
it(`should export DOTNET_ROOT env.var with value from DOTNET_INSTALL_DIR env.var`, async () => {
|
||||||
process.env['DOTNET_INSTALL_DIR'] = 'fictitious/dotnet/install/dir';
|
process.env['DOTNET_INSTALL_DIR'] = 'fictitious/dotnet/install/dir';
|
||||||
|
|||||||
@@ -66,6 +66,7 @@ describe('setup-dotnet tests', () => {
|
|||||||
const setOutputSpy = core.setOutput as jest.Mock;
|
const setOutputSpy = core.setOutput as jest.Mock;
|
||||||
|
|
||||||
const existsSyncSpy = fs.existsSync as jest.Mock;
|
const existsSyncSpy = fs.existsSync as jest.Mock;
|
||||||
|
const readFileSyncSpy = fs.readFileSync as jest.Mock;
|
||||||
|
|
||||||
const maxSatisfyingSpy = jest.spyOn(semver, 'maxSatisfying');
|
const maxSatisfyingSpy = jest.spyOn(semver, 'maxSatisfying');
|
||||||
|
|
||||||
@@ -85,11 +86,17 @@ describe('setup-dotnet tests', () => {
|
|||||||
DotnetInstallDir.addToPath = jest.fn();
|
DotnetInstallDir.addToPath = jest.fn();
|
||||||
getMultilineInputSpy.mockImplementation(input => inputs[input as string]);
|
getMultilineInputSpy.mockImplementation(input => inputs[input as string]);
|
||||||
getInputSpy.mockImplementation(input => inputs[input as string]);
|
getInputSpy.mockImplementation(input => inputs[input as string]);
|
||||||
getBooleanInputSpy.mockImplementation(input => inputs[input as string]);
|
getBooleanInputSpy.mockImplementation(input => {
|
||||||
|
const value = inputs[input as string];
|
||||||
|
return typeof value === 'string'
|
||||||
|
? value.toLowerCase() === 'true'
|
||||||
|
: !!value;
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
DotnetInstallDir.addToPath = addToPathOriginal;
|
DotnetInstallDir.addToPath = addToPathOriginal;
|
||||||
|
delete process.env['DOTNET_CHECK_LATEST'];
|
||||||
jest.clearAllMocks();
|
jest.clearAllMocks();
|
||||||
jest.resetAllMocks();
|
jest.resetAllMocks();
|
||||||
});
|
});
|
||||||
@@ -289,6 +296,77 @@ describe('setup-dotnet tests', () => {
|
|||||||
expect(DotnetInstallDir.addToPath).toHaveBeenCalledTimes(1);
|
expect(DotnetInstallDir.addToPath).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should read the 'check-latest' input and pass it to DotnetCoreInstaller", async () => {
|
||||||
|
inputs['dotnet-version'] = ['10.0.101'];
|
||||||
|
inputs['dotnet-quality'] = '';
|
||||||
|
inputs['architecture'] = '';
|
||||||
|
inputs['check-latest'] = 'false';
|
||||||
|
|
||||||
|
let capturedCheckLatest: boolean | undefined;
|
||||||
|
installDotnetSpy.mockImplementation(function (this: any) {
|
||||||
|
capturedCheckLatest = this.checkLatest;
|
||||||
|
return Promise.resolve('');
|
||||||
|
});
|
||||||
|
|
||||||
|
await setup.run();
|
||||||
|
|
||||||
|
expect(getBooleanInputSpy).toHaveBeenCalledWith('check-latest');
|
||||||
|
expect(installDotnetSpy).toHaveBeenCalledTimes(1);
|
||||||
|
expect(capturedCheckLatest).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("'check-latest' resolution", () => {
|
||||||
|
const captureCheckLatest = async () => {
|
||||||
|
inputs['dotnet-version'] = ['10.0.101'];
|
||||||
|
inputs['dotnet-quality'] = '';
|
||||||
|
inputs['architecture'] = '';
|
||||||
|
|
||||||
|
let capturedCheckLatest: boolean | undefined;
|
||||||
|
installDotnetSpy.mockImplementation(function (this: any) {
|
||||||
|
capturedCheckLatest = this.checkLatest;
|
||||||
|
return Promise.resolve('');
|
||||||
|
});
|
||||||
|
|
||||||
|
await setup.run();
|
||||||
|
return capturedCheckLatest;
|
||||||
|
};
|
||||||
|
|
||||||
|
it('defaults to true when neither the input nor the environment variable is set', async () => {
|
||||||
|
inputs['check-latest'] = '';
|
||||||
|
|
||||||
|
expect(await captureCheckLatest()).toBe(true);
|
||||||
|
expect(getBooleanInputSpy).not.toHaveBeenCalledWith('check-latest');
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each(['false', 'FALSE', 'False'])(
|
||||||
|
"reads 'check-latest' from the DOTNET_CHECK_LATEST environment variable ('%s')",
|
||||||
|
async envValue => {
|
||||||
|
inputs['check-latest'] = '';
|
||||||
|
process.env['DOTNET_CHECK_LATEST'] = envValue;
|
||||||
|
|
||||||
|
expect(await captureCheckLatest()).toBe(false);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
it('lets an explicit input win over the environment variable', async () => {
|
||||||
|
inputs['check-latest'] = 'true';
|
||||||
|
process.env['DOTNET_CHECK_LATEST'] = 'false';
|
||||||
|
|
||||||
|
expect(await captureCheckLatest()).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('warns and falls back to true when the environment variable is not a boolean', async () => {
|
||||||
|
inputs['check-latest'] = '';
|
||||||
|
process.env['DOTNET_CHECK_LATEST'] = 'yes';
|
||||||
|
|
||||||
|
expect(await captureCheckLatest()).toBe(true);
|
||||||
|
expect(warningSpy).toHaveBeenCalledWith(
|
||||||
|
`Value 'yes' is not supported for the DOTNET_CHECK_LATEST environment variable. Supported values are: true, false. The 'check-latest' option falls back to 'true'.`
|
||||||
|
);
|
||||||
|
expect(setFailedSpy).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it('should fail the action if unsupported architecture is provided', async () => {
|
it('should fail the action if unsupported architecture is provided', async () => {
|
||||||
inputs['dotnet-version'] = ['10.0.101'];
|
inputs['dotnet-version'] = ['10.0.101'];
|
||||||
inputs['dotnet-quality'] = '';
|
inputs['dotnet-quality'] = '';
|
||||||
@@ -389,5 +467,241 @@ describe('setup-dotnet tests', () => {
|
|||||||
`The 'dotnet-channel' input is only supported when 'dotnet-version' is set to 'latest'.`
|
`The 'dotnet-channel' input is only supported when 'dotnet-version' is set to 'latest'.`
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should pass the global.json rollForward minimum version to DotnetCoreInstaller', async () => {
|
||||||
|
inputs['dotnet-version'] = [];
|
||||||
|
inputs['dotnet-quality'] = '';
|
||||||
|
inputs['dotnet-channel'] = '';
|
||||||
|
inputs['architecture'] = '';
|
||||||
|
inputs['check-latest'] = 'false';
|
||||||
|
inputs['global-json-file'] = 'global.json';
|
||||||
|
|
||||||
|
existsSyncSpy.mockReturnValue(true);
|
||||||
|
readFileSyncSpy.mockReturnValue(
|
||||||
|
JSON.stringify({
|
||||||
|
sdk: {version: '8.0.400', rollForward: 'latestFeature'}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
let capturedVersion: string | undefined;
|
||||||
|
let capturedMinimumVersion: string | undefined;
|
||||||
|
installDotnetSpy.mockImplementation(function (this: any) {
|
||||||
|
capturedVersion = this.version;
|
||||||
|
capturedMinimumVersion = this.minimumVersion;
|
||||||
|
return Promise.resolve('8.0.412');
|
||||||
|
});
|
||||||
|
|
||||||
|
await setup.run();
|
||||||
|
inputs['global-json-file'] = '';
|
||||||
|
|
||||||
|
expect(capturedVersion).toBe('8.0');
|
||||||
|
expect(capturedMinimumVersion).toBe('8.0.400');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should pass non-latest rollForward policies to DotnetCoreInstaller', async () => {
|
||||||
|
inputs['dotnet-version'] = [];
|
||||||
|
inputs['dotnet-quality'] = '';
|
||||||
|
inputs['dotnet-channel'] = '';
|
||||||
|
inputs['architecture'] = '';
|
||||||
|
inputs['check-latest'] = 'false';
|
||||||
|
inputs['global-json-file'] = 'global.json';
|
||||||
|
|
||||||
|
existsSyncSpy.mockReturnValue(true);
|
||||||
|
readFileSyncSpy.mockReturnValue(
|
||||||
|
JSON.stringify({sdk: {version: '8.0.100', rollForward: 'feature'}})
|
||||||
|
);
|
||||||
|
|
||||||
|
let capturedVersion: string | undefined;
|
||||||
|
let capturedMinimumVersion: string | undefined;
|
||||||
|
let capturedRollForward: string | undefined;
|
||||||
|
installDotnetSpy.mockImplementation(function (this: any) {
|
||||||
|
capturedVersion = this.version;
|
||||||
|
capturedMinimumVersion = this.minimumVersion;
|
||||||
|
capturedRollForward = this.rollForward;
|
||||||
|
return Promise.resolve('8.0.200');
|
||||||
|
});
|
||||||
|
|
||||||
|
await setup.run();
|
||||||
|
inputs['global-json-file'] = '';
|
||||||
|
|
||||||
|
expect(capturedVersion).toBe('8.0.100');
|
||||||
|
expect(capturedMinimumVersion).toBe('8.0.100');
|
||||||
|
expect(capturedRollForward).toBe('feature');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should default an omitted rollForward to patch', async () => {
|
||||||
|
inputs['dotnet-version'] = [];
|
||||||
|
inputs['dotnet-quality'] = '';
|
||||||
|
inputs['dotnet-channel'] = '';
|
||||||
|
inputs['architecture'] = '';
|
||||||
|
inputs['check-latest'] = 'false';
|
||||||
|
inputs['global-json-file'] = 'global.json';
|
||||||
|
|
||||||
|
existsSyncSpy.mockReturnValue(true);
|
||||||
|
readFileSyncSpy.mockReturnValue(
|
||||||
|
JSON.stringify({sdk: {version: '8.0.100'}})
|
||||||
|
);
|
||||||
|
|
||||||
|
let capturedVersion: string | undefined;
|
||||||
|
let capturedMinimumVersion: string | undefined;
|
||||||
|
let capturedRollForward: string | undefined;
|
||||||
|
installDotnetSpy.mockImplementation(function (this: any) {
|
||||||
|
capturedVersion = this.version;
|
||||||
|
capturedMinimumVersion = this.minimumVersion;
|
||||||
|
capturedRollForward = this.rollForward;
|
||||||
|
return Promise.resolve('8.0.100');
|
||||||
|
});
|
||||||
|
|
||||||
|
await setup.run();
|
||||||
|
inputs['global-json-file'] = '';
|
||||||
|
|
||||||
|
expect(capturedVersion).toBe('8.0.100');
|
||||||
|
expect(capturedMinimumVersion).toBe('8.0.100');
|
||||||
|
expect(capturedRollForward).toBe('patch');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not default to patch for a prerelease sdk version', async () => {
|
||||||
|
inputs['dotnet-version'] = [];
|
||||||
|
inputs['dotnet-quality'] = '';
|
||||||
|
inputs['dotnet-channel'] = '';
|
||||||
|
inputs['architecture'] = '';
|
||||||
|
inputs['check-latest'] = 'false';
|
||||||
|
inputs['global-json-file'] = 'global.json';
|
||||||
|
|
||||||
|
existsSyncSpy.mockReturnValue(true);
|
||||||
|
readFileSyncSpy.mockReturnValue(
|
||||||
|
JSON.stringify({sdk: {version: '8.0.100-preview.1'}})
|
||||||
|
);
|
||||||
|
|
||||||
|
let capturedMinimumVersion: string | undefined;
|
||||||
|
let capturedRollForward: string | undefined;
|
||||||
|
installDotnetSpy.mockImplementation(function (this: any) {
|
||||||
|
capturedMinimumVersion = this.minimumVersion;
|
||||||
|
capturedRollForward = this.rollForward;
|
||||||
|
return Promise.resolve('8.0.100-preview.1');
|
||||||
|
});
|
||||||
|
|
||||||
|
await setup.run();
|
||||||
|
inputs['global-json-file'] = '';
|
||||||
|
|
||||||
|
expect(capturedMinimumVersion).toBeUndefined();
|
||||||
|
expect(capturedRollForward).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not default to patch for a version that is not a full SDK version', async () => {
|
||||||
|
inputs['dotnet-version'] = [];
|
||||||
|
inputs['dotnet-quality'] = '';
|
||||||
|
inputs['dotnet-channel'] = '';
|
||||||
|
inputs['architecture'] = '';
|
||||||
|
inputs['check-latest'] = 'false';
|
||||||
|
inputs['global-json-file'] = 'global.json';
|
||||||
|
|
||||||
|
existsSyncSpy.mockReturnValue(true);
|
||||||
|
readFileSyncSpy.mockReturnValue(
|
||||||
|
JSON.stringify({sdk: {version: '8.0.0'}})
|
||||||
|
);
|
||||||
|
|
||||||
|
let capturedMinimumVersion: string | undefined;
|
||||||
|
let capturedRollForward: string | undefined;
|
||||||
|
installDotnetSpy.mockImplementation(function (this: any) {
|
||||||
|
capturedMinimumVersion = this.minimumVersion;
|
||||||
|
capturedRollForward = this.rollForward;
|
||||||
|
return Promise.resolve('8.0.0');
|
||||||
|
});
|
||||||
|
|
||||||
|
await setup.run();
|
||||||
|
inputs['global-json-file'] = '';
|
||||||
|
|
||||||
|
expect(capturedMinimumVersion).toBeUndefined();
|
||||||
|
expect(capturedRollForward).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not roll forward a version pinned by the dotnet-version input', async () => {
|
||||||
|
inputs['dotnet-version'] = ['8.0.100'];
|
||||||
|
inputs['dotnet-quality'] = '';
|
||||||
|
inputs['dotnet-channel'] = '';
|
||||||
|
inputs['architecture'] = '';
|
||||||
|
inputs['check-latest'] = 'false';
|
||||||
|
inputs['global-json-file'] = 'global.json';
|
||||||
|
|
||||||
|
existsSyncSpy.mockReturnValue(true);
|
||||||
|
readFileSyncSpy.mockReturnValue(
|
||||||
|
JSON.stringify({sdk: {version: '8.0.100'}})
|
||||||
|
);
|
||||||
|
|
||||||
|
let capturedMinimumVersion: string | undefined;
|
||||||
|
let capturedRollForward: string | undefined;
|
||||||
|
installDotnetSpy.mockImplementation(function (this: any) {
|
||||||
|
capturedMinimumVersion = this.minimumVersion;
|
||||||
|
capturedRollForward = this.rollForward;
|
||||||
|
return Promise.resolve('8.0.100');
|
||||||
|
});
|
||||||
|
|
||||||
|
await setup.run();
|
||||||
|
inputs['global-json-file'] = '';
|
||||||
|
|
||||||
|
expect(installDotnetSpy).toHaveBeenCalledTimes(1);
|
||||||
|
expect(capturedMinimumVersion).toBeUndefined();
|
||||||
|
expect(capturedRollForward).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should keep the global.json floor when a floating dotnet-version matches', async () => {
|
||||||
|
inputs['dotnet-version'] = ['8.0'];
|
||||||
|
inputs['dotnet-quality'] = '';
|
||||||
|
inputs['dotnet-channel'] = '';
|
||||||
|
inputs['architecture'] = '';
|
||||||
|
inputs['check-latest'] = 'false';
|
||||||
|
inputs['global-json-file'] = 'global.json';
|
||||||
|
|
||||||
|
existsSyncSpy.mockReturnValue(true);
|
||||||
|
readFileSyncSpy.mockReturnValue(
|
||||||
|
JSON.stringify({
|
||||||
|
sdk: {version: '8.0.400', rollForward: 'latestFeature'}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
let capturedMinimumVersion: string | undefined;
|
||||||
|
let capturedRollForward: string | undefined;
|
||||||
|
installDotnetSpy.mockImplementation(function (this: any) {
|
||||||
|
capturedMinimumVersion = this.minimumVersion;
|
||||||
|
capturedRollForward = this.rollForward;
|
||||||
|
return Promise.resolve('8.0.400');
|
||||||
|
});
|
||||||
|
|
||||||
|
await setup.run();
|
||||||
|
inputs['global-json-file'] = '';
|
||||||
|
|
||||||
|
expect(installDotnetSpy).toHaveBeenCalledTimes(1);
|
||||||
|
expect(capturedMinimumVersion).toBe('8.0.400');
|
||||||
|
expect(capturedRollForward).toBe('latestFeature');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not set a rollForward policy for disable', async () => {
|
||||||
|
inputs['dotnet-version'] = [];
|
||||||
|
inputs['dotnet-quality'] = '';
|
||||||
|
inputs['dotnet-channel'] = '';
|
||||||
|
inputs['architecture'] = '';
|
||||||
|
inputs['check-latest'] = 'false';
|
||||||
|
inputs['global-json-file'] = 'global.json';
|
||||||
|
|
||||||
|
existsSyncSpy.mockReturnValue(true);
|
||||||
|
readFileSyncSpy.mockReturnValue(
|
||||||
|
JSON.stringify({sdk: {version: '8.0.100', rollForward: 'disable'}})
|
||||||
|
);
|
||||||
|
|
||||||
|
let capturedMinimumVersion: string | undefined;
|
||||||
|
let capturedRollForward: string | undefined;
|
||||||
|
installDotnetSpy.mockImplementation(function (this: any) {
|
||||||
|
capturedMinimumVersion = this.minimumVersion;
|
||||||
|
capturedRollForward = this.rollForward;
|
||||||
|
return Promise.resolve('8.0.100');
|
||||||
|
});
|
||||||
|
|
||||||
|
await setup.run();
|
||||||
|
inputs['global-json-file'] = '';
|
||||||
|
|
||||||
|
expect(capturedMinimumVersion).toBeUndefined();
|
||||||
|
expect(capturedRollForward).toBeUndefined();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -39,6 +39,9 @@ inputs:
|
|||||||
architecture:
|
architecture:
|
||||||
description: 'Optional architecture for the .NET install. Supported values: x64, x86, arm64, amd64, arm, s390x, ppc64le, riscv64. If not set, the installer auto-detects the current system architecture.'
|
description: 'Optional architecture for the .NET install. Supported values: x64, x86, arm64, amd64, arm, s390x, ppc64le, riscv64. If not set, the installer auto-detects the current system architecture.'
|
||||||
required: false
|
required: false
|
||||||
|
check-latest:
|
||||||
|
description: 'Optional, defaults to true. When false, a locally installed SDK that satisfies the requested version is reused without any network calls, otherwise the latest matching version is installed online. Cross-architecture requests always install online. Falls back to the DOTNET_CHECK_LATEST environment variable when this input is not set.'
|
||||||
|
required: false
|
||||||
outputs:
|
outputs:
|
||||||
cache-hit:
|
cache-hit:
|
||||||
description: 'A boolean value to indicate if a cache was hit.'
|
description: 'A boolean value to indicate if a cache was hit.'
|
||||||
|
|||||||
Vendored
+288
-24
@@ -45084,6 +45084,20 @@ const PLATFORM = (() => {
|
|||||||
|
|
||||||
const QUALITY_INPUT_MINIMAL_MAJOR_TAG = 6;
|
const QUALITY_INPUT_MINIMAL_MAJOR_TAG = 6;
|
||||||
const LATEST_PATCH_SYNTAX_MINIMAL_MAJOR_TAG = 5;
|
const LATEST_PATCH_SYNTAX_MINIMAL_MAJOR_TAG = 5;
|
||||||
|
function channelForMajor(major) {
|
||||||
|
// Starting with .NET 5, the minor version is always zero.
|
||||||
|
// Hardcode the earlier versions because they will not get new releases.
|
||||||
|
switch (major) {
|
||||||
|
case '1':
|
||||||
|
return '1.1';
|
||||||
|
case '2':
|
||||||
|
return '2.2';
|
||||||
|
case '3':
|
||||||
|
return '3.1';
|
||||||
|
default:
|
||||||
|
return `${major}.0`;
|
||||||
|
}
|
||||||
|
}
|
||||||
class DotnetVersionResolver {
|
class DotnetVersionResolver {
|
||||||
quality;
|
quality;
|
||||||
dotnetChannel;
|
dotnetChannel;
|
||||||
@@ -45163,22 +45177,7 @@ class DotnetVersionResolver {
|
|||||||
this.resolvedArgument.value = `${major}.${minor}`;
|
this.resolvedArgument.value = `${major}.${minor}`;
|
||||||
}
|
}
|
||||||
else if (this.isNumericTag(major)) {
|
else if (this.isNumericTag(major)) {
|
||||||
// Starting with .NET 5, the minor version is always zero.
|
this.resolvedArgument.value = channelForMajor(major);
|
||||||
// Hardcode the earlier versions because they will not get new releases.
|
|
||||||
switch (major) {
|
|
||||||
case '1':
|
|
||||||
this.resolvedArgument.value = '1.1';
|
|
||||||
break;
|
|
||||||
case '2':
|
|
||||||
this.resolvedArgument.value = '2.2';
|
|
||||||
break;
|
|
||||||
case '3':
|
|
||||||
this.resolvedArgument.value = '3.1';
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
this.resolvedArgument.value = `${major}.0`;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
// If "dotnet-version" is specified as *, x or X resolve latest version of .NET explicitly from LTS channel. The version argument will default to "latest" by install-dotnet script.
|
// If "dotnet-version" is specified as *, x or X resolve latest version of .NET explicitly from LTS channel. The version argument will default to "latest" by install-dotnet script.
|
||||||
@@ -45352,25 +45351,240 @@ function normalizeArch(arch) {
|
|||||||
return arch.toLowerCase();
|
return arch.toLowerCase();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
function isFile(filePath) {
|
||||||
|
try {
|
||||||
|
return (0,external_fs_namespaceObject.statSync)(filePath).isFile();
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function isExecutableFile(filePath) {
|
||||||
|
if (!isFile(filePath)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (utils_IS_WINDOWS) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
(0,external_fs_namespaceObject.accessSync)(filePath, external_fs_namespaceObject.constants.X_OK);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
class DotnetCoreInstaller {
|
class DotnetCoreInstaller {
|
||||||
version;
|
version;
|
||||||
quality;
|
quality;
|
||||||
architecture;
|
architecture;
|
||||||
dotnetChannel;
|
dotnetChannel;
|
||||||
|
checkLatest;
|
||||||
|
minimumVersion;
|
||||||
|
rollForward;
|
||||||
|
static FeatureBandSyntax = /^(\d+)\.(\d+)\.(\d)xx$/;
|
||||||
static {
|
static {
|
||||||
DotnetInstallDir.setEnvironmentVariable();
|
DotnetInstallDir.setEnvironmentVariable();
|
||||||
}
|
}
|
||||||
constructor(version, quality, architecture, dotnetChannel) {
|
constructor(version, quality, architecture, dotnetChannel, checkLatest = true, minimumVersion, rollForward) {
|
||||||
this.version = version;
|
this.version = version;
|
||||||
this.quality = quality;
|
this.quality = quality;
|
||||||
this.architecture = architecture;
|
this.architecture = architecture;
|
||||||
this.dotnetChannel = dotnetChannel;
|
this.dotnetChannel = dotnetChannel;
|
||||||
|
this.checkLatest = checkLatest;
|
||||||
|
this.minimumVersion = minimumVersion;
|
||||||
|
this.rollForward = rollForward;
|
||||||
|
this.version = version.trim();
|
||||||
|
}
|
||||||
|
getInstalledSdkVersions() {
|
||||||
|
const sdkDir = external_path_default().join(DotnetInstallDir.dirPath, 'sdk');
|
||||||
|
try {
|
||||||
|
const versions = (0,external_fs_namespaceObject.readdirSync)(sdkDir, { withFileTypes: true })
|
||||||
|
.filter(entry => entry.isDirectory() || entry.isSymbolicLink())
|
||||||
|
.map(entry => entry.name)
|
||||||
|
.filter(name => semver_default().valid(name) !== null)
|
||||||
|
.filter(name => isFile(external_path_default().join(sdkDir, name, 'dotnet.dll')));
|
||||||
|
core_debug(`Locally installed .NET SDKs in '${sdkDir}': ${versions.join(', ') || '<none>'}`);
|
||||||
|
return versions;
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
core_debug(`Unable to read the SDK directory '${sdkDir}'.`);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
hasDotnetMuxer() {
|
||||||
|
return isExecutableFile(external_path_default().join(DotnetInstallDir.dirPath, utils_IS_WINDOWS ? 'dotnet.exe' : 'dotnet'));
|
||||||
|
}
|
||||||
|
qualityApplies() {
|
||||||
|
if (this.version.toLowerCase() === 'latest') {
|
||||||
|
const major = (this.dotnetChannel || '').trim().match(/^(\d+)/)?.[1];
|
||||||
|
return major ? Number(major) >= QUALITY_INPUT_MINIMAL_MAJOR_TAG : true;
|
||||||
|
}
|
||||||
|
if (semver_default().valid(this.version)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const major = this.version.match(/^(\d+)/)?.[1];
|
||||||
|
return major ? Number(major) >= QUALITY_INPUT_MINIMAL_MAJOR_TAG : false;
|
||||||
|
}
|
||||||
|
findByScope(candidates, major, minor, band) {
|
||||||
|
return (candidates.find(version => {
|
||||||
|
const parsed = semver_default().parse(version);
|
||||||
|
return (parsed &&
|
||||||
|
parsed.major === Number(major) &&
|
||||||
|
parsed.minor === Number(minor) &&
|
||||||
|
(band === undefined ||
|
||||||
|
Math.floor(parsed.patch / 100) === Number(band)));
|
||||||
|
}) ?? null);
|
||||||
|
}
|
||||||
|
static isInRollForwardScope(version, policy, declared) {
|
||||||
|
const parsed = semver_default().parse(version);
|
||||||
|
if (!parsed) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
switch (policy) {
|
||||||
|
case 'patch':
|
||||||
|
case 'latestPatch':
|
||||||
|
return (parsed.major === declared.major &&
|
||||||
|
parsed.minor === declared.minor &&
|
||||||
|
Math.floor(parsed.patch / 100) === Math.floor(declared.patch / 100));
|
||||||
|
case 'feature':
|
||||||
|
case 'latestFeature':
|
||||||
|
return (parsed.major === declared.major && parsed.minor === declared.minor);
|
||||||
|
case 'minor':
|
||||||
|
case 'latestMinor':
|
||||||
|
return parsed.major === declared.major;
|
||||||
|
case 'major':
|
||||||
|
case 'latestMajor':
|
||||||
|
return true;
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
static highestVersion(versions) {
|
||||||
|
return versions.reduce((best, version) => semver_default().gt(version, best) ? version : best);
|
||||||
|
}
|
||||||
|
static nearestBandVersion(versions) {
|
||||||
|
return versions.reduce((best, version) => {
|
||||||
|
const delta = semver_default().major(version) - semver_default().major(best) ||
|
||||||
|
semver_default().minor(version) - semver_default().minor(best) ||
|
||||||
|
Math.floor(semver_default().patch(version) / 100) -
|
||||||
|
Math.floor(semver_default().patch(best) / 100);
|
||||||
|
return delta < 0 || (delta === 0 && semver_default().gt(version, best))
|
||||||
|
? version
|
||||||
|
: best;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
findByRollForward(candidates, policy, declaredVersion) {
|
||||||
|
const declared = semver_default().parse(declaredVersion);
|
||||||
|
if (!declared) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const scoped = candidates.filter(version => DotnetCoreInstaller.isInRollForwardScope(version, policy, declared));
|
||||||
|
if (!scoped.length) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
switch (policy) {
|
||||||
|
case 'patch':
|
||||||
|
return (scoped.find(version => semver_default().eq(version, declared)) ??
|
||||||
|
DotnetCoreInstaller.highestVersion(scoped));
|
||||||
|
case 'feature':
|
||||||
|
case 'minor':
|
||||||
|
case 'major':
|
||||||
|
return DotnetCoreInstaller.nearestBandVersion(scoped);
|
||||||
|
default:
|
||||||
|
return DotnetCoreInstaller.highestVersion(scoped);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
filterByQuality(allowed) {
|
||||||
|
const wantsPrerelease = ['preview', 'daily'].includes((this.quality || '').toLowerCase()) &&
|
||||||
|
this.qualityApplies();
|
||||||
|
return allowed
|
||||||
|
.filter(version => wantsPrerelease
|
||||||
|
? semver_default().prerelease(version) !== null
|
||||||
|
: semver_default().prerelease(version) === null)
|
||||||
|
.sort((semver_default()).rcompare);
|
||||||
|
}
|
||||||
|
findLocalSdkVersion() {
|
||||||
|
const installed = this.getInstalledSdkVersions();
|
||||||
|
if (!installed.length) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (!this.hasDotnetMuxer()) {
|
||||||
|
core_debug(`The 'dotnet' executable was not found in '${DotnetInstallDir.dirPath}'. Locally installed SDKs are ignored.`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const minimumVersion = this.minimumVersion;
|
||||||
|
const allowed = minimumVersion
|
||||||
|
? installed.filter(version => semver_default().gte(version, minimumVersion))
|
||||||
|
: installed;
|
||||||
|
if (!allowed.length) {
|
||||||
|
core_debug(`No locally installed .NET SDK satisfies the global.json minimum version '${minimumVersion}'.`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (this.rollForward && minimumVersion) {
|
||||||
|
return this.findByRollForward(this.filterByQuality(allowed), this.rollForward, minimumVersion);
|
||||||
|
}
|
||||||
|
if (semver_default().valid(this.version)) {
|
||||||
|
return allowed.find(version => version === this.version) ?? null;
|
||||||
|
}
|
||||||
|
if (this.version.toLowerCase() !== 'latest' &&
|
||||||
|
!DotnetCoreInstaller.FeatureBandSyntax.test(this.version) &&
|
||||||
|
!semver_default().validRange(this.version)) {
|
||||||
|
core_debug(`The requested version '${this.version}' is not a valid version spec. Locally installed SDKs are ignored.`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const candidates = this.filterByQuality(allowed);
|
||||||
|
if (!candidates.length) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const input = this.version.toLowerCase();
|
||||||
|
if (input === 'latest') {
|
||||||
|
const channel = (this.dotnetChannel || '').trim();
|
||||||
|
if (!channel) {
|
||||||
|
return candidates[0];
|
||||||
|
}
|
||||||
|
const channelMinor = channel.match(/^(\d+)\.(\d+)$/);
|
||||||
|
if (channelMinor) {
|
||||||
|
return this.findByScope(candidates, channelMinor[1], channelMinor[2]);
|
||||||
|
}
|
||||||
|
const channelBand = channel.match(/^(\d+)\.(\d+)\.(\d)xx$/);
|
||||||
|
if (channelBand) {
|
||||||
|
return this.findByScope(candidates, channelBand[1], channelBand[2], channelBand[3]);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const bandMatch = this.version.match(DotnetCoreInstaller.FeatureBandSyntax);
|
||||||
|
if (bandMatch) {
|
||||||
|
if (Number(bandMatch[1]) < LATEST_PATCH_SYNTAX_MINIMAL_MAJOR_TAG) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return this.findByScope(candidates, bandMatch[1], bandMatch[2], bandMatch[3]);
|
||||||
|
}
|
||||||
|
const minorMatch = this.version.match(/^(\d+)\.(\d+)(?:\.[xX*])?$/);
|
||||||
|
if (minorMatch) {
|
||||||
|
return this.findByScope(candidates, minorMatch[1], minorMatch[2]);
|
||||||
|
}
|
||||||
|
const majorMatch = this.version.match(/^(\d+)(?:\.[xX*])?$/);
|
||||||
|
if (majorMatch) {
|
||||||
|
const [major, minor] = channelForMajor(majorMatch[1]).split('.');
|
||||||
|
return this.findByScope(candidates, major, minor);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
async installDotnet() {
|
async installDotnet() {
|
||||||
|
const isCrossArch = !!this.architecture &&
|
||||||
|
normalizeArch(this.architecture) !== normalizeArch(external_os_default().arch());
|
||||||
|
if (!this.checkLatest && !isCrossArch) {
|
||||||
|
const localVersion = this.findLocalSdkVersion();
|
||||||
|
if (localVersion) {
|
||||||
|
info(`'check-latest' is false and a locally installed .NET SDK (${localVersion}) satisfies the '${this.version}' request. Skipping download.`);
|
||||||
|
return localVersion;
|
||||||
|
}
|
||||||
|
info(`'check-latest' is false but no locally installed .NET SDK satisfies the '${this.version}' request. Falling back to online installation.`);
|
||||||
|
}
|
||||||
const versionResolver = new DotnetVersionResolver(this.version, this.quality, this.dotnetChannel);
|
const versionResolver = new DotnetVersionResolver(this.version, this.quality, this.dotnetChannel);
|
||||||
const dotnetVersion = await versionResolver.createDotnetVersion();
|
const dotnetVersion = await versionResolver.createDotnetVersion();
|
||||||
const architectureArguments = this.architecture &&
|
const architectureArguments = isCrossArch
|
||||||
normalizeArch(this.architecture) !== normalizeArch(external_os_default().arch())
|
|
||||||
? [
|
? [
|
||||||
utils_IS_WINDOWS ? '-InstallDir' : '--install-dir',
|
utils_IS_WINDOWS ? '-InstallDir' : '--install-dir',
|
||||||
utils_IS_WINDOWS
|
utils_IS_WINDOWS
|
||||||
@@ -106580,6 +106794,7 @@ const supportedArchitectures = [
|
|||||||
'ppc64le',
|
'ppc64le',
|
||||||
'riscv64'
|
'riscv64'
|
||||||
];
|
];
|
||||||
|
const CHECK_LATEST_ENV_VAR = 'DOTNET_CHECK_LATEST';
|
||||||
function isValidChannel(channel) {
|
function isValidChannel(channel) {
|
||||||
const upper = channel.toUpperCase();
|
const upper = channel.toUpperCase();
|
||||||
if (upper === 'LTS' || upper === 'STS')
|
if (upper === 'LTS' || upper === 'STS')
|
||||||
@@ -106605,8 +106820,19 @@ async function run() {
|
|||||||
// Proxy, auth, (etc) are still set up, even if no version is identified
|
// Proxy, auth, (etc) are still set up, even if no version is identified
|
||||||
//
|
//
|
||||||
const versions = getMultilineInput('dotnet-version');
|
const versions = getMultilineInput('dotnet-version');
|
||||||
|
const explicitVersions = new Set(versions);
|
||||||
|
const globalJsonConstraints = new Map();
|
||||||
|
const addVersionFromGlobalJson = (globalJsonPath) => {
|
||||||
|
const { version, minimumVersion, rollForward } = getVersionFromGlobalJson(globalJsonPath);
|
||||||
|
versions.push(version);
|
||||||
|
const isRedundantExplicitPin = explicitVersions.has(version) && version === minimumVersion;
|
||||||
|
if (minimumVersion && !isRedundantExplicitPin) {
|
||||||
|
globalJsonConstraints.set(version, { minimumVersion, rollForward });
|
||||||
|
}
|
||||||
|
};
|
||||||
const installedDotnetVersions = [];
|
const installedDotnetVersions = [];
|
||||||
const architecture = getArchitectureInput();
|
const architecture = getArchitectureInput();
|
||||||
|
const checkLatest = getCheckLatestInput();
|
||||||
let dotnetChannel = getInput('dotnet-channel');
|
let dotnetChannel = getInput('dotnet-channel');
|
||||||
const isLatestRequested = versions.some(version => version && version.toLowerCase() === 'latest');
|
const isLatestRequested = versions.some(version => version && version.toLowerCase() === 'latest');
|
||||||
if (dotnetChannel && !isValidChannel(dotnetChannel)) {
|
if (dotnetChannel && !isValidChannel(dotnetChannel)) {
|
||||||
@@ -106628,14 +106854,14 @@ async function run() {
|
|||||||
if (!external_fs_namespaceObject.existsSync(globalJsonPath)) {
|
if (!external_fs_namespaceObject.existsSync(globalJsonPath)) {
|
||||||
throw new Error(`The specified global.json file '${globalJsonFileInput}' does not exist`);
|
throw new Error(`The specified global.json file '${globalJsonFileInput}' does not exist`);
|
||||||
}
|
}
|
||||||
versions.push(getVersionFromGlobalJson(globalJsonPath));
|
addVersionFromGlobalJson(globalJsonPath);
|
||||||
}
|
}
|
||||||
if (!versions.length) {
|
if (!versions.length) {
|
||||||
// Try to fall back to global.json
|
// Try to fall back to global.json
|
||||||
core_debug('No version found, trying to find version from global.json');
|
core_debug('No version found, trying to find version from global.json');
|
||||||
const globalJsonPath = external_path_default().join(process.cwd(), 'global.json');
|
const globalJsonPath = external_path_default().join(process.cwd(), 'global.json');
|
||||||
if (external_fs_namespaceObject.existsSync(globalJsonPath)) {
|
if (external_fs_namespaceObject.existsSync(globalJsonPath)) {
|
||||||
versions.push(getVersionFromGlobalJson(globalJsonPath));
|
addVersionFromGlobalJson(globalJsonPath);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
info(`The global.json wasn't found in the root directory. No .NET version will be installed.`);
|
info(`The global.json wasn't found in the root directory. No .NET version will be installed.`);
|
||||||
@@ -106649,7 +106875,8 @@ async function run() {
|
|||||||
let dotnetInstaller;
|
let dotnetInstaller;
|
||||||
const uniqueVersions = new Set(versions.map(v => (v.toLowerCase() === 'latest' ? 'latest' : v)));
|
const uniqueVersions = new Set(versions.map(v => (v.toLowerCase() === 'latest' ? 'latest' : v)));
|
||||||
for (const version of uniqueVersions) {
|
for (const version of uniqueVersions) {
|
||||||
dotnetInstaller = new DotnetCoreInstaller(version, quality, architecture, version.toLowerCase() === 'latest' ? dotnetChannel : undefined);
|
const constraint = globalJsonConstraints.get(version);
|
||||||
|
dotnetInstaller = new DotnetCoreInstaller(version, quality, architecture, version.toLowerCase() === 'latest' ? dotnetChannel : undefined, checkLatest, constraint?.minimumVersion, constraint?.rollForward);
|
||||||
const installedVersion = await dotnetInstaller.installDotnet();
|
const installedVersion = await dotnetInstaller.installDotnet();
|
||||||
installedDotnetVersions.push(installedVersion);
|
installedDotnetVersions.push(installedVersion);
|
||||||
}
|
}
|
||||||
@@ -106704,8 +106931,36 @@ function getArchitectureInput() {
|
|||||||
}
|
}
|
||||||
throw new Error(`Value '${raw}' is not supported for the 'architecture' option. Supported values are: ${supportedArchitectures.join(', ')}.`);
|
throw new Error(`Value '${raw}' is not supported for the 'architecture' option. Supported values are: ${supportedArchitectures.join(', ')}.`);
|
||||||
}
|
}
|
||||||
|
function getCheckLatestInput() {
|
||||||
|
if ((getInput('check-latest') || '').trim()) {
|
||||||
|
return getBooleanInput('check-latest');
|
||||||
|
}
|
||||||
|
const rawEnvValue = (process.env[CHECK_LATEST_ENV_VAR] || '').trim();
|
||||||
|
if (rawEnvValue) {
|
||||||
|
const envValue = rawEnvValue.toLowerCase();
|
||||||
|
if (envValue === 'true' || envValue === 'false') {
|
||||||
|
core_debug(`The 'check-latest' option is set to '${envValue}' by the ${CHECK_LATEST_ENV_VAR} environment variable.`);
|
||||||
|
return envValue === 'true';
|
||||||
|
}
|
||||||
|
warning(`Value '${rawEnvValue}' is not supported for the ${CHECK_LATEST_ENV_VAR} environment variable. Supported values are: true, false. The 'check-latest' option falls back to 'true'.`);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const ROLL_FORWARD_POLICIES = [
|
||||||
|
'patch',
|
||||||
|
'feature',
|
||||||
|
'minor',
|
||||||
|
'major',
|
||||||
|
'latestPatch',
|
||||||
|
'latestFeature',
|
||||||
|
'latestMinor',
|
||||||
|
'latestMajor'
|
||||||
|
];
|
||||||
|
const versionPattern = /^\d+\.\d+\.[1-9]\d{2,}$/;
|
||||||
function getVersionFromGlobalJson(globalJsonPath) {
|
function getVersionFromGlobalJson(globalJsonPath) {
|
||||||
let version = '';
|
let version = '';
|
||||||
|
let minimumVersion;
|
||||||
|
let rollForwardPolicy;
|
||||||
const globalJson = json5_lib_default().parse(
|
const globalJson = json5_lib_default().parse(
|
||||||
// .trim() is necessary to strip BOM https://github.com/nodejs/node/issues/20649
|
// .trim() is necessary to strip BOM https://github.com/nodejs/node/issues/20649
|
||||||
external_fs_namespaceObject.readFileSync(globalJsonPath, { encoding: 'utf8' }).trim(),
|
external_fs_namespaceObject.readFileSync(globalJsonPath, { encoding: 'utf8' }).trim(),
|
||||||
@@ -106719,7 +106974,6 @@ function getVersionFromGlobalJson(globalJsonPath) {
|
|||||||
version = globalJson.sdk.version;
|
version = globalJson.sdk.version;
|
||||||
const rollForward = globalJson.sdk.rollForward;
|
const rollForward = globalJson.sdk.rollForward;
|
||||||
if (rollForward && !semver_default().prerelease(version)) {
|
if (rollForward && !semver_default().prerelease(version)) {
|
||||||
const versionPattern = /^\d+\.\d+\.[1-9]\d{2,}$/;
|
|
||||||
if (!versionPattern.test(version)) {
|
if (!versionPattern.test(version)) {
|
||||||
throw new Error(`Version '${version}' is not valid for the 'sdk.version' value in global.json. ` +
|
throw new Error(`Version '${version}' is not valid for the 'sdk.version' value in global.json. ` +
|
||||||
`When 'rollForward' is specified, a full SDK version is required. ` +
|
`When 'rollForward' is specified, a full SDK version is required. ` +
|
||||||
@@ -106741,9 +106995,19 @@ function getVersionFromGlobalJson(globalJsonPath) {
|
|||||||
version = `${major}.${minor}.${feature}xx`;
|
version = `${major}.${minor}.${feature}xx`;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
if (ROLL_FORWARD_POLICIES.includes(rollForward)) {
|
||||||
|
minimumVersion = globalJson.sdk.version;
|
||||||
|
rollForwardPolicy = rollForward;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (!rollForward &&
|
||||||
|
!semver_default().prerelease(version) &&
|
||||||
|
versionPattern.test(version)) {
|
||||||
|
minimumVersion = version;
|
||||||
|
rollForwardPolicy = 'patch';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return version;
|
return { version, minimumVersion, rollForward: rollForwardPolicy };
|
||||||
}
|
}
|
||||||
function outputInstalledVersion(installedVersions, globalJsonFileInput) {
|
function outputInstalledVersion(installedVersions, globalJsonFileInput) {
|
||||||
if (!installedVersions.length) {
|
if (!installedVersions.length) {
|
||||||
|
|||||||
+340
-29
@@ -3,7 +3,13 @@ import * as core from '@actions/core';
|
|||||||
import * as exec from '@actions/exec';
|
import * as exec from '@actions/exec';
|
||||||
import * as io from '@actions/io';
|
import * as io from '@actions/io';
|
||||||
import * as hc from '@actions/http-client';
|
import * as hc from '@actions/http-client';
|
||||||
import {chmodSync} from 'fs';
|
import {
|
||||||
|
accessSync,
|
||||||
|
chmodSync,
|
||||||
|
constants as fsConstants,
|
||||||
|
readdirSync,
|
||||||
|
statSync
|
||||||
|
} from 'fs';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import {fileURLToPath} from 'url';
|
import {fileURLToPath} from 'url';
|
||||||
import os from 'os';
|
import os from 'os';
|
||||||
@@ -29,6 +35,22 @@ interface ReleaseIndexResponse {
|
|||||||
|
|
||||||
const QUALITY_INPUT_MINIMAL_MAJOR_TAG = 6;
|
const QUALITY_INPUT_MINIMAL_MAJOR_TAG = 6;
|
||||||
const LATEST_PATCH_SYNTAX_MINIMAL_MAJOR_TAG = 5;
|
const LATEST_PATCH_SYNTAX_MINIMAL_MAJOR_TAG = 5;
|
||||||
|
|
||||||
|
function channelForMajor(major: string): string {
|
||||||
|
// Starting with .NET 5, the minor version is always zero.
|
||||||
|
// Hardcode the earlier versions because they will not get new releases.
|
||||||
|
switch (major) {
|
||||||
|
case '1':
|
||||||
|
return '1.1';
|
||||||
|
case '2':
|
||||||
|
return '2.2';
|
||||||
|
case '3':
|
||||||
|
return '3.1';
|
||||||
|
default:
|
||||||
|
return `${major}.0`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export class DotnetVersionResolver {
|
export class DotnetVersionResolver {
|
||||||
private inputVersion: string;
|
private inputVersion: string;
|
||||||
private resolvedArgument: DotnetVersion;
|
private resolvedArgument: DotnetVersion;
|
||||||
@@ -126,22 +148,7 @@ export class DotnetVersionResolver {
|
|||||||
} else if (this.isNumericTag(major) && this.isNumericTag(minor)) {
|
} else if (this.isNumericTag(major) && this.isNumericTag(minor)) {
|
||||||
this.resolvedArgument.value = `${major}.${minor}`;
|
this.resolvedArgument.value = `${major}.${minor}`;
|
||||||
} else if (this.isNumericTag(major)) {
|
} else if (this.isNumericTag(major)) {
|
||||||
// Starting with .NET 5, the minor version is always zero.
|
this.resolvedArgument.value = channelForMajor(major);
|
||||||
// Hardcode the earlier versions because they will not get new releases.
|
|
||||||
switch (major) {
|
|
||||||
case '1':
|
|
||||||
this.resolvedArgument.value = '1.1';
|
|
||||||
break;
|
|
||||||
case '2':
|
|
||||||
this.resolvedArgument.value = '2.2';
|
|
||||||
break;
|
|
||||||
case '3':
|
|
||||||
this.resolvedArgument.value = '3.1';
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
this.resolvedArgument.value = `${major}.0`;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
// If "dotnet-version" is specified as *, x or X resolve latest version of .NET explicitly from LTS channel. The version argument will default to "latest" by install-dotnet script.
|
// If "dotnet-version" is specified as *, x or X resolve latest version of .NET explicitly from LTS channel. The version argument will default to "latest" by install-dotnet script.
|
||||||
this.resolvedArgument.value = 'LTS';
|
this.resolvedArgument.value = 'LTS';
|
||||||
@@ -377,7 +384,32 @@ export function normalizeArch(arch: string): string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isFile(filePath: string): boolean {
|
||||||
|
try {
|
||||||
|
return statSync(filePath).isFile();
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function isExecutableFile(filePath: string): boolean {
|
||||||
|
if (!isFile(filePath)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (IS_WINDOWS) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
accessSync(filePath, fsConstants.X_OK);
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export class DotnetCoreInstaller {
|
export class DotnetCoreInstaller {
|
||||||
|
private static readonly FeatureBandSyntax = /^(\d+)\.(\d+)\.(\d)xx$/;
|
||||||
|
|
||||||
static {
|
static {
|
||||||
DotnetInstallDir.setEnvironmentVariable();
|
DotnetInstallDir.setEnvironmentVariable();
|
||||||
}
|
}
|
||||||
@@ -386,10 +418,291 @@ export class DotnetCoreInstaller {
|
|||||||
private version: string,
|
private version: string,
|
||||||
private quality: QualityOptions,
|
private quality: QualityOptions,
|
||||||
private architecture?: string,
|
private architecture?: string,
|
||||||
private dotnetChannel?: string
|
private dotnetChannel?: string,
|
||||||
) {}
|
private checkLatest: boolean = true,
|
||||||
|
private minimumVersion?: string,
|
||||||
|
private rollForward?: string
|
||||||
|
) {
|
||||||
|
this.version = version.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
private getInstalledSdkVersions(): string[] {
|
||||||
|
const sdkDir = path.join(DotnetInstallDir.dirPath, 'sdk');
|
||||||
|
try {
|
||||||
|
const versions = readdirSync(sdkDir, {withFileTypes: true})
|
||||||
|
.filter(entry => entry.isDirectory() || entry.isSymbolicLink())
|
||||||
|
.map(entry => entry.name)
|
||||||
|
.filter(name => semver.valid(name) !== null)
|
||||||
|
.filter(name => isFile(path.join(sdkDir, name, 'dotnet.dll')));
|
||||||
|
core.debug(
|
||||||
|
`Locally installed .NET SDKs in '${sdkDir}': ${
|
||||||
|
versions.join(', ') || '<none>'
|
||||||
|
}`
|
||||||
|
);
|
||||||
|
return versions;
|
||||||
|
} catch {
|
||||||
|
core.debug(`Unable to read the SDK directory '${sdkDir}'.`);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private hasDotnetMuxer(): boolean {
|
||||||
|
return isExecutableFile(
|
||||||
|
path.join(DotnetInstallDir.dirPath, IS_WINDOWS ? 'dotnet.exe' : 'dotnet')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private qualityApplies(): boolean {
|
||||||
|
if (this.version.toLowerCase() === 'latest') {
|
||||||
|
const major = (this.dotnetChannel || '').trim().match(/^(\d+)/)?.[1];
|
||||||
|
return major ? Number(major) >= QUALITY_INPUT_MINIMAL_MAJOR_TAG : true;
|
||||||
|
}
|
||||||
|
if (semver.valid(this.version)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const major = this.version.match(/^(\d+)/)?.[1];
|
||||||
|
return major ? Number(major) >= QUALITY_INPUT_MINIMAL_MAJOR_TAG : false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private findByScope(
|
||||||
|
candidates: string[],
|
||||||
|
major: string,
|
||||||
|
minor: string,
|
||||||
|
band?: string
|
||||||
|
): string | null {
|
||||||
|
return (
|
||||||
|
candidates.find(version => {
|
||||||
|
const parsed = semver.parse(version);
|
||||||
|
return (
|
||||||
|
parsed &&
|
||||||
|
parsed.major === Number(major) &&
|
||||||
|
parsed.minor === Number(minor) &&
|
||||||
|
(band === undefined ||
|
||||||
|
Math.floor(parsed.patch / 100) === Number(band))
|
||||||
|
);
|
||||||
|
}) ?? null
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static isInRollForwardScope(
|
||||||
|
version: string,
|
||||||
|
policy: string,
|
||||||
|
declared: semver.SemVer
|
||||||
|
): boolean {
|
||||||
|
const parsed = semver.parse(version);
|
||||||
|
if (!parsed) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
switch (policy) {
|
||||||
|
case 'patch':
|
||||||
|
case 'latestPatch':
|
||||||
|
return (
|
||||||
|
parsed.major === declared.major &&
|
||||||
|
parsed.minor === declared.minor &&
|
||||||
|
Math.floor(parsed.patch / 100) === Math.floor(declared.patch / 100)
|
||||||
|
);
|
||||||
|
case 'feature':
|
||||||
|
case 'latestFeature':
|
||||||
|
return (
|
||||||
|
parsed.major === declared.major && parsed.minor === declared.minor
|
||||||
|
);
|
||||||
|
case 'minor':
|
||||||
|
case 'latestMinor':
|
||||||
|
return parsed.major === declared.major;
|
||||||
|
case 'major':
|
||||||
|
case 'latestMajor':
|
||||||
|
return true;
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static highestVersion(versions: string[]): string {
|
||||||
|
return versions.reduce((best, version) =>
|
||||||
|
semver.gt(version, best) ? version : best
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static nearestBandVersion(versions: string[]): string {
|
||||||
|
return versions.reduce((best, version) => {
|
||||||
|
const delta =
|
||||||
|
semver.major(version) - semver.major(best) ||
|
||||||
|
semver.minor(version) - semver.minor(best) ||
|
||||||
|
Math.floor(semver.patch(version) / 100) -
|
||||||
|
Math.floor(semver.patch(best) / 100);
|
||||||
|
return delta < 0 || (delta === 0 && semver.gt(version, best))
|
||||||
|
? version
|
||||||
|
: best;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private findByRollForward(
|
||||||
|
candidates: string[],
|
||||||
|
policy: string,
|
||||||
|
declaredVersion: string
|
||||||
|
): string | null {
|
||||||
|
const declared = semver.parse(declaredVersion);
|
||||||
|
if (!declared) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const scoped = candidates.filter(version =>
|
||||||
|
DotnetCoreInstaller.isInRollForwardScope(version, policy, declared)
|
||||||
|
);
|
||||||
|
if (!scoped.length) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (policy) {
|
||||||
|
case 'patch':
|
||||||
|
return (
|
||||||
|
scoped.find(version => semver.eq(version, declared)) ??
|
||||||
|
DotnetCoreInstaller.highestVersion(scoped)
|
||||||
|
);
|
||||||
|
case 'feature':
|
||||||
|
case 'minor':
|
||||||
|
case 'major':
|
||||||
|
return DotnetCoreInstaller.nearestBandVersion(scoped);
|
||||||
|
default:
|
||||||
|
return DotnetCoreInstaller.highestVersion(scoped);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private filterByQuality(allowed: string[]): string[] {
|
||||||
|
const wantsPrerelease =
|
||||||
|
['preview', 'daily'].includes((this.quality || '').toLowerCase()) &&
|
||||||
|
this.qualityApplies();
|
||||||
|
return allowed
|
||||||
|
.filter(version =>
|
||||||
|
wantsPrerelease
|
||||||
|
? semver.prerelease(version) !== null
|
||||||
|
: semver.prerelease(version) === null
|
||||||
|
)
|
||||||
|
.sort(semver.rcompare);
|
||||||
|
}
|
||||||
|
|
||||||
|
private findLocalSdkVersion(): string | null {
|
||||||
|
const installed = this.getInstalledSdkVersions();
|
||||||
|
if (!installed.length) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.hasDotnetMuxer()) {
|
||||||
|
core.debug(
|
||||||
|
`The 'dotnet' executable was not found in '${DotnetInstallDir.dirPath}'. Locally installed SDKs are ignored.`
|
||||||
|
);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const minimumVersion = this.minimumVersion;
|
||||||
|
const allowed = minimumVersion
|
||||||
|
? installed.filter(version => semver.gte(version, minimumVersion))
|
||||||
|
: installed;
|
||||||
|
|
||||||
|
if (!allowed.length) {
|
||||||
|
core.debug(
|
||||||
|
`No locally installed .NET SDK satisfies the global.json minimum version '${minimumVersion}'.`
|
||||||
|
);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.rollForward && minimumVersion) {
|
||||||
|
return this.findByRollForward(
|
||||||
|
this.filterByQuality(allowed),
|
||||||
|
this.rollForward,
|
||||||
|
minimumVersion
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (semver.valid(this.version)) {
|
||||||
|
return allowed.find(version => version === this.version) ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
this.version.toLowerCase() !== 'latest' &&
|
||||||
|
!DotnetCoreInstaller.FeatureBandSyntax.test(this.version) &&
|
||||||
|
!semver.validRange(this.version)
|
||||||
|
) {
|
||||||
|
core.debug(
|
||||||
|
`The requested version '${this.version}' is not a valid version spec. Locally installed SDKs are ignored.`
|
||||||
|
);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const candidates = this.filterByQuality(allowed);
|
||||||
|
|
||||||
|
if (!candidates.length) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const input = this.version.toLowerCase();
|
||||||
|
|
||||||
|
if (input === 'latest') {
|
||||||
|
const channel = (this.dotnetChannel || '').trim();
|
||||||
|
if (!channel) {
|
||||||
|
return candidates[0];
|
||||||
|
}
|
||||||
|
const channelMinor = channel.match(/^(\d+)\.(\d+)$/);
|
||||||
|
if (channelMinor) {
|
||||||
|
return this.findByScope(candidates, channelMinor[1], channelMinor[2]);
|
||||||
|
}
|
||||||
|
const channelBand = channel.match(/^(\d+)\.(\d+)\.(\d)xx$/);
|
||||||
|
if (channelBand) {
|
||||||
|
return this.findByScope(
|
||||||
|
candidates,
|
||||||
|
channelBand[1],
|
||||||
|
channelBand[2],
|
||||||
|
channelBand[3]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const bandMatch = this.version.match(DotnetCoreInstaller.FeatureBandSyntax);
|
||||||
|
if (bandMatch) {
|
||||||
|
if (Number(bandMatch[1]) < LATEST_PATCH_SYNTAX_MINIMAL_MAJOR_TAG) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return this.findByScope(
|
||||||
|
candidates,
|
||||||
|
bandMatch[1],
|
||||||
|
bandMatch[2],
|
||||||
|
bandMatch[3]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const minorMatch = this.version.match(/^(\d+)\.(\d+)(?:\.[xX*])?$/);
|
||||||
|
if (minorMatch) {
|
||||||
|
return this.findByScope(candidates, minorMatch[1], minorMatch[2]);
|
||||||
|
}
|
||||||
|
|
||||||
|
const majorMatch = this.version.match(/^(\d+)(?:\.[xX*])?$/);
|
||||||
|
if (majorMatch) {
|
||||||
|
const [major, minor] = channelForMajor(majorMatch[1]).split('.');
|
||||||
|
return this.findByScope(candidates, major, minor);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
public async installDotnet(): Promise<string | null> {
|
public async installDotnet(): Promise<string | null> {
|
||||||
|
const isCrossArch =
|
||||||
|
!!this.architecture &&
|
||||||
|
normalizeArch(this.architecture) !== normalizeArch(os.arch());
|
||||||
|
|
||||||
|
if (!this.checkLatest && !isCrossArch) {
|
||||||
|
const localVersion = this.findLocalSdkVersion();
|
||||||
|
if (localVersion) {
|
||||||
|
core.info(
|
||||||
|
`'check-latest' is false and a locally installed .NET SDK (${localVersion}) satisfies the '${this.version}' request. Skipping download.`
|
||||||
|
);
|
||||||
|
return localVersion;
|
||||||
|
}
|
||||||
|
core.info(
|
||||||
|
`'check-latest' is false but no locally installed .NET SDK satisfies the '${this.version}' request. Falling back to online installation.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const versionResolver = new DotnetVersionResolver(
|
const versionResolver = new DotnetVersionResolver(
|
||||||
this.version,
|
this.version,
|
||||||
this.quality,
|
this.quality,
|
||||||
@@ -397,16 +710,14 @@ export class DotnetCoreInstaller {
|
|||||||
);
|
);
|
||||||
const dotnetVersion = await versionResolver.createDotnetVersion();
|
const dotnetVersion = await versionResolver.createDotnetVersion();
|
||||||
|
|
||||||
const architectureArguments =
|
const architectureArguments = isCrossArch
|
||||||
this.architecture &&
|
? [
|
||||||
normalizeArch(this.architecture) !== normalizeArch(os.arch())
|
IS_WINDOWS ? '-InstallDir' : '--install-dir',
|
||||||
? [
|
IS_WINDOWS
|
||||||
IS_WINDOWS ? '-InstallDir' : '--install-dir',
|
? `"${path.join(DotnetInstallDir.dirPath, this.architecture!)}"`
|
||||||
IS_WINDOWS
|
: path.join(DotnetInstallDir.dirPath, this.architecture!)
|
||||||
? `"${path.join(DotnetInstallDir.dirPath, this.architecture)}"`
|
]
|
||||||
: path.join(DotnetInstallDir.dirPath, this.architecture)
|
: [];
|
||||||
]
|
|
||||||
: [];
|
|
||||||
/**
|
/**
|
||||||
* Install dotnet runtime first in order to get
|
* Install dotnet runtime first in order to get
|
||||||
* the latest stable version of dotnet CLI
|
* the latest stable version of dotnet CLI
|
||||||
|
|||||||
+82
-6
@@ -31,6 +31,8 @@ type SupportedArchitecture = (typeof supportedArchitectures)[number];
|
|||||||
|
|
||||||
export type QualityOptions = (typeof qualityOptions)[number] | '';
|
export type QualityOptions = (typeof qualityOptions)[number] | '';
|
||||||
|
|
||||||
|
const CHECK_LATEST_ENV_VAR = 'DOTNET_CHECK_LATEST';
|
||||||
|
|
||||||
function isValidChannel(channel: string): boolean {
|
function isValidChannel(channel: string): boolean {
|
||||||
const upper = channel.toUpperCase();
|
const upper = channel.toUpperCase();
|
||||||
if (upper === 'LTS' || upper === 'STS') return true;
|
if (upper === 'LTS' || upper === 'STS') return true;
|
||||||
@@ -54,8 +56,24 @@ export async function run() {
|
|||||||
// Proxy, auth, (etc) are still set up, even if no version is identified
|
// Proxy, auth, (etc) are still set up, even if no version is identified
|
||||||
//
|
//
|
||||||
const versions = core.getMultilineInput('dotnet-version');
|
const versions = core.getMultilineInput('dotnet-version');
|
||||||
|
const explicitVersions = new Set(versions);
|
||||||
|
const globalJsonConstraints = new Map<
|
||||||
|
string,
|
||||||
|
{minimumVersion: string; rollForward?: string}
|
||||||
|
>();
|
||||||
|
const addVersionFromGlobalJson = (globalJsonPath: string) => {
|
||||||
|
const {version, minimumVersion, rollForward} =
|
||||||
|
getVersionFromGlobalJson(globalJsonPath);
|
||||||
|
versions.push(version);
|
||||||
|
const isRedundantExplicitPin =
|
||||||
|
explicitVersions.has(version) && version === minimumVersion;
|
||||||
|
if (minimumVersion && !isRedundantExplicitPin) {
|
||||||
|
globalJsonConstraints.set(version, {minimumVersion, rollForward});
|
||||||
|
}
|
||||||
|
};
|
||||||
const installedDotnetVersions: (string | null)[] = [];
|
const installedDotnetVersions: (string | null)[] = [];
|
||||||
const architecture = getArchitectureInput();
|
const architecture = getArchitectureInput();
|
||||||
|
const checkLatest = getCheckLatestInput();
|
||||||
let dotnetChannel = core.getInput('dotnet-channel');
|
let dotnetChannel = core.getInput('dotnet-channel');
|
||||||
|
|
||||||
const isLatestRequested = versions.some(
|
const isLatestRequested = versions.some(
|
||||||
@@ -87,7 +105,7 @@ export async function run() {
|
|||||||
`The specified global.json file '${globalJsonFileInput}' does not exist`
|
`The specified global.json file '${globalJsonFileInput}' does not exist`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
versions.push(getVersionFromGlobalJson(globalJsonPath));
|
addVersionFromGlobalJson(globalJsonPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!versions.length) {
|
if (!versions.length) {
|
||||||
@@ -95,7 +113,7 @@ export async function run() {
|
|||||||
core.debug('No version found, trying to find version from global.json');
|
core.debug('No version found, trying to find version from global.json');
|
||||||
const globalJsonPath = path.join(process.cwd(), 'global.json');
|
const globalJsonPath = path.join(process.cwd(), 'global.json');
|
||||||
if (fs.existsSync(globalJsonPath)) {
|
if (fs.existsSync(globalJsonPath)) {
|
||||||
versions.push(getVersionFromGlobalJson(globalJsonPath));
|
addVersionFromGlobalJson(globalJsonPath);
|
||||||
} else {
|
} else {
|
||||||
core.info(
|
core.info(
|
||||||
`The global.json wasn't found in the root directory. No .NET version will be installed.`
|
`The global.json wasn't found in the root directory. No .NET version will be installed.`
|
||||||
@@ -117,11 +135,15 @@ export async function run() {
|
|||||||
versions.map(v => (v.toLowerCase() === 'latest' ? 'latest' : v))
|
versions.map(v => (v.toLowerCase() === 'latest' ? 'latest' : v))
|
||||||
);
|
);
|
||||||
for (const version of uniqueVersions) {
|
for (const version of uniqueVersions) {
|
||||||
|
const constraint = globalJsonConstraints.get(version);
|
||||||
dotnetInstaller = new DotnetCoreInstaller(
|
dotnetInstaller = new DotnetCoreInstaller(
|
||||||
version,
|
version,
|
||||||
quality,
|
quality,
|
||||||
architecture,
|
architecture,
|
||||||
version.toLowerCase() === 'latest' ? dotnetChannel : undefined
|
version.toLowerCase() === 'latest' ? dotnetChannel : undefined,
|
||||||
|
checkLatest,
|
||||||
|
constraint?.minimumVersion,
|
||||||
|
constraint?.rollForward
|
||||||
);
|
);
|
||||||
const installedVersion = await dotnetInstaller.installDotnet();
|
const installedVersion = await dotnetInstaller.installDotnet();
|
||||||
installedDotnetVersions.push(installedVersion);
|
installedDotnetVersions.push(installedVersion);
|
||||||
@@ -200,8 +222,51 @@ function getArchitectureInput(): SupportedArchitecture | '' {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getVersionFromGlobalJson(globalJsonPath: string): string {
|
function getCheckLatestInput(): boolean {
|
||||||
|
if ((core.getInput('check-latest') || '').trim()) {
|
||||||
|
return core.getBooleanInput('check-latest');
|
||||||
|
}
|
||||||
|
|
||||||
|
const rawEnvValue = (process.env[CHECK_LATEST_ENV_VAR] || '').trim();
|
||||||
|
if (rawEnvValue) {
|
||||||
|
const envValue = rawEnvValue.toLowerCase();
|
||||||
|
if (envValue === 'true' || envValue === 'false') {
|
||||||
|
core.debug(
|
||||||
|
`The 'check-latest' option is set to '${envValue}' by the ${CHECK_LATEST_ENV_VAR} environment variable.`
|
||||||
|
);
|
||||||
|
return envValue === 'true';
|
||||||
|
}
|
||||||
|
core.warning(
|
||||||
|
`Value '${rawEnvValue}' is not supported for the ${CHECK_LATEST_ENV_VAR} environment variable. Supported values are: true, false. The 'check-latest' option falls back to 'true'.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GlobalJsonVersion {
|
||||||
|
version: string;
|
||||||
|
minimumVersion?: string;
|
||||||
|
rollForward?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ROLL_FORWARD_POLICIES = [
|
||||||
|
'patch',
|
||||||
|
'feature',
|
||||||
|
'minor',
|
||||||
|
'major',
|
||||||
|
'latestPatch',
|
||||||
|
'latestFeature',
|
||||||
|
'latestMinor',
|
||||||
|
'latestMajor'
|
||||||
|
];
|
||||||
|
|
||||||
|
const versionPattern = /^\d+\.\d+\.[1-9]\d{2,}$/;
|
||||||
|
|
||||||
|
function getVersionFromGlobalJson(globalJsonPath: string): GlobalJsonVersion {
|
||||||
let version = '';
|
let version = '';
|
||||||
|
let minimumVersion: string | undefined;
|
||||||
|
let rollForwardPolicy: string | undefined;
|
||||||
const globalJson = JSON5.parse(
|
const globalJson = JSON5.parse(
|
||||||
// .trim() is necessary to strip BOM https://github.com/nodejs/node/issues/20649
|
// .trim() is necessary to strip BOM https://github.com/nodejs/node/issues/20649
|
||||||
fs.readFileSync(globalJsonPath, {encoding: 'utf8'}).trim(),
|
fs.readFileSync(globalJsonPath, {encoding: 'utf8'}).trim(),
|
||||||
@@ -215,7 +280,6 @@ function getVersionFromGlobalJson(globalJsonPath: string): string {
|
|||||||
version = globalJson.sdk.version;
|
version = globalJson.sdk.version;
|
||||||
const rollForward = globalJson.sdk.rollForward;
|
const rollForward = globalJson.sdk.rollForward;
|
||||||
if (rollForward && !semver.prerelease(version)) {
|
if (rollForward && !semver.prerelease(version)) {
|
||||||
const versionPattern = /^\d+\.\d+\.[1-9]\d{2,}$/;
|
|
||||||
if (!versionPattern.test(version)) {
|
if (!versionPattern.test(version)) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Version '${version}' is not valid for the 'sdk.version' value in global.json. ` +
|
`Version '${version}' is not valid for the 'sdk.version' value in global.json. ` +
|
||||||
@@ -244,9 +308,21 @@ function getVersionFromGlobalJson(globalJsonPath: string): string {
|
|||||||
version = `${major}.${minor}.${feature}xx`;
|
version = `${major}.${minor}.${feature}xx`;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (ROLL_FORWARD_POLICIES.includes(rollForward)) {
|
||||||
|
minimumVersion = globalJson.sdk.version;
|
||||||
|
rollForwardPolicy = rollForward;
|
||||||
|
}
|
||||||
|
} else if (
|
||||||
|
!rollForward &&
|
||||||
|
!semver.prerelease(version) &&
|
||||||
|
versionPattern.test(version)
|
||||||
|
) {
|
||||||
|
minimumVersion = version;
|
||||||
|
rollForwardPolicy = 'patch';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return version;
|
return {version, minimumVersion, rollForward: rollForwardPolicy};
|
||||||
}
|
}
|
||||||
|
|
||||||
function outputInstalledVersion(
|
function outputInstalledVersion(
|
||||||
|
|||||||
Reference in New Issue
Block a user