From 7d9b33fefac413e7cafcb1c174e369452f8f73c3 Mon Sep 17 00:00:00 2001 From: Enrico Campidoglio Date: Fri, 26 May 2023 13:41:52 +0200 Subject: [PATCH 1/3] Fetches the latest Cake version number from GitHub --- __tests__/cakeRelease.test.ts | 112 ++++++++++++++++++++++++++++++++++ package-lock.json | 1 + package.json | 1 + src/cakeRelease.ts | 26 ++++++++ 4 files changed, 140 insertions(+) create mode 100644 __tests__/cakeRelease.test.ts create mode 100644 src/cakeRelease.ts diff --git a/__tests__/cakeRelease.test.ts b/__tests__/cakeRelease.test.ts new file mode 100644 index 0000000..b189670 --- /dev/null +++ b/__tests__/cakeRelease.test.ts @@ -0,0 +1,112 @@ +import * as http from '@actions/http-client'; +import * as cakeRelease from '../src/cakeRelease'; + +describe('When retrieving the latest Cake version', () => { + beforeAll(async () => { + jest + .spyOn(http.HttpClient.prototype, 'getJson') + .mockImplementation(async () => ({ + statusCode: 200, + result: { + tag_name: 'v1.0.0' + }, + headers: {} + })); + }); + + test('it should return the latest version number from GitHub', async () => { + expect(await cakeRelease.getLatestVersion()).toBe('1.0.0'); + }); +}); + +describe('When retrieving the latest Cake version without the \'v\' prefix', () => { + beforeAll(async () => { + jest + .spyOn(http.HttpClient.prototype, 'getJson') + .mockImplementation(async () => ({ + statusCode: 200, + result: { + tag_name: '1.0.0' + }, + headers: {} + })); + }); + + test('it should return the latest version number from GitHub', async () => { + expect(await cakeRelease.getLatestVersion()).toBe('1.0.0'); + }); +}); + +describe('When failing to retrieve the latest Cake version due to a GitHub error', () => { + beforeAll(async () => { + jest + .spyOn(http.HttpClient.prototype, 'getJson') + .mockImplementation(async () => ({ + statusCode: 500, + result: {}, + headers: {} + })); + }); + + test('it should return null', async () => { + expect(await cakeRelease.getLatestVersion()).toBeNull(); + }); + + test('it should log the fact that the GitHub API returned an error', async () => { + const log = jest.spyOn(console, 'log'); + await cakeRelease.getLatestVersion(); + expect(log).toHaveBeenCalledWith('Could not determine the latest version of Cake. GitHub returned status code 500'); + }); +}); + +describe('When failing to retrieve the latest Cake version due to an empty response from GitHub', () => { + beforeAll(async () => { + jest + .spyOn(http.HttpClient.prototype, 'getJson') + .mockImplementation(async () => ({ + statusCode: 200, + result: {}, + headers: {} + })); + }); + + test('it should return null', async () => { + expect(await cakeRelease.getLatestVersion()).toBeNull(); + }); +}); + +describe('When failing to retrieve the latest Cake version due to a missing tag name in the GitHub response', () => { + beforeAll(async () => { + jest + .spyOn(http.HttpClient.prototype, 'getJson') + .mockImplementation(async () => ({ + statusCode: 200, + result: { + tag_name: null + }, + headers: {} + })); + }); + + test('it should return null', async () => { + expect(await cakeRelease.getLatestVersion()).toBeNull(); + }); +}); + +describe('When failing to retrieve the latest Cake version due to an empty tag name in the GitHub response', () => { + beforeAll(async () => { + jest + .spyOn(http.HttpClient.prototype, 'getJson') + .mockImplementation(async () => ({ + statusCode: 200, + result: { + tag_name: '' + }, + headers: {} + })); + }); + + test('it should return null', async () => { + expect(await cakeRelease.getLatestVersion()).toBeNull(); + }); +}); diff --git a/package-lock.json b/package-lock.json index 4d10570..a7e4729 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,7 @@ "dependencies": { "@actions/core": "^1.9.1", "@actions/exec": "^1.0.1", + "@actions/http-client": "^2.0.1", "@actions/io": "^1.0.1" }, "devDependencies": { diff --git a/package.json b/package.json index f8a7bd1..314d0a1 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "dependencies": { "@actions/core": "^1.9.1", "@actions/exec": "^1.0.1", + "@actions/http-client": "^2.0.1", "@actions/io": "^1.0.1" }, "devDependencies": { diff --git a/src/cakeRelease.ts b/src/cakeRelease.ts new file mode 100644 index 0000000..cf240e2 --- /dev/null +++ b/src/cakeRelease.ts @@ -0,0 +1,26 @@ +import * as http from '@actions/http-client'; + +export async function getLatestVersion(): Promise { + const release = await getLatestCakeReleaseFromGitHub(); + return extractVersionNumber(release); +} + +async function getLatestCakeReleaseFromGitHub(): Promise { + const client = new http.HttpClient('cake-build/cake-action'); + const response = await client.getJson('https://api.github.com/repos/cake-build/cake/releases/latest'); + + if (response.statusCode != 200) { + console.log(`Could not determine the latest version of Cake. GitHub returned status code ${response.statusCode}`); + return null; + } + + return response.result; +} + +function extractVersionNumber(release: GitHubRelease | null): string | null { + return release?.tag_name?.replace(/^v/, '') || null; +} + +interface GitHubRelease { + tag_name: string; +} From 2b410afd266a19d378c9de40f2f65bf602558b0c Mon Sep 17 00:00:00 2001 From: Enrico Campidoglio Date: Tue, 16 Apr 2024 14:15:16 +0200 Subject: [PATCH 2/3] Installs the latest version of the Cake.Tool (#55) Earlier, not providing an argument for the `cake-version` input parameter resulted in the Cake.Tool being installed without the `--version` parameter. This is fine in most cases, since in practice it translates into installing the latest version. The case when it stops being fine is when an earlier version of the Cake.Tool is already installed in the tools directory and you're running the action without a version number. In that case, you would expect the action to uninstall the existing version of the Cake.Tool and install the latest one, but what used to happen instead is that the action wouldn't even check what version was installed, since it had no version number to compare with, and it would just use whatever version of the tool was already installed. This commit addresses this corner case by explicitly fetching the latest version of the Cake.Tool from GitHub when no version number is specified by the user. This way, the action is able to compare that version number with whatever may already be installed and act appropriately. --- __tests__/cakeTool.test.ts | 50 ++++++++++++++++++++++++--- dist/index.js | 70 +++++++++++++++++++++++++++++++++++++- src/cakeTool.ts | 3 +- 3 files changed, 116 insertions(+), 7 deletions(-) diff --git a/__tests__/cakeTool.test.ts b/__tests__/cakeTool.test.ts index 139da87..11b35b0 100644 --- a/__tests__/cakeTool.test.ts +++ b/__tests__/cakeTool.test.ts @@ -1,11 +1,13 @@ import * as path from 'path'; import * as dotnet from '../src/dotnet'; import * as cakeTool from '../src/cakeTool'; +import * as cakeRelease from '../src/cakeRelease'; import { ToolsDirectory } from '../src/toolsDirectory'; const targetDirectory = path.join('target', 'directory'); jest.mock('../src/dotnet'); +jest.mock('../src/cakeRelease'); describe('When installing the Cake Tool based on the tool manifest', () => { const fakeRestoreTool = dotnet.restoreLocalTools as jest.MockedFunction; @@ -26,6 +28,11 @@ describe('When installing the Cake Tool based on the tool manifest', () => { describe('When installing the Cake Tool without a version number', () => { const fakeInstallLocalTool = dotnet.installLocalTool as jest.MockedFunction; + const fakeGetLatestVersion = cakeRelease.getLatestVersion as jest.MockedFunction; + + beforeAll(() => { + fakeGetLatestVersion.mockResolvedValue('theLatestVersion'); + }); test('it should install the latest version of the Cake.Tool in the tools directory', async () => { await cakeTool.install(); @@ -33,7 +40,7 @@ describe('When installing the Cake Tool without a version number', () => { 'Cake.Tool', 'dotnet-cake', new ToolsDirectory(), - undefined); + 'theLatestVersion'); }); test('it should install the latest version of the Cake.Tool in the specified target directory', async () => { @@ -43,13 +50,17 @@ describe('When installing the Cake Tool without a version number', () => { 'Cake.Tool', 'dotnet-cake', targetDir, - undefined); + 'theLatestVersion'); }); - }); describe('When installing the latest version of the Cake Tool', () => { const fakeInstallLocalTool = dotnet.installLocalTool as jest.MockedFunction; + const fakeGetLatestVersion = cakeRelease.getLatestVersion as jest.MockedFunction; + + beforeAll(() => { + fakeGetLatestVersion.mockResolvedValue('theLatestVersion'); + }); test('it should install the latest version of the Cake.Tool in the tools directory', async () => { await cakeTool.install(undefined, { version: 'latest' }); @@ -57,7 +68,7 @@ describe('When installing the latest version of the Cake Tool', () => { 'Cake.Tool', 'dotnet-cake', new ToolsDirectory(), - undefined); + 'theLatestVersion'); }); test('it should install the latest version of the Cake.Tool in the specified target directory', async () => { @@ -67,7 +78,7 @@ describe('When installing the latest version of the Cake Tool', () => { 'Cake.Tool', 'dotnet-cake', targetDir, - undefined); + 'theLatestVersion'); }); }); @@ -147,3 +158,32 @@ describe('When failing to install a specific version of the Cake Tool', () => { .toThrow(installError); }); }); + +describe('When failing to retrieve the latest version of the Cake Tool', () => { + const fakeInstallLocalTool = dotnet.installLocalTool as jest.MockedFunction; + const fakeGetLatestVersion = cakeRelease.getLatestVersion as jest.MockedFunction; + + beforeAll(() => { + fakeInstallLocalTool.mockReset(); + fakeGetLatestVersion.mockResolvedValue(null); + }); + + test('it should install the Cake.Tool without a version number in the tools directory', async () => { + await cakeTool.install(); + expect(fakeInstallLocalTool).toHaveBeenCalledWith( + 'Cake.Tool', + 'dotnet-cake', + new ToolsDirectory(), + undefined); + }); + + test('it should install the Cake.Tool without a version number in the specified target directory', async () => { + const targetDir = new ToolsDirectory(targetDirectory); + await cakeTool.install(targetDir); + expect(fakeInstallLocalTool).toHaveBeenCalledWith( + 'Cake.Tool', + 'dotnet-cake', + targetDir, + undefined); + }); +}); diff --git a/dist/index.js b/dist/index.js index 4ecfb68..5762626 100644 --- a/dist/index.js +++ b/dist/index.js @@ -4148,6 +4148,72 @@ class CakeSwitch { exports.CakeSwitch = CakeSwitch; +/***/ }), + +/***/ 3040: +/***/ (function(__unused_webpack_module, exports, __nccwpck_require__) { + +"use strict"; + +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); + __setModuleDefault(result, mod); + return result; +}; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.getLatestVersion = void 0; +const http = __importStar(__nccwpck_require__(6255)); +function getLatestVersion() { + return __awaiter(this, void 0, void 0, function* () { + const release = yield getLatestCakeReleaseFromGitHub(); + return extractVersionNumber(release); + }); +} +exports.getLatestVersion = getLatestVersion; +function getLatestCakeReleaseFromGitHub() { + return __awaiter(this, void 0, void 0, function* () { + const client = new http.HttpClient('cake-build/cake-action'); + const response = yield client.getJson('https://api.github.com/repos/cake-build/cake/releases/latest'); + if (response.statusCode != 200) { + console.log(`Could not determine the latest version of Cake. GitHub returned status code ${response.statusCode}`); + return null; + } + return response.result; + }); +} +function extractVersionNumber(release) { + var _a; + return ((_a = release === null || release === void 0 ? void 0 : release.tag_name) === null || _a === void 0 ? void 0 : _a.replace(/^v/, '')) || null; +} + + /***/ }), /***/ 4574: @@ -4191,7 +4257,9 @@ Object.defineProperty(exports, "__esModule", ({ value: true })); exports.install = void 0; const dotnet = __importStar(__nccwpck_require__(9870)); const toolsDirectory_1 = __nccwpck_require__(6745); +const cakeRelease_1 = __nccwpck_require__(3040); function install(toolsDir, version) { + var _a; return __awaiter(this, void 0, void 0, function* () { switch (version === null || version === void 0 ? void 0 : version.version) { case 'tool-manifest': @@ -4199,7 +4267,7 @@ function install(toolsDir, version) { break; case 'latest': case undefined: - yield installCakeLocalTool(toolsDir); + yield installCakeLocalTool(toolsDir, (_a = yield (0, cakeRelease_1.getLatestVersion)()) !== null && _a !== void 0 ? _a : undefined); break; case 'specific': yield installCakeLocalTool(toolsDir, version.number); diff --git a/src/cakeTool.ts b/src/cakeTool.ts index e2c89e9..6419cfa 100644 --- a/src/cakeTool.ts +++ b/src/cakeTool.ts @@ -1,6 +1,7 @@ import * as dotnet from './dotnet'; import { ToolsDirectory } from './toolsDirectory'; import { CakeVersion } from './action'; +import { getLatestVersion } from './cakeRelease'; export async function install(toolsDir?: ToolsDirectory, version?: CakeVersion) { switch (version?.version) { @@ -9,7 +10,7 @@ export async function install(toolsDir?: ToolsDirectory, version?: CakeVersion) break; case 'latest': case undefined: - await installCakeLocalTool(toolsDir); + await installCakeLocalTool(toolsDir, await getLatestVersion() ?? undefined); break; case 'specific': await installCakeLocalTool(toolsDir, version.number); From f125c6612ea1b6160cc5d918d573b90329a70735 Mon Sep 17 00:00:00 2001 From: Enrico Campidoglio Date: Fri, 26 Apr 2024 11:25:59 +0200 Subject: [PATCH 3/3] Adds an integration test for running with the latest Cake version --- .github/workflows/test.yml | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 1ffe6f6..61328cd 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -57,6 +57,24 @@ jobs: cake-version: tool-manifest script-path: ${{ env.script-directory }}/build.cake target: Test-Cake-Version + - name: Get the latest Cake release from GitHub + id: get-latest-cake-release + uses: octokit/request-action@v2.x + with: + route: GET /repos/cake-build/cake/releases/latest + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Set the EXPECTED_CAKE_VERSION environment variable + shell: bash + run: | + version=$(echo ${{ fromJson(steps.get-latest-cake-release.outputs.data).tag_name }} | sed 's/v//') + echo "EXPECTED_CAKE_VERSION=$version" >> $GITHUB_ENV + - name: Run with the latest Cake version + uses: ./ + with: + cake-version: latest + script-path: ${{ env.script-directory }}/build.cake + target: Test-Cake-Version - name: Run automatic bootstrapping of Cake modules (Cake >= 1.0.0) uses: ./ with: