Merge branch 'main' into refactor-installer

This commit is contained in:
Nikolai Laevskii 2023-05-24 16:40:29 +02:00
commit cce8e1bad1
12 changed files with 928 additions and 524 deletions

View File

@ -24,7 +24,7 @@ jobs:
- name: Clear toolcache
shell: pwsh
run: __tests__/clear-toolcache.ps1 ${{ runner.os }}
- name: Setup dotnet 2.2.402 and 3.1.404
- name: Setup dotnet 2.2.402, 3.1.404 and 3.0.x
uses: ./
with:
dotnet-version: |
@ -133,6 +133,27 @@ jobs:
shell: pwsh
run: __tests__/verify-dotnet.ps1 -Patterns "^2.2", "^3.1"
test-ABCxx-syntax:
runs-on: ${{ matrix.operating-system }}
strategy:
fail-fast: false
matrix:
operating-system: [ubuntu-latest, windows-latest, macOS-latest]
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Clear toolcache
shell: pwsh
run: __tests__/clear-toolcache.ps1 ${{ runner.os }}
- name: Setup dotnet 6.0.4xx
uses: ./
with:
dotnet-version: '6.0.4xx'
- name: Verify dotnet
shell: pwsh
run: __tests__/verify-dotnet.ps1 -Patterns "^6\.0\.4\d{2}"
test-setup-with-wildcard:
runs-on: ${{ matrix.operating-system }}
strategy:
@ -183,6 +204,31 @@ jobs:
shell: pwsh
run: __tests__/verify-dotnet.ps1 -Patterns "^2.2", "^3.1"
test-setup-global-json-only:
runs-on: ${{ matrix.operating-system }}
strategy:
fail-fast: false
matrix:
operating-system: [ubuntu-latest, windows-latest, macOS-latest]
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Clear toolcache
shell: pwsh
run: __tests__/clear-toolcache.ps1 ${{ runner.os }}
- name: Write global.json
shell: bash
run: |
mkdir subdirectory
echo '{"sdk":{"version": "2.2.207","rollForward": "latestFeature"}}' > ./subdirectory/global.json
- name: Setup dotnet
uses: ./
with:
global-json-file: ./subdirectory/global.json
- name: Verify dotnet
shell: pwsh
run: __tests__/verify-dotnet.ps1 -Patterns "^2.2"
test-setup-with-dotnet-quality:
runs-on: ${{ matrix.operating-system }}
strategy:

View File

