Skip to content

feat: support package+license specific exclusions#1047

Open
erikburt wants to merge 1 commit intoactions:mainfrom
erikburt:feat/purl-license-qualifier
Open

feat: support package+license specific exclusions#1047
erikburt wants to merge 1 commit intoactions:mainfrom
erikburt:feat/purl-license-qualifier

Conversation

@erikburt
Copy link

@erikburt erikburt commented Feb 10, 2026

Adds support so allowedDependenciesLicenses can be configured for specific licenses. This addresses #1046.

It does this by adding support for a license PURL qualifier on the list of allowed dependencies licenses. This maintains backwards compatibility when the license qualifier is not included.

See my changes to example.md for more information.

Changes

  • Update PURL parser to support license qualifier
  • Update licenses.ts logic to support filtering changes based on license
  • Update unit tests for purl parser and license logic

licenses.ts

  1. Moved the filtering logic to it's own function filterLicenseChange, rather than it being inlined into groupChanges
  2. Move the changes filtering to the top-level getInvalidLicenseChanges function
    • groupChanges function used to prefilter the changes with the exclusion list. It was able to blanket filter here as it didn't matter if a dependency's license had been determined or not. And filtering before the grouping means less licenses to pull.
    • Now, we split the exlcusions list (renamed to allowedDependenciesLicenses to match input/config), into 2 separate lists. 1: Those without ?license=... PURL qualifiers, and those with it.
      • We do this so we can maintain previous behaviour. ie. Any license-agnostic exclusions are applied before we start pulling dependency licenses.
  3. Continue groupChanges as normal, but apply exclusions that contain license qualifiers, after licenses have been determined

Notes

This does work but if the design is not acceptable, I'm happy to modify it.

  • ie. If using a qualifer in the PURL is not ideal or if you'd rather it be another/different input

Copilot AI review requested due to automatic review settings February 10, 2026 23:51
@erikburt erikburt requested a review from a team as a code owner February 10, 2026 23:51
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds support for configuring allow-dependencies-licenses entries that target a specific license via a ?license=... PURL qualifier, while preserving the previous wildcard behavior when no license qualifier is provided.

Changes:

  • Extend PURL parsing to extract a license qualifier.
  • Update license-change filtering to support both wildcard and license-qualified allow-list entries.
  • Update docs and tests to cover license-qualified exclusions.

Reviewed changes

Copilot reviewed 6 out of 8 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
src/purl.ts Parses license from PURL qualifiers to enable license-qualified filtering.
src/main.ts Renames the config key passed into license validation to allowedDependenciesLicenses.
src/licenses.ts Refactors and splits filtering to apply wildcard exclusions pre-license-fetch and license-qualified exclusions post-license-determination.
docs/examples.md Documents how to exclude dependency+license combinations using ?license= qualifier.
tests/purl.test.ts Adds tests verifying license qualifier parsing.
tests/licenses.test.ts Adds tests for wildcard vs license-qualified allow dependency filtering behavior.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +84 to +92
function parseLicenseQualifier(purl: string): string | null {
// Qualifiers are between '?' and '#', if present.
const qIndex = purl.indexOf('?')
if (qIndex !== -1) {
const hashIndex = purl.indexOf('#', qIndex + 1)
const query = purl.slice(
qIndex + 1,
hashIndex === -1 ? undefined : hashIndex
)
Copy link

Copilot AI Feb 10, 2026

Choose a reason for hiding this comment

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

The comment in parseLicenseQualifier states qualifiers are between ? and #, but the implementation will also parse license when ? appears after # (because it slices from ? to end when no later # exists). Since the tests explicitly cover ...#subpath?license=..., please update the comment to match the supported behavior (or enforce the stated ordering if that’s the intended contract).

Copilot uses AI. Check for mistakes.
Comment on lines +57 to +59
const preFilteredChanges = changes.filter(change =>
filterLicenseChange(change, filtersWithNoLicenseQualifier)
)
Copy link

Copilot AI Feb 10, 2026

Choose a reason for hiding this comment

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

filterLicenseChange reparses change.package_url on every call and then linearly scans the full allow-list. Since this runs once per change (and potentially again for groupedChanges.licensed), it can become O(changes × allowedList) with repeated parsing. Consider pre-parsing changes once (e.g., attach parsed PURLs in a temporary structure) and/or building an index (e.g., Map keyed by type|namespace|name with a set of allowed licenses and/or a wildcard flag) so each change check is O(1) or close to it.

Copilot uses AI. Check for mistakes.
Comment on lines +140 to +149
const changeAsPackageURL = parsePURL(change.package_url)

for (const allowedDep of allowedDependenciesLicenses) {
if (
allowedDep.type !== changeAsPackageURL.type ||
allowedDep.namespace !== changeAsPackageURL.namespace ||
allowedDep.name !== changeAsPackageURL.name
) {
continue
}
Copy link

Copilot AI Feb 10, 2026

Choose a reason for hiding this comment

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

filterLicenseChange reparses change.package_url on every call and then linearly scans the full allow-list. Since this runs once per change (and potentially again for groupedChanges.licensed), it can become O(changes × allowedList) with repeated parsing. Consider pre-parsing changes once (e.g., attach parsed PURLs in a temporary structure) and/or building an index (e.g., Map keyed by type|namespace|name with a set of allowed licenses and/or a wildcard flag) so each change check is O(1) or close to it.

Copilot uses AI. Check for mistakes.
if (
change.package_url.length === 0 ||
allowedDependenciesLicenses === undefined ||
allowedDependenciesLicenses?.length === 0
Copy link

Copilot AI Feb 10, 2026

Choose a reason for hiding this comment

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

The condition mixes an explicit === undefined check with optional chaining (allowedDependenciesLicenses?.length). Since allowedDependenciesLicenses === undefined is already covered, the optional-chain is redundant and slightly harder to read. Consider simplifying to a single nullish/length guard (e.g., !allowedDependenciesLicenses || allowedDependenciesLicenses.length === 0).

Suggested change
allowedDependenciesLicenses?.length === 0
allowedDependenciesLicenses.length === 0

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants