petadep 1.0.1 → 1.0.3
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 +17 -0
- package/bin/petadep.js +58 -7
- package/package.json +1 -1
- package/src/agent.js +1 -0
- package/src/cli.js +124 -0
- package/src/deployer.js +23 -8
package/README.md
CHANGED
|
@@ -16,6 +16,14 @@ chmod +x bin/petadep.js
|
|
|
16
16
|
|
|
17
17
|
## Initialize config
|
|
18
18
|
|
|
19
|
+
```bash
|
|
20
|
+
petadep
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Interactive setup will ask for port, webhook path, secret, deployment info, and where to save the config.
|
|
24
|
+
|
|
25
|
+
If you prefer the non-interactive command:
|
|
26
|
+
|
|
19
27
|
```bash
|
|
20
28
|
petadep init --config ./data/config.json
|
|
21
29
|
```
|
|
@@ -39,6 +47,12 @@ petadep add --config ./data/config.json \
|
|
|
39
47
|
petadep agent --config ./data/config.json
|
|
40
48
|
```
|
|
41
49
|
|
|
50
|
+
To install/use PM2 and run the agent under it:
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
petadep agent --config ./data/config.json --pm2
|
|
54
|
+
```
|
|
55
|
+
|
|
42
56
|
The server exposes:
|
|
43
57
|
|
|
44
58
|
- `POST /webhook` (or `config.path`)
|
|
@@ -63,6 +77,7 @@ The agent clones via SSH using `git@github.com:owner/repo.git`. Your VPS must ha
|
|
|
63
77
|
"port": 8787,
|
|
64
78
|
"path": "/webhook",
|
|
65
79
|
"secret": "<random>",
|
|
80
|
+
"sshKeyPath": "/root/.ssh/petadep_ed25519",
|
|
66
81
|
"deployments": [
|
|
67
82
|
{
|
|
68
83
|
"repo": "owner/repo",
|
|
@@ -76,3 +91,5 @@ The agent clones via SSH using `git@github.com:owner/repo.git`. Your VPS must ha
|
|
|
76
91
|
"logsDir": "./logs"
|
|
77
92
|
}
|
|
78
93
|
```
|
|
94
|
+
|
|
95
|
+
`sshKeyPath` is optional and forces git to use that SSH key for clone/fetch.
|
package/bin/petadep.js
CHANGED
|
@@ -1,12 +1,23 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import yargs from "yargs";
|
|
3
3
|
import { hideBin } from "yargs/helpers";
|
|
4
|
-
import {
|
|
4
|
+
import { execa } from "execa";
|
|
5
|
+
import path from "path";
|
|
6
|
+
import { fileURLToPath } from "url";
|
|
7
|
+
import { initConfig, initConfigInteractive, addDeployment } from "../src/cli.js";
|
|
5
8
|
import { startAgent } from "../src/agent.js";
|
|
6
9
|
|
|
7
10
|
async function main() {
|
|
8
11
|
yargs(hideBin(process.argv))
|
|
9
12
|
.scriptName("petadep")
|
|
13
|
+
.command(
|
|
14
|
+
"$0",
|
|
15
|
+
"Interactive setup",
|
|
16
|
+
() => {},
|
|
17
|
+
async () => {
|
|
18
|
+
await initConfigInteractive();
|
|
19
|
+
}
|
|
20
|
+
)
|
|
10
21
|
.command(
|
|
11
22
|
"init",
|
|
12
23
|
"Initialize a config file",
|
|
@@ -70,21 +81,61 @@ async function main() {
|
|
|
70
81
|
"agent",
|
|
71
82
|
"Start the webhook server",
|
|
72
83
|
(y) =>
|
|
73
|
-
y
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
84
|
+
y
|
|
85
|
+
.option("config", {
|
|
86
|
+
type: "string",
|
|
87
|
+
demandOption: true,
|
|
88
|
+
describe: "Path to config JSON",
|
|
89
|
+
})
|
|
90
|
+
.option("pm2", {
|
|
91
|
+
type: "boolean",
|
|
92
|
+
default: false,
|
|
93
|
+
describe: "Install/use pm2 and run the agent under it",
|
|
94
|
+
}),
|
|
78
95
|
async (argv) => {
|
|
96
|
+
if (argv.pm2) {
|
|
97
|
+
await startWithPm2({ configPath: argv.config });
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
79
100
|
await startAgent({ configPath: argv.config });
|
|
80
101
|
}
|
|
81
102
|
)
|
|
82
|
-
.demandCommand(1, "Choose a command")
|
|
83
103
|
.strict()
|
|
84
104
|
.help()
|
|
85
105
|
.parse();
|
|
86
106
|
}
|
|
87
107
|
|
|
108
|
+
async function ensurePm2() {
|
|
109
|
+
try {
|
|
110
|
+
await execa("pm2", ["-v"], { stdio: "ignore" });
|
|
111
|
+
return;
|
|
112
|
+
} catch {
|
|
113
|
+
await execa("npm", ["i", "-g", "pm2"], { stdio: "inherit" });
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async function startWithPm2({ configPath }) {
|
|
118
|
+
await ensurePm2();
|
|
119
|
+
const name = "petadep";
|
|
120
|
+
const resolvedConfig = path.resolve(configPath);
|
|
121
|
+
const binPath = fileURLToPath(import.meta.url);
|
|
122
|
+
|
|
123
|
+
try {
|
|
124
|
+
await execa("pm2", ["start", binPath, "--name", name, "--", "agent", "--config", resolvedConfig], {
|
|
125
|
+
stdio: "inherit",
|
|
126
|
+
});
|
|
127
|
+
} catch (err) {
|
|
128
|
+
await execa("pm2", ["restart", name], { stdio: "inherit" });
|
|
129
|
+
console.log(
|
|
130
|
+
`PM2 already had '${name}'. Restarted it; if you changed config path, run 'pm2 delete ${name}' then rerun.`
|
|
131
|
+
);
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
await execa("pm2", ["save"], { stdio: "inherit" }).catch(() => {});
|
|
136
|
+
console.log(`PM2 running. Use 'pm2 status' to check.`);
|
|
137
|
+
}
|
|
138
|
+
|
|
88
139
|
main().catch((err) => {
|
|
89
140
|
console.error(err?.message || err);
|
|
90
141
|
process.exit(1);
|
package/package.json
CHANGED
package/src/agent.js
CHANGED
package/src/cli.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from "fs/promises";
|
|
2
2
|
import path from "path";
|
|
3
3
|
import crypto from "crypto";
|
|
4
|
+
import readline from "readline";
|
|
4
5
|
|
|
5
6
|
function defaultConfig(secret) {
|
|
6
7
|
return {
|
|
@@ -13,6 +14,7 @@ function defaultConfig(secret) {
|
|
|
13
14
|
lockPerTarget: true,
|
|
14
15
|
timeoutSeconds: 900,
|
|
15
16
|
},
|
|
17
|
+
sshKeyPath: undefined,
|
|
16
18
|
logsDir: "./logs",
|
|
17
19
|
};
|
|
18
20
|
}
|
|
@@ -37,6 +39,128 @@ export async function initConfig({ configPath }) {
|
|
|
37
39
|
);
|
|
38
40
|
}
|
|
39
41
|
|
|
42
|
+
function createInterface() {
|
|
43
|
+
return readline.createInterface({
|
|
44
|
+
input: process.stdin,
|
|
45
|
+
output: process.stdout,
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const colors = {
|
|
50
|
+
reset: "\u001b[0m",
|
|
51
|
+
bold: "\u001b[1m",
|
|
52
|
+
cyan: "\u001b[36m",
|
|
53
|
+
green: "\u001b[32m",
|
|
54
|
+
yellow: "\u001b[33m",
|
|
55
|
+
magenta: "\u001b[35m",
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
function color(text, code) {
|
|
59
|
+
return `${code}${text}${colors.reset}`;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function prompt(rl, question, defaultValue) {
|
|
63
|
+
const suffix = defaultValue ? ` (${defaultValue})` : "";
|
|
64
|
+
return new Promise((resolve) => {
|
|
65
|
+
rl.question(`${question}${suffix}: `, (answer) => {
|
|
66
|
+
const trimmed = answer.trim();
|
|
67
|
+
resolve(trimmed.length ? trimmed : defaultValue);
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function promptHidden(question) {
|
|
73
|
+
return new Promise((resolve) => {
|
|
74
|
+
const input = process.stdin;
|
|
75
|
+
const output = process.stdout;
|
|
76
|
+
let value = "";
|
|
77
|
+
|
|
78
|
+
output.write(`${question}: `);
|
|
79
|
+
input.setRawMode(true);
|
|
80
|
+
input.resume();
|
|
81
|
+
|
|
82
|
+
function onData(chunk) {
|
|
83
|
+
const char = chunk.toString("utf8");
|
|
84
|
+
if (char === "\r" || char === "\n") {
|
|
85
|
+
input.setRawMode(false);
|
|
86
|
+
input.pause();
|
|
87
|
+
input.removeListener("data", onData);
|
|
88
|
+
output.write("\n");
|
|
89
|
+
resolve(value.trim());
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
if (char === "\u0003") {
|
|
93
|
+
process.exit(1);
|
|
94
|
+
}
|
|
95
|
+
if (char === "\u007f") {
|
|
96
|
+
value = value.slice(0, -1);
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
value += char;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
input.on("data", onData);
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async function promptRequired(rl, question) {
|
|
107
|
+
let value = "";
|
|
108
|
+
while (!value) {
|
|
109
|
+
const answer = await prompt(rl, question, "");
|
|
110
|
+
value = answer?.trim() || "";
|
|
111
|
+
if (!value) {
|
|
112
|
+
console.log(color("This value is required. Try again.", colors.yellow));
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return value;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export async function initConfigInteractive() {
|
|
119
|
+
const rl = createInterface();
|
|
120
|
+
try {
|
|
121
|
+
console.log(color("petadep setup", colors.bold + colors.magenta));
|
|
122
|
+
console.log(color("Let's create your webhook config.", colors.cyan));
|
|
123
|
+
const port = await prompt(rl, "Port", "8787");
|
|
124
|
+
const hookPath = await prompt(rl, "Webhook path", "/webhook");
|
|
125
|
+
const secretInput = await promptHidden("Secret (leave blank to auto-generate)");
|
|
126
|
+
const repo = await promptRequired(rl, "Repo (owner/repo)");
|
|
127
|
+
const branch = await prompt(rl, "Branch", "main");
|
|
128
|
+
const env = await prompt(rl, "Env name", "production");
|
|
129
|
+
const workdir = await promptRequired(rl, "Workdir");
|
|
130
|
+
const script = await prompt(rl, "Script path", "./deploy.sh");
|
|
131
|
+
const logsDir = await prompt(rl, "Logs dir", "./logs");
|
|
132
|
+
const sshKeyPath = await prompt(rl, "SSH key path (optional)", "");
|
|
133
|
+
const savePath = await prompt(rl, "Save config to", "./config.json");
|
|
134
|
+
|
|
135
|
+
const secret =
|
|
136
|
+
secretInput && secretInput.length > 0
|
|
137
|
+
? secretInput
|
|
138
|
+
: crypto.randomBytes(32).toString("hex");
|
|
139
|
+
|
|
140
|
+
const config = defaultConfig(secret);
|
|
141
|
+
config.port = Number(port);
|
|
142
|
+
config.path = hookPath;
|
|
143
|
+
config.logsDir = logsDir;
|
|
144
|
+
if (sshKeyPath) {
|
|
145
|
+
config.sshKeyPath = sshKeyPath;
|
|
146
|
+
} else {
|
|
147
|
+
delete config.sshKeyPath;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
config.deployments = [{ repo, branch, env, workdir, script }];
|
|
151
|
+
|
|
152
|
+
const resolvedPath = path.resolve(savePath);
|
|
153
|
+
await fs.mkdir(path.dirname(resolvedPath), { recursive: true });
|
|
154
|
+
await fs.writeFile(resolvedPath, JSON.stringify(config, null, 2));
|
|
155
|
+
|
|
156
|
+
console.log(color("Config created:", colors.green), resolvedPath);
|
|
157
|
+
console.log(color("Webhook secret:", colors.green), secret);
|
|
158
|
+
console.log(color("Start agent:", colors.cyan), `petadep agent --config ${resolvedPath}`);
|
|
159
|
+
} finally {
|
|
160
|
+
rl.close();
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
40
164
|
export async function addDeployment({
|
|
41
165
|
configPath,
|
|
42
166
|
repo,
|
package/src/deployer.js
CHANGED
|
@@ -14,6 +14,14 @@ function toIsoLine(message) {
|
|
|
14
14
|
return `[${new Date().toISOString()}] ${message}\n`;
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
+
function buildGitEnv(config) {
|
|
18
|
+
if (!config?.sshKeyPath) {
|
|
19
|
+
return {};
|
|
20
|
+
}
|
|
21
|
+
const sshCommand = `ssh -i ${config.sshKeyPath} -o IdentitiesOnly=yes`;
|
|
22
|
+
return { GIT_SSH_COMMAND: sshCommand };
|
|
23
|
+
}
|
|
24
|
+
|
|
17
25
|
async function runCommand(command, args, options, logStream) {
|
|
18
26
|
const child = execa(command, args, options);
|
|
19
27
|
if (child.stdout) {
|
|
@@ -40,25 +48,32 @@ async function ensureRepo(target, config, logStream, timeoutMs) {
|
|
|
40
48
|
logPathForTarget(config, target),
|
|
41
49
|
toIsoLine(`Cloning ${sshUrl} into ${target.workdir}`)
|
|
42
50
|
);
|
|
51
|
+
const env = buildGitEnv(config);
|
|
43
52
|
await runCommand(
|
|
44
53
|
"git",
|
|
45
54
|
["clone", sshUrl, target.workdir],
|
|
46
|
-
{ timeout: timeoutMs },
|
|
55
|
+
{ timeout: timeoutMs, env: { ...process.env, ...env } },
|
|
47
56
|
logStream
|
|
48
57
|
);
|
|
49
58
|
}
|
|
50
59
|
}
|
|
51
60
|
|
|
52
|
-
async function checkoutBranch(target, logStream, timeoutMs) {
|
|
61
|
+
async function checkoutBranch(target, config, logStream, timeoutMs) {
|
|
53
62
|
const cwd = target.workdir;
|
|
54
|
-
|
|
63
|
+
const env = buildGitEnv(config);
|
|
64
|
+
await runCommand(
|
|
65
|
+
"git",
|
|
66
|
+
["fetch", "--all", "--prune"],
|
|
67
|
+
{ cwd, timeout: timeoutMs, env: { ...process.env, ...env } },
|
|
68
|
+
logStream
|
|
69
|
+
);
|
|
55
70
|
|
|
56
71
|
let hasBranch = true;
|
|
57
72
|
try {
|
|
58
73
|
await runCommand(
|
|
59
74
|
"git",
|
|
60
75
|
["rev-parse", "--verify", target.branch],
|
|
61
|
-
{ cwd, timeout: timeoutMs },
|
|
76
|
+
{ cwd, timeout: timeoutMs, env: { ...process.env, ...env } },
|
|
62
77
|
logStream
|
|
63
78
|
);
|
|
64
79
|
} catch {
|
|
@@ -69,14 +84,14 @@ async function checkoutBranch(target, logStream, timeoutMs) {
|
|
|
69
84
|
await runCommand(
|
|
70
85
|
"git",
|
|
71
86
|
["checkout", target.branch],
|
|
72
|
-
{ cwd, timeout: timeoutMs },
|
|
87
|
+
{ cwd, timeout: timeoutMs, env: { ...process.env, ...env } },
|
|
73
88
|
logStream
|
|
74
89
|
);
|
|
75
90
|
} else {
|
|
76
91
|
await runCommand(
|
|
77
92
|
"git",
|
|
78
93
|
["checkout", "-b", target.branch, `origin/${target.branch}`],
|
|
79
|
-
{ cwd, timeout: timeoutMs },
|
|
94
|
+
{ cwd, timeout: timeoutMs, env: { ...process.env, ...env } },
|
|
80
95
|
logStream
|
|
81
96
|
);
|
|
82
97
|
}
|
|
@@ -84,7 +99,7 @@ async function checkoutBranch(target, logStream, timeoutMs) {
|
|
|
84
99
|
await runCommand(
|
|
85
100
|
"git",
|
|
86
101
|
["reset", "--hard", `origin/${target.branch}`],
|
|
87
|
-
{ cwd, timeout: timeoutMs },
|
|
102
|
+
{ cwd, timeout: timeoutMs, env: { ...process.env, ...env } },
|
|
88
103
|
logStream
|
|
89
104
|
);
|
|
90
105
|
}
|
|
@@ -109,7 +124,7 @@ export async function deployTarget(target, config) {
|
|
|
109
124
|
const logStream = fs.createWriteStream(logFile, { flags: "a" });
|
|
110
125
|
try {
|
|
111
126
|
await ensureRepo(target, config, logStream, timeoutMs);
|
|
112
|
-
await checkoutBranch(target, logStream, timeoutMs);
|
|
127
|
+
await checkoutBranch(target, config, logStream, timeoutMs);
|
|
113
128
|
await runScript(target, logStream, timeoutMs);
|
|
114
129
|
await appendLog(logFile, toIsoLine("Deployment finished"));
|
|
115
130
|
} catch (err) {
|