@tmpxinchen/tmp1 1.0.56 → 1.0.58
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.
|
@@ -0,0 +1,1088 @@
|
|
|
1
|
+
# issue-bot 对接公司 GitLab 8.16 改造说明
|
|
2
|
+
|
|
3
|
+
## 0. 目标与边界
|
|
4
|
+
|
|
5
|
+
目标是把当前本地 `ai-coding-pilot` 改成:
|
|
6
|
+
|
|
7
|
+
```text
|
|
8
|
+
本地 GitLab:issue / 队列 / AI 工作区 / 本地门禁
|
|
9
|
+
公司 GitLab:真实代码 / AI 分支 / 正式 MR
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
并使用一个单向只读镜像,让本地 GitLab 也能看到公司代码和分支。
|
|
13
|
+
|
|
14
|
+
```text
|
|
15
|
+
公司 GitLab(代码主库)
|
|
16
|
+
↓ 只读镜像
|
|
17
|
+
本地 GitLab(issue + 公司代码镜像)
|
|
18
|
+
↑
|
|
19
|
+
│ 本地 issue 触发
|
|
20
|
+
issue-bot
|
|
21
|
+
│ clone / push / API v3
|
|
22
|
+
↓
|
|
23
|
+
公司 GitLab(正式 MR)
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
本次涉及:
|
|
27
|
+
|
|
28
|
+
```text
|
|
29
|
+
automation/issue_bot.py
|
|
30
|
+
automation/company-projects.example.json
|
|
31
|
+
scripts/mirror-company.sh
|
|
32
|
+
docker-compose.yml
|
|
33
|
+
.env
|
|
34
|
+
.env.example
|
|
35
|
+
scripts/gitlab-init.sh
|
|
36
|
+
scripts/selfcheck.sh
|
|
37
|
+
start.sh
|
|
38
|
+
README.md
|
|
39
|
+
automation/README.md
|
|
40
|
+
docs/自动化流程.md
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
本次不做:
|
|
44
|
+
|
|
45
|
+
```text
|
|
46
|
+
公司 GitLab issue 接入
|
|
47
|
+
公司 GitLab webhook 配置
|
|
48
|
+
公司 GitLab runner 管理
|
|
49
|
+
公司 CI 变量修改
|
|
50
|
+
双向同步
|
|
51
|
+
main 自动同步
|
|
52
|
+
整仓 mirror 到公司
|
|
53
|
+
本地正式 MR
|
|
54
|
+
自动合并
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
前提已经确认:
|
|
58
|
+
|
|
59
|
+
```text
|
|
60
|
+
公司 GitLab 8.16.8 API v3 可访问
|
|
61
|
+
PAT 可用
|
|
62
|
+
可以 push feature 分支
|
|
63
|
+
可以创建 MR
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
## 1. 给内部模型的操作要求
|
|
69
|
+
|
|
70
|
+
内部代码已经分叉,**不要整文件覆盖 `automation/issue_bot.py`**。
|
|
71
|
+
|
|
72
|
+
请按函数/代码块修改:
|
|
73
|
+
|
|
74
|
+
```text
|
|
75
|
+
1. 先备份
|
|
76
|
+
2. 用 grep 找函数
|
|
77
|
+
3. 只替换本文明确指出的函数/代码块
|
|
78
|
+
4. 保留内部已有的标签名、路径、日志、变量名
|
|
79
|
+
5. 不要删除内部已有的其他功能
|
|
80
|
+
6. 改完执行 py_compile / bash -n / 端到端测试
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
建议先定位:
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
grep -n '^def \|^class \|^PROJECT_PATH\|^TASKS\|^SEEN' automation/issue_bot.py
|
|
87
|
+
grep -n 'issue-bot:' docker-compose.yml
|
|
88
|
+
grep -n 'setup_automation\|ensure_mirror_token' scripts/gitlab-init.sh
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
如果内部已有 `COMPANY_*` 或类似变量,保持语义一致即可,不要求和本文变量名完全相同。
|
|
92
|
+
|
|
93
|
+
---
|
|
94
|
+
|
|
95
|
+
# 第一部分:配置
|
|
96
|
+
|
|
97
|
+
## 2. `.env` 和 `.env.example`
|
|
98
|
+
|
|
99
|
+
增加:
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
# 公司 GitLab 模式:配置后代码和 MR 都走公司 GitLab,本地只保留 issue
|
|
103
|
+
COMPANY_GITLAB_URL=http://gitlab.company.internal
|
|
104
|
+
COMPANY_TOKEN=
|
|
105
|
+
COMPANY_TOKEN_FILE=/run/automation/company-token
|
|
106
|
+
COMPANY_PROJECTS_FILE=/run/automation/company-projects.json
|
|
107
|
+
|
|
108
|
+
# 本地 GitLab 只读镜像 token
|
|
109
|
+
GITLAB_MIRROR_TOKEN_FILE=/run/automation/mirror-token
|
|
110
|
+
MIRROR_INTERVAL_SECONDS=300
|
|
111
|
+
|
|
112
|
+
# 门禁脚本:默认用公司仓库内的 scripts/check-convention.sh
|
|
113
|
+
# 公司仓库没有该脚本时,改成挂载进来的绝对路径
|
|
114
|
+
AUTOMATION_GATE_SCRIPT=scripts/check-convention.sh
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
说明:
|
|
118
|
+
|
|
119
|
+
- `COMPANY_TOKEN` 可以留空,优先用 `COMPANY_TOKEN_FILE`;
|
|
120
|
+
- `COMPANY_TOKEN_FILE` 和 `COMPANY_PROJECTS_FILE` 在容器内路径;
|
|
121
|
+
- 本地 issue 仍由 `AUTOMATION_PROJECTS` 和 `GITLAB_PROJECTS` 控制;
|
|
122
|
+
- `MIRROR_INTERVAL_SECONDS` 只影响镜像服务。
|
|
123
|
+
|
|
124
|
+
## 3. 公司项目映射
|
|
125
|
+
|
|
126
|
+
新增:
|
|
127
|
+
|
|
128
|
+
```text
|
|
129
|
+
automation/company-projects.example.json
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
内容:
|
|
133
|
+
|
|
134
|
+
```json
|
|
135
|
+
{
|
|
136
|
+
"local-ai/ai-tasks-payment": {
|
|
137
|
+
"project_id": 456,
|
|
138
|
+
"path": "company-group/payment-service",
|
|
139
|
+
"target_branch": "main"
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
部署时复制到:
|
|
145
|
+
|
|
146
|
+
```bash
|
|
147
|
+
cp automation/company-projects.example.json \
|
|
148
|
+
data/automation/company-projects.json
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
规则:
|
|
152
|
+
|
|
153
|
+
```text
|
|
154
|
+
key = 本地 GitLab 的 project 路径
|
|
155
|
+
project_id = 公司 GitLab 的 project ID(数字)
|
|
156
|
+
path = 公司 GitLab 的 project 路径
|
|
157
|
+
target_branch = 公司目标分支
|
|
158
|
+
clone_url = 可选,SSH clone 地址
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
本地 project、公司 project、group 名字都不需要一致。
|
|
162
|
+
|
|
163
|
+
必须一致的是:
|
|
164
|
+
|
|
165
|
+
```text
|
|
166
|
+
本地实际 project 路径
|
|
167
|
+
=
|
|
168
|
+
AUTOMATION_PROJECTS 中的本地路径
|
|
169
|
+
=
|
|
170
|
+
company-projects.json 的 key
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
公司侧:
|
|
174
|
+
|
|
175
|
+
```text
|
|
176
|
+
company-projects.json 的 path
|
|
177
|
+
=
|
|
178
|
+
公司实际 project 路径
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
`project_id` 必须是公司项目 ID,不是本地 `.gitlab-project-id`。
|
|
182
|
+
|
|
183
|
+
## 4. token 文件
|
|
184
|
+
|
|
185
|
+
公司 token:
|
|
186
|
+
|
|
187
|
+
```bash
|
|
188
|
+
printf '%s' '公司PAT' > data/automation/company-token
|
|
189
|
+
chmod 600 data/automation/company-token
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
本地镜像 token:
|
|
193
|
+
|
|
194
|
+
```bash
|
|
195
|
+
# 由 gitlab-init.sh 生成,见后文
|
|
196
|
+
data/automation/mirror-token
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
不要:
|
|
200
|
+
|
|
201
|
+
```text
|
|
202
|
+
把 token 写进 git remote URL
|
|
203
|
+
把 token 提交到仓库
|
|
204
|
+
把 token 打到日志
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
---
|
|
208
|
+
|
|
209
|
+
# 第二部分:`automation/issue_bot.py`
|
|
210
|
+
|
|
211
|
+
## 5. 新增公司模式常量
|
|
212
|
+
|
|
213
|
+
在 `GITLAB_URL`、`PROJECT_PATH` 附近增加:
|
|
214
|
+
|
|
215
|
+
```python
|
|
216
|
+
COMPANY_GITLAB_URL = os.environ.get("COMPANY_GITLAB_URL", "").rstrip("/")
|
|
217
|
+
COMPANY_TOKEN_FILE = os.environ.get("COMPANY_TOKEN_FILE", "/run/automation/company-token")
|
|
218
|
+
COMPANY_PROJECTS_FILE = Path(os.environ.get(
|
|
219
|
+
"COMPANY_PROJECTS_FILE", "/run/automation/company-projects.json"))
|
|
220
|
+
COMPANY_PROJECTS = {}
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
`Path` 已经由原文件导入,不要重复导入。
|
|
224
|
+
|
|
225
|
+
增加门禁脚本常量:
|
|
226
|
+
|
|
227
|
+
```python
|
|
228
|
+
# 门禁脚本:默认用仓库内的 scripts/check-convention.sh;
|
|
229
|
+
# 公司仓库没有该脚本时,挂载本地门禁并配成绝对路径。
|
|
230
|
+
GATE_SCRIPT = os.environ.get("AUTOMATION_GATE_SCRIPT", "scripts/check-convention.sh")
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
---
|
|
234
|
+
|
|
235
|
+
## 6. 加载公司项目映射
|
|
236
|
+
|
|
237
|
+
在 `log = logging.getLogger(...)` 后面增加:
|
|
238
|
+
|
|
239
|
+
```python
|
|
240
|
+
def load_company_projects():
|
|
241
|
+
"""加载本地 issue 到公司 GitLab 项目的映射。"""
|
|
242
|
+
global COMPANY_PROJECTS
|
|
243
|
+
COMPANY_PROJECTS = {}
|
|
244
|
+
if not COMPANY_PROJECTS_FILE.exists():
|
|
245
|
+
return
|
|
246
|
+
try:
|
|
247
|
+
raw = json.loads(COMPANY_PROJECTS_FILE.read_text(encoding="utf-8"))
|
|
248
|
+
except Exception as e:
|
|
249
|
+
log.error("读取公司 GitLab 项目映射失败 %s: %s", COMPANY_PROJECTS_FILE, e)
|
|
250
|
+
return
|
|
251
|
+
if not isinstance(raw, dict):
|
|
252
|
+
log.error("公司 GitLab 项目映射必须是 JSON 对象: %s", COMPANY_PROJECTS_FILE)
|
|
253
|
+
return
|
|
254
|
+
for local_project, cfg in raw.items():
|
|
255
|
+
if not isinstance(cfg, dict):
|
|
256
|
+
continue
|
|
257
|
+
project_id = cfg.get("project_id")
|
|
258
|
+
project_path = cfg.get("path")
|
|
259
|
+
if not isinstance(project_id, int) or not project_path:
|
|
260
|
+
log.error("公司 GitLab 项目映射缺少 project_id/path: %s", local_project)
|
|
261
|
+
continue
|
|
262
|
+
COMPANY_PROJECTS[local_project] = {
|
|
263
|
+
"project_id": project_id,
|
|
264
|
+
"path": project_path,
|
|
265
|
+
"target_branch": cfg.get("target_branch") or TARGET_BRANCH or "main",
|
|
266
|
+
"clone_url": cfg.get("clone_url") or "",
|
|
267
|
+
}
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
注意:
|
|
271
|
+
|
|
272
|
+
- `project_id` 必须是 int;
|
|
273
|
+
- 映射文件损坏时只记日志,不要杀掉进程;
|
|
274
|
+
- 真正执行任务时如果公司模式已开启但项目没有映射,要 fail closed,见 `company_for()`。
|
|
275
|
+
|
|
276
|
+
---
|
|
277
|
+
|
|
278
|
+
## 7. 公司 token / 项目路由 / API v3
|
|
279
|
+
|
|
280
|
+
新增:
|
|
281
|
+
|
|
282
|
+
```python
|
|
283
|
+
def company_token():
|
|
284
|
+
t = os.environ.get("COMPANY_TOKEN", "").strip()
|
|
285
|
+
if t:
|
|
286
|
+
return t
|
|
287
|
+
try:
|
|
288
|
+
return Path(COMPANY_TOKEN_FILE).read_text(encoding="utf-8").strip()
|
|
289
|
+
except OSError:
|
|
290
|
+
return ""
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def company_for(project):
|
|
294
|
+
"""公司 GitLab 模式:返回该项目配置;未配置公司地址时返回 None。"""
|
|
295
|
+
if not COMPANY_GITLAB_URL:
|
|
296
|
+
return None
|
|
297
|
+
cfg = COMPANY_PROJECTS.get(project)
|
|
298
|
+
if not cfg:
|
|
299
|
+
raise RuntimeError("公司 GitLab 模式未配置本地 project 映射: " + project)
|
|
300
|
+
if not company_token():
|
|
301
|
+
raise RuntimeError("公司 GitLab token 为空:检查 COMPANY_TOKEN 或 " + COMPANY_TOKEN_FILE)
|
|
302
|
+
return cfg
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
def company_clone_url(cfg):
|
|
306
|
+
if cfg.get("clone_url"):
|
|
307
|
+
return cfg["clone_url"]
|
|
308
|
+
u = urllib.parse.urlparse(COMPANY_GITLAB_URL)
|
|
309
|
+
tok = urllib.parse.quote(company_token(), safe="")
|
|
310
|
+
return "%s://oauth2:%s@%s/%s.git" % (u.scheme, tok, u.netloc, cfg["path"])
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
def company_api_raw(cfg, method, path, data=None, params=None, timeout=30):
|
|
314
|
+
t = company_token()
|
|
315
|
+
if not t:
|
|
316
|
+
raise RuntimeError("公司 GitLab token 为空:检查 COMPANY_TOKEN 或 " + COMPANY_TOKEN_FILE)
|
|
317
|
+
q = {"private_token": t}
|
|
318
|
+
if params:
|
|
319
|
+
q.update(params)
|
|
320
|
+
url = COMPANY_GITLAB_URL + "/api/v3/projects/%d%s" % (int(cfg["project_id"]), path)
|
|
321
|
+
url += "?" + urllib.parse.urlencode(q)
|
|
322
|
+
body = urllib.parse.urlencode(data).encode("utf-8") if data is not None else None
|
|
323
|
+
req = urllib.request.Request(url, data=body, method=method)
|
|
324
|
+
if body is not None:
|
|
325
|
+
req.add_header("Content-Type", "application/x-www-form-urlencoded")
|
|
326
|
+
try:
|
|
327
|
+
with urllib.request.urlopen(req, timeout=timeout) as r:
|
|
328
|
+
raw = r.read()
|
|
329
|
+
return json.loads(raw) if raw else {}
|
|
330
|
+
except urllib.error.HTTPError as e:
|
|
331
|
+
detail = e.read().decode("utf-8", "replace")[:500]
|
|
332
|
+
raise RuntimeError("Company GitLab API %s %s -> %s: %s" %
|
|
333
|
+
(method, path, e.code, mask(detail)))
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
def company_api(cfg, method, path, data=None, params=None, timeout=30):
|
|
337
|
+
return company_api_raw(cfg, method, path, data=data, params=params, timeout=timeout)
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
def company_note_mr(cfg, mr_iid, body):
|
|
341
|
+
try:
|
|
342
|
+
company_api(cfg, "POST", "/merge_requests/%d/notes" % mr_iid, {"body": body})
|
|
343
|
+
except Exception as e:
|
|
344
|
+
log.warning("评论公司 MR !%s 失败: %s", mr_iid, mask(e))
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
关键点:
|
|
348
|
+
|
|
349
|
+
```text
|
|
350
|
+
公司 API 固定 /api/v3,不要用 /api/v4
|
|
351
|
+
认证参数用 private_token
|
|
352
|
+
POST 用 application/x-www-form-urlencoded
|
|
353
|
+
project_id 用公司 ID
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
如果公司 8.16 对 `private_token` 参数有限制,可以在 `company_api_raw()` 里改成已验证可用的认证方式,但不要把 token 打到日志。
|
|
357
|
+
|
|
358
|
+
---
|
|
359
|
+
|
|
360
|
+
## 8. 日志中屏蔽公司 token
|
|
361
|
+
|
|
362
|
+
找到原 `mask()`:
|
|
363
|
+
|
|
364
|
+
```python
|
|
365
|
+
def mask(text):
|
|
366
|
+
t = token()
|
|
367
|
+
if t:
|
|
368
|
+
return str(text).replace(t, "***")
|
|
369
|
+
return str(text)
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
改为:
|
|
373
|
+
|
|
374
|
+
```python
|
|
375
|
+
def mask(text):
|
|
376
|
+
out = str(text)
|
|
377
|
+
for value in (token(), company_token()):
|
|
378
|
+
if value:
|
|
379
|
+
out = out.replace(value, "***")
|
|
380
|
+
return out
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
---
|
|
384
|
+
|
|
385
|
+
## 9. 门禁脚本可配置
|
|
386
|
+
|
|
387
|
+
原 `build_prompt()` 中:
|
|
388
|
+
|
|
389
|
+
```text
|
|
390
|
+
4. 完成后运行 ./scripts/check-convention.sh,确保通过;
|
|
391
|
+
```
|
|
392
|
+
|
|
393
|
+
改为:
|
|
394
|
+
|
|
395
|
+
```text
|
|
396
|
+
4. 完成后运行 %s,确保通过;
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
并把 `GATE_SCRIPT` 加入格式化参数。
|
|
400
|
+
|
|
401
|
+
原 `_handle()` 中:
|
|
402
|
+
|
|
403
|
+
```python
|
|
404
|
+
r = run(["bash", "scripts/check-convention.sh"], cwd=ws, timeout=300)
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
改为:
|
|
408
|
+
|
|
409
|
+
```python
|
|
410
|
+
gate = GATE_SCRIPT
|
|
411
|
+
if not os.path.isabs(gate):
|
|
412
|
+
gate = str(ws / gate)
|
|
413
|
+
r = run(["bash", gate], cwd=ws, timeout=300)
|
|
414
|
+
```
|
|
415
|
+
|
|
416
|
+
公司仓库没有 `scripts/check-convention.sh` 时:
|
|
417
|
+
|
|
418
|
+
```bash
|
|
419
|
+
AUTOMATION_GATE_SCRIPT=/opt/automation/check-convention.sh
|
|
420
|
+
```
|
|
421
|
+
|
|
422
|
+
并把脚本挂进 issue-bot 容器。
|
|
423
|
+
|
|
424
|
+
---
|
|
425
|
+
|
|
426
|
+
## 10. `_handle()`:公司模式和重复 MR 检查
|
|
427
|
+
|
|
428
|
+
原 `_handle()` 在加载 state 后、发送“已接单”前,增加:
|
|
429
|
+
|
|
430
|
+
```python
|
|
431
|
+
# 公司 GitLab 模式:代码从公司仓库拉,MR 也开到公司;本地 GitLab 只保留 issue。
|
|
432
|
+
company = company_for(project)
|
|
433
|
+
company_mode = company is not None
|
|
434
|
+
|
|
435
|
+
if not company_mode:
|
|
436
|
+
# 同一 issue 已有打开的本地 MR 时不再重复开
|
|
437
|
+
for mr in api_get(project, "/merge_requests", state="opened", per_page=100):
|
|
438
|
+
if mr.get("source_branch", "").startswith("ai/issue-%d-" % iid) or \
|
|
439
|
+
("Closes #%d" % iid) in (mr.get("description") or ""):
|
|
440
|
+
note_issue(project, iid, "已有打开的 MR:%s,跳过重复处理。" % mr.get("web_url", ""))
|
|
441
|
+
set_labels_safe(project, iid, add=[DONE_LABEL], remove=[PROCESSING_LABEL])
|
|
442
|
+
return
|
|
443
|
+
else:
|
|
444
|
+
# 公司模式下必须查公司 MR,避免重启/重复触发后重复开 MR
|
|
445
|
+
try:
|
|
446
|
+
open_mrs = company_api(company, "GET", "/merge_requests",
|
|
447
|
+
params={"state": "opened"})
|
|
448
|
+
except Exception as e:
|
|
449
|
+
raise RuntimeError("查询公司 MR 失败,已停止以避免重复开 MR: " + mask(e))
|
|
450
|
+
for mr in open_mrs:
|
|
451
|
+
if mr.get("source_branch", "").startswith("ai/issue-%d-" % iid):
|
|
452
|
+
note_issue(project, iid, "已有打开的公司 MR:%s,跳过重复处理。"
|
|
453
|
+
% mr.get("web_url", ""))
|
|
454
|
+
set_labels_safe(project, iid, add=[DONE_LABEL], remove=[PROCESSING_LABEL])
|
|
455
|
+
return
|
|
456
|
+
```
|
|
457
|
+
|
|
458
|
+
注意:
|
|
459
|
+
|
|
460
|
+
- 公司模式必须查公司 MR,不能查本地 MR;
|
|
461
|
+
- 查询失败时停止,不要盲目重复创建;
|
|
462
|
+
- 重复时本地 issue 标 `ai-done`,不重新推送。
|
|
463
|
+
|
|
464
|
+
---
|
|
465
|
+
|
|
466
|
+
## 11. `_handle()`:clone 来源
|
|
467
|
+
|
|
468
|
+
原代码:
|
|
469
|
+
|
|
470
|
+
```python
|
|
471
|
+
project_info = api_get(project, "")
|
|
472
|
+
target = TARGET_BRANCH or project_info.get("default_branch") or "main"
|
|
473
|
+
ws.parent.mkdir(parents=True, exist_ok=True)
|
|
474
|
+
log.info("clone %s 到 %s", project, ws)
|
|
475
|
+
r = run(["git", "clone", "--depth", "1", "--branch", target, clone_url(project), str(ws)],
|
|
476
|
+
cwd="/tmp", timeout=300, log_cmd=False)
|
|
477
|
+
```
|
|
478
|
+
|
|
479
|
+
改为:
|
|
480
|
+
|
|
481
|
+
```python
|
|
482
|
+
if company_mode:
|
|
483
|
+
target = company["target_branch"]
|
|
484
|
+
source_url = company_clone_url(company)
|
|
485
|
+
log.info("使用公司 GitLab 仓库: %s", company["path"])
|
|
486
|
+
else:
|
|
487
|
+
project_info = api_get(project, "")
|
|
488
|
+
target = TARGET_BRANCH or project_info.get("default_branch") or "main"
|
|
489
|
+
source_url = clone_url(project)
|
|
490
|
+
ws.parent.mkdir(parents=True, exist_ok=True)
|
|
491
|
+
log.info("clone %s 到 %s", project, ws)
|
|
492
|
+
r = run(["git", "clone", "--depth", "1", "--branch", target, source_url, str(ws)],
|
|
493
|
+
cwd="/tmp", timeout=300, log_cmd=False)
|
|
494
|
+
```
|
|
495
|
+
|
|
496
|
+
这样公司模式会直接从公司仓库 clone,不再碰本地仓库代码。
|
|
497
|
+
|
|
498
|
+
---
|
|
499
|
+
|
|
500
|
+
## 12. `_handle()`:推送和开 MR
|
|
501
|
+
|
|
502
|
+
原代码在 `git push` 之后,会创建本地 MR 并等待本地 pipeline。
|
|
503
|
+
|
|
504
|
+
公司模式必须改成:
|
|
505
|
+
|
|
506
|
+
```text
|
|
507
|
+
push 到公司 origin
|
|
508
|
+
→ 调公司 API v3 创建 MR
|
|
509
|
+
→ 本地 issue 评论公司 MR 链接
|
|
510
|
+
→ 本地 issue 标 ai-done
|
|
511
|
+
→ 不创建本地 MR
|
|
512
|
+
→ 不等待本地 pipeline
|
|
513
|
+
```
|
|
514
|
+
|
|
515
|
+
在 `git push` 成功后,先提取 assumptions:
|
|
516
|
+
|
|
517
|
+
```python
|
|
518
|
+
assumptions = extract_block(output, "ASSUMPTIONS", "END_ASSUMPTIONS")
|
|
519
|
+
```
|
|
520
|
+
|
|
521
|
+
然后插入公司模式分支:
|
|
522
|
+
|
|
523
|
+
```python
|
|
524
|
+
if company_mode:
|
|
525
|
+
desc = "由 AI 自动化生成,关联本地任务 #%d。" % iid
|
|
526
|
+
if assumptions:
|
|
527
|
+
desc += "\n\n### AI 假设与待确认点\n\n" + assumptions[:4000]
|
|
528
|
+
mr = company_api(company, "POST", "/merge_requests", {
|
|
529
|
+
"source_branch": branch,
|
|
530
|
+
"target_branch": target,
|
|
531
|
+
"title": "feat(ai): %s (#%d)" % (title, iid),
|
|
532
|
+
"description": desc,
|
|
533
|
+
})
|
|
534
|
+
mr_iid = mr.get("iid")
|
|
535
|
+
mr_url = mr.get("web_url", "")
|
|
536
|
+
clear_state(project, iid)
|
|
537
|
+
set_labels_safe(project, iid, add=[DONE_LABEL],
|
|
538
|
+
remove=[PROCESSING_LABEL, TRIGGER_LABEL, RESUME_LABEL,
|
|
539
|
+
WAIT_LABEL, FAILED_LABEL])
|
|
540
|
+
note_issue(project, iid, "✅ 公司 MR 已创建:%s" % mr_url)
|
|
541
|
+
if mr_iid:
|
|
542
|
+
company_note_mr(company, mr_iid,
|
|
543
|
+
"🤖 本 MR 由本地 issue #%d 自动生成。" % iid)
|
|
544
|
+
log.info("%s issue #%s 完成,公司 MR=%s", project, iid, mr_url)
|
|
545
|
+
return
|
|
546
|
+
```
|
|
547
|
+
|
|
548
|
+
本地模式的原有逻辑保留在 `return` 之后:
|
|
549
|
+
|
|
550
|
+
```python
|
|
551
|
+
# 以下只用于未配置公司模式的本地模式
|
|
552
|
+
try:
|
|
553
|
+
api(project, "POST", "/labels", {"name": "path::ai", "color": "#428bca"})
|
|
554
|
+
except Exception:
|
|
555
|
+
pass
|
|
556
|
+
...
|
|
557
|
+
```
|
|
558
|
+
|
|
559
|
+
注意:
|
|
560
|
+
|
|
561
|
+
- 公司 MR 描述不要写 `Closes #iid`,因为公司 issue 编号和本地不同;
|
|
562
|
+
- 公司模式不调用 `wait_pipeline()`;
|
|
563
|
+
- 公司 CI 由公司 GitLab 自己执行。
|
|
564
|
+
|
|
565
|
+
---
|
|
566
|
+
|
|
567
|
+
## 13. 启动恢复
|
|
568
|
+
|
|
569
|
+
原 `recover_stale_processing()` 只查本地 MR:
|
|
570
|
+
|
|
571
|
+
```python
|
|
572
|
+
open_mrs = api_get(project, "/merge_requests", state="opened", per_page=100)
|
|
573
|
+
```
|
|
574
|
+
|
|
575
|
+
改为:
|
|
576
|
+
|
|
577
|
+
```python
|
|
578
|
+
try:
|
|
579
|
+
company = company_for(project)
|
|
580
|
+
except Exception as e:
|
|
581
|
+
log.warning("恢复 %s 时检查公司 GitLab 配置失败: %s", project, mask(e))
|
|
582
|
+
return
|
|
583
|
+
try:
|
|
584
|
+
if company:
|
|
585
|
+
open_mrs = company_api(company, "GET", "/merge_requests",
|
|
586
|
+
params={"state": "opened"})
|
|
587
|
+
else:
|
|
588
|
+
open_mrs = api_get(project, "/merge_requests", state="opened", per_page=100)
|
|
589
|
+
except Exception as e:
|
|
590
|
+
log.warning("查询 %s 打开 MR 失败,跳过启动恢复以避免误标: %s", project, mask(e))
|
|
591
|
+
return
|
|
592
|
+
```
|
|
593
|
+
|
|
594
|
+
其余判断保持不变:
|
|
595
|
+
|
|
596
|
+
```python
|
|
597
|
+
has_mr = any(
|
|
598
|
+
m.get("source_branch", "").startswith("ai/issue-%d-" % iid)
|
|
599
|
+
or ("Closes #%d" % iid) in (m.get("description") or "")
|
|
600
|
+
for m in open_mrs
|
|
601
|
+
)
|
|
602
|
+
```
|
|
603
|
+
|
|
604
|
+
这样:
|
|
605
|
+
|
|
606
|
+
```text
|
|
607
|
+
公司模式:查看公司 MR
|
|
608
|
+
本地模式:查看本地 MR
|
|
609
|
+
```
|
|
610
|
+
|
|
611
|
+
## 14. `main()` 加载公司配置
|
|
612
|
+
|
|
613
|
+
在 `main()` 开始处增加:
|
|
614
|
+
|
|
615
|
+
```python
|
|
616
|
+
load_company_projects()
|
|
617
|
+
```
|
|
618
|
+
|
|
619
|
+
并增加日志:
|
|
620
|
+
|
|
621
|
+
```python
|
|
622
|
+
if COMPANY_GITLAB_URL:
|
|
623
|
+
if not company_token():
|
|
624
|
+
log.error("COMPANY_GITLAB_URL 已配置,但公司 GitLab token 为空")
|
|
625
|
+
log.info("公司 GitLab 模式:%s,映射 %d 个本地 project",
|
|
626
|
+
COMPANY_GITLAB_URL, len(COMPANY_PROJECTS))
|
|
627
|
+
```
|
|
628
|
+
|
|
629
|
+
`/healthz` 可以增加:
|
|
630
|
+
|
|
631
|
+
```python
|
|
632
|
+
"company": bool(COMPANY_GITLAB_URL),
|
|
633
|
+
"company_projects": sorted(COMPANY_PROJECTS)
|
|
634
|
+
```
|
|
635
|
+
|
|
636
|
+
方便验证配置是否加载。
|
|
637
|
+
|
|
638
|
+
---
|
|
639
|
+
|
|
640
|
+
# 第三部分:本地只读镜像
|
|
641
|
+
|
|
642
|
+
## 15. 新增 `scripts/mirror-company.sh`
|
|
643
|
+
|
|
644
|
+
新增完整文件:
|
|
645
|
+
|
|
646
|
+
```bash
|
|
647
|
+
#!/usr/bin/env bash
|
|
648
|
+
# ============================================================================
|
|
649
|
+
# 公司 GitLab → 本地 GitLab 单向只读镜像
|
|
650
|
+
#
|
|
651
|
+
# 只同步公司仓库的分支和 tag 到本地同名 project。
|
|
652
|
+
# 本地 project 只用于查看代码和 issue,不允许在本地提交/开正式 MR。
|
|
653
|
+
#
|
|
654
|
+
# 用法:
|
|
655
|
+
# ./scripts/mirror-company.sh --once
|
|
656
|
+
# ./scripts/mirror-company.sh --loop
|
|
657
|
+
# ============================================================================
|
|
658
|
+
set -euo pipefail
|
|
659
|
+
umask 077
|
|
660
|
+
|
|
661
|
+
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
662
|
+
cd "$ROOT_DIR"
|
|
663
|
+
|
|
664
|
+
COMPANY_GITLAB_URL=${COMPANY_GITLAB_URL:-}
|
|
665
|
+
COMPANY_TOKEN_FILE=${COMPANY_TOKEN_FILE:-/run/automation/company-token}
|
|
666
|
+
COMPANY_PROJECTS_FILE=${COMPANY_PROJECTS_FILE:-/run/automation/company-projects.json}
|
|
667
|
+
LOCAL_GITLAB_URL=${LOCAL_GITLAB_URL:-${GITLAB_URL:-http://gitlab.local:8929}}
|
|
668
|
+
LOCAL_TOKEN_FILE=${GITLAB_MIRROR_TOKEN_FILE:-/run/automation/mirror-token}
|
|
669
|
+
MIRROR_CACHE_DIR=${MIRROR_CACHE_DIR:-data/mirror}
|
|
670
|
+
MIRROR_INTERVAL_SECONDS=${MIRROR_INTERVAL_SECONDS:-300}
|
|
671
|
+
|
|
672
|
+
log() { echo "[$(date '+%F %T')] $*"; }
|
|
673
|
+
|
|
674
|
+
read_secret() {
|
|
675
|
+
local file=$1
|
|
676
|
+
[ -s "$file" ] && tr -d '\r\n' < "$file" || true
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
repo_url() {
|
|
680
|
+
local base=${1%/} path=$2
|
|
681
|
+
printf '%s/%s.git\n' "$base" "$path"
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
write_askpass() {
|
|
685
|
+
local script=$1 token_file=$2
|
|
686
|
+
cat > "$script" <<-ASKPASS
|
|
687
|
+
#!/bin/sh
|
|
688
|
+
case "$1" in
|
|
689
|
+
*Username*) printf '%s\n' oauth2 ;;
|
|
690
|
+
*Password*) cat "$token_file" ;;
|
|
691
|
+
*) exit 1 ;;
|
|
692
|
+
esac
|
|
693
|
+
ASKPASS
|
|
694
|
+
chmod 700 "$script"
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
sync_one() {
|
|
698
|
+
local local_project=$1 company_path=$2
|
|
699
|
+
local cache company_url local_url hash
|
|
700
|
+
hash=$(printf '%s' "$local_project" | sha256sum | cut -c1-16)
|
|
701
|
+
cache="$MIRROR_CACHE_DIR/$hash.git"
|
|
702
|
+
company_url=$(repo_url "$COMPANY_GITLAB_URL" "$company_path")
|
|
703
|
+
local_url=$(repo_url "$LOCAL_GITLAB_URL" "$local_project")
|
|
704
|
+
|
|
705
|
+
mkdir -p "$MIRROR_CACHE_DIR"
|
|
706
|
+
chmod 700 "$MIRROR_CACHE_DIR" 2>/dev/null || true
|
|
707
|
+
if [ ! -d "$cache" ]; then
|
|
708
|
+
log "初始化镜像 $local_project ← $company_path"
|
|
709
|
+
GIT_ASKPASS="$COMPANY_ASKPASS" GIT_ASKPASS_REQUIRE=force GIT_TERMINAL_PROMPT=0 \
|
|
710
|
+
git clone --mirror "$company_url" "$cache" >/dev/null 2>&1 || {
|
|
711
|
+
log "ERROR clone 公司仓库失败:$company_path"
|
|
712
|
+
return 1
|
|
713
|
+
}
|
|
714
|
+
else
|
|
715
|
+
git -C "$cache" remote set-url origin "$company_url" >/dev/null 2>&1 || return 1
|
|
716
|
+
# clone --mirror 的 fetch refspec 是 +refs/*:refs/*,--prune 会删除公司已删的分支/tag
|
|
717
|
+
GIT_ASKPASS="$COMPANY_ASKPASS" GIT_ASKPASS_REQUIRE=force GIT_TERMINAL_PROMPT=0 \
|
|
718
|
+
git -C "$cache" fetch --prune origin >/dev/null 2>&1 || {
|
|
719
|
+
log "ERROR fetch 公司仓库失败:$company_path"
|
|
720
|
+
return 1
|
|
721
|
+
}
|
|
722
|
+
fi
|
|
723
|
+
chmod 700 "$cache" 2>/dev/null || true
|
|
724
|
+
|
|
725
|
+
GIT_ASKPASS="$LOCAL_ASKPASS" GIT_ASKPASS_REQUIRE=force GIT_TERMINAL_PROMPT=0 \
|
|
726
|
+
git -C "$cache" push --mirror "$local_url" >/dev/null 2>&1 || {
|
|
727
|
+
log "ERROR push 本地镜像失败:$local_project"
|
|
728
|
+
return 1
|
|
729
|
+
}
|
|
730
|
+
log "OK $local_project ← $company_path"
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
sync_all() {
|
|
734
|
+
if [ -z "$COMPANY_GITLAB_URL" ]; then
|
|
735
|
+
log "COMPANY_GITLAB_URL 为空,跳过镜像"
|
|
736
|
+
return 0
|
|
737
|
+
fi
|
|
738
|
+
if [ ! -s "$COMPANY_TOKEN_FILE" ] && [ -z "${COMPANY_TOKEN:-}" ]; then
|
|
739
|
+
log "公司 token 为空,跳过镜像"
|
|
740
|
+
return 1
|
|
741
|
+
fi
|
|
742
|
+
if [ ! -s "$LOCAL_TOKEN_FILE" ] && [ -z "${GITLAB_MIRROR_TOKEN:-}" ]; then
|
|
743
|
+
log "本地镜像 token 为空:$LOCAL_TOKEN_FILE"
|
|
744
|
+
return 1
|
|
745
|
+
fi
|
|
746
|
+
|
|
747
|
+
COMPANY_TOKEN=${COMPANY_TOKEN:-$(read_secret "$COMPANY_TOKEN_FILE")}
|
|
748
|
+
LOCAL_TOKEN=${GITLAB_MIRROR_TOKEN:-$(read_secret "$LOCAL_TOKEN_FILE")}
|
|
749
|
+
|
|
750
|
+
ASKPASS_DIR=$(mktemp -d)
|
|
751
|
+
trap 'rm -rf "$ASKPASS_DIR"' RETURN
|
|
752
|
+
printf '%s' "$COMPANY_TOKEN" > "$ASKPASS_DIR/company.token"
|
|
753
|
+
printf '%s' "$LOCAL_TOKEN" > "$ASKPASS_DIR/local.token"
|
|
754
|
+
chmod 700 "$ASKPASS_DIR"; chmod 600 "$ASKPASS_DIR"/*.token
|
|
755
|
+
COMPANY_ASKPASS="$ASKPASS_DIR/company-askpass"
|
|
756
|
+
LOCAL_ASKPASS="$ASKPASS_DIR/local-askpass"
|
|
757
|
+
write_askpass "$COMPANY_ASKPASS" "$ASKPASS_DIR/company.token"
|
|
758
|
+
write_askpass "$LOCAL_ASKPASS" "$ASKPASS_DIR/local.token"
|
|
759
|
+
|
|
760
|
+
local lines
|
|
761
|
+
lines=$(python3 - "$COMPANY_PROJECTS_FILE" <<'PY'
|
|
762
|
+
import json, sys
|
|
763
|
+
try:
|
|
764
|
+
data = json.load(open(sys.argv[1], encoding="utf-8"))
|
|
765
|
+
except Exception as e:
|
|
766
|
+
print("ERROR\t%s" % e)
|
|
767
|
+
raise SystemExit(1)
|
|
768
|
+
for local_project, cfg in data.items():
|
|
769
|
+
if isinstance(cfg, dict) and cfg.get("path"):
|
|
770
|
+
print("%s\t%s" % (local_project, cfg["path"]))
|
|
771
|
+
PY
|
|
772
|
+
) || { log "读取 $COMPANY_PROJECTS_FILE 失败"; return 1; }
|
|
773
|
+
|
|
774
|
+
local failed=0
|
|
775
|
+
while IFS=$'\t' read -r local_project company_path; do
|
|
776
|
+
[ -n "$local_project" ] || continue
|
|
777
|
+
sync_one "$local_project" "$company_path" || failed=1
|
|
778
|
+
done <<< "$lines"
|
|
779
|
+
return "$failed"
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
case "${1:-}" in
|
|
783
|
+
--once)
|
|
784
|
+
sync_all
|
|
785
|
+
;;
|
|
786
|
+
--loop|"")
|
|
787
|
+
while true; do
|
|
788
|
+
sync_all || true
|
|
789
|
+
sleep "$MIRROR_INTERVAL_SECONDS"
|
|
790
|
+
done
|
|
791
|
+
;;
|
|
792
|
+
*)
|
|
793
|
+
echo "用法: $0 [--once|--loop]" >&2
|
|
794
|
+
exit 2
|
|
795
|
+
;;
|
|
796
|
+
esac
|
|
797
|
+
```
|
|
798
|
+
|
|
799
|
+
关键点:
|
|
800
|
+
|
|
801
|
+
```text
|
|
802
|
+
公司 → 本地 单向
|
|
803
|
+
clone --mirror + fetch --prune + push --mirror
|
|
804
|
+
token 用 GIT_ASKPASS 临时提供,不写进 git remote
|
|
805
|
+
缓存目录 700,token 文件 600
|
|
806
|
+
公司删分支,本地也删
|
|
807
|
+
```
|
|
808
|
+
|
|
809
|
+
本地 project 必须提前创建,否则 push 镜像会失败。
|
|
810
|
+
|
|
811
|
+
本地镜像 project 建议:
|
|
812
|
+
|
|
813
|
+
```text
|
|
814
|
+
关闭 CI/CD
|
|
815
|
+
不要在本地提交
|
|
816
|
+
不要在本地创建正式 MR
|
|
817
|
+
```
|
|
818
|
+
|
|
819
|
+
---
|
|
820
|
+
|
|
821
|
+
## 16. `docker-compose.yml` 增加 company-mirror
|
|
822
|
+
|
|
823
|
+
在 issue-bot 服务后面增加:
|
|
824
|
+
|
|
825
|
+
```yaml
|
|
826
|
+
company-mirror:
|
|
827
|
+
image: ${AUTOMATION_BASE_IMAGE:-ai-coding-python:2026.10}
|
|
828
|
+
container_name: ai-coding-pilot-company-mirror
|
|
829
|
+
profiles: ["automation"]
|
|
830
|
+
restart: unless-stopped
|
|
831
|
+
depends_on:
|
|
832
|
+
gitlab:
|
|
833
|
+
condition: service_started
|
|
834
|
+
networks:
|
|
835
|
+
- internal
|
|
836
|
+
extra_hosts:
|
|
837
|
+
- "gitlab.local:host-gateway"
|
|
838
|
+
environment:
|
|
839
|
+
GITLAB_URL: http://${GITLAB_HOST:-gitlab.local}:${GITLAB_PORT:-8929}
|
|
840
|
+
GITLAB_MIRROR_TOKEN_FILE: /run/automation/mirror-token
|
|
841
|
+
GITLAB_MIRROR_TOKEN: ${GITLAB_MIRROR_TOKEN:-}
|
|
842
|
+
COMPANY_GITLAB_URL: ${COMPANY_GITLAB_URL:-}
|
|
843
|
+
COMPANY_TOKEN: ${COMPANY_TOKEN:-}
|
|
844
|
+
COMPANY_TOKEN_FILE: /run/automation/company-token
|
|
845
|
+
COMPANY_PROJECTS_FILE: /run/automation/company-projects.json
|
|
846
|
+
MIRROR_CACHE_DIR: /mirror
|
|
847
|
+
MIRROR_INTERVAL_SECONDS: ${MIRROR_INTERVAL_SECONDS:-300}
|
|
848
|
+
HOME: /root
|
|
849
|
+
volumes:
|
|
850
|
+
- ./scripts/mirror-company.sh:/usr/local/bin/mirror-company.sh:ro
|
|
851
|
+
- ./data/automation:/run/automation:ro
|
|
852
|
+
- ./data/mirror:/mirror
|
|
853
|
+
entrypoint: ["bash", "/usr/local/bin/mirror-company.sh", "--loop"]
|
|
854
|
+
```
|
|
855
|
+
|
|
856
|
+
如果内部 compose 没有 `AUTOMATION_BASE_IMAGE`,就用实际包含 `git/bash/python3/sha256sum` 的镜像。不要使用没有 `git` 的纯 Python 镜像。
|
|
857
|
+
|
|
858
|
+
同时 issue-bot 环境变量增加:
|
|
859
|
+
|
|
860
|
+
```yaml
|
|
861
|
+
COMPANY_GITLAB_URL: ${COMPANY_GITLAB_URL:-}
|
|
862
|
+
COMPANY_TOKEN: ${COMPANY_TOKEN:-}
|
|
863
|
+
COMPANY_TOKEN_FILE: /run/automation/company-token
|
|
864
|
+
COMPANY_PROJECTS_FILE: /run/automation/company-projects.json
|
|
865
|
+
AUTOMATION_GATE_SCRIPT: ${AUTOMATION_GATE_SCRIPT:-scripts/check-convention.sh}
|
|
866
|
+
```
|
|
867
|
+
|
|
868
|
+
---
|
|
869
|
+
|
|
870
|
+
## 17. `scripts/gitlab-init.sh` 生成本地镜像 token
|
|
871
|
+
|
|
872
|
+
增加函数:
|
|
873
|
+
|
|
874
|
+
```bash
|
|
875
|
+
ensure_mirror_token() {
|
|
876
|
+
mkdir -p data/automation
|
|
877
|
+
if [ -n "${GITLAB_MIRROR_TOKEN:-}" ]; then
|
|
878
|
+
info "使用 .env 里的 GITLAB_MIRROR_TOKEN"
|
|
879
|
+
return 0
|
|
880
|
+
fi
|
|
881
|
+
if [ -s .gitlab-pat ] && [ ! -s data/automation/mirror-token ]; then
|
|
882
|
+
cp .gitlab-pat data/automation/mirror-token
|
|
883
|
+
chmod 600 data/automation/mirror-token
|
|
884
|
+
info "本地 GitLab 镜像 token 已写入 data/automation/mirror-token"
|
|
885
|
+
elif [ -s data/automation/mirror-token ]; then
|
|
886
|
+
info "复用已有 data/automation/mirror-token"
|
|
887
|
+
fi
|
|
888
|
+
}
|
|
889
|
+
```
|
|
890
|
+
|
|
891
|
+
调用位置:
|
|
892
|
+
|
|
893
|
+
```bash
|
|
894
|
+
start_stack
|
|
895
|
+
mint_pat
|
|
896
|
+
ensure_mirror_token
|
|
897
|
+
create_project
|
|
898
|
+
```
|
|
899
|
+
|
|
900
|
+
生产环境建议换成:
|
|
901
|
+
|
|
902
|
+
```text
|
|
903
|
+
只对镜像 project 有 write_repository 权限的专用 token
|
|
904
|
+
```
|
|
905
|
+
|
|
906
|
+
不要长期使用 root PAT。
|
|
907
|
+
|
|
908
|
+
---
|
|
909
|
+
|
|
910
|
+
## 18. `start.sh` 自动启动镜像
|
|
911
|
+
|
|
912
|
+
在启动 issue-bot 后增加:
|
|
913
|
+
|
|
914
|
+
```bash
|
|
915
|
+
if grep -qE '^COMPANY_GITLAB_URL=.' .env; then
|
|
916
|
+
echo "启动公司 GitLab 只读镜像 ..."
|
|
917
|
+
docker compose --profile automation up -d company-mirror \
|
|
918
|
+
|| { echo -e "${RED}✘ company-mirror 启动失败,检查 automation/README.md${NC}"; exit 1; }
|
|
919
|
+
fi
|
|
920
|
+
```
|
|
921
|
+
|
|
922
|
+
---
|
|
923
|
+
|
|
924
|
+
## 19. `scripts/selfcheck.sh`
|
|
925
|
+
|
|
926
|
+
### 19.1 group webhook 路径编码
|
|
927
|
+
|
|
928
|
+
原来自检用 group path 直接拼 URL,子组会失败。改成:
|
|
929
|
+
|
|
930
|
+
```bash
|
|
931
|
+
local hooks hook_ok group_path group_enc
|
|
932
|
+
group_path=$(grep -E '^AUTOMATION_GROUP=' .env 2>/dev/null | cut -d= -f2)
|
|
933
|
+
group_path=${group_path:-observability}
|
|
934
|
+
group_enc=$(python3 -c 'import sys,urllib.parse;print(urllib.parse.quote(sys.argv[1],safe=""))' "$group_path")
|
|
935
|
+
hooks=$(curl -fsS -m 10 --header "PRIVATE-TOKEN: $pat" \
|
|
936
|
+
"${GITLAB_URL}/api/v4/groups/${group_enc}/hooks" 2>/dev/null || echo '[]')
|
|
937
|
+
```
|
|
938
|
+
|
|
939
|
+
保留原来的项目级 webhook 回退逻辑。
|
|
940
|
+
|
|
941
|
+
### 19.2 检查 company-mirror
|
|
942
|
+
|
|
943
|
+
在 automation 检查块中增加:
|
|
944
|
+
|
|
945
|
+
```bash
|
|
946
|
+
if grep -qE '^COMPANY_GITLAB_URL=.' .env 2>/dev/null; then
|
|
947
|
+
if docker ps --format '{{.Names}}' | grep -q '^ai-coding-pilot-company-mirror$'; then
|
|
948
|
+
ok "company-mirror 容器在跑"
|
|
949
|
+
else
|
|
950
|
+
bad "COMPANY_GITLAB_URL 已配置,但 company-mirror 容器没跑"
|
|
951
|
+
fi
|
|
952
|
+
fi
|
|
953
|
+
```
|
|
954
|
+
|
|
955
|
+
---
|
|
956
|
+
|
|
957
|
+
# 第四部分:测试与验收
|
|
958
|
+
|
|
959
|
+
## 20. 语法检查
|
|
960
|
+
|
|
961
|
+
```bash
|
|
962
|
+
python3 -m py_compile automation/issue_bot.py
|
|
963
|
+
bash -n scripts/mirror-company.sh
|
|
964
|
+
bash -n scripts/gitlab-init.sh
|
|
965
|
+
bash -n scripts/selfcheck.sh
|
|
966
|
+
bash -n start.sh
|
|
967
|
+
docker compose --profile automation config >/dev/null
|
|
968
|
+
```
|
|
969
|
+
|
|
970
|
+
## 21. 公司 API v3 验证
|
|
971
|
+
|
|
972
|
+
先在公司内网验证:
|
|
973
|
+
|
|
974
|
+
```bash
|
|
975
|
+
curl -i -sS -G "$COMPANY_GITLAB_URL/api/v3/user" \
|
|
976
|
+
--data-urlencode "private_token=$COMPANY_TOKEN"
|
|
977
|
+
```
|
|
978
|
+
|
|
979
|
+
以及:
|
|
980
|
+
|
|
981
|
+
```bash
|
|
982
|
+
curl -i -sS -G \
|
|
983
|
+
"$COMPANY_GITLAB_URL/api/v3/projects/$COMPANY_PROJECT_ID" \
|
|
984
|
+
--data-urlencode "private_token=$COMPANY_TOKEN"
|
|
985
|
+
```
|
|
986
|
+
|
|
987
|
+
## 22. 公司模式端到端
|
|
988
|
+
|
|
989
|
+
1. 本地建 project;
|
|
990
|
+
2. `.env` 配 `AUTOMATION_PROJECTS`;
|
|
991
|
+
3. `company-projects.json` 配映射;
|
|
992
|
+
4. 放公司 token;
|
|
993
|
+
5. 启动 issue-bot;
|
|
994
|
+
6. 本地 issue 打 `ai-fix`;
|
|
995
|
+
7. 预期:
|
|
996
|
+
|
|
997
|
+
```text
|
|
998
|
+
本地 issue:ai-processing
|
|
999
|
+
公司 GitLab:出现 ai/issue-xxx 分支
|
|
1000
|
+
公司 GitLab:出现正式 MR
|
|
1001
|
+
本地 issue:评论公司 MR 链接
|
|
1002
|
+
本地 issue:ai-done
|
|
1003
|
+
本地 GitLab:不出现新 MR
|
|
1004
|
+
```
|
|
1005
|
+
|
|
1006
|
+
## 23. 镜像验收
|
|
1007
|
+
|
|
1008
|
+
```bash
|
|
1009
|
+
docker compose --profile automation up -d company-mirror
|
|
1010
|
+
docker logs -f ai-coding-pilot-company-mirror
|
|
1011
|
+
```
|
|
1012
|
+
|
|
1013
|
+
应看到:
|
|
1014
|
+
|
|
1015
|
+
```text
|
|
1016
|
+
OK local-ai/ai-tasks-payment ← company-group/payment-service
|
|
1017
|
+
```
|
|
1018
|
+
|
|
1019
|
+
本地 project 的 Code / Branches 应出现公司分支。
|
|
1020
|
+
|
|
1021
|
+
测试分支新增和删除:
|
|
1022
|
+
|
|
1023
|
+
```text
|
|
1024
|
+
公司新增 feature 分支 → 本地出现
|
|
1025
|
+
公司删除 feature 分支 → 本地删除
|
|
1026
|
+
```
|
|
1027
|
+
|
|
1028
|
+
## 24. 失败场景
|
|
1029
|
+
|
|
1030
|
+
必须验证:
|
|
1031
|
+
|
|
1032
|
+
```text
|
|
1033
|
+
公司 project_id 写错 → 不 push、不开 MR、ai-failed
|
|
1034
|
+
公司 path 写错 → clone 失败、ai-failed
|
|
1035
|
+
target_branch 不存在 → clone/MR 失败、ai-failed
|
|
1036
|
+
token 无权限 → 401/403、ai-failed
|
|
1037
|
+
重复打 ai-fix → 已有公司 MR 时不重复开
|
|
1038
|
+
处理中重启 → 有公司 MR 恢复 ai-done
|
|
1039
|
+
门禁失败 → 不 push、ai-failed
|
|
1040
|
+
```
|
|
1041
|
+
|
|
1042
|
+
---
|
|
1043
|
+
|
|
1044
|
+
# 第五部分:安全与限制
|
|
1045
|
+
|
|
1046
|
+
## 25. 安全要求
|
|
1047
|
+
|
|
1048
|
+
```text
|
|
1049
|
+
公司 token 只放文件,600 权限
|
|
1050
|
+
本地镜像 token 只放文件,600 权限
|
|
1051
|
+
不要把 token 写进 git remote
|
|
1052
|
+
不要把 token 打进日志
|
|
1053
|
+
本地镜像 project 只读
|
|
1054
|
+
本地镜像 project 关闭 CI/CD
|
|
1055
|
+
不自动合并
|
|
1056
|
+
不 force push main
|
|
1057
|
+
不双向同步
|
|
1058
|
+
```
|
|
1059
|
+
|
|
1060
|
+
## 26. 当前限制
|
|
1061
|
+
|
|
1062
|
+
```text
|
|
1063
|
+
只支持同 project 的 source_branch → target_branch
|
|
1064
|
+
fork MR 需要额外适配 source_project_id
|
|
1065
|
+
公司 GitLab 8.16 API 的 MR 字段需以实测为准
|
|
1066
|
+
镜像默认只支持 HTTP(S) clone;SSH 需挂私钥
|
|
1067
|
+
镜像默认 300 秒同步一次,不是实时
|
|
1068
|
+
镜像会 force update,不能用于本地开发仓库
|
|
1069
|
+
issue-bot 配置启动时加载,新增映射后需重启
|
|
1070
|
+
公司 issue 不接入
|
|
1071
|
+
公司 webhook / runner / CI 配置不修改
|
|
1072
|
+
```
|
|
1073
|
+
|
|
1074
|
+
## 27. 完成标准
|
|
1075
|
+
|
|
1076
|
+
```text
|
|
1077
|
+
[ ] /api/v3 验证通过
|
|
1078
|
+
[ ] 公司模式 healthz 显示 company=true
|
|
1079
|
+
[ ] 本地 issue 能触发公司仓库修改
|
|
1080
|
+
[ ] 公司只出现一个正式 MR
|
|
1081
|
+
[ ] 本地不出现正式 MR
|
|
1082
|
+
[ ] 本地 issue 收到公司 MR 链接
|
|
1083
|
+
[ ] 公司分支能镜像回本地
|
|
1084
|
+
[ ] 重复触发不会重复开 MR
|
|
1085
|
+
[ ] 重启恢复能识别公司 MR
|
|
1086
|
+
[ ] 门禁失败不会 push
|
|
1087
|
+
[ ] 本地镜像 project 只读/关闭 CI
|
|
1088
|
+
```
|
package/package.json
CHANGED
|
Binary file
|