Compare commits

...
6 changed files with 179 additions and 61 deletions
+37 -1
View File
@@ -1,5 +1,5 @@
import {expect, jest, test} from '@jest/globals' import {expect, jest, test} from '@jest/globals'
import {Changes, ConfigurationOptions, Scorecard} from '../src/schemas' import {Change, Changes, ConfigurationOptions, Scorecard} from '../src/schemas'
import * as summary from '../src/summary' import * as summary from '../src/summary'
import * as core from '@actions/core' import * as core from '@actions/core'
import {createTestChange} from './fixtures/create-test-change' import {createTestChange} from './fixtures/create-test-change'
@@ -109,6 +109,42 @@ test('prints headline as h1', () => {
expect(text).toContain('<h1>Dependency Review</h1>') expect(text).toContain('<h1>Dependency Review</h1>')
}) })
test('returns minimal summary in case the core.summary is too large for a PR comment', () => {
let changes: Changes = [
createTestChange({name: 'lodash', version: '1.2.3'}),
createTestChange({name: 'colors', version: '2.3.4'}),
createTestChange({name: '@foo/bar', version: '*'}),
]
let minSummary: string = summary.addSummaryToSummary(
changes,
emptyInvalidLicenseChanges,
emptyChanges,
scorecard,
defaultConfig
)
// side effect DR report into core.summary as happens in main.ts
summary.addScannedDependencies(changes)
const text = core.summary.stringify()
expect(text).toContain('<h1>Dependency Review</h1>')
expect(minSummary).toContain('# Dependency Review')
expect(text).toContain('❌ 3 vulnerable package(s)')
expect(text).not.toContain('* ❌ 3 vulnerable package(s)')
expect(text).toContain('lodash')
expect(text).toContain('colors')
expect(text).toContain('@foo/bar')
expect(minSummary).toContain('* ❌ 3 vulnerable package(s)')
expect(minSummary).not.toContain('lodash')
expect(minSummary).not.toContain('colors')
expect(minSummary).not.toContain('@foo/bar')
expect(text.length).toBeGreaterThan(minSummary.length)
})
test('only includes "No vulnerabilities or license issues found"-message if both are configured and nothing was found', () => { test('only includes "No vulnerabilities or license issues found"-message if both are configured and nothing was found', () => {
summary.addSummaryToSummary( summary.addSummaryToSummary(
emptyChanges, emptyChanges,
Generated Vendored
+52 -5
View File
@@ -56,10 +56,10 @@ const retryingOctokit = githubUtils.GitHub.plugin(retry.retry);
const octo = new retryingOctokit(githubUtils.getOctokitOptions(core.getInput('repo-token', { required: true }))); const octo = new retryingOctokit(githubUtils.getOctokitOptions(core.getInput('repo-token', { required: true })));
// Comment Marker to identify an existing comment to update, so we don't spam the PR with comments // Comment Marker to identify an existing comment to update, so we don't spam the PR with comments
const COMMENT_MARKER = '<!-- dependency-review-pr-comment-marker -->'; const COMMENT_MARKER = '<!-- dependency-review-pr-comment-marker -->';
function commentPr(summary, config) { const MAX_COMMENT_LENGTH = 65536;
function commentPr(summary, config, minComment) {
return __awaiter(this, void 0, void 0, function* () { return __awaiter(this, void 0, void 0, function* () {
const commentContent = summary.stringify(); const commentContent = summary.stringify();
core.setOutput('comment-content', commentContent);
if (!(config.comment_summary_in_pr === 'always' || if (!(config.comment_summary_in_pr === 'always' ||
(config.comment_summary_in_pr === 'on-failure' && (config.comment_summary_in_pr === 'on-failure' &&
process.exitCode === core.ExitCode.Failure))) { process.exitCode === core.ExitCode.Failure))) {
@@ -69,7 +69,11 @@ function commentPr(summary, config) {
core.warning('Not in the context of a pull request. Skipping comment creation.'); core.warning('Not in the context of a pull request. Skipping comment creation.');
return; return;
} }
const commentBody = `${commentContent}\n\n${COMMENT_MARKER}`; let commentBody = `${commentContent}\n\n${COMMENT_MARKER}`;
if (commentBody.length >= MAX_COMMENT_LENGTH) {
core.debug('The comment was too big for the GitHub API. Falling back on a minimum comment');
commentBody = `${minComment}\n\n${COMMENT_MARKER}`;
}
try { try {
const existingCommentId = yield findCommentByMarker(COMMENT_MARKER); const existingCommentId = yield findCommentByMarker(COMMENT_MARKER);
if (existingCommentId) { if (existingCommentId) {
@@ -638,6 +642,7 @@ function run() {
const deniedChanges = yield (0, deny_1.getDeniedChanges)(filteredChanges, config.deny_packages, config.deny_groups); const deniedChanges = yield (0, deny_1.getDeniedChanges)(filteredChanges, config.deny_packages, config.deny_groups);
const scorecard = yield (0, scorecard_1.getScorecardLevels)(filteredChanges); const scorecard = yield (0, scorecard_1.getScorecardLevels)(filteredChanges);
summary.addSummaryToSummary(vulnerableChanges, invalidLicenseChanges, deniedChanges, scorecard, config); summary.addSummaryToSummary(vulnerableChanges, invalidLicenseChanges, deniedChanges, scorecard, config);
const minSummary = summary.getMinSummaryForComment(vulnerableChanges, invalidLicenseChanges, deniedChanges, scorecard, config);
if (snapshot_warnings) { if (snapshot_warnings) {
summary.addSnapshotWarnings(config, snapshot_warnings); summary.addSnapshotWarnings(config, snapshot_warnings);
} }
@@ -664,7 +669,7 @@ function run() {
core.setOutput('dependency-changes', JSON.stringify(changes)); core.setOutput('dependency-changes', JSON.stringify(changes));
summary.addScannedDependencies(changes); summary.addScannedDependencies(changes);
printScannedDependencies(changes); printScannedDependencies(changes);
yield (0, comment_pr_1.commentPr)(core.summary, config); yield (0, comment_pr_1.commentPr)(core.summary, config, minSummary);
} }
catch (error) { catch (error) {
if (error instanceof request_error_1.RequestError && error.status === 404) { if (error instanceof request_error_1.RequestError && error.status === 404) {
@@ -1138,7 +1143,7 @@ var __importStar = (this && this.__importStar) || function (mod) {
return result; return result;
}; };
Object.defineProperty(exports, "__esModule", ({ value: true })); Object.defineProperty(exports, "__esModule", ({ value: true }));
exports.addDeniedToSummary = exports.addSnapshotWarnings = exports.addScorecardToSummary = exports.addScannedDependencies = exports.addLicensesToSummary = exports.addChangeVulnerabilitiesToSummary = exports.addSummaryToSummary = void 0; exports.addDeniedToSummary = exports.addSnapshotWarnings = exports.addScorecardToSummary = exports.addScannedDependencies = exports.addLicensesToSummary = exports.addChangeVulnerabilitiesToSummary = exports.addSummaryToSummary = exports.getMinSummaryForComment = void 0;
const core = __importStar(__nccwpck_require__(2186)); const core = __importStar(__nccwpck_require__(2186));
const utils_1 = __nccwpck_require__(918); const utils_1 = __nccwpck_require__(918);
const icons = { const icons = {
@@ -1146,6 +1151,48 @@ const icons = {
cross: '❌', cross: '❌',
warning: '⚠️' warning: '⚠️'
}; };
function getMinSummaryForComment(vulnerableChanges, invalidLicenseChanges, deniedChanges, scorecard, config) {
const scorecardWarnings = countScorecardWarnings(scorecard, config);
const licenseIssues = countLicenseIssues(invalidLicenseChanges);
let minSummary = '# Dependency Review\n';
if (vulnerableChanges.length === 0 &&
licenseIssues === 0 &&
deniedChanges.length === 0 &&
scorecardWarnings === 0) {
const issueTypes = [
config.vulnerability_check ? 'vulnerabilities' : '',
config.license_check ? 'license issues' : '',
config.show_openssf_scorecard ? 'OpenSSF Scorecard issues' : ''
];
if (issueTypes.filter(Boolean).length === 0) {
minSummary += `${icons.check} No issues found.`;
}
else {
minSummary += `${icons.check} No ${issueTypes.filter(Boolean).join(' or ')} found.`;
}
}
minSummary += 'The following issues were found:\n';
minSummary += config.vulnerability_check
? `* ${checkOrFailIcon(vulnerableChanges.length)} ${vulnerableChanges.length} vulnerable package(s)\n`
: '';
minSummary += config.license_check
? `* ${checkOrFailIcon(invalidLicenseChanges.forbidden.length)} ${invalidLicenseChanges.forbidden.length} package(s) with incompatible licenses\n
* ${checkOrFailIcon(invalidLicenseChanges.unresolved.length)} ${invalidLicenseChanges.unresolved.length} package(s) with invalid SPDX license definitions\n
* ${checkOrWarnIcon(invalidLicenseChanges.unlicensed.length)} ${invalidLicenseChanges.unlicensed.length} package(s) with unknown licenses.\n`
: '';
minSummary +=
deniedChanges.length > 0
? `* ${checkOrWarnIcon(deniedChanges.length)} ${deniedChanges.length} package(s) denied.\n`
: '';
minSummary +=
config.show_openssf_scorecard && scorecardWarnings > 0
? `* ${checkOrWarnIcon(scorecardWarnings)} ${scorecardWarnings ? scorecardWarnings : 'No'} packages with OpenSSF Scorecard issues.\n`
: '';
// Add the link to the job summary provided by GitHub Actions for this workflow run
minSummary += `\n[View full job summary](${process.env.GITHUB_SERVER_URL}/${process.env.GITHUB_REPOSITORY}/actions/runs/${process.env.GITHUB_RUN_ID})`;
return minSummary;
}
exports.getMinSummaryForComment = getMinSummaryForComment;
function addSummaryToSummary(vulnerableChanges, invalidLicenseChanges, deniedChanges, scorecard, config) { function addSummaryToSummary(vulnerableChanges, invalidLicenseChanges, deniedChanges, scorecard, config) {
const scorecardWarnings = countScorecardWarnings(scorecard, config); const scorecardWarnings = countScorecardWarnings(scorecard, config);
const licenseIssues = countLicenseIssues(invalidLicenseChanges); const licenseIssues = countLicenseIssues(invalidLicenseChanges);
Generated Vendored
+1 -1
View File
File diff suppressed because one or more lines are too long
+5 -7
View File
@@ -5,6 +5,8 @@ import * as retry from '@octokit/plugin-retry'
import {RequestError} from '@octokit/request-error' import {RequestError} from '@octokit/request-error'
import {ConfigurationOptions} from './schemas' import {ConfigurationOptions} from './schemas'
export const MAX_COMMENT_LENGTH = 65536
const retryingOctokit = githubUtils.GitHub.plugin(retry.retry) const retryingOctokit = githubUtils.GitHub.plugin(retry.retry)
const octo = new retryingOctokit( const octo = new retryingOctokit(
githubUtils.getOctokitOptions(core.getInput('repo-token', {required: true})) githubUtils.getOctokitOptions(core.getInput('repo-token', {required: true}))
@@ -14,13 +16,9 @@ const octo = new retryingOctokit(
const COMMENT_MARKER = '<!-- dependency-review-pr-comment-marker -->' const COMMENT_MARKER = '<!-- dependency-review-pr-comment-marker -->'
export async function commentPr( export async function commentPr(
summary: typeof core.summary, commentContent: string,
config: ConfigurationOptions config: ConfigurationOptions,
): Promise<void> { ): Promise<void> {
const commentContent = summary.stringify()
core.setOutput('comment-content', commentContent)
if ( if (
!( !(
config.comment_summary_in_pr === 'always' || config.comment_summary_in_pr === 'always' ||
@@ -38,7 +36,7 @@ export async function commentPr(
return return
} }
const commentBody = `${commentContent}\n\n${COMMENT_MARKER}` let commentBody = `${commentContent}\n\n${COMMENT_MARKER}`
try { try {
const existingCommentId = await findCommentByMarker(COMMENT_MARKER) const existingCommentId = await findCommentByMarker(COMMENT_MARKER)
+21 -3
View File
@@ -22,9 +22,13 @@ import * as summary from './summary'
import {getRefs} from './git-refs' import {getRefs} from './git-refs'
import {groupDependenciesByManifest} from './utils' import {groupDependenciesByManifest} from './utils'
import {commentPr} from './comment-pr' import {
commentPr,
MAX_COMMENT_LENGTH
} from './comment-pr'
import {getDeniedChanges} from './deny' import {getDeniedChanges} from './deny'
async function delay(ms: number): Promise<void> { async function delay(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms)) return new Promise(resolve => setTimeout(resolve, ms))
} }
@@ -127,7 +131,7 @@ async function run(): Promise<void> {
const scorecard = await getScorecardLevels(filteredChanges) const scorecard = await getScorecardLevels(filteredChanges)
summary.addSummaryToSummary( const minSummary = summary.addSummaryToSummary(
vulnerableChanges, vulnerableChanges,
invalidLicenseChanges, invalidLicenseChanges,
deniedChanges, deniedChanges,
@@ -166,7 +170,21 @@ async function run(): Promise<void> {
core.setOutput('dependency-changes', JSON.stringify(changes)) core.setOutput('dependency-changes', JSON.stringify(changes))
summary.addScannedDependencies(changes) summary.addScannedDependencies(changes)
printScannedDependencies(changes) printScannedDependencies(changes)
await commentPr(core.summary, config)
// include full summary in output; Actions will truncate if oversized
let rendered = core.summary.stringify()
core.setOutput('content-comment', rendered)
// if the summary is oversized, replace with minimal version
if (rendered.length >= MAX_COMMENT_LENGTH) {
core.debug(
'The comment was too big for the GitHub API. Falling back on a minimum comment'
)
rendered = minSummary
}
// update the PR comment if needed with the right-sized summary
await commentPr(rendered, config)
} catch (error) { } catch (error) {
if (error instanceof RequestError && error.status === 404) { if (error instanceof RequestError && error.status === 404) {
core.setFailed( core.setFailed(
+63 -44
View File
@@ -10,17 +10,23 @@ const icons = {
warning: '⚠️' warning: '⚠️'
} }
// generates the DR report summmary and caches it to the Action's core.summary.
// returns the DR summary string, ready to be posted as a PR comment if the
// final DR report is too large
export function addSummaryToSummary( export function addSummaryToSummary(
vulnerableChanges: Changes, vulnerableChanges: Changes,
invalidLicenseChanges: InvalidLicenseChanges, invalidLicenseChanges: InvalidLicenseChanges,
deniedChanges: Changes, deniedChanges: Changes,
scorecard: Scorecard, scorecard: Scorecard,
config: ConfigurationOptions config: ConfigurationOptions,
): void { ): string {
let out: string[] = [];
const scorecardWarnings = countScorecardWarnings(scorecard, config) const scorecardWarnings = countScorecardWarnings(scorecard, config)
const licenseIssues = countLicenseIssues(invalidLicenseChanges) const licenseIssues = countLicenseIssues(invalidLicenseChanges)
core.summary.addHeading('Dependency Review', 1) core.summary.addHeading('Dependency Review', 1)
out.push('# Dependency Review')
if ( if (
vulnerableChanges.length === 0 && vulnerableChanges.length === 0 &&
@@ -33,54 +39,67 @@ export function addSummaryToSummary(
config.license_check ? 'license issues' : '', config.license_check ? 'license issues' : '',
config.show_openssf_scorecard ? 'OpenSSF Scorecard issues' : '' config.show_openssf_scorecard ? 'OpenSSF Scorecard issues' : ''
] ]
let msg = ''
if (issueTypes.filter(Boolean).length === 0) { if (issueTypes.filter(Boolean).length === 0) {
core.summary.addRaw(`${icons.check} No issues found.`) msg = `${icons.check} No issues found.`
} else { } else {
core.summary.addRaw( msg = `${icons.check} No ${issueTypes.filter(Boolean).join(' or ')} found.`
`${icons.check} No ${issueTypes.filter(Boolean).join(' or ')} found.`
)
} }
return core.summary.addRaw(msg)
out.push(msg)
return out.join('\n')
} }
core.summary const foundIssuesHeader = 'The following issues were found:'
.addRaw('The following issues were found:') core.summary.addRaw(foundIssuesHeader)
.addList([ out.push(foundIssuesHeader)
...(config.vulnerability_check
? [ const summaryList: string[] = [
`${checkOrFailIcon(vulnerableChanges.length)} ${ ...(config.vulnerability_check
vulnerableChanges.length ? [
} vulnerable package(s)` `${checkOrFailIcon(vulnerableChanges.length)} ${
] vulnerableChanges.length
: []), } vulnerable package(s)`
...(config.license_check ]
? [ : []),
`${checkOrFailIcon(invalidLicenseChanges.forbidden.length)} ${ ...(config.license_check
invalidLicenseChanges.forbidden.length ? [
} package(s) with incompatible licenses`, `${checkOrFailIcon(invalidLicenseChanges.forbidden.length)} ${
`${checkOrFailIcon(invalidLicenseChanges.unresolved.length)} ${ invalidLicenseChanges.forbidden.length
invalidLicenseChanges.unresolved.length } package(s) with incompatible licenses`,
} package(s) with invalid SPDX license definitions`, `${checkOrFailIcon(invalidLicenseChanges.unresolved.length)} ${
`${checkOrWarnIcon(invalidLicenseChanges.unlicensed.length)} ${ invalidLicenseChanges.unresolved.length
invalidLicenseChanges.unlicensed.length } package(s) with invalid SPDX license definitions`,
} package(s) with unknown licenses.` `${checkOrWarnIcon(invalidLicenseChanges.unlicensed.length)} ${
] invalidLicenseChanges.unlicensed.length
: []), } package(s) with unknown licenses.`
...(deniedChanges.length > 0 ]
? [ : []),
`${checkOrWarnIcon(deniedChanges.length)} ${ ...(deniedChanges.length > 0
deniedChanges.length ? [
} package(s) denied.` `${checkOrWarnIcon(deniedChanges.length)} ${
] deniedChanges.length
: []), } package(s) denied.`
...(config.show_openssf_scorecard && scorecardWarnings > 0 ]
? [ : []),
`${checkOrWarnIcon(scorecardWarnings)} ${scorecardWarnings ? scorecardWarnings : 'No'} packages with OpenSSF Scorecard issues.` ...(config.show_openssf_scorecard && scorecardWarnings > 0
] ? [
: []) `${checkOrWarnIcon(scorecardWarnings)} ${scorecardWarnings ? scorecardWarnings : 'No'} packages with OpenSSF Scorecard issues.`
]) ]
.addRaw('See the Details below.') : [])
];
core.summary.addList(summaryList)
summaryList.forEach( (line) => {
out.push('* ' + line)
})
core.summary.addRaw('See the Details below.')
out.push(`\n[View full job summary](${process.env.GITHUB_SERVER_URL}/${process.env.GITHUB_REPOSITORY}/actions/runs/${process.env.GITHUB_RUN_ID})`)
return out.join('\n')
} }
function countScorecardWarnings( function countScorecardWarnings(