Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,32 @@ steps:

> **Important**: When using `rollForward` in `global.json`, the `sdk.version` field must be a fully-qualified SDK version (e.g., `8.0.100`, `10.0.100`). Wildcard versions (e.g., `10.0.*`) and runtime-style versions (e.g., `8.0.0`) are not supported. See the [.NET SDK version specification](https://learn.microsoft.com/en-us/dotnet/core/tools/global-json#version) for details.

## Using the `dotnet-version-file` input
`setup-dotnet` can read the .NET SDK version from a version file via the `dotnet-version-file` input. This is handy for sharing a single source of truth with tools such as [asdf](https://asdf-vm.com/) and [mise](https://mise.jdx.dev/). The input accepts:

- a [`.tool-versions`](https://asdf-vm.com/manage/configuration.html#tool-versions) file — the version is read from its `dotnet` entry;
- a `global.json` file — the version is read from its `sdk.version` field.

If the file supplied to `dotnet-version-file` doesn't exist, the action fails. If the file exists but contains no .NET SDK version, a warning is logged and no version is installed from it.

```yml
steps:
- uses: actions/checkout@v6
- uses: actions/setup-dotnet@v5
with:
dotnet-version-file: .tool-versions
- run: dotnet build <my project>
```

A `.tool-versions` file may pin several tools; only the `dotnet` entry is used:

```
nodejs 20.11.0
dotnet 8.0.100
```

>**Note**: In case both `dotnet-version` and `dotnet-version-file` inputs are used, versions from both inputs will be installed.

## Caching NuGet Packages
The action has a built-in functionality for caching and restoring dependencies. It uses [toolkit/cache](https://github.com/actions/toolkit/tree/main/packages/cache) under the hood for caching global packages data but requires less configuration settings. The `cache` input is optional, and caching is turned off by default.

Expand Down
75 changes: 75 additions & 0 deletions __tests__/setup-dotnet.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ describe('setup-dotnet tests', () => {
const setOutputSpy = core.setOutput as jest.Mock;

const existsSyncSpy = fs.existsSync as jest.Mock;
const readFileSyncSpy = fs.readFileSync as jest.Mock;

const maxSatisfyingSpy = jest.spyOn(semver, 'maxSatisfying');

Expand Down Expand Up @@ -122,6 +123,80 @@ describe('setup-dotnet tests', () => {
expect(infoSpy).toHaveBeenCalledWith(expectedInfoMessage);
});

it('should fail the action if dotnet-version-file input is present, but the file does not exist in the file system', async () => {
inputs['dotnet-version'] = [];
inputs['global-json-file'] = '';
inputs['dotnet-version-file'] = '.tool-versions';

existsSyncSpy.mockReturnValue(false);

const expectedErrorMessage = `The specified dotnet-version-file '${inputs['dotnet-version-file']}' does not exist`;

await setup.run();
expect(setFailedSpy).toHaveBeenCalledWith(expectedErrorMessage);

inputs['dotnet-version-file'] = '';
});

it('should read the .NET SDK version from a .tool-versions file supplied via dotnet-version-file', async () => {
inputs['dotnet-version'] = [];
inputs['global-json-file'] = '';
inputs['dotnet-quality'] = '';
inputs['dotnet-version-file'] = '.tool-versions';

existsSyncSpy.mockReturnValue(true);
readFileSyncSpy.mockReturnValue('nodejs 20.0.0\ndotnet 8.0.100\n');
installDotnetSpy.mockImplementation(() => Promise.resolve('8.0.100'));
setOutputSpy.mockImplementation(() => {});

await setup.run();

expect(installDotnetSpy).toHaveBeenCalledTimes(1);
expect(setOutputSpy).toHaveBeenCalledWith('dotnet-version', '8.0.100');

inputs['dotnet-version-file'] = '';
});

it('should read the .NET SDK version from a global.json supplied via dotnet-version-file', async () => {
inputs['dotnet-version'] = [];
inputs['global-json-file'] = '';
inputs['dotnet-quality'] = '';
inputs['dotnet-version-file'] = 'csharp/global.json';

existsSyncSpy.mockReturnValue(true);
readFileSyncSpy.mockReturnValue('{"sdk":{"version":"8.0.100"}}');
installDotnetSpy.mockImplementation(() => Promise.resolve('8.0.100'));
setOutputSpy.mockImplementation(() => {});

await setup.run();

expect(installDotnetSpy).toHaveBeenCalledTimes(1);
expect(setOutputSpy).toHaveBeenCalledWith('dotnet-version', '8.0.100');

inputs['dotnet-version-file'] = '';
});

it('should warn and not install if dotnet-version-file has no dotnet entry', async () => {
inputs['dotnet-version'] = [];
inputs['global-json-file'] = '';
inputs['dotnet-version-file'] = '.tool-versions';

// The version file exists, but the fallback global.json in the repo root does not.
existsSyncSpy.mockImplementation(filePath =>
String(filePath).endsWith('.tool-versions')
);
readFileSyncSpy.mockReturnValue('nodejs 20.0.0\n# no dotnet here\n');

await setup.run();

expect(warningSpy).toHaveBeenCalledWith(
`No .NET SDK version was found in '${inputs['dotnet-version-file']}'. Make sure the file contains a 'dotnet' entry (.tool-versions) or an 'sdk.version' field (global.json).`
);
expect(installDotnetSpy).not.toHaveBeenCalled();

inputs['dotnet-version-file'] = '';
});

it('should fail the action if quality is supplied but its value is not supported', async () => {
inputs['global-json-file'] = '';
inputs['dotnet-version'] = ['10.0'];
Expand Down
3 changes: 3 additions & 0 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ inputs:
global-json-file:
description: 'Optional global.json location, if your global.json isn''t located in the root of the repo.'
required: false
dotnet-version-file:
description: 'Optional path to a file with the .NET SDK version to install. Supports a .tool-versions file (asdf/mise format, reads the "dotnet" entry) or a global.json file.'
required: false
source-url:
description: 'Optional package source for which to set up authentication. Will consult any existing NuGet.config in the root of the repo and provide a temporary NuGet.config using the NUGET_AUTH_TOKEN environment variable as a ClearTextPassword'
required: false
Expand Down
44 changes: 40 additions & 4 deletions dist/setup/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -105747,6 +105747,20 @@ async function run() {
}
versions.push(getVersionFromGlobalJson(globalJsonPath));
}
const versionFileInput = getInput('dotnet-version-file');
if (versionFileInput) {
const versionFilePath = external_path_default().resolve(process.cwd(), versionFileInput);
if (!external_fs_namespaceObject.existsSync(versionFilePath)) {
throw new Error(`The specified dotnet-version-file '${versionFileInput}' does not exist`);
}
const versionFromFile = getVersionFromFile(versionFilePath);
if (versionFromFile) {
versions.push(versionFromFile);
}
else {
warning(`No .NET SDK version was found in '${versionFileInput}'. Make sure the file contains a 'dotnet' entry (.tool-versions) or an 'sdk.version' field (global.json).`);
}
}
if (!versions.length) {
// Try to fall back to global.json
core_debug('No version found, trying to find version from global.json');
Expand Down Expand Up @@ -105799,7 +105813,7 @@ async function run() {
if (sourceUrl) {
configAuthentication(sourceUrl, configFile);
}
outputInstalledVersion(installedDotnetVersions, globalJsonFileInput);
outputInstalledVersion(installedDotnetVersions, globalJsonFileInput || versionFileInput);
if (getBooleanInput('cache') && isCacheFeatureAvailable()) {
const cacheDependencyPath = getInput('cache-dependency-path');
await cache_restore_restoreCache(cacheDependencyPath);
Expand Down Expand Up @@ -105862,7 +105876,29 @@ function getVersionFromGlobalJson(globalJsonPath) {
}
return version;
}
function outputInstalledVersion(installedVersions, globalJsonFileInput) {
function getVersionFromToolVersions(toolVersionsPath) {
const content = external_fs_namespaceObject.readFileSync(toolVersionsPath, { encoding: 'utf8' });
for (const line of content.split(/\r?\n/)) {
// A .tool-versions entry is `<tool> <version>`; lines starting with `#` are comments.
const trimmed = line.trim();
if (!trimmed || trimmed.startsWith('#'))
continue;
const [tool, version] = trimmed.split(/\s+/);
if (tool === 'dotnet' && version) {
return version;
}
}
return '';
}
function getVersionFromFile(versionFilePath) {
// Dispatch by file: a global.json is parsed as JSON, anything else
// (e.g. .tool-versions) is parsed in the asdf/mise `<tool> <version>` format.
if (external_path_default().basename(versionFilePath).toLowerCase().endsWith('.json')) {
return getVersionFromGlobalJson(versionFilePath);
}
return getVersionFromToolVersions(versionFilePath);
}
function outputInstalledVersion(installedVersions, versionFileInput) {
if (!installedVersions.length) {
info(`The '${Outputs.DotnetVersion}' output will not be set.`);
return;
Expand All @@ -105871,8 +105907,8 @@ function outputInstalledVersion(installedVersions, globalJsonFileInput) {
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
if (versionFileInput) {
const versionToOutput = installedVersions.at(-1); // .NET SDK version parsed from the version file is installed last
setOutput(Outputs.DotnetVersion, versionToOutput);
return;
}
Expand Down
52 changes: 48 additions & 4 deletions src/setup-dotnet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,24 @@ export async function run() {
versions.push(getVersionFromGlobalJson(globalJsonPath));
}

const versionFileInput = core.getInput('dotnet-version-file');
if (versionFileInput) {
const versionFilePath = path.resolve(process.cwd(), versionFileInput);
if (!fs.existsSync(versionFilePath)) {
throw new Error(
`The specified dotnet-version-file '${versionFileInput}' does not exist`
);
}
const versionFromFile = getVersionFromFile(versionFilePath);
if (versionFromFile) {
versions.push(versionFromFile);
} else {
core.warning(
`No .NET SDK version was found in '${versionFileInput}'. Make sure the file contains a 'dotnet' entry (.tool-versions) or an 'sdk.version' field (global.json).`
);
}
}

if (!versions.length) {
// Try to fall back to global.json
core.debug('No version found, trying to find version from global.json');
Expand Down Expand Up @@ -167,7 +185,10 @@ export async function run() {
auth.configAuthentication(sourceUrl, configFile);
}

outputInstalledVersion(installedDotnetVersions, globalJsonFileInput);
outputInstalledVersion(
installedDotnetVersions,
globalJsonFileInput || versionFileInput
);

if (core.getBooleanInput('cache') && isCacheFeatureAvailable()) {
const cacheDependencyPath = core.getInput('cache-dependency-path');
Expand Down Expand Up @@ -249,9 +270,32 @@ function getVersionFromGlobalJson(globalJsonPath: string): string {
return version;
}

function getVersionFromToolVersions(toolVersionsPath: string): string {
const content = fs.readFileSync(toolVersionsPath, {encoding: 'utf8'});
for (const line of content.split(/\r?\n/)) {
// A .tool-versions entry is `<tool> <version>`; lines starting with `#` are comments.
const trimmed = line.trim();
if (!trimmed || trimmed.startsWith('#')) continue;
const [tool, version] = trimmed.split(/\s+/);
if (tool === 'dotnet' && version) {
return version;
}
}
return '';
}

function getVersionFromFile(versionFilePath: string): string {
// Dispatch by file: a global.json is parsed as JSON, anything else
// (e.g. .tool-versions) is parsed in the asdf/mise `<tool> <version>` format.
if (path.basename(versionFilePath).toLowerCase().endsWith('.json')) {
return getVersionFromGlobalJson(versionFilePath);
}
return getVersionFromToolVersions(versionFilePath);
}

function outputInstalledVersion(
installedVersions: (string | null)[],
globalJsonFileInput: string
versionFileInput: string
): void {
if (!installedVersions.length) {
core.info(`The '${Outputs.DotnetVersion}' output will not be set.`);
Expand All @@ -265,8 +309,8 @@ function outputInstalledVersion(
return;
}

if (globalJsonFileInput) {
const versionToOutput = installedVersions.at(-1); // .NET SDK version parsed from the global.json file is installed last
if (versionFileInput) {
const versionToOutput = installedVersions.at(-1); // .NET SDK version parsed from the version file is installed last
core.setOutput(Outputs.DotnetVersion, versionToOutput);
return;
}
Expand Down