Skip to content

Commit d686854

Browse files
ronens88claude
andcommitted
test: add unit tests for SBOM summary + extract into testable module
Extract parseSBOMEntries and writeSBOMSummary into sbom-summary.js so they can be unit-tested independently. Add 22 tests covering: - Parsing with/without SBOM entries, multiple entries, edge cases - Summary rendering (singular/plural, CycloneDX-only, SPDX-only) - Backward compatibility with older cimon versions (no SBOM output) - Binary/garbage output handling Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 37b7ebb commit d686854

5 files changed

Lines changed: 547 additions & 99 deletions

File tree

dist/post/index.js

Lines changed: 97 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -3520,6 +3520,8 @@ __nccwpck_require__.a(__webpack_module__, async (__webpack_handle_async_dependen
35203520
/* harmony import */ var _actions_exec__WEBPACK_IMPORTED_MODULE_1__ = __nccwpck_require__(514);
35213521
/* harmony import */ var fs__WEBPACK_IMPORTED_MODULE_2__ = __nccwpck_require__(147);
35223522
/* harmony import */ var _actions_http_client__WEBPACK_IMPORTED_MODULE_3__ = __nccwpck_require__(255);
3523+
/* harmony import */ var _sbom_summary_js__WEBPACK_IMPORTED_MODULE_4__ = __nccwpck_require__(21);
3524+
35233525

35243526

35253527

@@ -3562,6 +3564,11 @@ async function sudoExists() {
35623564
* Determines the Cimon executable path.
35633565
* Prefers the release-path saved by the main step so the same binary
35643566
* is used for both start and stop.
3567+
*
3568+
* Backward compatibility: when the main step was run by an older version
3569+
* of cimon-action that did not call core.saveState('release-path', ...),
3570+
* core.getState() returns '' and we fall back to the default S3-downloaded
3571+
* binary at /tmp/cimon/cimon — exactly the same behavior as before.
35653572
*/
35663573
function getCimonPath() {
35673574
const savedPath = _actions_core__WEBPACK_IMPORTED_MODULE_0__.getState('release-path');
@@ -3571,53 +3578,6 @@ function getCimonPath() {
35713578
return CIMON_EXECUTABLE_PATH;
35723579
}
35733580

3574-
/**
3575-
* Parses SBOM file entries from cimon agent stop output.
3576-
* Returns an array of {cyclonedx, spdx} objects.
3577-
*/
3578-
function parseSBOMEntries(output) {
3579-
const entries = [];
3580-
for (const line of output.split('\n')) {
3581-
if (!line.includes('"SBOM files written"')) continue;
3582-
try {
3583-
const parsed = JSON.parse(line);
3584-
if (parsed.cyclonedx || parsed.spdx) {
3585-
entries.push({
3586-
cyclonedx: parsed.cyclonedx || '',
3587-
spdx: parsed.spdx || '',
3588-
});
3589-
}
3590-
} catch {
3591-
// Not valid JSON, skip.
3592-
}
3593-
}
3594-
return entries;
3595-
}
3596-
3597-
/**
3598-
* Builds a GitHub Actions job summary with SBOM results.
3599-
*/
3600-
async function writeSBOMSummary(sbomEntries) {
3601-
if (sbomEntries.length === 0) return;
3602-
3603-
const rows = [['Format', 'Path']];
3604-
for (const entry of sbomEntries) {
3605-
if (entry.cyclonedx) {
3606-
rows.push(['CycloneDX', `\`${entry.cyclonedx}\``]);
3607-
}
3608-
if (entry.spdx) {
3609-
rows.push(['SPDX', `\`${entry.spdx}\``]);
3610-
}
3611-
}
3612-
3613-
await _actions_core__WEBPACK_IMPORTED_MODULE_0__.summary.addHeading('Cimon SBOM Report', 2)
3614-
.addRaw(
3615-
`**${sbomEntries.length}** SBOM${sbomEntries.length > 1 ? 's' : ''} generated during build.\n\n`
3616-
)
3617-
.addTable(rows)
3618-
.write();
3619-
}
3620-
36213581
async function run(config) {
36223582
const cimonPath = getCimonPath();
36233583

@@ -3677,11 +3637,11 @@ async function run(config) {
36773637
}
36783638

36793639
// Parse and display SBOM summary regardless of stop exit code.
3680-
const sbomEntries = parseSBOMEntries(stopOutput);
3640+
const sbomEntries = (0,_sbom_summary_js__WEBPACK_IMPORTED_MODULE_4__/* .parseSBOMEntries */ .w)(stopOutput);
36813641
if (sbomEntries.length > 0) {
36823642
const reportJobSummary = _actions_core__WEBPACK_IMPORTED_MODULE_0__.getBooleanInput('report-job-summary');
36833643
if (reportJobSummary) {
3684-
await writeSBOMSummary(sbomEntries);
3644+
await (0,_sbom_summary_js__WEBPACK_IMPORTED_MODULE_4__/* .writeSBOMSummary */ .k)(_actions_core__WEBPACK_IMPORTED_MODULE_0__, sbomEntries);
36853645
}
36863646
}
36873647

@@ -3711,6 +3671,77 @@ try {
37113671
__webpack_handle_async_dependencies__();
37123672
}, 1);
37133673

3674+
/***/ }),
3675+
3676+
/***/ 21:
3677+
/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __nccwpck_require__) => {
3678+
3679+
/* harmony export */ __nccwpck_require__.d(__webpack_exports__, {
3680+
/* harmony export */ "w": () => (/* binding */ parseSBOMEntries),
3681+
/* harmony export */ "k": () => (/* binding */ writeSBOMSummary)
3682+
/* harmony export */ });
3683+
/**
3684+
* Pure helpers for parsing and displaying SBOM results in the
3685+
* GitHub Actions job summary. Extracted so they can be unit-tested
3686+
* independently of the @actions/* runtime.
3687+
*/
3688+
3689+
/**
3690+
* Parses SBOM file entries from cimon agent stop output.
3691+
* Each JSON log line that contains `"SBOM files written"` is expected
3692+
* to carry `cyclonedx` and/or `spdx` path fields.
3693+
*
3694+
* @param {string} output - Combined stdout+stderr from `cimon agent stop`.
3695+
* @returns {Array<{cyclonedx: string, spdx: string}>}
3696+
*/
3697+
function parseSBOMEntries(output) {
3698+
const entries = [];
3699+
for (const line of output.split('\n')) {
3700+
if (!line.includes('"SBOM files written"')) continue;
3701+
try {
3702+
const parsed = JSON.parse(line);
3703+
if (parsed.cyclonedx || parsed.spdx) {
3704+
entries.push({
3705+
cyclonedx: parsed.cyclonedx || '',
3706+
spdx: parsed.spdx || '',
3707+
});
3708+
}
3709+
} catch {
3710+
// Not valid JSON, skip.
3711+
}
3712+
}
3713+
return entries;
3714+
}
3715+
3716+
/**
3717+
* Builds a GitHub Actions job summary table from SBOM entries.
3718+
*
3719+
* @param {import('@actions/core')} core - The @actions/core module.
3720+
* @param {Array<{cyclonedx: string, spdx: string}>} sbomEntries
3721+
*/
3722+
async function writeSBOMSummary(core, sbomEntries) {
3723+
if (sbomEntries.length === 0) return;
3724+
3725+
const rows = [['Format', 'Path']];
3726+
for (const entry of sbomEntries) {
3727+
if (entry.cyclonedx) {
3728+
rows.push(['CycloneDX', `\`${entry.cyclonedx}\``]);
3729+
}
3730+
if (entry.spdx) {
3731+
rows.push(['SPDX', `\`${entry.spdx}\``]);
3732+
}
3733+
}
3734+
3735+
await core.summary
3736+
.addHeading('Cimon SBOM Report', 2)
3737+
.addRaw(
3738+
`**${sbomEntries.length}** SBOM${sbomEntries.length > 1 ? 's' : ''} generated during build.\n\n`
3739+
)
3740+
.addTable(rows)
3741+
.write();
3742+
}
3743+
3744+
37143745
/***/ })
37153746

