Merge pull request #5 from actions/weekly-sync-branch-16062172741

Sync Fork with Upstream
This commit is contained in:
Justin Holguín
2025-07-03 16:09:54 -07:00
committed by GitHub
7 changed files with 232 additions and 77 deletions
+74 -46
View File
@@ -1,4 +1,4 @@
import ComponentDetection from "./componentDetection"; import ComponentDetection, { DependencyGraphs } from "./componentDetection";
import fs from "fs"; import fs from "fs";
test("Downloads CLI", async () => { test("Downloads CLI", async () => {
@@ -70,7 +70,7 @@ describe("ComponentDetection.makePackageUrl", () => {
}); });
describe("ComponentDetection.processComponentsToManifests", () => { 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 = [ const componentsFound = [
{ {
component: { component: {
@@ -86,20 +86,29 @@ describe("ComponentDetection.processComponentsToManifests", () => {
}, },
isDevelopmentDependency: false, isDevelopmentDependency: false,
topLevelReferrers: [], // Empty = direct dependency 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).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].directDependencies()).toHaveLength(1);
expect(manifests[0].indirectDependencies()).toHaveLength(0); expect(manifests[0].indirectDependencies()).toHaveLength(0);
expect(manifests[0].countDependencies()).toBe(1); 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 = [ const componentsFound = [
{ {
component: { component: {
@@ -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).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].directDependencies()).toHaveLength(0);
expect(manifests[0].indirectDependencies()).toHaveLength(1); expect(manifests[0].indirectDependencies()).toHaveLength(1);
expect(manifests[0].countDependencies()).toBe(1); expect(manifests[0].countDependencies()).toBe(1);
}); });
});
test("adds package as direct dependency when top level referrer is itself", () => { describe('normalizeDependencyGraphPaths', () => {
const componentsFound = [ test('converts absolute paths to relative paths based on filePath input', () => {
{ // Simulate a repo at /repo and a scan root at /repo/packages
component: { const fakeCwd = '/workspaces';
name: "test-package", const filePathInput = 'my-super-cool-repo';
version: "1.0.0", const absBase = '/workspaces/my-super-cool-repo';
packageUrl: { const dependencyGraphs: DependencyGraphs = {
Scheme: "pkg", '/workspaces/my-super-cool-repo/a/package.json': {
Type: "npm", graph: { 'foo': null },
Name: "test-package", explicitlyReferencedComponentIds: [],
Version: "1.0.0" developmentDependencies: [],
}, dependencies: []
id: "test-package 1.0.0 - npm" },
}, '/workspaces/my-super-cool-repo/b/package.json': {
isDevelopmentDependency: false, graph: { 'bar': null },
topLevelReferrers: [ explicitlyReferencedComponentIds: [],
{ developmentDependencies: [],
name: "test-package", dependencies: []
version: "1.0.0",
packageUrl: {
Scheme: "pkg",
Type: "npm",
Name: "test-package",
Version: "1.0.0"
}
}
],
locationsFoundAt: ["package.json"]
} }
]; };
// Patch process.cwd for this test
const manifests = ComponentDetection.processComponentsToManifests(componentsFound); const originalCwd = process.cwd;
(process as any).cwd = () => fakeCwd;
expect(manifests).toHaveLength(1); const normalized = ComponentDetection.normalizeDependencyGraphPaths(dependencyGraphs, filePathInput);
expect(manifests[0].name).toBe("package.json"); // Restore process.cwd
expect(manifests[0].directDependencies()).toHaveLength(1); (process as any).cwd = originalCwd;
expect(manifests[0].indirectDependencies()).toHaveLength(0); expect(Object.keys(normalized)).toContain('/a/package.json');
expect(manifests[0].countDependencies()).toBe(1); 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 });
});
});
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
}
}); });
}); });
+80 -13
View File
@@ -7,7 +7,7 @@ import {
Package, Package,
Snapshot, Snapshot,
Manifest, Manifest,
submitSnapshot submitSnapshot,
} from '@github/dependency-submission-toolkit' } from '@github/dependency-submission-toolkit'
import fetch from 'cross-fetch' import fetch from 'cross-fetch'
import tar from 'tar' import tar from 'tar'
@@ -16,6 +16,7 @@ import * as exec from '@actions/exec';
import dotenv from 'dotenv' import dotenv from 'dotenv'
import { Context } from '@actions/github/lib/context' import { Context } from '@actions/github/lib/context'
import { unmockedModulePathPatterns } from './jest.config' import { unmockedModulePathPatterns } from './jest.config'
import path from 'path';
dotenv.config(); dotenv.config();
export default class ComponentDetection { export default class ComponentDetection {
@@ -70,10 +71,11 @@ export default class ComponentDetection {
core.info("Getting manifests from results"); core.info("Getting manifests from results");
const results = await fs.readFileSync(this.outputPath, 'utf8'); const results = await fs.readFileSync(this.outputPath, 'utf8');
var json: any = JSON.parse(results); 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 // Parse the result file and add the packages to the package cache
const packageCache = new PackageCache(); const packageCache = new PackageCache();
const packages: Array<ComponentDetectionPackage> = []; const packages: Array<ComponentDetectionPackage> = [];
@@ -126,6 +128,10 @@ export default class ComponentDetection {
try { try {
const referrerPackage = packageCache.lookupPackage(referrerUrl); const referrerPackage = packageCache.lookupPackage(referrerUrl);
if (referrerPackage === pkg) {
core.debug(`Skipping self-reference for package: ${pkg.id}`);
return; // Skip self-references
}
if (referrerPackage) { if (referrerPackage) {
referrerPackage.dependsOn(pkg); referrerPackage.dependsOn(pkg);
} }
@@ -139,12 +145,12 @@ export default class ComponentDetection {
const manifests: Array<Manifest> = []; const manifests: Array<Manifest> = [];
// Check the locationsFoundAt for every package and add each as a manifest // Check the locationsFoundAt for every package and add each as a manifest
this.addPackagesToManifests(packages, manifests); this.addPackagesToManifests(packages, manifests, dependencyGraphs);
return manifests; 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) => { packages.forEach((pkg: ComponentDetectionPackage) => {
pkg.locationsFoundAt.forEach((location: any) => { pkg.locationsFoundAt.forEach((location: any) => {
if (!manifests.find((manifest: Manifest) => manifest.name == location)) { if (!manifests.find((manifest: Manifest) => manifest.name == location)) {
@@ -152,16 +158,27 @@ export default class ComponentDetection {
manifests.push(manifest); manifests.push(manifest);
} }
// Filter out self-references from topLevelReferrers const depGraphEntry = dependencyGraphs[location];
const nonSelfReferrers = pkg.topLevelReferrers.filter((referrer: any) => { if (!depGraphEntry) {
if (!referrer.packageUrlString) return false; core.warning(`No dependency graph entry found for manifest location: ${location}`);
return referrer.packageUrlString !== pkg.packageUrlString; return; // Skip this location if not found in dependencyGraphs
}); }
if (nonSelfReferrers.length == 0) { const directDependencies = depGraphEntry.explicitlyReferencedComponentIds;
manifests.find((manifest: Manifest) => manifest.name == location)?.addDirectDependency(pkg, ComponentDetection.getDependencyScope(pkg)); if (directDependencies.includes(pkg.id)) {
manifests
.find((manifest: Manifest) => manifest.name == location)
?.addDirectDependency(
pkg,
ComponentDetection.getDependencyScope(pkg)
);
} else { } 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)
);
} }
}); });
}); });
@@ -249,6 +266,29 @@ export default class ComponentDetection {
throw new Error("Failed to download latest release"); 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 { class ComponentDetectionPackage extends Package {
@@ -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>;
+33 -1
View File
@@ -7,9 +7,41 @@ export default class ComponentDetection {
static runComponentDetection(path: string): Promise<void>; static runComponentDetection(path: string): Promise<void>;
private static getComponentDetectionParameters; private static getComponentDetectionParameters;
static getManifestsFromResults(): Promise<Manifest[] | undefined>; static getManifestsFromResults(): Promise<Manifest[] | undefined>;
static processComponentsToManifests(componentsFound: any[]): Manifest[]; static processComponentsToManifests(componentsFound: any[], dependencyGraphs: DependencyGraphs): Manifest[];
private static addPackagesToManifests; private static addPackagesToManifests;
private static getDependencyScope; private static getDependencyScope;
static makePackageUrl(packageUrlJson: any): string; static makePackageUrl(packageUrlJson: any): string;
private static getLatestReleaseURL; private static getLatestReleaseURL;
/**
* 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.
*/
static normalizeDependencyGraphPaths(dependencyGraphs: DependencyGraphs, filePathInput: string): DependencyGraphs;
} }
/**
* 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>;
Generated Vendored
+41 -13
View File
@@ -36002,6 +36002,7 @@ const cross_fetch_1 = __importDefault(__nccwpck_require__(3304));
const fs_1 = __importDefault(__nccwpck_require__(9896)); const fs_1 = __importDefault(__nccwpck_require__(9896));
const exec = __importStar(__nccwpck_require__(5236)); const exec = __importStar(__nccwpck_require__(5236));
const dotenv_1 = __importDefault(__nccwpck_require__(8889)); const dotenv_1 = __importDefault(__nccwpck_require__(8889));
const path_1 = __importDefault(__nccwpck_require__(6928));
dotenv_1.default.config(); dotenv_1.default.config();
class ComponentDetection { class ComponentDetection {
// This is the default entry point for this class. // This is the default entry point for this class.
@@ -36056,10 +36057,11 @@ class ComponentDetection {
core.info("Getting manifests from results"); core.info("Getting manifests from results");
const results = yield fs_1.default.readFileSync(this.outputPath, 'utf8'); const results = yield fs_1.default.readFileSync(this.outputPath, 'utf8');
var json = JSON.parse(results); var json = JSON.parse(results);
return this.processComponentsToManifests(json.componentsFound); let dependencyGraphs = this.normalizeDependencyGraphPaths(json.dependencyGraphs, core.getInput('filePath'));
return this.processComponentsToManifests(json.componentsFound, dependencyGraphs);
}); });
} }
static processComponentsToManifests(componentsFound) { static processComponentsToManifests(componentsFound, dependencyGraphs) {
// Parse the result file and add the packages to the package cache // Parse the result file and add the packages to the package cache
const packageCache = new dependency_submission_toolkit_1.PackageCache(); const packageCache = new dependency_submission_toolkit_1.PackageCache();
const packages = []; const packages = [];
@@ -36103,6 +36105,10 @@ class ComponentDetection {
} }
try { try {
const referrerPackage = packageCache.lookupPackage(referrerUrl); const referrerPackage = packageCache.lookupPackage(referrerUrl);
if (referrerPackage === pkg) {
core.debug(`Skipping self-reference for package: ${pkg.id}`);
return; // Skip self-references
}
if (referrerPackage) { if (referrerPackage) {
referrerPackage.dependsOn(pkg); referrerPackage.dependsOn(pkg);
} }
@@ -36115,10 +36121,10 @@ class ComponentDetection {
// Create manifests // Create manifests
const manifests = []; const manifests = [];
// Check the locationsFoundAt for every package and add each as a manifest // Check the locationsFoundAt for every package and add each as a manifest
this.addPackagesToManifests(packages, manifests); this.addPackagesToManifests(packages, manifests, dependencyGraphs);
return manifests; return manifests;
} }
static addPackagesToManifests(packages, manifests) { static addPackagesToManifests(packages, manifests, dependencyGraphs) {
packages.forEach((pkg) => { packages.forEach((pkg) => {
pkg.locationsFoundAt.forEach((location) => { pkg.locationsFoundAt.forEach((location) => {
var _a, _b; var _a, _b;
@@ -36126,17 +36132,19 @@ class ComponentDetection {
const manifest = new dependency_submission_toolkit_1.Manifest(location, location); const manifest = new dependency_submission_toolkit_1.Manifest(location, location);
manifests.push(manifest); manifests.push(manifest);
} }
// Filter out self-references from topLevelReferrers const depGraphEntry = dependencyGraphs[location];
const nonSelfReferrers = pkg.topLevelReferrers.filter((referrer) => { if (!depGraphEntry) {
if (!referrer.packageUrlString) core.warning(`No dependency graph entry found for manifest location: ${location}`);
return false; return; // Skip this location if not found in dependencyGraphs
return referrer.packageUrlString !== pkg.packageUrlString; }
}); const directDependencies = depGraphEntry.explicitlyReferencedComponentIds;
if (nonSelfReferrers.length == 0) { if (directDependencies.includes(pkg.id)) {
(_a = manifests.find((manifest) => manifest.name == location)) === null || _a === void 0 ? void 0 : _a.addDirectDependency(pkg, ComponentDetection.getDependencyScope(pkg)); (_a = manifests
.find((manifest) => manifest.name == location)) === null || _a === void 0 ? void 0 : _a.addDirectDependency(pkg, ComponentDetection.getDependencyScope(pkg));
} }
else { else {
(_b = manifests.find((manifest) => manifest.name == location)) === null || _b === void 0 ? void 0 : _b.addIndirectDependency(pkg, ComponentDetection.getDependencyScope(pkg)); (_b = manifests
.find((manifest) => manifest.name == location)) === null || _b === void 0 ? void 0 : _b.addIndirectDependency(pkg, ComponentDetection.getDependencyScope(pkg));
} }
}); });
}); });
@@ -36216,6 +36224,26 @@ class ComponentDetection {
} }
}); });
} }
/**
* 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.
*/
static normalizeDependencyGraphPaths(dependencyGraphs, filePathInput) {
// Resolve the base directory from filePathInput (relative to cwd if not absolute)
const baseDir = path_1.default.resolve(process.cwd(), filePathInput);
const normalized = {};
for (const absPath in dependencyGraphs) {
// Make the path relative to the baseDir
let relPath = path_1.default.relative(baseDir, absPath).replace(/\\/g, '/');
// Ensure leading slash to represent repo root
if (!relPath.startsWith('/'))
relPath = '/' + relPath;
normalized[relPath] = dependencyGraphs[absPath];
}
return normalized;
}
} }
exports["default"] = ComponentDetection; exports["default"] = ComponentDetection;
ComponentDetection.componentDetectionPath = process.platform === "win32" ? './component-detection.exe' : './component-detection'; ComponentDetection.componentDetectionPath = process.platform === "win32" ? './component-detection.exe' : './component-detection';
Generated Vendored
+1 -1
View File
File diff suppressed because one or more lines are too long
+2 -2
View File
@@ -1,12 +1,12 @@
{ {
"name": "component-detection-action", "name": "component-detection-action",
"version": "1.0.0", "version": "0.0.7",
"lockfileVersion": 2, "lockfileVersion": 2,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "component-detection-action", "name": "component-detection-action",
"version": "1.0.0", "version": "0.0.7",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@actions/core": "^1.10.0", "@actions/core": "^1.10.0",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "component-detection-action", "name": "component-detection-action",
"version": "1.0.0", "version": "0.0.7",
"description": "Component detection action", "description": "Component detection action",
"main": "dist/index.js", "main": "dist/index.js",
"type": "module", "type": "module",