@pipelab/core-node 1.0.0-beta.17 → 1.0.0-beta.18
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/CHANGELOG.md +9 -0
- package/dist/index.d.mts +12 -8
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +103 -136
- package/dist/index.mjs.map +1 -1
- package/package.json +4 -4
- package/src/config.test.ts +23 -15
- package/src/config.ts +18 -21
- package/src/context.ts +19 -2
- package/src/handlers/build-history.ts +47 -90
- package/src/handlers/config.ts +2 -21
- package/src/handlers/engine.ts +26 -22
- package/src/handlers/history.ts +0 -40
- package/src/plugins-registry.ts +15 -17
- package/src/runner.ts +2 -4
- package/src/server.ts +1 -0
- package/src/utils/github.test.ts +3 -5
- package/src/utils/remote.test.ts +15 -6
- package/src/utils/remote.ts +8 -5
- package/src/utils/storage.ts +2 -1
- package/src/utils.ts +6 -6
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pipelab/core-node",
|
|
3
|
-
"version": "1.0.0-beta.
|
|
3
|
+
"version": "1.0.0-beta.18",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "The Pipelab automation engine for Node.js",
|
|
6
6
|
"license": "FSL-1.1-MIT",
|
|
@@ -40,8 +40,8 @@
|
|
|
40
40
|
"type-fest": "4.39.0",
|
|
41
41
|
"ws": "8.18.3",
|
|
42
42
|
"yauzl": "2.10.0",
|
|
43
|
-
"@pipelab/constants": "1.0.0-beta.
|
|
44
|
-
"@pipelab/shared": "1.0.0-beta.
|
|
43
|
+
"@pipelab/constants": "1.0.0-beta.16",
|
|
44
|
+
"@pipelab/shared": "1.0.0-beta.14"
|
|
45
45
|
},
|
|
46
46
|
"devDependencies": {
|
|
47
47
|
"@types/adm-zip": "0.5.7",
|
|
@@ -56,7 +56,7 @@
|
|
|
56
56
|
"tsdown": "0.21.2",
|
|
57
57
|
"typescript": "^5.0.0",
|
|
58
58
|
"vitest": "3.1.4",
|
|
59
|
-
"@pipelab/tsconfig": "1.0.0-beta.
|
|
59
|
+
"@pipelab/tsconfig": "1.0.0-beta.14"
|
|
60
60
|
},
|
|
61
61
|
"scripts": {
|
|
62
62
|
"build": "tsdown",
|
package/src/config.test.ts
CHANGED
|
@@ -38,9 +38,9 @@ describe("setupConfigFile & Backup Creation", () => {
|
|
|
38
38
|
// 1. Setup V1 configuration file in the context's config path
|
|
39
39
|
const configDir = context.getConfigPath();
|
|
40
40
|
await fs.mkdir(configDir, { recursive: true });
|
|
41
|
-
|
|
41
|
+
|
|
42
42
|
const projectsFilePath = path.join(configDir, "projects.json");
|
|
43
|
-
|
|
43
|
+
|
|
44
44
|
const v1Config = {
|
|
45
45
|
version: "1.0.0",
|
|
46
46
|
data: {
|
|
@@ -49,10 +49,10 @@ describe("setupConfigFile & Backup Creation", () => {
|
|
|
49
49
|
type: "internal",
|
|
50
50
|
configName: "pipeline-1",
|
|
51
51
|
lastModified: "2026-06-04",
|
|
52
|
-
}
|
|
53
|
-
}
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
54
|
};
|
|
55
|
-
|
|
55
|
+
|
|
56
56
|
await fs.writeFile(projectsFilePath, JSON.stringify(v1Config));
|
|
57
57
|
|
|
58
58
|
// 2. Initialize setupConfigFile
|
|
@@ -64,11 +64,11 @@ describe("setupConfigFile & Backup Creation", () => {
|
|
|
64
64
|
// 3. Retrieve config (this triggers the migration process and onStep callback)
|
|
65
65
|
const migratedConfig = await configInstance.getConfig();
|
|
66
66
|
|
|
67
|
-
// 4. Assert the main config is updated on disk to version
|
|
68
|
-
expect(migratedConfig.version).toBe("
|
|
69
|
-
|
|
67
|
+
// 4. Assert the main config is updated on disk to version 3.0.0
|
|
68
|
+
expect(migratedConfig.version).toBe("3.0.0");
|
|
69
|
+
|
|
70
70
|
const updatedContent = JSON.parse(await fs.readFile(projectsFilePath, "utf8"));
|
|
71
|
-
expect(updatedContent.version).toBe("
|
|
71
|
+
expect(updatedContent.version).toBe("3.0.0");
|
|
72
72
|
|
|
73
73
|
// 5. Assert that the intermediate backup file was created on disk
|
|
74
74
|
const backupFilePath = path.join(configDir, "projects.v2.0.0.json");
|
|
@@ -92,7 +92,7 @@ describe("setupConfigFile & Backup Creation", () => {
|
|
|
92
92
|
// Should create file with default value
|
|
93
93
|
expect(existsSync(projectsFilePath)).toBe(true);
|
|
94
94
|
const content = JSON.parse(await fs.readFile(projectsFilePath, "utf8"));
|
|
95
|
-
expect(content.version).toBe("
|
|
95
|
+
expect(content.version).toBe("3.0.0");
|
|
96
96
|
expect(content.projects).toHaveLength(1);
|
|
97
97
|
expect(content.pipelines).toEqual([]);
|
|
98
98
|
});
|
|
@@ -107,13 +107,15 @@ describe("setupConfigFile & Backup Creation", () => {
|
|
|
107
107
|
const configInstance = await setupConfigFile<FileRepo>("projects", { context });
|
|
108
108
|
const config = await configInstance.getConfig();
|
|
109
109
|
|
|
110
|
-
expect(config.version).toBe("
|
|
110
|
+
expect(config.version).toBe("3.0.0");
|
|
111
111
|
expect(config.projects).toHaveLength(1);
|
|
112
112
|
expect(config.pipelines).toEqual([]);
|
|
113
113
|
|
|
114
114
|
// Verify a timestamped corrupted backup file exists
|
|
115
115
|
const files = await fs.readdir(configDir);
|
|
116
|
-
const corruptedFile = files.find(
|
|
116
|
+
const corruptedFile = files.find(
|
|
117
|
+
(f) => f.startsWith("projects.corrupted.") && f.endsWith(".json"),
|
|
118
|
+
);
|
|
117
119
|
expect(corruptedFile).toBeDefined();
|
|
118
120
|
const corruptedContent = await fs.readFile(path.join(configDir, corruptedFile!), "utf8");
|
|
119
121
|
expect(corruptedContent).toBe("{ corrupted json... }");
|
|
@@ -143,9 +145,13 @@ describe("setupConfigFile & Backup Creation", () => {
|
|
|
143
145
|
|
|
144
146
|
// Verify a timestamped corrupted backup file exists
|
|
145
147
|
const files = await fs.readdir(configDir);
|
|
146
|
-
const corruptedFile = files.find(
|
|
148
|
+
const corruptedFile = files.find(
|
|
149
|
+
(f) => f.startsWith("projects.corrupted.") && f.endsWith(".json"),
|
|
150
|
+
);
|
|
147
151
|
expect(corruptedFile).toBeDefined();
|
|
148
|
-
const corruptedContent = JSON.parse(
|
|
152
|
+
const corruptedContent = JSON.parse(
|
|
153
|
+
await fs.readFile(path.join(configDir, corruptedFile!), "utf8"),
|
|
154
|
+
);
|
|
149
155
|
expect(corruptedContent.invalidData).toBe(true);
|
|
150
156
|
});
|
|
151
157
|
|
|
@@ -169,7 +175,9 @@ describe("setupConfigFile & Backup Creation", () => {
|
|
|
169
175
|
const success = await configInstance.setConfig(newConfig);
|
|
170
176
|
expect(success).toBe(true);
|
|
171
177
|
|
|
172
|
-
const savedContent = JSON.parse(
|
|
178
|
+
const savedContent = JSON.parse(
|
|
179
|
+
await fs.readFile(path.join(context.getConfigPath(), "projects.json"), "utf8"),
|
|
180
|
+
);
|
|
173
181
|
expect(savedContent.pipelines).toHaveLength(1);
|
|
174
182
|
expect(savedContent.pipelines[0].id).toBe("pipeline-new");
|
|
175
183
|
});
|
package/src/config.ts
CHANGED
|
@@ -3,7 +3,7 @@ import path from "node:path";
|
|
|
3
3
|
import { ensure } from "./utils/fs-extras";
|
|
4
4
|
import fs from "node:fs/promises";
|
|
5
5
|
import { useLogger } from "@pipelab/shared";
|
|
6
|
-
import { configRegistry, Migrator
|
|
6
|
+
import { configRegistry, Migrator } from "@pipelab/shared";
|
|
7
7
|
|
|
8
8
|
export const getMigrator = <T>(name: string) => {
|
|
9
9
|
return (configRegistry[name] || configRegistry["pipeline"]) as Migrator<T>;
|
|
@@ -62,12 +62,15 @@ export const setupConfigFile = async <T>(
|
|
|
62
62
|
debug: false,
|
|
63
63
|
onStep: async (state: any, version: string) => {
|
|
64
64
|
const parsedPath = path.parse(filesPath);
|
|
65
|
-
const versionedPath =
|
|
65
|
+
const versionedPath = ctx.getConfigPath(`${parsedPath.name}.v${version}.json`);
|
|
66
66
|
try {
|
|
67
67
|
await fs.writeFile(versionedPath, JSON.stringify(state));
|
|
68
68
|
logger().info(`Intermediate backup created for ${name} at ${versionedPath}`);
|
|
69
69
|
} catch (e) {
|
|
70
|
-
logger().error(
|
|
70
|
+
logger().error(
|
|
71
|
+
`Failed to create intermediate backup for ${name} at v${version}:`,
|
|
72
|
+
e,
|
|
73
|
+
);
|
|
71
74
|
}
|
|
72
75
|
},
|
|
73
76
|
});
|
|
@@ -80,36 +83,23 @@ export const setupConfigFile = async <T>(
|
|
|
80
83
|
json = migrator.defaultValue;
|
|
81
84
|
}
|
|
82
85
|
|
|
83
|
-
let normalized = false;
|
|
84
|
-
const isPipeline =
|
|
85
|
-
name.startsWith("pipeline-") ||
|
|
86
|
-
path.isAbsolute(name) ||
|
|
87
|
-
name.endsWith(".json") ||
|
|
88
|
-
name === "pipeline";
|
|
89
|
-
if (isPipeline) {
|
|
90
|
-
normalized = normalizePipelineConfig(json);
|
|
91
|
-
}
|
|
92
|
-
|
|
93
86
|
const originalVersion = originalJson?.version;
|
|
94
87
|
const newVersion = json?.version;
|
|
95
88
|
|
|
96
89
|
const shouldSaveBack =
|
|
97
|
-
originalVersion !== newVersion ||
|
|
98
|
-
normalized ||
|
|
99
|
-
content === undefined ||
|
|
100
|
-
parseFailed ||
|
|
101
|
-
migrationFailed;
|
|
90
|
+
originalVersion !== newVersion || content === undefined || parseFailed || migrationFailed;
|
|
102
91
|
|
|
103
92
|
if (shouldSaveBack) {
|
|
104
93
|
if (parseFailed || migrationFailed) {
|
|
105
94
|
try {
|
|
106
95
|
const parsedPath = path.parse(filesPath);
|
|
107
96
|
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
108
|
-
const corruptedPath =
|
|
109
|
-
parsedPath.dir,
|
|
97
|
+
const corruptedPath = ctx.getConfigPath(
|
|
110
98
|
`${parsedPath.name}.corrupted.${timestamp}.json`,
|
|
111
99
|
);
|
|
112
|
-
const backupContent = parseFailed
|
|
100
|
+
const backupContent = parseFailed
|
|
101
|
+
? content || ""
|
|
102
|
+
: JSON.stringify(originalJson, null, 2);
|
|
113
103
|
await fs.writeFile(corruptedPath, backupContent);
|
|
114
104
|
logger().info(`Corrupted config file preserved at ${corruptedPath}`);
|
|
115
105
|
} catch (e) {
|
|
@@ -128,3 +118,10 @@ export const setupConfigFile = async <T>(
|
|
|
128
118
|
},
|
|
129
119
|
};
|
|
130
120
|
};
|
|
121
|
+
|
|
122
|
+
export const deleteConfigFile = async (nameOrPath: string, context: PipelabContext) => {
|
|
123
|
+
const isAbsolutePath = path.isAbsolute(nameOrPath);
|
|
124
|
+
const filesPath = isAbsolutePath ? nameOrPath : context.getConfigPath(`${nameOrPath}.json`);
|
|
125
|
+
|
|
126
|
+
await fs.rm(filesPath, { force: true });
|
|
127
|
+
};
|
package/src/context.ts
CHANGED
|
@@ -51,6 +51,14 @@ function findProjectRoot(startDir: string): string | null {
|
|
|
51
51
|
|
|
52
52
|
export const projectRoot = findProjectRoot(_dirname);
|
|
53
53
|
|
|
54
|
+
export const CacheFolder = {
|
|
55
|
+
Actions: "actions",
|
|
56
|
+
Pipelines: "pipelines",
|
|
57
|
+
Pacote: "pacote",
|
|
58
|
+
} as const;
|
|
59
|
+
|
|
60
|
+
export type CacheFolderType = (typeof CacheFolder)[keyof typeof CacheFolder];
|
|
61
|
+
|
|
54
62
|
export interface PipelabContextOptions {
|
|
55
63
|
userDataPath: string;
|
|
56
64
|
releaseTag?: string;
|
|
@@ -88,14 +96,23 @@ export class PipelabContext {
|
|
|
88
96
|
return await mkdtemp(join(realBaseDir, prefix));
|
|
89
97
|
}
|
|
90
98
|
|
|
91
|
-
getCachePath(
|
|
92
|
-
|
|
99
|
+
getCachePath(): string;
|
|
100
|
+
getCachePath(folder: CacheFolderType, ...subpaths: string[]): string;
|
|
101
|
+
getCachePath(folder?: CacheFolderType, ...subpaths: string[]) {
|
|
102
|
+
if (!folder) {
|
|
103
|
+
return join(this.userDataPath, "cache");
|
|
104
|
+
}
|
|
105
|
+
return join(this.userDataPath, "cache", folder, ...subpaths);
|
|
93
106
|
}
|
|
94
107
|
|
|
95
108
|
getPnpmPath(...subpaths: string[]) {
|
|
96
109
|
return join(this.userDataPath, "pnpm", ...subpaths);
|
|
97
110
|
}
|
|
98
111
|
|
|
112
|
+
getBuildHistoryPath(...subpaths: string[]) {
|
|
113
|
+
return join(this.userDataPath, "build-history", ...subpaths);
|
|
114
|
+
}
|
|
115
|
+
|
|
99
116
|
getNodePath(version = DEFAULT_NODE_VERSION) {
|
|
100
117
|
const isWindows = process.platform === "win32";
|
|
101
118
|
return this.getThirdPartyPath("node", version, isWindows ? "node.exe" : "bin/node");
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import { PipelabContext } from "../context";
|
|
2
2
|
import { join } from "node:path";
|
|
3
|
-
import { writeFile, readFile, unlink, mkdir, stat, readdir } from "node:fs/promises";
|
|
3
|
+
import { writeFile, readFile, unlink, mkdir, stat, readdir, rm } from "node:fs/promises";
|
|
4
4
|
import { useLogger, BuildHistoryEntry, IBuildHistoryStorage, AppConfig } from "@pipelab/shared";
|
|
5
|
-
import { setupConfigFile } from "../config";
|
|
6
5
|
import checkDiskSpace from "check-disk-space";
|
|
7
6
|
import { getFolderSize } from "../utils/fs-extras";
|
|
8
7
|
import { SandboxFolder } from "@pipelab/constants";
|
|
@@ -17,18 +16,11 @@ export class BuildHistoryStorage implements IBuildHistoryStorage {
|
|
|
17
16
|
}
|
|
18
17
|
|
|
19
18
|
private getStoragePath() {
|
|
20
|
-
return
|
|
19
|
+
return this.context.getConfigPath("pipelines");
|
|
21
20
|
}
|
|
22
21
|
|
|
23
22
|
private getPipelinePath(pipelineId: string): string {
|
|
24
|
-
|
|
25
|
-
// Replace invalid filename characters with underscores
|
|
26
|
-
const sanitizedId = pipelineId
|
|
27
|
-
.replace(/[/\:*?"<>|]/g, "_")
|
|
28
|
-
.replace(/__/g, "_") // Replace multiple underscores with single
|
|
29
|
-
.replace(/^_+|_+$/g, ""); // Remove leading/trailing underscores
|
|
30
|
-
|
|
31
|
-
return join(this.getStoragePath(), `pipeline-${sanitizedId}.json`);
|
|
23
|
+
return join(this.getStoragePath(), `${pipelineId}.history.json`);
|
|
32
24
|
}
|
|
33
25
|
|
|
34
26
|
private async ensureStoragePath(): Promise<void> {
|
|
@@ -65,51 +57,6 @@ export class BuildHistoryStorage implements IBuildHistoryStorage {
|
|
|
65
57
|
}
|
|
66
58
|
}
|
|
67
59
|
|
|
68
|
-
async applyRetentionPolicy(): Promise<void> {
|
|
69
|
-
try {
|
|
70
|
-
this.logger.logger().info("Applying build history retention policy...");
|
|
71
|
-
const settings = await setupConfigFile<AppConfig>("settings", { context: this.context });
|
|
72
|
-
const config = await settings.getConfig();
|
|
73
|
-
const policy = config?.buildHistory?.retentionPolicy;
|
|
74
|
-
|
|
75
|
-
if (!policy || !policy.enabled) {
|
|
76
|
-
this.logger.logger().info("Retention policy is disabled. Skipping.");
|
|
77
|
-
return;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
const { maxAge, maxEntries } = policy;
|
|
81
|
-
const pipelineFiles = await this.getAllPipelineFiles();
|
|
82
|
-
|
|
83
|
-
for (const file of pipelineFiles) {
|
|
84
|
-
const pipelineId = file.replace("pipeline-", "").replace(".json", "");
|
|
85
|
-
let entries = await this.loadPipelineHistory(pipelineId);
|
|
86
|
-
const originalCount = entries.length;
|
|
87
|
-
|
|
88
|
-
// 1. Filter by maxAge
|
|
89
|
-
if (maxAge > 0) {
|
|
90
|
-
const minDate = Date.now() - maxAge * 24 * 60 * 60 * 1000;
|
|
91
|
-
entries = entries.filter((entry) => entry.createdAt >= minDate);
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
// 2. Filter by maxEntries (sort by date first to keep the newest)
|
|
95
|
-
if (maxEntries > 0 && entries.length > maxEntries) {
|
|
96
|
-
entries = entries.sort((a, b) => b.createdAt - a.createdAt).slice(0, maxEntries);
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
if (entries.length < originalCount) {
|
|
100
|
-
this.logger
|
|
101
|
-
.logger()
|
|
102
|
-
.info(`[${pipelineId}] Pruned ${originalCount - entries.length} history entries.`);
|
|
103
|
-
await this.savePipelineHistory(pipelineId, entries);
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
this.logger.logger().info("Retention policy applied successfully.");
|
|
107
|
-
} catch (error) {
|
|
108
|
-
this.logger.logger().error("Failed to apply retention policy:", error);
|
|
109
|
-
// We don't re-throw here as this is a background task and shouldn't crash the app
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
|
|
113
60
|
async save(entry: BuildHistoryEntry): Promise<void> {
|
|
114
61
|
try {
|
|
115
62
|
const entries = await this.loadPipelineHistory(entry.pipelineId);
|
|
@@ -125,11 +72,6 @@ export class BuildHistoryStorage implements IBuildHistoryStorage {
|
|
|
125
72
|
this.logger
|
|
126
73
|
.logger()
|
|
127
74
|
.info(`Saved build history entry: ${entry.id} for pipeline: ${entry.pipelineId}`);
|
|
128
|
-
|
|
129
|
-
// Apply retention policy after each save
|
|
130
|
-
this.applyRetentionPolicy().catch((err) => {
|
|
131
|
-
this.logger.logger().error("Failed to apply retention policy after save:", err);
|
|
132
|
-
});
|
|
133
75
|
} catch (error) {
|
|
134
76
|
this.logger.logger().error("Failed to save build history entry:", error);
|
|
135
77
|
throw new Error(`Failed to save build history entry: ${error}`);
|
|
@@ -145,7 +87,8 @@ export class BuildHistoryStorage implements IBuildHistoryStorage {
|
|
|
145
87
|
|
|
146
88
|
const files = await this.getAllPipelineFiles();
|
|
147
89
|
for (const file of files) {
|
|
148
|
-
const pId =
|
|
90
|
+
const pId = this.parsePipelineIdFromFilename(file);
|
|
91
|
+
if (!pId) continue;
|
|
149
92
|
const entries = await this.loadPipelineHistory(pId);
|
|
150
93
|
const entry = entries.find((e) => e.id === id);
|
|
151
94
|
if (entry) return entry;
|
|
@@ -162,7 +105,8 @@ export class BuildHistoryStorage implements IBuildHistoryStorage {
|
|
|
162
105
|
const files = await this.getAllPipelineFiles();
|
|
163
106
|
const allEntries: BuildHistoryEntry[] = [];
|
|
164
107
|
for (const file of files) {
|
|
165
|
-
const pipelineId =
|
|
108
|
+
const pipelineId = this.parsePipelineIdFromFilename(file);
|
|
109
|
+
if (!pipelineId) continue;
|
|
166
110
|
const entries = await this.loadPipelineHistory(pipelineId);
|
|
167
111
|
allEntries.push(...entries);
|
|
168
112
|
}
|
|
@@ -200,7 +144,8 @@ export class BuildHistoryStorage implements IBuildHistoryStorage {
|
|
|
200
144
|
} else {
|
|
201
145
|
const files = await this.getAllPipelineFiles();
|
|
202
146
|
for (const file of files) {
|
|
203
|
-
const pId =
|
|
147
|
+
const pId = this.parsePipelineIdFromFilename(file);
|
|
148
|
+
if (!pId) continue;
|
|
204
149
|
const entries = await this.loadPipelineHistory(pId);
|
|
205
150
|
const entryIndex = entries.findIndex((e) => e.id === id);
|
|
206
151
|
if (entryIndex >= 0) {
|
|
@@ -231,7 +176,8 @@ export class BuildHistoryStorage implements IBuildHistoryStorage {
|
|
|
231
176
|
} else {
|
|
232
177
|
const files = await this.getAllPipelineFiles();
|
|
233
178
|
for (const file of files) {
|
|
234
|
-
const pId =
|
|
179
|
+
const pId = this.parsePipelineIdFromFilename(file);
|
|
180
|
+
if (!pId) continue;
|
|
235
181
|
const entries = await this.loadPipelineHistory(pId);
|
|
236
182
|
const entryIndex = entries.findIndex((e) => e.id === id);
|
|
237
183
|
if (entryIndex >= 0) {
|
|
@@ -253,8 +199,26 @@ export class BuildHistoryStorage implements IBuildHistoryStorage {
|
|
|
253
199
|
try {
|
|
254
200
|
await this.ensureStoragePath();
|
|
255
201
|
const files = await this.getAllPipelineFiles();
|
|
256
|
-
|
|
202
|
+
const cachePathsToDelete = new Set<string>();
|
|
203
|
+
|
|
204
|
+
for (const file of files) {
|
|
205
|
+
const pipelineId = this.parsePipelineIdFromFilename(file);
|
|
206
|
+
if (pipelineId) {
|
|
207
|
+
const entries = await this.loadPipelineHistory(pipelineId);
|
|
208
|
+
for (const entry of entries) {
|
|
209
|
+
if (entry.cachePath) {
|
|
210
|
+
cachePathsToDelete.add(entry.cachePath);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
await unlink(join(this.getStoragePath(), file));
|
|
215
|
+
}
|
|
216
|
+
|
|
257
217
|
this.logger.logger().info("Cleared all build history");
|
|
218
|
+
|
|
219
|
+
for (const cachePath of cachePathsToDelete) {
|
|
220
|
+
await rm(cachePath, { recursive: true, force: true }).catch(() => {});
|
|
221
|
+
}
|
|
258
222
|
} catch (error) {
|
|
259
223
|
this.logger.logger().error("Failed to clear build history:", error);
|
|
260
224
|
throw new Error(`Failed to clear build history: ${error}`);
|
|
@@ -264,8 +228,19 @@ export class BuildHistoryStorage implements IBuildHistoryStorage {
|
|
|
264
228
|
async clearByPipeline(pipelineId: string): Promise<void> {
|
|
265
229
|
try {
|
|
266
230
|
const pipelinePath = this.getPipelinePath(pipelineId);
|
|
231
|
+
const entries = await this.loadPipelineHistory(pipelineId);
|
|
267
232
|
await unlink(pipelinePath);
|
|
268
233
|
this.logger.logger().info(`Cleared history for pipeline "${pipelineId}"`);
|
|
234
|
+
|
|
235
|
+
const cachePathsToDelete = new Set<string>();
|
|
236
|
+
for (const entry of entries) {
|
|
237
|
+
if (entry.cachePath) {
|
|
238
|
+
cachePathsToDelete.add(entry.cachePath);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
for (const cachePath of cachePathsToDelete) {
|
|
242
|
+
await rm(cachePath, { recursive: true, force: true }).catch(() => {});
|
|
243
|
+
}
|
|
269
244
|
} catch (error: any) {
|
|
270
245
|
if (error.code === "ENOENT") {
|
|
271
246
|
this.logger
|
|
@@ -285,11 +260,6 @@ export class BuildHistoryStorage implements IBuildHistoryStorage {
|
|
|
285
260
|
newestEntry?: number;
|
|
286
261
|
numberOfPipelines: number;
|
|
287
262
|
userDataPath: string;
|
|
288
|
-
retentionPolicy: {
|
|
289
|
-
enabled: boolean;
|
|
290
|
-
maxEntries: number;
|
|
291
|
-
maxAge: number;
|
|
292
|
-
};
|
|
293
263
|
disk: {
|
|
294
264
|
total: number;
|
|
295
265
|
free: number;
|
|
@@ -314,25 +284,12 @@ export class BuildHistoryStorage implements IBuildHistoryStorage {
|
|
|
314
284
|
});
|
|
315
285
|
}
|
|
316
286
|
|
|
317
|
-
const settings = await setupConfigFile<AppConfig>("settings", { context: this.context });
|
|
318
|
-
const config = await settings.getConfig();
|
|
319
|
-
const policy = config?.buildHistory?.retentionPolicy || {
|
|
320
|
-
enabled: false,
|
|
321
|
-
maxEntries: 50,
|
|
322
|
-
maxAge: 30,
|
|
323
|
-
};
|
|
324
|
-
|
|
325
287
|
if (allEntries.length === 0) {
|
|
326
288
|
return {
|
|
327
289
|
totalEntries: 0,
|
|
328
290
|
totalSize: 0,
|
|
329
291
|
numberOfPipelines: files.length,
|
|
330
292
|
userDataPath: this.context.userDataPath,
|
|
331
|
-
retentionPolicy: {
|
|
332
|
-
enabled: policy.enabled,
|
|
333
|
-
maxEntries: policy.maxEntries,
|
|
334
|
-
maxAge: policy.maxAge,
|
|
335
|
-
},
|
|
336
293
|
disk: {
|
|
337
294
|
total: diskSpace.size,
|
|
338
295
|
free: diskSpace.free,
|
|
@@ -362,11 +319,6 @@ export class BuildHistoryStorage implements IBuildHistoryStorage {
|
|
|
362
319
|
newestEntry: sortedEntries[sortedEntries.length - 1]?.createdAt,
|
|
363
320
|
numberOfPipelines: files.length,
|
|
364
321
|
userDataPath: this.context.userDataPath,
|
|
365
|
-
retentionPolicy: {
|
|
366
|
-
enabled: policy.enabled,
|
|
367
|
-
maxEntries: policy.maxEntries,
|
|
368
|
-
maxAge: policy.maxAge,
|
|
369
|
-
},
|
|
370
322
|
disk: {
|
|
371
323
|
total: diskSpace.size,
|
|
372
324
|
free: diskSpace.free,
|
|
@@ -384,9 +336,14 @@ export class BuildHistoryStorage implements IBuildHistoryStorage {
|
|
|
384
336
|
try {
|
|
385
337
|
await this.ensureStoragePath();
|
|
386
338
|
const files = await readdir(this.getStoragePath());
|
|
387
|
-
return files.filter((file) => file.
|
|
339
|
+
return files.filter((file) => file.endsWith(".history.json"));
|
|
388
340
|
} catch (error) {
|
|
389
341
|
return [];
|
|
390
342
|
}
|
|
391
343
|
}
|
|
344
|
+
|
|
345
|
+
private parsePipelineIdFromFilename(filename: string): string | null {
|
|
346
|
+
const match = filename.match(/^(.+)\.history\.json$/);
|
|
347
|
+
return match ? match[1] : null;
|
|
348
|
+
}
|
|
392
349
|
}
|
package/src/handlers/config.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { useAPI } from "../ipc-core";
|
|
2
2
|
import { useLogger, configRegistry } from "@pipelab/shared";
|
|
3
|
-
import { setupConfigFile, getMigrator } from "../config";
|
|
3
|
+
import { setupConfigFile, getMigrator, deleteConfigFile } from "../config";
|
|
4
4
|
import { PipelabContext } from "../context";
|
|
5
5
|
import fs from "node:fs/promises";
|
|
6
6
|
import path from "node:path";
|
|
@@ -114,26 +114,7 @@ export const registerConfigHandlers = (context: PipelabContext) => {
|
|
|
114
114
|
logger().info("config:delete", name);
|
|
115
115
|
|
|
116
116
|
try {
|
|
117
|
-
|
|
118
|
-
// or a name/identifier (for internal config files stored in Pipelab's app data directory).
|
|
119
|
-
const isAbsolutePath = path.isAbsolute(name);
|
|
120
|
-
const filesPath = isAbsolutePath ? name : context.getConfigPath(`${name}.json`);
|
|
121
|
-
|
|
122
|
-
await fs.rm(filesPath, { force: true });
|
|
123
|
-
|
|
124
|
-
// Clean up versioned backups too
|
|
125
|
-
const parsedPath = path.parse(filesPath);
|
|
126
|
-
const dirEntries = await fs.readdir(parsedPath.dir).catch(() => [] as string[]);
|
|
127
|
-
const prefix = `${parsedPath.name}.v`;
|
|
128
|
-
const suffix = `.json`;
|
|
129
|
-
for (const entry of dirEntries) {
|
|
130
|
-
if (entry.startsWith(prefix) && entry.endsWith(suffix)) {
|
|
131
|
-
const backupPath = path.join(parsedPath.dir, entry);
|
|
132
|
-
await fs.rm(backupPath, { force: true }).catch((err) => {
|
|
133
|
-
logger().error(`Failed to delete backup ${backupPath}:`, err);
|
|
134
|
-
});
|
|
135
|
-
}
|
|
136
|
-
}
|
|
117
|
+
await deleteConfigFile(name, context);
|
|
137
118
|
|
|
138
119
|
send({
|
|
139
120
|
type: "end",
|
package/src/handlers/engine.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { useAPI, HandleListenerSendFn } from "../ipc-core";
|
|
2
|
-
import { PipelabContext } from "../context";
|
|
2
|
+
import { PipelabContext, CacheFolder } from "../context";
|
|
3
3
|
import { useLogger } from "@pipelab/shared";
|
|
4
4
|
import { getFinalPlugins, executeGraphWithHistory } from "../utils";
|
|
5
5
|
import { presets } from "../presets/list";
|
|
6
6
|
import { handleActionExecute } from "../handler-func";
|
|
7
7
|
import { join } from "node:path";
|
|
8
|
+
import { rm } from "node:fs/promises";
|
|
8
9
|
import { setupConfigFile } from "../config";
|
|
9
10
|
import { AppConfig } from "@pipelab/shared";
|
|
10
11
|
|
|
@@ -78,32 +79,35 @@ export const registerEngineHandlers = (context: PipelabContext) => {
|
|
|
78
79
|
|
|
79
80
|
handle("action:execute", async (event, { send, value }) => {
|
|
80
81
|
const { nodeId, params, pluginId } = value;
|
|
81
|
-
const
|
|
82
|
-
const config = await settings.getConfig();
|
|
83
|
-
|
|
84
|
-
const cachePath = join(context.userDataPath, "cache", "actions", pluginId, nodeId);
|
|
82
|
+
const cachePath = context.getCachePath(CacheFolder.Actions, pluginId, nodeId);
|
|
85
83
|
const cwd = await context.createTempFolder("action-execute-");
|
|
86
84
|
|
|
87
|
-
|
|
88
|
-
|
|
85
|
+
try {
|
|
86
|
+
const mainWindow: undefined = undefined;
|
|
87
|
+
abortControllerGraph = new AbortController();
|
|
89
88
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
89
|
+
const signalPromise = new Promise((resolve, reject) => {
|
|
90
|
+
abortControllerGraph!.signal.addEventListener("abort", async () => {
|
|
91
|
+
await send({
|
|
92
|
+
type: "end",
|
|
93
|
+
data: {
|
|
94
|
+
ipcError: "Action aborted",
|
|
95
|
+
type: "error",
|
|
96
|
+
},
|
|
97
|
+
});
|
|
98
|
+
return reject(new Error("Action interrupted"));
|
|
98
99
|
});
|
|
99
|
-
return reject(new Error("Action interrupted"));
|
|
100
100
|
});
|
|
101
|
-
});
|
|
102
101
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
102
|
+
await Promise.race([
|
|
103
|
+
signalPromise,
|
|
104
|
+
effectiveActionExecute(nodeId, pluginId, params, mainWindow, send, cwd, cachePath),
|
|
105
|
+
]);
|
|
106
|
+
} finally {
|
|
107
|
+
await rm(cwd, { recursive: true, force: true }).catch((err) => {
|
|
108
|
+
console.warn(`Failed to cleanup temp folder at ${cwd}:`, err);
|
|
109
|
+
});
|
|
110
|
+
}
|
|
107
111
|
});
|
|
108
112
|
|
|
109
113
|
handle("constants:get", async (_, { send }) => {
|
|
@@ -144,7 +148,7 @@ export const registerEngineHandlers = (context: PipelabContext) => {
|
|
|
144
148
|
const effectiveProjectName = projectName || "Unnamed Project";
|
|
145
149
|
const effectiveProjectPath = projectPath || "";
|
|
146
150
|
const effectivePipelineId = pipelineId || "unknown";
|
|
147
|
-
const effectiveCachePath =
|
|
151
|
+
const effectiveCachePath = context.getCachePath(CacheFolder.Pipelines, effectivePipelineId);
|
|
148
152
|
|
|
149
153
|
const mainWindow: undefined = undefined;
|
|
150
154
|
abortControllerGraph = new AbortController();
|
package/src/handlers/history.ts
CHANGED
|
@@ -284,44 +284,4 @@ export const registerHistoryHandlers = (context: PipelabContext) => {
|
|
|
284
284
|
});
|
|
285
285
|
}
|
|
286
286
|
});
|
|
287
|
-
|
|
288
|
-
handle("build-history:configure", async (_, { send, value }) => {
|
|
289
|
-
try {
|
|
290
|
-
logger().info("Updating build history configuration:", value.config);
|
|
291
|
-
|
|
292
|
-
const settings = await setupConfigFile<AppConfig>("settings", { context });
|
|
293
|
-
const currentConfig = await settings.getConfig();
|
|
294
|
-
|
|
295
|
-
// Deep merge the new config with the existing one
|
|
296
|
-
const newConfig = {
|
|
297
|
-
...currentConfig,
|
|
298
|
-
buildHistory: {
|
|
299
|
-
...currentConfig?.buildHistory,
|
|
300
|
-
retentionPolicy: {
|
|
301
|
-
...currentConfig?.buildHistory?.retentionPolicy,
|
|
302
|
-
...value.config.retentionPolicy,
|
|
303
|
-
},
|
|
304
|
-
},
|
|
305
|
-
};
|
|
306
|
-
|
|
307
|
-
await settings.setConfig(newConfig);
|
|
308
|
-
|
|
309
|
-
send({
|
|
310
|
-
type: "end",
|
|
311
|
-
data: {
|
|
312
|
-
type: "success",
|
|
313
|
-
result: { result: "ok" },
|
|
314
|
-
},
|
|
315
|
-
});
|
|
316
|
-
} catch (error) {
|
|
317
|
-
logger().error("Failed to configure build history:", error);
|
|
318
|
-
send({
|
|
319
|
-
type: "end",
|
|
320
|
-
data: {
|
|
321
|
-
type: "error",
|
|
322
|
-
ipcError: error instanceof Error ? error.message : "Failed to configure build history",
|
|
323
|
-
},
|
|
324
|
-
});
|
|
325
|
-
}
|
|
326
|
-
});
|
|
327
287
|
};
|