Skip to content
This repository was archived by the owner on Nov 14, 2025. It is now read-only.

Commit 2747a8c

Browse files
authored
Merge pull request #164 from sapientpants/feat/156-unified-versioning
feat: implement unified versioning system with single source of truth
2 parents ee816da + fe8f66d commit 2747a8c

File tree

12 files changed

+799
-8
lines changed

12 files changed

+799
-8
lines changed

.changeset/unified-versioning.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
---
2+
'deepsource-mcp-server': minor
3+
---
4+
5+
Add unified versioning system with single source of truth
6+
7+
- Created central `version.ts` module that reads from package.json
8+
- Added CLI support for `--version` and `-v` flags to display version
9+
- Added `--help` and `-h` flags with comprehensive help text
10+
- Replaced all hardcoded version strings with VERSION constant
11+
- Added version to startup logging for better debugging
12+
- Created build script to inject version at build time
13+
- Added comprehensive version utilities (parsing, validation, comparison)
14+
- Exported VERSION constant and helper functions for programmatic access
15+
- Added fallback to "0.0.0-dev" when package.json is unavailable
16+
17+
This ensures consistent version reporting across:
18+
19+
- CLI output
20+
- Server metadata
21+
- Startup logs
22+
- Error messages
23+
- MCP protocol responses

.deepsource.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@ exclude_patterns = [
2222
"jest.setup.ts",
2323
"vitest.setup.ts",
2424
"docs/**",
25-
"examples/**"
25+
"examples/**",
26+
"scripts/**"
2627
]
2728

2829
[[analyzers]]

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
},
2828
"scripts": {
2929
"build:watch": "tsc -p tsconfig.build.json --watch",
30-
"build": "tsc -p tsconfig.build.json",
30+
"build": "tsc -p tsconfig.build.json && node scripts/build-with-version.js",
3131
"changeset:status": "changeset status --since=main",
3232
"changeset": "changeset",
3333
"ci:local:fast": "./scripts/ci-local.sh --fast",

scripts/build-with-version.js

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
#!/usr/bin/env node
2+
3+
/* eslint-disable no-console */
4+
5+
/**
6+
* Build script that injects the version from package.json into the compiled code
7+
* This ensures the version is available even when package.json is not accessible at runtime
8+
*/
9+
10+
import { readFileSync, writeFileSync, readdirSync, statSync } from 'fs';
11+
import { join, dirname } from 'path';
12+
import { fileURLToPath } from 'url';
13+
14+
const __filename = fileURLToPath(import.meta.url);
15+
const __dirname = dirname(__filename);
16+
17+
// Read version from package.json
18+
const packageJsonPath = join(__dirname, '..', 'package.json');
19+
const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));
20+
const version = packageJson.version;
21+
22+
console.log(`Build script: Injecting version ${version} into compiled files...`);
23+
24+
// Function to recursively find all JavaScript files in dist
25+
function findJsFiles(dir, files = []) {
26+
const items = readdirSync(dir);
27+
28+
for (const item of items) {
29+
const fullPath = join(dir, item);
30+
const stat = statSync(fullPath);
31+
32+
if (stat.isDirectory()) {
33+
findJsFiles(fullPath, files);
34+
} else if (item.endsWith('.js')) {
35+
files.push(fullPath);
36+
}
37+
}
38+
39+
return files;
40+
}
41+
42+
// Path to dist directory
43+
const distDir = join(__dirname, '..', 'dist');
44+
45+
try {
46+
// Find all JavaScript files in dist
47+
const jsFiles = findJsFiles(distDir);
48+
49+
// Look for version.js specifically
50+
const versionFile = jsFiles.find((file) => file.endsWith('version.js'));
51+
52+
if (versionFile) {
53+
// Read the file
54+
let content = readFileSync(versionFile, 'utf-8');
55+
56+
// Replace the placeholder with the actual version
57+
const originalContent = content;
58+
content = content.replace(/__BUILD_VERSION__/g, version);
59+
60+
// Only write if something changed
61+
if (content !== originalContent) {
62+
writeFileSync(versionFile, content, 'utf-8');
63+
console.log(`✓ Injected version ${version} into ${versionFile}`);
64+
} else {
65+
console.log(`✓ Version already set correctly in ${versionFile}`);
66+
}
67+
} else {
68+
console.warn('Warning: version.js not found in dist directory');
69+
}
70+
71+
// Also update any other files that might reference __BUILD_VERSION__
72+
let filesUpdated = 0;
73+
for (const file of jsFiles) {
74+
if (file === versionFile) continue; // Already handled
75+
76+
let content = readFileSync(file, 'utf-8');
77+
if (content.includes('__BUILD_VERSION__')) {
78+
content = content.replace(/__BUILD_VERSION__/g, version);
79+
writeFileSync(file, content, 'utf-8');
80+
filesUpdated++;
81+
console.log(`✓ Updated version in ${file}`);
82+
}
83+
}
84+
85+
if (filesUpdated > 0) {
86+
console.log(`✓ Updated version in ${filesUpdated} additional file(s)`);
87+
}
88+
89+
console.log('✓ Build version injection complete');
90+
} catch (error) {
91+
console.error('Error during version injection:', error);
92+
process.exit(1);
93+
}

