@lvnt/release-radar 1.0.0 → 1.1.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/.env.example CHANGED
@@ -1,2 +1,4 @@
1
1
  TELEGRAM_BOT_TOKEN=your_bot_token_here
2
2
  TELEGRAM_CHAT_ID=your_chat_id_here
3
+ GITHUB_WEBHOOK_SECRET=your_webhook_secret_here
4
+ UPDATER_PORT=9000
package/README.md CHANGED
@@ -45,6 +45,36 @@ Built for environments with limited internet access (e.g., intranet) where manua
45
45
 
46
46
  ## Installation
47
47
 
48
+ ### From npm (recommended)
49
+
50
+ ```bash
51
+ # Install globally
52
+ npm install -g @lvnt/release-radar
53
+
54
+ # Create a directory for config and data
55
+ mkdir ~/release-radar && cd ~/release-radar
56
+
57
+ # First run creates config files (.env and config/tools.json)
58
+ release-radar
59
+
60
+ # Edit .env with your Telegram credentials
61
+ nano .env
62
+
63
+ # Run again
64
+ release-radar
65
+ ```
66
+
67
+ ### With pm2 (recommended for production)
68
+
69
+ ```bash
70
+ cd ~/release-radar
71
+ pm2 start release-radar --name release-radar
72
+ pm2 save
73
+ pm2 startup # Enable auto-start on boot
74
+ ```
75
+
76
+ ### From source (for development)
77
+
48
78
  ```bash
49
79
  # Clone the repository
50
80
  git clone https://github.com/lvntbkdmr/release-radar.git
@@ -57,8 +87,9 @@ npm install
57
87
  cp .env.example .env
58
88
  # Edit .env with your Telegram credentials
59
89
 
60
- # Build
90
+ # Build and run
61
91
  npm run build
92
+ npm start
62
93
  ```
63
94
 
64
95
  ## Configuration
@@ -100,24 +131,6 @@ Edit `config/tools.json` to add/remove tools:
100
131
 
101
132
  ## Usage
102
133
 
103
- ### Development
104
- ```bash
105
- npm run dev
106
- ```
107
-
108
- ### Production
109
- ```bash
110
- npm run build
111
- npm start
112
- ```
113
-
114
- ### With pm2 (recommended)
115
- ```bash
116
- npm install -g pm2
117
- pm2 start npm --name "release-radar" -- start
118
- pm2 save
119
- ```
120
-
121
134
  ### Telegram Commands
122
135
 
123
136
  | Command | Description |
@@ -127,6 +140,33 @@ pm2 save
127
140
  | `/interval` | Show current check interval |
128
141
  | `/setinterval <hours>` | Set check interval (1-24 hours) |
129
142
 
143
+ ## Auto-Updater (Optional)
144
+
145
+ ReleaseRadar includes an optional auto-updater that receives GitHub webhooks and automatically updates itself when you publish a new version.
146
+
147
+ ### Setup
148
+
149
+ 1. Add to your `.env`:
150
+ ```env
151
+ GITHUB_WEBHOOK_SECRET=your_secret_here
152
+ UPDATER_PORT=9000
153
+ ```
154
+
155
+ 2. Configure GitHub webhook:
156
+ - Go to your repo's Settings → Webhooks → Add webhook
157
+ - Payload URL: `https://your-domain.com/webhook`
158
+ - Content type: `application/json`
159
+ - Secret: same as `GITHUB_WEBHOOK_SECRET`
160
+ - Events: Select "Releases" only
161
+
162
+ 3. Start the updater with pm2:
163
+ ```bash
164
+ pm2 start release-radar-updater --name release-radar-updater
165
+ pm2 save
166
+ ```
167
+
168
+ When you publish a new release, the updater will automatically run `npm update -g @lvnt/release-radar` and restart the main service.
169
+
130
170
  ## Project Structure
131
171
 
