diff --git a/build/main.js b/build/main.js index a5d3f36..c94966d 100644 --- a/build/main.js +++ b/build/main.js @@ -24375,6 +24375,25 @@ function processDependencies(rootInfo, root, packageMap, prefix) { } } +// node_modules/lockparse/lib/traverse.js +var visitorKeys = [ + ["dependency", "dependencies"], + ["devDependency", "devDependencies"], + ["peerDependency", "peerDependencies"], + ["optionalDependency", "optionalDependencies"] +]; +function traverse(node, visitor) { + for (const [visitorKey, nodeKey] of visitorKeys) { + if (visitor[visitorKey]) { + for (const dep of node[nodeKey]) { + if (visitor[visitorKey](dep, node) !== false) { + traverse(dep, visitor); + } + } + } + } +} + // node_modules/lockparse/lib/main.js var typeMap = { "package-lock.json": "npm", @@ -24611,6 +24630,12 @@ function getDependenciesFromPackageJson(pkg, types) { } return result; } +function isSupportedArchitecture(pkg, os, cpu, libc) { + const osMatches = pkg.os === void 0 || pkg.os.length === 0 || pkg.os.includes(os); + const cpuMatches = pkg.cpu === void 0 || pkg.cpu.length === 0 || pkg.cpu.includes(cpu); + const libcMatches = pkg.libc === void 0 || pkg.libc.length === 0 || pkg.libc.includes(libc); + return osMatches && cpuMatches && libcMatches; +} // src/packs.ts var core3 = __toESM(require_core(), 1); @@ -24878,7 +24903,7 @@ function formatBytes(bytes) { } // src/checks/dependency-size.ts -async function scanForDependencySize(messages, threshold, currentDeps, baseDeps) { +async function scanForDependencySize(messages, threshold, currentDeps, baseDeps, currentLockFile) { const newVersions = []; const removedVersions = []; for (const [packageName, currentVersionSet] of currentDeps) { @@ -24904,6 +24929,31 @@ async function scanForDependencySize(messages, threshold, currentDeps, baseDeps) } } } + if (newVersions.length > 0) { + const allOptionalVersions = /* @__PURE__ */ new Map(); + for (const pkg of currentLockFile.packages) { + traverse(pkg, { + optionalDependency: (node) => { + const entry = allOptionalVersions.get(node.name) ?? /* @__PURE__ */ new Set(); + entry.add(node.version); + allOptionalVersions.set(node.name, entry); + } + }); + } + for (const [pkg, versions] of allOptionalVersions) { + for (const version of versions) { + const pkgMeta = await fetchPackageMetadata(pkg, version); + if (pkgMeta && !isSupportedArchitecture(pkgMeta, "linux", "x64", "glibc")) { + const entry = newVersions.findIndex( + (v) => v.name === pkg && v.version === version + ); + if (entry !== -1) { + newVersions.splice(entry, 1); + } + } + } + } + } core5.info(`Found ${newVersions.length} new package versions`); core5.info(`Found ${removedVersions.length} removed package versions.`); if (newVersions.length === 0 && removedVersions.length === 0) { @@ -25113,7 +25163,13 @@ async function run() { baseDeps ); scanForDuplicates(messages, duplicateThreshold, currentDeps, lockfilePath); - await scanForDependencySize(messages, sizeThreshold, currentDeps, baseDeps); + await scanForDependencySize( + messages, + sizeThreshold, + currentDeps, + baseDeps, + parsedCurrentLock + ); await scanForProvenance(messages, currentDeps, baseDeps); const basePackagesPattern = core7.getInput("base-packages"); const sourcePackagesPattern = core7.getInput("source-packages"); diff --git a/src/checks/dependency-size.ts b/src/checks/dependency-size.ts index e1811c8..9f3cad9 100644 --- a/src/checks/dependency-size.ts +++ b/src/checks/dependency-size.ts @@ -1,12 +1,18 @@ import * as core from '@actions/core'; -import {calculateTotalDependencySizeIncrease} from '../npm.js'; +import {type ParsedLockFile, traverse} from 'lockparse'; +import { + calculateTotalDependencySizeIncrease, + fetchPackageMetadata, + isSupportedArchitecture +} from '../npm.js'; import {formatBytes} from '../common.js'; export async function scanForDependencySize( messages: string[], threshold: number, currentDeps: Map>, - baseDeps: Map> + baseDeps: Map>, + currentLockFile: ParsedLockFile ): Promise { const newVersions: Array<{ name: string; @@ -45,6 +51,37 @@ export async function scanForDependencySize( } } + if (newVersions.length > 0) { + const allOptionalVersions = new Map>(); + + for (const pkg of currentLockFile.packages) { + traverse(pkg, { + optionalDependency: (node) => { + const entry = allOptionalVersions.get(node.name) ?? new Set(); + entry.add(node.version); + allOptionalVersions.set(node.name, entry); + } + }); + } + + for (const [pkg, versions] of allOptionalVersions) { + for (const version of versions) { + const pkgMeta = await fetchPackageMetadata(pkg, version); + if ( + pkgMeta && + !isSupportedArchitecture(pkgMeta, 'linux', 'x64', 'glibc') + ) { + const entry = newVersions.findIndex( + (v) => v.name === pkg && v.version === version + ); + if (entry !== -1) { + newVersions.splice(entry, 1); + } + } + } + } + } + core.info(`Found ${newVersions.length} new package versions`); core.info(`Found ${removedVersions.length} removed package versions.`); diff --git a/src/main.ts b/src/main.ts index fcf308d..07485ed 100644 --- a/src/main.ts +++ b/src/main.ts @@ -130,7 +130,13 @@ async function run(): Promise { ); scanForDuplicates(messages, duplicateThreshold, currentDeps, lockfilePath); - await scanForDependencySize(messages, sizeThreshold, currentDeps, baseDeps); + await scanForDependencySize( + messages, + sizeThreshold, + currentDeps, + baseDeps, + parsedCurrentLock + ); await scanForProvenance(messages, currentDeps, baseDeps); const basePackagesPattern = core.getInput('base-packages'); diff --git a/src/npm.ts b/src/npm.ts index 0fdd85a..068cba6 100644 --- a/src/npm.ts +++ b/src/npm.ts @@ -4,6 +4,9 @@ import type {PackageJson} from 'pkg-types'; export interface PackageMetadata { name: string; version: string; + os?: string[]; + cpu?: string[]; + libc?: string[]; dist?: { unpackedSize?: number; attestations?: { @@ -218,3 +221,18 @@ export function getDependenciesFromPackageJson( return result; } + +export function isSupportedArchitecture( + pkg: PackageJson, + os: string, + cpu: string, + libc: string +): boolean { + const osMatches = + pkg.os === undefined || pkg.os.length === 0 || pkg.os.includes(os); + const cpuMatches = + pkg.cpu === undefined || pkg.cpu.length === 0 || pkg.cpu.includes(cpu); + const libcMatches = + pkg.libc === undefined || pkg.libc.length === 0 || pkg.libc.includes(libc); + return osMatches && cpuMatches && libcMatches; +} diff --git a/test/npm_test.ts b/test/npm_test.ts index e1678dc..ee41dd3 100644 --- a/test/npm_test.ts +++ b/test/npm_test.ts @@ -1,4 +1,5 @@ import * as core from '@actions/core'; +import type {PackageJson} from 'pkg-types'; import { describe, it, @@ -14,6 +15,7 @@ import { getProvenance, getTrustLevel, getProvenanceForPackageVersions, + isSupportedArchitecture, getMinTrustLevel, getDependenciesFromPackageJson, type ProvenanceStatus, @@ -294,3 +296,94 @@ describe('getDependenciesFromPackageJson', () => { ); }); }); + +describe('isSupportedArchitecture', () => { + it('returns true if no os, cpu, or libc fields are present', () => { + const pkg: PackageJson = { + name: 'some-package' + }; + expect(isSupportedArchitecture(pkg, 'linux', 'x64', 'glibc')).toBe(true); + }); + + it('returns true if os matches, cpu/libc empty', () => { + const pkg: PackageJson = { + name: 'some-package', + os: ['linux', 'darwin'] + }; + expect(isSupportedArchitecture(pkg, 'linux', 'x64', 'glibc')).toBe(true); + expect(isSupportedArchitecture(pkg, 'darwin', 'x64', 'glibc')).toBe(true); + expect(isSupportedArchitecture(pkg, 'win32', 'x64', 'glibc')).toBe(false); + }); + + it('returns true if cpu matches, os/libc empty', () => { + const pkg: PackageJson = { + name: 'some-package', + cpu: ['x64', 'arm64'] + }; + expect(isSupportedArchitecture(pkg, 'linux', 'x64', 'glibc')).toBe(true); + expect(isSupportedArchitecture(pkg, 'linux', 'arm64', 'glibc')).toBe(true); + expect(isSupportedArchitecture(pkg, 'linux', 'ia32', 'glibc')).toBe(false); + }); + + it('returns true if libc matches, os/cpu empty', () => { + const pkg: PackageJson = { + name: 'some-package', + libc: ['glibc', 'musl'] + }; + expect(isSupportedArchitecture(pkg, 'linux', 'x64', 'glibc')).toBe(true); + expect(isSupportedArchitecture(pkg, 'linux', 'x64', 'musl')).toBe(true); + expect(isSupportedArchitecture(pkg, 'linux', 'x64', 'uclibc')).toBe(false); + }); + + it('returns true if all match', () => { + const pkg: PackageJson = { + name: 'some-package', + os: ['linux', 'darwin'], + cpu: ['x64', 'arm64'], + libc: ['glibc', 'musl'] + }; + expect(isSupportedArchitecture(pkg, 'linux', 'x64', 'glibc')).toBe(true); + expect(isSupportedArchitecture(pkg, 'darwin', 'arm64', 'musl')).toBe(true); + expect(isSupportedArchitecture(pkg, 'linux', 'ia32', 'glibc')).toBe(false); + expect(isSupportedArchitecture(pkg, 'linux', 'x64', 'uclibc')).toBe(false); + expect(isSupportedArchitecture(pkg, 'win32', 'x64', 'glibc')).toBe(false); + }); + + it('returns true if os is empty array', () => { + const pkg: PackageJson = { + name: 'some-package', + os: [], + cpu: ['x64'], + libc: ['glibc'] + }; + expect(isSupportedArchitecture(pkg, 'linux', 'x64', 'glibc')).toBe(true); + expect(isSupportedArchitecture(pkg, 'darwin', 'x64', 'glibc')).toBe(true); + expect(isSupportedArchitecture(pkg, 'win32', 'x64', 'glibc')).toBe(true); + }); + + it('returns true if cpu is empty array', () => { + const pkg: PackageJson = { + name: 'some-package', + os: ['linux'], + cpu: [], + libc: ['glibc'] + }; + expect(isSupportedArchitecture(pkg, 'linux', 'x64', 'glibc')).toBe(true); + expect(isSupportedArchitecture(pkg, 'linux', 'arm64', 'glibc')).toBe(true); + expect(isSupportedArchitecture(pkg, 'linux', 'ia32', 'glibc')).toBe(true); + expect(isSupportedArchitecture(pkg, 'darwin', 'x64', 'glibc')).toBe(false); + }); + + it('returns true if libc is empty array', () => { + const pkg: PackageJson = { + name: 'some-package', + os: ['linux'], + cpu: ['x64'], + libc: [] + }; + expect(isSupportedArchitecture(pkg, 'linux', 'x64', 'glibc')).toBe(true); + expect(isSupportedArchitecture(pkg, 'linux', 'x64', 'musl')).toBe(true); + expect(isSupportedArchitecture(pkg, 'linux', 'x64', 'uclibc')).toBe(true); + expect(isSupportedArchitecture(pkg, 'darwin', 'x64', 'glibc')).toBe(false); + }); +});