plum-e2e 2.1.0 → 2.3.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/README.md +61 -470
- package/backend/lib/runnerProcess.js +50 -4
- package/backend/logs/runner-cmqneqerz0000qq01i5ap2rvl.log +22 -0
- package/backend/logs/runner-cmqnfv7kr0000r101aeocm8eu.log +20 -0
- package/backend/logs/runner-cmqnfvb560001r101qoi0phau.log +43 -0
- package/backend/logs/runner-cmqnfvlm20002r101gsyqb837.log +20 -0
- package/backend/logs/runner-cmqnfvqfy0003r101fh41pzx3.log +20 -0
- package/backend/logs/runner-cmqnfvvwo0004r101q4dtqxd2.log +20 -0
- package/backend/package.json +1 -0
- package/backend/prisma/migrations/20260621000000_add_notifications/migration.sql +8 -0
- package/backend/prisma/migrations/20260621000001_add_backup_config/migration.sql +11 -0
- package/backend/prisma/schema.prisma +22 -7
- package/backend/routes/backup.routes.js +70 -5
- package/backend/routes/node.routes.js +9 -0
- package/backend/routes/runners.routes.js +10 -0
- package/backend/routes/settings.routes.js +27 -0
- package/backend/scripts/manage-runners.mjs +49 -8
- package/backend/server.js +25 -1
- package/backend/services/backupCronService.js +82 -0
- package/backend/services/backupService.js +254 -27
- package/backend/services/cronService.js +91 -7
- package/backend/services/notificationService.js +163 -0
- package/backend/services/settingsService.js +74 -1
- package/backend/websockets/socketHandler.js +82 -6
- package/frontend/src/lib/api/schedules.js +5 -1
- package/frontend/src/lib/api/settings.js +56 -0
- package/frontend/src/lib/components/layout/RunnerPanel.svelte +79 -3
- package/frontend/src/lib/stores/runner.js +4 -2
- package/frontend/src/routes/scheduled-tests/+page.svelte +65 -7
- package/frontend/src/routes/settings/+page.svelte +472 -9
- package/package.json +1 -1
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* This file is part of Plum.
|
|
3
|
+
*
|
|
4
|
+
* Plum is free software: you can redistribute it and/or modify
|
|
5
|
+
* it under the terms of the GNU General Public License as published by
|
|
6
|
+
* the Free Software Foundation, either version 3 of the License, or
|
|
7
|
+
* (at your option) any later version.
|
|
8
|
+
*
|
|
9
|
+
* Plum is distributed in the hope that it will be useful,
|
|
10
|
+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
11
|
+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
12
|
+
* GNU General Public License for more details.
|
|
13
|
+
*
|
|
14
|
+
* You should have received a copy of the GNU General Public License
|
|
15
|
+
* along with Plum. If not, see https://www.gnu.org/licenses/.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
const cron = require('node-cron');
|
|
19
|
+
const prisma = require('./prisma');
|
|
20
|
+
const backupService = require('./backupService');
|
|
21
|
+
|
|
22
|
+
let scheduledJob = null;
|
|
23
|
+
|
|
24
|
+
const runBackup = async () => {
|
|
25
|
+
let project;
|
|
26
|
+
try {
|
|
27
|
+
project = await prisma.project.findUnique({ where: { id: 1 } });
|
|
28
|
+
} catch (err) {
|
|
29
|
+
console.error('❌ Backup: could not read project config:', err.message);
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (!project?.backupEnabled) return;
|
|
34
|
+
|
|
35
|
+
try {
|
|
36
|
+
const data = await backupService.exportAll();
|
|
37
|
+
const key = await backupService.uploadToS3(data, project);
|
|
38
|
+
|
|
39
|
+
await prisma.project.update({
|
|
40
|
+
where: { id: 1 },
|
|
41
|
+
data: {
|
|
42
|
+
backupLastRunAt: new Date(),
|
|
43
|
+
backupLastStatus: `success:${key}`
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
console.log(`✅ Backup uploaded: ${key}`);
|
|
47
|
+
} catch (err) {
|
|
48
|
+
console.error('❌ Backup failed:', err.message);
|
|
49
|
+
try {
|
|
50
|
+
await prisma.project.update({
|
|
51
|
+
where: { id: 1 },
|
|
52
|
+
data: {
|
|
53
|
+
backupLastRunAt: new Date(),
|
|
54
|
+
backupLastStatus: `error:${err.message}`
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
} catch {}
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const schedule = (cronExpr, enabled) => {
|
|
62
|
+
if (scheduledJob) {
|
|
63
|
+
scheduledJob.stop();
|
|
64
|
+
scheduledJob = null;
|
|
65
|
+
}
|
|
66
|
+
if (!enabled || !cronExpr || !cron.validate(cronExpr)) return;
|
|
67
|
+
scheduledJob = cron.schedule(cronExpr, runBackup);
|
|
68
|
+
console.log(`⏰ Backup scheduled: ${cronExpr}`);
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const init = async () => {
|
|
72
|
+
try {
|
|
73
|
+
const project = await prisma.project.findUnique({ where: { id: 1 } });
|
|
74
|
+
schedule(project?.backupCron, project?.backupEnabled);
|
|
75
|
+
} catch (err) {
|
|
76
|
+
console.error('Failed to initialize backup cron:', err.message);
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const reload = init;
|
|
81
|
+
|
|
82
|
+
module.exports = { init, reload, runBackup };
|
|
@@ -17,45 +17,272 @@
|
|
|
17
17
|
|
|
18
18
|
const prisma = require('./prisma');
|
|
19
19
|
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
// Export — all data except reports (reports are too large; use pg_dump instead)
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
|
|
20
24
|
const exportAll = async () => {
|
|
21
|
-
const [cronJobs, project] = await Promise.all([
|
|
25
|
+
const [cronJobs, project, testSuites, testRuns, users, runners] = await Promise.all([
|
|
22
26
|
prisma.cronJob.findMany({ orderBy: { createdAt: 'asc' } }),
|
|
23
|
-
prisma.project.findUnique({ where: { id: 1 } })
|
|
27
|
+
prisma.project.findUnique({ where: { id: 1 } }),
|
|
28
|
+
prisma.testSuite.findMany({
|
|
29
|
+
orderBy: { createdAt: 'asc' },
|
|
30
|
+
include: {
|
|
31
|
+
cases: {
|
|
32
|
+
include: { steps: { orderBy: { order: 'asc' } } },
|
|
33
|
+
orderBy: { createdAt: 'asc' }
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}),
|
|
37
|
+
prisma.testRun.findMany({
|
|
38
|
+
orderBy: { createdAt: 'asc' },
|
|
39
|
+
include: { entries: { orderBy: { order: 'asc' } } }
|
|
40
|
+
}),
|
|
41
|
+
prisma.user.findMany({ orderBy: { createdAt: 'asc' } }),
|
|
42
|
+
prisma.runner.findMany({ orderBy: { createdAt: 'asc' } })
|
|
24
43
|
]);
|
|
25
44
|
|
|
26
45
|
return {
|
|
27
|
-
version: '
|
|
46
|
+
version: '2',
|
|
28
47
|
exportedAt: new Date().toISOString(),
|
|
29
|
-
|
|
30
|
-
|
|
48
|
+
disclaimer:
|
|
49
|
+
'Reports are not included in this backup. Use pg_dump on the PostgreSQL volume to back up report history.',
|
|
50
|
+
cronJobs: cronJobs.map(({ id, createdAt, updatedAt, reports: _, runnerId: __, ...r }) => r),
|
|
51
|
+
project: project
|
|
52
|
+
? {
|
|
53
|
+
name: project.name,
|
|
54
|
+
logoUrl: project.logoUrl,
|
|
55
|
+
testCasePrefix: project.testCasePrefix,
|
|
56
|
+
testSuitePrefix: project.testSuitePrefix,
|
|
57
|
+
discordWebhookUrl: project.discordWebhookUrl,
|
|
58
|
+
slackWebhookUrl: project.slackWebhookUrl,
|
|
59
|
+
notifyPublicUrl: project.notifyPublicUrl
|
|
60
|
+
}
|
|
61
|
+
: null,
|
|
62
|
+
users: users.map(({ updatedAt: _, ...u }) => u),
|
|
63
|
+
runners: runners.map(({ createdAt: _, cronJobs: __, reports: ___, ...r }) => r),
|
|
64
|
+
testSuites: testSuites.map(({ cases, ...suite }) => ({
|
|
65
|
+
...suite,
|
|
66
|
+
cases: cases.map(({ steps, runEntries: _, history: __, ...tc }) => ({
|
|
67
|
+
...tc,
|
|
68
|
+
steps: steps.map(({ createdAt: ___, ...step }) => step)
|
|
69
|
+
}))
|
|
70
|
+
})),
|
|
71
|
+
testRuns: testRuns.map(({ entries, history: _, ...run }) => ({
|
|
72
|
+
...run,
|
|
73
|
+
entries: entries.map(({ executedAt, ...entry }) => ({ ...entry, executedAt }))
|
|
74
|
+
}))
|
|
31
75
|
};
|
|
32
76
|
};
|
|
33
77
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
78
|
+
// ---------------------------------------------------------------------------
|
|
79
|
+
// Import — upsert all exported data, preserve IDs for relational integrity
|
|
80
|
+
// ---------------------------------------------------------------------------
|
|
81
|
+
|
|
82
|
+
const importAll = async (
|
|
83
|
+
{ cronJobs = [], project = null, users = [], runners = [], testSuites = [], testRuns = [] },
|
|
84
|
+
cronService
|
|
85
|
+
) => {
|
|
86
|
+
await prisma.$transaction(
|
|
87
|
+
async (tx) => {
|
|
88
|
+
// 1. Users (needed before suites/runs reference createdById)
|
|
89
|
+
for (const user of users) {
|
|
90
|
+
await tx.user.upsert({
|
|
91
|
+
where: { email: user.email },
|
|
92
|
+
create: user,
|
|
93
|
+
update: { name: user.name, role: user.role }
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// 2. Runners
|
|
98
|
+
for (const runner of runners) {
|
|
99
|
+
await tx.runner.upsert({
|
|
100
|
+
where: { id: runner.id },
|
|
101
|
+
create: runner,
|
|
102
|
+
update: {
|
|
103
|
+
name: runner.name,
|
|
104
|
+
url: runner.url,
|
|
105
|
+
token: runner.token,
|
|
106
|
+
browser: runner.browser
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// 3. CronJobs
|
|
112
|
+
for (const job of cronJobs) {
|
|
113
|
+
await tx.cronJob.upsert({
|
|
114
|
+
where: { taskName: job.taskName },
|
|
115
|
+
create: {
|
|
116
|
+
taskName: job.taskName,
|
|
117
|
+
cronExpression: job.cronExpression,
|
|
118
|
+
tags: job.tags,
|
|
119
|
+
workers: job.workers ?? 1,
|
|
120
|
+
browser: job.browser ?? 'chromium',
|
|
121
|
+
enabled: job.enabled ?? true,
|
|
122
|
+
runnerIds: job.runnerIds ?? 'built-in',
|
|
123
|
+
notifyDiscord: job.notifyDiscord ?? false,
|
|
124
|
+
notifySlack: job.notifySlack ?? false
|
|
125
|
+
},
|
|
126
|
+
update: {
|
|
127
|
+
cronExpression: job.cronExpression,
|
|
128
|
+
tags: job.tags,
|
|
129
|
+
workers: job.workers ?? 1,
|
|
130
|
+
browser: job.browser ?? 'chromium',
|
|
131
|
+
runnerIds: job.runnerIds ?? 'built-in',
|
|
132
|
+
notifyDiscord: job.notifyDiscord ?? false,
|
|
133
|
+
notifySlack: job.notifySlack ?? false
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// 4. Project settings
|
|
139
|
+
if (project) {
|
|
140
|
+
await tx.project.upsert({
|
|
141
|
+
where: { id: 1 },
|
|
142
|
+
create: { id: 1, ...project },
|
|
143
|
+
update: project
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// 5. Test suites + cases + steps
|
|
148
|
+
for (const suite of testSuites) {
|
|
149
|
+
const { cases = [], ...suiteData } = suite;
|
|
150
|
+
await tx.testSuite.upsert({
|
|
151
|
+
where: { displayId: suiteData.displayId },
|
|
152
|
+
create: suiteData,
|
|
153
|
+
update: {
|
|
154
|
+
name: suiteData.name,
|
|
155
|
+
description: suiteData.description,
|
|
156
|
+
priority: suiteData.priority
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
for (const tc of cases) {
|
|
161
|
+
const { steps = [], ...caseData } = tc;
|
|
162
|
+
await tx.testCase.upsert({
|
|
163
|
+
where: { displayId: caseData.displayId },
|
|
164
|
+
create: caseData,
|
|
165
|
+
update: {
|
|
166
|
+
title: caseData.title,
|
|
167
|
+
description: caseData.description,
|
|
168
|
+
priority: caseData.priority,
|
|
169
|
+
isAutomated: caseData.isAutomated
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
// Replace all steps for this case (order may have changed)
|
|
174
|
+
await tx.testStep.deleteMany({ where: { caseId: caseData.id } });
|
|
175
|
+
for (const step of steps) {
|
|
176
|
+
await tx.testStep.create({ data: step });
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// 6. Test runs + entries
|
|
182
|
+
for (const run of testRuns) {
|
|
183
|
+
const { entries = [], ...runData } = run;
|
|
184
|
+
await tx.testRun.upsert({
|
|
185
|
+
where: { id: runData.id },
|
|
186
|
+
create: runData,
|
|
187
|
+
update: { title: runData.title, status: runData.status }
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
for (const entry of entries) {
|
|
191
|
+
await tx.testRunEntry.upsert({
|
|
192
|
+
where: { id: entry.id },
|
|
193
|
+
create: entry,
|
|
194
|
+
update: { status: entry.status, notes: entry.notes, order: entry.order }
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
},
|
|
199
|
+
{ timeout: 30000 }
|
|
200
|
+
);
|
|
201
|
+
|
|
202
|
+
if (cronService) await cronService.reload();
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
// ---------------------------------------------------------------------------
|
|
206
|
+
// S3 upload — S3-compatible object storage (AWS, R2, B2, MinIO)
|
|
207
|
+
// ---------------------------------------------------------------------------
|
|
208
|
+
|
|
209
|
+
const uploadToS3 = async (jsonData, config) => {
|
|
210
|
+
const { S3Client, PutObjectCommand } = await import('@aws-sdk/client-s3');
|
|
211
|
+
|
|
212
|
+
const {
|
|
213
|
+
backupS3Endpoint,
|
|
214
|
+
backupS3Region,
|
|
215
|
+
backupS3Bucket,
|
|
216
|
+
backupS3AccessKey,
|
|
217
|
+
backupS3SecretKey,
|
|
218
|
+
backupS3Prefix
|
|
219
|
+
} = config;
|
|
220
|
+
|
|
221
|
+
const clientConfig = {
|
|
222
|
+
region: backupS3Region || 'auto',
|
|
223
|
+
credentials: {
|
|
224
|
+
accessKeyId: backupS3AccessKey,
|
|
225
|
+
secretAccessKey: backupS3SecretKey
|
|
47
226
|
}
|
|
227
|
+
};
|
|
228
|
+
if (backupS3Endpoint) clientConfig.endpoint = backupS3Endpoint;
|
|
229
|
+
|
|
230
|
+
const client = new S3Client(clientConfig);
|
|
231
|
+
|
|
232
|
+
const date = new Date().toISOString().slice(0, 10);
|
|
233
|
+
const prefix = backupS3Prefix ? backupS3Prefix.replace(/\/?$/, '/') : '';
|
|
234
|
+
const key = `${prefix}plum-backup-${date}.json`;
|
|
235
|
+
|
|
236
|
+
await client.send(
|
|
237
|
+
new PutObjectCommand({
|
|
238
|
+
Bucket: backupS3Bucket,
|
|
239
|
+
Key: key,
|
|
240
|
+
Body: JSON.stringify(jsonData, null, 2),
|
|
241
|
+
ContentType: 'application/json'
|
|
242
|
+
})
|
|
243
|
+
);
|
|
244
|
+
|
|
245
|
+
return key;
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
const testS3Connection = async (config) => {
|
|
249
|
+
const { S3Client, PutObjectCommand, DeleteObjectCommand } = await import('@aws-sdk/client-s3');
|
|
48
250
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
251
|
+
const {
|
|
252
|
+
backupS3Endpoint,
|
|
253
|
+
backupS3Region,
|
|
254
|
+
backupS3Bucket,
|
|
255
|
+
backupS3AccessKey,
|
|
256
|
+
backupS3SecretKey,
|
|
257
|
+
backupS3Prefix
|
|
258
|
+
} = config;
|
|
259
|
+
|
|
260
|
+
const clientConfig = {
|
|
261
|
+
region: backupS3Region || 'auto',
|
|
262
|
+
credentials: {
|
|
263
|
+
accessKeyId: backupS3AccessKey,
|
|
264
|
+
secretAccessKey: backupS3SecretKey
|
|
55
265
|
}
|
|
56
|
-
}
|
|
266
|
+
};
|
|
267
|
+
if (backupS3Endpoint) clientConfig.endpoint = backupS3Endpoint;
|
|
57
268
|
|
|
58
|
-
|
|
269
|
+
const client = new S3Client(clientConfig);
|
|
270
|
+
const prefix = backupS3Prefix ? backupS3Prefix.replace(/\/?$/, '/') : '';
|
|
271
|
+
const key = `${prefix}.plum-connection-test`;
|
|
272
|
+
|
|
273
|
+
await client.send(
|
|
274
|
+
new PutObjectCommand({
|
|
275
|
+
Bucket: backupS3Bucket,
|
|
276
|
+
Key: key,
|
|
277
|
+
Body: 'ok',
|
|
278
|
+
ContentType: 'text/plain'
|
|
279
|
+
})
|
|
280
|
+
);
|
|
281
|
+
|
|
282
|
+
// Clean up the test file
|
|
283
|
+
try {
|
|
284
|
+
await client.send(new DeleteObjectCommand({ Bucket: backupS3Bucket, Key: key }));
|
|
285
|
+
} catch {}
|
|
59
286
|
};
|
|
60
287
|
|
|
61
|
-
module.exports = { exportAll, importAll };
|
|
288
|
+
module.exports = { exportAll, importAll, uploadToS3, testS3Connection };
|
|
@@ -20,6 +20,7 @@ const { spawn } = require('child_process');
|
|
|
20
20
|
const prisma = require('./prisma');
|
|
21
21
|
const runnerService = require('./runnerService');
|
|
22
22
|
const reportService = require('./reportService');
|
|
23
|
+
const notificationService = require('./notificationService');
|
|
23
24
|
const { BUILT_IN_RUNNER_ID, TRIGGER_REMOTE } = require('../constants/triggers');
|
|
24
25
|
const { getTestIdsForTag, chunkTests, buildTagExpression } = require('../lib/testChunker');
|
|
25
26
|
const { readCucumberReportFile } = require('../lib/reportFilename');
|
|
@@ -63,7 +64,7 @@ async function resolveLaneInfos(runnerIds) {
|
|
|
63
64
|
* Single built-in runner — spawns tests locally.
|
|
64
65
|
* TRIGGER is set to taskName so generate-report.js can persist it correctly.
|
|
65
66
|
*/
|
|
66
|
-
function runSingleBuiltIn({ taskName, tags, workers, browser }) {
|
|
67
|
+
function runSingleBuiltIn({ taskName, tags, workers, browser, notifyDiscord, notifySlack }) {
|
|
67
68
|
const env = {
|
|
68
69
|
...process.env,
|
|
69
70
|
TAG: tags,
|
|
@@ -78,6 +79,29 @@ function runSingleBuiltIn({ taskName, tags, workers, browser }) {
|
|
|
78
79
|
task.on('close', (code) => {
|
|
79
80
|
console.log(`Task "${taskName}" finished with code ${code}`);
|
|
80
81
|
if (_io) _io.emit('cron-done', { taskName, code });
|
|
82
|
+
|
|
83
|
+
if (notifyDiscord || notifySlack) {
|
|
84
|
+
prisma.report
|
|
85
|
+
.findFirst({
|
|
86
|
+
where: { triggerType: taskName },
|
|
87
|
+
orderBy: { createdAt: 'desc' },
|
|
88
|
+
select: { id: true, status: true, content: true }
|
|
89
|
+
})
|
|
90
|
+
.then((report) => {
|
|
91
|
+
if (!report) return;
|
|
92
|
+
return notificationService.send({
|
|
93
|
+
jobName: taskName,
|
|
94
|
+
status: report.status,
|
|
95
|
+
content: report.content,
|
|
96
|
+
browser,
|
|
97
|
+
tags,
|
|
98
|
+
reportId: report.id,
|
|
99
|
+
notifyDiscord,
|
|
100
|
+
notifySlack
|
|
101
|
+
});
|
|
102
|
+
})
|
|
103
|
+
.catch((e) => console.error(`[cron] Notification failed: ${e.message}`));
|
|
104
|
+
}
|
|
81
105
|
});
|
|
82
106
|
}
|
|
83
107
|
|
|
@@ -85,7 +109,15 @@ function runSingleBuiltIn({ taskName, tags, workers, browser }) {
|
|
|
85
109
|
* Multi-runner distributed path — splits tests across nodes and combines reports.
|
|
86
110
|
* triggerType = taskName so the combined report is correctly attributed.
|
|
87
111
|
*/
|
|
88
|
-
async function runDistributed({
|
|
112
|
+
async function runDistributed({
|
|
113
|
+
taskName,
|
|
114
|
+
tags,
|
|
115
|
+
workers,
|
|
116
|
+
browser,
|
|
117
|
+
runnerIds,
|
|
118
|
+
notifyDiscord,
|
|
119
|
+
notifySlack
|
|
120
|
+
}) {
|
|
89
121
|
const allIds = getTestIdsForTag(tags);
|
|
90
122
|
const chunks = chunkTests(allIds, runnerIds.length);
|
|
91
123
|
|
|
@@ -123,6 +155,20 @@ async function runDistributed({ taskName, tags, workers, browser, runnerIds }) {
|
|
|
123
155
|
triggerType: taskName,
|
|
124
156
|
browser
|
|
125
157
|
})
|
|
158
|
+
.then((saved) => {
|
|
159
|
+
if (saved && (notifyDiscord || notifySlack)) {
|
|
160
|
+
return notificationService.send({
|
|
161
|
+
jobName: taskName,
|
|
162
|
+
status: saved.status,
|
|
163
|
+
content: saved.content,
|
|
164
|
+
browser,
|
|
165
|
+
tags,
|
|
166
|
+
reportId: saved.id,
|
|
167
|
+
notifyDiscord,
|
|
168
|
+
notifySlack
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
})
|
|
126
172
|
.catch((e) => console.error(`[cron] Failed to save combined report: ${e.message}`));
|
|
127
173
|
}
|
|
128
174
|
}
|
|
@@ -163,7 +209,15 @@ async function runDistributed({ taskName, tags, workers, browser, runnerIds }) {
|
|
|
163
209
|
// ---------------------------------------------------------------------------
|
|
164
210
|
|
|
165
211
|
async function runCronJob(job) {
|
|
166
|
-
const {
|
|
212
|
+
const {
|
|
213
|
+
taskName,
|
|
214
|
+
tags,
|
|
215
|
+
workers,
|
|
216
|
+
browser,
|
|
217
|
+
runnerIds: runnerIdsStr,
|
|
218
|
+
notifyDiscord,
|
|
219
|
+
notifySlack
|
|
220
|
+
} = job;
|
|
167
221
|
const runnerIds = parseRunnerIds(runnerIdsStr);
|
|
168
222
|
|
|
169
223
|
if (_io) _io.emit('cron-start', { taskName });
|
|
@@ -172,9 +226,17 @@ async function runCronJob(job) {
|
|
|
172
226
|
const isSingleBuiltIn = runnerIds.length === 1 && runnerIds[0] === BUILT_IN_RUNNER_ID;
|
|
173
227
|
|
|
174
228
|
if (isSingleBuiltIn) {
|
|
175
|
-
runSingleBuiltIn({ taskName, tags, workers, browser });
|
|
229
|
+
runSingleBuiltIn({ taskName, tags, workers, browser, notifyDiscord, notifySlack });
|
|
176
230
|
} else {
|
|
177
|
-
await runDistributed({
|
|
231
|
+
await runDistributed({
|
|
232
|
+
taskName,
|
|
233
|
+
tags,
|
|
234
|
+
workers,
|
|
235
|
+
browser,
|
|
236
|
+
runnerIds,
|
|
237
|
+
notifyDiscord,
|
|
238
|
+
notifySlack
|
|
239
|
+
});
|
|
178
240
|
}
|
|
179
241
|
}
|
|
180
242
|
|
|
@@ -212,7 +274,16 @@ const reload = async () => {
|
|
|
212
274
|
|
|
213
275
|
const getAllCronJobs = () => prisma.cronJob.findMany({ orderBy: { createdAt: 'asc' } });
|
|
214
276
|
|
|
215
|
-
const addCronJob = async ({
|
|
277
|
+
const addCronJob = async ({
|
|
278
|
+
taskName,
|
|
279
|
+
cronExpression,
|
|
280
|
+
tags,
|
|
281
|
+
workers,
|
|
282
|
+
browser,
|
|
283
|
+
runnerIds,
|
|
284
|
+
notifyDiscord,
|
|
285
|
+
notifySlack
|
|
286
|
+
}) => {
|
|
216
287
|
if (!cronExpression || !taskName || !tags) {
|
|
217
288
|
return { status: 400, message: 'Missing required parameters' };
|
|
218
289
|
}
|
|
@@ -227,6 +298,8 @@ const addCronJob = async ({ taskName, cronExpression, tags, workers, browser, ru
|
|
|
227
298
|
workers: workers ?? 1,
|
|
228
299
|
browser: browser ?? 'chromium',
|
|
229
300
|
runnerIds: runnerIdsStr,
|
|
301
|
+
notifyDiscord: notifyDiscord ?? false,
|
|
302
|
+
notifySlack: notifySlack ?? false,
|
|
230
303
|
runnerId: null
|
|
231
304
|
}
|
|
232
305
|
});
|
|
@@ -248,7 +321,16 @@ const removeCronJob = async (taskName) => {
|
|
|
248
321
|
|
|
249
322
|
const updateCronJob = async (
|
|
250
323
|
oldTaskName,
|
|
251
|
-
{
|
|
324
|
+
{
|
|
325
|
+
taskName: newTaskName,
|
|
326
|
+
cronExpression,
|
|
327
|
+
tags,
|
|
328
|
+
workers,
|
|
329
|
+
browser,
|
|
330
|
+
runnerIds,
|
|
331
|
+
notifyDiscord,
|
|
332
|
+
notifySlack
|
|
333
|
+
}
|
|
252
334
|
) => {
|
|
253
335
|
const job = await prisma.cronJob.findUnique({ where: { taskName: oldTaskName } });
|
|
254
336
|
if (!job) return { status: 404, message: `Cron job "${oldTaskName}" not found` };
|
|
@@ -271,6 +353,8 @@ const updateCronJob = async (
|
|
|
271
353
|
workers: workers ?? 1,
|
|
272
354
|
browser: browser ?? 'chromium',
|
|
273
355
|
runnerIds: runnerIdsStr,
|
|
356
|
+
notifyDiscord: notifyDiscord ?? false,
|
|
357
|
+
notifySlack: notifySlack ?? false,
|
|
274
358
|
runnerId: null
|
|
275
359
|
}
|
|
276
360
|
});
|