feat: Enable Git operations within a specified deployment directory by adding cwd options and configuring Git to trust the deploy directory.

This commit is contained in:
syntaxbullet
2026-01-30 14:56:29 +01:00
parent 9ff679ee5c
commit 35ecea16f7
2 changed files with 26 additions and 13 deletions

View File

@@ -19,10 +19,11 @@ const BUILD_TIMEOUT_MS = 180_000; // 3 minutes for web build
*/
async function execWithTimeout(
cmd: string,
timeoutMs: number = DEFAULT_TIMEOUT_MS
timeoutMs: number = DEFAULT_TIMEOUT_MS,
options: { cwd?: string } = {}
): Promise<{ stdout: string; stderr: string }> {
return new Promise((resolve, reject) => {
const process = exec(cmd, (error: ExecException | null, stdout: string, stderr: string) => {
const process = exec(cmd, { cwd: options.cwd }, (error: ExecException | null, stdout: string, stderr: string) => {
if (error) {
reject(error);
} else {
@@ -51,22 +52,24 @@ export class UpdateService {
* Optimized: Parallel git commands and combined requirements check
*/
static async checkForUpdates(): Promise<UpdateInfo & { requirements: UpdateCheckResult }> {
const cwd = this.getDeployDir();
// Get branch first (needed for subsequent commands)
const { stdout: branchName } = await execWithTimeout("git rev-parse --abbrev-ref HEAD");
const { stdout: branchName } = await execWithTimeout("git rev-parse --abbrev-ref HEAD", DEFAULT_TIMEOUT_MS, { cwd });
const branch = branchName.trim();
// Parallel execution: get current commit while fetching
const [currentResult] = await Promise.all([
execWithTimeout("git rev-parse --short HEAD"),
execWithTimeout(`git fetch origin ${branch} --prune`) // Only fetch current branch
execWithTimeout("git rev-parse --short HEAD", DEFAULT_TIMEOUT_MS, { cwd }),
execWithTimeout(`git fetch origin ${branch} --prune`, DEFAULT_TIMEOUT_MS, { cwd }) // Only fetch current branch
]);
const currentCommit = currentResult.stdout.trim();
// After fetch completes, get remote info in parallel
const [latestResult, logResult, diffResult] = await Promise.all([
execWithTimeout(`git rev-parse --short origin/${branch}`),
execWithTimeout(`git log HEAD..origin/${branch} --format="%h|%s|%an" --no-merges`),
execWithTimeout(`git diff HEAD..origin/${branch} --name-only`)
execWithTimeout(`git rev-parse --short origin/${branch}`, DEFAULT_TIMEOUT_MS, { cwd }),
execWithTimeout(`git log HEAD..origin/${branch} --format="%h|%s|%an" --no-merges`, DEFAULT_TIMEOUT_MS, { cwd }),
execWithTimeout(`git diff HEAD..origin/${branch} --name-only`, DEFAULT_TIMEOUT_MS, { cwd })
]);
const latestCommit = latestResult.stdout.trim();
@@ -135,8 +138,9 @@ export class UpdateService {
* Kept for backwards compatibility
*/
static async checkUpdateRequirements(branch: string): Promise<UpdateCheckResult> {
const cwd = this.getDeployDir();
try {
const { stdout } = await execWithTimeout(`git diff HEAD..origin/${branch} --name-only`);
const { stdout } = await execWithTimeout(`git diff HEAD..origin/${branch} --name-only`, DEFAULT_TIMEOUT_MS, { cwd });
const changedFiles = stdout.trim().split("\n").filter(f => f.length > 0);
return this.analyzeChangedFiles(changedFiles);
} catch (e) {
@@ -179,7 +183,8 @@ export class UpdateService {
* Save the current commit for potential rollback
*/
static async saveRollbackPoint(): Promise<string> {
const { stdout } = await execWithTimeout("git rev-parse HEAD");
const cwd = this.getDeployDir();
const { stdout } = await execWithTimeout("git rev-parse HEAD", DEFAULT_TIMEOUT_MS, { cwd });
const commit = stdout.trim();
await writeFile(this.ROLLBACK_FILE, commit);
this.rollbackPointExists = true; // Cache the state
@@ -190,9 +195,10 @@ export class UpdateService {
* Rollback to the previous commit
*/
static async rollback(): Promise<{ success: boolean; message: string }> {
const cwd = this.getDeployDir();
try {
const rollbackCommit = await readFile(this.ROLLBACK_FILE, "utf-8");
await execWithTimeout(`git reset --hard ${rollbackCommit.trim()}`);
await execWithTimeout(`git reset --hard ${rollbackCommit.trim()}`, DEFAULT_TIMEOUT_MS, { cwd });
await unlink(this.ROLLBACK_FILE);
this.rollbackPointExists = false;
return { success: true, message: `Rolled back to ${rollbackCommit.trim().substring(0, 7)}` };
@@ -226,7 +232,8 @@ export class UpdateService {
* Perform the git update
*/
static async performUpdate(branch: string): Promise<void> {
await execWithTimeout(`git reset --hard origin/${branch}`);
const cwd = this.getDeployDir();
await execWithTimeout(`git reset --hard origin/${branch}`, DEFAULT_TIMEOUT_MS, { cwd });
}
/**
@@ -236,6 +243,11 @@ export class UpdateService {
static async installDependencies(options: { root: boolean; web: boolean }): Promise<string> {
const tasks: Promise<{ label: string; output: string }>[] = [];
// Install dependencies in the App directory (not deploy dir) because we are updating the RUNNING app
// NOTE: If we hot-reload, we want to install in the current directory.
// If we restart, dependencies should be in the image.
// Assuming this method is used for hot-patching/minor updates without container rebuild.
if (options.root) {
tasks.push(
execWithTimeout("bun install", INSTALL_TIMEOUT_MS)