From dec3f4aae10fa0b42aa72c37eec388fd120f851b Mon Sep 17 00:00:00 2001 From: Franco Stramana Date: Mon, 22 Jan 2024 15:07:06 -0300 Subject: [PATCH] Adds token info --- .github/linters/.eslintrc.yml | 3 +- .github/workflows/test-action.yml | 2 + .prettierrc.json | 2 +- __tests__/index.test.ts | 12 +-- __tests__/main.test.ts | 72 +++++++++--------- dist/index.js | 107 +++++++++++++++++++++++++-- src/app.config.ts | 2 +- src/index.ts | 4 +- src/libs/github-check.ts | 55 -------------- src/main.ts | 61 ++++++++------- src/policies/license-policy-check.ts | 10 +++ src/policies/policy-check.ts | 56 ++++++++++++++ src/services/result.service.ts | 1 + src/utils/github.utils.ts | 33 +++++++++ 14 files changed, 285 insertions(+), 135 deletions(-) delete mode 100644 src/libs/github-check.ts create mode 100644 src/policies/license-policy-check.ts create mode 100644 src/policies/policy-check.ts create mode 100644 src/utils/github.utils.ts diff --git a/.github/linters/.eslintrc.yml b/.github/linters/.eslintrc.yml index 4f85053..a8d186c 100644 --- a/.github/linters/.eslintrc.yml +++ b/.github/linters/.eslintrc.yml @@ -45,6 +45,7 @@ rules: 'no-unused-vars': 'off', 'prettier/prettier': 'error', 'semi': 'off', + 'no-shadow': 'warn', '@typescript-eslint/array-type': 'error', '@typescript-eslint/await-thenable': 'error', '@typescript-eslint/ban-ts-comment': 'error', @@ -76,7 +77,7 @@ rules: '@typescript-eslint/promise-function-async': 'error', '@typescript-eslint/require-array-sort-compare': 'error', '@typescript-eslint/restrict-plus-operands': 'error', - '@typescript-eslint/semi': ['error', 'never'], + '@typescript-eslint/semi': ['error', 'always'], '@typescript-eslint/space-before-function-paren': 'off', '@typescript-eslint/type-annotation-spacing': 'error', '@typescript-eslint/unbound-method': 'error' diff --git a/.github/workflows/test-action.yml b/.github/workflows/test-action.yml index 924fa27..23c1f16 100644 --- a/.github/workflows/test-action.yml +++ b/.github/workflows/test-action.yml @@ -8,6 +8,8 @@ on: permissions: contents: read + pull-requests: write + checks: write jobs: test-action: diff --git a/.prettierrc.json b/.prettierrc.json index c173f6b..bf285aa 100644 --- a/.prettierrc.json +++ b/.prettierrc.json @@ -2,7 +2,7 @@ "printWidth": 120, "tabWidth": 2, "useTabs": false, - "semi": false, + "semi": true, "singleQuote": true, "quoteProps": "as-needed", "jsxSingleQuote": false, diff --git a/__tests__/index.test.ts b/__tests__/index.test.ts index 34a4dfe..93a2183 100644 --- a/__tests__/index.test.ts +++ b/__tests__/index.test.ts @@ -2,16 +2,16 @@ * Unit tests for the action's entrypoint, src/index.ts */ -import * as main from '../src/main' +import * as main from '../src/main'; // Mock the action's entrypoint -const runMock = jest.spyOn(main, 'run').mockImplementation() +const runMock = jest.spyOn(main, 'run').mockImplementation(); describe('index', () => { it('calls run when imported', async () => { // eslint-disable-next-line @typescript-eslint/no-require-imports - require('../src/index') + require('../src/index'); - expect(runMock).toHaveBeenCalled() - }) -}) + expect(runMock).toHaveBeenCalled(); + }); +}); diff --git a/__tests__/main.test.ts b/__tests__/main.test.ts index 51100b8..d1e82aa 100644 --- a/__tests__/main.test.ts +++ b/__tests__/main.test.ts @@ -6,71 +6,71 @@ * variables following the pattern `INPUT_`. */ -import * as core from '@actions/core' -import * as main from '../src/main' +import * as core from '@actions/core'; +import * as main from '../src/main'; // Mock the action's main function -const runMock = jest.spyOn(main, 'run') +const runMock = jest.spyOn(main, 'run'); // Other utilities -const timeRegex = /^\d{2}:\d{2}:\d{2}/ +const timeRegex = /^\d{2}:\d{2}:\d{2}/; // Mock the GitHub Actions core library -let debugMock: jest.SpyInstance -let errorMock: jest.SpyInstance -let getInputMock: jest.SpyInstance -let setFailedMock: jest.SpyInstance -let setOutputMock: jest.SpyInstance +let debugMock: jest.SpyInstance; +let errorMock: jest.SpyInstance; +let getInputMock: jest.SpyInstance; +let setFailedMock: jest.SpyInstance; +let setOutputMock: jest.SpyInstance; describe('action', () => { beforeEach(() => { - jest.clearAllMocks() + jest.clearAllMocks(); - debugMock = jest.spyOn(core, 'debug').mockImplementation() - errorMock = jest.spyOn(core, 'error').mockImplementation() - getInputMock = jest.spyOn(core, 'getInput').mockImplementation() - setFailedMock = jest.spyOn(core, 'setFailed').mockImplementation() - setOutputMock = jest.spyOn(core, 'setOutput').mockImplementation() - }) + debugMock = jest.spyOn(core, 'debug').mockImplementation(); + errorMock = jest.spyOn(core, 'error').mockImplementation(); + getInputMock = jest.spyOn(core, 'getInput').mockImplementation(); + setFailedMock = jest.spyOn(core, 'setFailed').mockImplementation(); + setOutputMock = jest.spyOn(core, 'setOutput').mockImplementation(); + }); it('sets the time output', async () => { // Set the action's inputs as return values from core.getInput() getInputMock.mockImplementation((name: string): string => { switch (name) { case 'milliseconds': - return '500' + return '500'; default: - return '' + return ''; } - }) + }); - await main.run() - expect(runMock).toHaveReturned() + await main.run(); + expect(runMock).toHaveReturned(); // Verify that all of the core library functions were called correctly - expect(debugMock).toHaveBeenNthCalledWith(1, 'Waiting 500 milliseconds ...') - expect(debugMock).toHaveBeenNthCalledWith(2, expect.stringMatching(timeRegex)) - expect(debugMock).toHaveBeenNthCalledWith(3, expect.stringMatching(timeRegex)) - expect(setOutputMock).toHaveBeenNthCalledWith(1, 'time', expect.stringMatching(timeRegex)) - expect(errorMock).not.toHaveBeenCalled() - }) + expect(debugMock).toHaveBeenNthCalledWith(1, 'Waiting 500 milliseconds ...'); + expect(debugMock).toHaveBeenNthCalledWith(2, expect.stringMatching(timeRegex)); + expect(debugMock).toHaveBeenNthCalledWith(3, expect.stringMatching(timeRegex)); + expect(setOutputMock).toHaveBeenNthCalledWith(1, 'time', expect.stringMatching(timeRegex)); + expect(errorMock).not.toHaveBeenCalled(); + }); it('sets a failed status', async () => { // Set the action's inputs as return values from core.getInput() getInputMock.mockImplementation((name: string): string => { switch (name) { case 'milliseconds': - return 'this is not a number' + return 'this is not a number'; default: - return '' + return ''; } - }) + }); - await main.run() - expect(runMock).toHaveReturned() + await main.run(); + expect(runMock).toHaveReturned(); // Verify that all of the core library functions were called correctly - expect(setFailedMock).toHaveBeenNthCalledWith(1, 'milliseconds not a number') - expect(errorMock).not.toHaveBeenCalled() - }) -}) + expect(setFailedMock).toHaveBeenNthCalledWith(1, 'milliseconds not a number'); + expect(errorMock).not.toHaveBeenCalled(); + }); +}); diff --git a/dist/index.js b/dist/index.js index 36f4bb0..6f51b2f 100644 --- a/dist/index.js +++ b/dist/index.js @@ -30150,21 +30150,45 @@ exports.CHECK_NAME = 'SCANOSS Policy Checker'; /***/ }), /***/ 1958: -/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { +/***/ (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; +}; Object.defineProperty(exports, "__esModule", ({ value: true })); exports.GitHubCheck = void 0; const github_1 = __nccwpck_require__(5438); -const core_1 = __nccwpck_require__(2186); +const core = __importStar(__nccwpck_require__(2186)); +const github_utils_1 = __nccwpck_require__(7889); const NO_INITIALIZATE = -1; class GitHubCheck { octokit; // TODO: type from actions/github ? checkName; checkRunId; constructor(checkName) { - const GITHUB_TOKEN = (0, core_1.getInput)('github-token'); // TODO: move to inputs.ts file? + const GITHUB_TOKEN = core.getInput('github-token'); // TODO: move to inputs.ts file? this.octokit = (0, github_1.getOctokit)(GITHUB_TOKEN); this.checkName = checkName; this.checkRunId = NO_INITIALIZATE; @@ -30175,7 +30199,7 @@ class GitHubCheck { owner: github_1.context.repo.owner, repo: github_1.context.repo.repo, name: this.checkName, - head_sha: github_1.context.sha + head_sha: (0, github_utils_1.getSha)() }); this.checkRunId = result.data.id; return result.data; @@ -30239,6 +30263,7 @@ const exec = __importStar(__nccwpck_require__(1514)); const result_service_1 = __nccwpck_require__(2414); const app_config_1 = __nccwpck_require__(9014); const github_check_1 = __nccwpck_require__(1958); +const github_utils_1 = __nccwpck_require__(7889); /** * The main function for the action. * @returns {Promise} Resolves when the action is complete. @@ -30266,12 +30291,17 @@ async function run() { await exec.exec(`docker run -v "${repoDir}":"/scanoss" ghcr.io/scanoss/scanoss-py:v1.9.0 scan . --output ${outputPath}`, [], options); const scannerResults = await (0, result_service_1.readResult)(outputPath); const licenses = (0, result_service_1.getLicenses)(scannerResults); + // get reports + const licenseReport = 'Here are the licenses found:'; + // finish + await check.finish('success', 'Code analysis completed successfully', licenseReport); + if ((0, github_utils_1.isPullRequest)()) { + (0, github_utils_1.createCommentOnPR)(licenseReport); + } // set outputs for other workflow steps to use core.setOutput('licenses', licenses.toString()); core.setOutput('output-command', output); core.setOutput('result-filepath', outputPath); - // finish - await check.finish('success', 'Code analysis completed successfully', 'Here are the licenses found:'); } catch (error) { // fail the workflow run if an error occurs @@ -30340,6 +30370,7 @@ async function readResult(filepath) { } exports.readResult = readResult; function getLicenses(results) { + // { spdxid, copyleft, url } const licenses = new Set(); for (const component of Object.values(results)) { for (const c of component) { @@ -30363,6 +30394,70 @@ function getLicenses(results) { exports.getLicenses = getLicenses; +/***/ }), + +/***/ 7889: +/***/ (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; +}; +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.createCommentOnPR = exports.getSha = exports.isPullRequest = void 0; +const github_1 = __nccwpck_require__(5438); +const core = __importStar(__nccwpck_require__(2186)); +const prEvents = ['pull_request', 'pull_request_review', 'pull_request_review_comment']; +function isPullRequest() { + return prEvents.includes(github_1.context.eventName); +} +exports.isPullRequest = isPullRequest; +function getSha() { + let sha = github_1.context.sha; + if (isPullRequest()) { + const pull = github_1.context.payload.pull_request; + if (pull?.head.sha) { + sha = pull?.head.sha; + } + } + return sha; +} +exports.getSha = getSha; +async function createCommentOnPR(message) { + const GITHUB_TOKEN = core.getInput('github-token'); + const octokit = (0, github_1.getOctokit)(GITHUB_TOKEN); + core.debug('Creating comment on PR'); + octokit.rest.issues.createComment({ + issue_number: github_1.context.issue.number, + owner: github_1.context.repo.owner, + repo: github_1.context.repo.repo, + body: message + }); +} +exports.createCommentOnPR = createCommentOnPR; + + /***/ }), /***/ 9491: diff --git a/src/app.config.ts b/src/app.config.ts index 746bfb8..0281d4d 100644 --- a/src/app.config.ts +++ b/src/app.config.ts @@ -1 +1 @@ -export const CHECK_NAME = 'SCANOSS Policy Checker' +export const CHECK_NAME = 'SCANOSS Policy Checker'; diff --git a/src/index.ts b/src/index.ts index b08f970..8395471 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,7 +1,7 @@ /** * The entrypoint for the action. */ -import { run } from './main' +import { run } from './main'; // eslint-disable-next-line @typescript-eslint/no-floating-promises -run() +run(); diff --git a/src/libs/github-check.ts b/src/libs/github-check.ts deleted file mode 100644 index 00b764a..0000000 --- a/src/libs/github-check.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { context, getOctokit } from '@actions/github' -import { getInput } from '@actions/core' - -const NO_INITIALIZATE = -1 - -export class GitHubCheck { - private octokit // TODO: type from actions/github ? - - private checkName: string - - private checkRunId: number - - constructor(checkName: string) { - const GITHUB_TOKEN = getInput('github-token') // TODO: move to inputs.ts file? - - this.octokit = getOctokit(GITHUB_TOKEN) - this.checkName = checkName - this.checkRunId = NO_INITIALIZATE - } - - async present(): Promise { - // Promise - const result = await this.octokit.rest.checks.create({ - owner: context.repo.owner, - repo: context.repo.repo, - name: this.checkName, - head_sha: context.sha - }) - - this.checkRunId = result.data.id - - return result.data - } - - async finish(conclusion: any, summary: string, text: string): Promise { - // Promise - if (this.checkRunId === NO_INITIALIZATE) - throw new Error(`Error on finish. Check "${this.checkName}" is not created.`) - - const result = await this.octokit.rest.checks.update({ - owner: context.repo.owner, - repo: context.repo.repo, - check_run_id: this.checkRunId, - status: 'completed', - conclusion, - output: { - title: this.checkName, - summary, - text - } - }) - - return result.data - } -} diff --git a/src/main.ts b/src/main.ts index 61db2c3..10a84a0 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,8 +1,8 @@ -import * as core from '@actions/core' -import * as exec from '@actions/exec' -import { getLicenses, readResult } from './services/result.service' -import { CHECK_NAME } from './app.config' -import { GitHubCheck } from './libs/github-check' +import * as core from '@actions/core'; +import * as exec from '@actions/exec'; +import { getLicenses, readResult } from './services/result.service'; +import { createCommentOnPR, isPullRequest } from './utils/github.utils'; +import { LicensePolicyCheck } from './policies/license-policy-check'; /** * The main function for the action. @@ -10,45 +10,52 @@ import { GitHubCheck } from './libs/github-check' */ export async function run(): Promise { try { - const repoDir = process.env.GITHUB_WORKSPACE as string - const outputPath = 'results.json' + const repoDir = process.env.GITHUB_WORKSPACE as string; + const outputPath = 'results.json'; - // init check - const check = new GitHubCheck(CHECK_NAME) - await check.present() + // create policies + const policies = [new LicensePolicyCheck()]; + policies.forEach(policy => policy.start()); - // Declara las opciones para ejecutar el exec - const options: exec.ExecOptions = {} - let output = '' + // options to get standar output + const options: exec.ExecOptions = {}; + let output = ''; options.listeners = { stdout: (data: Buffer) => { - output += data.toString() + output += data.toString(); }, stderr: (data: Buffer) => { - output += data.toString() + output += data.toString(); } - } - options.silent = true + }; + options.silent = true; // run scan await exec.exec( `docker run -v "${repoDir}":"/scanoss" ghcr.io/scanoss/scanoss-py:v1.9.0 scan . --output ${outputPath}`, [], options - ) + ); - const scannerResults = await readResult(outputPath) - const licenses = getLicenses(scannerResults) + const scannerResults = await readResult(outputPath); + const licenses = getLicenses(scannerResults); - // set outputs for other workflow steps to use - core.setOutput('licenses', licenses.toString()) - core.setOutput('output-command', output) - core.setOutput('result-filepath', outputPath) + // get reports + const licenseReport = 'Here are the licenses found:'; + + // run policies // TODO: define run action for each policy + policies.forEach(async (policy) => await policy.run(licenseReport)); - // finish - await check.finish('success', 'Code analysis completed successfully', 'Here are the licenses found:') + if (isPullRequest()) { + createCommentOnPR(licenseReport); + } + + // set outputs for other workflow steps to use + core.setOutput('licenses', licenses.toString()); + core.setOutput('output-command', output); + core.setOutput('result-filepath', outputPath); } catch (error) { // fail the workflow run if an error occurs - if (error instanceof Error) core.setFailed(error.message) + if (error instanceof Error) core.setFailed(error.message); } } diff --git a/src/policies/license-policy-check.ts b/src/policies/license-policy-check.ts new file mode 100644 index 0000000..abe18ed --- /dev/null +++ b/src/policies/license-policy-check.ts @@ -0,0 +1,10 @@ +import { CHECK_NAME } from "src/app.config"; +import { PolicyCheck } from "./policy-check"; + +export class LicensePolicyCheck extends PolicyCheck { + + constructor() { + super(`${CHECK_NAME}: Licenses Policy`); + } + +} \ No newline at end of file diff --git a/src/policies/policy-check.ts b/src/policies/policy-check.ts new file mode 100644 index 0000000..acf064a --- /dev/null +++ b/src/policies/policy-check.ts @@ -0,0 +1,56 @@ +import { context, getOctokit } from '@actions/github'; +import * as core from '@actions/core'; +import { getSHA } from '../utils/github.utils'; + +const NO_INITIALIZATE = -1; + +export abstract class PolicyCheck { + private octokit; // TODO: type from actions/github ? + + private checkName: string; + + private checkRunId: number; + + constructor(checkName: string) { + const GITHUB_TOKEN = core.getInput('github-token'); // TODO: move to inputs.ts file? + + this.octokit = getOctokit(GITHUB_TOKEN); + this.checkName = checkName; + this.checkRunId = NO_INITIALIZATE; + } + + async start(): Promise { + // Promise + const result = await this.octokit.rest.checks.create({ + owner: context.repo.owner, + repo: context.repo.repo, + name: this.checkName, + head_sha: getSHA() + }); + + this.checkRunId = result.data.id; + + return result.data; + } + + async run(text: string): Promise { + // Promise + if (this.checkRunId === NO_INITIALIZATE) + throw new Error(`Error on finish. Check "${this.checkName}" is not created.`); + + const result = await this.octokit.rest.checks.update({ + owner: context.repo.owner, + repo: context.repo.repo, + check_run_id: this.checkRunId, + status: 'completed', + conclusion: 'success', + output: { + title: this.checkName, + summary: 'Policy checker completed successfully', + text + } + }); + + return result.data; + } +} diff --git a/src/services/result.service.ts b/src/services/result.service.ts index 13294ac..003cf10 100644 --- a/src/services/result.service.ts +++ b/src/services/result.service.ts @@ -7,6 +7,7 @@ export async function readResult(filepath: string): Promise { } export function getLicenses(results: ScannerResults): string[] { + // { spdxid, copyleft, url } const licenses = new Set() for (const component of Object.values(results)) { diff --git a/src/utils/github.utils.ts b/src/utils/github.utils.ts new file mode 100644 index 0000000..008bf17 --- /dev/null +++ b/src/utils/github.utils.ts @@ -0,0 +1,33 @@ +import { context, getOctokit } from '@actions/github'; +import * as core from '@actions/core'; + +const prEvents = ['pull_request', 'pull_request_review', 'pull_request_review_comment']; + +export function isPullRequest(): boolean { + return prEvents.includes(context.eventName); +} + +export function getSHA(): string { + let sha = context.sha; + if (isPullRequest()) { + const pull = context.payload.pull_request; + if (pull?.head.sha) { + sha = pull?.head.sha; + } + } + + return sha; +} + +export async function createCommentOnPR(message: string): Promise { + const GITHUB_TOKEN = core.getInput('github-token'); + const octokit = getOctokit(GITHUB_TOKEN); + + core.debug('Creating comment on PR'); + octokit.rest.issues.createComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + body: message + }); +}