37163747
/******/ });
@@ -3820,6 +3851,23 @@ __webpack_handle_async_dependencies__();
38203851
/******/ };
38213852
/******/ })();
38223853
/******/
3854+
/******/ /* webpack/runtime/define property getters */
3855+
/******/ (() => {
3856+
/******/ // define getter functions for harmony exports
3857+
/******/ __nccwpck_require__.d = (exports, definition) => {
3858+
/******/ for(var key in definition) {
3859+
/******/ if(__nccwpck_require__.o(definition, key) && !__nccwpck_require__.o(exports, key)) {
3860+
/******/ Object.defineProperty(exports, key, { enumerable: true, get: definition[key] });
3861+
/******/ }
3862+
/******/ }
3863+
/******/ };
3864+
/******/ })();
3865+
/******/
3866+
/******/ /* webpack/runtime/hasOwnProperty shorthand */
3867+
/******/ (() => {
3868+
/******/ __nccwpck_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))
3869+
/******/ })();
3870+
/******/
38233871
/******/ /* webpack/runtime/compat */
38243872
/******/
38253873
/******/ if (typeof __nccwpck_require__ !== 'undefined') __nccwpck_require__.ab = new URL('.', import.meta.url).pathname.slice(import.meta.url.match(/^file:\/\/\/\w:/) ? 1 : 0, -1) + "/";

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@
77
"all": "npm run clean && npm run dist/main/index.js && npm run dist/post/index.js",
88
"dist/main/index.js": "ncc build --out dist/main src/main/index.js",
99
"dist/post/index.js": "ncc build --out dist/post src/post/index.js",
10-
"clean": "rm -rf dist"
10+
"clean": "rm -rf dist",
11+
"test": "node --test src/**/*.test.js"
1112
},
1213
"keywords": [],
1314
"author": "",

