diff --git a/__tests__/authutil.test.ts b/__tests__/authutil.test.ts index 0ed596d..ef1a6fa 100644 --- a/__tests__/authutil.test.ts +++ b/__tests__/authutil.test.ts @@ -1,308 +1,308 @@ -import io = require('@actions/io'); -import fs = require('fs'); -import path = require('path'); - -const fakeSourcesDirForTesting = path.join( - __dirname, - 'runner', - path.join( - Math.random() - .toString(36) - .substring(7) - ), - 's' -); - -const invalidNuGetConfig: string = ``; - -const emptyNuGetConfig: string = ` - -`; - -const nugetorgNuGetConfig: string = ` - - - - -`; - -const gprnugetorgNuGetConfig: string = ` - - - - - -`; - -const gprNuGetConfig: string = ` - - - - -`; - -const twogprNuGetConfig: string = ` - - - - - -`; - -const spaceNuGetConfig: string = ` - - - - -`; - -const azureartifactsNuGetConfig: string = ` - - - - -`; - -const azureartifactsnugetorgNuGetConfig: string = ` - - - - - -`; - -// We want a NuGet.config one level above the sources directory, so it doesn't trample a user's NuGet.config but is still picked up by NuGet/dotnet. -const nugetConfigFile = path.join(fakeSourcesDirForTesting, '../nuget.config'); - -process.env['GITHUB_REPOSITORY'] = 'OwnerName/repo'; -process.env['RUNNER_TEMP'] = fakeSourcesDirForTesting; -import * as auth from '../src/authutil'; - -describe('authutil tests', () => { - beforeEach(async () => { - await io.rmRF(fakeSourcesDirForTesting); - await io.mkdirP(fakeSourcesDirForTesting); - }, 100000); - - beforeEach(() => { - if (fs.existsSync(nugetConfigFile)) { - fs.unlinkSync(nugetConfigFile); - } - process.env['INPUT_OWNER'] = ''; - process.env['NUGET_AUTH_TOKEN'] = ''; - }); - - 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( - 'https://nuget.pkg.github.com/OwnerName/index.json' - ); - expect(fs.existsSync(nugetConfigFile)).toBe(true); - expect( - fs.readFileSync(nugetConfigFile, {encoding: 'utf8'}) - ).toMatchSnapshot(); - }); - - it('No existing config, auth token environment variable not provided, throws', async () => { - let thrown = false; - try { - await auth.configAuthentication( - 'https://nuget.pkg.github.com/OwnerName/index.json' - ); - } catch { - thrown = true; - } - expect(thrown).toBe(true); - }); - - 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( - 'https://nuget.pkg.github.com/otherorg/index.json' - ); - expect(fs.existsSync(nugetConfigFile)).toBe(true); - expect( - fs.readFileSync(nugetConfigFile, {encoding: 'utf8'}) - ).toMatchSnapshot(); - }); - - 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, - 'nuget.config' - ); - fs.writeFileSync(inputNuGetConfigPath, invalidNuGetConfig); - let thrown = false; - try { - await auth.configAuthentication( - 'https://nuget.pkg.github.com/OwnerName/index.json' - ); - } catch { - thrown = true; - } - expect(thrown).toBe(true); - }); - - 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( - 'https://nuget.pkg.github.com/OwnerName/index.json' - ); - expect(fs.existsSync(nugetConfigFile)).toBe(true); - expect( - fs.readFileSync(nugetConfigFile, {encoding: 'utf8'}) - ).toMatchSnapshot(); - }); - - 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( - 'https://nuget.pkg.github.com/OwnerName/index.json' - ); - expect(fs.existsSync(nugetConfigFile)).toBe(true); - expect( - fs.readFileSync(nugetConfigFile, {encoding: 'utf8'}) - ).toMatchSnapshot(); - }); - - 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( - 'https://nuget.pkg.github.com/OwnerName/index.json' - ); - expect(fs.existsSync(nugetConfigFile)).toBe(true); - expect( - fs.readFileSync(nugetConfigFile, {encoding: 'utf8'}) - ).toMatchSnapshot(); - }); - - 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( - 'https://nuget.pkg.github.com/OwnerName/index.json' - ); - expect(fs.existsSync(nugetConfigFile)).toBe(true); - expect( - fs.readFileSync(nugetConfigFile, {encoding: 'utf8'}) - ).toMatchSnapshot(); - }); - - 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('https://nuget.pkg.github.com'); - expect(fs.existsSync(nugetConfigFile)).toBe(true); - expect( - fs.readFileSync(nugetConfigFile, {encoding: 'utf8'}) - ).toMatchSnapshot(); - }); - - 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, - 'nuget.config' - ); - fs.writeFileSync(inputNuGetConfigPath, spaceNuGetConfig); - let thrown = false; - try { - await auth.configAuthentication( - 'https://nuget.pkg.github.com/OwnerName/index.json' - ); - } catch { - thrown = true; - } - expect(thrown).toBe(true); - }); - - 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, - 'subfolder' - ); - const inputNuGetConfigPath: string = path.join( - inputNuGetConfigDirectory, - 'nuget.config' - ); - fs.mkdirSync(inputNuGetConfigDirectory, {recursive: true}); - fs.writeFileSync(inputNuGetConfigPath, gprNuGetConfig); - await auth.configAuthentication( - 'https://nuget.pkg.github.com/OwnerName/index.json', - 'subfolder/nuget.config' - ); - expect(fs.existsSync(nugetConfigFile)).toBe(true); - expect( - fs.readFileSync(nugetConfigFile, {encoding: 'utf8'}) - ).toMatchSnapshot(); - }); - - 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( - 'https://pkgs.dev.azure.com/amullans/_packaging/GitHubBuilds/nuget/v3/index.json' - ); - expect(fs.existsSync(nugetConfigFile)).toBe(true); - expect( - fs.readFileSync(nugetConfigFile, {encoding: 'utf8'}) - ).toMatchSnapshot(); - }); - - 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( - 'https://pkgs.dev.azure.com/amullans/_packaging/GitHubBuilds/nuget/v3/index.json' - ); - expect(fs.existsSync(nugetConfigFile)).toBe(true); - expect( - fs.readFileSync(nugetConfigFile, {encoding: 'utf8'}) - ).toMatchSnapshot(); - }); - - 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( - 'https://pkgs.dev.azure.com/amullans/_packaging/GitHubBuilds/nuget/v3/index.json' - ); - expect(fs.existsSync(nugetConfigFile)).toBe(true); - expect( - fs.readFileSync(nugetConfigFile, {encoding: 'utf8'}) - ).toMatchSnapshot(); - }); -}); +import io = require('@actions/io'); +import fs = require('fs'); +import path = require('path'); + +const fakeSourcesDirForTesting = path.join( + __dirname, + 'runner', + path.join( + Math.random() + .toString(36) + .substring(7) + ), + 's' +); + +const invalidNuGetConfig: string = ``; + +const emptyNuGetConfig: string = ` + +`; + +const nugetorgNuGetConfig: string = ` + + + + +`; + +const gprnugetorgNuGetConfig: string = ` + + + + + +`; + +const gprNuGetConfig: string = ` + + + + +`; + +const twogprNuGetConfig: string = ` + + + + + +`; + +const spaceNuGetConfig: string = ` + + + + +`; + +const azureartifactsNuGetConfig: string = ` + + + + +`; + +const azureartifactsnugetorgNuGetConfig: string = ` + + + + + +`; + +// We want a NuGet.config one level above the sources directory, so it doesn't trample a user's NuGet.config but is still picked up by NuGet/dotnet. +const nugetConfigFile = path.join(fakeSourcesDirForTesting, '../nuget.config'); + +process.env['GITHUB_REPOSITORY'] = 'OwnerName/repo'; +process.env['RUNNER_TEMP'] = fakeSourcesDirForTesting; +import * as auth from '../src/authutil'; + +describe('authutil tests', () => { + beforeEach(async () => { + await io.rmRF(fakeSourcesDirForTesting); + await io.mkdirP(fakeSourcesDirForTesting); + }, 100000); + + beforeEach(() => { + if (fs.existsSync(nugetConfigFile)) { + fs.unlinkSync(nugetConfigFile); + } + process.env['INPUT_OWNER'] = ''; + process.env['NUGET_AUTH_TOKEN'] = ''; + }); + + 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( + 'https://nuget.pkg.github.com/OwnerName/index.json' + ); + expect(fs.existsSync(nugetConfigFile)).toBe(true); + expect( + fs.readFileSync(nugetConfigFile, {encoding: 'utf8'}) + ).toMatchSnapshot(); + }); + + it('No existing config, auth token environment variable not provided, throws', async () => { + let thrown = false; + try { + await auth.configAuthentication( + 'https://nuget.pkg.github.com/OwnerName/index.json' + ); + } catch { + thrown = true; + } + expect(thrown).toBe(true); + }); + + 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( + 'https://nuget.pkg.github.com/otherorg/index.json' + ); + expect(fs.existsSync(nugetConfigFile)).toBe(true); + expect( + fs.readFileSync(nugetConfigFile, {encoding: 'utf8'}) + ).toMatchSnapshot(); + }); + + 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, + 'nuget.config' + ); + fs.writeFileSync(inputNuGetConfigPath, invalidNuGetConfig); + let thrown = false; + try { + await auth.configAuthentication( + 'https://nuget.pkg.github.com/OwnerName/index.json' + ); + } catch { + thrown = true; + } + expect(thrown).toBe(true); + }); + + 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( + 'https://nuget.pkg.github.com/OwnerName/index.json' + ); + expect(fs.existsSync(nugetConfigFile)).toBe(true); + expect( + fs.readFileSync(nugetConfigFile, {encoding: 'utf8'}) + ).toMatchSnapshot(); + }); + + 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( + 'https://nuget.pkg.github.com/OwnerName/index.json' + ); + expect(fs.existsSync(nugetConfigFile)).toBe(true); + expect( + fs.readFileSync(nugetConfigFile, {encoding: 'utf8'}) + ).toMatchSnapshot(); + }); + + 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( + 'https://nuget.pkg.github.com/OwnerName/index.json' + ); + expect(fs.existsSync(nugetConfigFile)).toBe(true); + expect( + fs.readFileSync(nugetConfigFile, {encoding: 'utf8'}) + ).toMatchSnapshot(); + }); + + 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( + 'https://nuget.pkg.github.com/OwnerName/index.json' + ); + expect(fs.existsSync(nugetConfigFile)).toBe(true); + expect( + fs.readFileSync(nugetConfigFile, {encoding: 'utf8'}) + ).toMatchSnapshot(); + }); + + 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('https://nuget.pkg.github.com'); + expect(fs.existsSync(nugetConfigFile)).toBe(true); + expect( + fs.readFileSync(nugetConfigFile, {encoding: 'utf8'}) + ).toMatchSnapshot(); + }); + + 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, + 'nuget.config' + ); + fs.writeFileSync(inputNuGetConfigPath, spaceNuGetConfig); + let thrown = false; + try { + await auth.configAuthentication( + 'https://nuget.pkg.github.com/OwnerName/index.json' + ); + } catch { + thrown = true; + } + expect(thrown).toBe(true); + }); + + 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, + 'subfolder' + ); + const inputNuGetConfigPath: string = path.join( + inputNuGetConfigDirectory, + 'nuget.config' + ); + fs.mkdirSync(inputNuGetConfigDirectory, {recursive: true}); + fs.writeFileSync(inputNuGetConfigPath, gprNuGetConfig); + await auth.configAuthentication( + 'https://nuget.pkg.github.com/OwnerName/index.json', + 'subfolder/nuget.config' + ); + expect(fs.existsSync(nugetConfigFile)).toBe(true); + expect( + fs.readFileSync(nugetConfigFile, {encoding: 'utf8'}) + ).toMatchSnapshot(); + }); + + 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( + 'https://pkgs.dev.azure.com/amullans/_packaging/GitHubBuilds/nuget/v3/index.json' + ); + expect(fs.existsSync(nugetConfigFile)).toBe(true); + expect( + fs.readFileSync(nugetConfigFile, {encoding: 'utf8'}) + ).toMatchSnapshot(); + }); + + 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( + 'https://pkgs.dev.azure.com/amullans/_packaging/GitHubBuilds/nuget/v3/index.json' + ); + expect(fs.existsSync(nugetConfigFile)).toBe(true); + expect( + fs.readFileSync(nugetConfigFile, {encoding: 'utf8'}) + ).toMatchSnapshot(); + }); + + 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( + 'https://pkgs.dev.azure.com/amullans/_packaging/GitHubBuilds/nuget/v3/index.json' + ); + expect(fs.existsSync(nugetConfigFile)).toBe(true); + expect( + fs.readFileSync(nugetConfigFile, {encoding: 'utf8'}) + ).toMatchSnapshot(); + }); +}); diff --git a/__tests__/installer.test.ts b/__tests__/installer.test.ts index bafd74c..6709453 100644 --- a/__tests__/installer.test.ts +++ b/__tests__/installer.test.ts @@ -27,15 +27,16 @@ describe('version tests', () => { } ); - each([['3.1.x', '3.1'], ['1.1.*', '1.1'], ['2.0', '2.0']]).test( - "Generic version '%s' should be '%s'", - (vers, resVers) => { - let versInfo = new installer.DotNetVersionInfo(vers); + each([ + ['3.1.x', '3.1'], + ['1.1.*', '1.1'], + ['2.0', '2.0'] + ]).test("Generic version '%s' should be '%s'", (vers, resVers) => { + let versInfo = new installer.DotNetVersionInfo(vers); - expect(versInfo.isExactVersion()).toBe(false); - expect(versInfo.version()).toBe(resVers); - } - ); + expect(versInfo.isExactVersion()).toBe(false); + expect(versInfo.version()).toBe(resVers); + }); each([ '', diff --git a/dist/index.js b/dist/index.js index a7814cd..54b0a68 100644 --- a/dist/index.js +++ b/dist/index.js @@ -17816,9 +17816,11 @@ class SxSDotnetCoreInstaller { console.log(`Setting up .NET SDK from ${toolPath}...`); let entries = fs_2.readdirSync(toolPath); for (var entry of entries) { - yield io.cp(path.join(toolPath, entry), dest, { recursive: true, force: true }); + yield io.cp(path.join(toolPath, entry), dest, { + recursive: true, + force: true + }); } - ; } // cache SxS directory as a tool let cachedDir = yield tc.cacheDir(dest, this.cachedToolName, 'sxs', this.arch); diff --git a/package-lock.json b/package-lock.json index 6b4beb6..7638939 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3688,9 +3688,9 @@ "dev": true }, "prettier": { - "version": "1.18.2", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-1.18.2.tgz", - "integrity": "sha512-OeHeMc0JhFE9idD4ZdtNibzY0+TPHSpSSb9h8FqtP+YnoZZ1sl8Vc9b1sasjfymH3SonAF4QcA2+mzHPhMvIiw==", + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-1.19.1.tgz", + "integrity": "sha512-s7PoyDv/II1ObgQunCbB9PdLmUcBZcnWOcxDh7O0N/UwDEsHyqkW+Qh28jW+mVuCdx7gLB0BotYI1Y6uI9iyew==", "dev": true }, "pretty-format": { diff --git a/package.json b/package.json index 21f6a73..d35094b 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,7 @@ "@zeit/ncc": "^0.21.0", "jest": "^26.0.1", "jest-circus": "^26.0.1", - "prettier": "^1.17.1", + "prettier": "^1.19.1", "ts-jest": "^26.0.0", "typescript": "^3.9.2", "wget-improved": "^3.0.2" diff --git a/src/authutil.ts b/src/authutil.ts index 69f3750..621fb79 100644 --- a/src/authutil.ts +++ b/src/authutil.ts @@ -1,136 +1,136 @@ -import * as fs from 'fs'; -import * as path from 'path'; -import * as core from '@actions/core'; -import * as github from '@actions/github'; -import * as xmlbuilder from 'xmlbuilder'; -import * as xmlParser from 'fast-xml-parser'; - -export function configAuthentication( - feedUrl: string, - existingFileLocation: string = '' -) { - const existingNuGetConfig: string = path.resolve( - process.env['RUNNER_TEMP'] || process.cwd(), - existingFileLocation == '' ? 'nuget.config' : existingFileLocation - ); - - const tempNuGetConfig: string = path.resolve( - process.env['RUNNER_TEMP'] || process.cwd(), - '../', - 'nuget.config' - ); - - writeFeedToFile(feedUrl, existingNuGetConfig, tempNuGetConfig); -} - -function writeFeedToFile( - feedUrl: string, - existingFileLocation: string, - tempFileLocation: string -) { - console.log( - `dotnet-auth: Finding any source references in ${existingFileLocation}, writing a new temporary configuration file with credentials to ${tempFileLocation}` - ); - let xml: xmlbuilder.XMLElement; - let sourceKeys: string[] = []; - let owner: string = core.getInput('owner'); - let sourceUrl: string = feedUrl; - if (!owner) { - owner = github.context.repo.owner; - } - - if (!process.env.NUGET_AUTH_TOKEN || process.env.NUGET_AUTH_TOKEN == '') { - throw new Error( - 'The NUGET_AUTH_TOKEN environment variable was not provided. In this step, add the following: \r\nenv:\r\n NUGET_AUTH_TOKEN: ${{secrets.GITHUB_TOKEN}}' - ); - } - - if (fs.existsSync(existingFileLocation)) { - // get key from existing NuGet.config so NuGet/dotnet can match credentials - const curContents: string = fs.readFileSync(existingFileLocation, 'utf8'); - var json = xmlParser.parse(curContents, {ignoreAttributes: false}); - - if (typeof json.configuration == 'undefined') { - throw new Error(`The provided NuGet.config seems invalid.`); - } - if (typeof json.configuration.packageSources != 'undefined') { - if (typeof json.configuration.packageSources.add != 'undefined') { - // file has at least one - if (typeof json.configuration.packageSources.add[0] == 'undefined') { - // file has only one - if ( - json.configuration.packageSources.add['@_value'] - .toLowerCase() - .includes(feedUrl.toLowerCase()) - ) { - let key = json.configuration.packageSources.add['@_key']; - sourceKeys.push(key); - core.debug(`Found a URL with key ${key}`); - } - } else { - // file has 2+ - for ( - let i = 0; - i < json.configuration.packageSources.add.length; - i++ - ) { - const source = json.configuration.packageSources.add[i]; - const value = source['@_value']; - core.debug(`source '${value}'`); - if (value.toLowerCase().includes(feedUrl.toLowerCase())) { - let key = source['@_key']; - sourceKeys.push(key); - core.debug(`Found a URL with key ${key}`); - } - } - } - } - } - } - - xml = xmlbuilder - .create('configuration') - .ele('config') - .ele('add', {key: 'defaultPushSource', value: sourceUrl}) - .up() - .up(); - - if (sourceKeys.length == 0) { - let keystring = 'Source'; - xml = xml - .ele('packageSources') - .ele('add', {key: keystring, value: sourceUrl}) - .up() - .up(); - sourceKeys.push(keystring); - } - xml = xml.ele('packageSourceCredentials'); - - sourceKeys.forEach(key => { - if (key.indexOf(' ') > -1) { - throw new Error( - "This action currently can't handle source names with spaces. Remove the space from your repo's NuGet.config and try again." - ); - } - - xml = xml - .ele(key) - .ele('add', {key: 'Username', value: owner}) - .up() - .ele('add', { - key: 'ClearTextPassword', - value: process.env.NUGET_AUTH_TOKEN - }) - .up() - .up(); - }); - - // If NuGet fixes itself such that on Linux it can look for environment variables in the config file (it doesn't seem to work today), - // use this for the value above - // process.platform == 'win32' - // ? '%NUGET_AUTH_TOKEN%' - // : '$NUGET_AUTH_TOKEN' - - var output = xml.end({pretty: true}); - fs.writeFileSync(tempFileLocation, output); -} +import * as fs from 'fs'; +import * as path from 'path'; +import * as core from '@actions/core'; +import * as github from '@actions/github'; +import * as xmlbuilder from 'xmlbuilder'; +import * as xmlParser from 'fast-xml-parser'; + +export function configAuthentication( + feedUrl: string, + existingFileLocation: string = '' +) { + const existingNuGetConfig: string = path.resolve( + process.env['RUNNER_TEMP'] || process.cwd(), + existingFileLocation == '' ? 'nuget.config' : existingFileLocation + ); + + const tempNuGetConfig: string = path.resolve( + process.env['RUNNER_TEMP'] || process.cwd(), + '../', + 'nuget.config' + ); + + writeFeedToFile(feedUrl, existingNuGetConfig, tempNuGetConfig); +} + +function writeFeedToFile( + feedUrl: string, + existingFileLocation: string, + tempFileLocation: string +) { + console.log( + `dotnet-auth: Finding any source references in ${existingFileLocation}, writing a new temporary configuration file with credentials to ${tempFileLocation}` + ); + let xml: xmlbuilder.XMLElement; + let sourceKeys: string[] = []; + let owner: string = core.getInput('owner'); + let sourceUrl: string = feedUrl; + if (!owner) { + owner = github.context.repo.owner; + } + + if (!process.env.NUGET_AUTH_TOKEN || process.env.NUGET_AUTH_TOKEN == '') { + throw new Error( + 'The NUGET_AUTH_TOKEN environment variable was not provided. In this step, add the following: \r\nenv:\r\n NUGET_AUTH_TOKEN: ${{secrets.GITHUB_TOKEN}}' + ); + } + + if (fs.existsSync(existingFileLocation)) { + // get key from existing NuGet.config so NuGet/dotnet can match credentials + const curContents: string = fs.readFileSync(existingFileLocation, 'utf8'); + var json = xmlParser.parse(curContents, {ignoreAttributes: false}); + + if (typeof json.configuration == 'undefined') { + throw new Error(`The provided NuGet.config seems invalid.`); + } + if (typeof json.configuration.packageSources != 'undefined') { + if (typeof json.configuration.packageSources.add != 'undefined') { + // file has at least one + if (typeof json.configuration.packageSources.add[0] == 'undefined') { + // file has only one + if ( + json.configuration.packageSources.add['@_value'] + .toLowerCase() + .includes(feedUrl.toLowerCase()) + ) { + let key = json.configuration.packageSources.add['@_key']; + sourceKeys.push(key); + core.debug(`Found a URL with key ${key}`); + } + } else { + // file has 2+ + for ( + let i = 0; + i < json.configuration.packageSources.add.length; + i++ + ) { + const source = json.configuration.packageSources.add[i]; + const value = source['@_value']; + core.debug(`source '${value}'`); + if (value.toLowerCase().includes(feedUrl.toLowerCase())) { + let key = source['@_key']; + sourceKeys.push(key); + core.debug(`Found a URL with key ${key}`); + } + } + } + } + } + } + + xml = xmlbuilder + .create('configuration') + .ele('config') + .ele('add', {key: 'defaultPushSource', value: sourceUrl}) + .up() + .up(); + + if (sourceKeys.length == 0) { + let keystring = 'Source'; + xml = xml + .ele('packageSources') + .ele('add', {key: keystring, value: sourceUrl}) + .up() + .up(); + sourceKeys.push(keystring); + } + xml = xml.ele('packageSourceCredentials'); + + sourceKeys.forEach(key => { + if (key.indexOf(' ') > -1) { + throw new Error( + "This action currently can't handle source names with spaces. Remove the space from your repo's NuGet.config and try again." + ); + } + + xml = xml + .ele(key) + .ele('add', {key: 'Username', value: owner}) + .up() + .ele('add', { + key: 'ClearTextPassword', + value: process.env.NUGET_AUTH_TOKEN + }) + .up() + .up(); + }); + + // If NuGet fixes itself such that on Linux it can look for environment variables in the config file (it doesn't seem to work today), + // use this for the value above + // process.platform == 'win32' + // ? '%NUGET_AUTH_TOKEN%' + // : '$NUGET_AUTH_TOKEN' + + var output = xml.end({pretty: true}); + fs.writeFileSync(tempFileLocation, output); +} diff --git a/src/installer.ts b/src/installer.ts index 0120c57..fb7c3a5 100644 --- a/src/installer.ts +++ b/src/installer.ts @@ -1,549 +1,552 @@ -// Load tempDirectory before it gets wiped by tool-cache -let tempDirectory = process.env['RUNNER_TEMPDIRECTORY'] || ''; -import * as core from '@actions/core'; -import * as exec from '@actions/exec'; -import * as io from '@actions/io'; -import * as tc from '@actions/tool-cache'; -import hc = require('@actions/http-client'); -import {chmodSync} from 'fs'; -import {readdirSync} from 'fs'; -import * as os from 'os'; -import * as path from 'path'; -import * as semver from 'semver'; -import uuidV4 from 'uuid/v4' - -const IS_WINDOWS = process.platform === 'win32'; - -if (!tempDirectory) { - let baseLocation; - if (IS_WINDOWS) { - // On windows use the USERPROFILE env variable - baseLocation = process.env['USERPROFILE'] || 'C:\\'; - } else { - if (process.platform === 'darwin') { - baseLocation = '/Users'; - } else { - baseLocation = '/home'; - } - } - tempDirectory = path.join(baseLocation, 'actions', 'temp'); -} - -/** - * Represents the inputted version information - */ -export class DotNetVersionInfo { - private fullversion: string; - private isExactVersionSet: boolean = false; - - constructor(version: string) { - // Check for exact match - if (semver.valid(semver.clean(version) || '') != null) { - this.fullversion = semver.clean(version) as string; - this.isExactVersionSet = true; - - return; - } - - //Note: No support for previews when using generic - let parts: string[] = version.split('.'); - - if (parts.length < 2 || parts.length > 3) this.throwInvalidVersionFormat(); - - if (parts.length == 3 && parts[2] !== 'x' && parts[2] !== '*') { - this.throwInvalidVersionFormat(); - } - - let major = this.getVersionNumberOrThrow(parts[0]); - let minor = this.getVersionNumberOrThrow(parts[1]); - - this.fullversion = major + '.' + minor; - } - - private getVersionNumberOrThrow(input: string): number { - try { - if (!input || input.trim() === '') this.throwInvalidVersionFormat(); - - let number = Number(input); - - if (Number.isNaN(number) || number < 0) this.throwInvalidVersionFormat(); - - return number; - } catch { - this.throwInvalidVersionFormat(); - return -1; - } - } - - private throwInvalidVersionFormat() { - throw 'Invalid version format! Supported: 1.2.3, 1.2, 1.2.x, 1.2.*'; - } - - /** - * If true exacatly one version should be resolved - */ - public isExactVersion(): boolean { - return this.isExactVersionSet; - } - - public version(): string { - return this.fullversion; - } -} - -/** - * Represents a resolved version from the Web-Api - */ -class ResolvedVersionInfo { - downloadUrls: string[]; - resolvedVersion: string; - - constructor(downloadUrls: string[], resolvedVersion: string) { - if (downloadUrls.length === 0) { - throw 'DownloadUrls can not be empty'; - } - - if (!resolvedVersion) { - throw 'Resolved version is invalid'; - } - - this.downloadUrls = downloadUrls; - this.resolvedVersion = resolvedVersion; - } -} - -export class DotnetCoreInstaller { - constructor(version: string) { - this.versionInfo = new DotNetVersionInfo(version); - this.cachedToolName = 'dncs'; - this.arch = 'x64'; - } - - public async installDotnet(): Promise { - // Check cache - let toolPath: string = ''; - let osSuffixes = await this.detectMachineOS(); - let parts = osSuffixes[0].split('-'); - if (parts.length > 1) { - this.arch = parts[1]; - } - - // If version is not generic -> look up cache - if (this.versionInfo.isExactVersion()) - toolPath = this.getLocalTool(this.versionInfo.version()); - - if (!toolPath) { - // download, extract, cache - console.log('Getting a download url', this.versionInfo.version()); - let resolvedVersionInfo = await this.resolveInfos( - osSuffixes, - this.versionInfo - ); - - //Check if cache exists for resolved version - toolPath = this.getLocalTool(resolvedVersionInfo.resolvedVersion); - if (!toolPath) { - //If not exists install it - toolPath = await this.downloadAndInstall(resolvedVersionInfo); - } else { - console.log('Using cached tool'); - } - } else { - console.log('Using cached tool'); - } - - // Need to set this so that .NET Core global tools find the right locations. - core.exportVariable('DOTNET_ROOT', toolPath); - - // Prepend the tools path. instructs the agent to prepend for future tasks - core.addPath(toolPath); - - return toolPath; - } - - private getLocalTool(version: string): string { - console.log('Checking tool cache', version); - return tc.find(this.cachedToolName, version, this.arch); - } - - private async detectMachineOS(): Promise { - let osSuffix: string[] = []; - let output = ''; - - let resultCode = 0; - if (IS_WINDOWS) { - let escapedScript = path - .join(__dirname, '..', 'externals', 'get-os-platform.ps1') - .replace(/'/g, "''"); - let command = `& '${escapedScript}'`; - - const powershellPath = await io.which('powershell', true); - resultCode = await exec.exec( - `"${powershellPath}"`, - [ - '-NoLogo', - '-Sta', - '-NoProfile', - '-NonInteractive', - '-ExecutionPolicy', - 'Unrestricted', - '-Command', - command - ], - { - listeners: { - stdout: (data: Buffer) => { - output += data.toString(); - } - } - } - ); - } else { - let scriptPath = path.join( - __dirname, - '..', - 'externals', - 'get-os-distro.sh' - ); - chmodSync(scriptPath, '777'); - - const toolPath = await io.which(scriptPath, true); - resultCode = await exec.exec(`"${toolPath}"`, [], { - listeners: { - stdout: (data: Buffer) => { - output += data.toString(); - } - } - }); - } - - if (resultCode != 0) { - throw `Failed to detect os with result code ${resultCode}. Output: ${output}`; - } - - let index; - if ((index = output.indexOf('Primary:')) >= 0) { - let primary = output.substr(index + 'Primary:'.length).split(os.EOL)[0]; - osSuffix.push(primary); - } - - if ((index = output.indexOf('Legacy:')) >= 0) { - let legacy = output.substr(index + 'Legacy:'.length).split(os.EOL)[0]; - osSuffix.push(legacy); - } - - if (osSuffix.length == 0) { - throw 'Could not detect platform'; - } - - return osSuffix; - } - - private async downloadAndInstall(resolvedVersionInfo: ResolvedVersionInfo) { - let downloaded = false; - let downloadPath = ''; - for (const url of resolvedVersionInfo.downloadUrls) { - try { - downloadPath = await tc.downloadTool(url); - downloaded = true; - break; - } catch (error) { - console.log('Could not Download', url, JSON.stringify(error)); - } - } - - if (!downloaded) { - throw 'Failed to download package'; - } - - // extract - console.log('Extracting Package', downloadPath); - let extPath: string = IS_WINDOWS - ? await tc.extractZip(downloadPath) - : await tc.extractTar(downloadPath); - - // cache tool - console.log('Caching tool'); - let cachedDir = await tc.cacheDir( - extPath, - this.cachedToolName, - resolvedVersionInfo.resolvedVersion, - this.arch - ); - - console.log('Successfully installed', resolvedVersionInfo.resolvedVersion); - return cachedDir; - } - - // OsSuffixes - The suffix which is a part of the file name ex- linux-x64, windows-x86 - // Type - SDK / Runtime - // versionInfo - versionInfo of the SDK/Runtime - async resolveInfos( - osSuffixes: string[], - versionInfo: DotNetVersionInfo - ): Promise { - const httpClient = new hc.HttpClient('actions/setup-dotnet', [], { - allowRetries: true, - maxRetries: 3 - }); - - const releasesJsonUrl: string = await this.getReleasesJsonUrl( - httpClient, - versionInfo.version().split('.') - ); - - const releasesResponse = await httpClient.getJson(releasesJsonUrl); - const releasesResult = releasesResponse.result || {}; - let releasesInfo: any[] = releasesResult['releases']; - releasesInfo = releasesInfo.filter((releaseInfo: any) => { - return ( - semver.satisfies( - releaseInfo['sdk']['version'], - versionInfo.version() - ) || - semver.satisfies( - releaseInfo['sdk']['version-display'], - versionInfo.version() - ) - ); - }); - - // Exclude versions that are newer than the latest if using not exact - if (!versionInfo.isExactVersion()) { - let latestSdk: string = releasesResult['latest-sdk']; - - releasesInfo = releasesInfo.filter((releaseInfo: any) => - semver.lte(releaseInfo['sdk']['version'], latestSdk) - ); - } - - // Sort for latest version - releasesInfo = releasesInfo.sort((a, b) => - semver.rcompare(a['sdk']['version'], b['sdk']['version']) - ); - - let downloadedVersion: string = ''; - let downloadUrls: string[] = []; - - if (releasesInfo.length != 0) { - let release = releasesInfo[0]; - - downloadedVersion = release['sdk']['version']; - - let files: any[] = release['sdk']['files']; - files = files.filter((file: any) => { - if (file['rid'] == osSuffixes[0] || file['rid'] == osSuffixes[1]) { - return ( - file['url'].endsWith('.zip') || file['url'].endsWith('.tar.gz') - ); - } - }); - - if (files.length > 0) { - files.forEach((file: any) => { - downloadUrls.push(file['url']); - }); - } else { - throw `The specified version's download links are not correctly formed in the supported versions document => ${releasesJsonUrl}`; - } - } else { - console.log( - `Could not fetch download information for version ${versionInfo.version()}` - ); - - if (versionInfo.isExactVersion()) { - console.log('Using fallback'); - - downloadUrls = await this.getFallbackDownloadUrls( - versionInfo.version() - ); - downloadedVersion = versionInfo.version(); - } else { - console.log('Unable to use fallback, version is generic!'); - } - } - - if (downloadUrls.length == 0) { - throw `Could not construct download URL. Please ensure that specified version ${versionInfo.version()}/${downloadedVersion} is valid.`; - } - - core.debug(`Got download urls ${downloadUrls}`); - - return new ResolvedVersionInfo(downloadUrls, downloadedVersion); - } - - private async getReleasesJsonUrl( - httpClient: hc.HttpClient, - versionParts: string[] - ): Promise { - const response = await httpClient.getJson(DotNetCoreIndexUrl); - const result = response.result || {}; - let releasesInfo: any[] = result['releases-index']; - releasesInfo = releasesInfo.filter((info: any) => { - // channel-version is the first 2 elements of the version (e.g. 2.1), filter out versions that don't match 2.1.x. - const sdkParts: string[] = info['channel-version'].split('.'); - if (versionParts.length >= 2 && versionParts[1] != 'x') { - return versionParts[0] == sdkParts[0] && versionParts[1] == sdkParts[1]; - } - return versionParts[0] == sdkParts[0]; - }); - if (releasesInfo.length === 0) { - throw `Could not find info for version ${versionParts.join( - '.' - )} at ${DotNetCoreIndexUrl}`; - } - return releasesInfo[0]['releases.json']; - } - - private async getFallbackDownloadUrls(version: string): Promise { - let primaryUrlSearchString: string; - let legacyUrlSearchString: string; - let output = ''; - let resultCode = 0; - - if (IS_WINDOWS) { - let escapedScript = path - .join(__dirname, '..', 'externals', 'install-dotnet.ps1') - .replace(/'/g, "''"); - let command = `& '${escapedScript}' -Version ${version} -DryRun`; - - const powershellPath = await io.which('powershell', true); - resultCode = await exec.exec( - `"${powershellPath}"`, - [ - '-NoLogo', - '-Sta', - '-NoProfile', - '-NonInteractive', - '-ExecutionPolicy', - 'Unrestricted', - '-Command', - command - ], - { - listeners: { - stdout: (data: Buffer) => { - output += data.toString(); - } - } - } - ); - - primaryUrlSearchString = 'dotnet-install: Primary named payload URL: '; - legacyUrlSearchString = 'dotnet-install: Legacy named payload URL: '; - } else { - let escapedScript = path - .join(__dirname, '..', 'externals', 'install-dotnet.sh') - .replace(/'/g, "''"); - chmodSync(escapedScript, '777'); - - const scriptPath = await io.which(escapedScript, true); - resultCode = await exec.exec( - `"${scriptPath}"`, - ['--version', version, '--dry-run'], - { - listeners: { - stdout: (data: Buffer) => { - output += data.toString(); - } - } - } - ); - - primaryUrlSearchString = 'dotnet-install: Primary named payload URL: '; - legacyUrlSearchString = 'dotnet-install: Legacy named payload URL: '; - } - - if (resultCode != 0) { - throw `Failed to get download urls with result code ${resultCode}. ${output}`; - } - - let primaryUrl: string = ''; - let legacyUrl: string = ''; - if (!!output && output.length > 0) { - let lines: string[] = output.split(os.EOL); - - // Fallback to \n if initial split doesn't work (not consistent across versions) - if (lines.length === 1) { - lines = output.split('\n'); - } - if (!!lines && lines.length > 0) { - lines.forEach((line: string) => { - if (!line) { - return; - } - var primarySearchStringIndex = line.indexOf(primaryUrlSearchString); - if (primarySearchStringIndex > -1) { - primaryUrl = line.substring( - primarySearchStringIndex + primaryUrlSearchString.length - ); - return; - } - - var legacySearchStringIndex = line.indexOf(legacyUrlSearchString); - if (legacySearchStringIndex > -1) { - legacyUrl = line.substring( - legacySearchStringIndex + legacyUrlSearchString.length - ); - return; - } - }); - } - } - - return [primaryUrl, legacyUrl]; - } - - private versionInfo: DotNetVersionInfo; - private cachedToolName: string; - private arch: string; -} - -export class SxSDotnetCoreInstaller { - constructor(toolPaths: Array) { - this.toolPaths = toolPaths; - this.cachedToolName = 'dncs'; - this.arch = 'x64'; - } - - public async setupSxs() { - // create a temp dir - const tempDirectory = process.env['RUNNER_TEMP'] || ''; - const dest = path.join(tempDirectory, uuidV4()); - await io.mkdirP(dest) - - console.log(`Setting up SxS .NET SDK installation in ${dest}...`); - - // copy all the SDK versions into a temporary SxS directory - for(var toolPath of this.toolPaths) { - console.log(`Setting up .NET SDK from ${toolPath}...`); - let entries = readdirSync(toolPath); - for (var entry of entries) { - await io.cp(path.join(toolPath, entry), dest, { recursive: true, force: true }); - }; - } - - // cache SxS directory as a tool - let cachedDir = await tc.cacheDir( - dest, - this.cachedToolName, - 'sxs', - this.arch - ); - - console.log(`SxS .NET SDK installation in ${cachedDir}`) - - // Need to set this so that .NET Core global tools find the right locations. - core.exportVariable('DOTNET_ROOT', cachedDir); - - // Prepend the tools path. instructs the agent to prepend for future tasks - core.addPath(cachedDir); - } - - private toolPaths: Array; - private cachedToolName: string; - private arch: string; -} - -const DotNetCoreIndexUrl: string = - 'https://dotnetcli.blob.core.windows.net/dotnet/release-metadata/releases-index.json'; +// Load tempDirectory before it gets wiped by tool-cache +let tempDirectory = process.env['RUNNER_TEMPDIRECTORY'] || ''; +import * as core from '@actions/core'; +import * as exec from '@actions/exec'; +import * as io from '@actions/io'; +import * as tc from '@actions/tool-cache'; +import hc = require('@actions/http-client'); +import {chmodSync} from 'fs'; +import {readdirSync} from 'fs'; +import * as os from 'os'; +import * as path from 'path'; +import * as semver from 'semver'; +import uuidV4 from 'uuid/v4'; + +const IS_WINDOWS = process.platform === 'win32'; + +if (!tempDirectory) { + let baseLocation; + if (IS_WINDOWS) { + // On windows use the USERPROFILE env variable + baseLocation = process.env['USERPROFILE'] || 'C:\\'; + } else { + if (process.platform === 'darwin') { + baseLocation = '/Users'; + } else { + baseLocation = '/home'; + } + } + tempDirectory = path.join(baseLocation, 'actions', 'temp'); +} + +/** + * Represents the inputted version information + */ +export class DotNetVersionInfo { + private fullversion: string; + private isExactVersionSet: boolean = false; + + constructor(version: string) { + // Check for exact match + if (semver.valid(semver.clean(version) || '') != null) { + this.fullversion = semver.clean(version) as string; + this.isExactVersionSet = true; + + return; + } + + //Note: No support for previews when using generic + let parts: string[] = version.split('.'); + + if (parts.length < 2 || parts.length > 3) this.throwInvalidVersionFormat(); + + if (parts.length == 3 && parts[2] !== 'x' && parts[2] !== '*') { + this.throwInvalidVersionFormat(); + } + + let major = this.getVersionNumberOrThrow(parts[0]); + let minor = this.getVersionNumberOrThrow(parts[1]); + + this.fullversion = major + '.' + minor; + } + + private getVersionNumberOrThrow(input: string): number { + try { + if (!input || input.trim() === '') this.throwInvalidVersionFormat(); + + let number = Number(input); + + if (Number.isNaN(number) || number < 0) this.throwInvalidVersionFormat(); + + return number; + } catch { + this.throwInvalidVersionFormat(); + return -1; + } + } + + private throwInvalidVersionFormat() { + throw 'Invalid version format! Supported: 1.2.3, 1.2, 1.2.x, 1.2.*'; + } + + /** + * If true exacatly one version should be resolved + */ + public isExactVersion(): boolean { + return this.isExactVersionSet; + } + + public version(): string { + return this.fullversion; + } +} + +/** + * Represents a resolved version from the Web-Api + */ +class ResolvedVersionInfo { + downloadUrls: string[]; + resolvedVersion: string; + + constructor(downloadUrls: string[], resolvedVersion: string) { + if (downloadUrls.length === 0) { + throw 'DownloadUrls can not be empty'; + } + + if (!resolvedVersion) { + throw 'Resolved version is invalid'; + } + + this.downloadUrls = downloadUrls; + this.resolvedVersion = resolvedVersion; + } +} + +export class DotnetCoreInstaller { + constructor(version: string) { + this.versionInfo = new DotNetVersionInfo(version); + this.cachedToolName = 'dncs'; + this.arch = 'x64'; + } + + public async installDotnet(): Promise { + // Check cache + let toolPath: string = ''; + let osSuffixes = await this.detectMachineOS(); + let parts = osSuffixes[0].split('-'); + if (parts.length > 1) { + this.arch = parts[1]; + } + + // If version is not generic -> look up cache + if (this.versionInfo.isExactVersion()) + toolPath = this.getLocalTool(this.versionInfo.version()); + + if (!toolPath) { + // download, extract, cache + console.log('Getting a download url', this.versionInfo.version()); + let resolvedVersionInfo = await this.resolveInfos( + osSuffixes, + this.versionInfo + ); + + //Check if cache exists for resolved version + toolPath = this.getLocalTool(resolvedVersionInfo.resolvedVersion); + if (!toolPath) { + //If not exists install it + toolPath = await this.downloadAndInstall(resolvedVersionInfo); + } else { + console.log('Using cached tool'); + } + } else { + console.log('Using cached tool'); + } + + // Need to set this so that .NET Core global tools find the right locations. + core.exportVariable('DOTNET_ROOT', toolPath); + + // Prepend the tools path. instructs the agent to prepend for future tasks + core.addPath(toolPath); + + return toolPath; + } + + private getLocalTool(version: string): string { + console.log('Checking tool cache', version); + return tc.find(this.cachedToolName, version, this.arch); + } + + private async detectMachineOS(): Promise { + let osSuffix: string[] = []; + let output = ''; + + let resultCode = 0; + if (IS_WINDOWS) { + let escapedScript = path + .join(__dirname, '..', 'externals', 'get-os-platform.ps1') + .replace(/'/g, "''"); + let command = `& '${escapedScript}'`; + + const powershellPath = await io.which('powershell', true); + resultCode = await exec.exec( + `"${powershellPath}"`, + [ + '-NoLogo', + '-Sta', + '-NoProfile', + '-NonInteractive', + '-ExecutionPolicy', + 'Unrestricted', + '-Command', + command + ], + { + listeners: { + stdout: (data: Buffer) => { + output += data.toString(); + } + } + } + ); + } else { + let scriptPath = path.join( + __dirname, + '..', + 'externals', + 'get-os-distro.sh' + ); + chmodSync(scriptPath, '777'); + + const toolPath = await io.which(scriptPath, true); + resultCode = await exec.exec(`"${toolPath}"`, [], { + listeners: { + stdout: (data: Buffer) => { + output += data.toString(); + } + } + }); + } + + if (resultCode != 0) { + throw `Failed to detect os with result code ${resultCode}. Output: ${output}`; + } + + let index; + if ((index = output.indexOf('Primary:')) >= 0) { + let primary = output.substr(index + 'Primary:'.length).split(os.EOL)[0]; + osSuffix.push(primary); + } + + if ((index = output.indexOf('Legacy:')) >= 0) { + let legacy = output.substr(index + 'Legacy:'.length).split(os.EOL)[0]; + osSuffix.push(legacy); + } + + if (osSuffix.length == 0) { + throw 'Could not detect platform'; + } + + return osSuffix; + } + + private async downloadAndInstall(resolvedVersionInfo: ResolvedVersionInfo) { + let downloaded = false; + let downloadPath = ''; + for (const url of resolvedVersionInfo.downloadUrls) { + try { + downloadPath = await tc.downloadTool(url); + downloaded = true; + break; + } catch (error) { + console.log('Could not Download', url, JSON.stringify(error)); + } + } + + if (!downloaded) { + throw 'Failed to download package'; + } + + // extract + console.log('Extracting Package', downloadPath); + let extPath: string = IS_WINDOWS + ? await tc.extractZip(downloadPath) + : await tc.extractTar(downloadPath); + + // cache tool + console.log('Caching tool'); + let cachedDir = await tc.cacheDir( + extPath, + this.cachedToolName, + resolvedVersionInfo.resolvedVersion, + this.arch + ); + + console.log('Successfully installed', resolvedVersionInfo.resolvedVersion); + return cachedDir; + } + + // OsSuffixes - The suffix which is a part of the file name ex- linux-x64, windows-x86 + // Type - SDK / Runtime + // versionInfo - versionInfo of the SDK/Runtime + async resolveInfos( + osSuffixes: string[], + versionInfo: DotNetVersionInfo + ): Promise { + const httpClient = new hc.HttpClient('actions/setup-dotnet', [], { + allowRetries: true, + maxRetries: 3 + }); + + const releasesJsonUrl: string = await this.getReleasesJsonUrl( + httpClient, + versionInfo.version().split('.') + ); + + const releasesResponse = await httpClient.getJson(releasesJsonUrl); + const releasesResult = releasesResponse.result || {}; + let releasesInfo: any[] = releasesResult['releases']; + releasesInfo = releasesInfo.filter((releaseInfo: any) => { + return ( + semver.satisfies( + releaseInfo['sdk']['version'], + versionInfo.version() + ) || + semver.satisfies( + releaseInfo['sdk']['version-display'], + versionInfo.version() + ) + ); + }); + + // Exclude versions that are newer than the latest if using not exact + if (!versionInfo.isExactVersion()) { + let latestSdk: string = releasesResult['latest-sdk']; + + releasesInfo = releasesInfo.filter((releaseInfo: any) => + semver.lte(releaseInfo['sdk']['version'], latestSdk) + ); + } + + // Sort for latest version + releasesInfo = releasesInfo.sort((a, b) => + semver.rcompare(a['sdk']['version'], b['sdk']['version']) + ); + + let downloadedVersion: string = ''; + let downloadUrls: string[] = []; + + if (releasesInfo.length != 0) { + let release = releasesInfo[0]; + + downloadedVersion = release['sdk']['version']; + + let files: any[] = release['sdk']['files']; + files = files.filter((file: any) => { + if (file['rid'] == osSuffixes[0] || file['rid'] == osSuffixes[1]) { + return ( + file['url'].endsWith('.zip') || file['url'].endsWith('.tar.gz') + ); + } + }); + + if (files.length > 0) { + files.forEach((file: any) => { + downloadUrls.push(file['url']); + }); + } else { + throw `The specified version's download links are not correctly formed in the supported versions document => ${releasesJsonUrl}`; + } + } else { + console.log( + `Could not fetch download information for version ${versionInfo.version()}` + ); + + if (versionInfo.isExactVersion()) { + console.log('Using fallback'); + + downloadUrls = await this.getFallbackDownloadUrls( + versionInfo.version() + ); + downloadedVersion = versionInfo.version(); + } else { + console.log('Unable to use fallback, version is generic!'); + } + } + + if (downloadUrls.length == 0) { + throw `Could not construct download URL. Please ensure that specified version ${versionInfo.version()}/${downloadedVersion} is valid.`; + } + + core.debug(`Got download urls ${downloadUrls}`); + + return new ResolvedVersionInfo(downloadUrls, downloadedVersion); + } + + private async getReleasesJsonUrl( + httpClient: hc.HttpClient, + versionParts: string[] + ): Promise { + const response = await httpClient.getJson(DotNetCoreIndexUrl); + const result = response.result || {}; + let releasesInfo: any[] = result['releases-index']; + releasesInfo = releasesInfo.filter((info: any) => { + // channel-version is the first 2 elements of the version (e.g. 2.1), filter out versions that don't match 2.1.x. + const sdkParts: string[] = info['channel-version'].split('.'); + if (versionParts.length >= 2 && versionParts[1] != 'x') { + return versionParts[0] == sdkParts[0] && versionParts[1] == sdkParts[1]; + } + return versionParts[0] == sdkParts[0]; + }); + if (releasesInfo.length === 0) { + throw `Could not find info for version ${versionParts.join( + '.' + )} at ${DotNetCoreIndexUrl}`; + } + return releasesInfo[0]['releases.json']; + } + + private async getFallbackDownloadUrls(version: string): Promise { + let primaryUrlSearchString: string; + let legacyUrlSearchString: string; + let output = ''; + let resultCode = 0; + + if (IS_WINDOWS) { + let escapedScript = path + .join(__dirname, '..', 'externals', 'install-dotnet.ps1') + .replace(/'/g, "''"); + let command = `& '${escapedScript}' -Version ${version} -DryRun`; + + const powershellPath = await io.which('powershell', true); + resultCode = await exec.exec( + `"${powershellPath}"`, + [ + '-NoLogo', + '-Sta', + '-NoProfile', + '-NonInteractive', + '-ExecutionPolicy', + 'Unrestricted', + '-Command', + command + ], + { + listeners: { + stdout: (data: Buffer) => { + output += data.toString(); + } + } + } + ); + + primaryUrlSearchString = 'dotnet-install: Primary named payload URL: '; + legacyUrlSearchString = 'dotnet-install: Legacy named payload URL: '; + } else { + let escapedScript = path + .join(__dirname, '..', 'externals', 'install-dotnet.sh') + .replace(/'/g, "''"); + chmodSync(escapedScript, '777'); + + const scriptPath = await io.which(escapedScript, true); + resultCode = await exec.exec( + `"${scriptPath}"`, + ['--version', version, '--dry-run'], + { + listeners: { + stdout: (data: Buffer) => { + output += data.toString(); + } + } + } + ); + + primaryUrlSearchString = 'dotnet-install: Primary named payload URL: '; + legacyUrlSearchString = 'dotnet-install: Legacy named payload URL: '; + } + + if (resultCode != 0) { + throw `Failed to get download urls with result code ${resultCode}. ${output}`; + } + + let primaryUrl: string = ''; + let legacyUrl: string = ''; + if (!!output && output.length > 0) { + let lines: string[] = output.split(os.EOL); + + // Fallback to \n if initial split doesn't work (not consistent across versions) + if (lines.length === 1) { + lines = output.split('\n'); + } + if (!!lines && lines.length > 0) { + lines.forEach((line: string) => { + if (!line) { + return; + } + var primarySearchStringIndex = line.indexOf(primaryUrlSearchString); + if (primarySearchStringIndex > -1) { + primaryUrl = line.substring( + primarySearchStringIndex + primaryUrlSearchString.length + ); + return; + } + + var legacySearchStringIndex = line.indexOf(legacyUrlSearchString); + if (legacySearchStringIndex > -1) { + legacyUrl = line.substring( + legacySearchStringIndex + legacyUrlSearchString.length + ); + return; + } + }); + } + } + + return [primaryUrl, legacyUrl]; + } + + private versionInfo: DotNetVersionInfo; + private cachedToolName: string; + private arch: string; +} + +export class SxSDotnetCoreInstaller { + constructor(toolPaths: Array) { + this.toolPaths = toolPaths; + this.cachedToolName = 'dncs'; + this.arch = 'x64'; + } + + public async setupSxs() { + // create a temp dir + const tempDirectory = process.env['RUNNER_TEMP'] || ''; + const dest = path.join(tempDirectory, uuidV4()); + await io.mkdirP(dest); + + console.log(`Setting up SxS .NET SDK installation in ${dest}...`); + + // copy all the SDK versions into a temporary SxS directory + for (var toolPath of this.toolPaths) { + console.log(`Setting up .NET SDK from ${toolPath}...`); + let entries = readdirSync(toolPath); + for (var entry of entries) { + await io.cp(path.join(toolPath, entry), dest, { + recursive: true, + force: true + }); + } + } + + // cache SxS directory as a tool + let cachedDir = await tc.cacheDir( + dest, + this.cachedToolName, + 'sxs', + this.arch + ); + + console.log(`SxS .NET SDK installation in ${cachedDir}`); + + // Need to set this so that .NET Core global tools find the right locations. + core.exportVariable('DOTNET_ROOT', cachedDir); + + // Prepend the tools path. instructs the agent to prepend for future tasks + core.addPath(cachedDir); + } + + private toolPaths: Array; + private cachedToolName: string; + private arch: string; +} + +const DotNetCoreIndexUrl: string = + 'https://dotnetcli.blob.core.windows.net/dotnet/release-metadata/releases-index.json'; diff --git a/src/setup-dotnet.ts b/src/setup-dotnet.ts index 1637fa5..a69c1e3 100644 --- a/src/setup-dotnet.ts +++ b/src/setup-dotnet.ts @@ -34,12 +34,14 @@ export async function run() { let versions = version.split(','); console.log(`Specified .NET verions: ${versions}`); for (var currentVersion of versions) { - console.log(`Installing .NET SDK ${currentVersion}...`) - const dotnetInstaller = new installer.DotnetCoreInstaller(currentVersion); + console.log(`Installing .NET SDK ${currentVersion}...`); + const dotnetInstaller = new installer.DotnetCoreInstaller( + currentVersion + ); toolPaths.push(await dotnetInstaller.installDotnet()); } if (toolPaths.length > 0) { - console.log(`Setting up SxS .NET SDK versions...`) + console.log(`Setting up SxS .NET SDK versions...`); const sxsInstall = new installer.SxSDotnetCoreInstaller(toolPaths); await sxsInstall.setupSxs(); }