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
61 changes: 61 additions & 0 deletions __tests__/dependencies/dependency-change.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
const rule = require('../../src/rules/dependencies/dependency-change');

describe('Dependency Change Rule', () => {
it('labels for package.json', () => {
const files = [{ filename: 'package.json' }];
const labels = rule({ files, pr: {}, enableDebug: false });
expect(labels).toContain('dependency-change');
});

it('labels for package-lock.json', () => {
const files = [{ filename: 'package-lock.json' }];
const labels = rule({ files, pr: {}, enableDebug: false });
expect(labels).toContain('dependency-change');
});

it('labels for yarn.lock', () => {
const files = [{ filename: 'yarn.lock' }];
const labels = rule({ files, pr: {}, enableDebug: false });
expect(labels).toContain('dependency-change');
});

it('labels for pnpm-lock.yaml', () => {
const files = [{ filename: 'pnpm-lock.yaml' }];
const labels = rule({ files, pr: {}, enableDebug: false });
expect(labels).toContain('dependency-change');
});

it('labels for requirements.txt', () => {
const files = [{ filename: 'requirements.txt' }];
const labels = rule({ files, pr: {}, enableDebug: false });
expect(labels).toContain('dependency-change');
});

it('labels for poetry.lock', () => {
const files = [{ filename: 'poetry.lock' }];
const labels = rule({ files, pr: {}, enableDebug: false });
expect(labels).toContain('dependency-change');
});

it('labels for nested package.json', () => {
const files = [{ filename: 'packages/app/package.json' }];
const labels = rule({ files, pr: {}, enableDebug: false });
expect(labels).toContain('dependency-change');
});

it('does not label for source files', () => {
const files = [{ filename: 'src/app.ts' }];
const labels = rule({ files, pr: {}, enableDebug: false });
expect(labels).toEqual([]);
});

it('handles empty files array', () => {
const labels = rule({ files: [], pr: {}, enableDebug: false });
expect(labels).toEqual([]);
});

it('handles debug mode', () => {
const files = [{ filename: 'package.json' }];
expect(() => rule({ files, pr: {}, enableDebug: true })).not.toThrow();
});
});
62 changes: 62 additions & 0 deletions __tests__/dependencies/dependency-downgrade.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
const rule = require('../../src/rules/dependencies/dependency-downgrade');

describe('Dependency Downgrade Rule', () => {
it('labels when a version is downgraded', () => {
const patch = `@@\n- "react": "^18.2.0"\n+ "react": "^18.1.0"`;
const files = [{ filename: 'package.json', patch }];
const labels = rule({ files, pr: {}, enableDebug: false });
expect(labels).toContain('dependency-downgrade');
});

it('labels when major version decreases', () => {
const patch = `@@\n- "lodash": "^5.0.0"\n+ "lodash": "^4.17.0"`;
const files = [{ filename: 'package.json', patch }];
const labels = rule({ files, pr: {}, enableDebug: false });
expect(labels).toContain('dependency-downgrade');
});

it('labels when minor version decreases', () => {
const patch = `@@\n- "express": "^4.18.0"\n+ "express": "^4.17.0"`;
const files = [{ filename: 'package.json', patch }];
const labels = rule({ files, pr: {}, enableDebug: false });
expect(labels).toContain('dependency-downgrade');
});

it('does not label when a version is upgraded', () => {
const patch = `@@\n- "react": "^18.1.0"\n+ "react": "^18.2.0"`;
const files = [{ filename: 'package.json', patch }];
const labels = rule({ files, pr: {}, enableDebug: false });
expect(labels).toEqual([]);
});

it('does not label when versions are equal', () => {
const patch = `@@\n- "vue": "^3.2.0"\n+ "vue": "^3.2.0"`;
const files = [{ filename: 'package.json', patch }];
const labels = rule({ files, pr: {}, enableDebug: false });
expect(labels).toEqual([]);
});

it('does not label for non-package.json files', () => {
const patch = `@@\n- "version": "2.0.0"\n+ "version": "1.0.0"`;
const files = [{ filename: 'config.json', patch }];
const labels = rule({ files, pr: {}, enableDebug: false });
expect(labels).toEqual([]);
});

it('does not label when patch is missing', () => {
const files = [{ filename: 'package.json' }];
const labels = rule({ files, pr: {}, enableDebug: false });
expect(labels).toEqual([]);
});

it('handles empty files array', () => {
const labels = rule({ files: [], pr: {}, enableDebug: false });
expect(labels).toEqual([]);
});

it('handles debug mode', () => {
const patch = `@@\n- "axios": "^1.4.0"\n+ "axios": "^1.3.0"`;
const files = [{ filename: 'package.json', patch }];
expect(() => rule({ files, pr: {}, enableDebug: true })).not.toThrow();
});
});
55 changes: 55 additions & 0 deletions __tests__/dependencies/new-dependency.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
const rule = require('../../src/rules/dependencies/new-dependency');

