diff --git a/README.md b/README.md index 9cd8b81..6f7a66e 100644 --- a/README.md +++ b/README.md @@ -66,25 +66,25 @@ jobs: Configure this action by either inlining these options in your workflow file, or by using an external configuration file. All configuration options are optional. -| Option | Usage | Possible values | Default value | -|-----------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------|---------------| -| `fail-on-severity` | Defines the threshold for the level of severity. The action will fail on any pull requests that introduce vulnerabilities of the specified severity level or higher. | `low`, `moderate`, `high`, `critical` | `low` | -| `allow-licenses`* | Contains a list of allowed licenses. The action will fail on pull requests that introduce dependencies with licenses that do not match the list. | Any [SPDX-compliant identifier(s)](https://spdx.org/licenses/) | none | -| `deny-licenses`* | Contains a list of prohibited licenses. The action will fail on pull requests that introduce dependencies with licenses that match the list. | Any [SPDX-compliant identifier(s)](https://spdx.org/licenses/) | none | -| `fail-on-scopes`† | Contains a list of strings of the build environments you want to support. The action will fail on pull requests that introduce vulnerabilities in the scopes that match the list. |`runtime`, `development`, `unknown` | `runtime` | -| `allow-ghsas` | Contains a list of GitHub Advisory Database IDs that can be skipped during detection. | Any GHSAs from the [GitHub Advisory Database](https://github.com/advisories) | none | -| `license-check` | Enable or disable the license check performed by the action. | `true`, `false` | `true` | -| `vulnerability-check` | Enable or disable the vulnerability check performed by the action. | `true`, `false` | `true` | -| `base-ref`/`head-ref` | Provide custom git references for the git base/head when performing the comparison check. This is only used for event types other than `pull_request` and `pull_request_target`. | Any valid git ref(s) in your project | none | +| Option | Usage | Possible values | Default value | +| ----------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------- | ------------- | +| `fail-on-severity` | Defines the threshold for the level of severity. The action will fail on any pull requests that introduce vulnerabilities of the specified severity level or higher. | `low`, `moderate`, `high`, `critical` | `low` | +| `allow-licenses`* | Contains a list of allowed licenses. The action will fail on pull requests that introduce dependencies with licenses that do not match the list. | Any [SPDX-compliant identifier(s)](https://spdx.org/licenses/) | none | +| `deny-licenses`* | Contains a list of prohibited licenses. The action will fail on pull requests that introduce dependencies with licenses that match the list. | Any [SPDX-compliant identifier(s)](https://spdx.org/licenses/) | none | +| `fail-on-scopes`† | Contains a list of strings of the build environments you want to support. The action will fail on pull requests that introduce vulnerabilities in the scopes that match the list. | `runtime`, `development`, `unknown` | `runtime` | +| `allow-ghsas` | Contains a list of GitHub Advisory Database IDs that can be skipped during detection. | Any GHSAs from the [GitHub Advisory Database](https://github.com/advisories) | none | +| `license-check` | Enable or disable the license check performed by the action. | `true`, `false` | `true` | +| `vulnerability-check` | Enable or disable the vulnerability check performed by the action. | `true`, `false` | `true` | +| `base-ref`/`head-ref` | Provide custom git references for the git base/head when performing the comparison check. This is only used for event types other than `pull_request` and `pull_request_target`. | Any valid git ref(s) in your project | none | +| `comment-summary-in-pr` | Enable or disable reporting the review summary as a comment in the pull request. If enabled, you must give the workflow or job permission `pull-requests: write`. | `true`, `false` | `false` | *not supported for use with GitHub Enterprise Server -†will be supported with GitHub Enterprise Server 3.8 - +†will be supported with GitHub Enterprise Server 3.8 ### Inline Configuration -You can pass options to the Dependency Review GitHub Action using your workflow file. +You can pass options to the Dependency Review GitHub Action using your workflow file. #### Example @@ -112,10 +112,10 @@ jobs: You can use an external configuration file to specify the settings for this action. It can be a local file or a file in an external repository. Refer to the following options for the specification. -| Option | Usage | Possible values | -|-----------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------| -| `config-file` | A path to a file in the current repository or an external repository. Use this syntax for external files: `OWNER/REPOSITORY/FILENAME@BRANCH` | **Local file**: `./.github/dependency-review-config.yml`
**External repo**: `github/octorepo/dependency-review-config.yml@main` | -| `external-repo-token` | Specifies a token for fetching the configuration file. It is required if the file resides in a private external repository and for all GitHub Enterprise Server repositories. Create a token in [developer settings](https://github.com/settings/tokens). | Any token with `read` permissions to the repository hosting the config file. | +| Option | Usage | Possible values | +| --------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | +| `config-file` | A path to a file in the current repository or an external repository. Use this syntax for external files: `OWNER/REPOSITORY/FILENAME@BRANCH` | **Local file**: `./.github/dependency-review-config.yml`
**External repo**: `github/octorepo/dependency-review-config.yml@main` | +| `external-repo-token` | Specifies a token for fetching the configuration file. It is required if the file resides in a private external repository and for all GitHub Enterprise Server repositories. Create a token in [developer settings](https://github.com/settings/tokens). | Any token with `read` permissions to the repository hosting the config file. | #### Example @@ -128,7 +128,7 @@ Start by specifying that you will be using an external configuration file: config-file: './.github/dependency-review-config.yml' ``` -And then create the file in the path you just specified: +And then create the file in the path you just specified: ```yaml fail-on-severity: 'critical' diff --git a/__tests__/config.test.ts b/__tests__/config.test.ts index 312fde1..18506c4 100644 --- a/__tests__/config.test.ts +++ b/__tests__/config.test.ts @@ -22,7 +22,8 @@ function clearInputs() { 'VULNERABILITY-CHECK', 'CONFIG-FILE', 'BASE-REF', - 'HEAD-REF' + 'HEAD-REF', + 'COMMENT-SUMMARY-IN-PR' ] allowedOptions.forEach(option => { diff --git a/action.yml b/action.yml index 9701ed5..c7dad3e 100644 --- a/action.yml +++ b/action.yml @@ -41,6 +41,9 @@ inputs: vulnerability-check: description: A boolean to determine if vulnerability checks should be performed required: false + comment-summary-in-pr: + description: A boolean to determine if the report should be posted as a comment in the PR itself. Setting this to true requires you to give the workflow the write permissions for pull-requests + required: false runs: using: 'node16' main: 'dist/index.js' diff --git a/src/comment-pr.ts b/src/comment-pr.ts new file mode 100644 index 0000000..0c68d3d --- /dev/null +++ b/src/comment-pr.ts @@ -0,0 +1,84 @@ +import * as github from '@actions/github' +import * as core from '@actions/core' +import * as githubUtils from '@actions/github/lib/utils' +import * as retry from '@octokit/plugin-retry' +import {RequestError} from '@octokit/request-error' + +const retryingOctokit = githubUtils.GitHub.plugin(retry.retry) +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 +const COMMENT_MARKER = '' + +export async function commentPr(summary: typeof core.summary): Promise { + if (!github.context.payload.pull_request) { + core.warning( + 'Not in the context of a pull request. Skipping comment creation.' + ) + return + } + + const commentBody = `${summary.stringify()}\n\n${COMMENT_MARKER}` + + try { + const existingCommentId = await findCommentByMarker(COMMENT_MARKER) + + if (existingCommentId) { + await octo.rest.issues.updateComment({ + owner: github.context.repo.owner, + repo: github.context.repo.repo, + comment_id: existingCommentId, + body: commentBody + }) + } else { + await octo.rest.issues.createComment({ + owner: github.context.repo.owner, + repo: github.context.repo.repo, + issue_number: github.context.payload.pull_request.number, + body: commentBody + }) + } + } catch (error) { + if (error instanceof RequestError && error.status === 403) { + core.warning( + `Unable to write summary to pull-request. Make sure you are giving this workflow the permission 'pull-requests: write'.` + ) + } else { + if (error instanceof Error) { + core.warning( + `Unable to comment summary to pull-request, received error: ${error.message}` + ) + } else { + core.warning( + 'Unable to comment summary to pull-request: Unexpected fatal error' + ) + } + } + } +} + +async function findCommentByMarker( + commentBodyIncludes: string +): Promise { + const commentsIterator = octo.paginate.iterator( + octo.rest.issues.listComments, + { + owner: github.context.repo.owner, + repo: github.context.repo.repo, + // We are already checking if we are in the context of a pull request in the caller + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + issue_number: github.context.payload.pull_request!.number + } + ) + + for await (const {data: comments} of commentsIterator) { + const existingComment = comments.find(comment => + comment.body?.includes(commentBodyIncludes) + ) + if (existingComment) return existingComment.id + } + + return undefined +} diff --git a/src/config.ts b/src/config.ts index 6e9ffbc..1c3db9d 100644 --- a/src/config.ts +++ b/src/config.ts @@ -34,6 +34,7 @@ function readInlineConfig(): ConfigurationOptionsPartial { const vulnerability_check = getOptionalBoolean('vulnerability-check') const base_ref = getOptionalInput('base-ref') const head_ref = getOptionalInput('head-ref') + const comment_summary_in_pr = getOptionalBoolean('comment-summary-in-pr') validateLicenses('allow-licenses', allow_licenses) validateLicenses('deny-licenses', deny_licenses) @@ -47,7 +48,8 @@ function readInlineConfig(): ConfigurationOptionsPartial { license_check, vulnerability_check, base_ref, - head_ref + head_ref, + comment_summary_in_pr } return Object.fromEntries( diff --git a/src/main.ts b/src/main.ts index e1c5808..9885998 100644 --- a/src/main.ts +++ b/src/main.ts @@ -15,6 +15,7 @@ import * as summary from './summary' import {getRefs} from './git-refs' import {groupDependenciesByManifest} from './utils' +import {commentPr} from './comment-pr' async function run(): Promise { try { @@ -69,6 +70,9 @@ async function run(): Promise { summary.addScannedDependencies(changes) printScannedDependencies(changes) + if (config.comment_summary_in_pr) { + await commentPr(core.summary) + } } catch (error) { if (error instanceof RequestError && error.status === 404) { core.setFailed( diff --git a/src/schemas.ts b/src/schemas.ts index 0adf08d..523418a 100644 --- a/src/schemas.ts +++ b/src/schemas.ts @@ -45,7 +45,8 @@ export const ConfigurationOptionsSchema = z vulnerability_check: z.boolean().default(true), config_file: z.string().optional(), base_ref: z.string().optional(), - head_ref: z.string().optional() + head_ref: z.string().optional(), + comment_summary_in_pr: z.boolean().default(false) }) .superRefine((config, context) => { if (config.allow_licenses && config.deny_licenses) {