@ -49,12 +49,13 @@ The `dotnet-version` input supports following syntax:
- **A.B.C** (e.g 6.0.400, 7.0.100-preview.7.22377.5) - installs exact version of .NET SDK
- **A.B** or **A.B.x** (e.g. 3.1, 3.1.x) - installs the latest patch version of .NET SDK on the channel `3.1`, including prerelease versions (preview, rc)
- **A** or **A.x** (e.g. 3, 3.x) - installs the latest minor version of the specified major tag, including prerelease versions (preview, rc)
- **A.B.Cxx** (e.g. 6.0.4xx) - available since `.NET 5.0` release. Installs the latest version of the specific SDK release, including prerelease versions (preview, rc).
## Using the `dotnet-quality` input
This input sets up the action to install the latest build of the specified quality in the channel. The possible values of `dotnet-quality` are: **daily**, **signed**, **validated**, **preview**, **ga**.
> **Note**: `dotnet-quality` input can be used only with .NET SDK version in 'A.B', 'A.B.x', 'A' and 'A.x' formats where the major version is higher than 5. In other cases, `dotnet-quality` input will be ignored.
> **Note**: `dotnet-quality` input can be used only with .NET SDK version in 'A.B', 'A.B.x', 'A', 'A.x' and 'A.B.Cxx' formats where the major version is higher than 5. In other cases, `dotnet-quality` input will be ignored.
```yml
steps:

View File

@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`authutil tests Existing config not in repo root, sets up a partial NuGet.config user/PAT for GPR 1`] = `
exports[`authutil tests existing config not in repo root, sets up a partial NuGet.config user/PAT for GPR 1`] = `
"<?xml version=\\"1.0\\"?>
<configuration>
<config>
@ -15,7 +15,7 @@ exports[`authutil tests Existing config not in repo root, sets up a partial NuGe
</configuration>"
`;
exports[`authutil tests Existing config w/ Azure Artifacts source and NuGet.org, sets up a partial NuGet.config user/PAT for GPR 1`] = `
exports[`authutil tests existing config w/ Azure Artifacts source and NuGet.org, sets up a partial NuGet.config user/PAT for GPR 1`] = `
"<?xml version=\\"1.0\\"?>
<configuration>
<config>
@ -30,7 +30,7 @@ exports[`authutil tests Existing config w/ Azure Artifacts source and NuGet.org,
</configuration>"
`;
exports[`authutil tests Existing config w/ GPR source and NuGet.org, sets up a partial NuGet.config user/PAT for GPR 1`] = `
exports[`authutil tests existing config w/ GPR source and NuGet.org, sets up a partial NuGet.config user/PAT for GPR 1`] = `
"<?xml version=\\"1.0\\"?>
<configuration>
<config>
@ -45,7 +45,7 @@ exports[`authutil tests Existing config w/ GPR source and NuGet.org, sets up a p
</configuration>"
`;
exports[`authutil tests Existing config w/ no GPR sources, sets up a full NuGet.config with URL and user/PAT for GPR 1`] = `
exports[`authutil tests existing config w/ no GPR sources, sets up a full NuGet.config with URL and user/PAT for GPR 1`] = `
"<?xml version=\\"1.0\\"?>
<configuration>
<config>
@ -63,7 +63,7 @@ exports[`authutil tests Existing config w/ no GPR sources, sets up a full NuGet.
</configuration>"
`;
exports[`authutil tests Existing config w/ no sources, sets up a full NuGet.config with URL and user/PAT for GPR 1`] = `
exports[`authutil tests existing config w/ no sources, sets up a full NuGet.config with URL and user/PAT for GPR 1`] = `
"<?xml version=\\"1.0\\"?>
<configuration>
<config>
@ -81,7 +81,7 @@ exports[`authutil tests Existing config w/ no sources, sets up a full NuGet.conf
</configuration>"
`;
exports[`authutil tests Existing config w/ only Azure Artifacts source, sets up a partial NuGet.config user/PAT for GPR 1`] = `
exports[`authutil tests existing config w/ only Azure Artifacts source, sets up a partial NuGet.config user/PAT for GPR 1`] = `
"<?xml version=\\"1.0\\"?>
<configuration>
<config>
@ -96,7 +96,7 @@ exports[`authutil tests Existing config w/ only Azure Artifacts source, sets up
</configuration>"
`;
exports[`authutil tests Existing config w/ only GPR source, sets up a partial NuGet.config user/PAT for GPR 1`] = `
exports[`authutil tests existing config w/ only GPR source, sets up a partial NuGet.config user/PAT for GPR 1`] = `
"<?xml version=\\"1.0\\"?>
<configuration>
<config>
@ -111,7 +111,7 @@ exports[`authutil tests Existing config w/ only GPR source, sets up a partial Nu
</configuration>"
`;
exports[`authutil tests Existing config w/ two GPR sources, sets up a partial NuGet.config user/PAT for GPR 1`] = `
exports[`authutil tests existing config w/ two GPR sources, sets up a partial NuGet.config user/PAT for GPR 1`] = `
"<?xml version=\\"1.0\\"?>
<configuration>
<config>
@ -130,7 +130,7 @@ exports[`authutil tests Existing config w/ two GPR sources, sets up a partial Nu
</configuration>"
`;
exports[`authutil tests No existing config, sets up a full NuGet.config with URL and other owner/PAT for GPR 1`] = `
exports[`authutil tests no existing config, sets up a full NuGet.config with URL and other owner/PAT for GPR 1`] = `
"<?xml version=\\"1.0\\"?>
<configuration>
<config>
@ -148,7 +148,7 @@ exports[`authutil tests No existing config, sets up a full NuGet.config with URL
</configuration>"
`;
exports[`authutil tests No existing config, sets up a full NuGet.config with URL and token for other source 1`] = `
exports[`authutil tests no existing config, sets up a full NuGet.config with URL and token for other source 1`] = `
"<?xml version=\\"1.0\\"?>
<configuration>
<config>
@ -166,7 +166,7 @@ exports[`authutil tests No existing config, sets up a full NuGet.config with URL
</configuration>"
`;
exports[`authutil tests No existing config, sets up a full NuGet.config with URL and user/PAT for GPR 1`] = `
exports[`authutil tests no existing config, sets up a full NuGet.config with URL and user/PAT for GPR 1`] = `
"<?xml version=\\"1.0\\"?>
<configuration>
<config>

View File

@ -91,9 +91,9 @@ describe('authutil tests', () => {
process.env['NUGET_AUTH_TOKEN'] = '';
});
it('No existing config, sets up a full NuGet.config with URL and user/PAT for GPR', async () => {
it('no existing config, sets up a full NuGet.config with URL and user/PAT for GPR', async () => {
process.env['NUGET_AUTH_TOKEN'] = 'TEST_FAKE_AUTH_TOKEN';
await auth.configAuthentication(
auth.configAuthentication(
'https://nuget.pkg.github.com/OwnerName/index.json',
'',
fakeSourcesDirForTesting
@ -104,10 +104,10 @@ describe('authutil tests', () => {
).toMatchSnapshot();
});
it('No existing config, auth token environment variable not provided, throws', async () => {
it('no existing config, auth token environment variable not provided, throws', async () => {
let thrown = false;
try {
await auth.configAuthentication(
auth.configAuthentication(
'https://nuget.pkg.github.com/OwnerName/index.json',
'',
fakeSourcesDirForTesting
@ -118,10 +118,10 @@ describe('authutil tests', () => {
expect(thrown).toBe(true);
});
it('No existing config, sets up a full NuGet.config with URL and other owner/PAT for GPR', async () => {
it('no existing config, sets up a full NuGet.config with URL and other owner/PAT for GPR', async () => {
process.env['NUGET_AUTH_TOKEN'] = 'TEST_FAKE_AUTH_TOKEN';
process.env['INPUT_OWNER'] = 'otherorg';
await auth.configAuthentication(
auth.configAuthentication(
'https://nuget.pkg.github.com/otherorg/index.json',
'',
fakeSourcesDirForTesting
@ -132,7 +132,7 @@ describe('authutil tests', () => {
).toMatchSnapshot();
});
it('Existing config (invalid), tries to parse an invalid NuGet.config and throws', async () => {
it('existing config (invalid), tries to parse an invalid NuGet.config and throws', async () => {
process.env['NUGET_AUTH_TOKEN'] = 'TEST_FAKE_AUTH_TOKEN';
const inputNuGetConfigPath: string = path.join(
fakeSourcesDirForTesting,
@ -141,7 +141,7 @@ describe('authutil tests', () => {
fs.writeFileSync(inputNuGetConfigPath, invalidNuGetConfig);
let thrown = false;
try {
await auth.configAuthentication(
auth.configAuthentication(
'https://nuget.pkg.github.com/OwnerName/index.json',
'',
fakeSourcesDirForTesting
@ -152,14 +152,14 @@ describe('authutil tests', () => {
expect(thrown).toBe(true);
});
it('Existing config w/ no sources, sets up a full NuGet.config with URL and user/PAT for GPR', async () => {
it('existing config w/ no sources, sets up a full NuGet.config with URL and user/PAT for GPR', async () => {
process.env['NUGET_AUTH_TOKEN'] = 'TEST_FAKE_AUTH_TOKEN';
const inputNuGetConfigPath: string = path.join(
fakeSourcesDirForTesting,
'nuget.config'
);
fs.writeFileSync(inputNuGetConfigPath, emptyNuGetConfig);
await auth.configAuthentication(
auth.configAuthentication(
'https://nuget.pkg.github.com/OwnerName/index.json',
'',
fakeSourcesDirForTesting
@ -170,14 +170,14 @@ describe('authutil tests', () => {
).toMatchSnapshot();
});
it('Existing config w/ no GPR sources, sets up a full NuGet.config with URL and user/PAT for GPR', async () => {
it('existing config w/ no GPR sources, sets up a full NuGet.config with URL and user/PAT for GPR', async () => {
process.env['NUGET_AUTH_TOKEN'] = 'TEST_FAKE_AUTH_TOKEN';
const inputNuGetConfigPath: string = path.join(
fakeSourcesDirForTesting,
'nuget.config'
);
fs.writeFileSync(inputNuGetConfigPath, nugetorgNuGetConfig);
await auth.configAuthentication(
auth.configAuthentication(
'https://nuget.pkg.github.com/OwnerName/index.json',
'',
fakeSourcesDirForTesting
@ -188,14 +188,14 @@ describe('authutil tests', () => {
).toMatchSnapshot();
});
it('Existing config w/ only GPR source, sets up a partial NuGet.config user/PAT for GPR', async () => {
it('existing config w/ only GPR source, sets up a partial NuGet.config user/PAT for GPR', async () => {
process.env['NUGET_AUTH_TOKEN'] = 'TEST_FAKE_AUTH_TOKEN';
const inputNuGetConfigPath: string = path.join(
fakeSourcesDirForTesting,
'nuget.config'
);
fs.writeFileSync(inputNuGetConfigPath, gprNuGetConfig);
await auth.configAuthentication(
auth.configAuthentication(
'https://nuget.pkg.github.com/OwnerName/index.json',
'',
fakeSourcesDirForTesting
@ -206,14 +206,14 @@ describe('authutil tests', () => {
).toMatchSnapshot();
});
it('Existing config w/ GPR source and NuGet.org, sets up a partial NuGet.config user/PAT for GPR', async () => {
it('existing config w/ GPR source and NuGet.org, sets up a partial NuGet.config user/PAT for GPR', async () => {
process.env['NUGET_AUTH_TOKEN'] = 'TEST_FAKE_AUTH_TOKEN';
const inputNuGetConfigPath: string = path.join(
fakeSourcesDirForTesting,
'nuget.config'
);
fs.writeFileSync(inputNuGetConfigPath, gprnugetorgNuGetConfig);
await auth.configAuthentication(
auth.configAuthentication(
'https://nuget.pkg.github.com/OwnerName/index.json',
'',
fakeSourcesDirForTesting
@ -224,14 +224,14 @@ describe('authutil tests', () => {
).toMatchSnapshot();
});
it('Existing config w/ two GPR sources, sets up a partial NuGet.config user/PAT for GPR', async () => {
it('existing config w/ two GPR sources, sets up a partial NuGet.config user/PAT for GPR', async () => {
process.env['NUGET_AUTH_TOKEN'] = 'TEST_FAKE_AUTH_TOKEN';
const inputNuGetConfigPath: string = path.join(
fakeSourcesDirForTesting,
'nuget.config'
);
fs.writeFileSync(inputNuGetConfigPath, twogprNuGetConfig);
await auth.configAuthentication(
auth.configAuthentication(
'https://nuget.pkg.github.com',
'',
fakeSourcesDirForTesting
@ -242,7 +242,7 @@ describe('authutil tests', () => {
).toMatchSnapshot();
});
it('Existing config w/ spaces in key, throws for now', async () => {
it('existing config w/ spaces in key, throws for now', async () => {
process.env['NUGET_AUTH_TOKEN'] = 'TEST_FAKE_AUTH_TOKEN';
const inputNuGetConfigPath: string = path.join(
fakeSourcesDirForTesting,
@ -251,7 +251,7 @@ describe('authutil tests', () => {
fs.writeFileSync(inputNuGetConfigPath, spaceNuGetConfig);
let thrown = false;
try {
await auth.configAuthentication(
auth.configAuthentication(
'https://nuget.pkg.github.com/OwnerName/index.json',
'',
fakeSourcesDirForTesting
@ -262,7 +262,7 @@ describe('authutil tests', () => {
expect(thrown).toBe(true);
});
it('Existing config not in repo root, sets up a partial NuGet.config user/PAT for GPR', async () => {
it('existing config not in repo root, sets up a partial NuGet.config user/PAT for GPR', async () => {
process.env['NUGET_AUTH_TOKEN'] = 'TEST_FAKE_AUTH_TOKEN';
const inputNuGetConfigDirectory: string = path.join(
fakeSourcesDirForTesting,
@ -274,7 +274,7 @@ describe('authutil tests', () => {
);
fs.mkdirSync(inputNuGetConfigDirectory, {recursive: true});
fs.writeFileSync(inputNuGetConfigPath, gprNuGetConfig);
await auth.configAuthentication(
auth.configAuthentication(
'https://nuget.pkg.github.com/OwnerName/index.json',
'subfolder/nuget.config',
fakeSourcesDirForTesting
@ -285,14 +285,14 @@ describe('authutil tests', () => {
).toMatchSnapshot();
});
it('Existing config w/ only Azure Artifacts source, sets up a partial NuGet.config user/PAT for GPR', async () => {
it('existing config w/ only Azure Artifacts source, sets up a partial NuGet.config user/PAT for GPR', async () => {
process.env['NUGET_AUTH_TOKEN'] = 'TEST_FAKE_AUTH_TOKEN';
const inputNuGetConfigPath: string = path.join(
fakeSourcesDirForTesting,
'nuget.config'
);
fs.writeFileSync(inputNuGetConfigPath, azureartifactsNuGetConfig);
await auth.configAuthentication(
auth.configAuthentication(
'https://pkgs.dev.azure.com/amullans/_packaging/GitHubBuilds/nuget/v3/index.json',
'',
fakeSourcesDirForTesting
@ -303,14 +303,14 @@ describe('authutil tests', () => {
).toMatchSnapshot();
});
it('Existing config w/ Azure Artifacts source and NuGet.org, sets up a partial NuGet.config user/PAT for GPR', async () => {
it('existing config w/ Azure Artifacts source and NuGet.org, sets up a partial NuGet.config user/PAT for GPR', async () => {
process.env['NUGET_AUTH_TOKEN'] = 'TEST_FAKE_AUTH_TOKEN';
const inputNuGetConfigPath: string = path.join(
fakeSourcesDirForTesting,
'nuget.config'
);
fs.writeFileSync(inputNuGetConfigPath, azureartifactsnugetorgNuGetConfig);
await auth.configAuthentication(
auth.configAuthentication(
'https://pkgs.dev.azure.com/amullans/_packaging/GitHubBuilds/nuget/v3/index.json',
'',
fakeSourcesDirForTesting
@ -321,9 +321,9 @@ describe('authutil tests', () => {
).toMatchSnapshot();
});
it('No existing config, sets up a full NuGet.config with URL and token for other source', async () => {
it('no existing config, sets up a full NuGet.config with URL and token for other source', async () => {
process.env['NUGET_AUTH_TOKEN'] = 'TEST_FAKE_AUTH_TOKEN';
await auth.configAuthentication(
auth.configAuthentication(
'https://pkgs.dev.azure.com/amullans/_packaging/GitHubBuilds/nuget/v3/index.json',
'',
fakeSourcesDirForTesting

View File

@ -1,21 +1,45 @@
import cscFile from '../.github/csc.json';
describe('csc tests', () => {
it('Valid regular expression', async () => {
const regex = cscFile['problemMatcher'][0]['pattern'][0]['regexp'];
test('regular expression in csc.json is valid', async () => {
const regexPattern = cscFile['problemMatcher'][0]['pattern'][0]['regexp'];
const regexResultsMap = cscFile['problemMatcher'][0]['pattern'][0];
console.log(regex);
const re = new RegExp(regex);
const regex = new RegExp(regexPattern);
// Ideally we would verify that this
const stringsToMatch = [
'Program.cs(10,79): error CS1002: ; expected [/Users/zacharyeisinger/Documents/repo/setup-dotnet/__tests__/sample-broken-csproj/sample.csproj]',
"S:\\Msbuild\\src\\Build\\Evaluation\\ExpressionShredder.cs(33,7): error CS1003: Syntax error, ',' expected [S:\\msbuild\\src\\Build\\Microsoft.Build.csproj > Properties:prop]"
];
// Expected results are calculated according to the csc matcher located in csc.json file
const expectedResults = [
{
file: 'Program.cs',
line: '10',
severity: 'error',
code: 'CS1002',
message: '; expected',
fromPath:
'/Users/zacharyeisinger/Documents/repo/setup-dotnet/__tests__/sample-broken-csproj/sample.csproj'
},
{
file: 'S:\\Msbuild\\src\\Build\\Evaluation\\ExpressionShredder.cs',
line: '33',
severity: 'error',
code: 'CS1003',
message: "Syntax error, ',' expected",
fromPath:
'S:\\msbuild\\src\\Build\\Microsoft.Build.csproj > Properties:prop'
}
];
stringsToMatch.forEach(string => {
const matchStr = string.match(re);
console.log(matchStr);
expect(matchStr).toEqual(expect.anything());
stringsToMatch.map((string, index) => {
const matchedResultsArray = string.match(regex);
for (const propName in expectedResults[index]) {
const propertyIndex = regexResultsMap[propName];
const expectedPropValue = expectedResults[index][propName];
const matchedPropValue = matchedResultsArray![propertyIndex];
expect(matchedPropValue).toEqual(expectedPropValue);
}
});
}, 10000);
});

View File

@ -0,0 +1,65 @@
import path from 'path';
import fs from 'fs';
import * as hc from '@actions/http-client';
const HTTP_CLIENT_OPTIONS = {allowRetries: true, maxRetries: 10} as const;
const TEST_TIMEOUT = 30000;
describe('Dotnet installation scripts tests', () => {
it(
'Uses an up to date bash download script',
async () => {
const httpCallbackClient = new hc.HttpClient(
'setup-dotnet-test',
[],
HTTP_CLIENT_OPTIONS
);
const response: hc.HttpClientResponse = await httpCallbackClient.get(
'https://dot.net/v1/dotnet-install.sh'
);
expect(response.message.statusCode).toBe(200);
const upToDateContents: string = await response.readBody();
const currentContents: string = fs
.readFileSync(
path.join(__dirname, '..', 'externals', 'install-dotnet.sh')
)
.toString();
expect(normalizeFileContents(currentContents)).toBe(
normalizeFileContents(upToDateContents)
);
},
TEST_TIMEOUT
);
it(
'Uses an up to date powershell download script',
async () => {
const httpCallbackClient = new hc.HttpClient(
'setup-dotnet-test',
[],
HTTP_CLIENT_OPTIONS
);
const response: hc.HttpClientResponse = await httpCallbackClient.get(
'https://dot.net/v1/dotnet-install.ps1'
);
expect(response.message.statusCode).toBe(200);
const upToDateContents: string = await response.readBody();
const currentContents: string = fs
.readFileSync(
path.join(__dirname, '..', 'externals', 'install-dotnet.ps1')
)
.toString();
expect(normalizeFileContents(currentContents)).toBe(
normalizeFileContents(upToDateContents)
);
},
TEST_TIMEOUT
);
});
function normalizeFileContents(contents: string): string {
return contents
.trim()
.replace(new RegExp('\r\n', 'g'), '\n')
.replace(new RegExp('\r', 'g'), '\n');
}

View File

@ -1,168 +1,304 @@
import * as io from '@actions/io';
import * as os from 'os';
import fs from 'fs';
import path from 'path';
import each from 'jest-each';
import * as hc from '@actions/http-client';
import semver from 'semver';
import * as exec from '@actions/exec';
import * as core from '@actions/core';
import * as io from '@actions/io';
import * as installer from '../src/installer';
import {QualityOptions} from '../src/setup-dotnet';
import {IS_WINDOWS} from '../src/utils';
import {IS_LINUX} from '../src/utils';
import {QualityOptions} from '../src/setup-dotnet';
let toolDir: string;
describe('installer tests', () => {
const env = process.env;
if (IS_WINDOWS) {
toolDir = path.join(process.env['PROGRAMFILES'] + '', 'dotnet');
} else if (IS_LINUX) {
toolDir = '/usr/share/dotnet';
} else {
toolDir = path.join(process.env['HOME'] + '', '.dotnet');
}
const tempDir = path.join(__dirname, 'runner', 'temp');
process.env['RUNNER_TOOL_CACHE'] = toolDir;
process.env['RUNNER_TEMP'] = tempDir;
beforeEach(() => {
jest.resetModules();
process.env = {...env};
});
describe('DotnetCoreInstaller tests', () => {
beforeAll(async () => {
process.env.RUNNER_TOOL_CACHE = toolDir;
process.env.DOTNET_INSTALL_DIR = toolDir;
process.env.RUNNER_TEMP = tempDir;
process.env.DOTNET_ROOT = '';
try {
await io.rmRF(`${toolDir}/*`);
await io.rmRF(`${tempDir}/*`);
} catch (err) {
console.log(
`Failed to remove test directories, check the error message:${os.EOL}`,
err.message
);
}
}, 30000);
const getExecOutputSpy = jest.spyOn(exec, 'getExecOutput');
const warningSpy = jest.spyOn(core, 'warning');
const whichSpy = jest.spyOn(io, 'which');
const maxSatisfyingSpy = jest.spyOn(semver, 'maxSatisfying');
afterEach(async () => {
try {
await io.rmRF(`${toolDir}/*`);
await io.rmRF(`${tempDir}/*`);
} catch (err) {
console.log(
`Failed to remove test directories, check the error message:${os.EOL}`,
err.message
);
}
}, 30000);
describe('installDotnet() tests', () => {
whichSpy.mockImplementation(() => Promise.resolve('PathToShell'));
it('Aquires multiple versions of dotnet', async () => {
const versions = ['2.2.207', '3.1.120'];
for (const version of versions) {
await getDotnet(version);
}
expect(fs.existsSync(path.join(toolDir, 'sdk', '2.2.207'))).toBe(true);
expect(fs.existsSync(path.join(toolDir, 'sdk', '3.1.120'))).toBe(true);
if (IS_WINDOWS) {
expect(fs.existsSync(path.join(toolDir, 'dotnet.exe'))).toBe(true);
} else {
expect(fs.existsSync(path.join(toolDir, 'dotnet'))).toBe(true);
}
expect(process.env.DOTNET_ROOT).toBeDefined();
expect(process.env.PATH).toBeDefined();
expect(process.env.DOTNET_ROOT).toBe(toolDir);
expect(process.env.PATH?.startsWith(toolDir)).toBe(true);
}, 600000);
it('Acquires version of dotnet if no matching version is installed', async () => {
await getDotnet('3.1.201');
expect(fs.existsSync(path.join(toolDir, 'sdk', '3.1.201'))).toBe(true);
if (IS_WINDOWS) {
expect(fs.existsSync(path.join(toolDir, 'dotnet.exe'))).toBe(true);
} else {
expect(fs.existsSync(path.join(toolDir, 'dotnet'))).toBe(true);
}
expect(process.env.DOTNET_ROOT).toBeDefined();
expect(process.env.PATH).toBeDefined();
expect(process.env.DOTNET_ROOT).toBe(toolDir);
expect(process.env.PATH?.startsWith(toolDir)).toBe(true);
}, 600000); //This needs some time to download on "slower" internet connections
it('Acquires generic version of dotnet if no matching version is installed', async () => {
await getDotnet('3.1');
const directory = fs
.readdirSync(path.join(toolDir, 'sdk'))
.filter(fn => fn.startsWith('3.1.'));
expect(directory.length > 0).toBe(true);
if (IS_WINDOWS) {
expect(fs.existsSync(path.join(toolDir, 'dotnet.exe'))).toBe(true);
} else {
expect(fs.existsSync(path.join(toolDir, 'dotnet'))).toBe(true);
}
expect(process.env.DOTNET_ROOT).toBeDefined();
expect(process.env.PATH).toBeDefined();
expect(process.env.DOTNET_ROOT).toBe(toolDir);
expect(process.env.PATH?.startsWith(toolDir)).toBe(true);
}, 600000); //This needs some time to download on "slower" internet connections
it('Returns string with installed SDK version', async () => {
const version = '3.1.120';
const installedVersion = await getDotnet(version);
expect(installedVersion).toBe('3.1.120');
}, 600000);
it('Throws if no location contains correct dotnet version', async () => {
await expect(async () => {
await getDotnet('1000.0.0');
}).rejects.toThrow();
}, 30000);
it('Uses an up to date bash download script', async () => {
const httpCallbackClient = new hc.HttpClient('setup-dotnet-test', [], {
allowRetries: true,
maxRetries: 3
it('should throw the error in case of non-zero exit code of the installation script. The error message should contain logs.', async () => {
const inputVersion = '3.1.100';
const inputQuality = '' as QualityOptions;
const errorMessage = 'fictitious error message!';
getExecOutputSpy.mockImplementation(() => {
return Promise.resolve({
exitCode: 1,
stdout: '',
stderr: errorMessage
});
const response: hc.HttpClientResponse = await httpCallbackClient.get(
'https://dot.net/v1/dotnet-install.sh'
);
expect(response.message.statusCode).toBe(200);
const upToDateContents: string = await response.readBody();
const currentContents: string = fs
.readFileSync(
path.join(__dirname, '..', 'externals', 'install-dotnet.sh')
)
.toString();
expect(normalizeFileContents(currentContents)).toBe(
normalizeFileContents(upToDateContents)
);
}, 30000);
it('Uses an up to date powershell download script', async () => {
const httpCallbackClient = new hc.HttpClient('setup-dotnet-test', [], {
allowRetries: true,
maxRetries: 3
});
const response: hc.HttpClientResponse = await httpCallbackClient.get(
'https://dot.net/v1/dotnet-install.ps1'
const dotnetInstaller = new installer.DotnetCoreInstaller(
inputVersion,
inputQuality
);
expect(response.message.statusCode).toBe(200);
const upToDateContents: string = await response.readBody();
const currentContents: string = fs
.readFileSync(
path.join(__dirname, '..', 'externals', 'install-dotnet.ps1')
)
.toString();
expect(normalizeFileContents(currentContents)).toBe(
normalizeFileContents(upToDateContents)
await expect(dotnetInstaller.installDotnet()).rejects.toThrow(
`Failed to install dotnet, exit code: 1. ${errorMessage}`
);
}, 30000);
});
it('should return version of .NET SDK after installation complete', async () => {
const inputVersion = '3.1.100';
const inputQuality = '' as QualityOptions;
const stdout = `Fictitious dotnet version ${inputVersion} is installed`;
getExecOutputSpy.mockImplementation(() => {
return Promise.resolve({
exitCode: 0,
stdout: `${stdout}`,
stderr: ''
});
});
maxSatisfyingSpy.mockImplementation(() => inputVersion);
const dotnetInstaller = new installer.DotnetCoreInstaller(
inputVersion,
inputQuality
);
const installedVersion = await dotnetInstaller.installDotnet();
expect(installedVersion).toBe(inputVersion);
});
it(`should supply 'version' argument to the installation script if supplied version is in A.B.C syntax`, async () => {
const inputVersion = '6.0.300';
const inputQuality = '' as QualityOptions;
const stdout = `Fictitious dotnet version ${inputVersion} is installed`;
getExecOutputSpy.mockImplementation(() => {
return Promise.resolve({
exitCode: 0,
stdout: `${stdout}`,
stderr: ''
});
});
maxSatisfyingSpy.mockImplementation(() => inputVersion);
const dotnetInstaller = new installer.DotnetCoreInstaller(
inputVersion,
inputQuality
);
await dotnetInstaller.installDotnet();
const scriptArguments = (
getExecOutputSpy.mock.calls[0][1] as string[]
).join(' ');
const expectedArgument = IS_WINDOWS
? `-Version ${inputVersion}`
: `--version ${inputVersion}`;
expect(scriptArguments).toContain(expectedArgument);
});
it(`should warn if the 'quality' input is set and the supplied version is in A.B.C syntax`, async () => {
const inputVersion = '6.0.300';
const inputQuality = 'ga' as QualityOptions;
const stdout = `Fictitious dotnet version ${inputVersion} is installed`;
getExecOutputSpy.mockImplementation(() => {
return Promise.resolve({
exitCode: 0,
stdout: `${stdout}`,
stderr: ''
});
});
maxSatisfyingSpy.mockImplementation(() => inputVersion);
const dotnetInstaller = new installer.DotnetCoreInstaller(
inputVersion,
inputQuality
);
await dotnetInstaller.installDotnet();
expect(warningSpy).toHaveBeenCalledWith(
`The 'dotnet-quality' input can be used only with .NET SDK version in A.B, A.B.x, A, A.x and A.B.Cxx formats where the major tag is higher than 5. You specified: ${inputVersion}. 'dotnet-quality' input is ignored.`
);
});
it(`should warn if the 'quality' input is set and version isn't in A.B.C syntax but major tag is lower then 6`, async () => {
const inputVersion = '3.1';
const inputQuality = 'ga' as QualityOptions;
const stdout = `Fictitious dotnet version 3.1.100 is installed`;
getExecOutputSpy.mockImplementation(() => {
return Promise.resolve({
exitCode: 0,
stdout: `${stdout}`,
stderr: ''
});
});
maxSatisfyingSpy.mockImplementation(() => inputVersion);
const dotnetInstaller = new installer.DotnetCoreInstaller(
inputVersion,
inputQuality
);
await dotnetInstaller.installDotnet();
expect(warningSpy).toHaveBeenCalledWith(
`The 'dotnet-quality' input can be used only with .NET SDK version in A.B, A.B.x, A, A.x and A.B.Cxx formats where the major tag is higher than 5. You specified: ${inputVersion}. 'dotnet-quality' input is ignored.`
);
});
each(['6', '6.0', '6.0.x', '6.0.*', '6.0.X']).test(
`should supply 'quality' argument to the installation script if quality input is set and version (%s) is not in A.B.C syntax`,
async inputVersion => {
const inputQuality = 'ga' as QualityOptions;
const exitCode = 0;
const stdout = `Fictitious dotnet version 6.0.0 is installed`;
getExecOutputSpy.mockImplementation(() => {
return Promise.resolve({
exitCode: exitCode,
stdout: `${stdout}`,
stderr: ''
});
});
maxSatisfyingSpy.mockImplementation(() => inputVersion);
const dotnetInstaller = new installer.DotnetCoreInstaller(
inputVersion,
inputQuality
);
await dotnetInstaller.installDotnet();
const scriptArguments = (
getExecOutputSpy.mock.calls[0][1] as string[]
).join(' ');
const expectedArgument = IS_WINDOWS
? `-Quality ${inputQuality}`
: `--quality ${inputQuality}`;
expect(scriptArguments).toContain(expectedArgument);
}
);
each(['6', '6.0', '6.0.x', '6.0.*', '6.0.X']).test(
`should supply 'channel' argument to the installation script if version (%s) isn't in A.B.C syntax`,
async inputVersion => {
const inputQuality = '' as QualityOptions;
const exitCode = 0;
const stdout = `Fictitious dotnet version 6.0.0 is installed`;
getExecOutputSpy.mockImplementation(() => {
return Promise.resolve({
exitCode: exitCode,
stdout: `${stdout}`,
stderr: ''
});
});
maxSatisfyingSpy.mockImplementation(() => inputVersion);
const dotnetInstaller = new installer.DotnetCoreInstaller(
inputVersion,
inputQuality
);
await dotnetInstaller.installDotnet();
const scriptArguments = (
getExecOutputSpy.mock.calls[0][1] as string[]
).join(' ');
const expectedArgument = IS_WINDOWS
? `-Channel 6.0`
: `--channel 6.0`;
expect(scriptArguments).toContain(expectedArgument);
}
);
if (IS_WINDOWS) {
it(`should supply '-ProxyAddress' argument to the installation script if env.variable 'https_proxy' is set`, async () => {
process.env['https_proxy'] = 'https://proxy.com';
const inputVersion = '6.0.100';
const inputQuality = '' as QualityOptions;
const stdout = `Fictitious dotnet version ${inputVersion} is installed`;
getExecOutputSpy.mockImplementation(() => {
return Promise.resolve({
exitCode: 0,
stdout: `${stdout}`,
stderr: ''
});
});
maxSatisfyingSpy.mockImplementation(() => inputVersion);
const dotnetInstaller = new installer.DotnetCoreInstaller(
inputVersion,
inputQuality
);
await dotnetInstaller.installDotnet();
const scriptArguments = (
getExecOutputSpy.mock.calls[0][1] as string[]
).join(' ');
expect(scriptArguments).toContain(
`-ProxyAddress ${process.env['https_proxy']}`
);
});
it(`should supply '-ProxyBypassList' argument to the installation script if env.variable 'no_proxy' is set`, async () => {
process.env['no_proxy'] = 'first.url,second.url';
const inputVersion = '6.0.100';
const inputQuality = '' as QualityOptions;
const stdout = `Fictitious dotnet version 6.0.0 is installed`;
getExecOutputSpy.mockImplementation(() => {
return Promise.resolve({
exitCode: 0,
stdout: `${stdout}`,
stderr: ''
});
});
maxSatisfyingSpy.mockImplementation(() => inputVersion);
const dotnetInstaller = new installer.DotnetCoreInstaller(
inputVersion,
inputQuality
);
await dotnetInstaller.installDotnet();
const scriptArguments = (
getExecOutputSpy.mock.calls[0][1] as string[]
).join(' ');
expect(scriptArguments).toContain(
`-ProxyBypassList ${process.env['no_proxy']}`
);
});
}
});
describe('addToPath() tests', () => {
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';
installer.DotnetCoreInstaller.addToPath();
const dotnet_root = process.env['DOTNET_ROOT'];
expect(dotnet_root).toBe(process.env['DOTNET_INSTALL_DIR']);
});
it(`should export value from DOTNET_INSTALL_DIR env.var to the PATH`, async () => {
process.env['DOTNET_INSTALL_DIR'] = 'fictitious/dotnet/install/dir';
installer.DotnetCoreInstaller.addToPath();
const path = process.env['PATH'];
expect(path).toContain(process.env['DOTNET_INSTALL_DIR']);
});
});
});
describe('DotnetVersionResolver tests', () => {
describe('createDotNetVersion() tests', () => {
each([
'3.1',
'3.x',
@ -170,14 +306,16 @@ describe('DotnetVersionResolver tests', () => {
'3.1.*',
'3.1.X',
'3.1.2',
'3.1.0-preview1'
'3.1.0-preview1',
'6.0.2xx'
]).test(
"if valid version: '%s' is supplied, it should return version object with some value",
'if valid version is supplied (%s), it should return version object with some value',
async version => {
const dotnetVersionResolver = new installer.DotnetVersionResolver(
version
);
const versionObject = await dotnetVersionResolver.createDotnetVersion();
const versionObject =
await dotnetVersionResolver.createDotNetVersion();
expect(!!versionObject.value).toBe(true);
}
@ -209,52 +347,61 @@ describe('DotnetVersionResolver tests', () => {
' 0 . 1 . 2 ',
'invalid'
]).test(
"if invalid version: '%s' is supplied, it should throw",
'if invalid version is supplied (%s), it should throw',
async version => {
const dotnetVersionResolver = new installer.DotnetVersionResolver(
version
);
await expect(
async () => await dotnetVersionResolver.createDotnetVersion()
async () => await dotnetVersionResolver.createDotNetVersion()
).rejects.toThrow();
}
);
each(['3.1', '3.1.x', '3.1.*', '3.1.X']).test(
"if version: '%s' that can be resolved to 'channel' option is supplied, it should set type to 'channel' in version object",
each(['3', '3.1', '3.1.x', '3.1.*', '3.1.X', '6.0.2xx']).test(
"if version that can be resolved to 'channel' option is supplied (%s), it should set type to 'channel' in version object",
async version => {
const dotnetVersionResolver = new installer.DotnetVersionResolver(
version
);
const versionObject = await dotnetVersionResolver.createDotnetVersion();
const versionObject =
await dotnetVersionResolver.createDotNetVersion();
expect(versionObject.type.toLowerCase().includes('channel')).toBe(true);
expect(versionObject.type.toLowerCase().includes('channel')).toBe(
true
);
}
);
each(['6.0', '6.0.x', '6.0.*', '6.0.X']).test(
"if version: '%s' that can be resolved to 'channel' option is supplied and its major tag is >= 6, it should set type to 'channel' and qualityFlag to 'true' in version object",
each(['6.0', '6.0.x', '6.0.*', '6.0.X', '6.0.2xx']).test(
"if version that can be resolved to 'channel' option is supplied and its major tag is >= 6 (%s), it should set type to 'channel' and qualityFlag to 'true' in version object",
async version => {
const dotnetVersionResolver = new installer.DotnetVersionResolver(
version
);
const versionObject = await dotnetVersionResolver.createDotnetVersion();
const versionObject =
await dotnetVersionResolver.createDotNetVersion();
expect(versionObject.type.toLowerCase().includes('channel')).toBe(true);
expect(versionObject.type.toLowerCase().includes('channel')).toBe(
true
);
expect(versionObject.qualityFlag).toBe(true);
}
);
each(['3.1.2', '3.1.0-preview1']).test(
"if version: '%s' that can be resolved to 'version' option is supplied, it should set quality flag to 'false' and type to 'version' in version object",
"if version that can be resolved to 'version' option is supplied (%s), it should set quality flag to 'false' and type to 'version' in version object",
async version => {
const dotnetVersionResolver = new installer.DotnetVersionResolver(
version
);
const versionObject = await dotnetVersionResolver.createDotnetVersion();
const versionObject =
await dotnetVersionResolver.createDotNetVersion();
expect(versionObject.type.toLowerCase().includes('version')).toBe(true);
expect(versionObject.type.toLowerCase().includes('version')).toBe(
true
);
expect(versionObject.qualityFlag).toBe(false);
}
);
@ -265,9 +412,10 @@ describe('DotnetVersionResolver tests', () => {
const dotnetVersionResolver = new installer.DotnetVersionResolver(
version
);
const versionObject = await dotnetVersionResolver.createDotnetVersion();
const windowsRegEx = new RegExp(/^-[VC]/);
const nonWindowsRegEx = new RegExp(/^--[vc]/);
const versionObject =
await dotnetVersionResolver.createDotNetVersion();
const windowsRegEx = new RegExp(/^-(Version|Channel)/);
const nonWindowsRegEx = new RegExp(/^--(version|channel)/);
if (IS_WINDOWS) {
expect(windowsRegEx.test(versionObject.type)).toBe(true);
@ -278,21 +426,18 @@ describe('DotnetVersionResolver tests', () => {
}
}
);
});
function normalizeFileContents(contents: string): string {
return contents
.trim()
.replace(new RegExp('\r\n', 'g'), '\n')
.replace(new RegExp('\r', 'g'), '\n');
}
async function getDotnet(version: string, quality = ''): Promise<string> {
const dotnetInstaller = new installer.DotnetCoreInstaller(
version,
quality as QualityOptions
it(`should throw if dotnet-version is supplied in A.B.Cxx syntax with major tag lower that 5`, async () => {
const version = '3.0.1xx';
const dotnetVersionResolver = new installer.DotnetVersionResolver(
version
);
const installedVersion = await dotnetInstaller.installDotnet();
installer.DotnetCoreInstaller.addToPath();
return installedVersion;
}
await expect(
async () => await dotnetVersionResolver.createDotNetVersion()
).rejects.toThrow(
`'dotnet-version' was supplied in invalid format: ${version}! The A.B.Cxx syntax is available since the .NET 5.0 release.`
);
});
});
});
});

View File

@ -1,114 +1,173 @@
import * as io from '@actions/io';
import * as core from '@actions/core';
import fs from 'fs';
import os from 'os';
import path from 'path';
import semver from 'semver';
import * as auth from '../src/authutil';
import * as setup from '../src/setup-dotnet';
import {IS_WINDOWS} from '../src/utils';
import {IS_LINUX} from '../src/utils';
let toolDir: string;
if (IS_WINDOWS) {
toolDir = path.join(process.env['PROGRAMFILES'] + '', 'dotnet');
} else if (IS_LINUX) {
toolDir = '/usr/share/dotnet';
} else {
toolDir = path.join(process.env['HOME'] + '', '.dotnet');
}
function createGlobalJsonPath(dotnetVersion: string) {
const globalJsonPath = path.join(process.cwd(), 'global.json');
const jsonContents = `{${os.EOL}"sdk": {${os.EOL}"version": "${dotnetVersion}"${os.EOL}}${os.EOL}}`;
if (!fs.existsSync(globalJsonPath)) {
fs.writeFileSync(globalJsonPath, jsonContents);
}
return globalJsonPath;
}
const tempDir = path.join(__dirname, 'runner', 'temp2');
import {DotnetCoreInstaller} from '../src/installer';
describe('setup-dotnet tests', () => {
const getInputSpy = jest.spyOn(core, 'getInput');
const getMultilineInputSpy = jest.spyOn(core, 'getMultilineInput');
const setOutputSpy = jest.spyOn(core, 'setOutput');
const inputs = {} as any;
beforeAll(async () => {
process.env.RUNNER_TOOL_CACHE = toolDir;
process.env.DOTNET_INSTALL_DIR = toolDir;
process.env.RUNNER_TEMP = tempDir;
try {
await io.rmRF(`${toolDir}/*`);
await io.rmRF(`${tempDir}/*`);
} catch (err) {
console.log(err.message);
console.log('Failed to remove test directories');
}
}, 30000);
const getInputSpy = jest.spyOn(core, 'getInput');
const getMultilineInputSpy = jest.spyOn(core, 'getMultilineInput');
const setFailedSpy = jest.spyOn(core, 'setFailed');
const warningSpy = jest.spyOn(core, 'warning');
const debugSpy = jest.spyOn(core, 'debug');
const infoSpy = jest.spyOn(core, 'info');
const setOutputSpy = jest.spyOn(core, 'setOutput');
afterEach(async () => {
try {
await io.rmRF(path.join(process.cwd(), 'global.json'));
await io.rmRF(`${toolDir}/*`);
await io.rmRF(`${tempDir}/*`);
} catch (err) {
console.log(err.message);
console.log('Failed to remove test directories');
}
}, 30000);
const existsSyncSpy = jest.spyOn(fs, 'existsSync');
it('Acquires version of dotnet from global.json if no matching version is installed', async () => {
createGlobalJsonPath('3.1.201');
await setup.run();
const maxSatisfyingSpy = jest.spyOn(semver, 'maxSatisfying');
expect(fs.existsSync(path.join(toolDir, 'sdk', '3.1.201'))).toBe(true);
if (IS_WINDOWS) {
expect(fs.existsSync(path.join(toolDir, 'dotnet.exe'))).toBe(true);
} else {
expect(fs.existsSync(path.join(toolDir, 'dotnet'))).toBe(true);
}
}, 400000);
const installDotnetSpy = jest.spyOn(
DotnetCoreInstaller.prototype,
'installDotnet'
);
const addToPathSpy = jest.spyOn(DotnetCoreInstaller, 'addToPath');
it("Sets output with the latest installed by action version if global.json file isn't specified", async () => {
inputs['dotnet-version'] = ['3.1.201', '6.0.401'];
const configAuthenticationSpy = jest.spyOn(auth, 'configAuthentication');
getMultilineInputSpy.mockImplementation(input => inputs[input]);
await setup.run();
expect(setOutputSpy).toHaveBeenCalledWith('dotnet-version', '6.0.401');
}, 400000);
it("Sets output with the version specified in global.json, if it's present", async () => {
createGlobalJsonPath('3.0.103');
inputs['dotnet-version'] = ['3.1.201', '6.0.401'];
inputs['global-json-file'] = './global.json';
getMultilineInputSpy.mockImplementation(input => inputs[input]);
getInputSpy.mockImplementation(input => inputs[input]);
await setup.run();
expect(setOutputSpy).toHaveBeenCalledWith('dotnet-version', '3.0.103');
}, 400000);
it('Sets output with the version specified in global.json with absolute path', async () => {
const globalJsonPath = createGlobalJsonPath('3.0.103');
inputs['dotnet-version'] = ['3.1.201', '6.0.401'];
inputs['global-json-file'] = globalJsonPath;
getMultilineInputSpy.mockImplementation(input => inputs[input]);
getInputSpy.mockImplementation(input => inputs[input]);
await setup.run();
expect(setOutputSpy).toHaveBeenCalledWith('dotnet-version', '3.0.103');
}, 400000);
describe('run() tests', () => {
beforeEach(() => {
getMultilineInputSpy.mockImplementation(input => inputs[input as string]);
getInputSpy.mockImplementation(input => inputs[input as string]);
});
afterEach(() => {
jest.clearAllMocks();
jest.resetAllMocks();
});
it('should fail the action if global-json-file input is present, but the file does not exist in the file system', async () => {
inputs['global-json-file'] = 'fictitious.json';
inputs['dotnet-version'] = [];
const expectedErrorMessage = `The specified global.json file '${inputs['global-json-file']}' does not exist`;
await setup.run();
expect(setFailedSpy).toHaveBeenCalledWith(expectedErrorMessage);
});
test(`if 'dotnet-version' and 'global-json-file' inputs aren't present, should log into debug output, try to find global.json in the repo root, fail and log message into info output`, async () => {
inputs['global-json-file'] = '';
inputs['dotnet-version'] = [];
maxSatisfyingSpy.mockImplementation(() => null);
setOutputSpy.mockImplementation(() => {});
const expectedDebugMessage =
'No version found, trying to find version from global.json';
const expectedInfoMessage = `The global.json wasn't found in the root directory. No .NET version will be installed.`;
await setup.run();
expect(debugSpy).toHaveBeenCalledWith(expectedDebugMessage);
expect(existsSyncSpy).toHaveBeenCalled();
expect(infoSpy).toHaveBeenCalledWith(expectedInfoMessage);
});
it('should fail the action if quality is supplied but its value is not supported', async () => {
inputs['global-json-file'] = '';
inputs['dotnet-version'] = ['6.0'];
inputs['dotnet-quality'] = 'fictitiousQuality';
const expectedErrorMessage = `Value '${inputs['dotnet-quality']}' is not supported for the 'dotnet-quality' option. Supported values are: daily, signed, validated, preview, ga.`;
await setup.run();
expect(setFailedSpy).toHaveBeenCalledWith(expectedErrorMessage);
});
it('should call installDotnet() multiple times if dotnet-version multiline input is provided', async () => {
inputs['global-json-file'] = '';
inputs['dotnet-version'] = ['6.0', '7.0'];
inputs['dotnet-quality'] = '';
installDotnetSpy.mockImplementation(() => Promise.resolve(''));
await setup.run();
expect(installDotnetSpy).toHaveBeenCalledTimes(2);
});
it('should call addToPath() after installation complete', async () => {
inputs['global-json-file'] = '';
inputs['dotnet-version'] = ['6.0', '7.0'];
inputs['dotnet-quality'] = '';
installDotnetSpy.mockImplementation(() => Promise.resolve(''));
addToPathSpy.mockImplementation(() => {});
await setup.run();
expect(addToPathSpy).toHaveBeenCalledTimes(1);
});
it('should call auth.configAuthentication() if source-url input is provided', async () => {
inputs['global-json-file'] = '';
inputs['dotnet-version'] = [];
inputs['dotnet-quality'] = '';
inputs['source-url'] = 'fictitious.source.url';
configAuthenticationSpy.mockImplementation(() => {});
await setup.run();
expect(configAuthenticationSpy).toHaveBeenCalledWith(
inputs['source-url'],
undefined
);
});
it('should call auth.configAuthentication() with proper parameters if source-url and config-file inputs are provided', async () => {
inputs['global-json-file'] = '';
inputs['dotnet-version'] = [];
inputs['dotnet-quality'] = '';
inputs['source-url'] = 'fictitious.source.url';
inputs['config-file'] = 'fictitious.path';
configAuthenticationSpy.mockImplementation(() => {});
setOutputSpy.mockImplementation(() => {});
await setup.run();
expect(configAuthenticationSpy).toHaveBeenCalledWith(
inputs['source-url'],
inputs['config-file']
);
});
it('should call setOutput() after installation complete successfully', async () => {
inputs['dotnet-version'] = ['6.0.300'];
installDotnetSpy.mockImplementation(() =>
Promise.resolve(`${inputs['dotnet-version']}`)
);
addToPathSpy.mockImplementation(() => {});
await setup.run();
expect(setOutputSpy).toHaveBeenCalledTimes(1);
});
it(`shouldn't call setOutput() if parsing dotnet-installer logs failed`, async () => {
inputs['dotnet-version'] = ['6.0.300'];
const warningMessage = `Failed to output the installed version of .NET. The 'dotnet-version' output will not be set.`;
installDotnetSpy.mockImplementation(() => Promise.resolve(null));
addToPathSpy.mockImplementation(() => {});
await setup.run();
expect(warningSpy).toHaveBeenCalledWith(warningMessage);
expect(setOutputSpy).not.toHaveBeenCalled();
});
it(`shouldn't call setOutput() if actions didn't install .NET`, async () => {
inputs['dotnet-version'] = [];
const warningMessage = `The 'dotnet-version' output will not be set.`;
addToPathSpy.mockImplementation(() => {});
await setup.run();
expect(infoSpy).toHaveBeenCalledWith(warningMessage);
expect(setOutputSpy).not.toHaveBeenCalled();
});
});
});

View File

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

140
dist/index.js vendored
View File

@ -237,11 +237,12 @@ const exec = __importStar(__nccwpck_require__(1514));
const io = __importStar(__nccwpck_require__(7436));
const hc = __importStar(__nccwpck_require__(6255));
const fs_1 = __nccwpck_require__(7147);
const promises_1 = __nccwpck_require__(3292);
const path_1 = __importDefault(__nccwpck_require__(1017));
const os_1 = __importDefault(__nccwpck_require__(2037));
const semver_1 = __importDefault(__nccwpck_require__(5911));
const utils_1 = __nccwpck_require__(918);
const QUALITY_INPUT_MINIMAL_MAJOR_TAG = 6;
const LATEST_PATCH_SYNTAX_MINIMAL_MAJOR_TAG = 5;
class DotnetVersionResolver {
constructor(version) {
this.inputVersion = version.trim();
@ -249,36 +250,55 @@ class DotnetVersionResolver {
}
resolveVersionInput() {
return __awaiter(this, void 0, void 0, function* () {
if (!semver_1.default.validRange(this.inputVersion)) {
throw new Error(`'dotnet-version' was supplied in invalid format: ${this.inputVersion}! Supported syntax: A.B.C, A.B, A.B.x, A, A.x`);
if (!semver_1.default.validRange(this.inputVersion) && !this.isLatestPatchSyntax()) {
throw new Error(`The 'dotnet-version' was supplied in invalid format: ${this.inputVersion}! Supported syntax: A.B.C, A.B, A.B.x, A, A.x, A.B.Cxx`);
}
if (semver_1.default.valid(this.inputVersion)) {
this.resolvedArgument.type = 'version';
this.resolvedArgument.value = this.inputVersion;
this.createVersionArgument();
}
else {
const [major, minor] = this.inputVersion.split('.');
if (this.isNumericTag(major)) {
this.resolvedArgument.type = 'channel';
if (this.isNumericTag(minor)) {
this.resolvedArgument.value = `${major}.${minor}`;
}
else {
const httpClient = new hc.HttpClient('actions/setup-dotnet', [], {
allowRetries: true,
maxRetries: 3
});
this.resolvedArgument.value = yield this.getLatestVersion(httpClient, [major, minor]);
}
}
this.resolvedArgument.qualityFlag = +major >= 6 ? true : false;
yield this.createChannelArgument();
}
});
}
isNumericTag(versionTag) {
return /^\d+$/.test(versionTag);
}
createDotnetVersion() {
isLatestPatchSyntax() {
var _a, _b;
const majorTag = (_b = (_a = this.inputVersion.match(/^(?<majorTag>\d+)\.\d+\.\d{1}x{2}$/)) === null || _a === void 0 ? void 0 : _a.groups) === null || _b === void 0 ? void 0 : _b.majorTag;
if (majorTag &&
parseInt(majorTag) < LATEST_PATCH_SYNTAX_MINIMAL_MAJOR_TAG) {
throw new Error(`The 'dotnet-version' was supplied in invalid format: ${this.inputVersion}! The A.B.Cxx syntax is available since the .NET 5.0 release.`);
}
return majorTag ? true : false;
}
createVersionArgument() {
this.resolvedArgument.type = 'version';
this.resolvedArgument.value = this.inputVersion;
}
createChannelArgument() {
return __awaiter(this, void 0, void 0, function* () {
this.resolvedArgument.type = 'channel';
const [major, minor] = this.inputVersion.split('.');
if (this.isLatestPatchSyntax()) {
this.resolvedArgument.value = this.inputVersion;
}
else if (this.isNumericTag(major) && this.isNumericTag(minor)) {
this.resolvedArgument.value = `${major}.${minor}`;
}
else if (this.isNumericTag(major)) {
this.resolvedArgument.value = yield this.getLatestByMajorTag(major);
}
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.
this.resolvedArgument.value = 'LTS';
}
this.resolvedArgument.qualityFlag =
parseInt(major) >= QUALITY_INPUT_MINIMAL_MAJOR_TAG ? true : false;
});
}
createDotNetVersion() {
return __awaiter(this, void 0, void 0, function* () {
yield this.resolveVersionInput();
if (!this.resolvedArgument.type) {
@ -295,17 +315,21 @@ class DotnetVersionResolver {
return this.resolvedArgument;
});
}
getLatestVersion(httpClient, versionParts) {
getLatestByMajorTag(majorTag) {
return __awaiter(this, void 0, void 0, function* () {
const httpClient = new hc.HttpClient('actions/setup-dotnet', [], {
allowRetries: true,
maxRetries: 3
});
const response = yield httpClient.getJson(DotnetVersionResolver.DotNetCoreIndexUrl);
const result = response.result || {};
const releasesInfo = result['releases-index'];
const releaseInfo = releasesInfo.find(info => {
const sdkParts = info['channel-version'].split('.');
return sdkParts[0] === versionParts[0];
return sdkParts[0] === majorTag;
});
if (!releaseInfo) {
throw new Error(`Could not find info for version ${versionParts.join('.')} at ${DotnetVersionResolver.DotNetCoreIndexUrl}`);
throw new Error(`Could not find info for version with major tag: "${majorTag}" at ${DotnetVersionResolver.DotNetCoreIndexUrl}`);
}
return releaseInfo['channel-version'];
});
@ -344,7 +368,8 @@ class DotnetInstallScript {
if (process.env['no_proxy'] != null) {
this.scriptArguments.push(`-ProxyBypassList ${process.env['no_proxy']}`);
}
this.scriptPath = (yield io.which('pwsh', false)) || (yield io.which('powershell', true));
this.scriptPath =
(yield io.which('pwsh', false)) || (yield io.which('powershell', true));
});
}
setupScriptBash() {
@ -362,7 +387,7 @@ class DotnetInstallScript {
this.useArguments(dotnetVersion.type, dotnetVersion.value);
}
if (quality && !dotnetVersion.qualityFlag) {
core.warning(`'dotnet-quality' input can be used only with .NET SDK version in A.B, A.B.x, A and A.x formats where the major tag is higher than 5. You specified: ${dotnetVersion.value}. 'dotnet-quality' input is ignored.`);
core.warning(`The 'dotnet-quality' input can be used only with .NET SDK version in A.B, A.B.x, A, A.x and A.B.Cxx formats where the major tag is higher than 5. You specified: ${dotnetVersion.value}. 'dotnet-quality' input is ignored.`);
return this;
}
if (quality) {
@ -413,30 +438,28 @@ class DotnetCoreInstaller {
this.version = version;
this.quality = quality;
}
;
installDotnet() {
return __awaiter(this, void 0, void 0, function* () {
const versionResolver = new DotnetVersionResolver(this.version);
const dotnetVersion = yield versionResolver.createDotnetVersion();
const dotnetVersion = yield versionResolver.createDotNetVersion();
const installScript = new DotnetInstallScript()
.useArguments(utils_1.IS_WINDOWS ? '-SkipNonVersionedFiles' : '--skip-non-versioned-files')
.useVersion(dotnetVersion, this.quality);
const { exitCode, stderr } = yield installScript.execute();
const { exitCode, stderr, stdout } = yield installScript.execute();
if (exitCode) {
throw new Error(`Failed to install dotnet, exit code: ${exitCode}. ${stderr}`);
}
return this.outputDotnetVersion(dotnetVersion.value);
return this.parseInstalledVersion(stdout);
});
}
outputDotnetVersion(version) {
return __awaiter(this, void 0, void 0, function* () {
const installationPath = process.env['DOTNET_INSTALL_DIR'];
const versionsOnRunner = yield (0, promises_1.readdir)(path_1.default.join(installationPath.replace(/'/g, ''), 'sdk'));
const installedVersion = semver_1.default.maxSatisfying(versionsOnRunner, version, {
includePrerelease: true
});
return installedVersion;
});
parseInstalledVersion(stdout) {
const regex = /(?<version>\d+\.\d+\.\d+[a-z0-9._-]*)/gm;
const matchedResult = regex.exec(stdout);
if (!matchedResult) {
core.warning(`Failed to parse installed by the script version of .NET`);
return null;
}
return matchedResult.groups.version;
}
}
exports.DotnetCoreInstaller = DotnetCoreInstaller;
@ -533,13 +556,13 @@ function run() {
versions.push(getVersionFromGlobalJson(globalJsonPath));
}
else {
core.info(`global.json wasn't found in the root directory. No .NET version will be installed.`);
core.info(`The global.json wasn't found in the root directory. No .NET version will be installed.`);
}
}
if (versions.length) {
const quality = core.getInput('dotnet-quality');
if (quality && !qualityOptions.includes(quality)) {
throw new Error(`${quality} is not a supported value for 'dotnet-quality' option. Supported values are: daily, signed, validated, preview, ga.`);
throw new Error(`Value '${quality}' is not supported for the 'dotnet-quality' option. Supported values are: daily, signed, validated, preview, ga.`);
}
let dotnetInstaller;
const uniqueVersions = new Set(versions);
@ -555,13 +578,7 @@ function run() {
if (sourceUrl) {
auth.configAuthentication(sourceUrl, configFile);
}
const comparisonRange = globalJsonFileInput
? versions[versions.length - 1]
: '*';
const versionToOutput = semver_1.default.maxSatisfying(installedDotnetVersions, comparisonRange, {
includePrerelease: true
});
core.setOutput('dotnet-version', versionToOutput);
outputInstalledVersion(installedDotnetVersions, globalJsonFileInput);
const matchersPath = path_1.default.join(__dirname, '..', '.github');
core.info(`##[add-matcher]${path_1.default.join(matchersPath, 'csc.json')}`);
}
@ -586,6 +603,25 @@ function getVersionFromGlobalJson(globalJsonPath) {
}
return version;
}
function outputInstalledVersion(installedVersions, globalJsonFileInput) {
if (!installedVersions.length) {
core.info(`The 'dotnet-version' output will not be set.`);
return;
}
if (installedVersions.includes(null)) {
core.warning(`Failed to output the installed version of .NET. The 'dotnet-version' output will not be set.`);
return;
}
if (globalJsonFileInput) {
const versionToOutput = installedVersions.at(-1); // .NET SDK version parsed from the global.json file is installed last
core.setOutput('dotnet-version', versionToOutput);
return;
}
const versionToOutput = semver_1.default.maxSatisfying(installedVersions, '*', {
includePrerelease: true
});
core.setOutput('dotnet-version', versionToOutput);
}
run();
@ -21098,14 +21134,6 @@ module.exports = require("fs");
/***/ }),
/***/ 3292:
/***/ ((module) => {
"use strict";
module.exports = require("fs/promises");
/***/ }),
/***/ 3685:
/***/ ((module) => {

View File

@ -4,7 +4,6 @@ import * as exec from '@actions/exec';
import * as io from '@actions/io';
import * as hc from '@actions/http-client';
import {chmodSync} from 'fs';
import {readdir} from 'fs/promises';
import path from 'path';
import os from 'os';
import semver from 'semver';
@ -17,6 +16,8 @@ export interface DotnetVersion {
qualityFlag: boolean;
}
const QUALITY_INPUT_MINIMAL_MAJOR_TAG = 6;
const LATEST_PATCH_SYNTAX_MINIMAL_MAJOR_TAG = 5;
export class DotnetVersionResolver {
private inputVersion: string;
private resolvedArgument: DotnetVersion;
@ -27,33 +28,15 @@ export class DotnetVersionResolver {
}
private async resolveVersionInput(): Promise<void> {
if (!semver.validRange(this.inputVersion)) {
if (!semver.validRange(this.inputVersion) && !this.isLatestPatchSyntax()) {
throw new Error(
`'dotnet-version' was supplied in invalid format: ${this.inputVersion}! Supported syntax: A.B.C, A.B, A.B.x, A, A.x`
`The 'dotnet-version' was supplied in invalid format: ${this.inputVersion}! Supported syntax: A.B.C, A.B, A.B.x, A, A.x, A.B.Cxx`
);
}
if (semver.valid(this.inputVersion)) {
this.resolvedArgument.type = 'version';
this.resolvedArgument.value = this.inputVersion;
this.createVersionArgument();
} else {
const [major, minor] = this.inputVersion.split('.');
if (this.isNumericTag(major)) {
this.resolvedArgument.type = 'channel';
if (this.isNumericTag(minor)) {
this.resolvedArgument.value = `${major}.${minor}`;
} else {
const httpClient = new hc.HttpClient('actions/setup-dotnet', [], {
allowRetries: true,
maxRetries: 3
});
this.resolvedArgument.value = await this.getLatestVersion(
httpClient,
[major, minor]
);
}
}
this.resolvedArgument.qualityFlag = +major >= 6 ? true : false;
await this.createChannelArgument();
}
}
@ -61,11 +44,44 @@ export class DotnetVersionResolver {
return /^\d+$/.test(versionTag);
}
public async createDotnetVersion(): Promise<{
type: string;
value: string;
qualityFlag: boolean;
}> {
private isLatestPatchSyntax() {
const majorTag = this.inputVersion.match(
/^(?<majorTag>\d+)\.\d+\.\d{1}x{2}$/
)?.groups?.majorTag;
if (
majorTag &&
parseInt(majorTag) < LATEST_PATCH_SYNTAX_MINIMAL_MAJOR_TAG
) {
throw new Error(
`The 'dotnet-version' was supplied in invalid format: ${this.inputVersion}! The A.B.Cxx syntax is available since the .NET 5.0 release.`
);
}
return majorTag ? true : false;
}
private createVersionArgument() {
this.resolvedArgument.type = 'version';
this.resolvedArgument.value = this.inputVersion;
}
private async createChannelArgument() {
this.resolvedArgument.type = 'channel';
const [major, minor] = this.inputVersion.split('.');
if (this.isLatestPatchSyntax()) {
this.resolvedArgument.value = this.inputVersion;
} else if (this.isNumericTag(major) && this.isNumericTag(minor)) {
this.resolvedArgument.value = `${major}.${minor}`;
} else if (this.isNumericTag(major)) {
this.resolvedArgument.value = await this.getLatestByMajorTag(major);
} 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.
this.resolvedArgument.value = 'LTS';
}
this.resolvedArgument.qualityFlag =
parseInt(major) >= QUALITY_INPUT_MINIMAL_MAJOR_TAG ? true : false;
}
public async createDotNetVersion(): Promise<DotnetVersion> {
await this.resolveVersionInput();
if (!this.resolvedArgument.type) {
return this.resolvedArgument;
@ -80,10 +96,11 @@ export class DotnetVersionResolver {
return this.resolvedArgument;
}
private async getLatestVersion(
httpClient: hc.HttpClient,
versionParts: string[]
): Promise<string> {
private async getLatestByMajorTag(majorTag: string): Promise<string> {
const httpClient = new hc.HttpClient('actions/setup-dotnet', [], {
allowRetries: true,
maxRetries: 3
});
const response = await httpClient.getJson<any>(
DotnetVersionResolver.DotNetCoreIndexUrl
);
@ -92,14 +109,12 @@ export class DotnetVersionResolver {
const releaseInfo = releasesInfo.find(info => {
const sdkParts: string[] = info['channel-version'].split('.');
return sdkParts[0] === versionParts[0];
return sdkParts[0] === majorTag;
});
if (!releaseInfo) {
throw new Error(
`Could not find info for version ${versionParts.join('.')} at ${
DotnetVersionResolver.DotNetCoreIndexUrl
}`
`Could not find info for version with major tag: "${majorTag}" at ${DotnetVersionResolver.DotNetCoreIndexUrl}`
);
}
@ -127,6 +142,8 @@ export class DotnetInstallScript {
: this.setupScriptBash();
}
private async setupScriptPowershell() {
this.scriptArguments = [
'-NoLogo',
@ -169,7 +186,7 @@ export class DotnetInstallScript {
if (quality && !dotnetVersion.qualityFlag) {
core.warning(
`'dotnet-quality' input can be used only with .NET SDK version in A.B, A.B.x, A and A.x formats where the major tag is higher than 5. You specified: ${dotnetVersion.value}. 'dotnet-quality' input is ignored.`
`The 'dotnet-quality' input can be used only with .NET SDK version in A.B, A.B.x, A, A.x and A.B.Cxx formats where the major tag is higher than 5. You specified: ${dotnetVersion.value}. 'dotnet-quality' input is ignored.`
);
return this;
}
@ -239,9 +256,9 @@ export class DotnetCoreInstaller {
constructor(private version: string, private quality: QualityOptions) {}
public async installDotnet(): Promise<string> {
public async installDotnet(): Promise<string | null> {
const versionResolver = new DotnetVersionResolver(this.version);
const dotnetVersion = await versionResolver.createDotnetVersion();
const dotnetVersion = await versionResolver.createDotNetVersion();
const installScript = new DotnetInstallScript()
.useArguments(
@ -249,7 +266,7 @@ export class DotnetCoreInstaller {
)
.useVersion(dotnetVersion, this.quality);
const {exitCode, stderr} = await installScript.execute();
const {exitCode, stderr, stdout} = await installScript.execute();
if (exitCode) {
throw new Error(
@ -257,19 +274,17 @@ export class DotnetCoreInstaller {
);
}
return this.outputDotnetVersion(dotnetVersion.value);
return this.parseInstalledVersion(stdout);
}
private async outputDotnetVersion(version): Promise<string> {
const installationPath = process.env['DOTNET_INSTALL_DIR']!;
const versionsOnRunner: string[] = await readdir(
path.join(installationPath.replace(/'/g, ''), 'sdk')
);
private parseInstalledVersion(stdout: string): string | null {
const regex = /(?<version>\d+\.\d+\.\d+[a-z0-9._-]*)/gm;
const matchedResult = regex.exec(stdout);
const installedVersion = semver.maxSatisfying(versionsOnRunner, version, {
includePrerelease: true
})!;
return installedVersion;
if (!matchedResult) {
core.warning(`Failed to parse installed by the script version of .NET`);
return null;
}
return matchedResult.groups!.version;
}
}

View File

@ -27,7 +27,7 @@ export async function run() {
// Proxy, auth, (etc) are still set up, even if no version is identified
//
const versions = core.getMultilineInput('dotnet-version');
const installedDotnetVersions: string[] = [];
const installedDotnetVersions: (string | null)[] = [];
const globalJsonFileInput = core.getInput('global-json-file');
if (globalJsonFileInput) {
@ -48,7 +48,7 @@ export async function run() {
versions.push(getVersionFromGlobalJson(globalJsonPath));
} else {
core.info(
`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.`
);
}
}
@ -58,7 +58,7 @@ export async function run() {
if (quality && !qualityOptions.includes(quality)) {
throw new Error(
`${quality} is not a supported value for 'dotnet-quality' option. Supported values are: daily, signed, validated, preview, ga.`
`Value '${quality}' is not supported for the 'dotnet-quality' option. Supported values are: daily, signed, validated, preview, ga.`
);
}
@ -78,19 +78,7 @@ export async function run() {
auth.configAuthentication(sourceUrl, configFile);
}
const comparisonRange: string = globalJsonFileInput
? versions[versions.length - 1]!
: '*';
const versionToOutput = semver.maxSatisfying(
installedDotnetVersions,
comparisonRange,
{
includePrerelease: true
}
);
core.setOutput('dotnet-version', versionToOutput);
outputInstalledVersion(installedDotnetVersions, globalJsonFileInput);
const matchersPath = path.join(__dirname, '..', '.github');
core.info(`##[add-matcher]${path.join(matchersPath, 'csc.json')}`);
@ -116,4 +104,37 @@ function getVersionFromGlobalJson(globalJsonPath: string): string {
return version;
}
function outputInstalledVersion(
installedVersions: (string | null)[],
globalJsonFileInput: string
): void {
if (!installedVersions.length) {
core.info(`The 'dotnet-version' output will not be set.`);
return;
}
if (installedVersions.includes(null)) {
core.warning(
`Failed to output the installed version of .NET. The 'dotnet-version' output will not be set.`
);
return;
}
if (globalJsonFileInput) {
const versionToOutput = installedVersions.at(-1); // .NET SDK version parsed from the global.json file is installed last
core.setOutput('dotnet-version', versionToOutput);
return;
}
const versionToOutput = semver.maxSatisfying(
installedVersions as string[],
'*',
{
includePrerelease: true
}
);
core.setOutput('dotnet-version', versionToOutput);
}
run();