feat(clone): background clone jobs that survive a reload and a server restart
Both clone POSTs take background: true: every check answers synchronously,
then 202 {job}. git runs in a detached runner (scripts/clone-runner.js, its
own process group, stderr to a file — no SIGPIPE when the server restarts);
the server tails it for progress and runs the after-step (workspace / extra
folder) once when it exits. Jobs persist in <data dir>/runtime/clone-jobs.json
(0600, atomic, throttled progress writes); on start a live runner is adopted
(counted against the clone limit), an ended one finished, a queued one
requeued (interrupted if it carried a typed login), the rest interrupted.
GET /clone/jobs, DELETE /clone/jobs/:id, POST /clone/jobs/:id/cancel (queued:
dequeued; running: SIGTERM to the runner's group -> cancelled, no after-step)
and /retry (new job from the stored request, never a credential). The
held-open path is unchanged; worktree.gitCommand builds the runner's argv
and git-identity.cloneFailure the shared error classification.
This commit is contained in:
+42
-27
@@ -40,34 +40,12 @@ function lowPriority() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Promisified git wrapper. On failure, throws an Error with err.git = { args, code, stderr }.
|
||||
* Treats zero exit code as success even if stderr has hints.
|
||||
*
|
||||
* CRITICAL: Scrubs git hook environment variables (GIT_DIR, GIT_INDEX_FILE, etc.)
|
||||
* that leak from parent processes. Without this, git operations on a worktree (where
|
||||
* .git is a file, not a directory) fail with ".git/index: index file open failed:
|
||||
* Not a directory" when run from within a git hook or from a shell that inherited
|
||||
* these variables. This module's whole job is to run git safely against repos other
|
||||
* than the one enclosing the current working directory.
|
||||
* The command a git call runs — `{cmd, argv, env, gitCmd}` — exactly as
|
||||
* `git()` builds it (scrubbed env, a member's `sudo -n -u ah-<name> git`,
|
||||
* nice/ionice for `background`), without running it. A background clone's
|
||||
* detached runner (`scripts/clone-runner.js`) executes this very command.
|
||||
*/
|
||||
async function git(
|
||||
cwd,
|
||||
args,
|
||||
{
|
||||
timeout = 0,
|
||||
maxBuffer = 8 * 1024 * 1024,
|
||||
literal = false,
|
||||
env: extraEnv = {},
|
||||
// "buffer" for a binary read (an image at a revision).
|
||||
encoding = "utf8",
|
||||
// stdin (a patch for `apply -`): never a file the dashboard writes.
|
||||
input = null,
|
||||
// true: run under nice/ionice (a clone — see lowPriority above).
|
||||
background = false,
|
||||
// Called with each stderr chunk as it arrives (a clone's --progress).
|
||||
onStderr = null,
|
||||
} = {}
|
||||
) {
|
||||
function gitCommand(cwd, args, { literal = false, env: extraEnv = {}, background = false } = {}) {
|
||||
// Build a clean environment: copy process.env but scrub git hook variables
|
||||
// that could point to the outer repo's git directory or index.
|
||||
const env = { ...process.env };
|
||||
@@ -118,6 +96,41 @@ async function git(
|
||||
argv = [...rest, cmd, ...argv];
|
||||
cmd = first;
|
||||
}
|
||||
return { cmd, argv, env, gitCmd };
|
||||
}
|
||||
|
||||
/**
|
||||
* Promisified git wrapper. On failure, throws an Error with err.git = { args, code, stderr }.
|
||||
* Treats zero exit code as success even if stderr has hints.
|
||||
*
|
||||
* CRITICAL: Scrubs git hook environment variables (GIT_DIR, GIT_INDEX_FILE, etc.)
|
||||
* that leak from parent processes. Without this, git operations on a worktree (where
|
||||
* .git is a file, not a directory) fail with ".git/index: index file open failed:
|
||||
* Not a directory" when run from within a git hook or from a shell that inherited
|
||||
* these variables. This module's whole job is to run git safely against repos other
|
||||
* than the one enclosing the current working directory.
|
||||
*/
|
||||
async function git(
|
||||
cwd,
|
||||
args,
|
||||
{
|
||||
timeout = 0,
|
||||
maxBuffer = 8 * 1024 * 1024,
|
||||
literal = false,
|
||||
env: extraEnv = {},
|
||||
// "buffer" for a binary read (an image at a revision).
|
||||
encoding = "utf8",
|
||||
// stdin (a patch for `apply -`): never a file the dashboard writes.
|
||||
input = null,
|
||||
// true: run under nice/ionice (a clone — see lowPriority above).
|
||||
background = false,
|
||||
// Called with each stderr chunk as it arrives (a clone's --progress).
|
||||
onStderr = null,
|
||||
// An AbortSignal: aborting SIGTERMs the child (a cancelled clone).
|
||||
signal = null,
|
||||
} = {}
|
||||
) {
|
||||
const { cmd, argv, env, gitCmd } = gitCommand(cwd, args, { literal, env: extraEnv, background });
|
||||
|
||||
try {
|
||||
const run =
|
||||
@@ -142,6 +155,7 @@ async function git(
|
||||
// open forever; GIT_TERMINAL_PROMPT=0 only covers the prompting case.
|
||||
timeout,
|
||||
encoding,
|
||||
...(signal ? { signal } : {}),
|
||||
});
|
||||
// promisify(execFile) hands back its child; the stdin runner has none.
|
||||
if (onStderr && pending.child?.stderr) {
|
||||
@@ -1006,6 +1020,7 @@ async function hasNoRemotes(dir) {
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
gitCommand,
|
||||
LANES_ROOT,
|
||||
git,
|
||||
isGitRepo,
|
||||
|
||||
Reference in New Issue
Block a user