Skip to content

Commit 696e0bd

Browse files
Copilotmroz22
andcommitted
feat(requirements): add unified-dependency-versions requirement to detect version drift
Add a new repo-scoped requirement that verifies all external (non-workspace) dependencies use the same version specifier across all workspaces in the monorepo. - Scans all workspace package.json files for dependencies and devDependencies - Reports when the same package has different version specifiers in different workspaces - Supports fix mode to auto-align versions to the most frequently used specifier - Includes allowlist for known intentional version differences (major version migrations) - Runs automatically on every PR via the existing check-code-validation workflow Agent-Logs-Url: https://github.com/trezor/trezor-suite/sessions/cd558aa1-bb58-48d3-add8-a0355636ead6 Co-authored-by: mroz22 <30367552+mroz22@users.noreply.github.com>
1 parent 40c1f38 commit 696e0bd

3 files changed

Lines changed: 608 additions & 0 deletions

File tree

Lines changed: 276 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,276 @@
1+
import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
2+
import { tmpdir } from 'node:os';
3+
import { join } from 'node:path';
4+
5+
import { requireUnifiedDependencyVersions } from '../requirements/dependency-versions/requireUnifiedDependencyVersions';
6+
7+
const tempDirs: string[] = [];
8+
9+
afterEach(() => {
10+
for (const dir of tempDirs) {
11+
rmSync(dir, { recursive: true, force: true });
12+
}
13+
tempDirs.length = 0;
14+
});
15+
16+
const createTempRepo = () => {
17+
const root = mkdtempSync(join(tmpdir(), 'req-dep-test-'));
18+
tempDirs.push(root);
19+
20+
return {
21+
root,
22+
addWorkspace: (
23+
name: string,
24+
location: string,
25+
deps: Record<string, string> = {},
26+
devDeps: Record<string, string> = {},
27+
) => {
28+
const dir = join(root, location);
29+
mkdirSync(dir, { recursive: true });
30+
31+
const pkg: Record<string, unknown> = { name };
32+
33+
if (Object.keys(deps).length > 0) {
34+
pkg.dependencies = deps;
35+
}
36+
37+
if (Object.keys(devDeps).length > 0) {
38+
pkg.devDependencies = devDeps;
39+
}
40+
41+
writeFileSync(join(dir, 'package.json'), JSON.stringify(pkg, null, 4) + '\n');
42+
},
43+
setRootPackageJson: (
44+
workspaces: string[],
45+
deps: Record<string, string> = {},
46+
devDeps: Record<string, string> = {},
47+
) => {
48+
const pkg: Record<string, unknown> = {
49+
name: 'test-monorepo',
50+
workspaces: { packages: workspaces },
51+
};
52+
53+
if (Object.keys(deps).length > 0) {
54+
pkg.dependencies = deps;
55+
}
56+
57+
if (Object.keys(devDeps).length > 0) {
58+
pkg.devDependencies = devDeps;
59+
}
60+
61+
writeFileSync(join(root, 'package.json'), JSON.stringify(pkg, null, 4) + '\n');
62+
},
63+
};
64+
};
65+
66+
describe('requireUnifiedDependencyVersions', () => {
67+
it('reports no errors when all versions match', async () => {
68+
const repo = createTempRepo();
69+
repo.setRootPackageJson(['packages/*']);
70+
repo.addWorkspace('@test/alpha', 'packages/alpha', { lodash: '^4.17.21' });
71+
repo.addWorkspace('@test/beta', 'packages/beta', { lodash: '^4.17.21' });
72+
73+
const errors = await requireUnifiedDependencyVersions.verify({ repoRoot: repo.root });
74+
75+
expect(errors).toEqual([]);
76+
});
77+
78+
it('ignores dependencies in the allowed drifts list', async () => {
79+
const repo = createTempRepo();
80+
repo.setRootPackageJson(['packages/*']);
81+
// 'react' is in ALLOWED_DRIFTS
82+
repo.addWorkspace('@test/alpha', 'packages/alpha', { react: '18.2.0' });
83+
repo.addWorkspace('@test/beta', 'packages/beta', { react: '19.2.4' });
84+
85+
const errors = await requireUnifiedDependencyVersions.verify({ repoRoot: repo.root });
86+
87+
expect(errors).toEqual([]);
88+
});
89+
90+
it('reports errors when versions differ', async () => {
91+
const repo = createTempRepo();
92+
repo.setRootPackageJson(['packages/*']);
93+
repo.addWorkspace('@test/alpha', 'packages/alpha', { lodash: '^4.17.15' });
94+
repo.addWorkspace('@test/beta', 'packages/beta', { lodash: '^4.17.21' });
95+
96+
const errors = await requireUnifiedDependencyVersions.verify({ repoRoot: repo.root });
97+
98+
expect(errors).toHaveLength(1);
99+
expect(errors[0]).toContain('"lodash"');
100+
expect(errors[0]).toContain('^4.17.15');
101+
expect(errors[0]).toContain('^4.17.21');
102+
});
103+
104+
it('reports errors across dependencies and devDependencies', async () => {
105+
const repo = createTempRepo();
106+
repo.setRootPackageJson(['packages/*']);
107+
repo.addWorkspace('@test/alpha', 'packages/alpha', { express: '4.18.0' });
108+
repo.addWorkspace('@test/beta', 'packages/beta', {}, { express: '^4.17.0' });
109+
110+
const errors = await requireUnifiedDependencyVersions.verify({ repoRoot: repo.root });
111+
112+
expect(errors).toHaveLength(1);
113+
expect(errors[0]).toContain('"express"');
114+
});
115+
116+
it('ignores workspace: protocol references', async () => {
117+
const repo = createTempRepo();
118+
repo.setRootPackageJson(['packages/*']);
119+
repo.addWorkspace('@test/alpha', 'packages/alpha', { '@test/beta': 'workspace:*' });
120+
repo.addWorkspace('@test/beta', 'packages/beta', { '@test/alpha': 'workspace:^' });
121+
122+
const errors = await requireUnifiedDependencyVersions.verify({ repoRoot: repo.root });
123+
124+
expect(errors).toEqual([]);
125+
});
126+
127+
it('ignores git/file/link references', async () => {
128+
const repo = createTempRepo();
129+
repo.setRootPackageJson(['packages/*']);
130+
repo.addWorkspace('@test/alpha', 'packages/alpha', {
131+
'some-pkg': 'github:user/repo#v1',
132+
});
133+
repo.addWorkspace('@test/beta', 'packages/beta', {
134+
'some-pkg': 'github:user/repo#v2',
135+
});
136+
137+
const errors = await requireUnifiedDependencyVersions.verify({ repoRoot: repo.root });
138+
139+
expect(errors).toEqual([]);
140+
});
141+
142+
it('reports no error when a dependency is used in only one workspace', async () => {
143+
const repo = createTempRepo();
144+
repo.setRootPackageJson(['packages/*']);
145+
repo.addWorkspace('@test/alpha', 'packages/alpha', { lodash: '^4.17.21' });
146+
repo.addWorkspace('@test/beta', 'packages/beta', { express: '^4.18.0' });
147+
148+
const errors = await requireUnifiedDependencyVersions.verify({ repoRoot: repo.root });
149+
150+
expect(errors).toEqual([]);
151+
});
152+
153+
it('reports multiple drifted dependencies', async () => {
154+
const repo = createTempRepo();
155+
repo.setRootPackageJson(['packages/*']);
156+
repo.addWorkspace('@test/alpha', 'packages/alpha', {
157+
lodash: '^4.17.15',
158+
express: '4.18.0',
159+
});
160+
repo.addWorkspace('@test/beta', 'packages/beta', {
161+
lodash: '^4.17.21',
162+
express: '4.19.0',
163+
});
164+
165+
const errors = await requireUnifiedDependencyVersions.verify({ repoRoot: repo.root });
166+
167+
expect(errors).toHaveLength(2);
168+
});
169+
170+
it('detects drift between root package.json and workspace', async () => {
171+
const repo = createTempRepo();
172+
repo.setRootPackageJson(['packages/*'], { lodash: '^4.17.15' });
173+
repo.addWorkspace('@test/alpha', 'packages/alpha', { lodash: '^4.17.21' });
174+
175+
const errors = await requireUnifiedDependencyVersions.verify({ repoRoot: repo.root });
176+
177+
expect(errors).toHaveLength(1);
178+
expect(errors[0]).toContain('"lodash"');
179+
expect(errors[0]).toContain('test-monorepo');
180+
expect(errors[0]).toContain('^4.17.15');
181+
expect(errors[0]).toContain('^4.17.21');
182+
});
183+
184+
it('supports flat workspaces array', async () => {
185+
const repo = createTempRepo();
186+
// Use flat array format instead of { packages: [...] }
187+
writeFileSync(
188+
join(repo.root, 'package.json'),
189+
JSON.stringify({ name: 'test-monorepo', workspaces: ['packages/*'] }, null, 4) + '\n',
190+
);
191+
repo.addWorkspace('@test/alpha', 'packages/alpha', { lodash: '^4.17.15' });
192+
repo.addWorkspace('@test/beta', 'packages/beta', { lodash: '^4.17.21' });
193+
194+
const errors = await requireUnifiedDependencyVersions.verify({ repoRoot: repo.root });
195+
196+
expect(errors).toHaveLength(1);
197+
});
198+
199+
it('reports error for workspace with invalid package.json', async () => {
200+
const repo = createTempRepo();
201+
repo.setRootPackageJson(['packages/*']);
202+
repo.addWorkspace('@test/alpha', 'packages/alpha', { lodash: '^4.17.21' });
203+
204+
// Create a workspace with invalid JSON
205+
mkdirSync(join(repo.root, 'packages/broken'), { recursive: true });
206+
writeFileSync(join(repo.root, 'packages/broken/package.json'), '{ invalid json');
207+
208+
const errors = await requireUnifiedDependencyVersions.verify({ repoRoot: repo.root });
209+
210+
expect(errors.some(e => e.includes('packages/broken/package.json'))).toBe(true);
211+
});
212+
213+
describe('fix mode', () => {
214+
it('aligns all versions to the most common one', async () => {
215+
const repo = createTempRepo();
216+
repo.setRootPackageJson(['packages/*']);
217+
repo.addWorkspace('@test/alpha', 'packages/alpha', { lodash: '^4.17.21' });
218+
repo.addWorkspace('@test/beta', 'packages/beta', { lodash: '^4.17.21' });
219+
repo.addWorkspace('@test/gamma', 'packages/gamma', { lodash: '^4.17.15' });
220+
221+
const errors = await requireUnifiedDependencyVersions.fix!({ repoRoot: repo.root });
222+
223+
expect(errors).toEqual([]);
224+
225+
// Verify the file was updated
226+
const gammaPkg = JSON.parse(
227+
readFileSync(join(repo.root, 'packages/gamma/package.json'), 'utf-8'),
228+
);
229+
expect(gammaPkg.dependencies.lodash).toBe('^4.17.21');
230+
});
231+
232+
it('picks numerically higher version when frequencies are tied', async () => {
233+
const repo = createTempRepo();
234+
repo.setRootPackageJson(['packages/*']);
235+
// Both versions appear once - tie-break should prefer ^10.2.0 over ^9.5.0
236+
repo.addWorkspace('@test/alpha', 'packages/alpha', { storybook: '^9.5.0' });
237+
repo.addWorkspace('@test/beta', 'packages/beta', { storybook: '^10.2.0' });
238+
239+
const errors = await requireUnifiedDependencyVersions.fix!({ repoRoot: repo.root });
240+
241+
expect(errors).toEqual([]);
242+
243+
const alphaPkg = JSON.parse(
244+
readFileSync(join(repo.root, 'packages/alpha/package.json'), 'utf-8'),
245+
);
246+
expect(alphaPkg.dependencies.storybook).toBe('^10.2.0');
247+
});
248+
249+
it('fixes drift between root package.json and workspace', async () => {
250+
const repo = createTempRepo();
251+
// Root has one version, two workspaces have another — workspace version wins by frequency
252+
repo.setRootPackageJson(['packages/*'], { lodash: '^4.17.15' });
253+
repo.addWorkspace('@test/alpha', 'packages/alpha', { lodash: '^4.17.21' });
254+
repo.addWorkspace('@test/beta', 'packages/beta', { lodash: '^4.17.21' });
255+
256+
const errors = await requireUnifiedDependencyVersions.fix!({ repoRoot: repo.root });
257+
258+
expect(errors).toEqual([]);
259+
260+
// Verify the root was updated
261+
const rootPkg = JSON.parse(readFileSync(join(repo.root, 'package.json'), 'utf-8'));
262+
expect(rootPkg.dependencies.lodash).toBe('^4.17.21');
263+
});
264+
265+
it('returns no errors when there are no drifts', async () => {
266+
const repo = createTempRepo();
267+
repo.setRootPackageJson(['packages/*']);
268+
repo.addWorkspace('@test/alpha', 'packages/alpha', { lodash: '^4.17.21' });
269+
repo.addWorkspace('@test/beta', 'packages/beta', { lodash: '^4.17.21' });
270+
271+
const errors = await requireUnifiedDependencyVersions.fix!({ repoRoot: repo.root });
272+
273+
expect(errors).toEqual([]);
274+
});
275+
});
276+
});
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
import type { Requirement, RequirementScope } from './Requirement';
22
import { requireAgentsSkills } from './agents-skills/requireAgentsSkills';
3+
import { requireUnifiedDependencyVersions } from './dependency-versions/requireUnifiedDependencyVersions';
34
import { requireDocsSummary } from './docs-summary/requireDocsSummary';
45
import { requirePackageJsonScripts } from './package-json/requirePackageJsonScripts';
56

67
export const requirements: ReadonlyArray<Requirement<RequirementScope>> = [
78
requireAgentsSkills,
9+
requireUnifiedDependencyVersions,
810
requireDocsSummary,
911
requirePackageJsonScripts,
1012
];

0 commit comments

Comments
 (0)