Compare commits

..

16 Commits

Author SHA1 Message Date
Eric Sorenson 40c09b7dc9 Merge pull request #1001 from actions/ahpook/v4.8.1-release 2025-10-10 14:06:00 -07:00
Eric Sorenson 45529485b5 Bump version for 4.8.1 release 2025-10-10 12:55:32 -07:00
Eric Sorenson e63da9a041 Merge pull request #1000 from actions/ahpook/deprecation-redux 2025-10-10 12:21:31 -07:00
Eric Sorenson 71365c76bc (bug) Fix spamming link test in deprecation warning (again)
We'd thought that the syntax in #974 would avoid auto-linking
but didn't check closely enough, and now the deprecation issue
it links to cannot be loaded due to having too many references.

This updates the text to point to a new issue in a way that...
I hope... will not be auto-linked.
2025-10-10 09:37:13 -07:00
Barry Gordon 56339e523c Merge pull request #988 from actions/brrygrdn/rc-4.8.0
Bump to 4.8.0
2025-09-26 16:05:17 +01:00
Barry Gordon 1688b745f3 Bump to a 4.8.0 2025-09-26 15:45:28 +01:00
Barry Gordon 31c9f175b9 Merge pull request #987 from actions/rc-4.7.4
Prepare release of v4.7.4
2025-09-26 15:20:06 +01:00
Barry Gordon eacde7836e Update version 2025-09-26 14:42:22 +01:00
Barry Gordon 81510090e4 Merge pull request #986 from actions/brrygrdn/rc-4.7.4
Batch some contributions for release
2025-09-26 14:32:46 +01:00
Barry Gordon b472ec914b Add a quick regression test for the artefact summary 2025-09-26 13:34:03 +01:00
Matt Mencel e0cedc52dc feat: add large summary handling with artifact upload
When the dependency review summary exceeds GitHub's size limit (1024k), upload it as an artifact and provide a link in the comment. This ensures users can still access the full review details even when the summary is too large to display directly.
2025-09-26 12:55:14 +01:00
Jasper Kamerling e3fdf0f899 This ensures large allow or deny lists don't create huge comments 2025-09-26 12:49:38 +01:00
Lewis Jones 6fad417932 Merge pull request #978 from actions/ljones140/make-ruby-code-scannable
Make Ruby Code Scannable
2025-08-29 10:39:17 +01:00
Lewis Jones e86e9692ad Update scripts/scan_pr_lib.rb
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-08-28 16:24:02 +01:00
Lewis Jones 85c8e53ab7 Scan ruby 2025-08-28 16:12:23 +01:00
Lewis Jones c6a7eb7252 Extract ruby code
So can be scanned by code scanning
2025-08-28 16:11:56 +01:00
13 changed files with 104878 additions and 1868 deletions
+1 -1
View File
@@ -38,7 +38,7 @@ jobs:
# For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
# queries: security-extended,security-and-quality
config: |
paths-ignore:
paths-ignore:
- dist/index.js
- dist/sourcemap-register.js
+153
View File
@@ -0,0 +1,153 @@
import {
afterEach,
beforeEach,
describe,
expect,
jest,
test
} from '@jest/globals'
import * as fs from 'fs'
import * as core from '@actions/core'
import {DefaultArtifactClient} from '@actions/artifact'
import type {SpyInstance} from 'jest-mock'
import {handleLargeSummary} from '../src/main'
jest.mock('ansi-styles', () => ({
__esModule: true,
default: {
color: {
red: {open: '', close: ''},
yellow: {open: '', close: ''},
grey: {open: '', close: ''},
green: {open: '', close: ''}
},
bold: {open: '', close: ''}
}
}))
jest.mock('../src/dependency-graph', () => ({}))
jest.mock('@actions/core', () => {
const summary = {
addRaw: jest.fn().mockReturnThis(),
addHeading: jest.fn().mockReturnThis(),
addTable: jest.fn().mockReturnThis(),
addSeparator: jest.fn().mockReturnThis(),
addImage: jest.fn().mockReturnThis(),
addList: jest.fn().mockReturnThis(),
addBreak: jest.fn().mockReturnThis(),
addLink: jest.fn().mockReturnThis(),
addDetails: jest.fn().mockReturnThis(),
addSection: jest.fn().mockReturnThis(),
addCodeBlock: jest.fn().mockReturnThis(),
addFields: jest.fn().mockReturnThis(),
addEol: jest.fn().mockReturnThis(),
write: jest.fn(async () => undefined),
emptyBuffer: jest.fn(),
stringify: jest.fn(() => '')
}
return {
__esModule: true,
getInput: jest.fn((name: string) =>
name === 'repo-token' ? 'gh_test_token' : ''
),
setOutput: jest.fn(),
setFailed: jest.fn(),
warning: jest.fn(),
info: jest.fn(),
debug: jest.fn(),
startGroup: jest.fn(),
endGroup: jest.fn(),
group: jest.fn(async (_name: string, fn: () => Promise<unknown>) => fn()),
summary
}
})
jest.mock('@actions/artifact', () => ({
DefaultArtifactClient: jest.fn()
}))
const ORIGINAL_ENV = {...process.env}
type ArtifactClientInstance = {
uploadArtifact: jest.Mock
}
const DefaultArtifactClientMock = DefaultArtifactClient as unknown as jest.Mock
const createArtifactClient = (): ArtifactClientInstance => ({
uploadArtifact: jest.fn(async () => undefined)
})
describe('handleLargeSummary', () => {
let writeFileSpy: SpyInstance<typeof fs.promises.writeFile>
beforeEach(() => {
process.env = {...ORIGINAL_ENV}
writeFileSpy = jest
.spyOn(fs.promises, 'writeFile')
.mockImplementation(async () => undefined)
DefaultArtifactClientMock.mockClear()
DefaultArtifactClientMock.mockImplementation(() => createArtifactClient())
})
afterEach(() => {
writeFileSpy.mockRestore()
jest.clearAllMocks()
process.env = {...ORIGINAL_ENV}
})
test('returns original summary when under size threshold', async () => {
const summaryContent = 'short summary'
const result = await handleLargeSummary(summaryContent)
expect(result).toBe(summaryContent)
expect(writeFileSpy).not.toHaveBeenCalled()
expect(DefaultArtifactClientMock).not.toHaveBeenCalled()
})
test('uploads artifact and returns minimal summary when summary is too large', async () => {
process.env.GITHUB_SERVER_URL = 'https://github.com'
process.env.GITHUB_REPOSITORY = 'owner/repo'
process.env.GITHUB_RUN_ID = '12345'
const largeSummary = 'a'.repeat(1024 * 1024 + 1)
const result = await handleLargeSummary(largeSummary)
expect(writeFileSpy).toHaveBeenCalledTimes(1)
expect(writeFileSpy).toHaveBeenCalledWith('summary.md', largeSummary)
expect(DefaultArtifactClientMock).toHaveBeenCalledTimes(1)
const artifactInstance = DefaultArtifactClientMock.mock.results[0]
?.value as ArtifactClientInstance
expect(artifactInstance.uploadArtifact).toHaveBeenCalledWith(
'dependency-review-summary',
['summary.md'],
'.',
{retentionDays: 1}
)
expect(result).toContain('# Dependency Review Summary')
expect(result).toContain('dependency-review-summary')
expect(result).toContain('actions/runs/12345')
})
test('returns original summary and logs a warning when artifact handling fails', async () => {
const warningMock = core.warning as jest.Mock
warningMock.mockClear()
const largeSummary = 'b'.repeat(1024 * 1024 + 1)
DefaultArtifactClientMock.mockImplementation(() => ({
uploadArtifact: jest.fn(async () => {
throw new Error('upload failed')
})
}))
const result = await handleLargeSummary(largeSummary)
expect(result).toBe(largeSummary)
expect(warningMock).toHaveBeenCalledWith(
expect.stringContaining('Failed to handle large summary')
)
})
})
+27 -3
View File
@@ -464,7 +464,9 @@ test('addLicensesToSummary() - includes list of configured allowed licenses', ()
summary.addLicensesToSummary(licenseIssues, config)
const text = core.summary.stringify()
expect(text).toContain('<strong>Allowed Licenses</strong>: MIT, Apache-2.0')
expect(text).toContain(
'<details><summary><strong>Allowed Licenses</strong>:</summary> MIT, Apache-2.0</details>'
)
})
test('addLicensesToSummary() - includes configured denied license', () => {
@@ -476,11 +478,33 @@ test('addLicensesToSummary() - includes configured denied license', () => {
const config: ConfigurationOptions = {
...defaultConfig,
deny_licenses: ['MIT']
deny_licenses: ['MIT', 'Apache-2.0']
}
summary.addLicensesToSummary(licenseIssues, config)
const text = core.summary.stringify()
expect(text).toContain('<strong>Denied Licenses</strong>: MIT')
expect(text).toContain(
'<details><summary><strong>Denied Licenses</strong>:</summary> MIT, Apache-2.0</details>'
)
})
test('addLicensesToSummary() - includes allowed dependency licences', () => {
const licenseIssues = {
forbidden: [createTestChange()],
unresolved: [],
unlicensed: []
}
const config: ConfigurationOptions = {
...defaultConfig,
allow_dependencies_licenses: ['MIT', 'Apache-2.0']
}
summary.addLicensesToSummary(licenseIssues, config)
const text = core.summary.stringify()
expect(text).toContain(
'<details><summary><strong>Excluded from license check</strong>:</summary> MIT, Apache-2.0</details>'
)
})
Generated Vendored
+100223 -1306
View File
File diff suppressed because one or more lines are too long
Generated Vendored
+1 -1
View File
File diff suppressed because one or more lines are too long
Generated Vendored
+2911
View File
File diff suppressed because it is too large Load Diff
+1380 -463
View File
File diff suppressed because it is too large Load Diff
+2 -1
View File
@@ -1,6 +1,6 @@
{
"name": "dependency-review-action",
"version": "4.7.3",
"version": "4.8.1",
"private": true,
"description": "A GitHub Action for Dependency Review",
"main": "lib/main.js",
@@ -25,6 +25,7 @@
"author": "GitHub",
"license": "MIT",
"dependencies": {
"@actions/artifact": "^2.3.2",
"@actions/core": "^1.11.1",
"@actions/github": "^6.0.1",
"@octokit/plugin-retry": "^6.1.0",
+8
View File
@@ -0,0 +1,8 @@
#!/usr/bin/env ruby
# Load the scan_pr library
require_relative 'scan_pr_lib'
# Create and run the scanner
scanner = ScanPr.new
scanner.run(ARGV)
-87
View File
@@ -1,87 +0,0 @@
#!/usr/bin/env ruby
require 'json'
require 'tempfile'
require 'open3'
require 'bundler/inline'
require 'optparse'
gemfile do
source 'https://rubygems.org'
gem 'octokit'
end
config_file = nil
github_token = ENV["GITHUB_TOKEN"]
if !github_token || github_token.empty?
puts "Please set the GITHUB_TOKEN environment variable"
exit -1
end
op = OptionParser.new do |opts|
usage = <<EOF
Run Dependency Review on a repository.
\e[1mUsage:\e[22m
scripts/scan_pr [options] <pr_url>
\e[1mExample:\e[22m
scripts/scan_pr https://github.com/actions/dependency-review-action/pull/294
EOF
opts.banner = usage
opts.on('-c', '--config-file <FILE>', 'Use an external configuration file') do |cf|
config_file = cf
end
opts.on("-h", "--help", "Prints this help") do
puts opts
exit
end
end
op.parse!
# make sure we have a NWO somewhere in the parameters
arg = /(?<repo_nwo>[\w\-]+\/[\w\-]+)\/pull\/(?<pr_number>\d+)/.match(ARGV.join(" "))
if arg.nil?
puts op
exit -1
end
repo_nwo = arg[:repo_nwo]
pr_number = arg[:pr_number]
octo = Octokit::Client.new(access_token: github_token)
pr = octo.pull_request(repo_nwo, pr_number)
event_file = Tempfile.new
event_file.write("{ \"pull_request\": #{pr.to_h.to_json}}")
event_file.close
action_inputs = {
"repo-token": github_token,
"config-file": config_file
}
dev_cmd_env = {
"GITHUB_REPOSITORY" => repo_nwo,
"GITHUB_EVENT_NAME" => "pull_request",
"GITHUB_EVENT_PATH" => event_file.path,
"GITHUB_STEP_SUMMARY" => "/dev/null"
}
# bash does not like variable names with dashes like the ones Actions
# uses (e.g. INPUT_REPO-TOKEN). Passing them through `env` instead of
# manually setting them does the job.
action_inputs_env_str = action_inputs.map { |name, value| "\"INPUT_#{name.upcase}=#{value}\"" }.join(" ")
dev_cmd = "./node_modules/.bin/nodemon --exec \"env #{action_inputs_env_str} node -r esbuild-register\" src/main.ts"
Open3.popen2e(dev_cmd_env, dev_cmd) do |stdin, out|
while line = out.gets
puts line.gsub(github_token, "<REDACTED>")
end
end
+128
View File
@@ -0,0 +1,128 @@
require 'json'
require 'tempfile'
require 'open3'
require 'bundler/inline'
require 'optparse'
gemfile do
source 'https://rubygems.org'
gem 'octokit'
end
class ScanPr
def initialize
@config_file = nil
@github_token = ENV["GITHUB_TOKEN"]
validate_token
end
def run(args)
parse_options(args)
repo_nwo, pr_number = extract_repo_and_pr(args)
pr = fetch_pull_request(repo_nwo, pr_number)
event_file = create_event_file(pr)
execute_dependency_review(repo_nwo, event_file)
ensure
event_file&.unlink
end
private
def validate_token
if !@github_token || @github_token.empty?
puts "Please set the GITHUB_TOKEN environment variable"
exit -1
end
end
def parse_options(args)
op = OptionParser.new do |opts|
usage = <<EOF
Run Dependency Review on a repository.
\e[1mUsage:\e[22m
scripts/scan_pr [options] <pr_url>
\e[1mExample:\e[22m
scripts/scan_pr https://github.com/actions/dependency-review-action/pull/294
EOF
opts.banner = usage
opts.on('-c', '--config-file <FILE>', 'Use an external configuration file') do |cf|
@config_file = cf
end
opts.on("-h", "--help", "Prints this help") do
puts opts
exit
end
end
op.parse!(args)
@option_parser = op
end
def extract_repo_and_pr(args)
# make sure we have a NWO somewhere in the parameters
arg = /(?<repo_nwo>[\w\-]+\/[\w\-]+)\/pull\/(?<pr_number>\d+)/.match(args.join(" "))
if arg.nil?
puts @option_parser
exit -1
end
[arg[:repo_nwo], arg[:pr_number]]
end
def fetch_pull_request(repo_nwo, pr_number)
octo = Octokit::Client.new(access_token: @github_token)
octo.pull_request(repo_nwo, pr_number)
end
def create_event_file(pr)
event_file = Tempfile.new
event_file.write("{ \"pull_request\": #{pr.to_h.to_json}}")
event_file.close
event_file
end
def execute_dependency_review(repo_nwo, event_file)
action_inputs = {
"repo-token": @github_token,
"config-file": @config_file
}
dev_cmd_env = {
"GITHUB_REPOSITORY" => repo_nwo,
"GITHUB_EVENT_NAME" => "pull_request",
"GITHUB_EVENT_PATH" => event_file.path,
"GITHUB_STEP_SUMMARY" => "/dev/null"
}
# Merge action inputs into environment, formatting keys as INPUT_...
action_inputs_env = action_inputs.each_with_object({}) do |(name, value), h|
h["INPUT_#{name.to_s.upcase}"] = value unless value.nil?
end
env = dev_cmd_env.merge(action_inputs_env)
dev_cmd = [
"./node_modules/.bin/nodemon",
"--exec",
"node",
"-r",
"esbuild-register",
"src/main.ts"
]
Open3.popen2e(env, *dev_cmd) do |stdin, out|
while line = out.gets
puts line.gsub(@github_token, "<REDACTED>")
end
end
end
end
+40
View File
@@ -24,6 +24,8 @@ import {getRefs} from './git-refs'
import {groupDependenciesByManifest} from './utils'
import {commentPr, MAX_COMMENT_LENGTH} from './comment-pr'
import {getDeniedChanges} from './deny'
import * as artifact from '@actions/artifact'
import * as fs from 'fs'
async function delay(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms))
@@ -61,6 +63,41 @@ async function getComparison(
return comparison
}
export async function handleLargeSummary(
summaryContent: string
): Promise<string> {
const MAX_SUMMARY_SIZE = 1024 * 1024 // 1024k in bytes
if (Buffer.byteLength(summaryContent, 'utf8') <= MAX_SUMMARY_SIZE) {
return summaryContent
}
const artifactClient = new artifact.DefaultArtifactClient()
const artifactName = 'dependency-review-summary'
const files = ['summary.md']
try {
// Write the summary to a file
await fs.promises.writeFile('summary.md', summaryContent)
// Upload the artifact
await artifactClient.uploadArtifact(artifactName, files, '.', {
retentionDays: 1
})
// Return a minimal summary with a link to the artifact
return `# Dependency Review Summary
The full dependency review summary is too large to display here. Please download the artifact named "${artifactName}" to view the complete report.
[View full job summary](${process.env.GITHUB_SERVER_URL}/${process.env.GITHUB_REPOSITORY}/actions/runs/${process.env.GITHUB_RUN_ID})`
} catch (error) {
core.warning(
`Failed to handle large summary: ${error instanceof Error ? error.message : 'Unknown error'}`
)
return summaryContent
}
}
async function run(): Promise<void> {
try {
const config = await readConfig()
@@ -179,6 +216,9 @@ async function run(): Promise<void> {
let rendered = core.summary.stringify()
core.setOutput('comment-content', rendered)
// Handle large summaries by uploading as artifact
rendered = await handleLargeSummary(rendered)
// if the summary is oversized, replace with minimal version
if (rendered.length >= MAX_COMMENT_LENGTH) {
core.debug(
+4 -6
View File
@@ -112,7 +112,7 @@ export function addSummaryToSummary(
function addDenyListsDeprecationWarningToSummary(): void {
core.summary.addRaw(
`${icons.warning} <strong>Deprecation Warning</strong>: The <em>deny-licenses</em> option is deprecated for possible removal in the next major release. For more information, see actions/dependency-review-action/issues/938.`,
`${icons.warning} <strong>Deprecation Warning</strong>: The <em>deny-licenses</em> option is deprecated for possible removal in the next major release. For more information, see issue 997.`,
true
)
}
@@ -206,19 +206,17 @@ export function addLicensesToSummary(
if (config.allow_licenses && config.allow_licenses.length > 0) {
core.summary.addQuote(
`<strong>Allowed Licenses</strong>: ${config.allow_licenses.join(', ')}`
`<details><summary><strong>Allowed Licenses</strong>:</summary> ${config.allow_licenses.join(', ')}</details>`
)
}
if (config.deny_licenses && config.deny_licenses.length > 0) {
core.summary.addQuote(
`<strong>Denied Licenses</strong>: ${config.deny_licenses.join(', ')}`
`<details><summary><strong>Denied Licenses</strong>:</summary> ${config.deny_licenses.join(', ')}</details>`
)
}
if (config.allow_dependencies_licenses) {
core.summary.addQuote(
`<strong>Excluded from license check</strong>: ${config.allow_dependencies_licenses.join(
', '
)}`
`<details><summary><strong>Excluded from license check</strong>:</summary> ${config.allow_dependencies_licenses.join(', ')}</details>`
)
}