plum-e2e 2.5.7 → 2.5.9
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.
|
@@ -69,18 +69,13 @@ try {
|
|
|
69
69
|
|
|
70
70
|
// hooks.ts calls dotenv.config() with no path, which defaults to process.cwd().
|
|
71
71
|
// When running from a temp dir there is no .env there, so vars like BASE_URL
|
|
72
|
-
// would be undefined.
|
|
73
|
-
// process.env
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
const key = m[1];
|
|
80
|
-
const val = m[2].trim().replace(/^(['"])(.*)\1$/, '$2');
|
|
81
|
-
if (!(key in process.env)) process.env[key] = val;
|
|
82
|
-
}
|
|
83
|
-
}
|
|
72
|
+
// would be undefined. A dispatched node job already has these injected into
|
|
73
|
+
// process.env by node.routes.js from the primary's payload; this is just a
|
|
74
|
+
// fallback for local/standalone runs, so it never overwrites what's already set.
|
|
75
|
+
const { loadTestEnv } = require('../../lib/testEnv');
|
|
76
|
+
const backendEnv = loadTestEnv(path.resolve(__dirname, '..', '..'));
|
|
77
|
+
for (const [key, val] of Object.entries(backendEnv)) {
|
|
78
|
+
if (!(key in process.env)) process.env[key] = val;
|
|
84
79
|
}
|
|
85
80
|
|
|
86
81
|
fs.writeFileSync(
|
|
@@ -0,0 +1,67 @@
|
|
|
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
|
+
/**
|
|
19
|
+
* `.plum-server.json`/`.plum-node.json` live in whatever directory the user
|
|
20
|
+
* happened to run `plum server start`/`plum node start` from. `plum update`
|
|
21
|
+
* needs to find and restart those installs later regardless of the cwd it's
|
|
22
|
+
* invoked from — this is the one place, independent of any project
|
|
23
|
+
* directory, that remembers where they are.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
const fs = require('fs');
|
|
27
|
+
const os = require('os');
|
|
28
|
+
const path = require('path');
|
|
29
|
+
|
|
30
|
+
const REGISTRY_DIR = path.join(os.homedir(), '.plum');
|
|
31
|
+
const REGISTRY_PATH = path.join(REGISTRY_DIR, 'installs.json');
|
|
32
|
+
|
|
33
|
+
function load() {
|
|
34
|
+
try {
|
|
35
|
+
return JSON.parse(fs.readFileSync(REGISTRY_PATH, 'utf8'));
|
|
36
|
+
} catch {
|
|
37
|
+
return { server: [], node: [] };
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function save(data) {
|
|
42
|
+
fs.mkdirSync(REGISTRY_DIR, { recursive: true });
|
|
43
|
+
fs.writeFileSync(REGISTRY_PATH, JSON.stringify(data, null, 2) + '\n', 'utf8');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Records `dir` as a known `type` ('server' | 'node') install location. */
|
|
47
|
+
function registerInstall(type, dir) {
|
|
48
|
+
const data = load();
|
|
49
|
+
if (!data[type]) data[type] = [];
|
|
50
|
+
if (!data[type].includes(dir)) {
|
|
51
|
+
data[type].push(dir);
|
|
52
|
+
save(data);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Known install dirs for `type`, pruned of any that no longer exist on disk. */
|
|
57
|
+
function getInstalls(type) {
|
|
58
|
+
const data = load();
|
|
59
|
+
const dirs = (data[type] || []).filter((d) => fs.existsSync(d));
|
|
60
|
+
if (dirs.length !== (data[type] || []).length) {
|
|
61
|
+
data[type] = dirs;
|
|
62
|
+
save(data);
|
|
63
|
+
}
|
|
64
|
+
return dirs;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
module.exports = { REGISTRY_PATH, registerInstall, getInstalls };
|
|
@@ -0,0 +1,38 @@
|
|
|
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 fs = require('fs');
|
|
19
|
+
const path = require('path');
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Parses a dir's `.env` into a plain object. Used to hand a remote runner node
|
|
23
|
+
* the same BASE_URL/IS_HEADLESS/custom test vars the primary has, so nodes stay
|
|
24
|
+
* stateless runners instead of needing their own local `.env`.
|
|
25
|
+
*/
|
|
26
|
+
function loadTestEnv(dir) {
|
|
27
|
+
const out = {};
|
|
28
|
+
try {
|
|
29
|
+
const txt = fs.readFileSync(path.join(dir, '.env'), 'utf8');
|
|
30
|
+
for (const line of txt.split(/\r?\n/)) {
|
|
31
|
+
const m = line.match(/^([A-Za-z_]\w*)\s*=\s*(.*?)(\s*#.*)?$/);
|
|
32
|
+
if (m) out[m[1]] = m[2].trim().replace(/^(['"])(.*)\1$/, '$2');
|
|
33
|
+
}
|
|
34
|
+
} catch {}
|
|
35
|
+
return out;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
module.exports = { loadTestEnv };
|
|
@@ -47,7 +47,7 @@ router.post('/shutdown', authGuard, (req, res) => {
|
|
|
47
47
|
|
|
48
48
|
// Start a remote test job
|
|
49
49
|
router.post('/execute', authGuard, (req, res) => {
|
|
50
|
-
const { tags, browser = 'chromium', workers = 1, tests = null } = req.body;
|
|
50
|
+
const { tags, browser = 'chromium', workers = 1, tests = null, env: userEnv = {} } = req.body;
|
|
51
51
|
const jobId = crypto.randomUUID();
|
|
52
52
|
|
|
53
53
|
// path.resolve ensures absolute even if TMPDIR env var is set to a relative path
|
|
@@ -84,6 +84,11 @@ router.post('/execute', authGuard, (req, res) => {
|
|
|
84
84
|
|
|
85
85
|
const env = {
|
|
86
86
|
...process.env,
|
|
87
|
+
// User/test vars (BASE_URL, IS_HEADLESS, custom secrets) forwarded from the
|
|
88
|
+
// primary's own .env — nodes are stateless runners and shouldn't need their
|
|
89
|
+
// own copy. Spread before the job-control vars below so a stray same-named
|
|
90
|
+
// var in the user's .env can never override how this job actually runs.
|
|
91
|
+
...userEnv,
|
|
87
92
|
TAG: tags || '',
|
|
88
93
|
TRIGGER: TRIGGER_REMOTE,
|
|
89
94
|
BROWSER: browser,
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
const fs = require('fs');
|
|
19
19
|
const path = require('path');
|
|
20
20
|
const prisma = require('./prisma');
|
|
21
|
+
const { loadTestEnv } = require('../lib/testEnv');
|
|
21
22
|
|
|
22
23
|
// ---------------------------------------------------------------------------
|
|
23
24
|
// Runner CRUD
|
|
@@ -170,7 +171,13 @@ async function dispatchAndPoll(
|
|
|
170
171
|
'Content-Type': 'application/json',
|
|
171
172
|
Authorization: `Bearer ${runner.token}`
|
|
172
173
|
},
|
|
173
|
-
body: JSON.stringify({
|
|
174
|
+
body: JSON.stringify({
|
|
175
|
+
tags,
|
|
176
|
+
browser,
|
|
177
|
+
workers,
|
|
178
|
+
tests: collectTestFiles(),
|
|
179
|
+
env: loadTestEnv(process.cwd())
|
|
180
|
+
}),
|
|
174
181
|
signal: AbortSignal.timeout(10000)
|
|
175
182
|
});
|
|
176
183
|
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
package/bin/plum.js
CHANGED
|
@@ -138,6 +138,7 @@ const backendLib = path.join(plumRoot, 'backend', 'lib');
|
|
|
138
138
|
const serverConfigLib = () => require(path.join(backendLib, 'serverConfig.js'));
|
|
139
139
|
const nodeRegisterLib = () => require(path.join(backendLib, 'nodeRegister.js'));
|
|
140
140
|
const runnerProcessLib = () => require(path.join(backendLib, 'runnerProcess.js'));
|
|
141
|
+
const globalRegistryLib = () => require(path.join(backendLib, 'globalRegistry.js'));
|
|
141
142
|
|
|
142
143
|
/* -----------------------------------------------------
|
|
143
144
|
* Interactive prompts
|
|
@@ -254,6 +255,7 @@ async function configureServer({ force }) {
|
|
|
254
255
|
}
|
|
255
256
|
|
|
256
257
|
saveServerConfig(cwd, cfg);
|
|
258
|
+
globalRegistryLib().registerInstall('server', cwd);
|
|
257
259
|
return cfg;
|
|
258
260
|
}
|
|
259
261
|
|
|
@@ -484,9 +486,15 @@ function npmInstallLatestWithRetry() {
|
|
|
484
486
|
return false;
|
|
485
487
|
}
|
|
486
488
|
|
|
489
|
+
function readPlumVersion() {
|
|
490
|
+
return JSON.parse(fs.readFileSync(path.join(plumRoot, 'package.json'), 'utf8')).version;
|
|
491
|
+
}
|
|
492
|
+
|
|
487
493
|
async function serverUpdate() {
|
|
488
494
|
clack.intro(pc.bgMagenta(pc.white(' 🟣 Plum — Update ')));
|
|
489
|
-
|
|
495
|
+
|
|
496
|
+
const fromVersion = readPlumVersion();
|
|
497
|
+
clack.log.step(`Fetching latest Plum version… (currently ${fromVersion})`);
|
|
490
498
|
if (!npmInstallLatestWithRetry()) {
|
|
491
499
|
clack.log.error(
|
|
492
500
|
`Failed to install the latest version after ${NPM_INSTALL_RETRIES} attempts. Try again shortly, or run "npm install -g plum-e2e@latest" manually to see the full error.`
|
|
@@ -495,39 +503,61 @@ async function serverUpdate() {
|
|
|
495
503
|
process.exitCode = 1;
|
|
496
504
|
return;
|
|
497
505
|
}
|
|
498
|
-
clack.log.success('Plum CLI updated.');
|
|
499
506
|
|
|
500
|
-
|
|
501
|
-
const
|
|
507
|
+
// Re-read from disk (not require-cached) so this reflects what npm just installed.
|
|
508
|
+
const toVersion = readPlumVersion();
|
|
509
|
+
clack.log.success(`Plum CLI updated: ${fromVersion} → ${toVersion}`);
|
|
502
510
|
|
|
511
|
+
// Every install registers its directory here when configured (see
|
|
512
|
+
// configureServer/configureNode), so this finds them regardless of the cwd
|
|
513
|
+
// `plum update` happens to be run from.
|
|
514
|
+
const { getInstalls } = globalRegistryLib();
|
|
503
515
|
const { loadNodeConfig } = nodeRegisterLib();
|
|
504
516
|
const { loadRegistry, isAlive } = runnerProcessLib();
|
|
505
|
-
const nodeCfg = loadNodeConfig(process.cwd());
|
|
506
|
-
const registry = loadRegistry();
|
|
507
|
-
const nodeRunning = !!(
|
|
508
|
-
nodeCfg.id &&
|
|
509
|
-
registry[String(nodeCfg.id)]?.pid &&
|
|
510
|
-
isAlive(registry[String(nodeCfg.id)].pid)
|
|
511
|
-
);
|
|
512
517
|
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
//
|
|
516
|
-
//
|
|
517
|
-
//
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
518
|
+
let restartedAnything = false;
|
|
519
|
+
|
|
520
|
+
// Re-exec `plum` as a fresh process (cwd set to each install dir) for the
|
|
521
|
+
// restart steps rather than calling serverRestart()/nodeRestart() directly —
|
|
522
|
+
// this same process already loaded the OLD code into memory before npm
|
|
523
|
+
// install ran above, so calling them in-process would rebuild using stale
|
|
524
|
+
// logic no matter how new the just-installed files on disk actually are.
|
|
525
|
+
for (const dir of getInstalls('server')) {
|
|
526
|
+
if (!fs.existsSync(path.join(dir, '.plum-server.json'))) continue;
|
|
527
|
+
clack.log.step(`Rebuilding server at ${dir}…`);
|
|
528
|
+
try {
|
|
529
|
+
execSync('plum server restart', { stdio: 'inherit', cwd: dir });
|
|
530
|
+
restartedAnything = true;
|
|
531
|
+
} catch (e) {
|
|
532
|
+
clack.log.warn(`Could not restart server at ${dir}: ${e.message}`);
|
|
533
|
+
}
|
|
521
534
|
}
|
|
522
535
|
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
536
|
+
const registry = loadRegistry();
|
|
537
|
+
for (const dir of getInstalls('node')) {
|
|
538
|
+
const nodeCfg = loadNodeConfig(dir);
|
|
539
|
+
const running = !!(
|
|
540
|
+
nodeCfg.id &&
|
|
541
|
+
registry[String(nodeCfg.id)]?.pid &&
|
|
542
|
+
isAlive(registry[String(nodeCfg.id)].pid)
|
|
543
|
+
);
|
|
544
|
+
if (!running) continue;
|
|
545
|
+
clack.log.step(`Restarting node runner at ${dir}…`);
|
|
546
|
+
try {
|
|
547
|
+
execSync('plum node restart', { stdio: 'inherit', cwd: dir });
|
|
548
|
+
restartedAnything = true;
|
|
549
|
+
} catch (e) {
|
|
550
|
+
clack.log.warn(`Could not restart node at ${dir}: ${e.message}`);
|
|
551
|
+
}
|
|
526
552
|
}
|
|
527
553
|
|
|
528
|
-
if (!
|
|
529
|
-
clack.
|
|
554
|
+
if (!restartedAnything) {
|
|
555
|
+
clack.log.info(
|
|
556
|
+
'No running server or node found — run `plum server start` or `plum node start` when ready.'
|
|
557
|
+
);
|
|
530
558
|
}
|
|
559
|
+
|
|
560
|
+
clack.outro(pc.green(`Plum updated: ${fromVersion} → ${toVersion}`));
|
|
531
561
|
}
|
|
532
562
|
|
|
533
563
|
async function serverReconfig() {
|
|
@@ -610,6 +640,7 @@ async function configureNode({ force }) {
|
|
|
610
640
|
port,
|
|
611
641
|
pid: saved.pid ?? null
|
|
612
642
|
});
|
|
643
|
+
globalRegistryLib().registerInstall('node', cwd);
|
|
613
644
|
return { primary, port, browser, token, name, url };
|
|
614
645
|
}
|
|
615
646
|
|
|
@@ -665,6 +696,23 @@ async function nodeStart({ reconfig }) {
|
|
|
665
696
|
const backendDir = path.join(plumRoot, 'backend');
|
|
666
697
|
clack.intro(pc.bgMagenta(pc.white(' 🟣 Plum — Node Runner ')));
|
|
667
698
|
|
|
699
|
+
const { loadNodeConfig } = nodeRegisterLib();
|
|
700
|
+
const { statusOf } = runnerProcessLib();
|
|
701
|
+
const existing = loadNodeConfig(process.cwd());
|
|
702
|
+
|
|
703
|
+
// Re-running `node start` on an already-running node used to spawn a second
|
|
704
|
+
// process on the same port (orphaning the first) and re-register a duplicate
|
|
705
|
+
// runner on the primary. Route to the same menu this command ends on anyway
|
|
706
|
+
// instead of repeating the whole configure/register/spawn dance.
|
|
707
|
+
if (!reconfig && existing.id && statusOf(String(existing.id)) === 'running') {
|
|
708
|
+
clack.log.info(
|
|
709
|
+
`Node "${existing.name ?? existing.id}" is already running from this folder — opening the runner menu instead of starting a new one.`
|
|
710
|
+
);
|
|
711
|
+
await openManageRunnersMenu(existing.primary);
|
|
712
|
+
clack.outro(`Manage runners anytime: ${pc.cyan('plum manage-runners')}`);
|
|
713
|
+
return;
|
|
714
|
+
}
|
|
715
|
+
|
|
668
716
|
const cfg = await configureNode({ force: reconfig });
|
|
669
717
|
const registeredId = await registerNode(cfg);
|
|
670
718
|
|