cispacempt-v2 0.0.1
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/.codespacempt/origin +1 -0
- package/.csmptignore +3 -0
- package/README.md +108 -0
- package/package.json +20 -0
- package/src/extension.js +31 -0
- package/src/index.js +199 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
codespacempt_ci
|
package/.csmptignore
ADDED
package/README.md
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# CodespaceMPT CI
|
|
2
|
+
|
|
3
|
+
**CodespaceMPT CI** (CISpaceMPT) is a fast, efficient, and scalable Continuous Integration (CI) server that automates the execution of scripts in various stages of your CI pipeline. It is designed to integrate seamlessly with your repository, run defined commands, and log detailed results.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
## How It Works
|
|
8
|
+
|
|
9
|
+
### CI Script Structure
|
|
10
|
+
|
|
11
|
+
Your CI pipeline is defined in a JSON-based script. Each CI script contains several stages, each with its own set of commands to run. Below is an example of how to structure the CI script:
|
|
12
|
+
|
|
13
|
+
```json
|
|
14
|
+
{
|
|
15
|
+
"stages": {
|
|
16
|
+
"build": {
|
|
17
|
+
"scripts": [
|
|
18
|
+
"npm install",
|
|
19
|
+
"npm run build"
|
|
20
|
+
]
|
|
21
|
+
},
|
|
22
|
+
"test": {
|
|
23
|
+
"scripts": [
|
|
24
|
+
"npm test"
|
|
25
|
+
]
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
- **stages**: Defines the different stages of the CI pipeline.
|
|
32
|
+
- **scripts**: Lists the commands to be executed in each stage.
|
|
33
|
+
|
|
34
|
+
### Execution Flow
|
|
35
|
+
|
|
36
|
+
1. **Clone Repository**: The CI server clones the repository and checks out the required branch.
|
|
37
|
+
2. **Parse Directory**: The file system of the repository is recursively traversed (excluding `node_modules`), capturing the directory structure and content.
|
|
38
|
+
3. **Stage Execution**: For each stage defined in the CI script, the server executes the specified commands sequentially.
|
|
39
|
+
4. **Log Results**: The output (stdout, stderr) from each command is captured and logged along with any errors and exit codes.
|
|
40
|
+
5. **Time Tracking**: The duration of the CI process is calculated and logged.
|
|
41
|
+
6. **Clean-up**: Once the pipeline is complete, temporary directories are deleted.
|
|
42
|
+
|
|
43
|
+
### Example Usage
|
|
44
|
+
|
|
45
|
+
Here’s how you can use the `parse` function to execute a CI script:
|
|
46
|
+
|
|
47
|
+
```js
|
|
48
|
+
const ci = require('codespacempt-ci');
|
|
49
|
+
|
|
50
|
+
// Sample CI script
|
|
51
|
+
const ciScript = {
|
|
52
|
+
stages: {
|
|
53
|
+
"build": {
|
|
54
|
+
scripts: [
|
|
55
|
+
"npm install",
|
|
56
|
+
"npm run build"
|
|
57
|
+
]
|
|
58
|
+
},
|
|
59
|
+
"test": {
|
|
60
|
+
scripts: [
|
|
61
|
+
"npm test"
|
|
62
|
+
]
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
// Repository name
|
|
68
|
+
const repoName = "my-repo";
|
|
69
|
+
|
|
70
|
+
// File system content to be used during execution
|
|
71
|
+
const fileSystem = {/* your repository's file system in a json */};
|
|
72
|
+
|
|
73
|
+
ci.parse(ciScript, repoName, fileSystem)
|
|
74
|
+
.then(result => {
|
|
75
|
+
console.log("CI Log Result:", result);
|
|
76
|
+
})
|
|
77
|
+
.catch(err => {
|
|
78
|
+
console.error("CI process failed:", err);
|
|
79
|
+
});
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### Output
|
|
83
|
+
|
|
84
|
+
The `parse` function returns an object containing the CI process result:
|
|
85
|
+
|
|
86
|
+
```json
|
|
87
|
+
{
|
|
88
|
+
"output": "CI logs for each stage",
|
|
89
|
+
"timestamp": "2025-04-14 12:00:00",
|
|
90
|
+
"fileSys": { /* Parsed file system after ci */ },
|
|
91
|
+
"ci_duration": "10.23", // Duration in seconds
|
|
92
|
+
"exitCode": 0 // Exit code (0 for success, non-zero for failure)
|
|
93
|
+
}
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
- **output**: Logs from each stage.
|
|
97
|
+
- **timestamp**: The timestamp of when the CI process was run.
|
|
98
|
+
- **fileSys**: A JSON object representing the repository’s file structure.
|
|
99
|
+
- **ci_duration**: Total time taken for the CI process (in seconds).
|
|
100
|
+
- **exitCode**: The exit code from the final stage (0 if all commands succeed).
|
|
101
|
+
|
|
102
|
+
### Clean-Up
|
|
103
|
+
|
|
104
|
+
After the execution, temporary directories are automatically cleaned up to save disk space.
|
|
105
|
+
|
|
106
|
+
## License
|
|
107
|
+
|
|
108
|
+
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
package/package.json
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"main": "src/index.js",
|
|
3
|
+
"name": "cispacempt-v2",
|
|
4
|
+
"version": "0.0.1",
|
|
5
|
+
"description": "Fast CI engine for CodespaceMPT with script stages and file system hooks",
|
|
6
|
+
"author": "argentidev",
|
|
7
|
+
"license": "MIT",
|
|
8
|
+
"homepage": "https://codespacemptio.onrender.com/home",
|
|
9
|
+
"scripts": {
|
|
10
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
11
|
+
},
|
|
12
|
+
"repository": {
|
|
13
|
+
"type": "csmpt",
|
|
14
|
+
"url": "https://codespacemptio.onrender.com/codespacempt/ci"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [],
|
|
17
|
+
"dependencies": {
|
|
18
|
+
"typescript": "^5.8.3"
|
|
19
|
+
}
|
|
20
|
+
}
|
package/src/extension.js
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// Extension system
|
|
2
|
+
const extensionSystem = {
|
|
3
|
+
extensions: [],
|
|
4
|
+
loadExtension: function (extension) {
|
|
5
|
+
this.extensions.push(extension);
|
|
6
|
+
},
|
|
7
|
+
runPreExecution: function (stageName) {
|
|
8
|
+
this.extensions.forEach((extension) => {
|
|
9
|
+
if (extension.preExecute) {
|
|
10
|
+
extension.preExecute(stageName);
|
|
11
|
+
}
|
|
12
|
+
});
|
|
13
|
+
},
|
|
14
|
+
runPostExecution: function (stageName, results) {
|
|
15
|
+
this.extensions.forEach((extension) => {
|
|
16
|
+
if (extension.postExecute) {
|
|
17
|
+
extension.postExecute(stageName, results);
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
},
|
|
21
|
+
runEnd: function (ciLogResult, stageNames, finalResult) {
|
|
22
|
+
this.extensions.forEach((extension) => {
|
|
23
|
+
if (extension.endExecute) {
|
|
24
|
+
extension.endExecute(ciLogResult, stageNames, finalResult);
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
// Export the extension system
|
|
31
|
+
module.exports.extensionSystem = extensionSystem;
|
package/src/index.js
ADDED
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
const path = require("path"),
|
|
2
|
+
os = require("os"),
|
|
3
|
+
fs = require("fs"),
|
|
4
|
+
{ spawn } = require("child_process"),
|
|
5
|
+
getCPUUsageSnapshot = function () {
|
|
6
|
+
const cpus = os.cpus();
|
|
7
|
+
return cpus.map(cpu => ({ ...cpu.times }));
|
|
8
|
+
},
|
|
9
|
+
calculateCPUUsage = function (startSnapshot, endSnapshot) {
|
|
10
|
+
let idleDiff = 0;
|
|
11
|
+
let totalDiff = 0;
|
|
12
|
+
|
|
13
|
+
for (let i = 0; i < startSnapshot.length; i++) {
|
|
14
|
+
const start = startSnapshot[i];
|
|
15
|
+
const end = endSnapshot[i];
|
|
16
|
+
|
|
17
|
+
const idle = end.idle - start.idle;
|
|
18
|
+
const total = Object.keys(end).reduce((acc, key) => acc + (end[key] - start[key]), 0);
|
|
19
|
+
|
|
20
|
+
idleDiff += idle;
|
|
21
|
+
totalDiff += total;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const usage = 1 - idleDiff / totalDiff;
|
|
25
|
+
return (usage * 100).toFixed(2); // Percentage string
|
|
26
|
+
},
|
|
27
|
+
parseDirectoryToJson = function (directory) {
|
|
28
|
+
function parse(dirPath) {
|
|
29
|
+
let result = {};
|
|
30
|
+
|
|
31
|
+
const walkSync = (dir) => {
|
|
32
|
+
const files = fs.readdirSync(dir);
|
|
33
|
+
const dirs = files.filter((file) =>
|
|
34
|
+
fs.statSync(path.join(dir, file)).isDirectory(),
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
const currentFolder = {};
|
|
38
|
+
|
|
39
|
+
// Excluir el directorio node_modules
|
|
40
|
+
const dirsToProcess = dirs.filter((dirName) => dirName !== 'node_modules');
|
|
41
|
+
|
|
42
|
+
dirsToProcess.forEach((dirName) => {
|
|
43
|
+
const dirPath = path.join(dir, dirName);
|
|
44
|
+
walkSync(dirPath); // Recursively process subdirectories
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
files.forEach((fileName) => {
|
|
48
|
+
const filePath = path.join(dir, fileName);
|
|
49
|
+
if (fs.statSync(filePath).isFile()) {
|
|
50
|
+
const content = fs.readFileSync(filePath, "utf8");
|
|
51
|
+
if (dir === directory) {
|
|
52
|
+
result[fileName] = content; // Add files directly to the root
|
|
53
|
+
} else {
|
|
54
|
+
currentFolder[fileName] = content; // File content as string
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
// Agregar la estructura de carpetas, excepto node_modules
|
|
60
|
+
if (Object.keys(currentFolder).length > 0) {
|
|
61
|
+
const relativePath = path.relative(directory, dir);
|
|
62
|
+
if (relativePath !== 'node_modules') {
|
|
63
|
+
result[relativePath] = currentFolder; // Add folder structure
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
walkSync(dirPath);
|
|
69
|
+
return result;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return parse(directory);
|
|
73
|
+
}, cleanUpAndRespond = (tempDir) => fs.rmSync(tempDir, { recursive: true, force: true }),
|
|
74
|
+
downloadFiles = (fileSystem, baseDir) => {
|
|
75
|
+
Object.keys(fileSystem).forEach((file) => {
|
|
76
|
+
const filePath = path.join(baseDir, file);
|
|
77
|
+
if (typeof fileSystem[file] === "object") {
|
|
78
|
+
if (!fs.existsSync(filePath)) {
|
|
79
|
+
fs.mkdirSync(filePath, { recursive: true });
|
|
80
|
+
}
|
|
81
|
+
downloadFiles(fileSystem[file], filePath);
|
|
82
|
+
} else {
|
|
83
|
+
fs.writeFileSync(filePath, fileSystem[file]);
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
}, { extensionSystem } = require("./extension");
|
|
87
|
+
|
|
88
|
+
// Export as a Node JS module
|
|
89
|
+
module.exports = {
|
|
90
|
+
|
|
91
|
+
parse: async function (ciScript, repoName, fileSystem, tmp = true) {
|
|
92
|
+
const isLinux = os.platform() === "linux";
|
|
93
|
+
const isRoot = process.getuid && process.getuid() === 0;
|
|
94
|
+
let finalResult = [];
|
|
95
|
+
let timeTaken;
|
|
96
|
+
let combinedExitCode = 0;
|
|
97
|
+
let startTime = Date.now();
|
|
98
|
+
const cpuStart = getCPUUsageSnapshot();
|
|
99
|
+
|
|
100
|
+
const convertMsToSec = (ms) => (ms / 1000).toFixed(2);
|
|
101
|
+
|
|
102
|
+
const stages = Object.entries(ciScript.stages).map(([name, data]) => ({ name, ...data }));
|
|
103
|
+
const tempDir = path.join(__dirname, tmp == true ? "tmp/" + repoName : repoName);
|
|
104
|
+
|
|
105
|
+
const substituteEnvVariables = (command, env) => {
|
|
106
|
+
return command.replace(/\${\[(\w+)\]}/g, (_, varName) => {
|
|
107
|
+
return env[varName] ?? '';
|
|
108
|
+
});
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
for (const stage of stages) {
|
|
112
|
+
extensionSystem.runPreExecution(stage.name); // <-- pre execution hook
|
|
113
|
+
|
|
114
|
+
if (stage.scripts && Array.isArray(stage.scripts)) {
|
|
115
|
+
if (!fs.existsSync(tempDir)) {
|
|
116
|
+
fs.mkdirSync(tempDir, { recursive: true });
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
downloadFiles(fileSystem, tempDir);
|
|
120
|
+
|
|
121
|
+
await new Promise((resolve) => {
|
|
122
|
+
(async function (commands, callback) {
|
|
123
|
+
const results = [];
|
|
124
|
+
|
|
125
|
+
for (const command of commands) {
|
|
126
|
+
const resolvedCommand = substituteEnvVariables(command, ciScript.env || {});
|
|
127
|
+
|
|
128
|
+
await new Promise((res) => {
|
|
129
|
+
const commandToRun = resolvedCommand;
|
|
130
|
+
const child = spawn(commandToRun, { cwd: tempDir, shell: true });
|
|
131
|
+
let stdoutData = "";
|
|
132
|
+
let stderrData = "";
|
|
133
|
+
|
|
134
|
+
child.stdout.on("data", (data) => stdoutData += data.toString());
|
|
135
|
+
child.stderr.on("data", (data) => stderrData += data.toString());
|
|
136
|
+
|
|
137
|
+
child.on("close", (code) => {
|
|
138
|
+
results.push({
|
|
139
|
+
command: resolvedCommand,
|
|
140
|
+
stdout: stdoutData,
|
|
141
|
+
stderr: stderrData,
|
|
142
|
+
exitCode: code,
|
|
143
|
+
error: code === 0 ? null : `Command failed with exit code ${code}`,
|
|
144
|
+
});
|
|
145
|
+
res();
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
if (results.at(-1).exitCode !== 0) break;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
callback(results);
|
|
153
|
+
extensionSystem.runPostExecution(stage.name, results); // <-- post execution hook
|
|
154
|
+
resolve();
|
|
155
|
+
})(stage.scripts, (results) => {
|
|
156
|
+
let output = `[${stage.name}]\n\nResults:\n`;
|
|
157
|
+
|
|
158
|
+
results.forEach((result) => {
|
|
159
|
+
output += `$ ${result.command}\n`;
|
|
160
|
+
if (result.error) {
|
|
161
|
+
output += `Error: ${result.error}\n`;
|
|
162
|
+
output += `${result.stderr ? result.stderr : ''}\n`;
|
|
163
|
+
} else {
|
|
164
|
+
output += `${result.stdout ? result.stdout : ''}\n`;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (result.exitCode !== 0) {
|
|
168
|
+
combinedExitCode = result.exitCode;
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
finalResult.push(output);
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const finalResultEndTime = Date.now();
|
|
179
|
+
timeTaken = convertMsToSec(finalResultEndTime - startTime);
|
|
180
|
+
const cpuEnd = getCPUUsageSnapshot();
|
|
181
|
+
|
|
182
|
+
const ciLogResult = {
|
|
183
|
+
output: finalResult.join(''),
|
|
184
|
+
timestamp: String(new Date().toString()),
|
|
185
|
+
fileSys: parseDirectoryToJson(tempDir),
|
|
186
|
+
ci_duration: timeTaken,
|
|
187
|
+
exitCode: combinedExitCode,
|
|
188
|
+
cpu_usage: calculateCPUUsage(cpuStart, cpuEnd),
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
cleanUpAndRespond(tempDir);
|
|
192
|
+
|
|
193
|
+
extensionSystem.runEnd(ciLogResult, stages.map(stage => stage.name), finalResult); // <-- end hook
|
|
194
|
+
|
|
195
|
+
return ciLogResult;
|
|
196
|
+
},
|
|
197
|
+
|
|
198
|
+
dirToJson: parseDirectoryToJson
|
|
199
|
+
};
|