@waron97/prbot 1.1.0 → 2.0.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/.claude/settings.local.json +5 -0
- package/CLAUDE.md +0 -0
- package/README.md +128 -0
- package/index.js +135 -79
- package/package.json +5 -3
- package/src/commands/autopr.js +309 -0
- package/src/commands/changelog.js +189 -0
- package/src/commands/init.js +144 -0
- package/src/commands/pr.js +124 -0
- package/src/commands/ver.js +83 -0
- package/src/config.js +7 -0
- package/src/index.js +106 -0
- package/src/lib/addons.js +8 -0
- package/src/lib/git.js +12 -0
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import fetch from "node-fetch";
|
|
2
|
+
import fs from "fs/promises";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import { execFile } from "child_process";
|
|
5
|
+
import { resolveAddonsPath } from "../lib/addons.js";
|
|
6
|
+
|
|
7
|
+
async function getToken() {
|
|
8
|
+
const url = process.env.KC_URL;
|
|
9
|
+
const payload = new URLSearchParams();
|
|
10
|
+
|
|
11
|
+
payload.append("username", process.env.KC_USER);
|
|
12
|
+
payload.append("password", process.env.KC_PASSWORD);
|
|
13
|
+
payload.append("client_id", process.env.KC_ID);
|
|
14
|
+
payload.append("client_secret", process.env.KC_SECRET);
|
|
15
|
+
payload.append("grant_type", "password");
|
|
16
|
+
|
|
17
|
+
const response = await fetch(url, {
|
|
18
|
+
method: "POST",
|
|
19
|
+
headers: {
|
|
20
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
21
|
+
},
|
|
22
|
+
body: payload.toString(),
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
const json = await response.json();
|
|
26
|
+
return json.access_token;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async function getFiles(module_name, token) {
|
|
30
|
+
const url = `${process.env.RIP_URL}/ir.model/xml_prbot`;
|
|
31
|
+
const body = JSON.stringify({ module_name });
|
|
32
|
+
const headers = {
|
|
33
|
+
Authorization: `Bearer ${token}`,
|
|
34
|
+
"Content-Type": "application/json",
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const response = await fetch(url, { method: "POST", body, headers });
|
|
38
|
+
if (!response.ok) {
|
|
39
|
+
throw new Error(await response.text());
|
|
40
|
+
}
|
|
41
|
+
return await response.json();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async function main(module_name) {
|
|
45
|
+
const token = await getToken();
|
|
46
|
+
const files = await getFiles(module_name, token);
|
|
47
|
+
|
|
48
|
+
let ADDONS_PATH = resolveAddonsPath(process.env.ADDONS_PATH);
|
|
49
|
+
|
|
50
|
+
for (const file of files) {
|
|
51
|
+
const buffer = Buffer.from(file.data, "base64");
|
|
52
|
+
let content = buffer.toString();
|
|
53
|
+
const lines = content.split("\n");
|
|
54
|
+
|
|
55
|
+
const odooCloseIndex = lines.findIndex((l) => l.trim() === "</odoo>");
|
|
56
|
+
if (odooCloseIndex !== -1 && odooCloseIndex < lines.length - 1) {
|
|
57
|
+
const footer = lines.slice(odooCloseIndex + 1).join("\n");
|
|
58
|
+
const skippedMatch = footer.match(/Skipped records:\s*(\d+)/);
|
|
59
|
+
if (skippedMatch && parseInt(skippedMatch[1]) > 0) {
|
|
60
|
+
throw new Error(
|
|
61
|
+
`[${file.name}] Export contains skipped records:\n${footer.trim()}`,
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
const duplicatedMatch = footer.match(/Duplicated records:\s*(\d+)/);
|
|
65
|
+
if (duplicatedMatch && parseInt(duplicatedMatch[1]) > 0) {
|
|
66
|
+
throw new Error(
|
|
67
|
+
`[${file.name}] Export contains duplicated records:\n${footer.trim()}`,
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (lines.length > 2) {
|
|
73
|
+
lines.splice(-2);
|
|
74
|
+
}
|
|
75
|
+
content = lines.join("\n");
|
|
76
|
+
content = content.replace(
|
|
77
|
+
/<field name="bpmn_diagram"><!\[CDATA\[[\s\S]*?\]\]><\/field>/g,
|
|
78
|
+
"",
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
let destPath;
|
|
82
|
+
if (file.name.includes("Relazioni mancanti")) {
|
|
83
|
+
destPath = `${ADDONS_PATH}/config/${module_name}/data/workflow_missing_relations.xml`;
|
|
84
|
+
} else {
|
|
85
|
+
destPath = `${ADDONS_PATH}/config/${module_name}/data/workflow_configuration.xml`;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
await fs.mkdir(path.dirname(destPath), { recursive: true });
|
|
89
|
+
await fs.writeFile(destPath, content);
|
|
90
|
+
console.log(`Processed: ${file.name} -> ${destPath}`);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const workflowDir = path.join(ADDONS_PATH, "config", module_name, "data");
|
|
94
|
+
const filesToAdd = [
|
|
95
|
+
path.join(workflowDir, "workflow_missing_relations.xml"),
|
|
96
|
+
path.join(workflowDir, "workflow_configuration.xml"),
|
|
97
|
+
];
|
|
98
|
+
|
|
99
|
+
for (const filePath of filesToAdd) {
|
|
100
|
+
await new Promise((resolve, reject) => {
|
|
101
|
+
execFile("git", ["add", filePath], { cwd: ADDONS_PATH }, (error) => {
|
|
102
|
+
if (error) reject(error);
|
|
103
|
+
else resolve();
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const commitMessage = `[IMP][${module_name}] Update workflow`;
|
|
109
|
+
await new Promise((resolve, reject) => {
|
|
110
|
+
execFile(
|
|
111
|
+
"git",
|
|
112
|
+
["commit", "-m", commitMessage],
|
|
113
|
+
{ cwd: ADDONS_PATH },
|
|
114
|
+
(error) => {
|
|
115
|
+
if (error) reject(error);
|
|
116
|
+
else resolve();
|
|
117
|
+
},
|
|
118
|
+
);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
console.log(`Committed with message: ${commitMessage}`);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export { main };
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import fs from "fs/promises";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { execFile } from "child_process";
|
|
4
|
+
import { resolveAddonsPath } from "../lib/addons.js";
|
|
5
|
+
|
|
6
|
+
async function verbot(module_name, level) {
|
|
7
|
+
if (!["major", "minor", "patch"].includes(level)) {
|
|
8
|
+
throw new Error("Level must be one of major, minor, patch");
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
let ADDONS_PATH = resolveAddonsPath(process.env.ADDONS_PATH);
|
|
12
|
+
|
|
13
|
+
let manifestPath = path.join(ADDONS_PATH, module_name, "__manifest__.py");
|
|
14
|
+
try {
|
|
15
|
+
await fs.access(manifestPath);
|
|
16
|
+
} catch {
|
|
17
|
+
manifestPath = path.join(
|
|
18
|
+
ADDONS_PATH,
|
|
19
|
+
"config",
|
|
20
|
+
module_name,
|
|
21
|
+
"__manifest__.py",
|
|
22
|
+
);
|
|
23
|
+
try {
|
|
24
|
+
await fs.access(manifestPath);
|
|
25
|
+
} catch {
|
|
26
|
+
throw new Error(`__manifest__.py not found for module ${module_name}`);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const content = await fs.readFile(manifestPath, "utf-8");
|
|
31
|
+
const versionMatch = content.match(/"version":\s*"(15\.0\.\d+\.\d+\.\d+)"/);
|
|
32
|
+
if (!versionMatch) {
|
|
33
|
+
throw new Error("Version not found in manifest");
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const currentVersion = versionMatch[1];
|
|
37
|
+
const parts = currentVersion.split(".");
|
|
38
|
+
const base = `${parts[0]}.${parts[1]}`;
|
|
39
|
+
const major = parseInt(parts[2]);
|
|
40
|
+
const minor = parseInt(parts[3]);
|
|
41
|
+
const patch = parseInt(parts[4]);
|
|
42
|
+
|
|
43
|
+
let newVersion;
|
|
44
|
+
if (level === "patch") {
|
|
45
|
+
newVersion = `${base}.${major}.${minor}.${patch + 1}`;
|
|
46
|
+
} else if (level === "minor") {
|
|
47
|
+
newVersion = `${base}.${major}.${minor + 1}.0`;
|
|
48
|
+
} else if (level === "major") {
|
|
49
|
+
newVersion = `${base}.${major + 1}.0.0`;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const newContent = content.replace(
|
|
53
|
+
`"version": "${currentVersion}"`,
|
|
54
|
+
`"version": "${newVersion}"`,
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
await fs.writeFile(manifestPath, newContent);
|
|
58
|
+
console.log(`Updated version: ${currentVersion} -> ${newVersion}`);
|
|
59
|
+
|
|
60
|
+
await new Promise((resolve, reject) => {
|
|
61
|
+
execFile("git", ["add", manifestPath], { cwd: ADDONS_PATH }, (error) => {
|
|
62
|
+
if (error) reject(error);
|
|
63
|
+
else resolve();
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
const commitMessage = `[VER][${module_name}] Bump`;
|
|
68
|
+
await new Promise((resolve, reject) => {
|
|
69
|
+
execFile(
|
|
70
|
+
"git",
|
|
71
|
+
["commit", "-m", commitMessage],
|
|
72
|
+
{ cwd: ADDONS_PATH },
|
|
73
|
+
(error) => {
|
|
74
|
+
if (error) reject(error);
|
|
75
|
+
else resolve();
|
|
76
|
+
},
|
|
77
|
+
);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
console.log(`Committed with message: ${commitMessage}`);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export { verbot };
|
package/src/config.js
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import path from "path";
|
|
2
|
+
|
|
3
|
+
const CONFIG_DIR = path.join(process.env.HOME || "", ".config", "prbot");
|
|
4
|
+
const CONFIG_FILE = path.join(CONFIG_DIR, "config");
|
|
5
|
+
const COMPLETION_SCRIPT = path.join(CONFIG_DIR, "completion.sh");
|
|
6
|
+
|
|
7
|
+
export { CONFIG_DIR, CONFIG_FILE, COMPLETION_SCRIPT };
|
package/src/index.js
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { configDotenv } from "dotenv";
|
|
4
|
+
import { readdirSync, readFileSync } from "fs";
|
|
5
|
+
import path from "path";
|
|
6
|
+
import { program } from "commander";
|
|
7
|
+
import omelette from "omelette";
|
|
8
|
+
import { CONFIG_FILE, COMPLETION_SCRIPT } from "./config.js";
|
|
9
|
+
import { main as prMain } from "./commands/pr.js";
|
|
10
|
+
import { verbot } from "./commands/ver.js";
|
|
11
|
+
import { init } from "./commands/init.js";
|
|
12
|
+
import { changelog } from "./commands/changelog.js";
|
|
13
|
+
import { autopr } from "./commands/autopr.js";
|
|
14
|
+
|
|
15
|
+
const completion = omelette("prbot <command> <module>");
|
|
16
|
+
completion.on("command", ({ reply }) => {
|
|
17
|
+
reply(["pr", "ver", "init", "changelog", "autopr"]);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
completion.on("module", ({ before, reply }) => {
|
|
21
|
+
if (["init", "changelog", "autopr"].includes(before)) {
|
|
22
|
+
reply([]);
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
try {
|
|
26
|
+
const raw = readFileSync(CONFIG_FILE, "utf-8");
|
|
27
|
+
const match = raw.match(/^ADDONS_PATH=(.+)$/m);
|
|
28
|
+
if (!match) {
|
|
29
|
+
reply([]);
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
const addonsPath = match[1].trim().replace(/^~/, process.env.HOME || "");
|
|
33
|
+
reply(readdirSync(path.join(addonsPath, "config")));
|
|
34
|
+
} catch {
|
|
35
|
+
reply([]);
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
completion.init();
|
|
40
|
+
|
|
41
|
+
const isCompletionMode =
|
|
42
|
+
process.argv.includes("--compbash") || process.argv.includes("--compzsh");
|
|
43
|
+
|
|
44
|
+
if (!isCompletionMode) {
|
|
45
|
+
configDotenv({ path: CONFIG_FILE });
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
program
|
|
49
|
+
.command("pr <module>")
|
|
50
|
+
.option("-b, --bump <level>")
|
|
51
|
+
.action((module, opts) => {
|
|
52
|
+
prMain(module)
|
|
53
|
+
.then(() => {
|
|
54
|
+
if (opts.bump) {
|
|
55
|
+
return verbot(module, opts.bump);
|
|
56
|
+
}
|
|
57
|
+
})
|
|
58
|
+
.catch((err) => {
|
|
59
|
+
throw err;
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
program
|
|
64
|
+
.command("ver <module>")
|
|
65
|
+
.option("-b, --bump <level>")
|
|
66
|
+
.action((module, opts) => {
|
|
67
|
+
if (!opts.bump) {
|
|
68
|
+
throw new Error("No bump level specified");
|
|
69
|
+
}
|
|
70
|
+
verbot(module, opts.bump);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
program
|
|
74
|
+
.command("init")
|
|
75
|
+
.description("Create config file and install shell completion")
|
|
76
|
+
.action(() => {
|
|
77
|
+
init(completion);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
const collect = (val, prev) => [...(prev ?? []), val];
|
|
81
|
+
|
|
82
|
+
program
|
|
83
|
+
.command("changelog <pr>")
|
|
84
|
+
.option("-t, --trident <code>", "Trident issue codes (repeatable)", collect)
|
|
85
|
+
.option("-j, --jira <code>", "JIRA issue codes (repeatable)", collect)
|
|
86
|
+
.option("-m, --message <text>", "Changelog entry message")
|
|
87
|
+
.action((prNumber, opts) => {
|
|
88
|
+
changelog(prNumber, opts).catch((err) => {
|
|
89
|
+
throw err;
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
program
|
|
94
|
+
.command("autopr")
|
|
95
|
+
.option("-t, --trident <id>", "Trident task IDs (repeatable)", collect)
|
|
96
|
+
.option("-j, --jira <code>", "JIRA issue codes (repeatable)", collect)
|
|
97
|
+
.option("-m, --message <text>", "Changelog entry message")
|
|
98
|
+
.option("-b, --branch <name>", "Branch name (default: autopr_<taskId>)")
|
|
99
|
+
.option("-n, --name <text>", "PR title (default: task name from Odoo)")
|
|
100
|
+
.action((opts) => {
|
|
101
|
+
autopr(opts).catch((err) => {
|
|
102
|
+
throw err;
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
program.parse();
|
package/src/lib/git.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { execFile } from "child_process";
|
|
2
|
+
|
|
3
|
+
function execGit(args, cwd) {
|
|
4
|
+
return new Promise((resolve, reject) => {
|
|
5
|
+
execFile("git", args, { cwd }, (error, stdout) => {
|
|
6
|
+
if (error) reject(error);
|
|
7
|
+
else resolve(stdout);
|
|
8
|
+
});
|
|
9
|
+
});
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export { execGit };
|