@ricsam/r5d-worker 0.0.76 → 0.0.77
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/cjs/git-process-environment.cjs +133 -0
- package/dist/cjs/main.cjs +267 -100
- package/dist/cjs/package.json +1 -1
- package/dist/cjs/project-worktrees.cjs +72 -47
- package/dist/cjs/working-tree-mirror.cjs +2 -1
- package/dist/cjs/workspace-git-sync.cjs +67 -53
- package/dist/mjs/git-process-environment.mjs +105 -0
- package/dist/mjs/main.mjs +266 -100
- package/dist/mjs/package.json +1 -1
- package/dist/mjs/project-worktrees.mjs +72 -47
- package/dist/mjs/working-tree-mirror.mjs +2 -1
- package/dist/mjs/workspace-git-sync.mjs +67 -53
- package/dist/types/git-process-environment.d.ts +9 -0
- package/dist/types/main.d.ts +24 -0
- package/dist/types/project-worktrees.d.ts +22 -10
- package/dist/types/workspace-git-sync.d.ts +18 -5
- package/package.json +1 -1
package/dist/mjs/main.mjs
CHANGED
|
@@ -5,6 +5,7 @@ import { createHash } from "node:crypto";
|
|
|
5
5
|
import os, { hostname } from "node:os";
|
|
6
6
|
import { spawn as spawnChildProcess } from "node:child_process";
|
|
7
7
|
import { installCliUpdate, readInstalledCliVersion } from "./cli-update.mjs";
|
|
8
|
+
import { gitTransportSecurityArgs, workerGitProcessEnvironment } from "./git-process-environment.mjs";
|
|
8
9
|
import {
|
|
9
10
|
grantWorkerHeartbeatBusyGrace,
|
|
10
11
|
hasWorkerHeartbeatTimedOutWithBusyGrace,
|
|
@@ -636,29 +637,16 @@ const NON_RECURSIVE_GIT_CONFIG_ARGS = [
|
|
|
636
637
|
"-c",
|
|
637
638
|
"push.recurseSubmodules=false"
|
|
638
639
|
];
|
|
639
|
-
function gitExtraHeaderConfigKey(extraHeaderUrl) {
|
|
640
|
-
return `http.${normalizeBaseUrl(extraHeaderUrl)}/.extraHeader`;
|
|
641
|
-
}
|
|
642
|
-
function gitAuthArgs(auth) {
|
|
643
|
-
if (!auth) {
|
|
644
|
-
return [];
|
|
645
|
-
}
|
|
646
|
-
const scopedKey = gitExtraHeaderConfigKey(auth.extraHeaderUrl);
|
|
647
|
-
return ["-c", "http.extraHeader=", "-c", `${scopedKey}=`, "-c", `${scopedKey}=${auth.header}`];
|
|
648
|
-
}
|
|
649
640
|
const workerGitCommand = {
|
|
650
|
-
commandArgs: (args
|
|
641
|
+
commandArgs: (args) => ["git", ...NON_RECURSIVE_GIT_CONFIG_ARGS, ...args]
|
|
651
642
|
};
|
|
652
643
|
function runGit(args, options = {}) {
|
|
653
|
-
const command = workerGitCommand.commandArgs(args
|
|
644
|
+
const command = workerGitCommand.commandArgs(args);
|
|
654
645
|
const result = Bun.spawnSync(command, {
|
|
655
646
|
cwd: options.cwd,
|
|
656
647
|
stdout: "pipe",
|
|
657
648
|
stderr: "pipe",
|
|
658
|
-
env:
|
|
659
|
-
...process.env,
|
|
660
|
-
GIT_TERMINAL_PROMPT: "0"
|
|
661
|
-
}
|
|
649
|
+
env: workerGitProcessEnvironment()
|
|
662
650
|
});
|
|
663
651
|
if (result.exitCode !== 0) {
|
|
664
652
|
throw new Error(
|
|
@@ -791,28 +779,92 @@ function resolveWorkerFilePath(branchPath, inputPath, builtInPaths) {
|
|
|
791
779
|
function canonicalProjectRemoteUrl(baseUrl, projectId) {
|
|
792
780
|
return new URL(`/git/${projectId}.git`, baseUrl).toString();
|
|
793
781
|
}
|
|
794
|
-
function gitExtraHeaderUrlForRemote(remoteUrl) {
|
|
795
|
-
try {
|
|
796
|
-
return new URL(remoteUrl).origin;
|
|
797
|
-
} catch {
|
|
798
|
-
return remoteUrl;
|
|
799
|
-
}
|
|
800
|
-
}
|
|
801
|
-
function gitAuthForRemote(remoteUrl, authHeader) {
|
|
802
|
-
if (!authHeader) {
|
|
803
|
-
return void 0;
|
|
804
|
-
}
|
|
805
|
-
return {
|
|
806
|
-
extraHeaderUrl: gitExtraHeaderUrlForRemote(remoteUrl),
|
|
807
|
-
header: authHeader
|
|
808
|
-
};
|
|
809
|
-
}
|
|
810
782
|
function shellQuote(value) {
|
|
811
783
|
return `'${value.replace(/'/g, "'\\''")}'`;
|
|
812
784
|
}
|
|
813
|
-
function
|
|
785
|
+
function readOnlyCredentialStoreHelper(storePath) {
|
|
786
|
+
return `!f() { if [ "$1" = get ]; then git credential-store --file=${shellQuote(storePath)} get; else return 0; fi; }; f`;
|
|
787
|
+
}
|
|
788
|
+
function credentialStoreDirectory() {
|
|
789
|
+
return path.join(os.homedir(), ".r5d", "git-credentials");
|
|
790
|
+
}
|
|
791
|
+
function legacySharedCredentialStorePath() {
|
|
814
792
|
return path.join(os.homedir(), ".r5d", "github-credentials");
|
|
815
793
|
}
|
|
794
|
+
function credentialStorePathForProject(projectId) {
|
|
795
|
+
validateProjectId(projectId);
|
|
796
|
+
const digest = createHash("sha256").update(`project\0${projectId}`).digest("hex");
|
|
797
|
+
return path.join(credentialStoreDirectory(), `project-${digest}.store`);
|
|
798
|
+
}
|
|
799
|
+
function credentialStorePathForWorkspace(remoteUrl) {
|
|
800
|
+
const digest = createHash("sha256").update(`workspace\0${remoteUrl}`).digest("hex");
|
|
801
|
+
return path.join(credentialStoreDirectory(), `workspace-${digest}.store`);
|
|
802
|
+
}
|
|
803
|
+
function lstatIfExists(filePath) {
|
|
804
|
+
try {
|
|
805
|
+
return fs.lstatSync(filePath);
|
|
806
|
+
} catch (error) {
|
|
807
|
+
if (error.code === "ENOENT") return null;
|
|
808
|
+
throw error;
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
function ensurePrivateCredentialStoreDirectory(directoryPath) {
|
|
812
|
+
const stat = lstatIfExists(directoryPath);
|
|
813
|
+
if (stat) {
|
|
814
|
+
if (stat.isSymbolicLink() || !stat.isDirectory()) throw new Error("Git credential-store path must be a private directory");
|
|
815
|
+
} else {
|
|
816
|
+
fs.mkdirSync(directoryPath, { recursive: true, mode: 448 });
|
|
817
|
+
}
|
|
818
|
+
fs.chmodSync(directoryPath, 448);
|
|
819
|
+
}
|
|
820
|
+
function removeLegacySharedCredentialStore(storePath = legacySharedCredentialStorePath()) {
|
|
821
|
+
fs.rmSync(storePath, { force: true });
|
|
822
|
+
}
|
|
823
|
+
let credentialStoreDirectoryInitialized = false;
|
|
824
|
+
function resetCredentialStoreDirectory(directoryPath = credentialStoreDirectory()) {
|
|
825
|
+
const stat = lstatIfExists(directoryPath);
|
|
826
|
+
if (stat) {
|
|
827
|
+
if (stat.isSymbolicLink() || !stat.isDirectory()) fs.unlinkSync(directoryPath);
|
|
828
|
+
else fs.rmSync(directoryPath, { recursive: true, force: true });
|
|
829
|
+
}
|
|
830
|
+
ensurePrivateCredentialStoreDirectory(directoryPath);
|
|
831
|
+
}
|
|
832
|
+
function initializeCredentialStoreDirectoryOnce() {
|
|
833
|
+
if (credentialStoreDirectoryInitialized) return;
|
|
834
|
+
removeLegacySharedCredentialStore();
|
|
835
|
+
resetCredentialStoreDirectory();
|
|
836
|
+
credentialStoreDirectoryInitialized = true;
|
|
837
|
+
}
|
|
838
|
+
const APP_CREDENTIAL_STORE_FILE = /^(?:project|workspace)-[0-9a-f]{64}\.store$/;
|
|
839
|
+
function pruneCredentialStoreFiles(requiredStorePaths, directoryPath = credentialStoreDirectory()) {
|
|
840
|
+
ensurePrivateCredentialStoreDirectory(directoryPath);
|
|
841
|
+
const resolvedDirectory = path.resolve(directoryPath);
|
|
842
|
+
const requiredNames = /* @__PURE__ */ new Set();
|
|
843
|
+
for (const storePath of requiredStorePaths) {
|
|
844
|
+
const resolvedStorePath = path.resolve(storePath);
|
|
845
|
+
if (path.dirname(resolvedStorePath) !== resolvedDirectory || !APP_CREDENTIAL_STORE_FILE.test(path.basename(resolvedStorePath))) {
|
|
846
|
+
throw new Error("Invalid app-owned Git credential-store path");
|
|
847
|
+
}
|
|
848
|
+
requiredNames.add(path.basename(resolvedStorePath));
|
|
849
|
+
}
|
|
850
|
+
const removed = [];
|
|
851
|
+
for (const name of fs.readdirSync(directoryPath)) {
|
|
852
|
+
if (!APP_CREDENTIAL_STORE_FILE.test(name) || requiredNames.has(name)) continue;
|
|
853
|
+
const storePath = path.join(directoryPath, name);
|
|
854
|
+
const stat = fs.lstatSync(storePath);
|
|
855
|
+
if (!stat.isFile() && !stat.isSymbolicLink()) continue;
|
|
856
|
+
fs.unlinkSync(storePath);
|
|
857
|
+
removed.push(storePath);
|
|
858
|
+
}
|
|
859
|
+
return removed;
|
|
860
|
+
}
|
|
861
|
+
function removeProjectCredentialStore(projectId) {
|
|
862
|
+
const storePath = credentialStorePathForProject(projectId);
|
|
863
|
+
const stat = lstatIfExists(storePath);
|
|
864
|
+
if (!stat) return;
|
|
865
|
+
if (!stat.isFile() && !stat.isSymbolicLink()) throw new Error("Git credential store must be a regular file");
|
|
866
|
+
fs.unlinkSync(storePath);
|
|
867
|
+
}
|
|
816
868
|
function decodeGitAuthHeader(authHeader) {
|
|
817
869
|
const match = /^Authorization:\s*Basic\s+(.+)$/i.exec(authHeader.trim());
|
|
818
870
|
if (match) {
|
|
@@ -824,45 +876,84 @@ function decodeGitAuthHeader(authHeader) {
|
|
|
824
876
|
const bearer = /^Authorization:\s*Bearer\s+(.+)$/i.exec(authHeader.trim());
|
|
825
877
|
return bearer?.[1] ? { username: "r5d-worker", password: bearer[1] } : null;
|
|
826
878
|
}
|
|
827
|
-
function
|
|
879
|
+
function credentialUsernameForAuthHeader(authHeader) {
|
|
828
880
|
const credentials = decodeGitAuthHeader(authHeader);
|
|
829
|
-
if (!credentials)
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
881
|
+
if (!credentials) throw new Error("Unsupported Git authorization header");
|
|
882
|
+
if (/[\0\r\n]/.test(credentials.username)) throw new Error("Invalid Git credential username");
|
|
883
|
+
return credentials.username;
|
|
884
|
+
}
|
|
885
|
+
function replaceCredentialStoreFile(storePath, content) {
|
|
886
|
+
const temporaryPath = path.join(path.dirname(storePath), `.${path.basename(storePath)}.${process.pid}.${crypto.randomUUID()}.tmp`);
|
|
833
887
|
try {
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
}
|
|
838
|
-
|
|
839
|
-
return null;
|
|
888
|
+
fs.writeFileSync(temporaryPath, content, { flag: "wx", mode: 384 });
|
|
889
|
+
fs.chmodSync(temporaryPath, 384);
|
|
890
|
+
fs.renameSync(temporaryPath, storePath);
|
|
891
|
+
} finally {
|
|
892
|
+
fs.rmSync(temporaryPath, { force: true });
|
|
840
893
|
}
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
const
|
|
844
|
-
|
|
845
|
-
|
|
894
|
+
}
|
|
895
|
+
function writeCredentialStoreEntries(remotes, storePath) {
|
|
896
|
+
const authoritativeEntries = /* @__PURE__ */ new Map();
|
|
897
|
+
for (const input of remotes) {
|
|
898
|
+
let remote;
|
|
846
899
|
try {
|
|
847
|
-
|
|
848
|
-
return `${parsed.protocol}//${parsed.host}` !== hostKey;
|
|
900
|
+
remote = new URL(input.remoteUrl);
|
|
849
901
|
} catch {
|
|
850
|
-
|
|
902
|
+
if (input.authHeader != null) throw new Error("Invalid authenticated Git remote URL");
|
|
903
|
+
continue;
|
|
851
904
|
}
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
905
|
+
if ((remote.protocol === "https:" || remote.protocol === "http:") && (remote.username || remote.password)) {
|
|
906
|
+
throw new Error("Git credentials must not be embedded in a remote URL");
|
|
907
|
+
}
|
|
908
|
+
if (remote.protocol !== "https:" && remote.protocol !== "http:") {
|
|
909
|
+
if (input.authHeader != null) throw new Error("Unsupported authenticated Git remote protocol");
|
|
910
|
+
continue;
|
|
911
|
+
}
|
|
912
|
+
const hostKey = `${remote.protocol}//${remote.host}`;
|
|
913
|
+
let replacement = null;
|
|
914
|
+
if (input.authHeader != null) {
|
|
915
|
+
const credentials = decodeGitAuthHeader(input.authHeader);
|
|
916
|
+
if (!credentials) throw new Error("Unsupported Git authorization header");
|
|
917
|
+
const credentialUrl = new URL(hostKey);
|
|
918
|
+
credentialUrl.username = credentials.username;
|
|
919
|
+
credentialUrl.password = credentials.password;
|
|
920
|
+
replacement = credentialUrl.toString();
|
|
921
|
+
}
|
|
922
|
+
if (authoritativeEntries.has(hostKey) && authoritativeEntries.get(hostKey) !== replacement) {
|
|
923
|
+
throw new Error("Conflicting Git credentials for one transport host");
|
|
924
|
+
}
|
|
925
|
+
authoritativeEntries.set(hostKey, replacement);
|
|
926
|
+
}
|
|
927
|
+
const storeStat = lstatIfExists(storePath);
|
|
928
|
+
if (storeStat) {
|
|
929
|
+
if (storeStat.isSymbolicLink() || !storeStat.isFile()) throw new Error("Git credential store must be a regular file");
|
|
930
|
+
}
|
|
931
|
+
const replacements = [...authoritativeEntries.values()].filter((entry) => entry !== null);
|
|
932
|
+
if (replacements.length === 0) {
|
|
933
|
+
if (storeStat) fs.unlinkSync(storePath);
|
|
934
|
+
return null;
|
|
935
|
+
}
|
|
936
|
+
ensurePrivateCredentialStoreDirectory(path.dirname(storePath));
|
|
937
|
+
replaceCredentialStoreFile(storePath, `${replacements.join("\n")}
|
|
938
|
+
`);
|
|
939
|
+
return { hasCredentials: true, storePath };
|
|
940
|
+
}
|
|
941
|
+
function credentialHelperForRemotes(remotes, storePath) {
|
|
942
|
+
const credentialStore = writeCredentialStoreEntries(remotes, storePath);
|
|
943
|
+
return credentialStore?.hasCredentials ? readOnlyCredentialStoreHelper(credentialStore.storePath) : null;
|
|
944
|
+
}
|
|
945
|
+
const workerGitSecurityTestHarness = {
|
|
946
|
+
commandArgs: workerGitCommand.commandArgs,
|
|
947
|
+
credentialHelperForRemotes,
|
|
948
|
+
credentialStorePathForProject,
|
|
949
|
+
credentialStorePathForWorkspace,
|
|
950
|
+
legacySharedCredentialStorePath,
|
|
951
|
+
removeLegacySharedCredentialStore,
|
|
952
|
+
pruneCredentialStoreFiles,
|
|
953
|
+
readOnlyCredentialStoreHelper,
|
|
954
|
+
resetCredentialStoreDirectory,
|
|
955
|
+
replaceCredentialStoreFile
|
|
956
|
+
};
|
|
866
957
|
function dockerConfigPath() {
|
|
867
958
|
return path.join(os.homedir(), ".docker", "config.json");
|
|
868
959
|
}
|
|
@@ -2248,6 +2339,7 @@ async function startWorker(options) {
|
|
|
2248
2339
|
});
|
|
2249
2340
|
const config = readConfig(options.configPath);
|
|
2250
2341
|
const { baseUrl, token } = resolveCredentials(options, config);
|
|
2342
|
+
initializeCredentialStoreDirectoryOnce();
|
|
2251
2343
|
const label = requireValue(options.label, "Missing required --label <label>");
|
|
2252
2344
|
validateLabel(label);
|
|
2253
2345
|
const projectsRoot = path.resolve(options.projectRoot ?? defaultProjectsRoot());
|
|
@@ -2294,6 +2386,8 @@ async function startWorker(options) {
|
|
|
2294
2386
|
const lastObservedProjectHeads = /* @__PURE__ */ new Map();
|
|
2295
2387
|
const pendingMirrorDeletes = /* @__PURE__ */ new Map();
|
|
2296
2388
|
let workspaceRemoteUrl = null;
|
|
2389
|
+
let workspaceCredentialHelper = null;
|
|
2390
|
+
let workspaceCredentialUsername = null;
|
|
2297
2391
|
let workspaceGitIdentity = null;
|
|
2298
2392
|
let workspaceConfigured = false;
|
|
2299
2393
|
let activeWorkspaceIncidentId = null;
|
|
@@ -2308,6 +2402,7 @@ async function startWorker(options) {
|
|
|
2308
2402
|
let reloadAfterClose = false;
|
|
2309
2403
|
let shutdownAfterClose = false;
|
|
2310
2404
|
const bearerAuthHeader = `Authorization: Bearer ${token}`;
|
|
2405
|
+
const workerCredentialUsername = credentialUsernameForAuthHeader(bearerAuthHeader);
|
|
2311
2406
|
const projectBranchKey = (projectId, branchName) => `${projectId}\0${branchName}`;
|
|
2312
2407
|
const projectMountId = (projectId, branchName) => `project:${projectId}:${encodeURIComponent(branchName)}`;
|
|
2313
2408
|
const projectPlanMountId = (projectId, branchName) => `plan:${projectId}:${encodeURIComponent(branchName)}`;
|
|
@@ -2315,12 +2410,32 @@ async function startWorker(options) {
|
|
|
2315
2410
|
const workspacePlanRelativePath = (projectId, branchName) => path.posix.join("plans", projectId, encodeURIComponent(branchName));
|
|
2316
2411
|
const projectConnection = (project) => {
|
|
2317
2412
|
const mirrorUrl = canonicalProjectRemoteUrl(baseUrl, project.projectId);
|
|
2318
|
-
const mirrorAuth = gitAuthForRemote(mirrorUrl, bearerAuthHeader);
|
|
2319
2413
|
const originUrl = project.repoHttpUrl ?? mirrorUrl;
|
|
2320
2414
|
const originHeader = project.repoHttpUrl ? project.repoAuthHeader : bearerAuthHeader;
|
|
2321
|
-
const
|
|
2322
|
-
|
|
2323
|
-
|
|
2415
|
+
const credentialHelper = credentialHelperForRemotes(
|
|
2416
|
+
[
|
|
2417
|
+
{ remoteUrl: mirrorUrl, authHeader: bearerAuthHeader },
|
|
2418
|
+
{ remoteUrl: originUrl, authHeader: originHeader }
|
|
2419
|
+
],
|
|
2420
|
+
credentialStorePathForProject(project.projectId)
|
|
2421
|
+
);
|
|
2422
|
+
if (!credentialHelper) throw new Error("Project mirror credentials are unavailable");
|
|
2423
|
+
return {
|
|
2424
|
+
mirrorUrl,
|
|
2425
|
+
mirrorCredentialUsername: workerCredentialUsername,
|
|
2426
|
+
originUrl,
|
|
2427
|
+
originCredentialUsername: originHeader ? credentialUsernameForAuthHeader(originHeader) : null,
|
|
2428
|
+
credentialHelper
|
|
2429
|
+
};
|
|
2430
|
+
};
|
|
2431
|
+
const projectMirrorConnection = (projectId) => {
|
|
2432
|
+
const mirrorUrl = canonicalProjectRemoteUrl(baseUrl, projectId);
|
|
2433
|
+
const credentialHelper = credentialHelperForRemotes(
|
|
2434
|
+
[{ remoteUrl: mirrorUrl, authHeader: bearerAuthHeader }],
|
|
2435
|
+
credentialStorePathForProject(projectId)
|
|
2436
|
+
);
|
|
2437
|
+
if (!credentialHelper) throw new Error("Project mirror credentials are unavailable");
|
|
2438
|
+
return { mirrorUrl, credentialHelper, mirrorCredentialUsername: workerCredentialUsername };
|
|
2324
2439
|
};
|
|
2325
2440
|
const durableDeletionForBranch = (projectId, checkoutPathSegments, branchName) => projectWorkspaceState.tombstones.find(
|
|
2326
2441
|
(tombstone) => tombstone.projectId === projectId && tombstone.checkoutPathSegments[0] === checkoutPathSegments[0] && tombstone.checkoutPathSegments[1] === checkoutPathSegments[1] && (tombstone.kind === "project" || tombstone.branchName === branchName)
|
|
@@ -2344,7 +2459,8 @@ async function startWorker(options) {
|
|
|
2344
2459
|
branchName,
|
|
2345
2460
|
gitDirectory: configuredProjectBranchPath(projectsRoot, project, primaryBranchName),
|
|
2346
2461
|
mirrorUrl: connection.mirrorUrl,
|
|
2347
|
-
|
|
2462
|
+
credentialHelper: connection.credentialHelper,
|
|
2463
|
+
credentialUsername: connection.mirrorCredentialUsername,
|
|
2348
2464
|
sourcePath,
|
|
2349
2465
|
workspaceRelativePath: workspaceProjectRelativePath(project.projectId, branchName),
|
|
2350
2466
|
planSourcePath,
|
|
@@ -2355,7 +2471,7 @@ async function startWorker(options) {
|
|
|
2355
2471
|
};
|
|
2356
2472
|
const stageProjectDeletion = (project) => {
|
|
2357
2473
|
const projectRoot = configuredProjectRoot(projectsRoot, project);
|
|
2358
|
-
const connection =
|
|
2474
|
+
const connection = projectMirrorConnection(project.projectId);
|
|
2359
2475
|
const branches = project.branches.map(({ branchName }) => ({
|
|
2360
2476
|
branchName,
|
|
2361
2477
|
sourcePath: configuredProjectBranchPath(projectsRoot, project, branchName),
|
|
@@ -2376,7 +2492,8 @@ async function startWorker(options) {
|
|
|
2376
2492
|
// hidden ref only after that publication succeeds.
|
|
2377
2493
|
gitDirectory: workspaceShadowRoot,
|
|
2378
2494
|
mirrorUrl: connection.mirrorUrl,
|
|
2379
|
-
|
|
2495
|
+
credentialHelper: connection.credentialHelper,
|
|
2496
|
+
credentialUsername: connection.mirrorCredentialUsername,
|
|
2380
2497
|
sourcePath: branch.sourcePath,
|
|
2381
2498
|
workspaceRelativePath: workspaceProjectRelativePath(project.projectId, branch.branchName),
|
|
2382
2499
|
planSourcePath: branch.planSourcePath,
|
|
@@ -2434,14 +2551,17 @@ async function startWorker(options) {
|
|
|
2434
2551
|
for (const deletion of projectWorkspaceState.pendingMirrorRefDeletions) {
|
|
2435
2552
|
const key = projectBranchKey(deletion.projectId, deletion.branchName);
|
|
2436
2553
|
const planSourcePath = path.join(planRoot, deletion.projectId, ...deletion.branchName.split("/"));
|
|
2554
|
+
const liveProject = projectConfigById.get(deletion.projectId);
|
|
2555
|
+
const connection = liveProject ? projectConnection(liveProject) : projectMirrorConnection(deletion.projectId);
|
|
2437
2556
|
fs.rmSync(planSourcePath, { recursive: true, force: true });
|
|
2438
2557
|
pendingMirrorDeletes.set(key, {
|
|
2439
2558
|
id: projectMountId(deletion.projectId, deletion.branchName),
|
|
2440
2559
|
projectId: deletion.projectId,
|
|
2441
2560
|
branchName: deletion.branchName,
|
|
2442
2561
|
gitDirectory: workspaceShadowRoot,
|
|
2443
|
-
mirrorUrl:
|
|
2444
|
-
|
|
2562
|
+
mirrorUrl: connection.mirrorUrl,
|
|
2563
|
+
credentialHelper: connection.credentialHelper,
|
|
2564
|
+
credentialUsername: connection.mirrorCredentialUsername,
|
|
2445
2565
|
sourcePath: deletion.managedPath,
|
|
2446
2566
|
workspaceRelativePath: workspaceProjectRelativePath(deletion.projectId, deletion.branchName),
|
|
2447
2567
|
planSourcePath,
|
|
@@ -2589,9 +2709,9 @@ async function startWorker(options) {
|
|
|
2589
2709
|
primaryBranchName: primaryProjectBranch(project),
|
|
2590
2710
|
branches: project.branches,
|
|
2591
2711
|
originUrl: connection.originUrl,
|
|
2592
|
-
|
|
2712
|
+
originCredentialUsername: connection.originCredentialUsername,
|
|
2593
2713
|
mirrorUrl: connection.mirrorUrl,
|
|
2594
|
-
|
|
2714
|
+
mirrorCredentialUsername: connection.mirrorCredentialUsername,
|
|
2595
2715
|
credentialHelper: connection.credentialHelper,
|
|
2596
2716
|
gitIdentity: workspaceGitIdentity
|
|
2597
2717
|
});
|
|
@@ -2630,10 +2750,17 @@ async function startWorker(options) {
|
|
|
2630
2750
|
if (!readyProjectIds.has(project.projectId) || project.branches.length === 0) continue;
|
|
2631
2751
|
const connection = projectConnection(project);
|
|
2632
2752
|
const primaryPath = configuredProjectBranchPath(projectsRoot, project, primaryProjectBranch(project));
|
|
2633
|
-
if (!tryGit(
|
|
2634
|
-
|
|
2635
|
-
|
|
2636
|
-
|
|
2753
|
+
if (!tryGit(
|
|
2754
|
+
[
|
|
2755
|
+
...gitTransportSecurityArgs(connection.originUrl, connection.credentialHelper, connection.originCredentialUsername),
|
|
2756
|
+
"fetch",
|
|
2757
|
+
"--no-recurse-submodules",
|
|
2758
|
+
"--prune",
|
|
2759
|
+
"origin",
|
|
2760
|
+
"+refs/heads/*:refs/remotes/origin/*"
|
|
2761
|
+
],
|
|
2762
|
+
{ cwd: primaryPath }
|
|
2763
|
+
)) {
|
|
2637
2764
|
process.stderr.write(`[r5d-worker] could not refresh origin divergence for ${project.projectPath}
|
|
2638
2765
|
`);
|
|
2639
2766
|
}
|
|
@@ -2683,7 +2810,8 @@ async function startWorker(options) {
|
|
|
2683
2810
|
projectRoot: configuredProjectRoot(projectsRoot, project),
|
|
2684
2811
|
branchNames: project.branches.map(({ branchName }) => branchName),
|
|
2685
2812
|
mirrorUrl: connection.mirrorUrl,
|
|
2686
|
-
|
|
2813
|
+
credentialHelper: connection.credentialHelper,
|
|
2814
|
+
credentialUsername: connection.mirrorCredentialUsername,
|
|
2687
2815
|
onlyBranches: changed
|
|
2688
2816
|
});
|
|
2689
2817
|
for (const result of results) {
|
|
@@ -2710,7 +2838,8 @@ async function startWorker(options) {
|
|
|
2710
2838
|
gitDirectory: deletion.gitDirectory,
|
|
2711
2839
|
branchName: deletion.branchName,
|
|
2712
2840
|
mirrorUrl: deletion.mirrorUrl,
|
|
2713
|
-
|
|
2841
|
+
credentialHelper: deletion.credentialHelper,
|
|
2842
|
+
credentialUsername: deletion.credentialUsername
|
|
2714
2843
|
});
|
|
2715
2844
|
}
|
|
2716
2845
|
if (deletion.tombstoneId && !deletion.projectDeleted) {
|
|
@@ -2720,6 +2849,9 @@ async function startWorker(options) {
|
|
|
2720
2849
|
});
|
|
2721
2850
|
}
|
|
2722
2851
|
pendingMirrorDeletes.delete(key);
|
|
2852
|
+
if (deletion.projectDeleted && !projectConfigById.has(deletion.projectId) && ![...pendingMirrorDeletes.values()].some((pending) => pending.projectId === deletion.projectId)) {
|
|
2853
|
+
removeProjectCredentialStore(deletion.projectId);
|
|
2854
|
+
}
|
|
2723
2855
|
lastObservedProjectHeads.delete(key);
|
|
2724
2856
|
}
|
|
2725
2857
|
};
|
|
@@ -2733,7 +2865,8 @@ async function startWorker(options) {
|
|
|
2733
2865
|
primaryBranchName: primaryProjectBranch(project),
|
|
2734
2866
|
branchNames: project.branches.map(({ branchName }) => branchName),
|
|
2735
2867
|
mirrorUrl: connection.mirrorUrl,
|
|
2736
|
-
|
|
2868
|
+
credentialHelper: connection.credentialHelper,
|
|
2869
|
+
credentialUsername: connection.mirrorCredentialUsername
|
|
2737
2870
|
});
|
|
2738
2871
|
for (const state of refreshed) {
|
|
2739
2872
|
lastObservedProjectHeads.set(
|
|
@@ -2758,9 +2891,9 @@ async function startWorker(options) {
|
|
|
2758
2891
|
primaryBranchName: primaryProjectBranch(project),
|
|
2759
2892
|
branches: project.branches,
|
|
2760
2893
|
originUrl: connection.originUrl,
|
|
2761
|
-
|
|
2894
|
+
originCredentialUsername: connection.originCredentialUsername,
|
|
2762
2895
|
mirrorUrl: connection.mirrorUrl,
|
|
2763
|
-
|
|
2896
|
+
mirrorCredentialUsername: connection.mirrorCredentialUsername,
|
|
2764
2897
|
credentialHelper: connection.credentialHelper,
|
|
2765
2898
|
gitIdentity: workspaceGitIdentity
|
|
2766
2899
|
});
|
|
@@ -2807,16 +2940,16 @@ async function startWorker(options) {
|
|
|
2807
2940
|
error: error instanceof Error ? error.message : String(error)
|
|
2808
2941
|
});
|
|
2809
2942
|
const performWorkspaceSync = async (input) => {
|
|
2810
|
-
if (!workspaceRemoteUrl || !
|
|
2943
|
+
if (!workspaceRemoteUrl || !workspaceCredentialHelper || !workspaceCredentialUsername || !workspaceGitIdentity) {
|
|
2944
|
+
throw new Error("Worker workspace configuration has not been received");
|
|
2945
|
+
}
|
|
2811
2946
|
const mounts = buildWorkspaceMounts();
|
|
2812
|
-
const remoteAuth = gitAuthForRemote(workspaceRemoteUrl, bearerAuthHeader);
|
|
2813
|
-
const credentialHelper = credentialHelperForRemote(workspaceRemoteUrl, bearerAuthHeader);
|
|
2814
2947
|
if (input.resetToCanonical) {
|
|
2815
2948
|
const reset = resetWorkspaceGit({
|
|
2816
2949
|
workspacePath: workspaceShadowRoot,
|
|
2817
2950
|
remoteUrl: workspaceRemoteUrl,
|
|
2818
|
-
|
|
2819
|
-
|
|
2951
|
+
credentialHelper: workspaceCredentialHelper,
|
|
2952
|
+
credentialUsername: workspaceCredentialUsername,
|
|
2820
2953
|
gitIdentity: workspaceGitIdentity,
|
|
2821
2954
|
mounts
|
|
2822
2955
|
});
|
|
@@ -2845,8 +2978,8 @@ async function startWorker(options) {
|
|
|
2845
2978
|
workerLabel: label,
|
|
2846
2979
|
workspacePath: workspaceShadowRoot,
|
|
2847
2980
|
remoteUrl: workspaceRemoteUrl,
|
|
2848
|
-
|
|
2849
|
-
|
|
2981
|
+
credentialHelper: workspaceCredentialHelper,
|
|
2982
|
+
credentialUsername: workspaceCredentialUsername,
|
|
2850
2983
|
gitIdentity: workspaceGitIdentity,
|
|
2851
2984
|
mounts,
|
|
2852
2985
|
commitDetail: input.confirmationReason ?? input.trigger.detail,
|
|
@@ -2959,13 +3092,47 @@ async function startWorker(options) {
|
|
|
2959
3092
|
try {
|
|
2960
3093
|
return await workspaceSyncSingleFlight.runExclusive(async () => {
|
|
2961
3094
|
workspaceConfigured = false;
|
|
3095
|
+
projectWorkspaceState = projectWorkspaceStateStore.reconcile({
|
|
3096
|
+
desiredProjects: message.projects
|
|
3097
|
+
});
|
|
3098
|
+
const requiredCredentialStorePaths = /* @__PURE__ */ new Set();
|
|
3099
|
+
const workspaceStorePath = credentialStorePathForWorkspace(message.workspaceRemoteUrl);
|
|
3100
|
+
requiredCredentialStorePaths.add(workspaceStorePath);
|
|
3101
|
+
const nextWorkspaceCredentialHelper = credentialHelperForRemotes(
|
|
3102
|
+
[{ remoteUrl: message.workspaceRemoteUrl, authHeader: bearerAuthHeader }],
|
|
3103
|
+
workspaceStorePath
|
|
3104
|
+
);
|
|
3105
|
+
if (!nextWorkspaceCredentialHelper) throw new Error("Workspace credentials are unavailable");
|
|
3106
|
+
const desiredProjectConfigById = new Map(message.projects.map((project) => [project.projectId, project]));
|
|
3107
|
+
for (const project of message.projects) {
|
|
3108
|
+
requiredCredentialStorePaths.add(credentialStorePathForProject(project.projectId));
|
|
3109
|
+
projectConnection(project);
|
|
3110
|
+
}
|
|
3111
|
+
const pendingCredentialProjectIds = /* @__PURE__ */ new Set([
|
|
3112
|
+
...[...pendingMirrorDeletes.values()].map(({ projectId }) => projectId),
|
|
3113
|
+
...projectWorkspaceState.pendingTreeDeletions.map(({ projectId }) => projectId),
|
|
3114
|
+
...projectWorkspaceState.pendingMirrorRefDeletions.map(({ projectId }) => projectId)
|
|
3115
|
+
]);
|
|
3116
|
+
for (const projectId of pendingCredentialProjectIds) {
|
|
3117
|
+
const desiredProject = desiredProjectConfigById.get(projectId);
|
|
3118
|
+
if (desiredProject) projectConnection(desiredProject);
|
|
3119
|
+
else projectMirrorConnection(projectId);
|
|
3120
|
+
requiredCredentialStorePaths.add(credentialStorePathForProject(projectId));
|
|
3121
|
+
}
|
|
3122
|
+
for (const deletion of pendingMirrorDeletes.values()) {
|
|
3123
|
+
const desiredProject = desiredProjectConfigById.get(deletion.projectId);
|
|
3124
|
+
const connection = desiredProject ? projectConnection(desiredProject) : projectMirrorConnection(deletion.projectId);
|
|
3125
|
+
deletion.mirrorUrl = connection.mirrorUrl;
|
|
3126
|
+
deletion.credentialHelper = connection.credentialHelper;
|
|
3127
|
+
deletion.credentialUsername = connection.mirrorCredentialUsername;
|
|
3128
|
+
}
|
|
3129
|
+
pruneCredentialStoreFiles(requiredCredentialStorePaths);
|
|
2962
3130
|
configureGitHubAuth(message.githubCredential);
|
|
2963
3131
|
visibleGitIdentity = message.gitIdentity;
|
|
2964
3132
|
workspaceGitIdentity = message.gitIdentity;
|
|
2965
3133
|
workspaceRemoteUrl = message.workspaceRemoteUrl;
|
|
2966
|
-
|
|
2967
|
-
|
|
2968
|
-
});
|
|
3134
|
+
workspaceCredentialHelper = nextWorkspaceCredentialHelper;
|
|
3135
|
+
workspaceCredentialUsername = workerCredentialUsername;
|
|
2969
3136
|
pruneAuthoritativelyDesiredBranchDeletions(pendingMirrorDeletes, message.projects);
|
|
2970
3137
|
const effectiveProjects = effectiveWorkerProjects(message.projects).map((project) => ({
|
|
2971
3138
|
...project,
|
|
@@ -2996,13 +3163,11 @@ async function startWorker(options) {
|
|
|
2996
3163
|
for (const projectId of [...readyProjectIds]) {
|
|
2997
3164
|
if (!projectConfigById.has(projectId)) readyProjectIds.delete(projectId);
|
|
2998
3165
|
}
|
|
2999
|
-
const remoteAuth = gitAuthForRemote(message.workspaceRemoteUrl, bearerAuthHeader);
|
|
3000
|
-
const credentialHelper = credentialHelperForRemote(message.workspaceRemoteUrl, bearerAuthHeader);
|
|
3001
3166
|
ensureWorkspaceGitClone({
|
|
3002
3167
|
workspacePath: workspaceShadowRoot,
|
|
3003
3168
|
remoteUrl: message.workspaceRemoteUrl,
|
|
3004
|
-
|
|
3005
|
-
|
|
3169
|
+
credentialHelper: nextWorkspaceCredentialHelper,
|
|
3170
|
+
credentialUsername: workerCredentialUsername,
|
|
3006
3171
|
gitIdentity: message.gitIdentity
|
|
3007
3172
|
});
|
|
3008
3173
|
pruneStaleOuterWorkspaceEntries(projectWorkspaceStateStore.discoverStaleOuterEntries(workspaceShadowRoot));
|
|
@@ -3801,5 +3966,6 @@ export {
|
|
|
3801
3966
|
resolveWorkerFilePath,
|
|
3802
3967
|
resolveWorkerSessionTarget,
|
|
3803
3968
|
syncSessionArtifacts,
|
|
3969
|
+
workerGitSecurityTestHarness,
|
|
3804
3970
|
writeWorkerTextFile
|
|
3805
3971
|
};
|
package/dist/mjs/package.json
CHANGED