Skip to content

Commit f06e2ba

Browse files
committed
debug: add comprehensive logging to Composer scanning
Added detailed debug logging to understand why 0 Composer updates are found: - Log composer outdated command execution - Log raw output from composer outdated - Log parsed JSON data structure - Log composer.json file path and existence - Log packages found in composer.json require/require-dev This will help identify if the issue is: 1. composer outdated returning no packages 2. composer.json not being found/read 3. packages not matching between outdated and composer.json
1 parent 4edb42b commit f06e2ba

File tree

3 files changed

+405
-9
lines changed

3 files changed

+405
-9
lines changed

src/registry/registry-client.ts

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -568,8 +568,11 @@ export class RegistryClient {
568568

569569
try {
570570
// Use composer outdated to get all available updates, then filter based on version constraints
571+
this.logger.info('Running composer outdated command...')
571572
const output = await this.runCommand('composer', ['outdated', '--format=json', '--direct'])
573+
this.logger.info(`Composer outdated raw output: ${output}`)
572574
const composerData = JSON.parse(output)
575+
this.logger.info(`Parsed composer data:`, composerData)
573576

574577
const updates: PackageUpdate[] = []
575578

@@ -579,9 +582,13 @@ export class RegistryClient {
579582
const fs = await import('node:fs')
580583
const path = await import('node:path')
581584
const composerJsonPath = path.join(this.projectPath, 'composer.json')
585+
this.logger.info(`Looking for composer.json at: ${composerJsonPath}`)
582586
if (fs.existsSync(composerJsonPath)) {
583587
const composerContent = fs.readFileSync(composerJsonPath, 'utf-8')
584588
composerJsonData = JSON.parse(composerContent)
589+
this.logger.info(`Found composer.json with packages:`, Object.keys(composerJsonData.require || {}), Object.keys(composerJsonData['require-dev'] || {}))
590+
} else {
591+
this.logger.warn(`composer.json not found at ${composerJsonPath}`)
585592
}
586593
}
587594
catch (error) {
@@ -591,18 +598,18 @@ export class RegistryClient {
591598
// Parse composer outdated output and filter based on version constraints
592599
if (composerData.installed) {
593600
this.logger.info(`Processing ${composerData.installed.length} outdated Composer packages`)
594-
601+
595602
for (const pkg of composerData.installed) {
596603
if (pkg.name && pkg.version && pkg.latest) {
597604
this.logger.info(`Checking ${pkg.name}: ${pkg.version}${pkg.latest}`)
598-
605+
599606
// Get the version constraint from composer.json
600607
const requireConstraint = composerJsonData.require?.[pkg.name]
601608
const requireDevConstraint = composerJsonData['require-dev']?.[pkg.name]
602609
const constraint = requireConstraint || requireDevConstraint
603-
610+
604611
this.logger.info(`Constraint for ${pkg.name}: ${constraint}`)
605-
612+
606613
if (!constraint) {
607614
this.logger.info(`Skipping ${pkg.name}: not found in composer.json`)
608615
continue // Skip packages not found in composer.json
@@ -612,22 +619,22 @@ export class RegistryClient {
612619
let newVersion = pkg.latest
613620
const currentMajor = this.getMajorVersion(pkg.version)
614621
const latestMajor = this.getMajorVersion(pkg.latest)
615-
622+
616623
this.logger.info(`Version analysis for ${pkg.name}: current major=${currentMajor}, latest major=${latestMajor}`)
617-
624+
618625
// For caret constraints (^), only allow updates within the same major version
619626
if (constraint.startsWith('^')) {
620627
if (currentMajor !== latestMajor) {
621628
this.logger.info(`Skipping ${pkg.name}: major version change not allowed by ^ constraint`)
622629
continue // Skip major version updates for caret constraints
623630
}
624631
}
625-
632+
626633
// For tilde constraints (~), handle according to the constraint level
627634
if (constraint.startsWith('~')) {
628635
const currentMinor = this.getMinorVersion(pkg.version)
629636
const latestMinor = this.getMinorVersion(pkg.latest)
630-
637+
631638
// ~1.2 allows patch updates within 1.2.x
632639
if (constraint.includes('.')) {
633640
if (currentMajor !== latestMajor || currentMinor !== latestMinor) {
@@ -642,7 +649,7 @@ export class RegistryClient {
642649
}
643650
}
644651
}
645-
652+
646653
this.logger.info(`Accepted ${pkg.name}: ${pkg.version}${newVersion}`)
647654

648655
const updateType = getUpdateType(pkg.version, newVersion)
Lines changed: 251 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,251 @@
1+
import { beforeEach, describe, expect, it, spyOn } from 'bun:test'
2+
import { RegistryClient } from '../src/registry/registry-client'
3+
import fs from 'node:fs'
4+
5+
describe('Composer Constraint Filtering', () => {
6+
let registryClient: RegistryClient
7+
8+
beforeEach(() => {
9+
const mockConfig = {
10+
packages: {
11+
strategy: 'all' as const,
12+
ignore: [],
13+
includePrerelease: false,
14+
excludeMajor: false
15+
}
16+
}
17+
const mockLogger = {
18+
info: (...args: any[]) => console.log('[INFO]', ...args),
19+
warn: (...args: any[]) => console.log('[WARN]', ...args),
20+
error: (...args: any[]) => console.log('[ERROR]', ...args),
21+
success: (...args: any[]) => console.log('[SUCCESS]', ...args),
22+
debug: (...args: any[]) => console.log('[DEBUG]', ...args)
23+
}
24+
registryClient = new RegistryClient(mockConfig, '.', mockLogger as any)
25+
})
26+
27+
it('should include minor/patch updates within caret constraints', async () => {
28+
// Mock composer.json with caret constraints (real constraints from user's repo)
29+
const mockComposerJson = {
30+
"require": {
31+
"laravel/framework": "^10.0",
32+
"symfony/console": "^6.0",
33+
"monolog/monolog": "^3.0",
34+
"doctrine/dbal": "^3.0",
35+
"guzzlehttp/guzzle": "^7.0"
36+
},
37+
"require-dev": {
38+
"phpunit/phpunit": "^10.0",
39+
"mockery/mockery": "^1.5",
40+
"fakerphp/faker": "^1.20"
41+
}
42+
}
43+
44+
// Mock composer outdated output - this is what the real command returns
45+
const mockComposerOutdated = {
46+
"installed": [
47+
// These should be INCLUDED (minor/patch updates within constraints)
48+
{
49+
"name": "laravel/framework",
50+
"version": "10.0.0",
51+
"latest": "10.48.29" // Minor update within ^10.0
52+
},
53+
{
54+
"name": "symfony/console",
55+
"version": "6.0.0",
56+
"latest": "6.4.23" // Minor update within ^6.0
57+
},
58+
{
59+
"name": "monolog/monolog",
60+
"version": "3.0.0",
61+
"latest": "3.9.0" // Minor update within ^3.0
62+
},
63+
{
64+
"name": "doctrine/dbal",
65+
"version": "3.0.0",
66+
"latest": "3.10.0" // Minor update within ^3.0
67+
},
68+
{
69+
"name": "guzzlehttp/guzzle",
70+
"version": "7.0.0",
71+
"latest": "7.9.3" // Minor update within ^7.0
72+
},
73+
{
74+
"name": "phpunit/phpunit",
75+
"version": "10.0.0",
76+
"latest": "10.5.48" // Minor update within ^10.0
77+
},
78+
{
79+
"name": "mockery/mockery",
80+
"version": "1.5.0",
81+
"latest": "1.6.12" // Minor update within ^1.5
82+
},
83+
{
84+
"name": "fakerphp/faker",
85+
"version": "1.20.0",
86+
"latest": "1.24.1" // Minor update within ^1.20
87+
},
88+
// These should be EXCLUDED (major updates outside constraints)
89+
{
90+
"name": "symfony/console",
91+
"version": "6.4.23",
92+
"latest": "7.3.1" // Major update outside ^6.0
93+
},
94+
{
95+
"name": "laravel/framework",
96+
"version": "10.48.29",
97+
"latest": "12.21.0" // Major update outside ^10.0
98+
}
99+
]
100+
}
101+
102+
// Mock file system and commands
103+
spyOn(fs, 'existsSync').mockReturnValue(true)
104+
spyOn(fs, 'readFileSync').mockReturnValue(JSON.stringify(mockComposerJson))
105+
106+
spyOn(registryClient as any, 'runCommand').mockImplementation((command: string, args: string[]) => {
107+
if (command === 'composer' && args[0] === '--version') {
108+
return Promise.resolve('Composer version 2.7.1')
109+
}
110+
if (command === 'composer' && args.includes('outdated')) {
111+
return Promise.resolve(JSON.stringify(mockComposerOutdated))
112+
}
113+
return Promise.reject(new Error('Unexpected command'))
114+
})
115+
116+
const updates = await registryClient.getComposerOutdatedPackages()
117+
118+
// Should find 8 minor/patch updates, exclude 2 major updates
119+
expect(updates).toHaveLength(8)
120+
121+
// Verify included packages
122+
const packageNames = updates.map(u => u.name)
123+
expect(packageNames).toContain('laravel/framework')
124+
expect(packageNames).toContain('symfony/console')
125+
expect(packageNames).toContain('monolog/monolog')
126+
expect(packageNames).toContain('doctrine/dbal')
127+
expect(packageNames).toContain('guzzlehttp/guzzle')
128+
expect(packageNames).toContain('phpunit/phpunit')
129+
expect(packageNames).toContain('mockery/mockery')
130+
expect(packageNames).toContain('fakerphp/faker')
131+
132+
// Verify versions are the minor updates, not major
133+
const laravelUpdate = updates.find(u => u.name === 'laravel/framework')!
134+
expect(laravelUpdate.newVersion).toBe('10.48.29') // Not 12.21.0
135+
136+
const symfonyUpdate = updates.find(u => u.name === 'symfony/console')!
137+
expect(symfonyUpdate.newVersion).toBe('6.4.23') // Not 7.3.1
138+
139+
// Verify all are minor/patch updates
140+
updates.forEach(update => {
141+
expect(['minor', 'patch']).toContain(update.updateType)
142+
})
143+
})
144+
145+
it('should allow major updates when no constraints restrict them', async () => {
146+
// Mock composer.json with loose constraints or no constraints
147+
const mockComposerJson = {
148+
"require": {
149+
"some/package": ">=1.0", // Allows major updates
150+
"other/package": "*" // Allows any version
151+
}
152+
}
153+
154+
const mockComposerOutdated = {
155+
"installed": [
156+
{
157+
"name": "some/package",
158+
"version": "1.0.0",
159+
"latest": "2.0.0" // Major update allowed by >=1.0
160+
},
161+
{
162+
"name": "other/package",
163+
"version": "1.0.0",
164+
"latest": "3.0.0" // Major update allowed by *
165+
}
166+
]
167+
}
168+
169+
spyOn(fs, 'existsSync').mockReturnValue(true)
170+
spyOn(fs, 'readFileSync').mockReturnValue(JSON.stringify(mockComposerJson))
171+
172+
spyOn(registryClient as any, 'runCommand').mockImplementation((command: string, args: string[]) => {
173+
if (command === 'composer' && args[0] === '--version') {
174+
return Promise.resolve('Composer version 2.7.1')
175+
}
176+
if (command === 'composer' && args.includes('outdated')) {
177+
return Promise.resolve(JSON.stringify(mockComposerOutdated))
178+
}
179+
return Promise.reject(new Error('Unexpected command'))
180+
})
181+
182+
const updates = await registryClient.getComposerOutdatedPackages()
183+
184+
// Should include both major updates
185+
expect(updates).toHaveLength(2)
186+
187+
updates.forEach(update => {
188+
expect(update.updateType).toBe('major')
189+
})
190+
})
191+
192+
it('should handle tilde constraints correctly', async () => {
193+
const mockComposerJson = {
194+
"require": {
195+
"patch-only": "~1.2.3", // Only allows 1.2.x patches
196+
"minor-allowed": "~1.2" // Allows 1.x.x minor/patch
197+
}
198+
}
199+
200+
const mockComposerOutdated = {
201+
"installed": [
202+
// Should be included
203+
{
204+
"name": "patch-only",
205+
"version": "1.2.3",
206+
"latest": "1.2.5" // Patch within ~1.2.3
207+
},
208+
{
209+
"name": "minor-allowed",
210+
"version": "1.2.0",
211+
"latest": "1.5.0" // Minor within ~1.2
212+
},
213+
// Should be excluded
214+
{
215+
"name": "patch-only",
216+
"version": "1.2.5",
217+
"latest": "1.3.0" // Minor outside ~1.2.3
218+
},
219+
{
220+
"name": "minor-allowed",
221+
"version": "1.5.0",
222+
"latest": "2.0.0" // Major outside ~1.2
223+
}
224+
]
225+
}
226+
227+
spyOn(fs, 'existsSync').mockReturnValue(true)
228+
spyOn(fs, 'readFileSync').mockReturnValue(JSON.stringify(mockComposerJson))
229+
230+
spyOn(registryClient as any, 'runCommand').mockImplementation((command: string, args: string[]) => {
231+
if (command === 'composer' && args[0] === '--version') {
232+
return Promise.resolve('Composer version 2.7.1')
233+
}
234+
if (command === 'composer' && args.includes('outdated')) {
235+
return Promise.resolve(JSON.stringify(mockComposerOutdated))
236+
}
237+
return Promise.reject(new Error('Unexpected command'))
238+
})
239+
240+
const updates = await registryClient.getComposerOutdatedPackages()
241+
242+
// Should find 2 allowed updates
243+
expect(updates).toHaveLength(2)
244+
245+
const patchUpdate = updates.find(u => u.name === 'patch-only')!
246+
expect(patchUpdate.newVersion).toBe('1.2.5')
247+
248+
const minorUpdate = updates.find(u => u.name === 'minor-allowed')!
249+
expect(minorUpdate.newVersion).toBe('1.5.0')
250+
})
251+
})

0 commit comments

Comments
 (0)