@lvnt/release-radar 1.2.0 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/config/downloads.json +17 -0
- package/dist/checker.d.ts +4 -1
- package/dist/checker.js +1 -0
- package/dist/checker.test.js +16 -0
- package/dist/cli-publisher.d.ts +13 -0
- package/dist/cli-publisher.js +50 -0
- package/dist/index.js +76 -14
- package/dist/types.d.ts +20 -0
- package/dist/versions-generator.d.ts +2 -0
- package/dist/versions-generator.js +23 -0
- package/dist/versions-generator.test.d.ts +1 -0
- package/dist/versions-generator.test.js +47 -0
- package/package.json +1 -1
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"Ninja": {
|
|
3
|
+
"displayName": "Ninja",
|
|
4
|
+
"downloadUrl": "github.com/ninja-build/ninja/releases/download/v{{VERSION}}/ninja-linux.zip",
|
|
5
|
+
"filename": "ninja-{{VERSION}}-linux.zip"
|
|
6
|
+
},
|
|
7
|
+
"CMake": {
|
|
8
|
+
"displayName": "CMake",
|
|
9
|
+
"downloadUrl": "github.com/Kitware/CMake/releases/download/v{{VERSION}}/cmake-{{VERSION}}-linux-x86_64.tar.gz",
|
|
10
|
+
"filename": "cmake-{{VERSION}}-linux-x86_64.tar.gz"
|
|
11
|
+
},
|
|
12
|
+
"Git": {
|
|
13
|
+
"displayName": "Git for Windows",
|
|
14
|
+
"downloadUrl": "github.com/git-for-windows/git/releases/download/v{{VERSION}}.windows.1/Git-{{VERSION}}-64-bit.exe",
|
|
15
|
+
"filename": "Git-{{VERSION}}-64-bit.exe"
|
|
16
|
+
}
|
|
17
|
+
}
|
package/dist/checker.d.ts
CHANGED
package/dist/checker.js
CHANGED
package/dist/checker.test.js
CHANGED
|
@@ -61,4 +61,20 @@ describe('Checker', () => {
|
|
|
61
61
|
expect(mockNotifier.sendBatchedUpdates).toHaveBeenCalledWith([]);
|
|
62
62
|
expect(mockStorage.setVersion).not.toHaveBeenCalled();
|
|
63
63
|
});
|
|
64
|
+
it('returns hasUpdates true when updates found', async () => {
|
|
65
|
+
mockStorage.getVersion.mockReturnValueOnce('1.11.1').mockReturnValueOnce('2.43.0');
|
|
66
|
+
vi.mocked(fetchVersion)
|
|
67
|
+
.mockResolvedValueOnce('1.12.0')
|
|
68
|
+
.mockResolvedValueOnce('2.43.0');
|
|
69
|
+
const result = await checker.checkAll();
|
|
70
|
+
expect(result.hasUpdates).toBe(true);
|
|
71
|
+
expect(result.updateCount).toBe(1);
|
|
72
|
+
});
|
|
73
|
+
it('returns hasUpdates false when no updates', async () => {
|
|
74
|
+
mockStorage.getVersion.mockReturnValue('1.12.0');
|
|
75
|
+
vi.mocked(fetchVersion).mockResolvedValue('1.12.0');
|
|
76
|
+
const result = await checker.checkAll();
|
|
77
|
+
expect(result.hasUpdates).toBe(false);
|
|
78
|
+
expect(result.updateCount).toBe(0);
|
|
79
|
+
});
|
|
64
80
|
});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { DownloadsConfig } from './types.js';
|
|
2
|
+
export interface PublishResult {
|
|
3
|
+
success: boolean;
|
|
4
|
+
version?: string;
|
|
5
|
+
error?: string;
|
|
6
|
+
}
|
|
7
|
+
export declare class CliPublisher {
|
|
8
|
+
private downloadsConfig;
|
|
9
|
+
private cliPath;
|
|
10
|
+
constructor(downloadsConfig: DownloadsConfig, cliPath?: string);
|
|
11
|
+
isConfigured(): boolean;
|
|
12
|
+
publish(versions: Record<string, string>): Promise<PublishResult>;
|
|
13
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// src/cli-publisher.ts
|
|
2
|
+
import { execSync } from 'child_process';
|
|
3
|
+
import { readFileSync, writeFileSync, existsSync } from 'fs';
|
|
4
|
+
import { generateVersionsJson } from './versions-generator.js';
|
|
5
|
+
export class CliPublisher {
|
|
6
|
+
downloadsConfig;
|
|
7
|
+
cliPath;
|
|
8
|
+
constructor(downloadsConfig, cliPath = './cli') {
|
|
9
|
+
this.downloadsConfig = downloadsConfig;
|
|
10
|
+
this.cliPath = cliPath;
|
|
11
|
+
}
|
|
12
|
+
isConfigured() {
|
|
13
|
+
return Object.keys(this.downloadsConfig).length > 0 && existsSync(this.cliPath);
|
|
14
|
+
}
|
|
15
|
+
async publish(versions) {
|
|
16
|
+
if (!this.isConfigured()) {
|
|
17
|
+
return { success: false, error: 'CLI publisher not configured' };
|
|
18
|
+
}
|
|
19
|
+
try {
|
|
20
|
+
// Generate versions.json
|
|
21
|
+
const versionsJson = generateVersionsJson(versions, this.downloadsConfig);
|
|
22
|
+
// Write to data/ for reference
|
|
23
|
+
writeFileSync('./data/cli-versions.json', JSON.stringify(versionsJson, null, 2));
|
|
24
|
+
// Copy to CLI package
|
|
25
|
+
const cliVersionsPath = `${this.cliPath}/versions.json`;
|
|
26
|
+
writeFileSync(cliVersionsPath, JSON.stringify(versionsJson, null, 2));
|
|
27
|
+
// Read current CLI version
|
|
28
|
+
const pkgPath = `${this.cliPath}/package.json`;
|
|
29
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
|
|
30
|
+
const currentVersion = pkg.version;
|
|
31
|
+
// Bump patch version
|
|
32
|
+
const versionParts = currentVersion.split('.').map(Number);
|
|
33
|
+
versionParts[2]++;
|
|
34
|
+
const newVersion = versionParts.join('.');
|
|
35
|
+
pkg.version = newVersion;
|
|
36
|
+
writeFileSync(pkgPath, JSON.stringify(pkg, null, 2));
|
|
37
|
+
// Build CLI
|
|
38
|
+
execSync('npm run build', { cwd: this.cliPath, stdio: 'pipe' });
|
|
39
|
+
// Publish to npm
|
|
40
|
+
execSync('npm publish --access public', { cwd: this.cliPath, stdio: 'pipe' });
|
|
41
|
+
console.log(`[CliPublisher] Published @lvnt/release-radar-cli v${newVersion}`);
|
|
42
|
+
return { success: true, version: newVersion };
|
|
43
|
+
}
|
|
44
|
+
catch (error) {
|
|
45
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
46
|
+
console.error(`[CliPublisher] Failed to publish: ${message}`);
|
|
47
|
+
return { success: false, error: message };
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -7,6 +7,8 @@ import { readFileSync, writeFileSync } from 'fs';
|
|
|
7
7
|
import { Storage } from './storage.js';
|
|
8
8
|
import { Notifier } from './notifier.js';
|
|
9
9
|
import { Checker } from './checker.js';
|
|
10
|
+
import { generateVersionsJson } from './versions-generator.js';
|
|
11
|
+
import { CliPublisher } from './cli-publisher.js';
|
|
10
12
|
const BOT_TOKEN = process.env.TELEGRAM_BOT_TOKEN;
|
|
11
13
|
const CHAT_ID = process.env.TELEGRAM_CHAT_ID;
|
|
12
14
|
const CONFIG_PATH = './config/tools.json';
|
|
@@ -14,13 +16,24 @@ if (!BOT_TOKEN || !CHAT_ID) {
|
|
|
14
16
|
console.error('Missing TELEGRAM_BOT_TOKEN or TELEGRAM_CHAT_ID environment variables');
|
|
15
17
|
process.exit(1);
|
|
16
18
|
}
|
|
19
|
+
// Type-safe constants after validation
|
|
20
|
+
const validatedChatId = CHAT_ID;
|
|
17
21
|
// Load config
|
|
18
22
|
let configData = JSON.parse(readFileSync(CONFIG_PATH, 'utf-8'));
|
|
23
|
+
const DOWNLOADS_PATH = './config/downloads.json';
|
|
24
|
+
let downloadsConfig = {};
|
|
25
|
+
try {
|
|
26
|
+
downloadsConfig = JSON.parse(readFileSync(DOWNLOADS_PATH, 'utf-8'));
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
console.log('No downloads.json found, CLI generation disabled');
|
|
30
|
+
}
|
|
19
31
|
// Initialize components
|
|
20
32
|
const bot = new TelegramBot(BOT_TOKEN, { polling: true });
|
|
21
33
|
const storage = new Storage('./data/versions.json');
|
|
22
|
-
const notifier = new Notifier(bot,
|
|
34
|
+
const notifier = new Notifier(bot, validatedChatId);
|
|
23
35
|
const checker = new Checker(configData.tools, storage, notifier);
|
|
36
|
+
const cliPublisher = new CliPublisher(downloadsConfig);
|
|
24
37
|
// Track scheduled task for rescheduling
|
|
25
38
|
let scheduledTask = null;
|
|
26
39
|
let lastCheckTime = null;
|
|
@@ -44,23 +57,42 @@ function scheduleChecks(intervalHours) {
|
|
|
44
57
|
scheduledTask = cron.schedule(cronExpression, async () => {
|
|
45
58
|
console.log(`[${new Date().toISOString()}] Running scheduled check`);
|
|
46
59
|
lastCheckTime = new Date();
|
|
47
|
-
await checker.checkAll();
|
|
60
|
+
const result = await checker.checkAll();
|
|
48
61
|
nextCheckTime = calculateNextCheckTime(intervalHours);
|
|
62
|
+
// Auto-publish CLI if updates were detected
|
|
63
|
+
if (result.hasUpdates && cliPublisher.isConfigured()) {
|
|
64
|
+
const state = storage.load();
|
|
65
|
+
const publishResult = await cliPublisher.publish(state.versions);
|
|
66
|
+
if (publishResult.success) {
|
|
67
|
+
await bot.sendMessage(validatedChatId, `📦 CLI published: v${publishResult.version}`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
49
70
|
});
|
|
50
71
|
console.log(`Scheduled checks every ${intervalHours} hours`);
|
|
51
72
|
}
|
|
52
73
|
// Bot commands
|
|
53
74
|
bot.onText(/\/check/, async (msg) => {
|
|
54
|
-
if (msg.chat.id.toString() !==
|
|
75
|
+
if (msg.chat.id.toString() !== validatedChatId)
|
|
55
76
|
return;
|
|
56
|
-
await bot.sendMessage(
|
|
77
|
+
await bot.sendMessage(validatedChatId, 'Checking for updates...');
|
|
57
78
|
lastCheckTime = new Date();
|
|
58
|
-
await checker.checkAll();
|
|
79
|
+
const result = await checker.checkAll();
|
|
59
80
|
nextCheckTime = calculateNextCheckTime(configData.checkIntervalHours);
|
|
60
|
-
|
|
81
|
+
// Auto-publish CLI if updates were detected
|
|
82
|
+
if (result.hasUpdates && cliPublisher.isConfigured()) {
|
|
83
|
+
const state = storage.load();
|
|
84
|
+
const publishResult = await cliPublisher.publish(state.versions);
|
|
85
|
+
if (publishResult.success) {
|
|
86
|
+
await bot.sendMessage(validatedChatId, `📦 CLI published: v${publishResult.version}`);
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
await bot.sendMessage(validatedChatId, `⚠️ CLI publish failed: ${publishResult.error}`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
await bot.sendMessage(validatedChatId, 'Check complete.');
|
|
61
93
|
});
|
|
62
94
|
bot.onText(/\/status/, async (msg) => {
|
|
63
|
-
if (msg.chat.id.toString() !==
|
|
95
|
+
if (msg.chat.id.toString() !== validatedChatId)
|
|
64
96
|
return;
|
|
65
97
|
const state = storage.load();
|
|
66
98
|
const lines = Object.entries(state.versions)
|
|
@@ -89,24 +121,24 @@ bot.onText(/\/status/, async (msg) => {
|
|
|
89
121
|
message += '\nNext check: soon';
|
|
90
122
|
}
|
|
91
123
|
}
|
|
92
|
-
await bot.sendMessage(
|
|
124
|
+
await bot.sendMessage(validatedChatId, message);
|
|
93
125
|
});
|
|
94
126
|
bot.onText(/\/interval$/, async (msg) => {
|
|
95
|
-
if (msg.chat.id.toString() !==
|
|
127
|
+
if (msg.chat.id.toString() !== validatedChatId)
|
|
96
128
|
return;
|
|
97
|
-
await bot.sendMessage(
|
|
129
|
+
await bot.sendMessage(validatedChatId, `Check interval: every ${configData.checkIntervalHours} hours`);
|
|
98
130
|
});
|
|
99
131
|
bot.onText(/\/setinterval(?:\s+(\d+))?/, async (msg, match) => {
|
|
100
|
-
if (msg.chat.id.toString() !==
|
|
132
|
+
if (msg.chat.id.toString() !== validatedChatId)
|
|
101
133
|
return;
|
|
102
134
|
const hoursStr = match?.[1];
|
|
103
135
|
if (!hoursStr) {
|
|
104
|
-
await bot.sendMessage(
|
|
136
|
+
await bot.sendMessage(validatedChatId, 'Usage: /setinterval <hours>\nExample: /setinterval 12');
|
|
105
137
|
return;
|
|
106
138
|
}
|
|
107
139
|
const hours = parseInt(hoursStr, 10);
|
|
108
140
|
if (hours < 1 || hours > 24) {
|
|
109
|
-
await bot.sendMessage(
|
|
141
|
+
await bot.sendMessage(validatedChatId, 'Interval must be between 1 and 24 hours');
|
|
110
142
|
return;
|
|
111
143
|
}
|
|
112
144
|
// Update config
|
|
@@ -114,7 +146,37 @@ bot.onText(/\/setinterval(?:\s+(\d+))?/, async (msg, match) => {
|
|
|
114
146
|
writeFileSync(CONFIG_PATH, JSON.stringify(configData, null, 2));
|
|
115
147
|
// Reschedule
|
|
116
148
|
scheduleChecks(hours);
|
|
117
|
-
await bot.sendMessage(
|
|
149
|
+
await bot.sendMessage(validatedChatId, `Check interval updated to every ${hours} hours`);
|
|
150
|
+
});
|
|
151
|
+
bot.onText(/\/generate/, async (msg) => {
|
|
152
|
+
if (msg.chat.id.toString() !== validatedChatId)
|
|
153
|
+
return;
|
|
154
|
+
if (Object.keys(downloadsConfig).length === 0) {
|
|
155
|
+
await bot.sendMessage(validatedChatId, 'No downloads.json configured.');
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
const state = storage.load();
|
|
159
|
+
const versionsJson = generateVersionsJson(state.versions, downloadsConfig);
|
|
160
|
+
const outputPath = './data/cli-versions.json';
|
|
161
|
+
writeFileSync(outputPath, JSON.stringify(versionsJson, null, 2));
|
|
162
|
+
await bot.sendMessage(CHAT_ID, `Generated versions.json with ${versionsJson.tools.length} tools.\nPath: ${outputPath}`);
|
|
163
|
+
});
|
|
164
|
+
bot.onText(/\/publishcli/, async (msg) => {
|
|
165
|
+
if (msg.chat.id.toString() !== validatedChatId)
|
|
166
|
+
return;
|
|
167
|
+
if (!cliPublisher.isConfigured()) {
|
|
168
|
+
await bot.sendMessage(validatedChatId, 'CLI publisher not configured. Check downloads.json and cli/ directory.');
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
await bot.sendMessage(validatedChatId, 'Publishing CLI...');
|
|
172
|
+
const state = storage.load();
|
|
173
|
+
const result = await cliPublisher.publish(state.versions);
|
|
174
|
+
if (result.success) {
|
|
175
|
+
await bot.sendMessage(validatedChatId, `✅ CLI published: v${result.version}`);
|
|
176
|
+
}
|
|
177
|
+
else {
|
|
178
|
+
await bot.sendMessage(validatedChatId, `❌ CLI publish failed: ${result.error}`);
|
|
179
|
+
}
|
|
118
180
|
});
|
|
119
181
|
// Start scheduled checks
|
|
120
182
|
scheduleChecks(configData.checkIntervalHours);
|
package/dist/types.d.ts
CHANGED
|
@@ -12,3 +12,23 @@ export interface Config {
|
|
|
12
12
|
checkIntervalHours: number;
|
|
13
13
|
tools: ToolConfig[];
|
|
14
14
|
}
|
|
15
|
+
export interface DownloadConfig {
|
|
16
|
+
displayName: string;
|
|
17
|
+
downloadUrl: string;
|
|
18
|
+
filename: string;
|
|
19
|
+
}
|
|
20
|
+
export interface DownloadsConfig {
|
|
21
|
+
[toolName: string]: DownloadConfig;
|
|
22
|
+
}
|
|
23
|
+
export interface VersionsJsonTool {
|
|
24
|
+
name: string;
|
|
25
|
+
displayName: string;
|
|
26
|
+
version: string;
|
|
27
|
+
publishedAt: string;
|
|
28
|
+
downloadUrl: string;
|
|
29
|
+
filename: string;
|
|
30
|
+
}
|
|
31
|
+
export interface VersionsJson {
|
|
32
|
+
generatedAt: string;
|
|
33
|
+
tools: VersionsJsonTool[];
|
|
34
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export function generateVersionsJson(versions, downloads) {
|
|
2
|
+
const tools = [];
|
|
3
|
+
for (const [toolName, version] of Object.entries(versions)) {
|
|
4
|
+
const downloadConfig = downloads[toolName];
|
|
5
|
+
if (!downloadConfig)
|
|
6
|
+
continue;
|
|
7
|
+
const downloadUrl = '{{NEXUS_URL}}/' +
|
|
8
|
+
downloadConfig.downloadUrl.replace(/\{\{VERSION\}\}/g, version);
|
|
9
|
+
const filename = downloadConfig.filename.replace(/\{\{VERSION\}\}/g, version);
|
|
10
|
+
tools.push({
|
|
11
|
+
name: toolName,
|
|
12
|
+
displayName: downloadConfig.displayName,
|
|
13
|
+
version,
|
|
14
|
+
publishedAt: new Date().toISOString(),
|
|
15
|
+
downloadUrl,
|
|
16
|
+
filename,
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
return {
|
|
20
|
+
generatedAt: new Date().toISOString(),
|
|
21
|
+
tools,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { generateVersionsJson } from './versions-generator.js';
|
|
3
|
+
describe('generateVersionsJson', () => {
|
|
4
|
+
it('merges version data with download config', () => {
|
|
5
|
+
const versions = {
|
|
6
|
+
'Ninja': '1.12.0',
|
|
7
|
+
'CMake': '3.28.0',
|
|
8
|
+
};
|
|
9
|
+
const downloads = {
|
|
10
|
+
'Ninja': {
|
|
11
|
+
displayName: 'Ninja Build',
|
|
12
|
+
downloadUrl: 'github.com/ninja-build/ninja/releases/download/v{{VERSION}}/ninja-linux.zip',
|
|
13
|
+
filename: 'ninja-{{VERSION}}-linux.zip',
|
|
14
|
+
},
|
|
15
|
+
'CMake': {
|
|
16
|
+
displayName: 'CMake',
|
|
17
|
+
downloadUrl: 'github.com/Kitware/CMake/releases/download/v{{VERSION}}/cmake-{{VERSION}}.tar.gz',
|
|
18
|
+
filename: 'cmake-{{VERSION}}.tar.gz',
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
|
+
const result = generateVersionsJson(versions, downloads);
|
|
22
|
+
expect(result.tools).toHaveLength(2);
|
|
23
|
+
expect(result.generatedAt).toBeDefined();
|
|
24
|
+
const ninja = result.tools.find(t => t.name === 'Ninja');
|
|
25
|
+
expect(ninja).toBeDefined();
|
|
26
|
+
expect(ninja.displayName).toBe('Ninja Build');
|
|
27
|
+
expect(ninja.version).toBe('1.12.0');
|
|
28
|
+
expect(ninja.downloadUrl).toBe('{{NEXUS_URL}}/github.com/ninja-build/ninja/releases/download/v1.12.0/ninja-linux.zip');
|
|
29
|
+
expect(ninja.filename).toBe('ninja-1.12.0-linux.zip');
|
|
30
|
+
});
|
|
31
|
+
it('only includes tools that have download config', () => {
|
|
32
|
+
const versions = {
|
|
33
|
+
'Ninja': '1.12.0',
|
|
34
|
+
'UnknownTool': '1.0.0',
|
|
35
|
+
};
|
|
36
|
+
const downloads = {
|
|
37
|
+
'Ninja': {
|
|
38
|
+
displayName: 'Ninja',
|
|
39
|
+
downloadUrl: 'github.com/ninja/releases/{{VERSION}}/ninja.zip',
|
|
40
|
+
filename: 'ninja-{{VERSION}}.zip',
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
const result = generateVersionsJson(versions, downloads);
|
|
44
|
+
expect(result.tools).toHaveLength(1);
|
|
45
|
+
expect(result.tools[0].name).toBe('Ninja');
|
|
46
|
+
});
|
|
47
|
+
});
|