src/__tests__/cli-version.test.ts

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
/**
2+
* @fileoverview Tests for CLI version flag functionality
3+
*/
4+
5+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
6+
import { VERSION, getVersion, getVersionInfo, parseVersion } from '../version.js';
7+
import { handleCliArgs } from '../index.js';
8+
9+
describe('CLI Version Module', () => {
10+
describe('Version exports', () => {
11+
it('should export VERSION constant matching expected format', () => {
12+
expect(VERSION).toMatch(/^\d+\.\d+\.\d+(-.*)?$/);
13+
});
14+
15+
it('should return version from getVersion function', () => {
16+
expect(getVersion()).toBe(VERSION);
17+
});
18+
19+
it('should return version info from getVersionInfo', () => {
20+
const info = getVersionInfo();
21+
expect(info.version).toBe(VERSION);
22+
23+
const parsed = parseVersion(VERSION);
24+
if (parsed) {
25+
expect(info.major).toBe(parsed.major);
26+
expect(info.minor).toBe(parsed.minor);
27+
expect(info.patch).toBe(parsed.patch);
28+
}
29+
});
30+
});
31+
32+
describe('CLI argument handling', () => {
33+
let consoleLogSpy: ReturnType<typeof vi.spyOn>;
34+
let logOutput: string[];
35+
36+
beforeEach(() => {
37+
logOutput = [];
38+
// Mock console.log to capture output
39+
consoleLogSpy = vi.spyOn(console, 'log').mockImplementation((...args) => {
40+
logOutput.push(args.join(' '));
41+
});
42+
});
43+
44+
afterEach(() => {
45+
consoleLogSpy.mockRestore();
46+
});
47+
48+
it('should display version with --version flag', () => {
49+
const result = handleCliArgs(['--version']);
50+
51+
expect(result).toBe(true);
52+
expect(logOutput).toContain(`deepsource-mcp-server version ${VERSION}`);
53+
});
54+
55+
it('should display version with -v flag', () => {
56+
const result = handleCliArgs(['-v']);
57+
58+
expect(result).toBe(true);
59+
expect(logOutput).toContain(`deepsource-mcp-server version ${VERSION}`);
60+
});
61+
62+
it('should display help with --help flag', () => {
63+
const result = handleCliArgs(['--help']);
64+
65+
expect(result).toBe(true);
66+
expect(logOutput.join('\n')).toContain(`DeepSource MCP Server v${VERSION}`);
67+
expect(logOutput.join('\n')).toContain('Usage: deepsource-mcp-server [options]');
68+
expect(logOutput.join('\n')).toContain('Options:');
69+
expect(logOutput.join('\n')).toContain('-v, --version Display version information');
70+
expect(logOutput.join('\n')).toContain('-h, --help Display this help message');
71+
});
72+
73+
it('should display help with -h flag', () => {
74+
const result = handleCliArgs(['-h']);
75+
76+
expect(result).toBe(true);
77+
expect(logOutput.join('\n')).toContain(`DeepSource MCP Server v${VERSION}`);
78+
expect(logOutput.join('\n')).toContain('Usage: deepsource-mcp-server [options]');
79+
});
80+
81+
it('should handle --version flag at any position', () => {
82+
const result = handleCliArgs(['some', 'args', '--version']);
83+
84+
expect(result).toBe(true);
85+
expect(logOutput).toContain(`deepsource-mcp-server version ${VERSION}`);
86+
});
87+
88+
it('should return false for non-CLI arguments', () => {
89+
const result = handleCliArgs(['some', 'other', 'args']);
90+
91+
expect(result).toBe(false);
92+
expect(logOutput).toEqual([]);
93+
});
94+
95+
it('should handle empty arguments', () => {
96+
const result = handleCliArgs([]);
97+
98+
expect(result).toBe(false);
99+
expect(logOutput).toEqual([]);
100+
});
101+
102+
it('should work without DEEPSOURCE_API_KEY for version flag', () => {
103+
// Remove DEEPSOURCE_API_KEY if present
104+
const originalApiKey = process.env.DEEPSOURCE_API_KEY;
105+
delete process.env.DEEPSOURCE_API_KEY;
106+
107+
const result = handleCliArgs(['--version']);
108+
109+
expect(result).toBe(true);
110+
expect(logOutput).toContain(`deepsource-mcp-server version ${VERSION}`);
111+
112+
// Restore API key if it was set
113+
if (originalApiKey) {
114+
process.env.DEEPSOURCE_API_KEY = originalApiKey;
115+
}
116+
});
117+
});
118+
});

0 commit comments

Comments
 (0)