@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.
@@ -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
+ };
@@ -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
+ };
@@ -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
+ };