7
0

NodeJs's Git Auto Update Daemon

2026-08-26

分享一个NodeJs的基于git的自动升级守护进程

import { fork, exec, ExecException, ChildProcess } from "child_process";
import path from "path";
import fs from "fs";
import getLogger from "./utils/logger";

class AppDeamon {
  private readonly logger = getLogger("AppDeamon");
  private appPath: string;
  private appEntry: string;
  private shell: string;
  private appProcess: ChildProcess | null = null;
  private isDeploying = false;
  private checkInterval = 60 * 1000;

  private currentBranch = "";

  constructor() {
    this.logger.info("🛡️ 守护进程 v1.0.0");

    this.shell = this.detectShell(); // 自动检测
    this.appPath = path.join(process.cwd(), "dist");
    this.appEntry = path.join(this.appPath, "app.js");

    this.logger.info(`📂 应用路径: ${this.appPath}`);
    this.logger.info(`📂 应用入口: ${this.appEntry}`);
    this.logger.info(`💻 平台: ${process.platform}`);
    this.logger.info(`💻 Shell: ${this.shell}`);
    this.logger.info(`⏱️ 检查间隔: ${this.checkInterval / 1000}秒`);
  }

  private detectShell(): string {
    const platform = process.platform;

    if (platform === "win32") {
      // Windows: 检查是否在Git Bash或WSL环境中
      if (process.env.SHELL?.includes("bash") || process.env.WSL_DISTRO_NAME) {
        this.logger.debug("检测到 WSL 环境, Shell: 使用 WSL 的 bash");
        return "/bin/bash";
      }
      this.logger.debug("检测到 Windows 环境, Shell: 使用 Cmd");
      // 默认使用cmd
      return "cmd.exe";
    } else if (platform === "darwin") {
      // macOS
      this.logger.debug("检测到 macOS 环境, Shell: 使用 zsh");
      return "/bin/zsh";
    } else {
      this.logger.debug("检测到 linux 环境, Shell: 使用 bash");
      // Linux
      return "/bin/bash";
    }
  }

  private async executeCommand(
    command: string,
    options?: {
      cwd?: string;
      timeout?: number;
    },
  ): Promise<{ stdout: string; stderr: string }> {
    return new Promise((resolve, reject) => {
      let finalCommand = command;
      if (this.shell === "cmd.exe") {
        finalCommand = this.convertToWindowsCommand(command);
      }

      const execOptions = {
        cwd: options?.cwd || process.cwd(),
        shell: this.shell,
        encoding: "utf-8" as BufferEncoding,
        timeout: options?.timeout || 30000,
        maxBuffer: 1024 * 1024 * 10,
        // Windows
        ...(this.shell === "cmd.exe" && {
          env: {
            ...process.env,
            PATH: process.env.PATH || "",
          },
        }),
      };

      exec(finalCommand, execOptions, (error: ExecException | null, stdout: string, stderr: string) => {
        if (error) {
          reject(new Error(`命令执行失败: ${command}\n` + `Shell: ${this.shell}\n` + `错误: ${error.message}\n` + `stderr: ${stderr}`));
        } else {
          resolve({
            stdout: stdout.trim(),
            stderr: stderr.trim(),
          });
        }
      });
    });
  }

  private async getCurrentBranch(): Promise<string> {
    try {
      const result = await this.executeCommand("git branch --show-current");
      if (result.stdout) {
        return result.stdout;
      }

      const result2 = await this.executeCommand("git rev-parse --abbrev-ref HEAD");
      return result2.stdout;
    } catch (error) {
      this.logger.error("获取分支失败:", error);
      throw error;
    }
  }

  private async hasUpdate(branch: string): Promise<boolean> {
    try {
      // 先fetch
      await this.executeCommand(`git fetch origin ${branch}`);

      // 获取本地和远程hash
      const localResult = await this.executeCommand("git rev-parse HEAD");
      const remoteResult = await this.executeCommand(`git rev-parse origin/${branch}`);

      return localResult.stdout !== remoteResult.stdout;
    } catch (error) {
      this.logger.error("检查更新失败:", error);
      return false;
    }
  }