describe('New Dependency Rule', () => {
it('labels when dependency is added in patch', () => {
const patch = `@@\n+ "dependencies": {\n+ + "left-pad": "^1.3.0"\n+ }`;
const files = [{ filename: 'package.json', patch }];
const labels = rule({ files, pr: {}, enableDebug: false });
expect(labels).toContain('new-dependency');
});

it('labels for simple dependency addition', () => {
const patch = `@@\n+ "lodash": "^4.17.21",`;
const files = [{ filename: 'package.json', patch }];
const labels = rule({ files, pr: {}, enableDebug: false });
expect(labels).toContain('new-dependency');
});

it('labels for nested package.json', () => {
const patch = `@@\n+ "express": "^4.18.0"`;
const files = [{ filename: 'api/package.json', patch }];
const labels = rule({ files, pr: {}, enableDebug: false });
expect(labels).toContain('new-dependency');
});

it('does not label without additions', () => {
const patch = `@@\n- "left-pad": "^1.2.0"`;
const files = [{ filename: 'package.json', patch }];
const labels = rule({ files, pr: {}, enableDebug: false });
expect(labels).toEqual([]);
});

it('does not label for non-package.json files', () => {
const patch = `@@\n+ "test": "value"`;
const files = [{ filename: 'config.json', patch }];
const labels = rule({ files, pr: {}, enableDebug: false });
expect(labels).toEqual([]);
});

it('does not label when patch is missing', () => {
const files = [{ filename: 'package.json' }];
const labels = rule({ files, pr: {}, enableDebug: false });
expect(labels).toEqual([]);
});

it('handles empty files array', () => {
const labels = rule({ files: [], pr: {}, enableDebug: false });
expect(labels).toEqual([]);
});

it('handles debug mode', () => {
const patch = `@@\n+ "react": "^18.0.0"`;
const files = [{ filename: 'package.json', patch }];
expect(() => rule({ files, pr: {}, enableDebug: true })).not.toThrow();
});
});
46 changes: 46 additions & 0 deletions src/rules/dependencies/dependency-change.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/**
* Dependency Files Changed Rule
*
* Adds `dependency-change` if dependency manifest/lock files change.
*/

module.exports = function dependencyChangeRule({ files, pr, enableDebug }) {
const labels = [];

const patterns = [
/(^|\/)package\.json$/i,
/(^|\/)package-lock\.json$/i,
/(^|\/)yarn\.lock$/i,
/(^|\/)pnpm-lock\.yaml$/i,
/(^|\/)requirements\.txt$/i,
/(^|\/)poetry\.lock$/i
];

const detected = (files || []).some(f => {
const name = (f && f.filename ? String(f.filename) : '').toLowerCase();
return name && patterns.some(rx => rx.test(name));
});

if (detected) {
labels.push('dependency-change');
}

if (enableDebug) {
console.log(`[Dependency Change Rule] → ${labels.join(', ') || 'none'}`);
}

return labels;
};

