Merge pull request #110 from advanced-security/juxtin/file-centric-manifests

Stop aggregating manifests in multi-module projects
This commit is contained in:
Kevin Dangoor
2025-05-21 13:56:28 -04:00
committed by GitHub
11 changed files with 216 additions and 213 deletions
+4 -8
View File
@@ -2,12 +2,12 @@
This is a GitHub Action that will generate a complete dependency graph for a Maven project and submit the graph to the GitHub repository so that the graph is complete and includes all the transitive dependencies. This is a GitHub Action that will generate a complete dependency graph for a Maven project and submit the graph to the GitHub repository so that the graph is complete and includes all the transitive dependencies.
The action will invoke maven using the `com.github.ferstl:depgraph-maven-plugin:4.0.2` plugin to generate JSON output of the complete dependency graph, which is then processed and submitted using the [Dependency Submission Toolkit](https://github.com/github/dependency-submission-toolkit) to the GitHub repository. The action will invoke maven using the `com.github.ferstl:depgraph-maven-plugin:4.0.3` plugin to generate JSON output of the complete dependency graph, which is then processed and submitted using the [Dependency Submission Toolkit](https://github.com/github/dependency-submission-toolkit) to the GitHub repository.
## Usage ## Usage
As of version `3.0.0` this action now support Maven multi-module projects as well as additional Maven configuration parameters. As of version `3.0.0` this action now supports Maven multi-module projects as well as additional Maven configuration parameters. As of version `5.0.0`, multi-module projects report dependencies as coming from their respective `pom.xml` files.
### Pre-requisites ### Pre-requisites
@@ -15,7 +15,7 @@ For this action to work properly, you must have the Maven available on PATH (`mv
Custom maven `settings.xml` can now be specified as an input parameter to the action. Custom maven `settings.xml` can now be specified as an input parameter to the action.
This action writes informations in the repository dependency graph, so if you are using the default token, you need to set the `contents: write` permission to the workflow or job. If you are using a personal access token, this token must have the `repo` scope. ([API used by this action](https://docs.github.com/en/rest/dependency-graph/dependency-submission#create-a-snapshot-of-dependencies-for-a-repository)) This action writes information in the repository dependency graph, so if you are using the default token, you need to set the `contents: write` permission to the workflow or job. If you are using a personal access token, this token must have the `repo` scope. ([API used by this action](https://docs.github.com/en/rest/dependency-graph/dependency-submission#create-a-snapshot-of-dependencies-for-a-repository))
### Inputs ### Inputs
@@ -29,10 +29,6 @@ This action writes informations in the repository dependency graph, so if you ar
* `maven-args` - An optional string value (space separated) options to pass to the maven command line when generating the dependency snapshot. This is empty by default. * `maven-args` - An optional string value (space separated) options to pass to the maven command line when generating the dependency snapshot. This is empty by default.
* `snapshot-include-file-name`: Optional flag to control whether or no the path and file name of the pom.xml is provided with the snapshot submission. Defaults to `true` so as to create a link to the repository file from the dependency tree view, but at the cost of losing the POM `artifactId` when it renders.
* `snapshot-dependency-file-name`: An optional user control file path to the POM file, requires `snapshot-include-file-name` to be `true` for the value to be submitted.
* `correlator`: An optional identifier to distinguish between multiple dependency snapshots of the same type. Defaults to the [job_id](https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_id) of the current job. * `correlator`: An optional identifier to distinguish between multiple dependency snapshots of the same type. Defaults to the [job_id](https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_id) of the current job.
## Examples ## Examples
@@ -41,7 +37,7 @@ Generating and submitting a dependency snapshot using the defaults:
``` ```
- name: Submit Dependency Snapshot - name: Submit Dependency Snapshot
uses: advanced-security/maven-dependency-submission-action@v4 uses: advanced-security/maven-dependency-submission-action@v5
``` ```
Upon success it will generate a snapshot captured from Maven POM like; Upon success it will generate a snapshot captured from Maven POM like;
-10
View File
@@ -25,16 +25,6 @@ inputs:
type: string type: string
default: '' default: ''
snapshot-include-file-name:
description: Optionally include the file name in the dependency snapshot report to GitHub. This is required to be true if you want the results in the dependency tree to have a working link.
type: boolean
default: true
snapshot-dependency-file-name:
description: An optional override to specify the path to the file in the repository that the snapshot should be associated with.
type: string
required: false
token: token:
description: The GitHub token to use to submit the depedency snapshot to the repository description: The GitHub token to use to submit the depedency snapshot to the repository
type: string type: string
+76 -90
View File
@@ -7,10 +7,11 @@ require('./sourcemap-register.js');/******/ (() => { // webpackBootstrap
"use strict"; "use strict";
Object.defineProperty(exports, "__esModule", ({ value: true })); Object.defineProperty(exports, "__esModule", ({ value: true }));
exports.artifactToPackageURL = exports.parseDependencyJson = exports.MavenDependencyGraph = void 0; exports.artifactToPackageURL = exports.parseDependencyJson = exports.MavenDependencyGraph = exports.depgraphfilename = void 0;
const packageurl_js_1 = __nccwpck_require__(8915); const packageurl_js_1 = __nccwpck_require__(8915);
const dependency_submission_toolkit_1 = __nccwpck_require__(3415); const dependency_submission_toolkit_1 = __nccwpck_require__(3415);
const file_utils_1 = __nccwpck_require__(799); const file_utils_1 = __nccwpck_require__(799);
exports.depgraphfilename = 'maven-dependency-submission-action-depgraph.json';
class MavenDependencyGraph { class MavenDependencyGraph {
constructor(graph) { constructor(graph) {
this.depGraph = graph; this.depGraph = graph;
@@ -119,20 +120,20 @@ class MavenDependencyGraph {
} }
} }
exports.MavenDependencyGraph = MavenDependencyGraph; exports.MavenDependencyGraph = MavenDependencyGraph;
function parseDependencyJson(file, isMultiModule = false) { function parseDependencyJson(file) {
const data = (0, file_utils_1.loadFileContents)(file); const data = (0, file_utils_1.loadFileContents)(file);
const pomXmlFilepath = file.replace(`target/${exports.depgraphfilename}`, 'pom.xml');
if (!data) { if (!data) {
return { return {
filePath: pomXmlFilepath,
graphName: 'empty', graphName: 'empty',
artifacts: [], artifacts: [],
dependencies: [], dependencies: [],
isMultiModule: isMultiModule
}; };
} }
try { try {
const depGraph = JSON.parse(data); const depGraph = JSON.parse(data);
depGraph.isMultiModule = isMultiModule; return Object.assign(Object.assign({}, depGraph), { filePath: pomXmlFilepath });
return depGraph;
} }
catch (err) { catch (err) {
throw new Error(`Failed to parse JSON dependency data: ${err.message}`); throw new Error(`Failed to parse JSON dependency data: ${err.message}`);
@@ -252,8 +253,6 @@ function run() {
mavenArgs: core.getInput('maven-args') || '', mavenArgs: core.getInput('maven-args') || '',
}; };
const snapshotConfig = { const snapshotConfig = {
includeManifestFile: core.getBooleanInput('snapshot-include-file-name'),
manifestFile: core.getInput('snapshot-dependency-file-name'),
sha: core.getInput('snapshot-sha'), sha: core.getInput('snapshot-sha'),
ref: core.getInput('snapshot-ref'), ref: core.getInput('snapshot-ref'),
}; };
@@ -482,56 +481,45 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
}); });
}; };
Object.defineProperty(exports, "__esModule", ({ value: true })); Object.defineProperty(exports, "__esModule", ({ value: true }));
exports.generateDependencyGraph = exports.generateSnapshot = void 0; exports.generateDependencyGraphs = exports.generateSnapshot = void 0;
const core = __importStar(__nccwpck_require__(2186)); const core = __importStar(__nccwpck_require__(2186));
const path = __importStar(__nccwpck_require__(1017)); const path = __importStar(__nccwpck_require__(1017));
const dependency_submission_toolkit_1 = __nccwpck_require__(3415); const dependency_submission_toolkit_1 = __nccwpck_require__(3415);
const depgraph_1 = __nccwpck_require__(8047); const depgraph_1 = __nccwpck_require__(8047);
const maven_runner_1 = __nccwpck_require__(7433); const maven_runner_1 = __nccwpck_require__(7433);
const file_utils_1 = __nccwpck_require__(799); const fs_1 = __nccwpck_require__(7147);
const packageData = __nccwpck_require__(2876); const packageData = __nccwpck_require__(2876);
const DEPGRAPH_MAVEN_PLUGIN_VERSION = '4.0.3'; const DEPGRAPH_MAVEN_PLUGIN_VERSION = '4.0.3';
function generateSnapshot(directory, mvnConfig, snapshotConfig) { function generateSnapshot(directory, mvnConfig, snapshotConfig) {
return __awaiter(this, void 0, void 0, function* () { return __awaiter(this, void 0, void 0, function* () {
var _a, _b; var _a, _b;
const depgraph = yield generateDependencyGraph(directory, mvnConfig); const depgraphs = yield generateDependencyGraphs(directory, mvnConfig);
const detector = (_a = snapshotConfig === null || snapshotConfig === void 0 ? void 0 : snapshotConfig.detector) !== null && _a !== void 0 ? _a : getDetector();
let snapshot = new dependency_submission_toolkit_1.Snapshot(detector, snapshotConfig === null || snapshotConfig === void 0 ? void 0 : snapshotConfig.context, snapshotConfig === null || snapshotConfig === void 0 ? void 0 : snapshotConfig.job);
snapshot.job.correlator = (snapshotConfig === null || snapshotConfig === void 0 ? void 0 : snapshotConfig.correlator)
? snapshotConfig.correlator
: (_b = snapshot.job) === null || _b === void 0 ? void 0 : _b.correlator;
const specifiedRef = getNonEmptyValue(snapshotConfig === null || snapshotConfig === void 0 ? void 0 : snapshotConfig.ref);
if (specifiedRef) {
snapshot.ref = specifiedRef;
}
const specifiedSha = getNonEmptyValue(snapshot === null || snapshot === void 0 ? void 0 : snapshot.sha);
if (specifiedSha) {
snapshot.sha = specifiedSha;
}
try { try {
const mavenDependencies = new depgraph_1.MavenDependencyGraph(depgraph); for (const depgraph of depgraphs) {
let manifest; const mavenDependencies = new depgraph_1.MavenDependencyGraph(depgraph);
if (snapshotConfig === null || snapshotConfig === void 0 ? void 0 : snapshotConfig.includeManifestFile) { const pomFile = getRepositoryRelativePath(depgraph.filePath);
let pomFile; const manifest = mavenDependencies.createManifest(pomFile);
if (snapshotConfig === null || snapshotConfig === void 0 ? void 0 : snapshotConfig.manifestFile) { snapshot.addManifest(manifest);
pomFile = snapshotConfig.manifestFile;
}
else {
// The filepath to the POM needs to be relative to the root of the GitHub repository for the links to work once uploaded
pomFile = getRepositoryRelativePath(path.join(directory, 'pom.xml'));
}
manifest = mavenDependencies.createManifest(pomFile);
} }
else {
manifest = mavenDependencies.createManifest();
}
const detector = (_a = snapshotConfig === null || snapshotConfig === void 0 ? void 0 : snapshotConfig.detector) !== null && _a !== void 0 ? _a : getDetector();
const snapshot = new dependency_submission_toolkit_1.Snapshot(detector, snapshotConfig === null || snapshotConfig === void 0 ? void 0 : snapshotConfig.context, snapshotConfig === null || snapshotConfig === void 0 ? void 0 : snapshotConfig.job);
snapshot.addManifest(manifest);
snapshot.job.correlator = (snapshotConfig === null || snapshotConfig === void 0 ? void 0 : snapshotConfig.correlator)
? snapshotConfig.correlator
: (_b = snapshot.job) === null || _b === void 0 ? void 0 : _b.correlator;
const specifiedRef = getNonEmptyValue(snapshotConfig === null || snapshotConfig === void 0 ? void 0 : snapshotConfig.ref);
if (specifiedRef) {
snapshot.ref = specifiedRef;
}
const specifiedSha = getNonEmptyValue(snapshot === null || snapshot === void 0 ? void 0 : snapshot.sha);
if (specifiedSha) {
snapshot.sha = specifiedSha;
}
return snapshot;
} }
catch (err) { catch (err) {
core.error(err); core.error(err);
throw new Error(`Could not generate a snapshot of the dependencies; ${err.message}`); throw new Error(`Could not generate a snapshot of the dependencies; ${err.message}`);
} }
return snapshot;
}); });
} }
exports.generateSnapshot = generateSnapshot; exports.generateSnapshot = generateSnapshot;
@@ -542,71 +530,44 @@ function getDetector() {
version: packageData.version version: packageData.version
}; };
} }
function generateDependencyGraph(directory, config) { function generateDependencyGraphs(directory, config) {
return __awaiter(this, void 0, void 0, function* () { return __awaiter(this, void 0, void 0, function* () {
try { try {
const mvn = new maven_runner_1.MavenRunner(directory, config === null || config === void 0 ? void 0 : config.settingsFile, config === null || config === void 0 ? void 0 : config.ignoreMavenWrapper, config === null || config === void 0 ? void 0 : config.mavenArgs); const mvn = new maven_runner_1.MavenRunner(directory, config === null || config === void 0 ? void 0 : config.settingsFile, config === null || config === void 0 ? void 0 : config.ignoreMavenWrapper, config === null || config === void 0 ? void 0 : config.mavenArgs);
core.startGroup('depgraph-maven-plugin:reactor');
const mavenReactorArguments = [
`com.github.ferstl:depgraph-maven-plugin:${DEPGRAPH_MAVEN_PLUGIN_VERSION}:reactor`,
'-DgraphFormat=json',
'-DoutputFileName=reactor.json'
];
const reactorResults = yield mvn.exec(directory, mavenReactorArguments);
core.info(reactorResults.stdout);
core.info(reactorResults.stderr);
core.endGroup();
if (reactorResults.exitCode !== 0) {
throw new Error(`Failed to successfully generate reactor results with Maven, exit code: ${reactorResults.exitCode}`);
}
core.startGroup('depgraph-maven-plugin:aggregate'); core.startGroup('depgraph-maven-plugin:aggregate');
const mavenAggregateArguments = [ const mavenGraphArguments = [
`com.github.ferstl:depgraph-maven-plugin:${DEPGRAPH_MAVEN_PLUGIN_VERSION}:aggregate`, `com.github.ferstl:depgraph-maven-plugin:${DEPGRAPH_MAVEN_PLUGIN_VERSION}:graph`,
'-DgraphFormat=json', '-DgraphFormat=json',
'-DoutputDirectory=target', `-DoutputFileName=${depgraph_1.depgraphfilename}`,
'-DoutputFileName=aggregate-depgraph.json'
]; ];
const aggregateResults = yield mvn.exec(directory, mavenAggregateArguments); const graphResults = yield mvn.exec(directory, mavenGraphArguments);
core.info(aggregateResults.stdout); core.info(graphResults.stdout);
core.info(aggregateResults.stderr); core.info(graphResults.stderr);
core.endGroup(); core.endGroup();
if (aggregateResults.exitCode !== 0) { if (graphResults.exitCode !== 0) {
throw new Error(`Failed to successfully dependency results with Maven, exit code: ${aggregateResults.exitCode}`); throw new Error(`Failed to successfully dependency results with Maven, exit code: ${graphResults.exitCode}`);
} }
} }
catch (err) { catch (err) {
core.error(err); core.error(err);
throw new Error(`A problem was encountered generating dependency files, please check execution logs for details; ${err.message}`); throw new Error(`A problem was encountered generating dependency files, please check execution logs for details; ${err.message}`);
} }
const targetPath = path.join(directory, 'target'); const graphFiles = getDepgraphFiles(directory, depgraph_1.depgraphfilename);
const isMultiModule = checkForMultiModule(path.join(targetPath, 'reactor.json')); let results = [];
// Now we have the aggregate dependency graph file to process for (const graphFile of graphFiles) {
const aggregateGraphFile = path.join(targetPath, 'aggregate-depgraph.json'); core.debug(`Found depgraph file: ${graphFile}`);
try { try {
return (0, depgraph_1.parseDependencyJson)(aggregateGraphFile, isMultiModule); const depgraph = (0, depgraph_1.parseDependencyJson)(graphFile);
} results.push(depgraph);
catch (err) { }
core.error(err); catch (err) {
throw new Error(`Could not parse maven dependency file, '${aggregateGraphFile}': ${err.message}`); core.error(`Could not parse depgraph file, '${graphFile}': ${err.message}`);
}
} }
return results;
}); });
} }
exports.generateDependencyGraph = generateDependencyGraph; exports.generateDependencyGraphs = generateDependencyGraphs;
function checkForMultiModule(reactorJsonFile) {
const data = (0, file_utils_1.loadFileContents)(reactorJsonFile);
if (data) {
try {
const reactor = JSON.parse(data);
// The reactor file will have an array of artifacts making up the parent and child modules if it is a multi module project
return reactor.artifacts && reactor.artifacts.length > 0;
}
catch (err) {
throw new Error(`Failed to parse reactor JSON payload: ${err.message}`);
}
}
// If no data report that it is not a multi module project
return false;
}
// TODO this is assuming the checkout was made into the base path of the workspace... // TODO this is assuming the checkout was made into the base path of the workspace...
function getRepositoryRelativePath(file) { function getRepositoryRelativePath(file) {
const workspaceDirectory = path.resolve(process.env.GITHUB_WORKSPACE || '.'); const workspaceDirectory = path.resolve(process.env.GITHUB_WORKSPACE || '.');
@@ -631,6 +592,31 @@ function getNonEmptyValue(str) {
} }
return undefined; return undefined;
} }
// getDepgraphFiles recursively finds all files that match the filename within the directory
function getDepgraphFiles(directory, filename) {
let files = [];
// debug only
files = (0, fs_1.readdirSync)(directory);
try {
files = (0, fs_1.readdirSync)(directory)
.filter((f) => f === filename)
.map((f) => path.join(directory, f));
}
catch (err) {
core.error(`Could not read depgraphs directory: ${err.message}`);
return [];
}
// recursively find all files that match the filename within the directory
const subdirs = (0, fs_1.readdirSync)(directory, { withFileTypes: true })
.filter(dirent => dirent.isDirectory())
.map(dirent => dirent.name);
for (const subdir of subdirs) {
const subdirPath = path.join(directory, subdir);
const subdirFiles = getDepgraphFiles(subdirPath, filename);
files = files.concat(subdirFiles);
}
return files;
}
//# sourceMappingURL=snapshot-generator.js.map //# sourceMappingURL=snapshot-generator.js.map
/***/ }), /***/ }),
@@ -33301,7 +33287,7 @@ exports.submitSnapshot = L;
/***/ ((module) => { /***/ ((module) => {
"use strict"; "use strict";
module.exports = JSON.parse('{"name":"maven-dependency-submission-action","version":"4.1.2","description":"Submit Maven dependencies to GitHub dependency submission API","main":"index.js","scripts":{"base-build":"npm ci && tsc","build":"npm run base-build && npm exec -- @vercel/ncc build --source-map lib/src/index.js","build-exe":"npm run build && pkg package.json --compress Gzip","test":"vitest --run"},"repository":{"type":"git","url":"git+https://github.com/advanced-security/maven-dependency-submission-action.git"},"keywords":[],"author":"GitHub, Inc","license":"MIT","bugs":{"url":"https://github.com/advanced-security/maven-dependency-submission-action/issues"},"homepage":"https://github.com/advanced-security/maven-dependency-submission-action","dependencies":{"@actions/core":"^1.10.1","@actions/exec":"^1.1.1","@github/dependency-submission-toolkit":"^2.0.0","commander":"^12.0.0","packageurl-js":"^1.2.0"},"devDependencies":{"@types/chai":"^4.3.1","@vercel/ncc":"^0.38.1","chai":"^4.3.6","@yao-pkg/pkg":"^5.11.5","ts-node":"^10.9.2","typescript":"^5.3.3","vitest":"^1.6.1"},"bin":{"cli":"lib/src/executable/cli.js"},"pkg":{"targets":["node20-linux-x64","node20-win-x64","node20-macos-x64"],"assets":["package.json"],"publicPackages":"*","outputPath":"cli"}}'); module.exports = JSON.parse('{"name":"maven-dependency-submission-action","version":"5.0.0","description":"Submit Maven dependencies to GitHub dependency submission API","main":"index.js","scripts":{"base-build":"npm ci && tsc","build":"npm run base-build && npm exec -- @vercel/ncc build --source-map lib/src/index.js","build-exe":"npm run build && pkg package.json --compress Gzip","test":"vitest --run"},"repository":{"type":"git","url":"git+https://github.com/advanced-security/maven-dependency-submission-action.git"},"keywords":[],"author":"GitHub, Inc","license":"MIT","bugs":{"url":"https://github.com/advanced-security/maven-dependency-submission-action/issues"},"homepage":"https://github.com/advanced-security/maven-dependency-submission-action","dependencies":{"@actions/core":"^1.10.1","@actions/exec":"^1.1.1","@github/dependency-submission-toolkit":"^2.0.0","commander":"^12.0.0","packageurl-js":"^1.2.0"},"devDependencies":{"@types/chai":"^4.3.1","@vercel/ncc":"^0.38.1","chai":"^4.3.6","@yao-pkg/pkg":"^5.11.5","ts-node":"^10.9.2","typescript":"^5.3.3","vitest":"^1.6.1"},"bin":{"cli":"lib/src/executable/cli.js"},"pkg":{"targets":["node20-linux-x64","node20-win-x64","node20-macos-x64"],"assets":["package.json"],"publicPackages":"*","outputPath":"cli"}}');
/***/ }) /***/ })
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "maven-dependency-submission-action", "name": "maven-dependency-submission-action",
"version": "4.1.3", "version": "5.0.0",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "maven-dependency-submission-action", "name": "maven-dependency-submission-action",
"version": "4.1.3", "version": "5.0.0",
"description": "Submit Maven dependencies to GitHub dependency submission API", "description": "Submit Maven dependencies to GitHub dependency submission API",
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {
+10 -5
View File
@@ -4,10 +4,10 @@ import { DependencyScope } from '@github/dependency-submission-toolkit';
import { loadFileContents } from './utils/file-utils'; import { loadFileContents } from './utils/file-utils';
export type Depgraph = { export type Depgraph = {
filePath: string,
graphName: string, graphName: string,
artifacts: DepgraphArtifact[], artifacts: DepgraphArtifact[],
dependencies: DepgraphDependency[], dependencies: DepgraphDependency[],
isMultiModule: boolean,
} }
export type DepgraphArtifact = { export type DepgraphArtifact = {
@@ -30,6 +30,8 @@ export type DepgraphDependency = {
resolution: string, resolution: string,
} }
export const depgraphfilename = 'maven-dependency-submission-action-depgraph.json';
export class MavenDependencyGraph { export class MavenDependencyGraph {
private depGraph: Depgraph; private depGraph: Depgraph;
@@ -171,22 +173,25 @@ export class MavenDependencyGraph {
} }
} }
export function parseDependencyJson(file: string, isMultiModule: boolean = false): Depgraph { export function parseDependencyJson(file: string): Depgraph {
const data = loadFileContents(file); const data = loadFileContents(file);
const pomXmlFilepath = file.replace(`target/${depgraphfilename}`, 'pom.xml');
if (!data) { if (!data) {
return { return {
filePath: pomXmlFilepath,
graphName: 'empty', graphName: 'empty',
artifacts: [], artifacts: [],
dependencies: [], dependencies: [],
isMultiModule: isMultiModule
}; };
} }
try { try {
const depGraph: Depgraph = JSON.parse(data); const depGraph: Depgraph = JSON.parse(data);
depGraph.isMultiModule = isMultiModule; return {
return depGraph; ...depGraph,
filePath: pomXmlFilepath,
};
} catch (err: any) { } catch (err: any) {
throw new Error(`Failed to parse JSON dependency data: ${err.message}`); throw new Error(`Failed to parse JSON dependency data: ${err.message}`);
} }
-3
View File
@@ -18,7 +18,6 @@ program.option('-j --job-name <jobName>', 'Optional name for the activity creati
program.option('-i --run-id <jobName>', 'Optional Run ID number for the activity that is providing the graph'); program.option('-i --run-id <jobName>', 'Optional Run ID number for the activity that is providing the graph');
program.option('--snapshot-exclude-file-name', 'exclude the file name in the dependency snapshot report. If false the name of the artifactor from the POM will be used, but any links in GitHub will not work.'); program.option('--snapshot-exclude-file-name', 'exclude the file name in the dependency snapshot report. If false the name of the artifactor from the POM will be used, but any links in GitHub will not work.');
program.option('--snapshot-dependency-file-name <fileName>', 'optional override to specificy the path to the file that the snapshot will be associated with in the repository');
program.option('--detector-name <detectorName>', 'optional name of the detector that generated the snapshot'); program.option('--detector-name <detectorName>', 'optional name of the detector that generated the snapshot');
program.option('--detector-url <detectorUrl>', 'optional URL of the detector that generated the snapshot, but not optional if you specify an detector-name'); program.option('--detector-url <detectorUrl>', 'optional URL of the detector that generated the snapshot, but not optional if you specify an detector-name');
@@ -92,8 +91,6 @@ async function execute() {
sha: opts.sha, sha: opts.sha,
ref: opts.branchRef, ref: opts.branchRef,
manifestFile: opts.snapshotDependencyFileName,
includeManifestFile: !opts.snapshotExcludeFileName,
detector: detector detector: detector
} }
-2
View File
@@ -13,8 +13,6 @@ async function run() {
mavenArgs: core.getInput('maven-args') || '', mavenArgs: core.getInput('maven-args') || '',
} }
const snapshotConfig: SnapshotConfig = { const snapshotConfig: SnapshotConfig = {
includeManifestFile: core.getBooleanInput('snapshot-include-file-name'),
manifestFile: core.getInput('snapshot-dependency-file-name'),
sha: core.getInput('snapshot-sha'), sha: core.getInput('snapshot-sha'),
ref: core.getInput('snapshot-ref'), ref: core.getInput('snapshot-ref'),
} }
+55 -5
View File
@@ -1,6 +1,7 @@
import { getMavenProjectDirectory } from './utils/test-util'; import { getMavenProjectDirectory } from './utils/test-util';
import { generateDependencyGraph, generateSnapshot } from './snapshot-generator'; import { generateDependencyGraphs, generateSnapshot } from './snapshot-generator';
import {describe, it, expect} from 'vitest'; import {describe, it, expect} from 'vitest';
import { Manifest } from '@github/dependency-submission-toolkit';
describe('snapshot-generator', () => { describe('snapshot-generator', () => {
@@ -8,7 +9,11 @@ describe('snapshot-generator', () => {
it('should generate a snapshot for a simple project', async () => { it('should generate a snapshot for a simple project', async () => {
const projectDir = getMavenProjectDirectory('simple'); const projectDir = getMavenProjectDirectory('simple');
const depGraph = await generateDependencyGraph(projectDir); const depGraphs = await generateDependencyGraphs(projectDir);
expect(depGraphs).toBeDefined();
expect(depGraphs.length).toBe(1);
const depGraph = depGraphs[0];
expect(depGraph.dependencies.length).toBe(20); expect(depGraph.dependencies.length).toBe(20);
}, 20000); }, 20000);
}); });
@@ -37,9 +42,50 @@ describe('snapshot-generator', () => {
const projectDir = getMavenProjectDirectory('multi-module-multi-branch'); const projectDir = getMavenProjectDirectory('multi-module-multi-branch');
const snapshot = await generateSnapshot(projectDir); const snapshot = await generateSnapshot(projectDir);
expect(snapshot.manifests['bs-parent']).toBeDefined();
expect(snapshot.detector.version).toBe(version); expect(snapshot.detector.version).toBe(version);
expect(snapshot.manifests['bs-parent'].countDependencies()).toBe(20);
const bsParentManifest = snapshot.manifests['bs-parent'];
expect(bsParentManifest).toBeDefined();
expect(getDirectDependencyPurls(bsParentManifest)).toEqual([
'pkg:maven/junit/junit@4.13?type=jar']);
const bsApplicationManifest = snapshot.manifests['bs-application'];
expect(bsApplicationManifest).toBeDefined();
expect(getDirectDependencyPurls(bsApplicationManifest)).toEqual([
'pkg:maven/com.github.octodemo/bs-library-web@1.0.0-SNAPSHOT?type=jar',
'pkg:maven/junit/junit@4.13?type=jar',
'pkg:maven/org.eclipse.jetty/jetty-server@10.0.10?type=jar',
]);
const bsLibrariesManifest = snapshot.manifests['bs-libraries'];
expect(bsLibrariesManifest).toBeDefined();
expect(getDirectDependencyPurls(bsLibrariesManifest)).toEqual([
'pkg:maven/junit/junit@4.13?type=jar',
'pkg:maven/org.apache.logging.log4j/log4j-api@2.19.0?type=jar',
]);
const bsOtherManifest = snapshot.manifests['bs-other'];
expect(bsOtherManifest).toBeDefined();
expect(getDirectDependencyPurls(bsOtherManifest)).toEqual([
'pkg:maven/junit/junit@4.13?type=jar',
]);
const bsLibraryDatabaseManifest = snapshot.manifests['bs-library-database'];
expect(bsLibraryDatabaseManifest).toBeDefined();
expect(getDirectDependencyPurls(bsLibraryDatabaseManifest)).toEqual([
'pkg:maven/junit/junit@4.13?type=jar',
'pkg:maven/org.apache.logging.log4j/log4j-api@2.19.0?type=jar',
'pkg:maven/org.postgresql/postgresql@42.5.0?type=jar',
'pkg:maven/org.xerial/sqlite-jdbc@3.36.0.3?type=jar',
]);
const bsLibraryWebManifest = snapshot.manifests['bs-library-web'];
expect(bsLibraryWebManifest).toBeDefined();
expect(getDirectDependencyPurls(bsLibraryWebManifest)).toEqual([
'pkg:maven/junit/junit@4.13?type=jar',
'pkg:maven/org.apache.logging.log4j/log4j-api@2.19.0?type=jar',
'pkg:maven/org.eclipse.jetty.http2/http2-http-client-transport@10.0.10?type=jar',
]);
}, 20000); }, 20000);
it('should generate a snapshot for a maven-wrapper project', async () => { it('should generate a snapshot for a maven-wrapper project', async () => {
@@ -94,4 +140,8 @@ describe('snapshot-generator', () => {
expect(snapshot.job.correlator).toBe('jobCorrelator'); expect(snapshot.job.correlator).toBe('jobCorrelator');
}, 20000); }, 20000);
}); });
}); });
function getDirectDependencyPurls(manifest: Manifest): string[] {
return Object.values(manifest.resolved).filter(dep => dep.relationship === 'direct').map(dep => dep.depPackage.packageURL.toString()).sort();
}
+68 -87
View File
@@ -2,9 +2,10 @@ import * as core from '@actions/core';
import * as path from 'path'; import * as path from 'path';
import { Manifest, Snapshot } from '@github/dependency-submission-toolkit'; import { Manifest, Snapshot } from '@github/dependency-submission-toolkit';
import { Depgraph, MavenDependencyGraph, parseDependencyJson } from './depgraph'; import { Depgraph, MavenDependencyGraph, parseDependencyJson, depgraphfilename } from './depgraph';
import { MavenRunner } from './maven-runner'; import { MavenRunner } from './maven-runner';
import { loadFileContents } from './utils/file-utils'; import { loadFileContents } from './utils/file-utils';
import { readdirSync } from 'fs';
const packageData = require('../package.json'); const packageData = require('../package.json');
const DEPGRAPH_MAVEN_PLUGIN_VERSION = '4.0.3'; const DEPGRAPH_MAVEN_PLUGIN_VERSION = '4.0.3';
@@ -16,8 +17,6 @@ export type MavenConfiguration = {
} }
export type SnapshotConfig = { export type SnapshotConfig = {
includeManifestFile?: boolean;
manifestFile?: string;
context?: any; context?: any;
job?: any; job?: any;
sha?: any; sha?: any;
@@ -31,48 +30,37 @@ export type SnapshotConfig = {
}; };
export async function generateSnapshot(directory: string, mvnConfig?: MavenConfiguration, snapshotConfig?: SnapshotConfig) { export async function generateSnapshot(directory: string, mvnConfig?: MavenConfiguration, snapshotConfig?: SnapshotConfig) {
const depgraph = await generateDependencyGraph(directory, mvnConfig); const depgraphs = await generateDependencyGraphs(directory, mvnConfig);
const detector = snapshotConfig?.detector ?? getDetector();
let snapshot = new Snapshot(detector, snapshotConfig?.context, snapshotConfig?.job);
snapshot.job.correlator = snapshotConfig?.correlator
? snapshotConfig.correlator
: snapshot.job?.correlator;
const specifiedRef = getNonEmptyValue(snapshotConfig?.ref);
if (specifiedRef) {
snapshot.ref = specifiedRef;
}
const specifiedSha = getNonEmptyValue(snapshot?.sha);
if (specifiedSha) {
snapshot.sha = specifiedSha;
}
try { try {
const mavenDependencies = new MavenDependencyGraph(depgraph); for (const depgraph of depgraphs) {
const mavenDependencies = new MavenDependencyGraph(depgraph);
const pomFile = getRepositoryRelativePath(depgraph.filePath);
const manifest = mavenDependencies.createManifest(pomFile);
let manifest: Manifest; snapshot.addManifest(manifest);
if (snapshotConfig?.includeManifestFile) {
let pomFile;
if (snapshotConfig?.manifestFile) {
pomFile = snapshotConfig.manifestFile;
} else {
// The filepath to the POM needs to be relative to the root of the GitHub repository for the links to work once uploaded
pomFile = getRepositoryRelativePath(path.join(directory, 'pom.xml'));
}
manifest = mavenDependencies.createManifest(pomFile);
} else {
manifest = mavenDependencies.createManifest();
} }
const detector = snapshotConfig?.detector ?? getDetector();
const snapshot = new Snapshot(detector, snapshotConfig?.context, snapshotConfig?.job);
snapshot.addManifest(manifest);
snapshot.job.correlator = snapshotConfig?.correlator
? snapshotConfig.correlator
: snapshot.job?.correlator;
const specifiedRef = getNonEmptyValue(snapshotConfig?.ref);
if (specifiedRef) {
snapshot.ref = specifiedRef;
}
const specifiedSha = getNonEmptyValue(snapshot?.sha);
if (specifiedSha) {
snapshot.sha = specifiedSha;
}
return snapshot;
} catch (err: any) { } catch (err: any) {
core.error(err); core.error(err);
throw new Error(`Could not generate a snapshot of the dependencies; ${err.message}`); throw new Error(`Could not generate a snapshot of the dependencies; ${err.message}`);
} }
return snapshot;
} }
function getDetector() { function getDetector() {
@@ -83,75 +71,42 @@ function getDetector() {
}; };
} }
export async function generateDependencyGraph(directory: string, config?: MavenConfiguration): Promise<Depgraph> { export async function generateDependencyGraphs(directory: string, config?: MavenConfiguration): Promise<Depgraph[]> {
try { try {
const mvn = new MavenRunner(directory, config?.settingsFile, config?.ignoreMavenWrapper, config?.mavenArgs); const mvn = new MavenRunner(directory, config?.settingsFile, config?.ignoreMavenWrapper, config?.mavenArgs);
core.startGroup('depgraph-maven-plugin:reactor');
const mavenReactorArguments = [
`com.github.ferstl:depgraph-maven-plugin:${DEPGRAPH_MAVEN_PLUGIN_VERSION}:reactor`,
'-DgraphFormat=json',
'-DoutputFileName=reactor.json'
];
const reactorResults = await mvn.exec(directory, mavenReactorArguments);
core.info(reactorResults.stdout);
core.info(reactorResults.stderr);
core.endGroup();
if (reactorResults.exitCode !== 0) {
throw new Error(`Failed to successfully generate reactor results with Maven, exit code: ${reactorResults.exitCode}`);
}
core.startGroup('depgraph-maven-plugin:aggregate'); core.startGroup('depgraph-maven-plugin:aggregate');
const mavenAggregateArguments = [ const mavenGraphArguments = [
`com.github.ferstl:depgraph-maven-plugin:${DEPGRAPH_MAVEN_PLUGIN_VERSION}:aggregate`, `com.github.ferstl:depgraph-maven-plugin:${DEPGRAPH_MAVEN_PLUGIN_VERSION}:graph`,
'-DgraphFormat=json', '-DgraphFormat=json',
'-DoutputDirectory=target', `-DoutputFileName=${depgraphfilename}`,
'-DoutputFileName=aggregate-depgraph.json'
]; ];
const aggregateResults = await mvn.exec(directory, mavenAggregateArguments); const graphResults = await mvn.exec(directory, mavenGraphArguments);
core.info(aggregateResults.stdout); core.info(graphResults.stdout);
core.info(aggregateResults.stderr); core.info(graphResults.stderr);
core.endGroup(); core.endGroup();
if (aggregateResults.exitCode !== 0) { if (graphResults.exitCode !== 0) {
throw new Error(`Failed to successfully dependency results with Maven, exit code: ${aggregateResults.exitCode}`); throw new Error(`Failed to successfully generate dependency results with Maven, exit code: ${graphResults.exitCode}`);
} }
} catch (err: any) { } catch (err: any) {
core.error(err); core.error(err);
throw new Error(`A problem was encountered generating dependency files, please check execution logs for details; ${err.message}`); throw new Error(`A problem was encountered generating dependency files, please check execution logs for details; ${err.message}`);
} }
const targetPath = path.join(directory, 'target'); const graphFiles = getDepgraphFiles(directory, depgraphfilename);
const isMultiModule = checkForMultiModule(path.join(targetPath, 'reactor.json')); let results: Depgraph[] = [];
for (const graphFile of graphFiles) {
// Now we have the aggregate dependency graph file to process core.debug(`Found depgraph file: ${graphFile}`);
const aggregateGraphFile = path.join(targetPath, 'aggregate-depgraph.json');
try {
return parseDependencyJson(aggregateGraphFile, isMultiModule);
} catch (err: any) {
core.error(err);
throw new Error(`Could not parse maven dependency file, '${aggregateGraphFile}': ${err.message}`);
}
}
function checkForMultiModule(reactorJsonFile): boolean {
const data = loadFileContents(reactorJsonFile);
if (data) {
try { try {
const reactor = JSON.parse(data); const depgraph = parseDependencyJson(graphFile);
// The reactor file will have an array of artifacts making up the parent and child modules if it is a multi module project results.push(depgraph);
return reactor.artifacts && reactor.artifacts.length > 0;
} catch (err: any) { } catch (err: any) {
throw new Error(`Failed to parse reactor JSON payload: ${err.message}`); core.error(`Could not parse depgraph file, '${graphFile}': ${err.message}`);
} }
} }
return results;
// If no data report that it is not a multi module project
return false;
} }
// TODO this is assuming the checkout was made into the base path of the workspace... // TODO this is assuming the checkout was made into the base path of the workspace...
@@ -182,3 +137,29 @@ function getNonEmptyValue(str?: string) {
} }
return undefined; return undefined;
} }
// getDepgraphFiles recursively finds all files that match the filename within the directory
function getDepgraphFiles(directory: string, filename: string): string[] {
let files: string[] = [];
try {
files = readdirSync(directory)
.filter((f: string) => f === filename)
.map((f: string) => path.join(directory, f));
} catch (err: any) {
core.error(`Could not read depgraphs directory: ${err.message}`);
return [];
}
// recursively find all files that match the filename within the directory
const subdirs = readdirSync(directory, { withFileTypes: true })
.filter(dirent => dirent.isDirectory())
.map(dirent => dirent.name);
for (const subdir of subdirs) {
const subdirPath = path.join(directory, subdir);
const subdirFiles = getDepgraphFiles(subdirPath, filename);
files = files.concat(subdirFiles);
}
return files;
}