132
172
  ```
@@ -0,0 +1,41 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { fileURLToPath } from 'url';
4
+ import { dirname, join } from 'path';
5
+ import { existsSync } from 'fs';
6
+ import { config } from 'dotenv';
7
+ import TelegramBot from 'node-telegram-bot-api';
8
+ import { startUpdater } from '../dist/updater.js';
9
+
10
+ const __filename = fileURLToPath(import.meta.url);
11
+ const __dirname = dirname(__filename);
12
+
13
+ // Load .env from current working directory
14
+ const envPath = join(process.cwd(), '.env');
15
+ if (existsSync(envPath)) {
16
+ config({ path: envPath });
17
+ }
18
+
19
+ const BOT_TOKEN = process.env.TELEGRAM_BOT_TOKEN;
20
+ const CHAT_ID = process.env.TELEGRAM_CHAT_ID;
21
+ const WEBHOOK_SECRET = process.env.GITHUB_WEBHOOK_SECRET;
22
+ const PORT = parseInt(process.env.UPDATER_PORT || '9000', 10);
23
+
24
+ if (!BOT_TOKEN || !CHAT_ID) {
25
+ console.error('Missing TELEGRAM_BOT_TOKEN or TELEGRAM_CHAT_ID in .env');
26
+ process.exit(1);
27
+ }
28
+
29
+ if (!WEBHOOK_SECRET) {
30
+ console.error('Missing GITHUB_WEBHOOK_SECRET in .env');
31
+ process.exit(1);
32
+ }
33
+
34
+ const bot = new TelegramBot(BOT_TOKEN);
35
+
36
+ startUpdater({
37
+ port: PORT,
38
+ webhookSecret: WEBHOOK_SECRET,
39
+ telegramBot: bot,
40
+ chatId: CHAT_ID
41
+ });
@@ -0,0 +1,21 @@
1
+ import TelegramBot from 'node-telegram-bot-api';
2
+ export interface GitHubReleasePayload {
3
+ action: string;
4
+ release?: {
5
+ tag_name: string;
6
+ };
7
+ }
8
+ export declare function parseReleaseEvent(payload: GitHubReleasePayload): string | null;
9
+ export declare function verifySignature(payload: string, signature: string, secret: string): boolean;
10
+ export interface UpdateResult {
11
+ success: boolean;
12
+ error?: string;
13
+ }
14
+ export declare function executeUpdate(): Promise<UpdateResult>;
15
+ export interface UpdaterConfig {
16
+ port: number;
17
+ webhookSecret: string;
18
+ telegramBot: TelegramBot;
19
+ chatId: string;
20
+ }
21
+ export declare function startUpdater(config: UpdaterConfig): void;
@@ -0,0 +1,101 @@
1
+ import { createHmac, timingSafeEqual } from 'crypto';
2
+ import { spawn } from 'child_process';
3
+ import { createServer } from 'http';
4
+ export function parseReleaseEvent(payload) {
5
+ if (payload.action !== 'published' || !payload.release?.tag_name) {
6
+ return null;
7
+ }
8
+ const tag = payload.release.tag_name;
9
+ return tag.startsWith('v') ? tag.slice(1) : tag;
10
+ }
11
+ export function verifySignature(payload, signature, secret) {
12
+ if (!signature || !signature.startsWith('sha256=')) {
13
+ return false;
14
+ }
15
+ const expected = 'sha256=' + createHmac('sha256', secret)
16
+ .update(payload)
17
+ .digest('hex');
18
+ try {
19
+ return timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
20
+ }
21
+ catch {
22
+ return false;
23
+ }
24
+ }
25
+ export function executeUpdate() {
26
+ return new Promise((resolve) => {
27
+ const npmProcess = spawn('npm', ['update', '-g', '@lvnt/release-radar'], {
28
+ stdio: 'inherit'
29
+ });
30
+ npmProcess.on('close', (code) => {
31
+ if (code !== 0) {
32
+ resolve({ success: false, error: `npm update failed with code ${code}` });
33
+ return;
34
+ }
35
+ const pm2Process = spawn('pm2', ['restart', 'release-radar'], {
36
+ stdio: 'inherit'
37
+ });
38
+ pm2Process.on('close', (pm2Code) => {
39
+ if (pm2Code !== 0) {
40
+ resolve({ success: false, error: `pm2 restart failed with code ${pm2Code}` });
41
+ }
42
+ else {
43
+ resolve({ success: true });
44
+ }
45
+ });
46
+ });
47
+ });
48
+ }
49
+ export function startUpdater(config) {
50
+ const server = createServer(async (req, res) => {
51
+ // Only accept POST /webhook
52
+ if (req.method !== 'POST' || req.url !== '/webhook') {
53
+ res.writeHead(404);
54
+ res.end('Not Found');
55
+ return;
56
+ }
57
+ // Read body
58
+ let body = '';
59
+ for await (const chunk of req) {
60
+ body += chunk;
61
+ }
62
+ // Verify signature
63
+ const signature = req.headers['x-hub-signature-256'];
64
+ if (!verifySignature(body, signature, config.webhookSecret)) {
65
+ res.writeHead(401);
66
+ res.end('Invalid signature');
67
+ return;
68
+ }
69
+ // Parse event
70
+ let payload;
71
+ try {
72
+ payload = JSON.parse(body);
73
+ }
74
+ catch {
75
+ res.writeHead(400);
76
+ res.end('Invalid JSON');
77
+ return;
78
+ }
79
+ const version = parseReleaseEvent(payload);
80
+ if (!version) {
81
+ // Not a release.published event, acknowledge but don't update
82
+ res.writeHead(200);
83
+ res.end('OK (ignored)');
84
+ return;
85
+ }
86
+ // Respond immediately, update async
87
+ res.writeHead(200);
88
+ res.end('OK');
89
+ console.log(`Received release event for v${version}, updating...`);
90
+ const result = await executeUpdate();
91
+ if (result.success) {
92
+ await config.telegramBot.sendMessage(config.chatId, `🔄 Updated release-radar to v${version}`);
93
+ }
94
+ else {
95
+ await config.telegramBot.sendMessage(config.chatId, `⚠️ Failed to update release-radar: ${result.error}`);
96
+ }
97
+ });
98
+ server.listen(config.port, () => {
99
+ console.log(`Updater webhook server listening on port ${config.port}`);
100
+ });
101
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,104 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+ import { verifySignature, parseReleaseEvent, executeUpdate } from './updater.js';
3
+ import { spawn } from 'child_process';
4
+ vi.mock('child_process', () => ({
5
+ spawn: vi.fn()
6
+ }));
7
+ describe('verifySignature', () => {
8
+ const secret = 'test-secret';
9
+ const payload = '{"action":"published"}';
10
+ it('returns true for valid signature', () => {
11
+ // SHA256 HMAC of payload with secret
12
+ const validSig = 'sha256=0d408f71f2420c71a03fe2dd4aa32e00a84408ab43f239d7a41b4ab658e1b064';
13
+ expect(verifySignature(payload, validSig, secret)).toBe(true);
14
+ });
15
+ it('returns false for invalid signature', () => {
16
+ expect(verifySignature(payload, 'sha256=invalid', secret)).toBe(false);
17
+ });
18
+ it('returns false for missing signature', () => {
19
+ expect(verifySignature(payload, '', secret)).toBe(false);
20
+ });
21
+ });
22
+ describe('parseReleaseEvent', () => {
23
+ it('returns version for release.published event', () => {
24
+ const payload = {
25
+ action: 'published',
26
+ release: {
27
+ tag_name: 'v1.2.0'
28
+ }
29
+ };
30
+ expect(parseReleaseEvent(payload)).toBe('1.2.0');
31
+ });
32
+ it('returns null for non-published action', () => {
33
+ const payload = {
34
+ action: 'created',
35
+ release: {
36
+ tag_name: 'v1.2.0'
37
+ }
38
+ };
39
+ expect(parseReleaseEvent(payload)).toBeNull();
40
+ });
41
+ it('returns null for missing release', () => {
42
+ const payload = { action: 'published' };
43
+ expect(parseReleaseEvent(payload)).toBeNull();
44
+ });
45
+ it('strips v prefix from tag', () => {
46
+ const payload = {
47
+ action: 'published',
48
+ release: { tag_name: 'v2.0.0' }
49
+ };
50
+ expect(parseReleaseEvent(payload)).toBe('2.0.0');
51
+ });
52
+ it('handles tag without v prefix', () => {
53
+ const payload = {
54
+ action: 'published',
55
+ release: { tag_name: '2.0.0' }
56
+ };
57
+ expect(parseReleaseEvent(payload)).toBe('2.0.0');
58
+ });
59
+ });
60
+ describe('executeUpdate', () => {
61
+ beforeEach(() => {
62
+ vi.clearAllMocks();
63
+ });
64
+ it('runs npm update then pm2 restart on success', async () => {
65
+ const mockSpawn = vi.mocked(spawn);
66
+ // Mock npm update success
67
+ const npmProcess = {
68
+ on: vi.fn((event, cb) => {
69
+ if (event === 'close')
70
+ cb(0);
71
+ return npmProcess;
72
+ })
73
+ };
74
+ // Mock pm2 restart success
75
+ const pm2Process = {
76
+ on: vi.fn((event, cb) => {
77
+ if (event === 'close')
78
+ cb(0);
79
+ return pm2Process;
80
+ })
81
+ };
82
+ mockSpawn
83
+ .mockReturnValueOnce(npmProcess)
84
+ .mockReturnValueOnce(pm2Process);
85
+ const result = await executeUpdate();
86
+ expect(result.success).toBe(true);
87
+ expect(mockSpawn).toHaveBeenCalledWith('npm', ['update', '-g', '@lvnt/release-radar'], expect.any(Object));
88
+ expect(mockSpawn).toHaveBeenCalledWith('pm2', ['restart', 'release-radar'], expect.any(Object));
89
+ });
90
+ it('returns failure if npm update fails', async () => {
91
+ const mockSpawn = vi.mocked(spawn);
92
+ const npmProcess = {
93
+ on: vi.fn((event, cb) => {
94
+ if (event === 'close')
95
+ cb(1);
96
+ return npmProcess;
97
+ })
98
+ };
99
+ mockSpawn.mockReturnValueOnce(npmProcess);
100
+ const result = await executeUpdate();
101
+ expect(result.success).toBe(false);
102
+ expect(result.error).toBe('npm update failed with code 1');
103
+ });
104
+ });
package/package.json CHANGED
@@ -1,10 +1,11 @@
1
1
  {
2
2
  "name": "@lvnt/release-radar",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "Monitor tool versions and notify via Telegram when updates are detected",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
7
- "release-radar": "./bin/release-radar.js"
7
+ "release-radar": "bin/release-radar.js",
8
+ "release-radar-updater": "bin/release-radar-updater.js"
8
9
  },
9
10
  "type": "module",
10
11
  "scripts": {