module.exports.metadata = {
name: 'Dependency Files Changed',
description: 'Detects changes to dependency manifests/lock files',
labels: [
{ name: 'dependency-change', color: '0075CA', description: 'Dependency files modified' }
],
author: 'pr-auto-labeler',
version: '1.0.0',
category: 'dependencies'
};


72 changes: 72 additions & 0 deletions src/rules/dependencies/dependency-downgrade.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
/**
* Dependency Downgrade Detection Rule
*
* Adds `dependency-downgrade` if a version decreases in package.json patch.
*/

function parseVersion(v) {
const cleaned = (v || '').replace(/^[^0-9]*/, ''); // drop ^ ~ etc.
const [major, minor, patch] = cleaned.split('.').map(n => parseInt(n, 10) || 0);
return { major, minor, patch };
}

function isLess(a, b) {
if (a.major !== b.major) return a.major < b.major;
if (a.minor !== b.minor) return a.minor < b.minor;
return a.patch < b.patch;
}

module.exports = function dependencyDowngradeRule({ files, pr, enableDebug }) {
const labels = [];

const isPkgJson = (name) => /(^|\/)package\.json$/i.test(name);

let detected = false;
for (const file of files || []) {
const name = (file && file.filename ? String(file.filename) : '').toLowerCase();
if (!isPkgJson(name)) continue;
const patch = file && file.patch ? String(file.patch) : '';
if (!patch) continue;

// Scan pairs of removed and added lines for same dep with lower version
const removed = [...patch.matchAll(/^-\s*"([^"]+)"\s*:\s*"([^"]+)"/gm)];
const added = [...patch.matchAll(/^\+\s*"([^"]+)"\s*:\s*"([^"]+)"/gm)];
const addMap = new Map(added.map((m) => [m[1], m[2]]));

for (const m of removed) {
const dep = m[1];
const oldV = parseVersion(m[2]);
const newVRaw = addMap.get(dep);
if (!newVRaw) continue;
const newV = parseVersion(newVRaw);
if (isLess(newV, oldV)) {
detected = true;
break;
}
}
if (detected) break;
}

if (detected) {
labels.push('dependency-downgrade');
}

if (enableDebug) {
console.log(`[Dependency Downgrade Rule] → ${labels.join(', ') || 'none'}`);
}

return labels;
};

module.exports.metadata = {
name: 'Dependency Downgrade Detection',
description: 'Detects decreased versions in package.json changes',
labels: [
{ name: 'dependency-downgrade', color: 'D93F0B', description: 'Dependency version downgraded' }
],
author: 'pr-auto-labeler',
version: '1.0.0',
category: 'dependencies'
};


50 changes: 50 additions & 0 deletions src/rules/dependencies/new-dependency.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/**
* New Dependency Added Rule
*
* Adds `new-dependency` if package.json adds new dependencies in patch.
*/

module.exports = function newDependencyRule({ files, pr, enableDebug }) {
const labels = [];

const isPkgJson = (name) => /(^|\/)package\.json$/i.test(name);

let detected = false;
for (const file of files || []) {
const name = (file && file.filename ? String(file.filename) : '').toLowerCase();
if (!isPkgJson(name)) continue;
const patch = file && file.patch ? String(file.patch) : '';
if (!patch) continue;

// Look for added dependency lines like: + "lib": "^1.2.3",
// Some fixtures may include an extra '+' artifact, allow optional second '+'.
const addedDepLine = /^\+\s*\+?\s*"[^"]+"\s*:\s*"[^"]+",?\s*$/m;
if (addedDepLine.test(patch)) {
detected = true;
break;
}
}

if (detected) {
labels.push('new-dependency');
}

if (enableDebug) {
console.log(`[New Dependency Rule] → ${labels.join(', ') || 'none'}`);
}

return labels;
};

module.exports.metadata = {
name: 'New Dependency Added',
description: 'Detects additions of dependencies in package.json',
labels: [
{ name: 'new-dependency', color: '0E8A16', description: 'New dependency added' }
],
author: 'pr-auto-labeler',
version: '1.0.0',
category: 'dependencies'
};


Loading