@saeed42/worktree-worker 1.3.1 → 1.3.3
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/main.js +162 -86
- package/package.json +1 -1
package/dist/main.js
CHANGED
|
@@ -13,9 +13,10 @@ var env = {
|
|
|
13
13
|
PORT: parseInt(process.env.PORT || "8787", 10),
|
|
14
14
|
NODE_ENV: process.env.NODE_ENV || "development",
|
|
15
15
|
WORKER_TOKEN: process.env.WORKER_TOKEN || "",
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
16
|
+
// CRITICAL: Must be /project/sandbox to match trigger tasks and OpenCode
|
|
17
|
+
BASE_WORKSPACE_DIR: "/project/sandbox",
|
|
18
|
+
WORKSPACES_ROOT: "/project/workspaces",
|
|
19
|
+
TRIALS_WORKSPACE_DIR: "/project/workspaces/trials",
|
|
19
20
|
DEFAULT_BRANCH: process.env.DEFAULT_BRANCH || "main",
|
|
20
21
|
GIT_TIMEOUT_MS: parseInt(process.env.GIT_TIMEOUT_MS || "60000", 10),
|
|
21
22
|
CLEANUP_AFTER_HOURS: parseInt(process.env.CLEANUP_AFTER_HOURS || "24", 10)
|
|
@@ -613,57 +614,107 @@ var RepoService = class {
|
|
|
613
614
|
if (isSameRepo) {
|
|
614
615
|
log.info("Repository already initialized with same URL, fetching latest");
|
|
615
616
|
await gitService.fetch("origin", repoRoot, auth);
|
|
616
|
-
const
|
|
617
|
+
const targetBranch2 = options.branch || env.DEFAULT_BRANCH;
|
|
617
618
|
const currentBranch = status.branch || "";
|
|
618
|
-
if (
|
|
619
|
-
log.info("Switching to requested branch", { from: currentBranch, to:
|
|
620
|
-
const checkoutResult = await gitService.exec(["checkout",
|
|
619
|
+
if (targetBranch2 !== currentBranch) {
|
|
620
|
+
log.info("Switching to requested branch", { from: currentBranch, to: targetBranch2 });
|
|
621
|
+
const checkoutResult = await gitService.exec(["checkout", targetBranch2], repoRoot);
|
|
621
622
|
if (checkoutResult.code !== 0) {
|
|
622
623
|
await gitService.exec(
|
|
623
|
-
["checkout", "-b",
|
|
624
|
+
["checkout", "-b", targetBranch2, `origin/${targetBranch2}`],
|
|
624
625
|
repoRoot
|
|
625
626
|
);
|
|
626
627
|
}
|
|
627
628
|
}
|
|
628
|
-
const
|
|
629
|
-
const
|
|
630
|
-
return { path: repoRoot, branch:
|
|
629
|
+
const headSha3 = await gitService.getHeadSha(repoRoot);
|
|
630
|
+
const branch3 = await gitService.getCurrentBranch(repoRoot);
|
|
631
|
+
return { path: repoRoot, branch: branch3, headSha: headSha3, remote: status.remote };
|
|
631
632
|
}
|
|
632
633
|
if (!options.force) {
|
|
633
634
|
throw new Error(
|
|
634
635
|
`Repository already initialized with different URL. Current: ${currentUrl}, Requested: ${requestedUrl}. Use force=true to re-initialize (this will delete all worktrees).`
|
|
635
636
|
);
|
|
636
637
|
}
|
|
637
|
-
log.warn("Force re-init: cleaning
|
|
638
|
+
log.warn("Force re-init: cleaning worktrees and updating remote in-place");
|
|
638
639
|
await this.cleanAllWorktrees();
|
|
639
|
-
|
|
640
|
+
const cleanUrl = options.repoUrl.replace(/^https:\/\/[^@]+@/, "https://");
|
|
641
|
+
await gitService.exec(["remote", "set-url", "origin", cleanUrl], repoRoot);
|
|
642
|
+
await gitService.exec(["config", "--local", "user.name", "origin-agent[bot]"], repoRoot);
|
|
643
|
+
await gitService.exec([
|
|
644
|
+
"config",
|
|
645
|
+
"--local",
|
|
646
|
+
"user.email",
|
|
647
|
+
"origin-agent[bot]@users.noreply.github.com"
|
|
648
|
+
], repoRoot);
|
|
649
|
+
await gitService.exec(["config", "--local", "safe.directory", repoRoot], repoRoot);
|
|
650
|
+
const targetBranch = options.branch || env.DEFAULT_BRANCH;
|
|
651
|
+
log.info("Fetching from new remote", { branch: targetBranch });
|
|
652
|
+
await gitService.fetch("origin", repoRoot, auth);
|
|
653
|
+
try {
|
|
654
|
+
await gitService.exec(["checkout", "-B", targetBranch, `origin/${targetBranch}`, "--force"], repoRoot);
|
|
655
|
+
} catch {
|
|
656
|
+
await gitService.exec(["checkout", "-B", targetBranch, "--force"], repoRoot);
|
|
657
|
+
}
|
|
658
|
+
await gitService.exec(["branch", `--set-upstream-to=origin/${targetBranch}`, targetBranch], repoRoot).catch(
|
|
659
|
+
() => {
|
|
660
|
+
}
|
|
661
|
+
);
|
|
662
|
+
const headSha2 = await gitService.getHeadSha(repoRoot);
|
|
663
|
+
const branch2 = await gitService.getCurrentBranch(repoRoot);
|
|
664
|
+
const remote2 = await gitService.getRemoteUrl("origin", repoRoot);
|
|
665
|
+
log.info("Repository re-initialized in-place", { branch: branch2, headSha: headSha2 });
|
|
666
|
+
return { path: repoRoot, branch: branch2, headSha: headSha2, remote: remote2 };
|
|
640
667
|
}
|
|
641
668
|
const parentDir = repoRoot.split("/").slice(0, -1).join("/");
|
|
642
669
|
await mkdir2(parentDir, { recursive: true });
|
|
643
670
|
await mkdir2(env.TRIALS_WORKSPACE_DIR, { recursive: true });
|
|
671
|
+
const dirExists = await stat2(repoRoot).then(() => true).catch(() => false);
|
|
644
672
|
const branch = options.branch || env.DEFAULT_BRANCH;
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
"
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
673
|
+
if (dirExists) {
|
|
674
|
+
log.info("Directory exists, initializing git in-place", { branch });
|
|
675
|
+
await gitService.exec(["init"], repoRoot);
|
|
676
|
+
await gitService.exec(["config", "--local", "user.name", "origin-agent[bot]"], repoRoot);
|
|
677
|
+
await gitService.exec([
|
|
678
|
+
"config",
|
|
679
|
+
"--local",
|
|
680
|
+
"user.email",
|
|
681
|
+
"origin-agent[bot]@users.noreply.github.com"
|
|
682
|
+
], repoRoot);
|
|
683
|
+
await gitService.exec(["config", "--local", "safe.directory", repoRoot], repoRoot);
|
|
684
|
+
const cleanUrl = options.repoUrl.replace(/^https:\/\/[^@]+@/, "https://");
|
|
685
|
+
await gitService.exec(["remote", "add", "origin", cleanUrl], repoRoot).catch(async () => {
|
|
686
|
+
await gitService.exec(["remote", "set-url", "origin", cleanUrl], repoRoot);
|
|
687
|
+
});
|
|
688
|
+
await gitService.fetch("origin", repoRoot, auth);
|
|
689
|
+
await gitService.exec(["checkout", "-B", branch, `origin/${branch}`, "--force"], repoRoot);
|
|
690
|
+
await gitService.exec(["branch", `--set-upstream-to=origin/${branch}`, branch], repoRoot).catch(
|
|
691
|
+
() => {
|
|
692
|
+
}
|
|
693
|
+
);
|
|
694
|
+
} else {
|
|
695
|
+
log.info("Cloning repository", { branch });
|
|
696
|
+
await gitService.cloneRepo(options.repoUrl, repoRoot, {
|
|
697
|
+
branch,
|
|
698
|
+
blobless: true,
|
|
699
|
+
githubToken: options.githubToken
|
|
700
|
+
});
|
|
701
|
+
await gitService.exec(["config", "--local", "user.name", "origin-agent[bot]"], repoRoot);
|
|
702
|
+
await gitService.exec([
|
|
703
|
+
"config",
|
|
704
|
+
"--local",
|
|
705
|
+
"user.email",
|
|
706
|
+
"origin-agent[bot]@users.noreply.github.com"
|
|
707
|
+
], repoRoot);
|
|
708
|
+
await gitService.exec(["config", "--local", "safe.directory", repoRoot], repoRoot);
|
|
709
|
+
const cleanUrl = options.repoUrl.replace(/^https:\/\/[^@]+@/, "https://");
|
|
710
|
+
await gitService.exec(["remote", "set-url", "origin", cleanUrl], repoRoot);
|
|
711
|
+
log.info("Fetching all remote refs");
|
|
712
|
+
await gitService.fetch("origin", repoRoot, auth);
|
|
713
|
+
await gitService.exec(["branch", `--set-upstream-to=origin/${branch}`, branch], repoRoot).catch(
|
|
714
|
+
() => {
|
|
715
|
+
}
|
|
716
|
+
);
|
|
717
|
+
}
|
|
667
718
|
const isEmpty = await gitService.isEmptyRepo(repoRoot);
|
|
668
719
|
if (isEmpty) {
|
|
669
720
|
log.warn("Repository is empty (no commits)", { repoUrl: options.repoUrl });
|
|
@@ -873,6 +924,7 @@ var WorktreeService = class {
|
|
|
873
924
|
hasRepoUrl: !!options.repoUrl
|
|
874
925
|
});
|
|
875
926
|
await mkdir3(env.TRIALS_WORKSPACE_DIR, { recursive: true });
|
|
927
|
+
await this.opportunisticCleanup();
|
|
876
928
|
let repoStatus = await repoService.getStatus();
|
|
877
929
|
const isValidGitHubRemote = (remote) => {
|
|
878
930
|
if (!remote) return false;
|
|
@@ -1118,59 +1170,98 @@ var WorktreeService = class {
|
|
|
1118
1170
|
return { branch, pushed: true };
|
|
1119
1171
|
}
|
|
1120
1172
|
/**
|
|
1121
|
-
*
|
|
1122
|
-
*
|
|
1123
|
-
* Strategy:
|
|
1124
|
-
* 1. Always clean worktrees older than CLEANUP_AFTER_HOURS (default 24h)
|
|
1125
|
-
* 2. If disk usage is high (>80%), also clean worktrees older than 6h
|
|
1126
|
-
* 3. If disk usage is critical (>90%), clean all worktrees older than 1h
|
|
1173
|
+
* Count current worktrees
|
|
1127
1174
|
*/
|
|
1128
|
-
async
|
|
1129
|
-
const log = logger.child({ service: "worktree", action: "cleanup" });
|
|
1130
|
-
let cleaned = 0;
|
|
1131
|
-
const errors = [];
|
|
1132
|
-
let diskUsagePercent;
|
|
1133
|
-
let cutoffHours = env.CLEANUP_AFTER_HOURS;
|
|
1175
|
+
async countWorktrees() {
|
|
1134
1176
|
try {
|
|
1135
|
-
const
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
log.warn("Critical disk usage, aggressive cleanup", { diskUsagePercent, cutoffHours });
|
|
1140
|
-
} else if (diskUsagePercent > 80) {
|
|
1141
|
-
cutoffHours = 6;
|
|
1142
|
-
log.info("High disk usage, moderate cleanup", { diskUsagePercent, cutoffHours });
|
|
1143
|
-
}
|
|
1144
|
-
} catch (err) {
|
|
1145
|
-
log.warn("Could not check disk usage", { error: err instanceof Error ? err.message : String(err) });
|
|
1177
|
+
const entries = await readdir2(env.TRIALS_WORKSPACE_DIR, { withFileTypes: true });
|
|
1178
|
+
return entries.filter((e) => e.isDirectory()).length;
|
|
1179
|
+
} catch {
|
|
1180
|
+
return 0;
|
|
1146
1181
|
}
|
|
1147
|
-
|
|
1182
|
+
}
|
|
1183
|
+
/**
|
|
1184
|
+
* Get worktrees sorted by modification time (oldest first)
|
|
1185
|
+
*/
|
|
1186
|
+
async getWorktreesByAge() {
|
|
1148
1187
|
try {
|
|
1149
1188
|
const entries = await readdir2(env.TRIALS_WORKSPACE_DIR, { withFileTypes: true });
|
|
1150
|
-
const
|
|
1189
|
+
const worktrees = [];
|
|
1151
1190
|
for (const entry of entries) {
|
|
1152
1191
|
if (!entry.isDirectory()) continue;
|
|
1153
1192
|
const worktreePath = `${env.TRIALS_WORKSPACE_DIR}/${entry.name}`;
|
|
1154
1193
|
try {
|
|
1155
1194
|
const stats = await stat3(worktreePath);
|
|
1156
|
-
|
|
1195
|
+
worktrees.push({ path: worktreePath, mtime: stats.mtimeMs });
|
|
1157
1196
|
} catch {
|
|
1158
1197
|
}
|
|
1159
1198
|
}
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1199
|
+
return worktrees.sort((a, b) => a.mtime - b.mtime);
|
|
1200
|
+
} catch {
|
|
1201
|
+
return [];
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
1204
|
+
/**
|
|
1205
|
+
* Opportunistic cleanup - run before creating new worktrees
|
|
1206
|
+
*
|
|
1207
|
+
* Strategy:
|
|
1208
|
+
* 1. Always clean worktrees older than CLEANUP_AFTER_HOURS
|
|
1209
|
+
* 2. If count exceeds MAX_WORKTREES, clean oldest until under limit
|
|
1210
|
+
*/
|
|
1211
|
+
async opportunisticCleanup(maxWorktrees = 20) {
|
|
1212
|
+
const log = logger.child({ service: "worktree", action: "opportunistic-cleanup" });
|
|
1213
|
+
try {
|
|
1214
|
+
const worktrees = await this.getWorktreesByAge();
|
|
1215
|
+
const count = worktrees.length;
|
|
1216
|
+
const cutoffTime = Date.now() - env.CLEANUP_AFTER_HOURS * 60 * 60 * 1e3;
|
|
1217
|
+
let cleaned = 0;
|
|
1218
|
+
for (const wt of worktrees) {
|
|
1219
|
+
const isStale = wt.mtime < cutoffTime;
|
|
1220
|
+
const isOverLimit = count - cleaned > maxWorktrees;
|
|
1221
|
+
if (isStale || isOverLimit) {
|
|
1163
1222
|
try {
|
|
1164
|
-
|
|
1223
|
+
await rm2(wt.path, { recursive: true, force: true });
|
|
1224
|
+
cleaned++;
|
|
1225
|
+
log.debug("Cleaned worktree", {
|
|
1165
1226
|
path: wt.path,
|
|
1227
|
+
reason: isStale ? "stale" : "over_limit",
|
|
1166
1228
|
ageHours: Math.round((Date.now() - wt.mtime) / (60 * 60 * 1e3))
|
|
1167
1229
|
});
|
|
1168
|
-
|
|
1230
|
+
} catch {
|
|
1231
|
+
}
|
|
1232
|
+
}
|
|
1233
|
+
}
|
|
1234
|
+
if (cleaned > 0) {
|
|
1235
|
+
log.info("Opportunistic cleanup completed", { cleaned, remaining: count - cleaned });
|
|
1236
|
+
await gitService.pruneWorktrees(env.BASE_WORKSPACE_DIR).catch(() => {
|
|
1237
|
+
});
|
|
1238
|
+
}
|
|
1239
|
+
} catch {
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
1242
|
+
/**
|
|
1243
|
+
* Cleanup stale worktrees
|
|
1244
|
+
*/
|
|
1245
|
+
async cleanupStaleWorktrees() {
|
|
1246
|
+
const log = logger.child({ service: "worktree", action: "cleanup" });
|
|
1247
|
+
let cleaned = 0;
|
|
1248
|
+
const errors = [];
|
|
1249
|
+
const cutoffTime = Date.now() - env.CLEANUP_AFTER_HOURS * 60 * 60 * 1e3;
|
|
1250
|
+
try {
|
|
1251
|
+
const entries = await readdir2(env.TRIALS_WORKSPACE_DIR, { withFileTypes: true });
|
|
1252
|
+
for (const entry of entries) {
|
|
1253
|
+
if (!entry.isDirectory()) continue;
|
|
1254
|
+
const worktreePath = `${env.TRIALS_WORKSPACE_DIR}/${entry.name}`;
|
|
1255
|
+
try {
|
|
1256
|
+
const stats = await stat3(worktreePath);
|
|
1257
|
+
if (stats.mtimeMs < cutoffTime) {
|
|
1258
|
+
log.info("Cleaning up stale worktree", { path: worktreePath });
|
|
1259
|
+
await rm2(worktreePath, { recursive: true, force: true });
|
|
1169
1260
|
cleaned++;
|
|
1170
|
-
} catch (err) {
|
|
1171
|
-
const errMsg = err instanceof Error ? err.message : String(err);
|
|
1172
|
-
errors.push(`${wt.path}: ${errMsg}`);
|
|
1173
1261
|
}
|
|
1262
|
+
} catch (err) {
|
|
1263
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
1264
|
+
errors.push(`${worktreePath}: ${errMsg}`);
|
|
1174
1265
|
}
|
|
1175
1266
|
}
|
|
1176
1267
|
await gitService.pruneWorktrees(env.BASE_WORKSPACE_DIR);
|
|
@@ -1178,23 +1269,8 @@ var WorktreeService = class {
|
|
|
1178
1269
|
const errMsg = err instanceof Error ? err.message : String(err);
|
|
1179
1270
|
errors.push(`readdir: ${errMsg}`);
|
|
1180
1271
|
}
|
|
1181
|
-
log.info("Cleanup completed", { cleaned, errorCount: errors.length
|
|
1182
|
-
return { cleaned, errors
|
|
1183
|
-
}
|
|
1184
|
-
/**
|
|
1185
|
-
* Get disk usage information for the workspace partition
|
|
1186
|
-
*/
|
|
1187
|
-
async getDiskUsage() {
|
|
1188
|
-
const { exec } = await import("child_process");
|
|
1189
|
-
const { promisify } = await import("util");
|
|
1190
|
-
const execAsync = promisify(exec);
|
|
1191
|
-
const { stdout } = await execAsync(`df -B1 ${env.BASE_WORKSPACE_DIR} | tail -1`);
|
|
1192
|
-
const parts = stdout.trim().split(/\s+/);
|
|
1193
|
-
const total = parseInt(parts[1], 10);
|
|
1194
|
-
const used = parseInt(parts[2], 10);
|
|
1195
|
-
const free = parseInt(parts[3], 10);
|
|
1196
|
-
const usedPercent = parseInt(parts[4].replace("%", ""), 10);
|
|
1197
|
-
return { total, used, free, usedPercent };
|
|
1272
|
+
log.info("Cleanup completed", { cleaned, errorCount: errors.length });
|
|
1273
|
+
return { cleaned, errors };
|
|
1198
1274
|
}
|
|
1199
1275
|
};
|
|
1200
1276
|
var worktreeService = new WorktreeService();
|