mirror of
https://github.com/actions/setup-dotnet.git
synced 2026-10-06 06:33:10 +01:00
* 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>
362 lines
12 KiB
TypeScript
362 lines
12 KiB
TypeScript
import * as core from '@actions/core';
|
|
import * as exec from '@actions/exec';
|
|
import {
|
|
DotnetCoreInstaller,
|
|
DotnetInstallDir,
|
|
normalizeArch
|
|
} from './installer.js';
|
|
import * as fs from 'fs';
|
|
import path from 'path';
|
|
import {fileURLToPath} from 'url';
|
|
import semver from 'semver';
|
|
import os from 'os';
|
|
import * as auth from './authutil.js';
|
|
import {isCacheFeatureAvailable} from './cache-utils.js';
|
|
import {restoreCache} from './cache-restore.js';
|
|
import {Outputs} from './constants.js';
|
|
import JSON5 from 'json5';
|
|
|
|
const qualityOptions = ['daily', 'preview', 'ga'] as const;
|
|
const supportedArchitectures = [
|
|
'x64',
|
|
'x86',
|
|
'arm64',
|
|
'amd64',
|
|
'arm',
|
|
's390x',
|
|
'ppc64le',
|
|
'riscv64'
|
|
] as const;
|
|
type SupportedArchitecture = (typeof supportedArchitectures)[number];
|
|
|
|
export type QualityOptions = (typeof qualityOptions)[number] | '';
|
|
|
|
const CHECK_LATEST_ENV_VAR = 'DOTNET_CHECK_LATEST';
|
|
|
|
function isValidChannel(channel: string): boolean {
|
|
const upper = channel.toUpperCase();
|
|
if (upper === 'LTS' || upper === 'STS') return true;
|
|
// A.B format (e.g., 3.1, 8.0)
|
|
if (/^\d+\.\d+$/.test(channel)) return true;
|
|
// A.B.Cxx format (e.g., 8.0.1xx) - available since 5.0
|
|
const match = channel.match(/^(?<major>\d+)\.\d+\.\d{1}xx$/);
|
|
if (match && parseInt(match.groups!.major) >= 5) return true;
|
|
return false;
|
|
}
|
|
|
|
export async function run() {
|
|
try {
|
|
//
|
|
// dotnet-version is optional, but needs to be provided for most use cases.
|
|
// If supplied, install / use from the tool cache.
|
|
// global-version-file may be specified to point to a specific global.json
|
|
// and will be used to install an additional version.
|
|
// If not supplied, look for version in ./global.json.
|
|
// If a valid version still can't be identified, nothing will be installed.
|
|
// Proxy, auth, (etc) are still set up, even if no version is identified
|
|
//
|
|
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 architecture = getArchitectureInput();
|
|
const checkLatest = getCheckLatestInput();
|
|
let dotnetChannel = core.getInput('dotnet-channel');
|
|
|
|
const isLatestRequested = versions.some(
|
|
version => version && version.toLowerCase() === 'latest'
|
|
);
|
|
if (dotnetChannel && !isValidChannel(dotnetChannel)) {
|
|
if (isLatestRequested) {
|
|
throw new Error(
|
|
`Value '${dotnetChannel}' is not supported for the 'dotnet-channel' option. Supported values are: LTS, STS, A.B (e.g. 8.0), A.B.Cxx (e.g. 8.0.1xx).`
|
|
);
|
|
} else {
|
|
core.warning(
|
|
`Value '${dotnetChannel}' is not supported for the 'dotnet-channel' option and will be ignored because 'dotnet-version' is not set to 'latest'. Supported values are: LTS, STS, A.B (e.g. 8.0), A.B.Cxx (e.g. 8.0.1xx).`
|
|
);
|
|
dotnetChannel = '';
|
|
}
|
|
} else if (dotnetChannel && !isLatestRequested) {
|
|
core.warning(
|
|
`The 'dotnet-channel' input is only supported when 'dotnet-version' is set to 'latest'.`
|
|
);
|
|
dotnetChannel = '';
|
|
}
|
|
|
|
const globalJsonFileInput = core.getInput('global-json-file');
|
|
if (globalJsonFileInput) {
|
|
const globalJsonPath = path.resolve(process.cwd(), globalJsonFileInput);
|
|
if (!fs.existsSync(globalJsonPath)) {
|
|
throw new Error(
|
|
`The specified global.json file '${globalJsonFileInput}' does not exist`
|
|
);
|
|
}
|
|
addVersionFromGlobalJson(globalJsonPath);
|
|
}
|
|
|
|
if (!versions.length) {
|
|
// Try to fall back to global.json
|
|
core.debug('No version found, trying to find version from global.json');
|
|
const globalJsonPath = path.join(process.cwd(), 'global.json');
|
|
if (fs.existsSync(globalJsonPath)) {
|
|
addVersionFromGlobalJson(globalJsonPath);
|
|
} else {
|
|
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') as QualityOptions;
|
|
|
|
if (quality && !qualityOptions.includes(quality)) {
|
|
throw new Error(
|
|
`Value '${quality}' is not supported for the 'dotnet-quality' option. Supported values are: daily, preview, ga.`
|
|
);
|
|
}
|
|
|
|
let dotnetInstaller: DotnetCoreInstaller;
|
|
const uniqueVersions = new Set<string>(
|
|
versions.map(v => (v.toLowerCase() === 'latest' ? 'latest' : v))
|
|
);
|
|
for (const version of uniqueVersions) {
|
|
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();
|
|
installedDotnetVersions.push(installedVersion);
|
|
}
|
|
if (
|
|
architecture &&
|
|
normalizeArch(architecture) !== normalizeArch(os.arch())
|
|
) {
|
|
process.env['DOTNET_INSTALL_DIR'] = path.join(
|
|
DotnetInstallDir.dirPath,
|
|
architecture
|
|
);
|
|
}
|
|
DotnetInstallDir.addToPath();
|
|
|
|
const workloadsInput = core.getInput('workloads');
|
|
if (workloadsInput) {
|
|
const workloads = workloadsInput
|
|
.split(',')
|
|
.map(w => w.trim())
|
|
.filter(Boolean);
|
|
|
|
if (workloads.length) {
|
|
try {
|
|
core.info(`Refreshing workload manifests...`);
|
|
await exec.exec('dotnet', ['workload', 'update']);
|
|
|
|
core.info(`Installing workloads: ${workloads.join(', ')}`);
|
|
await exec.exec('dotnet', ['workload', 'install', ...workloads]);
|
|
} catch (err) {
|
|
throw new Error(
|
|
`Failed to install workloads [${workloads.join(', ')}]: ${err}`,
|
|
{cause: err}
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
const sourceUrl: string = core.getInput('source-url');
|
|
const configFile: string = core.getInput('config-file');
|
|
if (sourceUrl) {
|
|
auth.configAuthentication(sourceUrl, configFile);
|
|
}
|
|
|
|
outputInstalledVersion(installedDotnetVersions, globalJsonFileInput);
|
|
|
|
if (core.getBooleanInput('cache') && isCacheFeatureAvailable()) {
|
|
const cacheDependencyPath = core.getInput('cache-dependency-path');
|
|
await restoreCache(cacheDependencyPath);
|
|
}
|
|
|
|
const matchersPath = path.join(
|
|
path.dirname(fileURLToPath(import.meta.url)),
|
|
'..',
|
|
'..',
|
|
'.github'
|
|
);
|
|
core.info(`##[add-matcher]${path.join(matchersPath, 'csc.json')}`);
|
|
} catch (error) {
|
|
core.setFailed(error.message);
|
|
}
|
|
}
|
|
|
|
function getArchitectureInput(): SupportedArchitecture | '' {
|
|
const raw = (core.getInput('architecture') || '').trim();
|
|
if (!raw) return '';
|
|
const normalized = raw.toLowerCase();
|
|
if ((supportedArchitectures as readonly string[]).includes(normalized)) {
|
|
return normalizeArch(normalized) as SupportedArchitecture;
|
|
}
|
|
throw new Error(
|
|
`Value '${raw}' is not supported for the 'architecture' option. Supported values are: ${supportedArchitectures.join(
|
|
', '
|
|
)}.`
|
|
);
|
|
}
|
|
|
|
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 minimumVersion: string | undefined;
|
|
let rollForwardPolicy: string | undefined;
|
|
const globalJson = JSON5.parse(
|
|
// .trim() is necessary to strip BOM https://github.com/nodejs/node/issues/20649
|
|
fs.readFileSync(globalJsonPath, {encoding: 'utf8'}).trim(),
|
|
// is necessary as JSON5 supports wider variety of options for numbers: https://www.npmjs.com/package/json5#numbers
|
|
(key, value) => {
|
|
if (key === 'version' || key === 'rollForward') return String(value);
|
|
return value;
|
|
}
|
|
);
|
|
if (globalJson.sdk && globalJson.sdk.version) {
|
|
version = globalJson.sdk.version;
|
|
const rollForward = globalJson.sdk.rollForward;
|
|
if (rollForward && !semver.prerelease(version)) {
|
|
if (!versionPattern.test(version)) {
|
|
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. ` +
|
|
`See: https://learn.microsoft.com/en-us/dotnet/core/tools/global-json`
|
|
);
|
|
}
|
|
|
|
const [major, minor, featurePatch] = version.split('.');
|
|
const feature = featurePatch.substring(0, 1);
|
|
|
|
switch (rollForward) {
|
|
case 'latestMajor':
|
|
version = '';
|
|
break;
|
|
|
|
case 'latestMinor':
|
|
version = `${major}`;
|
|
break;
|
|
|
|
case 'latestFeature':
|
|
version = `${major}.${minor}`;
|
|
break;
|
|
|
|
case 'latestPatch':
|
|
version = `${major}.${minor}.${feature}xx`;
|
|
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, minimumVersion, rollForward: rollForwardPolicy};
|
|
}
|
|
|
|
function outputInstalledVersion(
|
|
installedVersions: (string | null)[],
|
|
globalJsonFileInput: string
|
|
): void {
|
|
if (!installedVersions.length) {
|
|
core.info(`The '${Outputs.DotnetVersion}' output will not be set.`);
|
|
return;
|
|
}
|
|
|
|
if (installedVersions.includes(null)) {
|
|
core.warning(
|
|
`Failed to output the installed version of .NET. The '${Outputs.DotnetVersion}' 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(Outputs.DotnetVersion, versionToOutput);
|
|
return;
|
|
}
|
|
|
|
const versionToOutput = semver.maxSatisfying(
|
|
installedVersions as string[],
|
|
'*',
|
|
{
|
|
includePrerelease: true
|
|
}
|
|
);
|
|
|
|
core.setOutput(Outputs.DotnetVersion, versionToOutput);
|
|
}
|
|
|
|
run();
|