Skip to content

Commit 135069b

Browse files
committed
refactor: isolate Poki upload process by using temporary directories and improve type safety for PipelabContext paths.
1 parent 4f14bee commit 135069b

4 files changed

Lines changed: 129 additions & 45 deletions

File tree

packages/core-node/src/context.ts

Lines changed: 31 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,12 @@ export interface PipelabContextOptions {
6666
releaseTag?: string;
6767
}
6868

69+
type Join<T extends string[], D extends string> =
70+
T extends [] ? "" :
71+
T extends [infer F extends string] ? F :
72+
T extends [infer F extends string, ...infer R extends string[]] ? `${F}${D}${Join<R, D>}` :
73+
string;
74+
6975
export class PipelabContext {
7076
public readonly userDataPath: string;
7177
public readonly releaseTag: string;
@@ -75,27 +81,30 @@ export class PipelabContext {
7581
this.releaseTag = options.releaseTag || "latest";
7682
}
7783

78-
getPackagesPath(...subpaths: string[]) {
84+
getPackagesPath<S extends string[]>(...subpaths: S): `PACKAGES/${Join<S, "/">}`;
85+
getPackagesPath(...subpaths: string[]): string {
7986
return join(this.userDataPath, "packages", ...subpaths);
8087
}
8188

82-
getThirdPartyPath(...subpaths: string[]) {
89+
getThirdPartyPath<S extends string[]>(...subpaths: S): `THIRDPARTY/${Join<S, "/">}`;
90+
getThirdPartyPath(...subpaths: string[]): string {
8391
return join(this.userDataPath, "thirdparty", ...subpaths);
8492
}
8593

86-
getConfigPath(...subpaths: string[]) {
94+
getConfigPath<S extends string[]>(...subpaths: S): `CONFIG/${Join<S, "/">}`;
95+
getConfigPath(...subpaths: string[]): string {
8796
return join(this.userDataPath, "config", ...subpaths);
8897
}
8998

90-
getSettingsPath() {
99+
getSettingsPath(): `CONFIG/settings.json` {
91100
return this.getConfigPath("settings.json");
92101
}
93102

94-
getConnectionsPath() {
103+
getConnectionsPath(): `CONFIG/connections.json` {
95104
return this.getConfigPath("connections.json");
96105
}
97106

98-
getProjectsPath() {
107+
getProjectsPath(): `CONFIG/projects.json` {
99108
return this.getConfigPath("projects.json");
100109
}
101110

@@ -121,22 +130,24 @@ export class PipelabContext {
121130
}
122131
}
123132

124-
getTempPath(...subpaths: string[]) {
133+
getTempPath<S extends string[]>(...subpaths: S): `TEMP/${Join<S, "/">}`;
134+
getTempPath(...subpaths: string[]): string {
125135
const settings = this.getSettings();
126136
const base = settings?.tempFolder || join(this.userDataPath, "temp");
127137
return join(base, ...subpaths);
128138
}
129139

130-
async createTempFolder(prefix = "pipelab-") {
140+
createTempFolder<T extends string = "pipelab-">(prefix?: T): Promise<`TEMP/${T}${string}`>;
141+
async createTempFolder(prefix = "pipelab-"): Promise<string> {
131142
const baseDir = this.getTempPath();
132143
await mkdir(baseDir, { recursive: true });
133144
const realBaseDir = await realpath(baseDir);
134-
return await mkdtemp(join(realBaseDir, prefix));
145+
return mkdtemp(join(realBaseDir, prefix));
135146
}
136147

137-
getCachePath(): string;
138-
getCachePath(folder: CacheFolderType, ...subpaths: string[]): string;
139-
getCachePath(folder?: CacheFolderType, ...subpaths: string[]) {
148+
getCachePath(): `CACHE/`;
149+
getCachePath<F extends CacheFolderType, S extends string[]>(folder: F, ...subpaths: S): `CACHE/${F}/${Join<S, "/">}`;
150+
getCachePath(folder?: CacheFolderType, ...subpaths: string[]): string {
140151
const settings = this.getSettings();
141152
const base = settings?.cacheFolder || join(this.userDataPath, "cache");
142153
if (!folder) {
@@ -145,20 +156,24 @@ export class PipelabContext {
145156
return join(base, folder, ...subpaths);
146157
}
147158

148-
getPnpmPath(...subpaths: string[]) {
159+
getPnpmPath<S extends string[]>(...subpaths: S): `PNPM/${Join<S, "/">}`;
160+
getPnpmPath(...subpaths: string[]): string {
149161
return join(this.userDataPath, "pnpm", ...subpaths);
150162
}
151163

152-
getBuildHistoryPath(...subpaths: string[]) {
164+
getBuildHistoryPath<S extends string[]>(...subpaths: S): `BUILD_HISTORY/${Join<S, "/">}`;
165+
getBuildHistoryPath(...subpaths: string[]): string {
153166
return join(this.userDataPath, "build-history", ...subpaths);
154167
}
155168

156-
getNodePath(version = DEFAULT_NODE_VERSION) {
169+
getNodePath<V extends string = typeof DEFAULT_NODE_VERSION>(version?: V): `THIRDPARTY/node/${V}/${string}`;
170+
getNodePath(version = DEFAULT_NODE_VERSION): string {
157171
const isWindows = process.platform === "win32";
158172
return this.getThirdPartyPath("node", version, isWindows ? "node.exe" : "bin/node");
159173
}
160174

161-
getPnpmBinPath(version = DEFAULT_PNPM_VERSION) {
175+
getPnpmBinPath<V extends string = typeof DEFAULT_PNPM_VERSION>(version?: V): `PACKAGES/pnpm/${V}/bin/pnpm.cjs`;
176+
getPnpmBinPath(version = DEFAULT_PNPM_VERSION): string {
162177
return this.getPackagesPath("pnpm", version, "bin", "pnpm.cjs");
163178
}
164179

plugins/plugin-construct/src/export-shared.ts

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import * as v from "valibot";
1515
import { BrowserContext } from "playwright";
1616
import { dirname, join, delimiter } from "node:path";
1717
import { cp, mkdir } from "node:fs/promises";
18+
import { existsSync } from "node:fs";
1819
import { homedir } from "node:os";
1920
import { createRequire } from "node:module";
2021

@@ -190,13 +191,24 @@ export const exportc3p = async <ACTION extends Action>(
190191
// if (newInputs.customBrowser && newInputs.customProfile) {
191192
if (newInputs.customProfile) {
192193
const customProfile = join(cwd, "playwright-profile");
194+
log("Setting up Playwright profile from custom Chrome profile...");
195+
log(` - Target playwright-profile folder: ${customProfile}`);
193196

194197
await mkdir(customProfile, {
195198
recursive: true,
196199
});
197200

198201
const indexedDbPathSource = join(newInputs.customProfile, "Default", "IndexedDB");
199202
const indexedDbPathDestination = join(customProfile, "Default", "IndexedDB");
203+
log(` - Source IndexedDB folder: ${indexedDbPathSource}`);
204+
log(` - Destination IndexedDB folder: ${indexedDbPathDestination}`);
205+
206+
if (!existsSync(indexedDbPathSource)) {
207+
log(
208+
` [WARNING] Source IndexedDB directory does not exist: "${indexedDbPathSource}". Verify your custom profile path.`,
209+
);
210+
}
211+
200212
await mkdir(indexedDbPathDestination, { recursive: true });
201213

202214
const pathsToCopy = [
@@ -207,10 +219,19 @@ export const exportc3p = async <ACTION extends Action>(
207219
for (const p of pathsToCopy) {
208220
const from = join(indexedDbPathSource, p);
209221
const to = join(indexedDbPathDestination, p);
210-
try {
211-
await cp(from, to, { recursive: true });
212-
} catch (e) {
213-
// Skip files/directories that do not exist in the source profile
222+
if (existsSync(from)) {
223+
log(` - Copying: "${p}" to "${indexedDbPathDestination}"`);
224+
try {
225+
await cp(from, to, { recursive: true });
226+
log(` [OK] Successfully copied "${p}"`);
227+
} catch (e) {
228+
log(
229+
` [ERROR] Failed to copy "${p}":`,
230+
e instanceof Error ? `${e.message}\n${e.stack}` : String(e),
231+
);
232+
}
233+
} else {
234+
log(` - Skipping: "${p}" (does not exist in source profile)`);
214235
}
215236
}
216237

plugins/plugin-poki/src/export.test.ts

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { expect, test, describe, afterEach } from "vitest";
22
import { uploadToPokiRunner, POKI_CLI_VERSION } from "./export.js";
3-
import { mkdir, writeFile, readFile, access } from "node:fs/promises";
3+
import { mkdir, writeFile, readFile, access, readdir } from "node:fs/promises";
44
import { join, resolve, dirname } from "node:path";
55
import { fileURLToPath } from "node:url";
66
import { createSandbox, runAction } from "@pipelab/test-utils";
@@ -82,15 +82,21 @@ describe("End-to-End: Poki Upload Action", () => {
8282
}
8383

8484
// 4. Verification
85-
const pokiJsonPath = join(sandbox.path, "poki.json");
85+
const tempPath = join(sandbox.path, "user-data", "temp");
86+
const files = await readdir(tempPath);
87+
const uploadFolder = files.find((f) => f.startsWith("poki-upload-"));
88+
if (!uploadFolder) throw new Error("Temp upload folder not found");
89+
const tempUploadFolder = join(tempPath, uploadFolder);
90+
91+
const pokiJsonPath = join(tempUploadFolder, "poki.json");
8692
console.log("pokiJsonPath test", pokiJsonPath);
8793
await expect(access(pokiJsonPath)).resolves.not.toThrow();
8894

8995
const pokiJsonContent = JSON.parse(await readFile(pokiJsonPath, "utf-8"));
9096
expect(pokiJsonContent.game_id).toBe("poki-game-123");
9197

9298
// Verify that the Poki CLI process was indeed run with the sandboxed thirdparty environment variables
93-
const mockEnvPath = join(sandbox.path, "mock-env.json");
99+
const mockEnvPath = join(tempUploadFolder, "mock-env.json");
94100
await expect(access(mockEnvPath)).resolves.not.toThrow();
95101
const mockEnv = JSON.parse(await readFile(mockEnvPath, "utf-8"));
96102
const expectedSandboxConfigDir = join(sandbox.path, SandboxFolder.ThirdParty);
@@ -103,9 +109,10 @@ describe("End-to-End: Poki Upload Action", () => {
103109
expect(mockEnv.argv).toContain("E2E test notes");
104110

105111
// Verify that the input files were copied to the dist directory
106-
const distHtmlPath = join(sandbox.path, "dist", "index.html");
107-
await expect(access(distHtmlPath)).resolves.not.toThrow();
108-
const htmlContent = await readFile(distHtmlPath, "utf-8");
112+
const absoluteBuildDir = join(tempUploadFolder, pokiJsonContent.build_dir);
113+
const htmlPath = join(absoluteBuildDir, "index.html");
114+
await expect(access(htmlPath)).resolves.not.toThrow();
115+
const htmlContent = await readFile(htmlPath, "utf-8");
109116
expect(htmlContent).toBe("<html><body>Test</body></html>");
110117
},
111118
30 * 60 * 1000,

plugins/plugin-poki/src/export.ts

Lines changed: 60 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@ import {
66
fetchPackage,
77
runWithLiveLogs,
88
} from "@pipelab/plugin-core";
9-
import { dirname, join, delimiter } from "node:path";
10-
import { writeFile, cp, mkdir } from "node:fs/promises";
9+
import { dirname, join, delimiter, resolve, relative } from "node:path";
10+
import { writeFile, cp, access } from "node:fs/promises";
1111

1212
export const ID = "poki-upload";
1313
export const POKI_CLI_VERSION = "0.1.19";
@@ -53,55 +53,96 @@ export const uploadToPoki = createAction({
5353
export const uploadToPokiRunner = createActionRunner<typeof uploadToPoki>(
5454
async ({ log, inputs, paths, abortSignal, cwd, context }) => {
5555
const { node, thirdparty, pnpm, userData } = paths;
56+
57+
const absoluteInputFolder = resolve(inputs["input-folder"] as string);
58+
59+
log("Starting Poki upload action...");
60+
log(`- Game ID: ${inputs.project}`);
61+
log(`- Version name: ${inputs.name}`);
62+
log(`- Version notes: ${inputs.notes}`);
63+
log(`- Input folder: ${absoluteInputFolder}`);
64+
65+
log(`Fetching @poki/cli version ${POKI_CLI_VERSION}...`);
5666
const { packageDir: pokiDir } = await fetchPackage("@poki/cli", POKI_CLI_VERSION, {
5767
context,
5868
installDeps: true,
5969
});
6070
const poki = join(pokiDir, "bin", "index.js");
71+
log(`Successfully resolved @poki/cli. Executable: ${poki}`);
6172

62-
const dist = join(cwd, "dist");
73+
const tempUploadFolder = await context.createTempFolder("poki-upload-");
74+
log(`Created temporary upload root directory: ${tempUploadFolder}`);
6375

64-
await mkdir(dist, { recursive: true });
65-
await cp(inputs["input-folder"] as string, dist, {
76+
const tempDistFolder = join(tempUploadFolder, "dist");
77+
log(`Copying build files from "${absoluteInputFolder}" to "${tempDistFolder}"...`);
78+
await cp(absoluteInputFolder, tempDistFolder, {
6679
recursive: true,
6780
});
6881

69-
const pokiJsonPath = join(cwd, "poki.json");
82+
const pokiJsonPath = join(tempUploadFolder, "poki.json");
7083

71-
console.log("pokiJsonPath", pokiJsonPath);
84+
log(`Writing temporary poki.json configuration at: ${pokiJsonPath}`);
85+
const pokiConfig = {
86+
game_id: inputs.project,
87+
build_dir: "dist",
88+
};
89+
log(`poki.json configuration content:\n${JSON.stringify(pokiConfig, null, 2)}`);
7290

7391
// create file at the same place the folder to upload
7492
await writeFile(
7593
pokiJsonPath,
76-
JSON.stringify(
77-
{
78-
game_id: inputs.project,
79-
build_dir: "dist",
80-
},
81-
undefined,
82-
2,
83-
),
94+
JSON.stringify(pokiConfig, undefined, 2),
8495
"utf-8",
8596
);
8697

87-
log("process.env.MSW_BRIDGE_PORT", process.env.MSW_BRIDGE_PORT);
88-
log("process.env.NODE_OPTIONS", process.env.NODE_OPTIONS);
89-
9098
// Direct Poki CLI to read/write credentials inside Pipelab's thirdparty folder
9199
const sandboxConfigDir = thirdparty;
92100

101+
log("Checking for sandboxed authentication credentials...");
102+
const possibleAuthPaths = [
103+
join(sandboxConfigDir, "poki", "auth.json"),
104+
join(sandboxConfigDir, "Poki", "auth.json"),
105+
];
106+
let authFileFound = false;
107+
let foundPath = "";
108+
for (const p of possibleAuthPaths) {
109+
try {
110+
await access(p);
111+
authFileFound = true;
112+
foundPath = p;
113+
break;
114+
} catch {}
115+
}
116+
117+
if (authFileFound) {
118+
log(`[Poki] Authentication file found at: ${foundPath}`);
119+
} else {
120+
log("[Poki] [WARNING] No authentication file (auth.json) found in the sandboxed config directory.");
121+
log("[Poki] [WARNING] Poki CLI might try to open a browser for interactive login, which could hang/fail in headless environments.");
122+
log(`[Poki] Expected location: ${join(sandboxConfigDir, "poki", "auth.json")} or ${join(sandboxConfigDir, "Poki", "auth.json")}`);
123+
}
124+
125+
log("Configuring environment variables:");
126+
log(` - XDG_CONFIG_HOME: ${sandboxConfigDir}`);
127+
log(` - LOCALAPPDATA: ${sandboxConfigDir}`);
128+
log(` - PATH: ${dirname(node)}${delimiter}${process.env.PATH}`);
129+
log(` - MSW_BRIDGE_PORT: ${process.env.MSW_BRIDGE_PORT || "not set"}`);
130+
log(` - NODE_OPTIONS: ${process.env.NODE_OPTIONS || "not set"}`);
131+
93132
const env: Record<string, string> = {
94133
...process.env,
95134
XDG_CONFIG_HOME: sandboxConfigDir,
96135
LOCALAPPDATA: sandboxConfigDir,
97136
PATH: `${dirname(node)}${delimiter}${process.env.PATH}`,
98137
};
99138

139+
log(`Running Poki CLI upload command: node ${poki} upload --name "${inputs.name}" --notes "${inputs.notes}"`);
140+
100141
await runWithLiveLogs(
101142
node,
102143
[poki, "upload", "--name", inputs.name as string, "--notes", inputs.notes as string],
103144
{
104-
cwd,
145+
cwd: tempUploadFolder,
105146
env,
106147
cancelSignal: abortSignal,
107148
},

0 commit comments

Comments
 (0)