diff --git a/github-actions/scan/README.adoc b/github-actions/scan/README.adoc index 28f945113b..e5fd3f57de 100644 --- a/github-actions/scan/README.adoc +++ b/github-actions/scan/README.adoc @@ -142,10 +142,19 @@ npm run test ==== Integration-Test As a precondition to run the integration tests locally you have to +execute `01-start.sh $secHubServerVersion $sechubServerPortNr $pdsVersion $pdsPortN` +inside the integration test folder. -- execute `__test__/01-start.sh $secHubServerVersion $sechubServerPortNr $pdsVersion $pdsPortNr` +An example: -TIP: You can also start a SecHub server and a PDS (both in integration test mode) instead of using the `01-start` script. +[source,bash] +---- +# Next lines will start a SecHub server of version 2.4.0 and a PDS with version 2.1.0 +cd ./github-actions/scan/__test__/integrationtest +./01-start.sh 2.4.0 8443 2.1.0 8444 +---- + +TIP: You can also start a SecHub server and a PDS from IDE (both in integration test mode) instead of using the `01-start` script. After the script has been executed, you can execute integration tests multiple times via following command: @@ -176,7 +185,9 @@ In this setup the tests can be executed from sidebar and from links created insi [TIP] ==== -Unfortunately, the Jest UI integration works only for npm script "test". But to handle integration tests different (the tests shall only be executed when all is build and servers are started) they are not executed by "test" script. +Unfortunately, the Jest UI integration works only for npm script "test". +But to handle integration tests different (the tests shall only be executed +when all is build and servers are started) they are not executed by "test" script. If you want to **debug an integration test**, there is a temporary workaround necessary while you debug the test: diff --git a/github-actions/scan/__test__/output-helper.test.ts b/github-actions/scan/__test__/output-helper.test.ts new file mode 100644 index 0000000000..0fabd3d1fb --- /dev/null +++ b/github-actions/scan/__test__/output-helper.test.ts @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: MIT +import * as outputHelper from '../src/output-helper'; + +import * as fs from 'fs'; +import * as path from 'path'; + +describe('storeOutput', () => { + const outputPath = path.join(__dirname, 'test_output.txt'); + + beforeAll(() => { + process.env.GITHUB_OUTPUT = outputPath; + }); + + afterEach(() => { + if (fs.existsSync(outputPath)) { + fs.unlinkSync(outputPath); + } + }); + + it('should append a line with key=value to the file', () => { + /* execute */ + outputHelper.storeOutput('TEST_KEY', 'TEST_VALUE'); + + /* test */ + const content = fs.readFileSync(outputPath, 'utf8'); + expect(content).toBe('TEST_KEY=TEST_VALUE\n'); + }); + + it('should append multiple lines correctly', () => { + /* execute */ + outputHelper.storeOutput('KEY1', 'VALUE1'); + outputHelper.storeOutput('KEY2', 'VALUE2'); + + /* test */ + const content = fs.readFileSync(outputPath, 'utf8'); + expect(content).toBe('KEY1=VALUE1\nKEY2=VALUE2\n'); + }); + + it('should throw an error if GITHUB_OUTPUT is not set', () => { + /* prepare */ + delete process.env.GITHUB_OUTPUT; + + /* execute + test */ + expect(() => outputHelper.storeOutput('KEY', 'VALUE')).toThrow('GITHUB_OUTPUT environment variable is not set'); + }); +}); \ No newline at end of file diff --git a/github-actions/scan/__test__/post-scan.test.ts b/github-actions/scan/__test__/post-scan.test.ts index 8f1d49ce3e..ada369953b 100644 --- a/github-actions/scan/__test__/post-scan.test.ts +++ b/github-actions/scan/__test__/post-scan.test.ts @@ -1,12 +1,15 @@ // SPDX-License-Identifier: MIT import * as core from '@actions/core'; +import * as outputHelper from '../src/output-helper'; import { collectReportData, reportOutputs } from '../src/post-scan'; import { getReport } from '../src/sechub-cli'; import { LAUNCHER_CONTEXT_DEFAULTS } from '../src/launcher'; jest.mock('@actions/core'); +jest.mock('../src/output-helper'); const mockedCore = core as jest.Mocked; +const mockedOutputHelper = outputHelper as jest.Mocked; jest.mock('../src/sechub-cli'); const mockedGetReport = getReport as jest.MockedFunction; @@ -20,7 +23,7 @@ describe('collectReportData', function () { /* prepare */ const testContext = Object.create(LAUNCHER_CONTEXT_DEFAULTS); - testContext.reportFormats= []; + testContext.reportFormats = []; /* execute */ collectReportData(testContext); @@ -33,7 +36,7 @@ describe('collectReportData', function () { it('format "json" - logs called 1 time , getReport never called', function () { /* prepare */ const testContext = Object.create(LAUNCHER_CONTEXT_DEFAULTS); - testContext.reportFormats= ['json']; + testContext.reportFormats = ['json']; /* execute */ collectReportData(testContext); @@ -47,8 +50,8 @@ describe('collectReportData', function () { /* prepare */ const testContext = Object.create(LAUNCHER_CONTEXT_DEFAULTS); - testContext.reportFormats= ['json','html']; - testContext.jobUUID=1234; // necessary for download + testContext.reportFormats = ['json', 'html']; + testContext.jobUUID = 1234; // necessary for download collectReportData(testContext); @@ -61,8 +64,8 @@ describe('collectReportData', function () { /* prepare */ const testContext = Object.create(LAUNCHER_CONTEXT_DEFAULTS); - testContext.reportFormats= ['json','html']; - testContext.jobUUID=1234; // necessary for download + testContext.reportFormats = ['json', 'html']; + testContext.jobUUID = 1234; // necessary for download /* execute */ collectReportData(testContext); @@ -78,11 +81,11 @@ describe('collectReportData', function () { /* prepare */ const testContext = Object.create(LAUNCHER_CONTEXT_DEFAULTS); - testContext.reportFormats= ['json','html','xyz','bla']; - testContext.jobUUID=1234; // necessary for download - + testContext.reportFormats = ['json', 'html', 'xyz', 'bla']; + testContext.jobUUID = 1234; // necessary for download + fsMock.readFileSync = jest.fn(() => '{"test": "test"}'); // Mock an empty JSON report - const sampleJson = {'test': 'test'}; + const sampleJson = { 'test': 'test' }; /* execute */ collectReportData(testContext); @@ -90,7 +93,7 @@ describe('collectReportData', function () { /* test */ expect(mockedCore.info).toHaveBeenCalledTimes(4); // "json, html, xyz, bla" - 4 times logged (valid format check is not done here) expect(mockedGetReport).toHaveBeenCalledTimes(3); // we fetch not json via getReport again (already done before), so only "html, xyz, bla" used - + expect(testContext.secHubReportJsonObject).toEqual(sampleJson); // json object is available }); @@ -136,13 +139,13 @@ describe('reportOutputs', function () { /* test */ expect(mockedCore.debug).toHaveBeenCalledTimes(6); - expect(mockedCore.setOutput).toHaveBeenCalledTimes(6); - expect(mockedCore.setOutput).toBeCalledWith('scan-trafficlight', 'RED'); - expect(mockedCore.setOutput).toBeCalledWith('scan-findings-count', '2'); - expect(mockedCore.setOutput).toBeCalledWith('scan-findings-high', '1'); - expect(mockedCore.setOutput).toBeCalledWith('scan-findings-medium', '0'); - expect(mockedCore.setOutput).toBeCalledWith('scan-findings-low', '1'); - expect(mockedCore.setOutput).toBeCalledWith('scan-readable-summary', 'SecHub reported traffic light color RED with 2 findings, categorized as follows: HIGH (1), LOW (1)'); + expect(mockedOutputHelper.storeOutput).toHaveBeenCalledTimes(6); + expect(mockedOutputHelper.storeOutput).toBeCalledWith('scan-trafficlight', 'RED'); + expect(mockedOutputHelper.storeOutput).toBeCalledWith('scan-findings-count', '2'); + expect(mockedOutputHelper.storeOutput).toBeCalledWith('scan-findings-high', '1'); + expect(mockedOutputHelper.storeOutput).toBeCalledWith('scan-findings-medium', '0'); + expect(mockedOutputHelper.storeOutput).toBeCalledWith('scan-findings-low', '1'); + expect(mockedOutputHelper.storeOutput).toBeCalledWith('scan-readable-summary', 'SecHub reported traffic light color RED with 2 findings, categorized as follows: HIGH (1), LOW (1)'); }); it('calls set github output with correct values when JSON report did not exist', function () { @@ -152,13 +155,13 @@ describe('reportOutputs', function () { /* test */ expect(mockedCore.debug).toHaveBeenCalledTimes(7); expect(mockedCore.debug).toBeCalledWith('No findings reported to be categorized.'); - expect(mockedCore.setOutput).toHaveBeenCalledTimes(6); - expect(mockedCore.setOutput).toBeCalledWith('scan-trafficlight', 'FAILURE'); - expect(mockedCore.setOutput).toBeCalledWith('scan-findings-count', '0'); - expect(mockedCore.setOutput).toBeCalledWith('scan-findings-high', '0'); - expect(mockedCore.setOutput).toBeCalledWith('scan-findings-medium', '0'); - expect(mockedCore.setOutput).toBeCalledWith('scan-findings-low', '0'); - expect(mockedCore.setOutput).toBeCalledWith('scan-readable-summary', 'SecHub scan could not be executed.'); + expect(mockedOutputHelper.storeOutput).toHaveBeenCalledTimes(6); + expect(mockedOutputHelper.storeOutput).toBeCalledWith('scan-trafficlight', 'FAILURE'); + expect(mockedOutputHelper.storeOutput).toBeCalledWith('scan-findings-count', '0'); + expect(mockedOutputHelper.storeOutput).toBeCalledWith('scan-findings-high', '0'); + expect(mockedOutputHelper.storeOutput).toBeCalledWith('scan-findings-medium', '0'); + expect(mockedOutputHelper.storeOutput).toBeCalledWith('scan-findings-low', '0'); + expect(mockedOutputHelper.storeOutput).toBeCalledWith('scan-readable-summary', 'SecHub scan could not be executed.'); }); it('calls set github output with correct values when traffic light is green without findings.', function () { @@ -180,13 +183,13 @@ describe('reportOutputs', function () { /* test */ expect(mockedCore.debug).toHaveBeenCalledTimes(6); - expect(mockedCore.setOutput).toHaveBeenCalledTimes(6); - expect(mockedCore.setOutput).toBeCalledWith('scan-trafficlight', 'GREEN'); - expect(mockedCore.setOutput).toBeCalledWith('scan-findings-count', '0'); - expect(mockedCore.setOutput).toBeCalledWith('scan-findings-high', '0'); - expect(mockedCore.setOutput).toBeCalledWith('scan-findings-medium', '0'); - expect(mockedCore.setOutput).toBeCalledWith('scan-findings-low', '0'); - expect(mockedCore.setOutput).toBeCalledWith('scan-readable-summary', 'SecHub reported traffic light color GREEN without findings'); + expect(mockedOutputHelper.storeOutput).toHaveBeenCalledTimes(6); + expect(mockedOutputHelper.storeOutput).toBeCalledWith('scan-trafficlight', 'GREEN'); + expect(mockedOutputHelper.storeOutput).toBeCalledWith('scan-findings-count', '0'); + expect(mockedOutputHelper.storeOutput).toBeCalledWith('scan-findings-high', '0'); + expect(mockedOutputHelper.storeOutput).toBeCalledWith('scan-findings-medium', '0'); + expect(mockedOutputHelper.storeOutput).toBeCalledWith('scan-findings-low', '0'); + expect(mockedOutputHelper.storeOutput).toBeCalledWith('scan-readable-summary', 'SecHub reported traffic light color GREEN without findings'); }); }); diff --git a/github-actions/scan/dist/index.js b/github-actions/scan/dist/index.js index da1b79d485..aaf960bcaa 100644 --- a/github-actions/scan/dist/index.js +++ b/github-actions/scan/dist/index.js @@ -28403,6 +28403,7 @@ function getReport(jobUUID, reportFormat, context) { } ;// CONCATENATED MODULE: ./src/json-helper.ts +// SPDX-License-Identifier: MIT /** * Reads the given field from JSON. @@ -28428,6 +28429,27 @@ function getFieldFromJson(field, jsonData) { return currentKey; } +;// CONCATENATED MODULE: ./src/github-output.ts +// SPDX-License-Identifier: MIT + +const NEW_LINE_SEPARATOR = '\n'; +/** + * Sets the value of an output variable for the GitHub Action. + * This method is a replacement of usage of core.setOutput(..) method. + * There were problems with core.setOutput(...), see + * - https://github.com/mercedes-benz/sechub/issues/3481#issuecomment-2539015176 and + * - https://github.com/actions/toolkit/issues/1218 + * + */ +function storeOutput(field, value) { + const filePath = process.env['GITHUB_OUTPUT'] || ''; + if (filePath) { + } + else { + core.setOutput(field, value); + } +} + ;// CONCATENATED MODULE: ./src/post-scan.ts // SPDX-License-Identifier: MIT @@ -28439,7 +28461,8 @@ function getFieldFromJson(field, jsonData) { -const NEW_LINE_SEPARATOR = '\n'; + +const post_scan_NEW_LINE_SEPARATOR = '\n'; /** * Collect all necessary report data, downloads additional report formats (e.g. 'html') if necessary */ @@ -28513,7 +28536,7 @@ async function uploadArtifact(context, name, files) { if (core.isDebug()) { const filesInWorkspace = (0,external_child_process_.execFileSync)('ls', [rootDirectory], { encoding: 'utf-8' - }).split(NEW_LINE_SEPARATOR); + }).split(post_scan_NEW_LINE_SEPARATOR); for (const fileName of filesInWorkspace) { core.debug(fileName); } @@ -28536,7 +28559,7 @@ function resolveReportNameForScanJob(context) { const workspaceDir = sanitize(getWorkspaceDir()); const filesInWorkspace = (0,external_child_process_.execFileSync)('ls', [workspaceDir], { encoding: 'utf-8' - }).split(NEW_LINE_SEPARATOR); + }).split(post_scan_NEW_LINE_SEPARATOR); if (!context.jobUUID) { core.error('Illegal state: No job uuid resolved - not allowed at this point'); return ''; @@ -28662,7 +28685,7 @@ function buildSummary(trafficLight, totalFindings, findings) { function setOutput(field, value, dataFormat) { value = value !== null && value !== void 0 ? value : (dataFormat === 'number' ? 0 : 'FAILURE'); core.debug(`Output ${field} set to ${value}`); - core.setOutput(field, value.toString()); // Ensure value is converted to a string as GitHub Actions expects output variables to be strings. + storeOutput(field, value.toString()); // Ensure value is converted to a string as GitHub Actions expects output variables to be strings. } ;// CONCATENATED MODULE: ./src/projectname-resolver.ts @@ -28702,6 +28725,7 @@ function projectname_resolver_asJsonObject(text) { // EXTERNAL MODULE: external "os" var external_os_ = __nccwpck_require__(2037); ;// CONCATENATED MODULE: ./src/platform-helper.ts +// SPDX-License-Identifier: MIT function getPlatform() { return external_os_.platform(); @@ -46017,6 +46041,7 @@ const { parseHTML: esm_parseHTML } = static_namespaceObject; const { root: esm_root } = static_namespaceObject; //# sourceMappingURL=index.js.map ;// CONCATENATED MODULE: ./src/client-version-helper.ts +// SPDX-License-Identifier: MIT diff --git a/github-actions/scan/src/main.ts b/github-actions/scan/src/main.ts index f4cb84aa3f..4cf6dbc799 100644 --- a/github-actions/scan/src/main.ts +++ b/github-actions/scan/src/main.ts @@ -6,7 +6,7 @@ import { handleError } from './action-helper'; main().catch(handleError); async function main(): Promise { - // Seperated launcher and main method. + // Separated launcher and main method. // Reason: launch mechanism would be loaded on imports // before we can handle mocking in integration tests! await launch(); diff --git a/github-actions/scan/src/output-helper.ts b/github-actions/scan/src/output-helper.ts new file mode 100644 index 0000000000..d3ef3dcc72 --- /dev/null +++ b/github-actions/scan/src/output-helper.ts @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: MIT + +import * as fs from 'fs'; + +/** + * Sets the value of an output variable for the GitHub Action. + * This method is a replacement of usage of core.setOutput(..) method. + * There were problems with core.setOutput(...), see + * - https://github.com/mercedes-benz/sechub/issues/3481#issuecomment-2539015176 and + * - https://github.com/actions/toolkit/issues/1218 + * + */ +export function storeOutput(field: string, value: string) { + const outputFilePath = process.env['GITHUB_OUTPUT'] || ''; + + if (!outputFilePath) { + throw new Error('GITHUB_OUTPUT environment variable is not set'); + } + + const outputLine = `${field}=${value}\n`; + + fs.appendFileSync(outputFilePath, outputLine, { encoding: 'utf8' }); +} diff --git a/github-actions/scan/src/post-scan.ts b/github-actions/scan/src/post-scan.ts index e7ffa5fca6..3442bf097c 100644 --- a/github-actions/scan/src/post-scan.ts +++ b/github-actions/scan/src/post-scan.ts @@ -8,8 +8,9 @@ import { LaunchContext } from './launcher'; import { logExitCode } from './exitcode'; import { getReport } from './sechub-cli'; import { getFieldFromJson } from './json-helper'; -import { execFileSync } from "child_process"; -import { sanitize } from "./shell-arg-sanitizer"; +import { execFileSync } from 'child_process'; +import { sanitize } from './shell-arg-sanitizer'; +import { storeOutput } from './output-helper'; const NEW_LINE_SEPARATOR = '\n'; @@ -280,5 +281,5 @@ function setOutput(field: string, value: any, dataFormat: string) { value = value ?? (dataFormat === 'number' ? 0 : 'FAILURE'); core.debug(`Output ${field} set to ${value}`); - core.setOutput(field, value.toString()); // Ensure value is converted to a string as GitHub Actions expects output variables to be strings. + storeOutput(field, value.toString()); // Ensure value is converted to a string as GitHub Actions expects output variables to be strings. }