  private convertToWindowsCommand(command: string): string {
    // Git命令在cmd下需要特殊处理
    if (command.startsWith("git ")) {
      // 确保git在PATH中
      return command;
    }

    // 处理&&和||这类shell操作符
    if (command.includes("&&") || command.includes("||")) {
      // cmd使用 & 而不是 &&
      return command.replace(/&&/g, "&");
    }

    // 处理引号问题
    if (command.includes("'")) {
      return command.replace(/'/g, '"');
    }

    return command;
  }

  async init() {
    this.logger.info("守护进程启动");

    if (!process.env.NODE_ENV) {
      this.logger.warn("未检测到当前环境类型, 已采用默认值: 生产环境");
      process.env.NODE_ENV = "production";
    }

    this.logger.info("当前环境: " + process.env.NODE_ENV);

    // fork(this.appPath, [], {
    //   execArgv: ["--enable-source-maps"],
    // });

    this.logger.info("正在安装 Pnpm⏳");
    try {
      await this.executeCommand("npm install -g pnpm --ignore-scripts");
    } catch (e) {
      this.logger.error("安装 Pnpm 失败: ", e);
      process.exit(0);
    }

    this.logger.info("正在检查 Pnpm⏳");
    try {
      const pnpmVersion = (await this.executeCommand("pnpm -v")).stdout;

      if (!pnpmVersion) throw new Error("版本无输出");

      this.logger.info("Pnpm 版本: " + pnpmVersion);
    } catch (e) {
      this.logger.error("检查 Pnpm 时出错: ", e);
      process.exit(0);
    }

    this.logger.info("正在安装依赖⏳");
    try {
      const result = (await this.executeCommand("pnpm install")).stdout;

      if (!result.includes("Done")) throw new Error(result);
    } catch (e) {
      this.logger.error("Pnpm 安装依赖时出错: ", e);
      process.exit(0);
    }

    this.logger.info("正在检查 git⏳");
    try {
      const gitVersion = (await this.executeCommand("git -v")).stdout;

      if (!gitVersion) throw new Error("版本无输出");

      this.logger.info("git 版本: " + gitVersion);

      const branch = await this.getCurrentBranch();

      if (!branch) throw new Error("获取当前分支失败");

      this.currentBranch = branch;

      this.logger.info("当前分支: " + branch);
    } catch (e) {
      this.logger.error("检查 git 时出错: ", e);
      process.exit(0);
    }

    this.logger.info("正在构建当前版本⏳");
    try {
      const result = (await this.executeCommand("pnpm build")).stdout;

      if (!result.includes("Build success")) throw new Error(result);
    } catch (e) {
      this.logger.error("构建失败: ", e);
      process.exit(0);
    }

    this.logger.info("准备App⏳");
    try {
      if (!fs.existsSync(this.appEntry)) throw new Error("入口文件不存在");
    } catch (e) {
      this.logger.error("准备App时出错: ", e);
      process.exit(0);
    }

    this.checkAndDeploy().catch(this.logger.error);

    // 定时轮询
    setInterval(() => {
      this.checkAndDeploy().catch(this.logger.error);
    }, this.checkInterval);
  }

  private async performDeploy(branch: string): Promise<void> {
    this.logger.info(`🔄 开始部署分支 ${branch}...`);
    this.logger.info(`🖥️ 系统: ${process.platform}, Shell: ${this.shell}`);

    try {
      // 1. 停止应用
      this.logger.info("🛑 停止应用...");
      this.appProcess?.kill();

      // 2. 拉取代码
      this.logger.info("📥 拉取最新代码...");
      await this.executeCommand(`git reset --hard origin/${branch}`);

      // 3. 安装依赖
      this.logger.info("📦 安装依赖...");
      await this.executeCommand("pnpm install");

      // 4. 构建(检查平台)
      this.logger.info("🏗️ 构建项目...");
      try {
        await this.executeCommand("pnpm build");
      } catch (error) {
        this.logger.info("⚠️ 无build脚本或构建失败,跳过");
      }

      // 5. 启动应用
      this.logger.info("🚀 启动应用...");
      this.start();

      this.logger.info("✅ 部署完成!");
    } catch (error) {
      this.logger.error("❌ 部署失败:", error);
    }
  }

  async checkAndDeploy(): Promise<void> {
    if (this.isDeploying) {
      this.logger.info("⏳ 部署进行中,跳过本次检查");
      return;
    }

    this.isDeploying = true;

    try {
      const branch = await this.getCurrentBranch();
      if (branch !== this.currentBranch) {
        this.logger.warn("检测到分支切换: " + `${this.currentBranch} -> ${branch}`);
        this.currentBranch = branch;
      }

      if (await this.hasUpdate(branch)) {
        await this.performDeploy(branch);
        // 部署
      } else {
        this.logger.info("✅ 代码已是最新");
      }
    } catch (error) {
      this.logger.error("❌ 检查失败:", error);
    } finally {
      this.isDeploying = false;
    }
  }

  async start() {
    this.logger.info("启动App⏳");
    this.appProcess = fork(this.appEntry, [], {
      cwd: this.appPath,
      execArgv: ["--enable-source-maps"],
    });

    this.appProcess.on("exit", () => {
      this.logger.warn("进程退出");
    });
  }
}

使用

const daemon = new AppDeamon();

(async () => {
  await daemon.init();
  await daemon.start();
})();

代办

  • 闲置时进行

NodeJs's Git Auto Update Daemon
/archives/nodejs-s-git-auto-update-daemon
Author
Administrator
Published at
2026-08-26
License
CC BY-NC-SA 4.0

Comments