Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
118 changes: 73 additions & 45 deletions componentDetection.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import ComponentDetection from "./componentDetection";
import ComponentDetection, { DependencyGraphs } from "./componentDetection";
import fs from "fs";

test("Downloads CLI", async () => {
Expand Down Expand Up @@ -70,7 +70,7 @@ describe("ComponentDetection.makePackageUrl", () => {
});

describe("ComponentDetection.processComponentsToManifests", () => {
test("adds package as direct dependency when no top level referrers", () => {
test("adds package as direct dependency when it is listed as an explicitlyReferencedComponentIds", () => {
const componentsFound = [
{
component: {
Expand All @@ -86,20 +86,29 @@ describe("ComponentDetection.processComponentsToManifests", () => {
},
isDevelopmentDependency: false,
topLevelReferrers: [], // Empty = direct dependency
locationsFoundAt: ["package.json"]
locationsFoundAt: ["/package.json"]
}
];

const manifests = ComponentDetection.processComponentsToManifests(componentsFound);
const dependencyGraphs: DependencyGraphs = {
"/package.json": {
graph: { "test-package": null },
explicitlyReferencedComponentIds: ["test-package 1.0.0 - npm"],
developmentDependencies: [],
dependencies: []
}
};

const manifests = ComponentDetection.processComponentsToManifests(componentsFound, dependencyGraphs);

expect(manifests).toHaveLength(1);
expect(manifests[0].name).toBe("package.json");
expect(manifests[0].name).toBe("/package.json");
expect(manifests[0].directDependencies()).toHaveLength(1);
expect(manifests[0].indirectDependencies()).toHaveLength(0);
expect(manifests[0].countDependencies()).toBe(1);
});

test("adds package as indirect dependency when has top level referrers", () => {
test("adds package as indirect dependency when it is not in explicitlyReferencedComponentIds", () => {
const componentsFound = [
{
component: {
Expand All @@ -126,56 +135,75 @@ describe("ComponentDetection.processComponentsToManifests", () => {
}
}
],
locationsFoundAt: ["package.json"]
locationsFoundAt: ["/package.json"]
}
];

const manifests = ComponentDetection.processComponentsToManifests(componentsFound);
const dependencyGraphs: DependencyGraphs = {
"/package.json": {
graph: { "parent-package": null },
explicitlyReferencedComponentIds: [],
developmentDependencies: [],
dependencies: []
}
};

const manifests = ComponentDetection.processComponentsToManifests(componentsFound, dependencyGraphs);

expect(manifests).toHaveLength(1);
expect(manifests[0].name).toBe("package.json");
expect(manifests[0].name).toBe("/package.json");
expect(manifests[0].directDependencies()).toHaveLength(0);
expect(manifests[0].indirectDependencies()).toHaveLength(1);
expect(manifests[0].countDependencies()).toBe(1);
});
});

test("adds package as direct dependency when top level referrer is itself", () => {
const componentsFound = [
{
component: {
name: "test-package",
version: "1.0.0",
packageUrl: {
Scheme: "pkg",
Type: "npm",
Name: "test-package",
Version: "1.0.0"
},
id: "test-package 1.0.0 - npm"
},
isDevelopmentDependency: false,
topLevelReferrers: [
{
name: "test-package",
version: "1.0.0",
packageUrl: {
Scheme: "pkg",
Type: "npm",
Name: "test-package",
Version: "1.0.0"
}
}
],
locationsFoundAt: ["package.json"]
describe('normalizeDependencyGraphPaths', () => {
test('converts absolute paths to relative paths based on filePath input', () => {
// Simulate a repo at /repo and a scan root at /repo/packages
const fakeCwd = '/workspaces';
const filePathInput = 'my-super-cool-repo';
const absBase = '/workspaces/my-super-cool-repo';
const dependencyGraphs: DependencyGraphs = {
'/workspaces/my-super-cool-repo/a/package.json': {
graph: { 'foo': null },
explicitlyReferencedComponentIds: [],
developmentDependencies: [],
dependencies: []
},
'/workspaces/my-super-cool-repo/b/package.json': {
graph: { 'bar': null },
explicitlyReferencedComponentIds: [],
developmentDependencies: [],
dependencies: []
}
];

const manifests = ComponentDetection.processComponentsToManifests(componentsFound);
};
// Patch process.cwd for this test
const originalCwd = process.cwd;
(process as any).cwd = () => fakeCwd;
const normalized = ComponentDetection.normalizeDependencyGraphPaths(dependencyGraphs, filePathInput);
// Restore process.cwd
(process as any).cwd = originalCwd;
expect(Object.keys(normalized)).toContain('/a/package.json');
expect(Object.keys(normalized)).toContain('/b/package.json');
expect(normalized['/a/package.json'].graph).toEqual({ 'foo': null });
expect(normalized['/b/package.json'].graph).toEqual({ 'bar': null });
});
});

expect(manifests).toHaveLength(1);
expect(manifests[0].name).toBe("package.json");
expect(manifests[0].directDependencies()).toHaveLength(1);
expect(manifests[0].indirectDependencies()).toHaveLength(0);
expect(manifests[0].countDependencies()).toBe(1);
describe('normalizeDependencyGraphPaths with real output.json', () => {
test('converts absolute paths in output.json to relative paths using current cwd and filePath', () => {
const output = JSON.parse(fs.readFileSync('./output.json', 'utf8'));
const dependencyGraphs = output.dependencyGraphs;
// Use the same filePath as the action default (".")
const normalized = ComponentDetection.normalizeDependencyGraphPaths(dependencyGraphs, 'test');
// Should contain /package.json and /package-lock.json as keys
expect(Object.keys(normalized)).toContain('/package.json');
expect(Object.keys(normalized)).toContain('/package-lock.json');
// All keys should now be relative to the repo root (cwd) and start with '/'
for (const key of Object.keys(normalized)) {
expect(key.startsWith('/')).toBe(true);
expect(key).not.toMatch(/^\w:\\|^\/\/|^\.{1,2}\//); // Not windows absolute, not network, not relative
}
});
});
93 changes: 80 additions & 13 deletions componentDetection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
Package,
Snapshot,
Manifest,
submitSnapshot
submitSnapshot,
} from '@github/dependency-submission-toolkit'
import fetch from 'cross-fetch'
import tar from 'tar'
Expand All @@ -16,6 +16,7 @@ import * as exec from '@actions/exec';
import dotenv from 'dotenv'
import { Context } from '@actions/github/lib/context'
import { unmockedModulePathPatterns } from './jest.config'
import path from 'path';
dotenv.config();

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

public static processComponentsToManifests(componentsFound: any[]): Manifest[] {
public static processComponentsToManifests(componentsFound: any[], dependencyGraphs: DependencyGraphs): Manifest[] {
// Parse the result file and add the packages to the package cache
const packageCache = new PackageCache();
const packages: Array<ComponentDetectionPackage> = [];
Expand Down Expand Up @@ -126,6 +128,10 @@ export default class ComponentDetection {

try {
const referrerPackage = packageCache.lookupPackage(referrerUrl);
if (referrerPackage === pkg) {
core.debug(`Skipping self-reference for package: ${pkg.id}`);
return; // Skip self-references
}
Comment on lines +131 to +134
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

New code to eliminate self-referential paths

if (referrerPackage) {
referrerPackage.dependsOn(pkg);
}
Expand All @@ -139,29 +145,40 @@ export default class ComponentDetection {
const manifests: Array<Manifest> = [];

// Check the locationsFoundAt for every package and add each as a manifest
this.addPackagesToManifests(packages, manifests);
this.addPackagesToManifests(packages, manifests, dependencyGraphs);

return manifests;
}

private static addPackagesToManifests(packages: Array<ComponentDetectionPackage>, manifests: Array<Manifest>): void {
private static addPackagesToManifests(packages: Array<ComponentDetectionPackage>, manifests: Array<Manifest>, dependencyGraphs: DependencyGraphs): void {
packages.forEach((pkg: ComponentDetectionPackage) => {
pkg.locationsFoundAt.forEach((location: any) => {
if (!manifests.find((manifest: Manifest) => manifest.name == location)) {
const manifest = new Manifest(location, location);
manifests.push(manifest);
}

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

if (nonSelfReferrers.length == 0) {
manifests.find((manifest: Manifest) => manifest.name == location)?.addDirectDependency(pkg, ComponentDetection.getDependencyScope(pkg));
const directDependencies = depGraphEntry.explicitlyReferencedComponentIds;
if (directDependencies.includes(pkg.id)) {
manifests
.find((manifest: Manifest) => manifest.name == location)
?.addDirectDependency(
pkg,
ComponentDetection.getDependencyScope(pkg)
);
} else {
manifests.find((manifest: Manifest) => manifest.name == location)?.addIndirectDependency(pkg, ComponentDetection.getDependencyScope(pkg));
manifests
.find((manifest: Manifest) => manifest.name == location)
?.addIndirectDependency(
pkg,
ComponentDetection.getDependencyScope(pkg)
);
}
});
});
Expand Down Expand Up @@ -249,6 +266,29 @@ export default class ComponentDetection {
throw new Error("Failed to download latest release");
}
}

/**
* Normalizes the keys of a DependencyGraphs object to be relative paths from the resolved filePath input.
* @param dependencyGraphs The DependencyGraphs object to normalize.
* @param filePathInput The filePath input (relative or absolute) from the action configuration.
* @returns A new DependencyGraphs object with relative path keys.
*/
public static normalizeDependencyGraphPaths(
dependencyGraphs: DependencyGraphs,
filePathInput: string
): DependencyGraphs {
// Resolve the base directory from filePathInput (relative to cwd if not absolute)
const baseDir = path.resolve(process.cwd(), filePathInput);
const normalized: DependencyGraphs = {};
for (const absPath in dependencyGraphs) {
// Make the path relative to the baseDir
let relPath = path.relative(baseDir, absPath).replace(/\\/g, '/');
// Ensure leading slash to represent repo root
if (!relPath.startsWith('/')) relPath = '/' + relPath;
normalized[relPath] = dependencyGraphs[absPath];
}
return normalized;
}
}

class ComponentDetectionPackage extends Package {
Expand All @@ -261,6 +301,33 @@ class ComponentDetectionPackage extends Package {
}
}

/**
* Types for the dependencyGraphs section of output.json
*/
export type DependencyGraph = {
/**
* The dependency graph: keys are component IDs, values are either null (no dependencies) or an array of component IDs (dependencies)
*/
graph: Record<string, string[] | null>;
/**
* Explicitly referenced component IDs
*/
explicitlyReferencedComponentIds: string[];
/**
* Development dependencies
*/
developmentDependencies: string[];
/**
* Regular dependencies
*/
dependencies: string[];
};

/**
* The top-level dependencyGraphs object: keys are manifest file paths, values are DependencyGraph objects
*/
export type DependencyGraphs = Record<string, DependencyGraph>;




Expand Down
34 changes: 33 additions & 1 deletion dist/componentDetection.d.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading