cispacempt-v2 0.1.2 → 0.1.4

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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/index.js +63 -210
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "main": "src/index.js",
3
3
  "name": "cispacempt-v2",
4
- "version": "0.1.2",
4
+ "version": "0.1.4",
5
5
  "description": "Fast CI engine for CodespaceMPT with script stages and file system hooks",
6
6
  "author": "argentidev",
7
7
  "license": "MIT",
package/src/index.js CHANGED
@@ -2,92 +2,6 @@ const path = require("path"),
2
2
  os = require("os"),
3
3
  fs = require("fs"),
4
4
  { spawn } = require("child_process"),
5
- yaml = require("js-yaml"),
6
- https = require("https"),
7
- { spawnSync } = require("child_process"),
8
- rootfsMap = {
9
- debian: "https://github.com/EXALAB/Anlinux-Resources/blob/master/Rootfs/Debian/arm64/debian-rootfs-arm64.tar.xz",
10
- alpine: "https://dl-cdn.alpinelinux.org/alpine/latest-stable/releases/x86_64/alpine-minirootfs-3.19.1-x86_64.tar.gz",
11
- ubuntu: "https://partner-images.canonical.com/core/focal/current/ubuntu-focal-core-cloudimg-amd64-root.tar.gz",
12
- arch: "https://mirror.rackspace.com/archlinux/iso/latest/archlinux-bootstrap-x86_64.tar.gz",
13
- void: "https://repo-default.voidlinux.org/live/current/void-x86_64-ROOTFS-20230628.tar.xz",
14
- fedora: "https://download.fedoraproject.org/pub/fedora/linux/releases/39/Container/x86_64/images/Fedora-Container-Base-39-1.5.x86_64.tar.xz",
15
- centos: "https://buildlogs.centos.org/centos/7/container/x86_64/images/CentOS-7-Container-7.9.2009-20210916.0.x86_64.tar.xz"
16
- };
17
-
18
- const downloadFile = (url, dest) => new Promise((resolve, reject) => {
19
- console.log(`Starting download from: ${url}`);
20
- const file = fs.createWriteStream(dest);
21
-
22
- const request = https.get(url, (response) => {
23
- if (response.statusCode >= 300 && response.statusCode < 400 && response.headers.location) {
24
- console.log(`Redirected to: ${response.headers.location}`);
25
- // Si hay redirección, cerrar este stream y descargar la nueva url
26
- file.close();
27
- fs.unlinkSync(dest);
28
- return downloadFile(response.headers.location, dest).then(resolve).catch(reject);
29
- }
30
-
31
- if (response.statusCode !== 200) {
32
- file.close();
33
- fs.unlinkSync(dest);
34
- return reject(new Error(`Failed to download file, status code: ${response.statusCode}`));
35
- }
36
-
37
- response.pipe(file);
38
-
39
- file.on("finish", () => {
40
- console.log(`Download finished: ${dest}`);
41
- file.close(resolve);
42
- });
43
- });
44
-
45
- request.on("error", (err) => {
46
- console.error(`Download error: ${err.message}`);
47
- file.close();
48
- fs.unlinkSync(dest);
49
- reject(err);
50
- });
51
-
52
- file.on("error", (err) => {
53
- console.error(`File write error: ${err.message}`);
54
- file.close();
55
- fs.unlinkSync(dest);
56
- reject(err);
57
- });
58
- }),
59
- prepareProotEnv = async (osName, cacheDir) => {
60
- const prootBin = path.join(cacheDir, "proot");
61
- const rootfsDir = path.join(cacheDir, `rootfs_${osName}`);
62
- const tarball = path.join(cacheDir, `${osName}.tar.${osName.includes("xz") ? "xz" : "gz"}`);
63
-
64
- // Crear el directorio cacheDir si no existe
65
- fs.mkdirSync(cacheDir, { recursive: true });
66
-
67
- // Descargar proot
68
- if (!fs.existsSync(prootBin)) {
69
- const url = "https://proot.gitlab.io/proot/bin/proot";
70
- await downloadFile(url, prootBin);
71
- fs.chmodSync(prootBin, 0o755);
72
- }
73
-
74
- // Descargar rootfs
75
- if (!fs.existsSync(rootfsDir)) {
76
- const url = rootfsMap[osName];
77
- if (!url) throw new Error(`Unsupported OS "${osName}"`);
78
- await downloadFile(url, tarball);
79
-
80
- fs.mkdirSync(rootfsDir, { recursive: true });
81
-
82
- // Extraer el tarball
83
- const extCmd = tarball.endsWith(".xz") ? "tar -xJf" : "tar -xzf";
84
- spawnSync(extCmd, [tarball, "-C", rootfsDir], { shell: true });
85
-
86
- fs.unlinkSync(tarball); // limpiar
87
- }
88
-
89
- return { prootBin, rootfsDir };
90
- },
91
5
  getCPUUsageSnapshot = function () {
92
6
  const cpus = os.cpus();
93
7
  return cpus.map(cpu => ({ ...cpu.times }));
@@ -169,40 +83,20 @@ const path = require("path"),
169
83
  fs.writeFileSync(filePath, fileSystem[file]);
170
84
  }
