---
date: 2026-05-19
status: 工程化保险已加（systemd Environment=GITEA_REPO 写死）
tags: [ops, systemd, daemon, gitea, detect_repo, deployment]
related-errs: []
related-learnings: [2026-05-19-cron-cancelled-when-runner-saturated.md]
---

# ERR-20260519-001 — Daemon 部署到 /opt/ 后 detect_repo() 失败（host 不是 git repo）

## 现象

auto-merge-daemon 第一次 systemd timer 触发，service 立刻退出 status=1/FAILURE。`journalctl` 末尾：

```
File "/opt/auto-merge-daemon/_gitea_api.py", line 85, in detect_repo
    url = subprocess.check_output(
subprocess.CalledProcessError: Command '['git', 'config', '--get', 'remote.origin.url']' returned non-zero exit status 1.
```

倒推到 main 第 1 行业务代码 `detect_repo()`——还没调任何 Gitea API 就崩了。

## 直接原因

`scripts/ops/_gitea_api.py` 的 `detect_repo()` 用 `git config --get remote.origin.url` 解析 owner/repo。daemon 部署后：

- 文件在 `/opt/auto-merge-daemon/`（不是 git checkout，是 scp 来的孤立文件）
- systemd unit `WorkingDirectory=/opt/auto-merge-daemon`
- 当前目录不在任何 git tree 内 → `git config --get` exit 1 → check_output 抛 CalledProcessError

## 元根因

把"在 git checkout 里跑"假设带到了"host 标准 ops daemon"场景。`detect_repo()` 当初为 CI workflow 设计——workflow 步骤先 `actions/checkout` 把代码克隆到 workdir，git 命令自然有结果。daemon 部署直接 scp 单文件到 `/opt/`，**这个隐式假设破了没人发现**。

`_gitea_api.detect_repo()` 本身有 fallback：

```python
def detect_repo() -> str:
    env = os.environ.get("GITEA_REPO")
    if env:
        return env
    # git config --get remote.origin.url ...
```

`auto-merge-develop.py` 调用方也写了对：

```python
repo = os.environ.get("GITEA_REPO") or detect_repo()
```

**但 systemd unit 没传这个 env**，所以 `os.environ.get("GITEA_REPO")` 返回 None，掉进 git 那条路径。

## 解法

`scripts/ops/auto-merge-daemon/auto-merge-daemon.service` 加：

```ini
# Host /opt/auto-merge-daemon 不是 git repo，detect_repo() 会失败——必须显式给 GITEA_REPO
Environment=GITEA_REPO=FFAIWorkspace/workspace
```

redeploy 后两台 host 手工触发 `systemctl start auto-merge-daemon.service` 验证：

```
[info] repo=FFAIWorkspace/workspace required_checks=[6 checks]
[info] 发现 2 个 open PR → develop
  · #440 [chentao.jia] ... — wait: required check 未通过
  · #434 [lijian.dai] ... — wait: required check 未通过
[done] merged=0 updated=0
```

干净跑完。

## 监控点 / 早发现

- daemon 第一次 tick 必须 manual `systemctl start <service>` 验证 ——不要光看 timer 设上了就走
- 看到 `subprocess.CalledProcessError: Command '['git', ...]'` 在 ops daemon 的 journal 里 = host 缺 git env，加 `Environment=GITEA_REPO=...` 解
- 部署脚本应在 deploy 完最后跑一次 `systemctl start service` smoke test

## 工程化保险

**Option A**（已采用）：systemd unit 显式 `Environment=GITEA_REPO=FFAIWorkspace/workspace`——repo 是固定值，写死最直接。

**Option B**（可选）：让 `_gitea_api.detect_repo()` 在 git 命令失败时给更好的报错提示，比如 "GITEA_REPO env not set AND not in git repo; one of them must be provided"。提升下次类似坑的诊断速度。

**Option C**（推广）：所有 ops daemon 都应该在 `.service` 里至少把 "git 上下文 env" 显式列出来（`GITEA_REPO`，未来可能还有 `GITEA_HOST` / `GITEA_DEFAULT_BRANCH`）。本次只补了 auto-merge-daemon；其他 daemon（如果之后有）抄这条铁律。

## 复现

```bash
# 部署一台没 git checkout 的 host（VM、容器、任何 standalone 环境）
scp scripts/ops/auto-merge-develop.py scripts/ops/_gitea_api.py host:/opt/foo/
ssh host "cd /opt/foo && GITEA_API_TOKEN=<...> python3 auto-merge-develop.py"
# → subprocess.CalledProcessError on git config
ssh host "cd /opt/foo && GITEA_API_TOKEN=<...> GITEA_REPO=FFAIWorkspace/workspace python3 auto-merge-develop.py"
# → 正常
```

## 相关

- 本次发现的 PR：本 PR
- 前置 PR：#431（dedicated-runner-2 上线）—— 同一组 host 部署链路
- 前置 learning：[2026-05-19-cron-cancelled-when-runner-saturated.md](../2026-05-19-cron-cancelled-when-runner-saturated.md) —— 触发 daemon 化的根因
- `_gitea_api.detect_repo()` 源码：`scripts/ops/_gitea_api.py:74`