src/post/index.js

Lines changed: 7 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import core from '@actions/core';
22
import exec from '@actions/exec';
33
import fs from 'fs';
44
import * as http from '@actions/http-client';
5+
import { parseSBOMEntries, writeSBOMSummary } from './sbom-summary.js';
56

67
const CIMON_SCRIPT_DOWNLOAD_URL =
78
'https://cimon-releases.s3.amazonaws.com/install.sh';
@@ -40,6 +41,11 @@ async function sudoExists() {
4041
* Determines the Cimon executable path.
4142
* Prefers the release-path saved by the main step so the same binary
4243
* is used for both start and stop.
44+
*
45+
* Backward compatibility: when the main step was run by an older version
46+
* of cimon-action that did not call core.saveState('release-path', ...),
47+
* core.getState() returns '' and we fall back to the default S3-downloaded
48+
* binary at /tmp/cimon/cimon — exactly the same behavior as before.
4349
*/
4450
function getCimonPath() {
4551
const savedPath = core.getState('release-path');
@@ -49,54 +55,6 @@ function getCimonPath() {
4955
return CIMON_EXECUTABLE_PATH;
5056
}
5157

52-
/**
53-
* Parses SBOM file entries from cimon agent stop output.
54-
* Returns an array of {cyclonedx, spdx} objects.
55-
*/
56-
function parseSBOMEntries(output) {
57-
const entries = [];
58-
for (const line of output.split('\n')) {
59-
if (!line.includes('"SBOM files written"')) continue;
60-
try {
61-
const parsed = JSON.parse(line);
62-
if (parsed.cyclonedx || parsed.spdx) {
63-
entries.push({
64-
cyclonedx: parsed.cyclonedx || '',
65-
spdx: parsed.spdx || '',
66-
});
67-
}
68-
} catch {
69-
// Not valid JSON, skip.
70-
}
71-
}
72-
return entries;
73-
}
74-
75-
/**
76-
* Builds a GitHub Actions job summary with SBOM results.
77-
*/
78-
async function writeSBOMSummary(sbomEntries) {
79-
if (sbomEntries.length === 0) return;
80-
81-
const rows = [['Format', 'Path']];
82-
for (const entry of sbomEntries) {
83-
if (entry.cyclonedx) {
84-
rows.push(['CycloneDX', `\`${entry.cyclonedx}\``]);
85-
}
86-
if (entry.spdx) {
87-
rows.push(['SPDX', `\`${entry.spdx}\``]);
88-
}
89-
}
90-
91-
await core.summary
92-
.addHeading('Cimon SBOM Report', 2)
93-
.addRaw(
94-
`**${sbomEntries.length}** SBOM${sbomEntries.length > 1 ? 's' : ''} generated during build.\n\n`
95-
)
96-
.addTable(rows)
97-
.write();
98-
}
99-
10058
async function run(config) {
10159
const cimonPath = getCimonPath();
10260

@@ -160,7 +118,7 @@ async function run(config) {
160118
if (sbomEntries.length > 0) {
161119
const reportJobSummary = core.getBooleanInput('report-job-summary');
162120
if (reportJobSummary) {
163-
await writeSBOMSummary(sbomEntries);
121+
await writeSBOMSummary(core, sbomEntries);
164122
}
165123
}
166124

src/post/sbom-summary.js

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
/**
2+
* Pure helpers for parsing and displaying SBOM results in the
3+
* GitHub Actions job summary. Extracted so they can be unit-tested
4+
* independently of the @actions/* runtime.
5+
*/
6+
7+
/**
8+
* Parses SBOM file entries from cimon agent stop output.
9+
* Each JSON log line that contains `"SBOM files written"` is expected
10+
* to carry `cyclonedx` and/or `spdx` path fields.
11+
*
12+
* @param {string} output - Combined stdout+stderr from `cimon agent stop`.
13+
* @returns {Array<{cyclonedx: string, spdx: string}>}
14+
*/
15+
export function parseSBOMEntries(output) {
16+
const entries = [];
17+
for (const line of output.split('\n')) {
18+
if (!line.includes('"SBOM files written"')) continue;
19+
try {
20+
const parsed = JSON.parse(line);
21+
if (parsed.cyclonedx || parsed.spdx) {
22+
entries.push({
23+
cyclonedx: parsed.cyclonedx || '',
24+
spdx: parsed.spdx || '',
25+
});
26+
}
27+
} catch {
28+
// Not valid JSON, skip.
29+
}
30+
}
31+
return entries;
32+
}
33+
34+
/**
35+
* Builds a GitHub Actions job summary table from SBOM entries.
36+
*
37+
* @param {import('@actions/core')} core - The @actions/core module.
38+
* @param {Array<{cyclonedx: string, spdx: string}>} sbomEntries
39+
*/
40+
export async function writeSBOMSummary(core, sbomEntries) {
41+
if (sbomEntries.length === 0) return;
42+
43+
const rows = [['Format', 'Path']];
44+
for (const entry of sbomEntries) {
45+
if (entry.cyclonedx) {
46+
rows.push(['CycloneDX', `\`${entry.cyclonedx}\``]);
47+
}
48+
if (entry.spdx) {
49+
rows.push(['SPDX', `\`${entry.spdx}\``]);
50+
}
51+
}
52+
53+
await core.summary
54+
.addHeading('Cimon SBOM Report', 2)
55+
.addRaw(
56+
`**${sbomEntries.length}** SBOM${sbomEntries.length > 1 ? 's' : ''} generated during build.\n\n`
57+
)
58+
.addTable(rows)
59+
.write();
60+
}

0 commit comments

Comments
 (0)