patchrelay 0.41.6 → 0.41.8
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/dist/build-info.json +3 -3
- package/dist/hook-runner.js +18 -1
- package/dist/worktree-manager.js +52 -3
- package/package.json +1 -1
package/dist/build-info.json
CHANGED
package/dist/hook-runner.js
CHANGED
|
@@ -8,7 +8,7 @@ export async function runProjectHook(repoPath, hookName, options) {
|
|
|
8
8
|
}
|
|
9
9
|
const result = await execCommand(hookPath, [], {
|
|
10
10
|
cwd: options.cwd,
|
|
11
|
-
env: { ...
|
|
11
|
+
env: { ...sanitizedParentEnv(), ...(options.env ?? {}) },
|
|
12
12
|
timeoutMs: options.timeoutMs ?? 120_000,
|
|
13
13
|
});
|
|
14
14
|
return {
|
|
@@ -18,6 +18,23 @@ export async function runProjectHook(repoPath, hookName, options) {
|
|
|
18
18
|
stderr: result.stderr,
|
|
19
19
|
};
|
|
20
20
|
}
|
|
21
|
+
// Patchrelay runs as a service with NODE_ENV=production, which makes `npm ci`
|
|
22
|
+
// in a project hook silently omit devDependencies. The subject project's install
|
|
23
|
+
// surface is a separate concern from patchrelay's own runtime, so scrub the
|
|
24
|
+
// variables that would leak patchrelay's production posture into the hook.
|
|
25
|
+
const STRIPPED_PARENT_ENV_VARS = [
|
|
26
|
+
"NODE_ENV",
|
|
27
|
+
"NPM_CONFIG_PRODUCTION",
|
|
28
|
+
"NPM_CONFIG_OMIT",
|
|
29
|
+
"NPM_CONFIG_INCLUDE",
|
|
30
|
+
];
|
|
31
|
+
function sanitizedParentEnv() {
|
|
32
|
+
const env = { ...process.env };
|
|
33
|
+
for (const key of STRIPPED_PARENT_ENV_VARS) {
|
|
34
|
+
delete env[key];
|
|
35
|
+
}
|
|
36
|
+
return env;
|
|
37
|
+
}
|
|
21
38
|
export function buildHookEnv(issueKey, branchName, stage, worktreePath) {
|
|
22
39
|
return {
|
|
23
40
|
PATCHRELAY_ISSUE_KEY: issueKey,
|
package/dist/worktree-manager.js
CHANGED
|
@@ -1,6 +1,17 @@
|
|
|
1
1
|
import { existsSync, lstatSync, realpathSync } from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { ensureDir, execCommand } from "./utils.js";
|
|
4
|
+
const FETCH_RETRY_DELAYS_MS = [0, 1_000];
|
|
5
|
+
const TRANSIENT_FETCH_ERROR_PATTERNS = [
|
|
6
|
+
/connection reset by peer/i,
|
|
7
|
+
/recv failure/i,
|
|
8
|
+
/tls handshake timeout/i,
|
|
9
|
+
/connection timed out/i,
|
|
10
|
+
/operation timed out/i,
|
|
11
|
+
/timed out after \d+ms/i,
|
|
12
|
+
/remote end hung up unexpectedly/i,
|
|
13
|
+
/unexpected disconnect while reading sideband packet/i,
|
|
14
|
+
];
|
|
4
15
|
export class WorktreeManager {
|
|
5
16
|
config;
|
|
6
17
|
constructor(config) {
|
|
@@ -84,9 +95,7 @@ export class WorktreeManager {
|
|
|
84
95
|
// Fetch latest main so the branch forks from a clean, up-to-date base.
|
|
85
96
|
// This prevents branch contamination when local HEAD has drifted.
|
|
86
97
|
// freshenWorktree in run-orchestrator acts as a secondary safety net.
|
|
87
|
-
const fetchResult = await
|
|
88
|
-
timeoutMs: 60_000,
|
|
89
|
-
});
|
|
98
|
+
const fetchResult = await this.fetchWithTransientRetry(repoPath, "main");
|
|
90
99
|
if (fetchResult.exitCode !== 0) {
|
|
91
100
|
throw new Error(`Failed to fetch origin/main before creating issue worktree: ${fetchResult.stderr?.slice(0, 300) ?? "git fetch failed"}`);
|
|
92
101
|
}
|
|
@@ -95,6 +104,34 @@ export class WorktreeManager {
|
|
|
95
104
|
throw new Error(`Failed to create issue worktree at ${worktreePath}: ${addResult.stderr?.slice(0, 300) ?? "git worktree add failed"}`);
|
|
96
105
|
}
|
|
97
106
|
}
|
|
107
|
+
async fetchWithTransientRetry(repoPath, branchName) {
|
|
108
|
+
let lastResult;
|
|
109
|
+
let lastError;
|
|
110
|
+
for (let attempt = 0; attempt <= FETCH_RETRY_DELAYS_MS.length; attempt += 1) {
|
|
111
|
+
if (attempt > 0) {
|
|
112
|
+
await delay(FETCH_RETRY_DELAYS_MS[attempt - 1] ?? 0);
|
|
113
|
+
}
|
|
114
|
+
try {
|
|
115
|
+
const result = await execCommand(this.config.runner.gitBin, ["-C", repoPath, "fetch", "origin", branchName], {
|
|
116
|
+
timeoutMs: 60_000,
|
|
117
|
+
});
|
|
118
|
+
if (result.exitCode === 0 || !isTransientFetchFailure(result.stderr)) {
|
|
119
|
+
return result;
|
|
120
|
+
}
|
|
121
|
+
lastResult = result;
|
|
122
|
+
}
|
|
123
|
+
catch (error) {
|
|
124
|
+
if (!isTransientFetchFailure(error instanceof Error ? error.message : String(error))) {
|
|
125
|
+
throw error;
|
|
126
|
+
}
|
|
127
|
+
lastError = error instanceof Error ? error : new Error(String(error));
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
if (lastResult) {
|
|
131
|
+
return lastResult;
|
|
132
|
+
}
|
|
133
|
+
throw lastError ?? new Error(`Failed to fetch origin/${branchName}`);
|
|
134
|
+
}
|
|
98
135
|
async assertTrustedExistingWorktree(repoPath, worktreeRoot, worktreePath, options) {
|
|
99
136
|
const worktreeStats = lstatSync(worktreePath);
|
|
100
137
|
if (worktreeStats.isSymbolicLink()) {
|
|
@@ -145,3 +182,15 @@ function isPathWithinRoot(rootPath, candidatePath) {
|
|
|
145
182
|
const relative = path.relative(rootPath, candidatePath);
|
|
146
183
|
return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));
|
|
147
184
|
}
|
|
185
|
+
function isTransientFetchFailure(message) {
|
|
186
|
+
if (!message) {
|
|
187
|
+
return false;
|
|
188
|
+
}
|
|
189
|
+
return TRANSIENT_FETCH_ERROR_PATTERNS.some((pattern) => pattern.test(message));
|
|
190
|
+
}
|
|
191
|
+
function delay(delayMs) {
|
|
192
|
+
if (delayMs <= 0) {
|
|
193
|
+
return Promise.resolve();
|
|
194
|
+
}
|
|
195
|
+
return new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
196
|
+
}
|