Compare commits

..

9 Commits

Author SHA1 Message Date
aparnajyothi-y
55ec9447dd V3 - Update the file name publish-immutable-actions to publish-immutable-action (#630)
* add Publish Immutable Action

* file name update
2025-04-17 13:21:33 -05:00
aparnajyothi-y
70918d1a1f add Publish Immutable Action (#627) 2025-04-17 09:04:29 -05:00
mahabaleshwars
a6d1e7b915 update actions/cache (#620) 2025-04-13 22:10:06 -05:00
Zachary Taylor
b2ace4b12f v3 - Remove azureedge.net fallback logic and update install scripts (#573)
* Remove logic for azureedge.net fallback in preparation for install script changes (v3)

* remove fallback in index.js

* fix ci

* add updated install scripts

* Update e2e-tests.yml

* Update e2e-tests.yml to remove outdated URLs in workflow

* Fix audit issues

* Revert audit fixes for heap memory failures

---------

Co-authored-by: HarithaVattikuti <73516759+HarithaVattikuti@users.noreply.github.com>
2025-02-20 12:23:35 -06:00
HarithaVattikuti
ea9897a6e5 V3 - Use new .NET CDN URLs and update to latest install scripts (#567)
* Update new cdn

* new cdn changes

* update latest install scripts

* Format install scripts

* Fix Ubuntu-latest issues

* Fallback logic

* Format check

* Update signed version
2024-12-26 15:42:59 -06:00
Nogic
3447fd6a9f feat: Cache NuGet global-packages folder (#303)
* feat: cache NuGet global-packages folder

* fix: remove unused files

* docs: fix incorrect action

* ci: add e2e test for cache

* docs: accept suggested changes on README

* docs: add simple cache example

* build: change main script path

* fix: change relative path to install scripts

* fix: change relative path to problem matcher

* refactor: accept changes on cache-utils

* fix: revert main script path changes

* test: fix cache-utils unit test

* test: fix cache-utils unit test

* feat: add `cache-dependency-path` variables

* build: change main script dist path

* ci: add `cache-dependency-path` e2e test & missing lock file

* fix: accept change suggestions

* ci: copy NuGet lock file to root

to pass "test-setup-with-cache" e2e test

* docs: change README guide

* fix: apply suggestions from code review

Co-authored-by: Ivan <98037481+IvanZosimov@users.noreply.github.com>

* test: fix some failed unit tests

- fix `restoreCache()` test for 9703c8
- update installer script

* build: rebuild dist

* Update unit-tests
- Additional unit test were added to setup-dotnet.test.ts

* Update unit tests for unix systems

* Format and lint unit tests

* fix: avoid use '/' on `path.join`

* fix: rebuild dist

* fix: apply suggestions from code review

Co-authored-by: Ivan <98037481+IvanZosimov@users.noreply.github.com>

* build: add `DisableImplicitNuGetFallbackFolder` option

also add guide on README

* docs: highlight warnings and notes

* docs: update note about handling NU1403

---------

Co-authored-by: Ivan <98037481+IvanZosimov@users.noreply.github.com>
Co-authored-by: IvanZosimov <ivanzosimov@github.com>
2023-05-29 12:43:18 +02:00
Marko Zivic
916351aac9 Merge pull request #430 from akv-platform/remove-implicit-dependencies
Remove implicit dependencies
2023-05-26 08:30:48 +02:00
Nikolai Laevskii
1ad2e312fa Add missing dependency 2023-05-25 13:40:06 +02:00
Nikolai Laevskii
e3f84b8f7a Install eslint-plugin-node 2023-05-25 13:38:43 +02:00
84 changed files with 198051 additions and 24160 deletions

View File

@@ -17,7 +17,7 @@ jobs:
strategy:
fail-fast: false
matrix:
operating-system: [ubuntu-latest, windows-latest, macOS-latest]
operating-system: [ubuntu-22.04, windows-latest, macOS-latest]
steps:
- name: Checkout
uses: actions/checkout@v3
@@ -40,7 +40,7 @@ jobs:
strategy:
fail-fast: false
matrix:
operating-system: [ubuntu-latest, windows-latest, macOS-latest]
operating-system: [ubuntu-22.04, windows-latest, macos-13]
steps:
- name: Checkout
uses: actions/checkout@v3
@@ -69,7 +69,7 @@ jobs:
strategy:
fail-fast: false
matrix:
operating-system: [ubuntu-latest, windows-latest, macOS-latest]
operating-system: [ubuntu-22.04, windows-latest, macOS-latest]
steps:
- name: Checkout
uses: actions/checkout@v3
@@ -94,7 +94,7 @@ jobs:
strategy:
fail-fast: false
matrix:
operating-system: [ubuntu-latest, windows-latest, macOS-latest]
operating-system: [ubuntu-22.04, windows-latest, macos-13]
steps:
- name: Checkout
uses: actions/checkout@v3
@@ -114,7 +114,7 @@ jobs:
strategy:
fail-fast: false
matrix:
operating-system: [ubuntu-latest, windows-latest, macOS-latest]
operating-system: [ubuntu-22.04, windows-latest, macOS-latest]
steps:
- name: Checkout
uses: actions/checkout@v3
@@ -138,7 +138,7 @@ jobs:
strategy:
fail-fast: false
matrix:
operating-system: [ubuntu-latest, windows-latest, macOS-latest]
operating-system: [ubuntu-22.04, windows-latest, macOS-latest]
steps:
- name: Checkout
uses: actions/checkout@v3
@@ -159,7 +159,7 @@ jobs:
strategy:
fail-fast: false
matrix:
operating-system: [ubuntu-latest, windows-latest, macOS-latest]
operating-system: [ubuntu-22.04, windows-latest, macOS-latest]
steps:
- name: Checkout
uses: actions/checkout@v3
@@ -183,7 +183,7 @@ jobs:
strategy:
fail-fast: false
matrix:
operating-system: [ubuntu-latest, windows-latest, macOS-latest]
operating-system: [ubuntu-22.04, windows-latest, macOS-latest]
steps:
- name: Checkout
uses: actions/checkout@v3
@@ -209,7 +209,7 @@ jobs:
strategy:
fail-fast: false
matrix:
operating-system: [ubuntu-latest, windows-latest, macOS-latest]
operating-system: [ubuntu-22.04, windows-latest, macOS-latest]
steps:
- name: Checkout
uses: actions/checkout@v3
@@ -234,7 +234,7 @@ jobs:
strategy:
fail-fast: false
matrix:
operating-system: [ubuntu-latest, windows-latest, macOS-latest]
operating-system: [ubuntu-22.04, windows-latest, macOS-latest]
steps:
- name: Checkout
uses: actions/checkout@v3
@@ -251,12 +251,72 @@ jobs:
shell: pwsh
run: __tests__/verify-dotnet.ps1 -Patterns "^7\.0\.\d+-"
test-setup-with-cache:
runs-on: ${{ matrix.operating-system }}
strategy:
fail-fast: false
matrix:
operating-system: [ubuntu-22.04, windows-latest, macos-latest]
env:
NUGET_PACKAGES: ${{ github.workspace }}/.nuget/packages
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Clear toolcache
shell: pwsh
run: __tests__/clear-toolcache.ps1 ${{ runner.os }}
- name: Copy NuGet lock file to root
shell: bash
run: cp ./__tests__/e2e-test-csproj/packages.lock.json ./packages.lock.json
- name: Setup .NET Core 3.1
id: setup-dotnet
uses: ./
with:
dotnet-version: 3.1
cache: true
- name: Verify Cache
if: steps.setup-dotnet.outputs.cache-hit == 'true'
shell: bash
run: if [[ -e ${NUGET_PACKAGES} ]]; then exit 0; else exit 1; fi
- name: Verify dotnet
shell: pwsh
run: __tests__/verify-dotnet.ps1 -Patterns "^3.1"
test-setup-with-cache-dependency-path:
runs-on: ${{ matrix.operating-system }}
strategy:
fail-fast: false
matrix:
operating-system: [ubuntu-22.04, windows-latest, macos-latest]
env:
NUGET_PACKAGES: ${{ github.workspace }}/.nuget/packages
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Clear toolcache
shell: pwsh
run: __tests__/clear-toolcache.ps1 ${{ runner.os }}
- name: Setup .NET Core 3.1
id: setup-dotnet
uses: ./
with:
dotnet-version: 3.1
cache: true
cache-dependency-path: './__tests__/e2e-test-csproj/packages.lock.json'
- name: Verify Cache
if: steps.setup-dotnet.outputs.cache-hit == 'true'
shell: bash
run: if [[ -e ${NUGET_PACKAGES} ]]; then exit 0; else exit 1; fi
- name: Verify dotnet
shell: pwsh
run: __tests__/verify-dotnet.ps1 -Patterns "^3.1"
test-dotnet-version-output-during-single-version-installation:
runs-on: ${{ matrix.operating-system }}
strategy:
fail-fast: false
matrix:
operating-system: [ubuntu-latest, windows-latest, macOS-latest]
operating-system: [ubuntu-22.04, windows-latest, macOS-latest]
steps:
- name: Checkout
uses: actions/checkout@v3
@@ -282,7 +342,7 @@ jobs:
strategy:
fail-fast: false
matrix:
operating-system: [ubuntu-latest, windows-latest, macOS-latest]
operating-system: [ubuntu-22.04, windows-latest, macOS-latest]
steps:
- name: Checkout
uses: actions/checkout@v3
@@ -306,7 +366,7 @@ jobs:
if (-not ($version -eq '${{steps.step2.outputs.dotnet-version}}')) { throw "Unexpected output value" }
test-proxy:
runs-on: ubuntu-latest
runs-on: ubuntu-22.04
container:
image: ubuntu:latest
options: --dns 127.0.0.1
@@ -346,10 +406,10 @@ jobs:
__tests__/verify-dotnet.ps1 -Patterns "^6.0" -CheckNugetConfig
test-bypass-proxy:
runs-on: ubuntu-latest
runs-on: ubuntu-22.04
env:
https_proxy: http://no-such-proxy:3128
no_proxy: github.com,dotnetcli.blob.core.windows.net,download.visualstudio.microsoft.com,api.nuget.org,dotnetcli.azureedge.net
no_proxy: github.com,download.visualstudio.microsoft.com,api.nuget.org,builds.dotnet.microsoft.com,ci.dot.net
steps:
- name: Checkout
uses: actions/checkout@v3

View File

@@ -0,0 +1,20 @@
name: 'Publish Immutable Action Version'
on:
release:
types: [published]
jobs:
publish:
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write
packages: write
steps:
- name: Checking out
uses: actions/checkout@v4
- name: Publish
id: publish
uses: actions/publish-immutable-action@v0.0.4

View File

@@ -0,0 +1,20 @@
name: 'Publish Immutable Action Version'
on:
release:
types: [published]
jobs:
publish:
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write
packages: write
steps:
- name: Checking out
uses: actions/checkout@v4
- name: Publish
id: publish
uses: actions/publish-immutable-action@v0.0.4

View File

@@ -17,7 +17,7 @@ jobs:
strategy:
fail-fast: false
matrix:
operating-system: [ubuntu-latest, windows-latest, macOS-latest]
operating-system: [ubuntu-22.04, windows-latest, macOS-latest]
dotnet-version: ['2.1', '2.2', '3.0', '3.1', '5.0', '6.0', '7.0', '8.0']
steps:
- name: Checkout

View File

@@ -3,6 +3,7 @@ sources:
allowed:
- apache-2.0
- 0bsd
- bsd-2-clause
- bsd-3-clause
- isc
@@ -11,4 +12,5 @@ allowed:
- unlicense
reviewed:
npm:
npm:
- sax # ISC + MIT

BIN
.licenses/npm/@actions/cache.dep.yml generated Normal file

Binary file not shown.

Binary file not shown.

BIN
.licenses/npm/@actions/glob-0.1.2.dep.yml generated Normal file

Binary file not shown.

BIN
.licenses/npm/@actions/glob-0.3.0.dep.yml generated Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
.licenses/npm/@azure/core-auth.dep.yml generated Normal file

Binary file not shown.

BIN
.licenses/npm/@azure/core-http.dep.yml generated Normal file

Binary file not shown.

BIN
.licenses/npm/@azure/core-lro.dep.yml generated Normal file

Binary file not shown.

BIN
.licenses/npm/@azure/core-paging.dep.yml generated Normal file

Binary file not shown.

BIN
.licenses/npm/@azure/core-tracing.dep.yml generated Normal file

Binary file not shown.

BIN
.licenses/npm/@azure/core-util.dep.yml generated Normal file

Binary file not shown.

BIN
.licenses/npm/@azure/logger.dep.yml generated Normal file

Binary file not shown.

BIN
.licenses/npm/@azure/ms-rest-js.dep.yml generated Normal file

Binary file not shown.

BIN
.licenses/npm/@azure/storage-blob.dep.yml generated Normal file

Binary file not shown.

BIN
.licenses/npm/@fastify/busboy.dep.yml generated Normal file

Binary file not shown.

BIN
.licenses/npm/@opentelemetry/api.dep.yml generated Normal file

Binary file not shown.

Binary file not shown.

BIN
.licenses/npm/@protobuf-ts/plugin.dep.yml generated Normal file

Binary file not shown.

BIN
.licenses/npm/@protobuf-ts/protoc.dep.yml generated Normal file

Binary file not shown.

Binary file not shown.

BIN
.licenses/npm/@protobuf-ts/runtime.dep.yml generated Normal file

Binary file not shown.

BIN
.licenses/npm/@types/node-fetch.dep.yml generated Normal file

Binary file not shown.

BIN
.licenses/npm/@types/node.dep.yml generated Normal file

Binary file not shown.

BIN
.licenses/npm/@types/tunnel.dep.yml generated Normal file

Binary file not shown.

BIN
.licenses/npm/abort-controller.dep.yml generated Normal file

Binary file not shown.

BIN
.licenses/npm/asynckit.dep.yml generated Normal file

Binary file not shown.

BIN
.licenses/npm/balanced-match.dep.yml generated Normal file

Binary file not shown.

BIN
.licenses/npm/brace-expansion.dep.yml generated Normal file

Binary file not shown.

BIN
.licenses/npm/combined-stream.dep.yml generated Normal file

Binary file not shown.

BIN
.licenses/npm/concat-map.dep.yml generated Normal file

Binary file not shown.

BIN
.licenses/npm/delayed-stream.dep.yml generated Normal file

Binary file not shown.

BIN
.licenses/npm/event-target-shim.dep.yml generated Normal file

Binary file not shown.

BIN
.licenses/npm/events.dep.yml generated Normal file

Binary file not shown.

BIN
.licenses/npm/form-data-2.5.1.dep.yml generated Normal file

Binary file not shown.

BIN
.licenses/npm/form-data-3.0.1.dep.yml generated Normal file

Binary file not shown.

BIN
.licenses/npm/form-data-4.0.0.dep.yml generated Normal file

Binary file not shown.

BIN
.licenses/npm/ip-regex.dep.yml generated Normal file

Binary file not shown.

BIN
.licenses/npm/mime-db.dep.yml generated Normal file

Binary file not shown.

BIN
.licenses/npm/mime-types.dep.yml generated Normal file

Binary file not shown.

BIN
.licenses/npm/minimatch.dep.yml generated Normal file

Binary file not shown.

BIN
.licenses/npm/process.dep.yml generated Normal file

Binary file not shown.

BIN
.licenses/npm/psl.dep.yml generated Normal file

Binary file not shown.

BIN
.licenses/npm/punycode.dep.yml generated Normal file

Binary file not shown.

BIN
.licenses/npm/sax.dep.yml generated Normal file

Binary file not shown.

Binary file not shown.

BIN
.licenses/npm/tough-cookie.dep.yml generated Normal file

Binary file not shown.

BIN
.licenses/npm/tslib-1.14.1.dep.yml generated Normal file

Binary file not shown.

BIN
.licenses/npm/tslib-2.5.0.dep.yml generated Normal file

Binary file not shown.

BIN
.licenses/npm/typescript.dep.yml generated Normal file

Binary file not shown.

BIN
.licenses/npm/undici.dep.yml generated Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
.licenses/npm/xml2js.dep.yml generated Normal file

Binary file not shown.

BIN
.licenses/npm/xmlbuilder.dep.yml generated Normal file

Binary file not shown.

View File

@@ -82,6 +82,63 @@ steps:
working-directory: csharp
```
## 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.
The action searches for [NuGet Lock files](https://learn.microsoft.com/nuget/consume-packages/package-references-in-project-files#locking-dependencies) (`packages.lock.json`) in the repository root, calculates their hash and uses it as a part of the cache key. If lock file does not exist, this action throws error. Use `cache-dependency-path` for cases when multiple dependency files are used, or they are located in different subdirectories.
> **Warning**: Caching NuGet packages is available since .NET SDK 2.1.500 and 2.2.100 as the NuGet lock file [is available](https://learn.microsoft.com/nuget/consume-packages/package-references-in-project-files#locking-dependencies) only for NuGet 4.9 and above.
```yaml
steps:
- uses: actions/checkout@v3
- uses: actions/setup-dotnet@v3
with:
dotnet-version: 6.x
cache: true
- run: dotnet restore --locked-mode
```
> **Note**: This action will only restore `global-packages` folder, so you will probably get the [NU1403](https://learn.microsoft.com/nuget/reference/errors-and-warnings/nu1403) error when running `dotnet restore`.
> To avoid this, you can use [`DisableImplicitNuGetFallbackFolder`](https://github.com/dotnet/reproducible-builds/blob/abfe986832aa28597d3340b92469d1a702013d23/Documentation/Reproducible-MSBuild/Techniques/DisableImplicitNuGetFallbackFolder.md) option.
```xml
<PropertyGroup>
<DisableImplicitNuGetFallbackFolder>true</DisableImplicitNuGetFallbackFolder>
</PropertyGroup>
```
### Reduce caching size
> **Note**: Use [`NUGET_PACKAGES`](https://learn.microsoft.com/nuget/reference/cli-reference/cli-ref-environment-variables) environment variable if available. Some action runners already has huge libraries. (ex. Xamarin)
```yaml
env:
NUGET_PACKAGES: ${{ github.workspace }}/.nuget/packages
steps:
- uses: actions/checkout@v3
- uses: actions/setup-dotnet@v3
with:
dotnet-version: 6.x
cache: true
- run: dotnet restore --locked-mode
```
### Caching NuGet packages in monorepos
```yaml
env:
NUGET_PACKAGES: ${{ github.workspace }}/.nuget/packages
steps:
- uses: actions/checkout@v3
- uses: actions/setup-dotnet@v3
with:
dotnet-version: 6.x
cache: true
cache-dependency-path: subdir/packages.lock.json
- run: dotnet restore --locked-mode
```
## Matrix Testing
Using `setup-dotnet` it's possible to use [matrix syntax](https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstrategymatrix) to install several versions of .NET SDK:
```yml
@@ -214,6 +271,9 @@ When the `dotnet-version` input is used along with the `global-json-file` input,
- run: echo '${{ steps.stepid.outputs.dotnet-version }}' # outputs 2.2.207
```
### `cache-hit`
A boolean value to indicate an exact match was found for the cache key (follows [actions/cache](https://github.com/actions/cache#outputs))
## Environment variables
Some environment variables may be necessary for your particular case or to improve logging. Some examples are listed below, but the full list with complete details can be found here: https://docs.microsoft.com/en-us/dotnet/core/tools/dotnet-environment-variables
@@ -224,25 +284,28 @@ Some environment variables may be necessary for your particular case or to impro
| 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_MULTILEVEL_LOOKUP |Configures whether the global install location is used as a fall-back|*true*|
| NUGET_PACKAGES |Configures a path to the [NuGet `global-packages` folder](https://learn.microsoft.com/nuget/consume-packages/managing-the-global-packages-and-cache-folders)|*default value for each OS* |
The default value of the `DOTNET_INSTALL_DIR` environment variable depends on the operation system which is used on a runner:
| **Operation system** | **Default value** |
| ----------- | ----------- |
| **Windows** | `C:\Program Files\dotnet` |
| **Ubuntu** | `/usr/share/dotnet` |
| **macOS** | `/Users/runner/.dotnet` |
The default values of the `DOTNET_INSTALL_DIR` and `NUGET_PACKAGES` environment variables depend on the operation system which is used on a runner:
| **Operation system** | `DOTNET_INSTALL_DIR` | `NUGET_PACKAGES` |
| ----------- | ----------- | ----------- |
| **Windows** | `C:\Program Files\dotnet` | `%userprofile%\.nuget\packages` |
| **Ubuntu** | `/usr/share/dotnet` | `~/.nuget/packages` |
| **macOS** | `/Users/runner/.dotnet` | `~/.nuget/packages` |
**Example usage**:
**Example usage of environment variable**:
```yml
build:
runs-on: ubuntu-latest
env:
DOTNET_INSTALL_DIR: "path/to/directory"
NUGET_PACKAGES: ${{ github.workspace }}/.nuget/packages
steps:
- uses: actions/checkout@main
- uses: actions/setup-dotnet@v3
with:
dotnet-version: '3.1.x'
cache: true
```
## License

View File

@@ -0,0 +1,101 @@
import {readdir} from 'node:fs/promises';
import * as cache from '@actions/cache';
import * as core from '@actions/core';
import * as glob from '@actions/glob';
import {restoreCache} from '../src/cache-restore';
import {getNuGetFolderPath} from '../src/cache-utils';
import {lockFilePatterns} from '../src/constants';
jest.mock('node:fs/promises');
jest.mock('@actions/cache');
jest.mock('@actions/core');
jest.mock('@actions/glob');
jest.mock('../src/cache-utils');
describe('cache-restore tests', () => {
describe.each(lockFilePatterns)('restoreCache("%s")', lockFilePattern => {
/** Store original process.env.GITHUB_WORKSPACE */
let githubWorkspace: string | undefined;
beforeAll(() => {
githubWorkspace = process.env.GITHUB_WORKSPACE;
jest.mocked(getNuGetFolderPath).mockResolvedValue({
'global-packages': 'global-packages',
'http-cache': 'http-cache',
temp: 'temp',
'plugins-cache': 'plugins-cache'
});
});
beforeEach(() => {
process.env.GITHUB_WORKSPACE = './';
jest.mocked(glob.hashFiles).mockClear();
jest.mocked(core.saveState).mockClear();
jest.mocked(core.setOutput).mockClear();
jest.mocked(cache.restoreCache).mockClear();
});
afterEach(() => (process.env.GITHUB_WORKSPACE = githubWorkspace));
it('throws error when lock file is not found', async () => {
jest.mocked(glob.hashFiles).mockResolvedValue('');
await expect(restoreCache(lockFilePattern)).rejects.toThrow();
expect(jest.mocked(core.saveState)).not.toHaveBeenCalled();
expect(jest.mocked(core.setOutput)).not.toHaveBeenCalled();
expect(jest.mocked(cache.restoreCache)).not.toHaveBeenCalled();
});
it('does not call core.saveState("CACHE_RESULT") when cache.restoreCache() returns falsy', async () => {
jest.mocked(glob.hashFiles).mockResolvedValue('hash');
jest.mocked(cache.restoreCache).mockResolvedValue(undefined);
await restoreCache(lockFilePattern);
const expectedKey = `dotnet-cache-${process.env.RUNNER_OS}-hash`;
expect(jest.mocked(core.saveState)).toHaveBeenCalledWith(
'CACHE_KEY',
expectedKey
);
expect(jest.mocked(core.saveState)).not.toHaveBeenCalledWith(
'CACHE_RESULT',
expectedKey
);
expect(jest.mocked(core.setOutput)).toHaveBeenCalledWith(
'cache-hit',
false
);
});
it('calls core.saveState("CACHE_RESULT") when cache.restoreCache() returns key', async () => {
const expectedKey = `dotnet-cache-${process.env.RUNNER_OS}-hash`;
jest.mocked(glob.hashFiles).mockResolvedValue('hash');
jest.mocked(cache.restoreCache).mockResolvedValue(expectedKey);
await restoreCache(lockFilePattern);
expect(jest.mocked(core.saveState)).toHaveBeenCalledWith(
'CACHE_KEY',
expectedKey
);
expect(jest.mocked(core.saveState)).toHaveBeenCalledWith(
'CACHE_RESULT',
expectedKey
);
expect(jest.mocked(core.setOutput)).toHaveBeenCalledWith(
'cache-hit',
true
);
});
it('calls glob.hashFiles("/packages.lock.json") if cacheDependencyPath is falsy', async () => {
const expectedKey = `dotnet-cache-${process.env.RUNNER_OS}-hash`;
jest.mocked(glob.hashFiles).mockResolvedValue('hash');
jest.mocked(cache.restoreCache).mockResolvedValue(expectedKey);
jest.mocked(readdir).mockResolvedValue([lockFilePattern] as any);
await restoreCache('');
expect(jest.mocked(glob.hashFiles)).not.toHaveBeenCalledWith('');
expect(jest.mocked(glob.hashFiles)).toHaveBeenCalledWith(lockFilePattern);
});
});
});

View File

@@ -0,0 +1,87 @@
import * as cache from '@actions/cache';
import * as core from '@actions/core';
import fs from 'node:fs';
import {run} from '../src/cache-save';
import {getNuGetFolderPath} from '../src/cache-utils';
jest.mock('@actions/cache');
jest.mock('@actions/core');
jest.mock('node:fs');
jest.mock('../src/cache-utils');
describe('cache-save tests', () => {
beforeAll(() => {
jest.mocked(getNuGetFolderPath).mockResolvedValue({
'global-packages': 'global-packages',
'http-cache': 'http-cache',
temp: 'temp',
'plugins-cache': 'plugins-cache'
});
});
beforeEach(() => {
jest.mocked(core.setFailed).mockClear();
jest.mocked(core.getState).mockClear();
jest.mocked(core.setOutput).mockClear();
jest.mocked(cache.saveCache).mockClear();
jest.mocked(fs.existsSync).mockClear();
});
it('does not save cache when inputs:cache === false', async () => {
jest.mocked(core.getBooleanInput).mockReturnValue(false);
await run();
expect(jest.mocked(core.setFailed)).not.toHaveBeenCalled();
expect(jest.mocked(core.getState)).not.toHaveBeenCalled();
expect(jest.mocked(fs.existsSync)).not.toHaveBeenCalled();
expect(jest.mocked(cache.saveCache)).not.toHaveBeenCalled();
});
it('does not save cache when core.getState("CACHE_KEY") returns ""', async () => {
jest.mocked(core.getBooleanInput).mockReturnValue(true);
jest.mocked(core.getState).mockReturnValue('');
await run();
expect(jest.mocked(core.setFailed)).not.toHaveBeenCalled();
expect(jest.mocked(core.getState)).toHaveBeenCalledTimes(2);
expect(jest.mocked(fs.existsSync)).not.toHaveBeenCalled();
expect(jest.mocked(cache.saveCache)).not.toHaveBeenCalled();
});
it('throws Error when cachePath not exists', async () => {
jest.mocked(core.getBooleanInput).mockReturnValue(true);
jest.mocked(core.getState).mockReturnValue('cache-key');
jest.mocked(fs.existsSync).mockReturnValue(false);
await run();
expect(jest.mocked(core.setFailed)).toHaveBeenCalled();
expect(jest.mocked(core.getState)).toHaveBeenCalledTimes(2);
expect(jest.mocked(cache.saveCache)).not.toHaveBeenCalled();
});
it('does not save cache when state.CACHE_KEY === state.CACHE_RESULT', async () => {
jest.mocked(core.getBooleanInput).mockReturnValue(true);
jest.mocked(core.getState).mockReturnValue('cache-key');
jest.mocked(fs.existsSync).mockReturnValue(true);
await run();
expect(jest.mocked(core.setFailed)).not.toHaveBeenCalled();
expect(jest.mocked(core.getState)).toHaveBeenCalledTimes(2);
expect(jest.mocked(cache.saveCache)).not.toHaveBeenCalled();
});
it('saves cache when state.CACHE_KEY !== state.CACHE_RESULT', async () => {
jest.mocked(core.getBooleanInput).mockReturnValue(true);
jest.mocked(core.getState).mockImplementation(s => s);
jest.mocked(fs.existsSync).mockReturnValue(true);
await run();
expect(jest.mocked(core.setFailed)).not.toHaveBeenCalled();
expect(jest.mocked(core.getState)).toHaveBeenCalledTimes(2);
expect(jest.mocked(cache.saveCache)).toHaveBeenCalled();
});
});

View File

@@ -0,0 +1,122 @@
import * as cache from '@actions/cache';
import * as exec from '@actions/exec';
import {getNuGetFolderPath, isCacheFeatureAvailable} from '../src/cache-utils';
jest.mock('@actions/cache');
jest.mock('@actions/core');
jest.mock('@actions/exec');
describe('cache-utils tests', () => {
describe('getNuGetFolderPath()', () => {
it.each([
[
`
http-cache: /home/codespace/.local/share/NuGet/v3-cache
global-packages: /var/nuget
temp: /tmp/NuGetScratch
plugins-cache: /home/codespace/.local/share/NuGet/plugins-cache
`,
{
'http-cache': '/home/codespace/.local/share/NuGet/v3-cache',
'global-packages': '/var/nuget',
temp: '/tmp/NuGetScratch',
'plugins-cache': '/home/codespace/.local/share/NuGet/plugins-cache'
}
],
[
`
http-cache: /home/codespace/.local/share/NuGet/v3-cache
global-packages: /var/nuget
temp: /tmp/NuGetScratch
plugins-cache: /home/codespace/.local/share/NuGet/plugins-cache
`,
{
'http-cache': '/home/codespace/.local/share/NuGet/v3-cache',
'global-packages': '/var/nuget',
temp: '/tmp/NuGetScratch',
'plugins-cache': '/home/codespace/.local/share/NuGet/plugins-cache'
}
],
[
`
http-cache: C:\\Users\\user\\AppData\\Local\\NuGet\\v3-cache
global-packages: C:\\Users\\user\\.nuget\\packages\\
temp: C:\\Users\\user\\AppData\\Local\\Temp\\NuGetScratch
plugins-cache: C:\\Users\\user\\AppData\\Local\\NuGet\\plugins-cache
`,
{
'http-cache': 'C:\\Users\\user\\AppData\\Local\\NuGet\\v3-cache',
'global-packages': 'C:\\Users\\user\\.nuget\\packages\\',
temp: 'C:\\Users\\user\\AppData\\Local\\Temp\\NuGetScratch',
'plugins-cache':
'C:\\Users\\user\\AppData\\Local\\NuGet\\plugins-cache'
}
],
[
`
http-cache: C:\\Users\\user\\AppData\\Local\\NuGet\\v3-cache
global-packages: C:\\Users\\user\\.nuget\\packages\\
temp: C:\\Users\\user\\AppData\\Local\\Temp\\NuGetScratch
plugins-cache: C:\\Users\\user\\AppData\\Local\\NuGet\\plugins-cache
`,
{
'http-cache': 'C:\\Users\\user\\AppData\\Local\\NuGet\\v3-cache',
'global-packages': 'C:\\Users\\user\\.nuget\\packages\\',
temp: 'C:\\Users\\user\\AppData\\Local\\Temp\\NuGetScratch',
'plugins-cache':
'C:\\Users\\user\\AppData\\Local\\NuGet\\plugins-cache'
}
]
])('(stdout: "%s") returns %p', async (stdout, expected) => {
jest
.mocked(exec.getExecOutput)
.mockResolvedValue({stdout, stderr: '', exitCode: 0});
const pathes = await getNuGetFolderPath();
expect(pathes).toStrictEqual(expected);
});
it.each([
`
error: An invalid local resource name was provided. Provide one of the following values: http-cache, temp, global-packages, all.
Usage: dotnet nuget locals [arguments] [options]
Arguments:
Cache Location(s) Specifies the cache location(s) to list or clear.
<all | http-cache | global-packages | temp>
Options:
-h|--help Show help information
--force-english-output Forces the application to run using an invariant, English-based culture.
-c|--clear Clear the selected local resources or cache location(s).
-l|--list List the selected local resources or cache location(s).
`,
'bash: dotnet: command not found',
''
])('(stderr: "%s", exitCode: 1) throws Error', async stderr => {
jest
.mocked(exec.getExecOutput)
.mockResolvedValue({stdout: '', stderr, exitCode: 1});
await expect(getNuGetFolderPath()).rejects.toThrow();
});
});
describe.each(['', 'https://github.com/', 'https://example.com/'])(
'isCacheFeatureAvailable()',
url => {
// Save & Restore env
let serverUrlEnv: string | undefined;
beforeAll(() => (serverUrlEnv = process.env['GITHUB_SERVER_URL']));
beforeEach(() => (process.env['GITHUB_SERVER_URL'] = url));
afterEach(() => (process.env['GITHUB_SERVER_URL'] = serverUrlEnv));
it('returns true when cache.isFeatureAvailable() === true', () => {
jest.mocked(cache.isFeatureAvailable).mockReturnValue(true);
expect(isCacheFeatureAvailable()).toBe(true);
});
it('returns false when cache.isFeatureAvailable() === false', () => {
jest.mocked(cache.isFeatureAvailable).mockReturnValue(false);
expect(isCacheFeatureAvailable()).toBe(false);
});
}
);
});

File diff suppressed because it is too large Load Diff

View File

@@ -3,6 +3,7 @@
<PropertyGroup>
<TargetFramework>$(TEST_TARGET_FRAMEWORK)</TargetFramework>
<IsPackable>false</IsPackable>
<DisableImplicitNuGetFallbackFolder>true</DisableImplicitNuGetFallbackFolder>
</PropertyGroup>
<ItemGroup>

View File

@@ -1,5 +1,7 @@
import each from 'jest-each';
import semver from 'semver';
import fs from 'fs';
import fspromises from 'fs/promises';
import * as exec from '@actions/exec';
import * as core from '@actions/core';
import * as io from '@actions/io';
@@ -21,14 +23,25 @@ describe('installer tests', () => {
const warningSpy = jest.spyOn(core, 'warning');
const whichSpy = jest.spyOn(io, 'which');
const maxSatisfyingSpy = jest.spyOn(semver, 'maxSatisfying');
const chmodSyncSpy = jest.spyOn(fs, 'chmodSync');
const readdirSpy = jest.spyOn(fspromises, 'readdir');
describe('installDotnet() tests', () => {
whichSpy.mockImplementation(() => Promise.resolve('PathToShell'));
beforeAll(() => {
whichSpy.mockImplementation(() => Promise.resolve('PathToShell'));
chmodSyncSpy.mockImplementation(() => {});
readdirSpy.mockImplementation(() => Promise.resolve([]));
});
afterAll(() => {
jest.resetAllMocks();
});
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,
@@ -36,6 +49,7 @@ describe('installer tests', () => {
stderr: errorMessage
});
});
const dotnetInstaller = new installer.DotnetCoreInstaller(
inputVersion,
inputQuality

View File

@@ -5,12 +5,15 @@ import * as auth from '../src/authutil';
import * as setup from '../src/setup-dotnet';
import {DotnetCoreInstaller} from '../src/installer';
import * as cacheUtils from '../src/cache-utils';
import * as cacheRestore from '../src/cache-restore';
describe('setup-dotnet tests', () => {
const inputs = {} as any;
const getInputSpy = jest.spyOn(core, 'getInput');
const getMultilineInputSpy = jest.spyOn(core, 'getMultilineInput');
const getBooleanInputSpy = jest.spyOn(core, 'getBooleanInput');
const setFailedSpy = jest.spyOn(core, 'setFailed');
const warningSpy = jest.spyOn(core, 'warning');
const debugSpy = jest.spyOn(core, 'debug');
@@ -26,13 +29,18 @@ describe('setup-dotnet tests', () => {
'installDotnet'
);
const addToPathSpy = jest.spyOn(DotnetCoreInstaller, 'addToPath');
const isCacheFeatureAvailableSpy = jest.spyOn(
cacheUtils,
'isCacheFeatureAvailable'
);
const restoreCacheSpy = jest.spyOn(cacheRestore, 'restoreCache');
const configAuthenticationSpy = jest.spyOn(auth, 'configAuthentication');
describe('run() tests', () => {
beforeEach(() => {
getMultilineInputSpy.mockImplementation(input => inputs[input as string]);
getInputSpy.mockImplementation(input => inputs[input as string]);
getBooleanInputSpy.mockImplementation(input => inputs[input as string]);
});
afterEach(() => {
@@ -169,5 +177,54 @@ describe('setup-dotnet tests', () => {
expect(infoSpy).toHaveBeenCalledWith(warningMessage);
expect(setOutputSpy).not.toHaveBeenCalled();
});
it(`should get 'cache-dependency-path' and call restoreCache() if input cache is set to true and cache feature is available`, async () => {
inputs['dotnet-version'] = ['6.0.300'];
inputs['dotnet-quality'] = '';
inputs['cache'] = true;
inputs['cache-dependency-path'] = 'fictitious.package.lock.json';
installDotnetSpy.mockImplementation(() => Promise.resolve(''));
addToPathSpy.mockImplementation(() => {});
isCacheFeatureAvailableSpy.mockImplementation(() => true);
restoreCacheSpy.mockImplementation(() => Promise.resolve());
await setup.run();
expect(isCacheFeatureAvailableSpy).toHaveBeenCalledTimes(1);
expect(restoreCacheSpy).toHaveBeenCalledWith(
inputs['cache-dependency-path']
);
});
it(`shouldn't call restoreCache() if input cache isn't set to true`, async () => {
inputs['dotnet-version'] = ['6.0.300'];
inputs['dotnet-quality'] = '';
inputs['cache'] = false;
installDotnetSpy.mockImplementation(() => Promise.resolve(''));
addToPathSpy.mockImplementation(() => {});
isCacheFeatureAvailableSpy.mockImplementation(() => true);
restoreCacheSpy.mockImplementation(() => Promise.resolve());
await setup.run();
expect(restoreCacheSpy).not.toHaveBeenCalled();
});
it(`shouldn't call restoreCache() if cache feature isn't available`, async () => {
inputs['dotnet-version'] = ['6.0.300'];
inputs['dotnet-quality'] = '';
inputs['cache'] = true;
installDotnetSpy.mockImplementation(() => Promise.resolve(''));
addToPathSpy.mockImplementation(() => {});
isCacheFeatureAvailableSpy.mockImplementation(() => false);
restoreCacheSpy.mockImplementation(() => Promise.resolve());
await setup.run();
expect(restoreCacheSpy).not.toHaveBeenCalled();
});
});
});

View File

@@ -114,4 +114,4 @@ foreach ($version in $Versions)
Remove-Item ./global.json
}
Set-Location $workingDir
Set-Location $workingDir

View File

@@ -17,9 +17,20 @@ inputs:
description: 'Optional OWNER for using packages from GitHub Package Registry organizations/users other than the current repository''s owner. Only used if a GPR URL is also provided in source-url'
config-file:
description: 'Optional NuGet.config location, if your NuGet.config isn''t located in the root of the repo.'
cache:
description: 'Optional input to enable caching of the NuGet global-packages folder'
required: false
default: false
cache-dependency-path:
description: 'Used to specify the path to a dependency file: packages.lock.json. Supports wildcards or a list of file names for caching multiple dependencies.'
required: false
outputs:
cache-hit:
description: 'A boolean value to indicate if a cache was hit.'
dotnet-version:
description: 'Contains the installed by action .NET SDK version for reuse.'
runs:
using: 'node16'
main: 'dist/index.js'
main: 'dist/setup/index.js'
post: 'dist/cache-save/index.js'
post-if: success()

89370
dist/cache-save/index.js vendored Normal file

File diff suppressed because one or more lines are too long

21305
dist/index.js vendored

File diff suppressed because one or more lines are too long

102276
dist/setup/index.js vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

View File

@@ -298,11 +298,20 @@ get_machine_architecture() {
if command -v uname > /dev/null; then
CPUName=$(uname -m)
case $CPUName in
armv1*|armv2*|armv3*|armv4*|armv5*|armv6*)
echo "armv6-or-below"
return 0
;;
armv*l)
echo "arm"
return 0
;;
aarch64|arm64)
if [ "$(getconf LONG_BIT)" -lt 64 ]; then
# This is 32-bit OS running on 64-bit CPU (for example Raspberry Pi OS)
echo "arm"
return 0
fi
echo "arm64"
return 0
;;
@@ -310,6 +319,22 @@ get_machine_architecture() {
echo "s390x"
return 0
;;
ppc64le)
echo "ppc64le"
return 0
;;
loongarch64)
echo "loongarch64"
return 0
;;
riscv64)
echo "riscv64"
return 0
;;
powerpc|ppc)
echo "ppc"
return 0
;;
esac
fi
@@ -326,7 +351,13 @@ get_normalized_architecture_from_architecture() {
local architecture="$(to_lowercase "$1")"
if [[ $architecture == \<auto\> ]]; then
echo "$(get_machine_architecture)"
machine_architecture="$(get_machine_architecture)"
if [[ "$machine_architecture" == "armv6-or-below" ]]; then
say_err "Architecture \`$machine_architecture\` not supported. If you think this is a bug, report it at https://github.com/dotnet/install-scripts/issues"
return 1
fi
echo $machine_architecture
return 0
fi
@@ -347,6 +378,14 @@ get_normalized_architecture_from_architecture() {
echo "s390x"
return 0
;;
ppc64le)
echo "ppc64le"
return 0
;;
loongarch64)
echo "loongarch64"
return 0
;;
esac
say_err "Architecture \`$architecture\` not supported. If you think this is a bug, report it at https://github.com/dotnet/install-scripts/issues"
@@ -384,11 +423,17 @@ get_normalized_architecture_for_specific_sdk_version() {
# args:
# version or channel - $1
is_arm64_supported() {
#any channel or version that starts with the specified versions
case "$1" in
( "1"* | "2"* | "3"* | "4"* | "5"*)
echo false
return 0
# Extract the major version by splitting on the dot
major_version="${1%%.*}"
# Check if the major version is a valid number and less than 6
case "$major_version" in
[0-9]*)
if [ "$major_version" -lt 6 ]; then
echo false
return 0
fi
;;
esac
echo true
@@ -407,8 +452,13 @@ get_normalized_os() {
echo "$osname"
return 0
;;
macos)
osname='osx'
echo "$osname"
return 0
;;
*)
say_err "'$user_defined_os' is not a supported value for --os option, supported values are: osx, linux, linux-musl, freebsd, rhel.6. If you think this is a bug, report it at https://github.com/dotnet/install-scripts/issues."
say_err "'$user_defined_os' is not a supported value for --os option, supported values are: osx, macos, linux, linux-musl, freebsd, rhel.6. If you think this is a bug, report it at https://github.com/dotnet/install-scripts/issues."
return 1
;;
esac
@@ -538,6 +588,40 @@ is_dotnet_package_installed() {
fi
}
# args:
# downloaded file - $1
# remote_file_size - $2
validate_remote_local_file_sizes()
{
eval $invocation
local downloaded_file="$1"
local remote_file_size="$2"
local file_size=''
if [[ "$OSTYPE" == "linux-gnu"* ]]; then
file_size="$(stat -c '%s' "$downloaded_file")"
elif [[ "$OSTYPE" == "darwin"* ]]; then
# hardcode in order to avoid conflicts with GNU stat
file_size="$(/usr/bin/stat -f '%z' "$downloaded_file")"
fi
if [ -n "$file_size" ]; then
say "Downloaded file size is $file_size bytes."
if [ -n "$remote_file_size" ] && [ -n "$file_size" ]; then
if [ "$remote_file_size" -ne "$file_size" ]; then
say "The remote and local file sizes are not equal. The remote file size is $remote_file_size bytes and the local size is $file_size bytes. The local package may be corrupted."
else
say "The remote and local file sizes are equal."
fi
fi
else
say "Either downloaded or local package size can not be measured. One of them may be corrupted."
fi
}
# args:
# azure_feed - $1
# channel - $2
@@ -872,6 +956,37 @@ get_absolute_path() {
return 0
}
# args:
# override - $1 (boolean, true or false)
get_cp_options() {
eval $invocation
local override="$1"
local override_switch=""
if [ "$override" = false ]; then
override_switch="-n"
# create temporary files to check if 'cp -u' is supported
tmp_dir="$(mktemp -d)"
tmp_file="$tmp_dir/testfile"
tmp_file2="$tmp_dir/testfile2"
touch "$tmp_file"
# use -u instead of -n if it's available
if cp -u "$tmp_file" "$tmp_file2" 2>/dev/null; then
override_switch="-u"
fi
# clean up
rm -f "$tmp_file" "$tmp_file2"
rm -rf "$tmp_dir"
fi
echo "$override_switch"
}
# args:
# input_files - stdin
# root_path - $1
@@ -883,15 +998,7 @@ copy_files_or_dirs_from_list() {
local root_path="$(remove_trailing_slash "$1")"
local out_path="$(remove_trailing_slash "$2")"
local override="$3"
local osname="$(get_current_os_name)"
local override_switch=$(
if [ "$override" = false ]; then
if [ "$osname" = "linux-musl" ]; then
printf -- "-u";
else
printf -- "-n";
fi
fi)
local override_switch="$(get_cp_options "$override")"
cat | uniq | while read -r file_path; do
local path="$(remove_beginning_slash "${file_path#$root_path}")"
@@ -906,14 +1013,39 @@ copy_files_or_dirs_from_list() {
done
}
# args:
# zip_uri - $1
get_remote_file_size() {
local zip_uri="$1"
if machine_has "curl"; then
file_size=$(curl -sI "$zip_uri" | grep -i content-length | awk '{ num = $2 + 0; print num }')
elif machine_has "wget"; then
file_size=$(wget --spider --server-response -O /dev/null "$zip_uri" 2>&1 | grep -i 'Content-Length:' | awk '{ num = $2 + 0; print num }')
else
say "Neither curl nor wget is available on this system."
return
fi
if [ -n "$file_size" ]; then
say "Remote file $zip_uri size is $file_size bytes."
echo "$file_size"
else
say_verbose "Content-Length header was not extracted for $zip_uri."
echo ""
fi
}
# args:
# zip_path - $1
# out_path - $2
# remote_file_size - $3
extract_dotnet_package() {
eval $invocation
local zip_path="$1"
local out_path="$2"
local remote_file_size="$3"
local temp_out_path="$(mktemp -d "$temporary_file_template")"
@@ -923,9 +1055,13 @@ extract_dotnet_package() {
local folders_with_version_regex='^.*/[0-9]+\.[0-9]+[^/]+/'
find "$temp_out_path" -type f | grep -Eo "$folders_with_version_regex" | sort | copy_files_or_dirs_from_list "$temp_out_path" "$out_path" false
find "$temp_out_path" -type f | grep -Ev "$folders_with_version_regex" | copy_files_or_dirs_from_list "$temp_out_path" "$out_path" "$override_non_versioned_files"
validate_remote_local_file_sizes "$zip_path" "$remote_file_size"
rm -rf "$temp_out_path"
rm -f "$zip_path" && say_verbose "Temporary zip file $zip_path was removed"
if [ -z ${keep_zip+x} ]; then
rm -f "$zip_path" && say_verbose "Temporary archive file $zip_path was removed"
fi
if [ "$failed" = true ]; then
say_err "Extraction failed"
@@ -1136,6 +1272,61 @@ downloadwget() {
return 0
}
extract_stem() {
local url="$1"
# extract the protocol
proto="$(echo $1 | grep :// | sed -e's,^\(.*://\).*,\1,g')"
# remove the protocol
url="${1/$proto/}"
# extract the path (if any) - since we know all of our feeds have a first path segment, we can skip the first one. otherwise we'd use -f2- to get the full path
full_path="$(echo $url | grep / | cut -d/ -f2-)"
path="$(echo $full_path | cut -d/ -f2-)"
echo $path
}
check_url_exists() {
eval $invocation
local url="$1"
local code=""
if machine_has "curl"
then
code=$(curl --head -o /dev/null -w "%{http_code}" -s --fail "$url");
elif machine_has "wget"
then
# get the http response, grab the status code
server_response=$(wget -qO- --method=HEAD --server-response "$url" 2>&1)
code=$(echo "$server_response" | grep "HTTP/" | awk '{print $2}')
fi
if [ $code = "200" ]; then
return 0
else
return 1
fi
}
sanitize_redirect_url() {
eval $invocation
local url_stem
url_stem=$(extract_stem "$1")
say_verbose "Checking configured feeds for the asset at ${yellow:-}$url_stem${normal:-}"
for feed in "${feeds[@]}"
do
local trial_url="$feed/$url_stem"
say_verbose "Checking ${yellow:-}$trial_url${normal:-}"
if check_url_exists "$trial_url"; then
say_verbose "Found a match at ${yellow:-}$trial_url${normal:-}"
echo "$trial_url"
return 0
else
say_verbose "No match at ${yellow:-}$trial_url${normal:-}"
fi
done
return 1
}
get_download_link_from_aka_ms() {
eval $invocation
@@ -1172,6 +1363,12 @@ get_download_link_from_aka_ms() {
http_codes=$( echo "$response" | awk '$1 ~ /^HTTP/ {print $2}' )
# They all need to be 301, otherwise some links are broken (except for the last, which is not a redirect but 200 or 404).
broken_redirects=$( echo "$http_codes" | sed '$d' | grep -v '301' )
# The response may end without final code 2xx/4xx/5xx somehow, e.g. network restrictions on www.bing.com causes redirecting to bing.com fails with connection refused.
# In this case it should not exclude the last.
last_http_code=$( echo "$http_codes" | tail -n 1 )
if ! [[ $last_http_code =~ ^(2|4|5)[0-9][0-9]$ ]]; then
broken_redirects=$( echo "$http_codes" | grep -v '301' )
fi
# All HTTP codes are 301 (Moved Permanently), the redirect link exists.
if [[ -z "$broken_redirects" ]]; then
@@ -1182,6 +1379,11 @@ get_download_link_from_aka_ms() {
return 1
fi
sanitized_redirect_url=$(sanitize_redirect_url "$aka_ms_download_link")
if [[ -n "$sanitized_redirect_url" ]]; then
aka_ms_download_link="$sanitized_redirect_url"
fi
say_verbose "The redirect location retrieved: '$aka_ms_download_link'."
return 0
else
@@ -1193,23 +1395,16 @@ get_download_link_from_aka_ms() {
get_feeds_to_use()
{
feeds=(
"https://dotnetcli.azureedge.net/dotnet"
"https://dotnetbuilds.azureedge.net/public"
"https://builds.dotnet.microsoft.com/dotnet"
"https://ci.dot.net/public"
)
if [[ -n "$azure_feed" ]]; then
feeds=("$azure_feed")
fi
if [[ "$no_cdn" == "true" ]]; then
feeds=(
"https://dotnetcli.blob.core.windows.net/dotnet"
"https://dotnetbuilds.blob.core.windows.net/public"
)
if [[ -n "$uncached_feed" ]]; then
feeds=("$uncached_feed")
fi
if [[ -n "$uncached_feed" ]]; then
feeds=("$uncached_feed")
fi
}
@@ -1341,7 +1536,7 @@ generate_regular_links() {
link_types+=("legacy")
else
legacy_download_link=""
say_verbose "Cound not construct a legacy_download_link; omitting..."
say_verbose "Could not construct a legacy_download_link; omitting..."
fi
# Check if the SDK version is already installed.
@@ -1419,10 +1614,11 @@ install_dotnet() {
eval $invocation
local download_failed=false
local download_completed=false
local remote_file_size=0
mkdir -p "$install_root"
zip_path="$(mktemp "$temporary_file_template")"
say_verbose "Zip path: $zip_path"
zip_path="${zip_path:-$(mktemp "$temporary_file_template")}"
say_verbose "Archive path: $zip_path"
for link_index in "${!download_links[@]}"
do
@@ -1443,10 +1639,10 @@ install_dotnet() {
say "The resource at $link_type link '$download_link' is not available."
;;
*)
say "Failed to download $link_type link '$download_link': $download_error_msg"
say "Failed to download $link_type link '$download_link': $http_code $download_error_msg"
;;
esac
rm -f "$zip_path" 2>&1 && say_verbose "Temporary zip file $zip_path was removed"
rm -f "$zip_path" 2>&1 && say_verbose "Temporary archive file $zip_path was removed"
else
download_completed=true
break
@@ -1459,8 +1655,10 @@ install_dotnet() {
return 1
fi
say "Extracting zip from $download_link"
extract_dotnet_package "$zip_path" "$install_root" || return 1
remote_file_size="$(get_remote_file_size "$download_link")"
say "Extracting archive from $download_link"
extract_dotnet_package "$zip_path" "$install_root" "$remote_file_size" || return 1
# Check if the SDK version is installed; if not, fail the installation.
# if the version contains "RTM" or "servicing"; check if a 'release-type' SDK version is installed.
@@ -1502,7 +1700,6 @@ install_dir="<auto>"
architecture="<auto>"
dry_run=false
no_path=false
no_cdn=false
azure_feed=""
uncached_feed=""
feed_credential=""
@@ -1575,10 +1772,6 @@ do
verbose=true
non_dynamic_parameters+=" $name"
;;
--no-cdn|-[Nn]o[Cc]dn)
no_cdn=true
non_dynamic_parameters+=" $name"
;;
--azure-feed|-[Aa]zure[Ff]eed)
shift
azure_feed="$1"
@@ -1610,10 +1803,22 @@ do
override_non_versioned_files=false
non_dynamic_parameters+=" $name"
;;
--keep-zip|-[Kk]eep[Zz]ip)
keep_zip=true
non_dynamic_parameters+=" $name"
;;
--zip-path|-[Zz]ip[Pp]ath)
shift
zip_path="$1"
;;
-?|--?|-h|--help|-[Hh]elp)
script_name="$(basename "$0")"
script_name="dotnet-install.sh"
echo ".NET Tools Installer"
echo "Usage: $script_name [-c|--channel <CHANNEL>] [-v|--version <VERSION>] [-p|--prefix <DESTINATION>]"
echo "Usage:"
echo " # Install a .NET SDK of a given Quality from a given Channel"
echo " $script_name [-c|--channel <CHANNEL>] [-q|--quality <QUALITY>]"
echo " # Install a .NET SDK of a specific public version"
echo " $script_name [-v|--version <VERSION>]"
echo " $script_name -h|-?|--help"
echo ""
echo "$script_name is a simple command line interface for obtaining dotnet cli."
@@ -1655,7 +1860,7 @@ do
echo " -InstallDir"
echo " --architecture <ARCHITECTURE> Architecture of dotnet binaries to be installed, Defaults to \`$architecture\`."
echo " --arch,-Architecture,-Arch"
echo " Possible values: x64, arm, arm64 and s390x"
echo " Possible values: x64, arm, arm64, s390x, ppc64le and loongarch64"
echo " --os <system> Specifies operating system to be used when selecting the installer."
echo " Overrides the OS determination approach used by the script. Supported values: osx, linux, linux-musl, freebsd, rhel.6."
echo " In case any other value is provided, the platform will be determined by the script based on machine configuration."
@@ -1671,15 +1876,14 @@ do
echo " --verbose,-Verbose Display diagnostics information."
echo " --azure-feed,-AzureFeed For internal use only."
echo " Allows using a different storage to download SDK archives from."
echo " This parameter is only used if --no-cdn is false."
echo " --uncached-feed,-UncachedFeed For internal use only."
echo " Allows using a different storage to download SDK archives from."
echo " This parameter is only used if --no-cdn is true."
echo " --skip-non-versioned-files Skips non-versioned files if they already exist, such as the dotnet executable."
echo " -SkipNonVersionedFiles"
echo " --no-cdn,-NoCdn Disable downloading from the Azure CDN, and use the uncached feed directly."
echo " --jsonfile <JSONFILE> Determines the SDK version from a user specified global.json file."
echo " Note: global.json must have a value for 'SDK:Version'"
echo " --keep-zip,-KeepZip If set, downloaded file is kept."
echo " --zip-path, -ZipPath If set, downloaded file is stored at the specified path."
echo " -?,--?,-h,--help,-Help Shows this help message"
echo ""
echo "Install Location:"

3562
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -3,9 +3,9 @@
"version": "3.0.2",
"private": true,
"description": "setup dotnet action",
"main": "lib/setup-dotnet.js",
"main": "dist/setup/index.js",
"scripts": {
"build": "tsc && ncc build",
"build": "ncc build -o dist/setup src/setup-dotnet.ts && ncc build -o dist/cache-save src/cache-save.ts",
"format": "prettier --no-error-on-unmatched-pattern --config ./.prettierrc.js --write \"**/*.{ts,yml,yaml}\"",
"format-check": "prettier --no-error-on-unmatched-pattern --config ./.prettierrc.js --check \"**/*.{ts,yml,yaml}\"",
"lint": "eslint --config ./.eslintrc.js \"**/*.ts\"",
@@ -26,27 +26,31 @@
"author": "GitHub",
"license": "MIT",
"dependencies": {
"@actions/cache": "^4.0.3",
"@actions/core": "^1.10.0",
"@actions/exec": "^1.0.4",
"@actions/exec": "^1.1.1",
"@actions/github": "^1.1.0",
"@actions/glob": "^0.3.0",
"@actions/http-client": "^2.0.1",
"@actions/io": "^1.0.2",
"fast-xml-parser": "^4.0.10",
"semver": "^6.3.0"
},
"devDependencies": {
"@types/jest": "^27.0.2",
"@types/jest": "^27.5.2",
"@types/node": "^16.11.25",
"@types/semver": "^6.2.2",
"@typescript-eslint/eslint-plugin": "^5.54.0",
"@typescript-eslint/parser": "^5.54.0",
"@vercel/ncc": "^0.33.4",
"@vercel/ncc": "^0.34.0",
"eslint": "^8.35.0",
"eslint-config-prettier": "^8.6.0",
"eslint-plugin-jest": "^27.2.1",
"eslint-plugin-node": "^11.1.0",
"husky": "^8.0.1",
"jest": "^27.2.5",
"jest-circus": "^27.2.5",
"jest": "^27.5.1",
"jest-circus": "^27.5.1",
"jest-each": "^27.5.1",
"prettier": "^2.8.4",
"ts-jest": "^27.0.5",
"typescript": "^4.8.4",

50
src/cache-restore.ts Normal file
View File

@@ -0,0 +1,50 @@
import {readdir} from 'node:fs/promises';
import {join} from 'node:path';
import * as cache from '@actions/cache';
import * as core from '@actions/core';
import * as glob from '@actions/glob';
import {getNuGetFolderPath} from './cache-utils';
import {lockFilePatterns, State, Outputs} from './constants';
export const restoreCache = async (cacheDependencyPath?: string) => {
const lockFilePath = cacheDependencyPath || (await findLockFile());
const fileHash = await glob.hashFiles(lockFilePath);
if (!fileHash) {
throw new Error(
'Some specified paths were not resolved, unable to cache dependencies.'
);
}
const platform = process.env.RUNNER_OS;
const primaryKey = `dotnet-cache-${platform}-${fileHash}`;
core.debug(`primary key is ${primaryKey}`);
core.saveState(State.CachePrimaryKey, primaryKey);
const {'global-packages': cachePath} = await getNuGetFolderPath();
const cacheKey = await cache.restoreCache([cachePath], primaryKey);
core.setOutput(Outputs.CacheHit, Boolean(cacheKey));
if (!cacheKey) {
core.info('Dotnet cache is not found');
return;
}
core.saveState(State.CacheMatchedKey, cacheKey);
core.info(`Cache restored from key: ${cacheKey}`);
};
const findLockFile = async () => {
const workspace = process.env.GITHUB_WORKSPACE!;
const rootContent = await readdir(workspace);
const lockFile = lockFilePatterns.find(item => rootContent.includes(item));
if (!lockFile) {
throw new Error(
`Dependencies lock file is not found in ${workspace}. Supported file patterns: ${lockFilePatterns.toString()}`
);
}
return join(workspace, lockFile);
};

57
src/cache-save.ts Normal file
View File

@@ -0,0 +1,57 @@
import * as core from '@actions/core';
import * as cache from '@actions/cache';
import fs from 'node:fs';
import {getNuGetFolderPath} from './cache-utils';
import {State} from './constants';
// Catch and log any unhandled exceptions. These exceptions can leak out of the uploadChunk method in
// @actions/toolkit when a failed upload closes the file descriptor causing any in-process reads to
// throw an uncaught exception. Instead of failing this action, just warn.
process.on('uncaughtException', e => {
const warningPrefix = '[warning]';
core.info(`${warningPrefix}${e.message}`);
});
export async function run() {
try {
if (core.getBooleanInput('cache')) {
await cachePackages();
}
} catch (error) {
core.setFailed(error.message);
}
}
const cachePackages = async () => {
const state = core.getState(State.CacheMatchedKey);
const primaryKey = core.getState(State.CachePrimaryKey);
if (!primaryKey) {
core.info('Primary key was not generated, not saving cache.');
return;
}
const {'global-packages': cachePath} = await getNuGetFolderPath();
if (!fs.existsSync(cachePath)) {
throw new Error(
`Cache folder path is retrieved for .NET CLI but doesn't exist on disk: ${cachePath}`
);
}
if (primaryKey === state) {
core.info(
`Cache hit occurred on the primary key ${primaryKey}, not saving cache.`
);
return;
}
const cacheId = await cache.saveCache([cachePath], primaryKey);
if (cacheId == -1) {
return;
}
core.info(`Cache saved with the key: ${primaryKey}`);
};
run();

98
src/cache-utils.ts Normal file
View File

@@ -0,0 +1,98 @@
import * as cache from '@actions/cache';
import * as core from '@actions/core';
import * as exec from '@actions/exec';
import {cliCommand} from './constants';
type NuGetFolderName =
| 'http-cache'
| 'global-packages'
| 'temp'
| 'plugins-cache';
/**
* Get NuGet global packages, cache, and temp folders from .NET CLI.
* @returns (Folder Name)-(Path) mappings
* @see https://docs.microsoft.com/nuget/consume-packages/managing-the-global-packages-and-cache-folders
* @example
* Windows
* ```json
* {
* "http-cache": "C:\\Users\\user1\\AppData\\Local\\NuGet\\v3-cache",
* "global-packages": "C:\\Users\\user1\\.nuget\\packages\\",
* "temp": "C:\\Users\\user1\\AppData\\Local\\Temp\\NuGetScratch",
* "plugins-cache": "C:\\Users\\user1\\AppData\\Local\\NuGet\\plugins-cache"
* }
* ```
*
* Mac/Linux
* ```json
* {
* "http-cache": "/home/user1/.local/share/NuGet/v3-cache",
* "global-packages": "/home/user1/.nuget/packages/",
* "temp": "/tmp/NuGetScratch",
* "plugins-cache": "/home/user1/.local/share/NuGet/plugins-cache"
* }
* ```
*/
export const getNuGetFolderPath = async () => {
const {stdout, stderr, exitCode} = await exec.getExecOutput(
cliCommand,
undefined,
{ignoreReturnCode: true, silent: true}
);
if (exitCode) {
throw new Error(
!stderr.trim()
? `The '${cliCommand}' command failed with exit code: ${exitCode}`
: stderr
);
}
const result: Record<NuGetFolderName, string> = {
'http-cache': '',
'global-packages': '',
temp: '',
'plugins-cache': ''
};
const regex = /(?:^|\s)(?<key>[a-z-]+): (?<path>.+[/\\].+)$/gm;
let match: RegExpExecArray | null;
while ((match = regex.exec(stdout)) !== null) {
const key = match.groups!.key;
if ((key as NuGetFolderName) in result) {
result[key] = match.groups!.path;
}
}
return result;
};
export function isCacheFeatureAvailable(): boolean {
if (cache.isFeatureAvailable()) {
return true;
}
if (isGhes()) {
core.warning(
'Cache action is only supported on GHES version >= 3.5. If you are on version >=3.5 Please check with GHES admin if Actions cache service is enabled or not.'
);
return false;
}
core.warning(
'The runner was not able to contact the cache service. Caching will be skipped'
);
return false;
}
/**
* Returns this action runs on GitHub Enterprise Server or not.
* (port from https://github.com/actions/toolkit/blob/457303960f03375db6f033e214b9f90d79c3fe5c/packages/cache/src/internal/cacheUtils.ts#L134)
*/
function isGhes(): boolean {
const url = process.env['GITHUB_SERVER_URL'] || 'https://github.com';
return new URL(url).hostname.toUpperCase() !== 'GITHUB.COM';
}

19
src/constants.ts Normal file
View File

@@ -0,0 +1,19 @@
/** NuGet lock file patterns */
export const lockFilePatterns = ['packages.lock.json'];
/**
* .NET CLI command to list local NuGet resources.
* @see https://docs.microsoft.com/dotnet/core/tools/dotnet-nuget-locals
*/
export const cliCommand =
'dotnet nuget locals all --list --force-english-output';
export enum State {
CachePrimaryKey = 'CACHE_KEY',
CacheMatchedKey = 'CACHE_RESULT'
}
export enum Outputs {
CacheHit = 'cache-hit',
DotnetVersion = 'dotnet-version'
}

View File

@@ -101,9 +101,11 @@ export class DotnetVersionResolver {
allowRetries: true,
maxRetries: 3
});
const response = await httpClient.getJson<any>(
DotnetVersionResolver.DotNetCoreIndexUrl
);
const result = response.result || {};
const releasesInfo: any[] = result['releases-index'];
@@ -122,7 +124,7 @@ export class DotnetVersionResolver {
}
static DotNetCoreIndexUrl =
'https://dotnetcli.azureedge.net/dotnet/release-metadata/releases-index.json';
'https://builds.dotnet.microsoft.com/dotnet/release-metadata/releases-index.json';
}
export class DotnetCoreInstaller {
@@ -203,7 +205,7 @@ export class DotnetCoreInstaller {
];
const scriptName = IS_WINDOWS ? 'install-dotnet.ps1' : 'install-dotnet.sh';
const escapedScript = path
.join(__dirname, '..', 'externals', scriptName)
.join(__dirname, '..', '..', 'externals', scriptName)
.replace(/'/g, "''");
let scriptArguments: string[];
let scriptPath = '';

View File

@@ -4,6 +4,9 @@ import * as fs from 'fs';
import path from 'path';
import semver from 'semver';
import * as auth from './authutil';
import {isCacheFeatureAvailable} from './cache-utils';
import {restoreCache} from './cache-restore';
import {Outputs} from './constants';
const qualityOptions = [
'daily',
@@ -80,7 +83,12 @@ export async function run() {
outputInstalledVersion(installedDotnetVersions, globalJsonFileInput);
const matchersPath = path.join(__dirname, '..', '.github');
if (core.getBooleanInput('cache') && isCacheFeatureAvailable()) {
const cacheDependencyPath = core.getInput('cache-dependency-path');
await restoreCache(cacheDependencyPath);
}
const matchersPath = path.join(__dirname, '..', '..', '.github');
core.info(`##[add-matcher]${path.join(matchersPath, 'csc.json')}`);
} catch (error) {
core.setFailed(error.message);
@@ -109,20 +117,20 @@ function outputInstalledVersion(
globalJsonFileInput: string
): void {
if (!installedVersions.length) {
core.info(`The 'dotnet-version' output will not be set.`);
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 'dotnet-version' output will not be set.`
`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('dotnet-version', versionToOutput);
core.setOutput(Outputs.DotnetVersion, versionToOutput);
return;
}
@@ -134,7 +142,7 @@ function outputInstalledVersion(
}
);
core.setOutput('dotnet-version', versionToOutput);
core.setOutput(Outputs.DotnetVersion, versionToOutput);
}
run();