@pushpalsdev/cli 1.0.39 → 1.0.40
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/package.json
CHANGED
|
@@ -240,6 +240,7 @@ export class DockerExecutor {
|
|
|
240
240
|
private lastLoggedExecutionConfig = "";
|
|
241
241
|
private lastLoggedEndpointRewrite = "";
|
|
242
242
|
private warmedBackends = new Set<string>();
|
|
243
|
+
private preparedMergeConflictJobs = new Set<string>();
|
|
243
244
|
private mergeConflictRefreshPromise: Promise<void> | null = null;
|
|
244
245
|
private readonly config: WorkerpalsRuntimeConfig;
|
|
245
246
|
|
|
@@ -318,7 +319,6 @@ export class DockerExecutor {
|
|
|
318
319
|
const worktreePath = resolve(this.worktreeDir, worktreeName);
|
|
319
320
|
|
|
320
321
|
try {
|
|
321
|
-
await this.ensureFreshImageForMergeConflictJob(job, onLog);
|
|
322
322
|
const worktreeBaseRef = await this.resolveWorktreeBaseRefForJob(job, onLog);
|
|
323
323
|
// Step 1: Create isolated git worktree
|
|
324
324
|
await this.createWorktree(worktreePath, worktreeBaseRef);
|
|
@@ -398,6 +398,7 @@ export class DockerExecutor {
|
|
|
398
398
|
stderr: `Retries exhausted after ${this.jobRetryMaxAttempts} attempts`,
|
|
399
399
|
};
|
|
400
400
|
} finally {
|
|
401
|
+
this.preparedMergeConflictJobs.delete(job.id);
|
|
401
402
|
this.activeJobs = Math.max(0, this.activeJobs - 1);
|
|
402
403
|
// Step 4: Clean up worktree (always cleanup)
|
|
403
404
|
await this.removeWorktree(worktreePath).catch((err) => {
|
|
@@ -1690,6 +1691,22 @@ export class DockerExecutor {
|
|
|
1690
1691
|
return resolutionType === "merge_conflict";
|
|
1691
1692
|
}
|
|
1692
1693
|
|
|
1694
|
+
shouldPrepareMergeConflictJobBeforeExecution(job: Job): boolean {
|
|
1695
|
+
return this.isMergeConflictResolutionJob(job) && !this.preparedMergeConflictJobs.has(job.id);
|
|
1696
|
+
}
|
|
1697
|
+
|
|
1698
|
+
async prepareMergeConflictJobEnvironment(
|
|
1699
|
+
job: Job,
|
|
1700
|
+
onLog?: (stream: "stdout" | "stderr", line: string) => void,
|
|
1701
|
+
): Promise<void> {
|
|
1702
|
+
await this.ensureFreshImageForMergeConflictJob(job, onLog);
|
|
1703
|
+
this.preparedMergeConflictJobs.add(job.id);
|
|
1704
|
+
}
|
|
1705
|
+
|
|
1706
|
+
recommendedMergeConflictDeferMs(): number {
|
|
1707
|
+
return Math.max(60_000, Math.min(this.options.timeoutMs, 5 * 60_000));
|
|
1708
|
+
}
|
|
1709
|
+
|
|
1693
1710
|
private async ensureFreshImageForMergeConflictJob(
|
|
1694
1711
|
job: Job,
|
|
1695
1712
|
onLog?: (stream: "stdout" | "stderr", line: string) => void,
|
|
@@ -965,6 +965,40 @@ async function failActiveJobOnShutdown(
|
|
|
965
965
|
}
|
|
966
966
|
}
|
|
967
967
|
|
|
968
|
+
async function deferClaimedJobForMaintenance(
|
|
969
|
+
opts: ReturnType<typeof parseArgs>,
|
|
970
|
+
headers: Record<string, string>,
|
|
971
|
+
jobId: string,
|
|
972
|
+
deferMs: number,
|
|
973
|
+
): Promise<{ ok: boolean; availableAt?: string; message?: string }> {
|
|
974
|
+
try {
|
|
975
|
+
const response = await postJsonWithTimeout(`${opts.server}/jobs/${jobId}/defer`, headers, {
|
|
976
|
+
workerId: opts.workerId,
|
|
977
|
+
deferMs,
|
|
978
|
+
});
|
|
979
|
+
const payload = (await response.json().catch(() => ({}))) as {
|
|
980
|
+
ok?: boolean;
|
|
981
|
+
availableAt?: string;
|
|
982
|
+
message?: string;
|
|
983
|
+
};
|
|
984
|
+
if (!response.ok || !payload.ok) {
|
|
985
|
+
return {
|
|
986
|
+
ok: false,
|
|
987
|
+
message: payload.message || `HTTP ${response.status}`,
|
|
988
|
+
};
|
|
989
|
+
}
|
|
990
|
+
return {
|
|
991
|
+
ok: true,
|
|
992
|
+
availableAt: payload.availableAt,
|
|
993
|
+
};
|
|
994
|
+
} catch (error) {
|
|
995
|
+
return {
|
|
996
|
+
ok: false,
|
|
997
|
+
message: error instanceof Error ? error.message : String(error),
|
|
998
|
+
};
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
|
|
968
1002
|
async function workerLoop(
|
|
969
1003
|
opts: ReturnType<typeof parseArgs>,
|
|
970
1004
|
dockerExecutor: DockerExecutor | null,
|
|
@@ -1034,6 +1068,83 @@ async function workerLoop(
|
|
|
1034
1068
|
const job = data.job;
|
|
1035
1069
|
|
|
1036
1070
|
if (job) {
|
|
1071
|
+
if (
|
|
1072
|
+
dockerExecutor &&
|
|
1073
|
+
dockerExecutor.shouldPrepareMergeConflictJobBeforeExecution(job)
|
|
1074
|
+
) {
|
|
1075
|
+
const deferMs = dockerExecutor.recommendedMergeConflictDeferMs();
|
|
1076
|
+
const deferred = await deferClaimedJobForMaintenance(opts, headers, job.id, deferMs);
|
|
1077
|
+
if (!deferred.ok) {
|
|
1078
|
+
console.warn(
|
|
1079
|
+
`[WorkerPals] Failed to defer merge-conflict job ${job.id} for image refresh; falling back to claimed execution path: ${
|
|
1080
|
+
deferred.message || "unknown error"
|
|
1081
|
+
}`,
|
|
1082
|
+
);
|
|
1083
|
+
} else {
|
|
1084
|
+
console.log(
|
|
1085
|
+
`[WorkerPals] Deferred merge-conflict job ${job.id} until ${
|
|
1086
|
+
deferred.availableAt ?? "maintenance complete"
|
|
1087
|
+
} while refreshing Docker image outside claimed-job lifetime.`,
|
|
1088
|
+
);
|
|
1089
|
+
const maintenanceHeartbeat = setInterval(() => {
|
|
1090
|
+
void transport.sendHeartbeat({
|
|
1091
|
+
...buildHeartbeatPayload("idle", null),
|
|
1092
|
+
details: {
|
|
1093
|
+
repo: opts.repo,
|
|
1094
|
+
baseRef: opts.worktreeBaseRef,
|
|
1095
|
+
dockerImage: opts.docker ? opts.dockerImage : null,
|
|
1096
|
+
dockerNetworkMode: opts.docker ? opts.dockerNetworkMode : null,
|
|
1097
|
+
maintenance: "merge_conflict_image_refresh",
|
|
1098
|
+
deferredJobId: job.id,
|
|
1099
|
+
},
|
|
1100
|
+
});
|
|
1101
|
+
}, heartbeatEveryMs);
|
|
1102
|
+
try {
|
|
1103
|
+
await maybeHeartbeat("idle", null, true);
|
|
1104
|
+
await dockerExecutor.prepareMergeConflictJobEnvironment(job);
|
|
1105
|
+
} catch (error) {
|
|
1106
|
+
const detail = redactSensitiveText(
|
|
1107
|
+
error instanceof Error ? error.stack || error.message : String(error),
|
|
1108
|
+
);
|
|
1109
|
+
console.error(
|
|
1110
|
+
`[WorkerPals] Merge-conflict environment preparation failed for ${job.id}: ${detail}`,
|
|
1111
|
+
);
|
|
1112
|
+
try {
|
|
1113
|
+
const failResponse = await postJsonWithTimeout(
|
|
1114
|
+
`${opts.server}/jobs/${job.id}/fail-deferred`,
|
|
1115
|
+
headers,
|
|
1116
|
+
{
|
|
1117
|
+
workerId: opts.workerId,
|
|
1118
|
+
message: "Merge-conflict environment preparation failed",
|
|
1119
|
+
detail,
|
|
1120
|
+
},
|
|
1121
|
+
);
|
|
1122
|
+
const failPayload = (await failResponse.json().catch(() => ({}))) as {
|
|
1123
|
+
ok?: boolean;
|
|
1124
|
+
message?: string;
|
|
1125
|
+
};
|
|
1126
|
+
if (!failResponse.ok || !failPayload.ok) {
|
|
1127
|
+
console.error(
|
|
1128
|
+
`[WorkerPals] Failed to mark deferred job ${job.id} as failed: ${
|
|
1129
|
+
failPayload.message || `HTTP ${failResponse.status}`
|
|
1130
|
+
}`,
|
|
1131
|
+
);
|
|
1132
|
+
}
|
|
1133
|
+
} catch (failErr) {
|
|
1134
|
+
console.error(
|
|
1135
|
+
`[WorkerPals] Failed to mark deferred job ${job.id} as failed: ${
|
|
1136
|
+
failErr instanceof Error ? failErr.message : String(failErr)
|
|
1137
|
+
}`,
|
|
1138
|
+
);
|
|
1139
|
+
}
|
|
1140
|
+
} finally {
|
|
1141
|
+
clearInterval(maintenanceHeartbeat);
|
|
1142
|
+
}
|
|
1143
|
+
await maybeHeartbeat("idle", null, true);
|
|
1144
|
+
continue;
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1037
1148
|
runtimeState.currentJobId = job.id;
|
|
1038
1149
|
runtimeState.currentSessionId = job.sessionId ?? null;
|
|
1039
1150
|
console.log(`[WorkerPals] Claimed job ${job.id} (${job.kind})`);
|