Skip to content

Commit 5f4db12

Browse files
authored
Use explicitlyReferencedComponentIds to mark directs
1 parent b242ddf commit 5f4db12

File tree

2 files changed

+149
-58
lines changed

2 files changed

+149
-58
lines changed

componentDetection.test.ts

Lines changed: 73 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import ComponentDetection from "./componentDetection";
1+
import ComponentDetection, { DependencyGraphs } from "./componentDetection";
22
import fs from "fs";
33

44
test("Downloads CLI", async () => {
@@ -70,7 +70,7 @@ describe("ComponentDetection.makePackageUrl", () => {
7070
});
7171

7272
describe("ComponentDetection.processComponentsToManifests", () => {
73-
test("adds package as direct dependency when no top level referrers", () => {
73+
test("adds package as direct dependency when it is listed as an explicitlyReferencedComponentIds", () => {
7474
const componentsFound = [
7575
{
7676
component: {
@@ -86,20 +86,29 @@ describe("ComponentDetection.processComponentsToManifests", () => {
8686
},
8787
isDevelopmentDependency: false,
8888
topLevelReferrers: [], // Empty = direct dependency
89-
locationsFoundAt: ["package.json"]
89+
locationsFoundAt: ["/package.json"]
9090
}
9191
];
9292

93-
const manifests = ComponentDetection.processComponentsToManifests(componentsFound);
93+
const dependencyGraphs: DependencyGraphs = {
94+
"/package.json": {
95+
graph: { "test-package": null },
96+
explicitlyReferencedComponentIds: ["test-package 1.0.0 - npm"],
97+
developmentDependencies: [],
98+
dependencies: []
99+
}
100+
};
101+
102+
const manifests = ComponentDetection.processComponentsToManifests(componentsFound, dependencyGraphs);
94103

95104
expect(manifests).toHaveLength(1);
96-
expect(manifests[0].name).toBe("package.json");
105+
expect(manifests[0].name).toBe("/package.json");
97106
expect(manifests[0].directDependencies()).toHaveLength(1);
98107
expect(manifests[0].indirectDependencies()).toHaveLength(0);
99108
expect(manifests[0].countDependencies()).toBe(1);
100109
});
101110

102-
test("adds package as indirect dependency when has top level referrers", () => {
111+
test("adds package as indirect dependency when it is not in explicitlyReferencedComponentIds", () => {
103112
const componentsFound = [
104113
{
105114
component: {
@@ -126,56 +135,75 @@ describe("ComponentDetection.processComponentsToManifests", () => {
126135
}
127136
}
128137
],
129-
locationsFoundAt: ["package.json"]
138+
locationsFoundAt: ["/package.json"]
130139
}
131140
];
132141

133-
const manifests = ComponentDetection.processComponentsToManifests(componentsFound);
142+
const dependencyGraphs: DependencyGraphs = {
143+
"/package.json": {
144+
graph: { "parent-package": null },
145+
explicitlyReferencedComponentIds: [],
146+
developmentDependencies: [],
147+
dependencies: []
148+
}
149+
};
150+
151+
const manifests = ComponentDetection.processComponentsToManifests(componentsFound, dependencyGraphs);
134152

135153
expect(manifests).toHaveLength(1);
136-
expect(manifests[0].name).toBe("package.json");
154+
expect(manifests[0].name).toBe("/package.json");
137155
expect(manifests[0].directDependencies()).toHaveLength(0);
138156
expect(manifests[0].indirectDependencies()).toHaveLength(1);
139157
expect(manifests[0].countDependencies()).toBe(1);
140158
});
159+
});
141160

142-
test("adds package as direct dependency when top level referrer is itself", () => {
143-
const componentsFound = [
144-
{
145-
component: {
146-
name: "test-package",
147-
version: "1.0.0",
148-
packageUrl: {
149-
Scheme: "pkg",
150-
Type: "npm",
151-
Name: "test-package",
152-
Version: "1.0.0"
153-
},
154-
id: "test-package 1.0.0 - npm"
155-
},
156-
isDevelopmentDependency: false,
157-
topLevelReferrers: [
158-
{
159-
name: "test-package",
160-
version: "1.0.0",
161-
packageUrl: {
162-
Scheme: "pkg",
163-
Type: "npm",
164-
Name: "test-package",
165-
Version: "1.0.0"
166-
}
167-
}
168-
],
169-
locationsFoundAt: ["package.json"]
161+
describe('normalizeDependencyGraphPaths', () => {
162+
test('converts absolute paths to relative paths based on filePath input', () => {
163+
// Simulate a repo at /repo and a scan root at /repo/packages
164+
const fakeCwd = '/workspaces';
165+
const filePathInput = 'my-super-cool-repo';
166+
const absBase = '/workspaces/my-super-cool-repo';
167+
const dependencyGraphs: DependencyGraphs = {
168+
'/workspaces/my-super-cool-repo/a/package.json': {
169+
graph: { 'foo': null },
170+
explicitlyReferencedComponentIds: [],
171+
developmentDependencies: [],
172+
dependencies: []
173+
},
174+
'/workspaces/my-super-cool-repo/b/package.json': {
175+
graph: { 'bar': null },
176+
explicitlyReferencedComponentIds: [],
177+
developmentDependencies: [],
178+
dependencies: []
170179
}
171-
];
172-
173-
const manifests = ComponentDetection.processComponentsToManifests(componentsFound);
180+
};
181+
// Patch process.cwd for this test
182+
const originalCwd = process.cwd;
183+
(process as any).cwd = () => fakeCwd;
184+
const normalized = ComponentDetection.normalizeDependencyGraphPaths(dependencyGraphs, filePathInput);
185+
// Restore process.cwd
186+
(process as any).cwd = originalCwd;
187+
expect(Object.keys(normalized)).toContain('/a/package.json');
188+
expect(Object.keys(normalized)).toContain('/b/package.json');
189+
expect(normalized['/a/package.json'].graph).toEqual({ 'foo': null });
190+
expect(normalized['/b/package.json'].graph).toEqual({ 'bar': null });
191+
});
192+
});
174193

175-
expect(manifests).toHaveLength(1);
176-
expect(manifests[0].name).toBe("package.json");
177-
expect(manifests[0].directDependencies()).toHaveLength(1);
178-
expect(manifests[0].indirectDependencies()).toHaveLength(0);
179-
expect(manifests[0].countDependencies()).toBe(1);
194+
describe('normalizeDependencyGraphPaths with real output.json', () => {
195+
test('converts absolute paths in output.json to relative paths using current cwd and filePath', () => {
196+
const output = JSON.parse(fs.readFileSync('./output.json', 'utf8'));
197+
const dependencyGraphs = output.dependencyGraphs;
198+
// Use the same filePath as the action default (".")
199+
const normalized = ComponentDetection.normalizeDependencyGraphPaths(dependencyGraphs, 'test');
200+
// Should contain /package.json and /package-lock.json as keys
201+
expect(Object.keys(normalized)).toContain('/package.json');
202+
expect(Object.keys(normalized)).toContain('/package-lock.json');
203+
// All keys should now be relative to the repo root (cwd) and start with '/'
204+
for (const key of Object.keys(normalized)) {
205+
expect(key.startsWith('/')).toBe(true);
206+
expect(key).not.toMatch(/^\w:\\|^\/\/|^\.{1,2}\//); // Not windows absolute, not network, not relative
207+
}
180208
});
181209
});

componentDetection.ts

Lines changed: 76 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import {
77
Package,
88
Snapshot,
99
Manifest,
10-
submitSnapshot
10+
submitSnapshot,
1111
} from '@github/dependency-submission-toolkit'
1212
import fetch from 'cross-fetch'
1313
import tar from 'tar'
@@ -16,6 +16,7 @@ import * as exec from '@actions/exec';
1616
import dotenv from 'dotenv'
1717
import { Context } from '@actions/github/lib/context'
1818
import { unmockedModulePathPatterns } from './jest.config'
19+
import path from 'path';
1920
dotenv.config();
2021

2122
export default class ComponentDetection {
@@ -70,10 +71,11 @@ export default class ComponentDetection {
7071
core.info("Getting manifests from results");
7172
const results = await fs.readFileSync(this.outputPath, 'utf8');
7273
var json: any = JSON.parse(results);
73-
return this.processComponentsToManifests(json.componentsFound);
74+
let dependencyGraphs: DependencyGraphs = this.normalizeDependencyGraphPaths(json.dependencyGraphs, core.getInput('filePath'));
75+
return this.processComponentsToManifests(json.componentsFound, dependencyGraphs);
7476
}
7577

76-
public static processComponentsToManifests(componentsFound: any[]): Manifest[] {
78+
public static processComponentsToManifests(componentsFound: any[], dependencyGraphs: DependencyGraphs): Manifest[] {
7779
// Parse the result file and add the packages to the package cache
7880
const packageCache = new PackageCache();
7981
const packages: Array<ComponentDetectionPackage> = [];
@@ -139,29 +141,40 @@ export default class ComponentDetection {
139141
const manifests: Array<Manifest> = [];
140142

141143
// Check the locationsFoundAt for every package and add each as a manifest
142-
this.addPackagesToManifests(packages, manifests);
144+
this.addPackagesToManifests(packages, manifests, dependencyGraphs);
143145

144146
return manifests;
145147
}
146148

147-
private static addPackagesToManifests(packages: Array<ComponentDetectionPackage>, manifests: Array<Manifest>): void {
149+
private static addPackagesToManifests(packages: Array<ComponentDetectionPackage>, manifests: Array<Manifest>, dependencyGraphs: DependencyGraphs): void {
148150
packages.forEach((pkg: ComponentDetectionPackage) => {
149151
pkg.locationsFoundAt.forEach((location: any) => {
150152
if (!manifests.find((manifest: Manifest) => manifest.name == location)) {
151153
const manifest = new Manifest(location, location);
152154
manifests.push(manifest);
153155
}
154156

155-
// Filter out self-references from topLevelReferrers
156-
const nonSelfReferrers = pkg.topLevelReferrers.filter((referrer: any) => {
157-
if (!referrer.packageUrlString) return false;
158-
return referrer.packageUrlString !== pkg.packageUrlString;
159-
});
157+
const depGraphEntry = dependencyGraphs[location];
158+
if (!depGraphEntry) {
159+
core.warning(`No dependency graph entry found for manifest location: ${location}`);
160+
return; // Skip this location if not found in dependencyGraphs
161+
}
160162

161-
if (nonSelfReferrers.length == 0) {
162-
manifests.find((manifest: Manifest) => manifest.name == location)?.addDirectDependency(pkg, ComponentDetection.getDependencyScope(pkg));
163+
const directDependencies = depGraphEntry.explicitlyReferencedComponentIds;
164+
if (directDependencies.includes(pkg.id)) {
165+
manifests
166+
.find((manifest: Manifest) => manifest.name == location)
167+
?.addDirectDependency(
168+
pkg,
169+
ComponentDetection.getDependencyScope(pkg)
170+
);
163171
} else {
164-
manifests.find((manifest: Manifest) => manifest.name == location)?.addIndirectDependency(pkg, ComponentDetection.getDependencyScope(pkg));
172+
manifests
173+
.find((manifest: Manifest) => manifest.name == location)
174+
?.addIndirectDependency(
175+
pkg,
176+
ComponentDetection.getDependencyScope(pkg)
177+
);
165178
}
166179
});
167180
});
@@ -249,6 +262,29 @@ export default class ComponentDetection {
249262
throw new Error("Failed to download latest release");
250263
}
251264
}
265+
266+
/**
267+
* Normalizes the keys of a DependencyGraphs object to be relative paths from the resolved filePath input.
268+
* @param dependencyGraphs The DependencyGraphs object to normalize.
269+
* @param filePathInput The filePath input (relative or absolute) from the action configuration.
270+
* @returns A new DependencyGraphs object with relative path keys.
271+
*/
272+
public static normalizeDependencyGraphPaths(
273+
dependencyGraphs: DependencyGraphs,
274+
filePathInput: string
275+
): DependencyGraphs {
276+
// Resolve the base directory from filePathInput (relative to cwd if not absolute)
277+
const baseDir = path.resolve(process.cwd(), filePathInput);
278+
const normalized: DependencyGraphs = {};
279+
for (const absPath in dependencyGraphs) {
280+
// Make the path relative to the baseDir
281+
let relPath = path.relative(baseDir, absPath).replace(/\\/g, '/');
282+
// Ensure leading slash to represent repo root
283+
if (!relPath.startsWith('/')) relPath = '/' + relPath;
284+
normalized[relPath] = dependencyGraphs[absPath];
285+
}
286+
return normalized;
287+
}
252288
}
253289

254290
class ComponentDetectionPackage extends Package {
@@ -261,6 +297,33 @@ class ComponentDetectionPackage extends Package {
261297
}
262298
}
263299

300+
/**
301+
* Types for the dependencyGraphs section of output.json
302+
*/
303+
export type DependencyGraph = {
304+
/**
305+
* The dependency graph: keys are component IDs, values are either null (no dependencies) or an array of component IDs (dependencies)
306+
*/
307+
graph: Record<string, string[] | null>;
308+
/**
309+
* Explicitly referenced component IDs
310+
*/
311+
explicitlyReferencedComponentIds: string[];
312+
/**
313+
* Development dependencies
314+
*/
315+
developmentDependencies: string[];
316+
/**
317+
* Regular dependencies
318+
*/
319+
dependencies: string[];
320+
};
321+
322+
/**
323+
* The top-level dependencyGraphs object: keys are manifest file paths, values are DependencyGraph objects
324+
*/
325+
export type DependencyGraphs = Record<string, DependencyGraph>;
326+
264327

265328

266329

0 commit comments

Comments
 (0)