@ongtrieuhau861457/runner-tailscale-sync 1.260202.11920
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 +310 -0
- package/bin/runner-sync.js +85 -0
- package/package.json +41 -0
- package/src/adapters/fs.js +160 -0
- package/src/adapters/git.js +185 -0
- package/src/adapters/http.js +56 -0
- package/src/adapters/process.js +151 -0
- package/src/adapters/ssh.js +103 -0
- package/src/adapters/tailscale.js +406 -0
- package/src/cli/commands/init.js +13 -0
- package/src/cli/commands/push.js +13 -0
- package/src/cli/commands/status.js +13 -0
- package/src/cli/commands/sync.js +22 -0
- package/src/cli/parser.js +114 -0
- package/src/core/data-sync.js +177 -0
- package/src/core/init.js +141 -0
- package/src/core/push.js +113 -0
- package/src/core/runner-detector.js +167 -0
- package/src/core/service-controller.js +141 -0
- package/src/core/status.js +130 -0
- package/src/core/sync-orchestrator.js +260 -0
- package/src/index.js +140 -0
- package/src/utils/config.js +129 -0
- package/src/utils/constants.js +33 -0
- package/src/utils/errors.js +45 -0
- package/src/utils/logger.js +154 -0
- package/src/utils/time.js +65 -0
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* core/data-sync.js
|
|
3
|
+
* Pull/Push .runner-data directory giữa các runners
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const path = require("path");
|
|
7
|
+
const fs_adapter = require("../adapters/fs");
|
|
8
|
+
const process_adapter = require("../adapters/process");
|
|
9
|
+
const { SyncError, ValidationError } = require("../utils/errors");
|
|
10
|
+
const CONST = require("../utils/constants");
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Parse input
|
|
14
|
+
*/
|
|
15
|
+
function parseInput(config, previousRunner, logger) {
|
|
16
|
+
return {
|
|
17
|
+
localDataDir: config.runnerDataDir,
|
|
18
|
+
remoteHost: previousRunner?.dnsName || previousRunner?.ips?.[0],
|
|
19
|
+
remoteDataDir: config.runnerDataDir,
|
|
20
|
+
rsyncPath: config.rsyncPath,
|
|
21
|
+
sshPath: config.sshPath,
|
|
22
|
+
logger,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Validate
|
|
28
|
+
*/
|
|
29
|
+
function validate(input) {
|
|
30
|
+
const errors = [];
|
|
31
|
+
|
|
32
|
+
if (!input.localDataDir) {
|
|
33
|
+
errors.push("Local data directory is required");
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (!input.remoteHost) {
|
|
37
|
+
errors.push("Remote host is required");
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (errors.length > 0) {
|
|
41
|
+
throw new ValidationError(`Validation failed: ${errors.join(", ")}`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Plan
|
|
47
|
+
*/
|
|
48
|
+
function plan(input) {
|
|
49
|
+
return {
|
|
50
|
+
action: "rsync_pull",
|
|
51
|
+
source: `${input.remoteHost}:${input.remoteDataDir}/`,
|
|
52
|
+
destination: input.localDataDir,
|
|
53
|
+
rsyncPath: input.rsyncPath,
|
|
54
|
+
sshPath: input.sshPath,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Execute - pull data từ remote runner
|
|
60
|
+
*/
|
|
61
|
+
async function execute(planResult, input) {
|
|
62
|
+
const { logger } = input;
|
|
63
|
+
|
|
64
|
+
logger.info(`Syncing data from ${planResult.source}...`);
|
|
65
|
+
|
|
66
|
+
// Ensure local directory exists
|
|
67
|
+
fs_adapter.ensureDir(planResult.destination);
|
|
68
|
+
|
|
69
|
+
// Build rsync command
|
|
70
|
+
// rsync -avz -e ssh remote:.runner-data/ local/.runner-data/
|
|
71
|
+
const isLocalNetwork = input.remoteHost?.startsWith("100.");
|
|
72
|
+
const rsyncCmd = [
|
|
73
|
+
planResult.rsyncPath,
|
|
74
|
+
isLocalNetwork ? "-av" : "-avz",
|
|
75
|
+
"--delete",
|
|
76
|
+
"--partial",
|
|
77
|
+
"--progress",
|
|
78
|
+
"-e",
|
|
79
|
+
`${planResult.sshPath} -o StrictHostKeyChecking=no`,
|
|
80
|
+
planResult.source,
|
|
81
|
+
planResult.destination,
|
|
82
|
+
];
|
|
83
|
+
|
|
84
|
+
try {
|
|
85
|
+
await process_adapter.runWithTimeout(
|
|
86
|
+
rsyncCmd,
|
|
87
|
+
CONST.RSYNC_TIMEOUT,
|
|
88
|
+
{ logger }
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
logger.success("Data synced successfully");
|
|
92
|
+
|
|
93
|
+
// Get synced size
|
|
94
|
+
const size = fs_adapter.getDirSize(planResult.destination);
|
|
95
|
+
logger.info(`Synced size: ${fs_adapter.formatBytes(size)}`);
|
|
96
|
+
|
|
97
|
+
return {
|
|
98
|
+
success: true,
|
|
99
|
+
size,
|
|
100
|
+
};
|
|
101
|
+
} catch (err) {
|
|
102
|
+
// If rsync not available, try scp as fallback
|
|
103
|
+
logger.warn("Rsync failed, trying scp as fallback...");
|
|
104
|
+
|
|
105
|
+
try {
|
|
106
|
+
const scpCmd = `${planResult.sshPath} -r ${planResult.source} ${planResult.destination}`;
|
|
107
|
+
await process_adapter.runWithTimeout(
|
|
108
|
+
scpCmd,
|
|
109
|
+
CONST.RSYNC_TIMEOUT,
|
|
110
|
+
{ logger }
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
logger.success("Data synced via scp");
|
|
114
|
+
const size = fs_adapter.getDirSize(planResult.destination);
|
|
115
|
+
return {
|
|
116
|
+
success: true,
|
|
117
|
+
size,
|
|
118
|
+
};
|
|
119
|
+
} catch (scpErr) {
|
|
120
|
+
throw new SyncError(`Failed to sync data: ${scpErr.message}`);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Report
|
|
127
|
+
*/
|
|
128
|
+
function report(result, input) {
|
|
129
|
+
const { logger } = input;
|
|
130
|
+
|
|
131
|
+
if (result.success) {
|
|
132
|
+
logger.success("Data synchronization completed");
|
|
133
|
+
return {
|
|
134
|
+
success: true,
|
|
135
|
+
syncedSize: result.size,
|
|
136
|
+
};
|
|
137
|
+
} else {
|
|
138
|
+
logger.error("Data synchronization failed");
|
|
139
|
+
return {
|
|
140
|
+
success: false,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Main pull function
|
|
147
|
+
*/
|
|
148
|
+
async function pullData(config, previousRunner, logger) {
|
|
149
|
+
if (!previousRunner) {
|
|
150
|
+
logger.info("No previous runner - skipping data pull");
|
|
151
|
+
return { success: true, syncedSize: 0 };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Step 1: Parse Input
|
|
155
|
+
const input = parseInput(config, previousRunner, logger);
|
|
156
|
+
|
|
157
|
+
// Step 2: Validate
|
|
158
|
+
validate(input);
|
|
159
|
+
|
|
160
|
+
// Step 3: Plan
|
|
161
|
+
const planResult = plan(input);
|
|
162
|
+
|
|
163
|
+
// Step 4: Execute
|
|
164
|
+
const execResult = await execute(planResult, input);
|
|
165
|
+
|
|
166
|
+
// Step 5: Report
|
|
167
|
+
return report(execResult, input);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
module.exports = {
|
|
171
|
+
pullData,
|
|
172
|
+
parseInput,
|
|
173
|
+
validate,
|
|
174
|
+
plan,
|
|
175
|
+
execute,
|
|
176
|
+
report,
|
|
177
|
+
};
|
package/src/core/init.js
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* core/init.js
|
|
3
|
+
* Initialize Tailscale and detect previous runner
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const tailscale = require("../adapters/tailscale");
|
|
7
|
+
const fs_adapter = require("../adapters/fs");
|
|
8
|
+
const runnerDetector = require("./runner-detector");
|
|
9
|
+
const { ValidationError, ProcessError } = require("../utils/errors");
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Parse input
|
|
13
|
+
*/
|
|
14
|
+
function parseInput(config, logger) {
|
|
15
|
+
return {
|
|
16
|
+
config,
|
|
17
|
+
logger,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Validate
|
|
23
|
+
*/
|
|
24
|
+
function validate(input) {
|
|
25
|
+
const errors = input.config.validate();
|
|
26
|
+
if (errors.length > 0) {
|
|
27
|
+
throw new ValidationError(`Validation failed:\n - ${errors.join("\n - ")}`);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Plan
|
|
33
|
+
*/
|
|
34
|
+
function plan(input) {
|
|
35
|
+
const { config } = input;
|
|
36
|
+
|
|
37
|
+
return {
|
|
38
|
+
steps: [
|
|
39
|
+
{ name: "setup_directories", enabled: true },
|
|
40
|
+
{ name: "connect_tailscale", enabled: config.tailscaleEnable },
|
|
41
|
+
{ name: "detect_previous_runner", enabled: config.tailscaleEnable },
|
|
42
|
+
],
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Execute
|
|
48
|
+
*/
|
|
49
|
+
async function execute(planResult, input) {
|
|
50
|
+
const { config, logger } = input;
|
|
51
|
+
const results = {};
|
|
52
|
+
|
|
53
|
+
for (const step of planResult.steps) {
|
|
54
|
+
if (!step.enabled) {
|
|
55
|
+
logger.debug(`Skipping step: ${step.name}`);
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
logger.info(`━━━ Step: ${step.name} ━━━`);
|
|
60
|
+
|
|
61
|
+
switch (step.name) {
|
|
62
|
+
case "setup_directories": {
|
|
63
|
+
const dirs = config.getDirectoriesToEnsure();
|
|
64
|
+
fs_adapter.ensureDirs(dirs);
|
|
65
|
+
logger.success(`Created ${dirs.length} directories`);
|
|
66
|
+
results.setupDirs = dirs;
|
|
67
|
+
break;
|
|
68
|
+
}
|
|
69
|
+
case "connect_tailscale": {
|
|
70
|
+
const installed = tailscale.install(logger);
|
|
71
|
+
if (!installed) {
|
|
72
|
+
throw new ProcessError("Failed to install Tailscale");
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
await tailscale.login(
|
|
76
|
+
config.tailscaleClientId,
|
|
77
|
+
config.tailscaleClientSecret,
|
|
78
|
+
config.tailscaleTags,
|
|
79
|
+
logger,
|
|
80
|
+
config
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
const ip = tailscale.getIP(logger);
|
|
84
|
+
const hostname = tailscale.getHostname(logger);
|
|
85
|
+
logger.success(`Tailscale connected: ${ip || hostname}`);
|
|
86
|
+
results.tailscale = { ip, hostname };
|
|
87
|
+
break;
|
|
88
|
+
}
|
|
89
|
+
case "detect_previous_runner": {
|
|
90
|
+
results.detection = await runnerDetector.detectPreviousRunner(config, logger);
|
|
91
|
+
if (results.detection.previousRunner) {
|
|
92
|
+
logger.success(`Previous runner: ${results.detection.previousRunner.hostname}`);
|
|
93
|
+
logger.info(` IP: ${results.detection.previousRunner.ips[0]}`);
|
|
94
|
+
} else {
|
|
95
|
+
logger.info("No previous runner found - this is the first runner");
|
|
96
|
+
}
|
|
97
|
+
break;
|
|
98
|
+
}
|
|
99
|
+
default:
|
|
100
|
+
logger.warn(`Unknown step: ${step.name}`);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return results;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Report
|
|
109
|
+
*/
|
|
110
|
+
function report(results, input) {
|
|
111
|
+
const { logger } = input;
|
|
112
|
+
if (!input.config.tailscaleEnable) {
|
|
113
|
+
logger.info("Tailscale disabled - skipping network setup");
|
|
114
|
+
}
|
|
115
|
+
logger.success("Init workflow completed");
|
|
116
|
+
return {
|
|
117
|
+
success: true,
|
|
118
|
+
tailscale: results.tailscale,
|
|
119
|
+
previousRunner: results.detection?.previousRunner || null,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Main init function
|
|
125
|
+
*/
|
|
126
|
+
async function initRunner(config, logger) {
|
|
127
|
+
const input = parseInput(config, logger);
|
|
128
|
+
validate(input);
|
|
129
|
+
const planResult = plan(input);
|
|
130
|
+
const execResult = await execute(planResult, input);
|
|
131
|
+
return report(execResult, input);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
module.exports = {
|
|
135
|
+
initRunner,
|
|
136
|
+
parseInput,
|
|
137
|
+
validate,
|
|
138
|
+
plan,
|
|
139
|
+
execute,
|
|
140
|
+
report,
|
|
141
|
+
};
|
package/src/core/push.js
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* core/push.js
|
|
3
|
+
* Push .runner-data to git repository
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const git = require("../adapters/git");
|
|
7
|
+
const { getTimestamp } = require("../utils/time");
|
|
8
|
+
const { ValidationError, ProcessError } = require("../utils/errors");
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Parse input
|
|
12
|
+
*/
|
|
13
|
+
function parseInput(config, logger) {
|
|
14
|
+
return {
|
|
15
|
+
gitEnabled: config.gitEnabled,
|
|
16
|
+
gitBranch: config.gitBranch,
|
|
17
|
+
cwd: config.cwd,
|
|
18
|
+
logger,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Validate
|
|
24
|
+
*/
|
|
25
|
+
function validate(input) {
|
|
26
|
+
if (!input.gitEnabled) {
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (!git.isAvailable()) {
|
|
31
|
+
throw new ProcessError("Git is not available");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (!git.isGitRepo(input.cwd)) {
|
|
35
|
+
throw new ValidationError("Not a git repository");
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Plan
|
|
41
|
+
*/
|
|
42
|
+
function plan(input) {
|
|
43
|
+
if (!input.gitEnabled) {
|
|
44
|
+
return { action: "skip" };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return {
|
|
48
|
+
action: "push_runner_data",
|
|
49
|
+
branch: input.gitBranch,
|
|
50
|
+
cwd: input.cwd,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Execute
|
|
56
|
+
*/
|
|
57
|
+
async function execute(planResult, input) {
|
|
58
|
+
const { logger } = input;
|
|
59
|
+
|
|
60
|
+
if (planResult.action === "skip") {
|
|
61
|
+
logger.warn("Git push disabled (GIT_PUSH_ENABLED=0)");
|
|
62
|
+
return { skipped: true };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const timestamp = getTimestamp();
|
|
66
|
+
const message = `[runner-sync] Update .runner-data at ${timestamp}`;
|
|
67
|
+
|
|
68
|
+
const pushed = await git.commitAndPush(message, planResult.branch, {
|
|
69
|
+
logger,
|
|
70
|
+
cwd: planResult.cwd,
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
return { pushed };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Report
|
|
78
|
+
*/
|
|
79
|
+
function report(result, input) {
|
|
80
|
+
const { logger } = input;
|
|
81
|
+
|
|
82
|
+
if (result.skipped) {
|
|
83
|
+
return { success: false, skipped: true };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (result.pushed) {
|
|
87
|
+
logger.success("Pushed to git repository");
|
|
88
|
+
return { success: true };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
logger.info("No changes to push");
|
|
92
|
+
return { success: true, noChanges: true };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Main push function
|
|
97
|
+
*/
|
|
98
|
+
async function pushRunnerData(config, logger) {
|
|
99
|
+
const input = parseInput(config, logger);
|
|
100
|
+
validate(input);
|
|
101
|
+
const planResult = plan(input);
|
|
102
|
+
const execResult = await execute(planResult, input);
|
|
103
|
+
return report(execResult, input);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
module.exports = {
|
|
107
|
+
pushRunnerData,
|
|
108
|
+
parseInput,
|
|
109
|
+
validate,
|
|
110
|
+
plan,
|
|
111
|
+
execute,
|
|
112
|
+
report,
|
|
113
|
+
};
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* core/runner-detector.js
|
|
3
|
+
* Phát hiện runner trước đó trên Tailscale network
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const tailscale = require("../adapters/tailscale");
|
|
7
|
+
const ssh = require("../adapters/ssh");
|
|
8
|
+
const { ValidationError } = require("../utils/errors");
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Parse input
|
|
12
|
+
*/
|
|
13
|
+
function parseInput(config, logger) {
|
|
14
|
+
return {
|
|
15
|
+
tags: String(config.tailscaleTags || "").split(",").map(s=>s.trim()).filter(Boolean),
|
|
16
|
+
sshPath: config.sshPath,
|
|
17
|
+
logger,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Validate
|
|
23
|
+
*/
|
|
24
|
+
function validate(input) {
|
|
25
|
+
const errors = [];
|
|
26
|
+
|
|
27
|
+
if (!input.tags || input.tags.length === 0) {
|
|
28
|
+
errors.push("Tailscale tag is required");
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (errors.length > 0) {
|
|
32
|
+
throw new ValidationError(`Validation failed: ${errors.join(", ")}`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Plan - xác định strategy
|
|
38
|
+
*/
|
|
39
|
+
function plan(input) {
|
|
40
|
+
return {
|
|
41
|
+
action: "detect_previous_runner",
|
|
42
|
+
tags: input.tags,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Execute - tìm runner trước đó
|
|
48
|
+
*/
|
|
49
|
+
async function execute(planResult, input) {
|
|
50
|
+
const { logger } = input;
|
|
51
|
+
|
|
52
|
+
logger.info("Searching for previous runner on Tailscale network...");
|
|
53
|
+
|
|
54
|
+
// Get all peers with same tag
|
|
55
|
+
const peers = tailscale.findPeersWithTag(planResult.tags, logger);
|
|
56
|
+
|
|
57
|
+
if (peers.length === 0) {
|
|
58
|
+
logger.info("No previous runner found");
|
|
59
|
+
return {
|
|
60
|
+
found: false,
|
|
61
|
+
peer: null,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
for (const peer of peers) {
|
|
66
|
+
const targetHost = peer.ips?.[0];
|
|
67
|
+
if (!targetHost) {
|
|
68
|
+
peer.accessible = false;
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
peer.accessible = await Promise.resolve(
|
|
72
|
+
ssh.checkConnection(targetHost, { logger, sshPath: input.sshPath })
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const accessiblePeers = peers.filter((peer) => peer.accessible);
|
|
77
|
+
|
|
78
|
+
for (const peer of accessiblePeers) {
|
|
79
|
+
const targetHost = peer.ips?.[0];
|
|
80
|
+
if (!targetHost) {
|
|
81
|
+
peer.hasData = false;
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
const result = ssh.executeCommandCapture(
|
|
85
|
+
targetHost,
|
|
86
|
+
'test -d .runner-data && echo "yes"',
|
|
87
|
+
{ sshPath: input.sshPath }
|
|
88
|
+
);
|
|
89
|
+
peer.hasData = result === "yes";
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const peer = accessiblePeers
|
|
93
|
+
.filter((item) => item.hasData)
|
|
94
|
+
.slice()
|
|
95
|
+
.sort((a, b) => {
|
|
96
|
+
const ta = a.lastSeen ? Date.parse(a.lastSeen) : 0;
|
|
97
|
+
const tb = b.lastSeen ? Date.parse(b.lastSeen) : 0;
|
|
98
|
+
return tb - ta;
|
|
99
|
+
})[0];
|
|
100
|
+
|
|
101
|
+
if (!peer) {
|
|
102
|
+
logger.info("No accessible runner with data found");
|
|
103
|
+
return {
|
|
104
|
+
found: false,
|
|
105
|
+
peer: null,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
logger.success(`Found previous runner: ${peer.hostname || peer.id}`);
|
|
110
|
+
logger.info(` IP: ${peer.ips[0] || "N/A"}`);
|
|
111
|
+
logger.info(` DNS: ${peer.dnsName || "N/A"}`);
|
|
112
|
+
|
|
113
|
+
return {
|
|
114
|
+
found: true,
|
|
115
|
+
peer,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Report
|
|
121
|
+
*/
|
|
122
|
+
function report(result, input) {
|
|
123
|
+
const { logger } = input;
|
|
124
|
+
|
|
125
|
+
if (result.found) {
|
|
126
|
+
logger.success("Previous runner detected");
|
|
127
|
+
return {
|
|
128
|
+
success: true,
|
|
129
|
+
previousRunner: result.peer,
|
|
130
|
+
};
|
|
131
|
+
} else {
|
|
132
|
+
logger.info("No previous runner - this is the first runner");
|
|
133
|
+
return {
|
|
134
|
+
success: true,
|
|
135
|
+
previousRunner: null,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Main detect function
|
|
142
|
+
*/
|
|
143
|
+
async function detectPreviousRunner(config, logger) {
|
|
144
|
+
// Step 1: Parse Input
|
|
145
|
+
const input = parseInput(config, logger);
|
|
146
|
+
|
|
147
|
+
// Step 2: Validate
|
|
148
|
+
validate(input);
|
|
149
|
+
|
|
150
|
+
// Step 3: Plan
|
|
151
|
+
const planResult = plan(input);
|
|
152
|
+
|
|
153
|
+
// Step 4: Execute
|
|
154
|
+
const execResult = await execute(planResult, input);
|
|
155
|
+
|
|
156
|
+
// Step 5: Report
|
|
157
|
+
return report(execResult, input);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
module.exports = {
|
|
161
|
+
detectPreviousRunner,
|
|
162
|
+
parseInput,
|
|
163
|
+
validate,
|
|
164
|
+
plan,
|
|
165
|
+
execute,
|
|
166
|
+
report,
|
|
167
|
+
};
|