171
85
  });
172
- },
173
- detectAvailableProotOSID = () => {
174
- const baseDir = path.resolve(__dirname); // o __dirname donde esté el código
175
- const entries = fs.readdirSync(baseDir, { withFileTypes: true });
176
-
177
- for (const entry of entries) {
178
- if (entry.isDirectory() && entry.name.startsWith("rootfs-")) {
179
- const osReleasePath = path.join(baseDir, entry.name, "etc", "os-release");
180
-
181
- try {
182
- const content = fs.readFileSync(osReleasePath, "utf8");
183
- const match = content.match(/^ID="?([\w\-]+)"?/m);
184
-
185
- if (match) {
186
- return {
187
- id: match[1], // ej. "debian", "alpine"
188
- rootfsPath: path.join(baseDir, entry.name), // ruta completa
189
- };
190
- }
191
- } catch (_) {
192
- // No es un rootfs válido, seguimos con el siguiente
193
- }
194
- }
195
- }
196
-
197
- return null; // No se encontró ninguno válido
198
86
  }, { extensionSystem } = require("./extension");
199
-
200
- // Export as a Node JS module
201
87
  module.exports = {
202
-
203
- parse: async function (ciScript, repoName, fileSystem, tmp = true) {
204
- let errorIn = [];
205
- let successIn = [];
88
+ run: async function (
89
+ ciScript, // the script to run
90
+ fileSystem, // a path or a json representation of your file system (kinda like {"app.js":"console.log('Hi!');"})
91
+ tmp = true, // self explanatory; run in a temporary environment
92
+ returnJsonFs = false, // if you want a json representation of your fs (dont use this if your working on binaries)
93
+ repoName = (
94
+ require('crypto')
95
+ .randomBytes(16)
96
+ .toString('hex')
97
+ ), // to name the tmp dir
98
+ onOutput = () => { } // for live outputs
99
+ ) {
206
100
  let finalResult = [];
207
101
  let timeTaken;
208
102
  let combinedExitCode = 0;
@@ -212,106 +106,68 @@ module.exports = {
212
106
  const convertMsToSec = (ms) => (ms / 1000).toFixed(2);
213
107
 
214
108
  const stages = Object.entries(ciScript.stages).map(([name, data]) => ({ name, ...data }));
215
- const tempDir = path.join(__dirname, tmp == true ? "tmp/" + repoName : repoName);
109
+ const tempDir = path.join(__dirname, tmp === true ? "tmp/" + repoName : repoName);
216
110
 
111
+ // Helper to substitute env variables in commands
217
112
  const substituteEnvVariables = (command, env) => {
218
113
  return command.replace(/\${\[(\w+)\]}/g, (_, varName) => {
219
114
  return env[varName] ?? '';
220
115
  });
221
116
  };
222
117
 
223
- const findFileInMap = function (fileSystem, searchFilePath) {
224
- const pathParts = searchFilePath.split("/");
225
- let currentMap = fileSystem;
226
-
227
- for (let i = 0; i < pathParts.length; i++) {
228
- const part = pathParts[i];
118
+ // Detect if fileSystem is a directory path string
119
+ const isFileSystemPath = typeof fileSystem === 'string' && fs.existsSync(fileSystem) && fs.statSync(fileSystem).isDirectory();
229
120
 
230
- if (currentMap[part]) {
231
- if (typeof currentMap[part] === "object") {
232
- currentMap = currentMap[part];
233
- } else {
234
- return currentMap[part]; // Return the found file if it exists
235
- }
236
- } else {
237
- return null; // Return null if not found at all
238
- }
239
- }
240
-
241
- return null; // Return null if path doesn't exist
242
- }
243
-
244
- if (ciScript.include) {
245
- for (const includePath of ciScript.include) {
246
- const templateContent = findFileInMap(fileSystem, includePath);
247
- if (!templateContent) {
248
- console.warn(`Included file '${includePath}' not found`);
249
- continue;
250
- }
251
-
252
- const includedYaml = yaml.load(templateContent);
253
-
254
- // Merge included content
255
- ciScript.stages = {
256
- ...(includedYaml.stages || {}),
257
- ...(ciScript.stages || {})
258
- };
259
- ciScript.env = {
260
- ...(includedYaml.env || {}),
261
- ...(ciScript.env || {}),
262
- };
263
- }
264
- }
121
+ const stageExecuted = {}; // Track executed stages
265
122
 
266
123
  for (const stage of stages) {
267
- extensionSystem.runPreExecution(stage.name); // <-- pre execution hook
124
+ extensionSystem.runPreExecution(stage.name);
268
125
 
269
126
  if (stage.scripts && Array.isArray(stage.scripts)) {
270
- if (!fs.existsSync(tempDir)) {
271
- fs.mkdirSync(tempDir, { recursive: true });
272
- }
127
+ let workingDir;
273
128
 
274
- downloadFiles(fileSystem, tempDir);
275
-
276
- const installCommands = {
277
- "package.json": "npm install",
278
- "requirements.txt": "pip install -r requirements.txt",
279
- "go.mod": "go mod download",
280
- "composer.json": "composer install",
281
- "Gemfile": "bundle install",
282
- "Pipfile": "pipenv install"
283
- };
129
+ if (isFileSystemPath) {
130
+ // Use the existing directory directly
131
+ workingDir = fileSystem;
132
+ } else {
133
+ // Write files to tempDir
134
+ if (!fs.existsSync(tempDir)) {
135
+ fs.mkdirSync(tempDir, { recursive: true });
136
+ }
137
+ downloadFiles(fileSystem, tempDir);
138
+ workingDir = tempDir;
139
+ }
284
140
 
285
141
  await new Promise((resolve) => {
286
142
  (async function (commands, callback) {
287
- for (const fileName of Object.keys(installCommands)) {
288
- const foundKey = Object.keys(parseDirectoryToJson(tempDir)).find(key => key.includes(fileName));
289
- if (foundKey && !commands.includes(installCommands[fileName])) {
290
- commands.push(installCommands[fileName]);
291
- }
292
- }
293
143
  const results = [];
294
144
 
145
+ if (!stageExecuted[stage.name]) {
146
+ onOutput(`[${stage.name}]\n`);
147
+ stageExecuted[stage.name] = true;
148
+ }
149
+
295
150
  for (const command of commands) {
296
151
  const resolvedCommand = substituteEnvVariables(command, ciScript.env || {});
297
152
 
298
- await new Promise(async (res) => {
153
+ await new Promise((res) => {
299
154
  const commandToRun = resolvedCommand;
300
- let finalCommand;
301
- if (ciScript.os && Object.keys(rootfsMap || {}).includes(ciScript.os)) {
302
- const { prootBin, rootfsDir } = await prepareProotEnv(ciScript.os, path.join(__dirname, "ci_proot_cache"));
303
- console.log("PROOT BIN:", prootBin, fs.existsSync(prootBin));
304
-
305
- finalCommand = `${prootBin} -R ${rootfsDir} -b ${tempDir}:/sandbox -w /sandbox /bin/sh -c "${commandToRun.replace(/"/g, '\\"')}"`;
306
- } else {
307
- finalCommand = commandToRun;
308
- }
309
- const child = spawn(finalCommand, { cwd: tempDir, shell: true });
155
+ const child = spawn(commandToRun, { cwd: workingDir, shell: true });
310
156
  let stdoutData = "";
311
157
  let stderrData = "";
312
158
 
313
- child.stdout.on("data", (data) => stdoutData += data.toString());
314
- child.stderr.on("data", (data) => stderrData += data.toString());
159
+ onOutput(`$ ${resolvedCommand}\n`);
160
+
161
+ child.stdout.on("data", (data) => {
162
+ const text = data.toString();
163
+ stdoutData += text;
164
+ onOutput(text);
165
+ });
166
+
167
+ child.stderr.on("data", (data) => {
168
+ const text = data.toString();
169
+ onOutput(`${commandToRun}\n${text}`);
170
+ });
315
171
 
316
172
  child.on("close", (code) => {
317
173
  results.push({
@@ -321,27 +177,28 @@ module.exports = {
321
177
  exitCode: code,
322
178
  error: code === 0 ? null : `Command failed with exit code ${code}`,
323
179
  });
180
+
181
+ if (code !== 0) onOutput(`[!] Exit code ${code}\n`);
324
182
  res();
325
183
  });
326
184
  });
327
185
 
328
186
  if (results.at(-1).exitCode !== 0) break;
329
187
  }
188
+
330
189
  callback(results);
331
- extensionSystem.runPostExecution(stage.name, results); // <-- post execution hook
190
+ extensionSystem.runPostExecution(stage.name, results);
332
191
  resolve();
333
192
  })(stage.scripts, (results) => {
334
- let output = `[${stage.name}]\n\nResults:\n`;
193
+ let output = "";
335
194
 
336
195
  results.forEach((result) => {
337
196
  output += `$ ${result.command}\n`;
338
197
  if (result.error) {
339
198
  output += `Error: ${result.error}\n`;
340
199
  output += `${result.stderr ? result.stderr : ''}\n`;
341
- !errorIn.includes(stage.name) ? errorIn.push(stage.name) : null;
342
200
  } else {
343
201
  output += `${result.stdout ? result.stdout : ''}\n`;
344
- !successIn.includes(stage.name) ? successIn.push(stage.name) : null;
345
202
  }
346
203
 
347
204
  if (result.exitCode !== 0) {
@@ -355,31 +212,27 @@ module.exports = {
355
212
  }
356
213
  }
357
214
 
358
- console.log(successIn, errorIn);
215
+ // Only parse directory JSON if we created tempDir (fileSystem not a path)
216
+ const fileSysJson = isFileSystemPath ? null : parseDirectoryToJson(tempDir);
217
+
359
218
  const finalResultEndTime = Date.now();
360
219
  timeTaken = convertMsToSec(finalResultEndTime - startTime);
361
220
  const cpuEnd = getCPUUsageSnapshot();
362
221
 
363
222
  const ciLogResult = {
364
223
  output: finalResult.join(''),
365
- timestamp: String(new Date().toString()),
366
- fileSys: parseDirectoryToJson(tempDir),
224
+ timestamp: new Date().toString(),
225
+ ...(returnJsonFs && { fileSys: fileSysJson }),
367
226
  ci_duration: timeTaken,
368
227
  exitCode: combinedExitCode,
369
228
  cpu_usage: calculateCPUUsage(cpuStart, cpuEnd),
370
- os: {
371
- name: os.platform(),
372
- version: os.release(),
373
- arch: os.arch(),
374
- id: detectAvailableProotOSID()?.id || 'ubuntu',
375
- },
376
- errorIn: errorIn.length > 0 ? errorIn : null,
377
- successIn: successIn.length > 0 ? successIn : null,
378
229
  };
379
230
 
380
- cleanUpAndRespond(tempDir);
231
+ if (!isFileSystemPath) {
232
+ cleanUpAndRespond(tempDir);
233
+ }
381
234
 
382
- extensionSystem.runEnd(ciLogResult, stages.map(stage => stage.name), finalResult); // <-- end hook
235
+ extensionSystem.runEnd(ciLogResult, stages.map(stage => stage.name), finalResult);
383
236
 
384
237
  return ciLogResult;
385
238
  },