gitinstall 1.1.0__py3-none-any.whl
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.
- gitinstall/__init__.py +61 -0
- gitinstall/_sdk.py +541 -0
- gitinstall/academic.py +831 -0
- gitinstall/admin.html +327 -0
- gitinstall/auto_update.py +384 -0
- gitinstall/autopilot.py +349 -0
- gitinstall/badge.py +476 -0
- gitinstall/checkpoint.py +330 -0
- gitinstall/cicd.py +499 -0
- gitinstall/clawhub.html +718 -0
- gitinstall/config_schema.py +353 -0
- gitinstall/db.py +984 -0
- gitinstall/db_backend.py +445 -0
- gitinstall/dep_chain.py +337 -0
- gitinstall/dependency_audit.py +1153 -0
- gitinstall/detector.py +542 -0
- gitinstall/doctor.py +493 -0
- gitinstall/education.py +869 -0
- gitinstall/enterprise.py +802 -0
- gitinstall/error_fixer.py +953 -0
- gitinstall/event_bus.py +251 -0
- gitinstall/executor.py +577 -0
- gitinstall/feature_flags.py +138 -0
- gitinstall/fetcher.py +921 -0
- gitinstall/huggingface.py +922 -0
- gitinstall/hw_detect.py +988 -0
- gitinstall/i18n.py +664 -0
- gitinstall/installer_registry.py +362 -0
- gitinstall/knowledge_base.py +379 -0
- gitinstall/license_check.py +605 -0
- gitinstall/llm.py +569 -0
- gitinstall/log.py +236 -0
- gitinstall/main.py +1408 -0
- gitinstall/mcp_agent.py +841 -0
- gitinstall/mcp_server.py +386 -0
- gitinstall/monorepo.py +810 -0
- gitinstall/multi_source.py +425 -0
- gitinstall/onboard.py +276 -0
- gitinstall/planner.py +222 -0
- gitinstall/planner_helpers.py +323 -0
- gitinstall/planner_known_projects.py +1010 -0
- gitinstall/planner_templates.py +996 -0
- gitinstall/remote_gpu.py +633 -0
- gitinstall/resilience.py +608 -0
- gitinstall/run_tests.py +572 -0
- gitinstall/skills.py +476 -0
- gitinstall/tool_schemas.py +324 -0
- gitinstall/trending.py +279 -0
- gitinstall/uninstaller.py +415 -0
- gitinstall/validate_top100.py +607 -0
- gitinstall/watchdog.py +180 -0
- gitinstall/web.py +1277 -0
- gitinstall/web_ui.html +2277 -0
- gitinstall-1.1.0.dist-info/METADATA +275 -0
- gitinstall-1.1.0.dist-info/RECORD +59 -0
- gitinstall-1.1.0.dist-info/WHEEL +5 -0
- gitinstall-1.1.0.dist-info/entry_points.txt +3 -0
- gitinstall-1.1.0.dist-info/licenses/LICENSE +21 -0
- gitinstall-1.1.0.dist-info/top_level.txt +1 -0
gitinstall/cicd.py
ADDED
|
@@ -0,0 +1,499 @@
|
|
|
1
|
+
"""
|
|
2
|
+
cicd.py - CI/CD 集成模块
|
|
3
|
+
==========================
|
|
4
|
+
|
|
5
|
+
为 GitHub Actions、GitLab CI、Jenkins、Azure Pipelines 等
|
|
6
|
+
CI/CD 平台提供 gitinstall 集成能力。
|
|
7
|
+
|
|
8
|
+
功能:
|
|
9
|
+
1. 生成 CI/CD 配置文件(GitHub Actions YAML 等)
|
|
10
|
+
2. CI 环境检测和自适应
|
|
11
|
+
3. 批量安装 + 缓存策略
|
|
12
|
+
4. 安装结果报告(JUnit / JSON)
|
|
13
|
+
5. 安装锁文件(可复现安装)
|
|
14
|
+
|
|
15
|
+
零外部依赖,纯 Python 标准库。
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import hashlib
|
|
21
|
+
import json
|
|
22
|
+
import os
|
|
23
|
+
import time
|
|
24
|
+
from dataclasses import dataclass, field
|
|
25
|
+
from typing import Optional
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
# ─────────────────────────────────────────────
|
|
29
|
+
# CI 环境检测
|
|
30
|
+
# ─────────────────────────────────────────────
|
|
31
|
+
|
|
32
|
+
@dataclass
|
|
33
|
+
class CIEnvironment:
|
|
34
|
+
"""CI/CD 环境信息"""
|
|
35
|
+
is_ci: bool = False
|
|
36
|
+
platform: str = "" # github_actions, gitlab_ci, jenkins, azure, circle, travis
|
|
37
|
+
runner_os: str = "" # linux, macos, windows
|
|
38
|
+
runner_arch: str = "" # x64, arm64
|
|
39
|
+
branch: str = ""
|
|
40
|
+
commit_sha: str = ""
|
|
41
|
+
pr_number: str = ""
|
|
42
|
+
repo_url: str = ""
|
|
43
|
+
workspace: str = ""
|
|
44
|
+
job_name: str = ""
|
|
45
|
+
run_id: str = ""
|
|
46
|
+
cache_dir: str = ""
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def detect_ci_environment() -> CIEnvironment:
|
|
50
|
+
"""自动检测当前 CI/CD 环境"""
|
|
51
|
+
env = CIEnvironment()
|
|
52
|
+
|
|
53
|
+
# GitHub Actions
|
|
54
|
+
if os.getenv("GITHUB_ACTIONS") == "true":
|
|
55
|
+
env.is_ci = True
|
|
56
|
+
env.platform = "github_actions"
|
|
57
|
+
env.runner_os = os.getenv("RUNNER_OS", "").lower()
|
|
58
|
+
env.runner_arch = os.getenv("RUNNER_ARCH", "").lower()
|
|
59
|
+
env.branch = os.getenv("GITHUB_REF_NAME", "")
|
|
60
|
+
env.commit_sha = os.getenv("GITHUB_SHA", "")
|
|
61
|
+
env.pr_number = os.getenv("GITHUB_EVENT_NUMBER", "")
|
|
62
|
+
env.repo_url = f"https://github.com/{os.getenv('GITHUB_REPOSITORY', '')}"
|
|
63
|
+
env.workspace = os.getenv("GITHUB_WORKSPACE", "")
|
|
64
|
+
env.job_name = os.getenv("GITHUB_JOB", "")
|
|
65
|
+
env.run_id = os.getenv("GITHUB_RUN_ID", "")
|
|
66
|
+
env.cache_dir = os.path.expanduser("~/.cache/gitinstall")
|
|
67
|
+
return env
|
|
68
|
+
|
|
69
|
+
# GitLab CI
|
|
70
|
+
if os.getenv("GITLAB_CI") == "true":
|
|
71
|
+
env.is_ci = True
|
|
72
|
+
env.platform = "gitlab_ci"
|
|
73
|
+
env.branch = os.getenv("CI_COMMIT_REF_NAME", "")
|
|
74
|
+
env.commit_sha = os.getenv("CI_COMMIT_SHA", "")
|
|
75
|
+
env.pr_number = os.getenv("CI_MERGE_REQUEST_IID", "")
|
|
76
|
+
env.repo_url = os.getenv("CI_PROJECT_URL", "")
|
|
77
|
+
env.workspace = os.getenv("CI_PROJECT_DIR", "")
|
|
78
|
+
env.job_name = os.getenv("CI_JOB_NAME", "")
|
|
79
|
+
env.run_id = os.getenv("CI_PIPELINE_ID", "")
|
|
80
|
+
env.cache_dir = os.path.expanduser("~/.cache/gitinstall")
|
|
81
|
+
return env
|
|
82
|
+
|
|
83
|
+
# Jenkins
|
|
84
|
+
if os.getenv("JENKINS_URL"):
|
|
85
|
+
env.is_ci = True
|
|
86
|
+
env.platform = "jenkins"
|
|
87
|
+
env.branch = os.getenv("GIT_BRANCH", "")
|
|
88
|
+
env.commit_sha = os.getenv("GIT_COMMIT", "")
|
|
89
|
+
env.workspace = os.getenv("WORKSPACE", "")
|
|
90
|
+
env.job_name = os.getenv("JOB_NAME", "")
|
|
91
|
+
env.run_id = os.getenv("BUILD_NUMBER", "")
|
|
92
|
+
env.cache_dir = os.path.expanduser("~/.cache/gitinstall")
|
|
93
|
+
return env
|
|
94
|
+
|
|
95
|
+
# Azure Pipelines
|
|
96
|
+
if os.getenv("TF_BUILD") == "True":
|
|
97
|
+
env.is_ci = True
|
|
98
|
+
env.platform = "azure"
|
|
99
|
+
env.runner_os = os.getenv("Agent.OS", "").lower()
|
|
100
|
+
env.branch = os.getenv("Build.SourceBranchName", "")
|
|
101
|
+
env.commit_sha = os.getenv("Build.SourceVersion", "")
|
|
102
|
+
env.pr_number = os.getenv("System.PullRequest.PullRequestId", "")
|
|
103
|
+
env.workspace = os.getenv("Build.SourcesDirectory", "")
|
|
104
|
+
env.run_id = os.getenv("Build.BuildId", "")
|
|
105
|
+
env.cache_dir = os.path.expanduser("~/.cache/gitinstall")
|
|
106
|
+
return env
|
|
107
|
+
|
|
108
|
+
# CircleCI
|
|
109
|
+
if os.getenv("CIRCLECI") == "true":
|
|
110
|
+
env.is_ci = True
|
|
111
|
+
env.platform = "circle"
|
|
112
|
+
env.branch = os.getenv("CIRCLE_BRANCH", "")
|
|
113
|
+
env.commit_sha = os.getenv("CIRCLE_SHA1", "")
|
|
114
|
+
env.pr_number = os.getenv("CIRCLE_PR_NUMBER", "")
|
|
115
|
+
env.repo_url = os.getenv("CIRCLE_REPOSITORY_URL", "")
|
|
116
|
+
env.workspace = os.getenv("CIRCLE_WORKING_DIRECTORY", "")
|
|
117
|
+
env.job_name = os.getenv("CIRCLE_JOB", "")
|
|
118
|
+
env.run_id = os.getenv("CIRCLE_BUILD_NUM", "")
|
|
119
|
+
env.cache_dir = os.path.expanduser("~/.cache/gitinstall")
|
|
120
|
+
return env
|
|
121
|
+
|
|
122
|
+
# 通用 CI 检测
|
|
123
|
+
if os.getenv("CI") == "true" or os.getenv("CI") == "1":
|
|
124
|
+
env.is_ci = True
|
|
125
|
+
env.platform = "unknown"
|
|
126
|
+
env.cache_dir = os.path.expanduser("~/.cache/gitinstall")
|
|
127
|
+
return env
|
|
128
|
+
|
|
129
|
+
return env
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
# ─────────────────────────────────────────────
|
|
133
|
+
# GitHub Actions 配置生成
|
|
134
|
+
# ─────────────────────────────────────────────
|
|
135
|
+
|
|
136
|
+
def generate_github_action(
|
|
137
|
+
repos: list[str],
|
|
138
|
+
python_version: str = "3.12",
|
|
139
|
+
os_list: Optional[list[str]] = None,
|
|
140
|
+
cache_enabled: bool = True,
|
|
141
|
+
sbom_export: bool = False,
|
|
142
|
+
audit_enabled: bool = True,
|
|
143
|
+
) -> str:
|
|
144
|
+
"""
|
|
145
|
+
生成 GitHub Actions workflow YAML。
|
|
146
|
+
|
|
147
|
+
Args:
|
|
148
|
+
repos: 要安装的仓库列表
|
|
149
|
+
python_version: Python 版本
|
|
150
|
+
os_list: 运行平台列表
|
|
151
|
+
cache_enabled: 是否启用缓存
|
|
152
|
+
sbom_export: 是否导出 SBOM
|
|
153
|
+
audit_enabled: 是否运行安全审计
|
|
154
|
+
|
|
155
|
+
Returns:
|
|
156
|
+
YAML 字符串
|
|
157
|
+
"""
|
|
158
|
+
if os_list is None:
|
|
159
|
+
os_list = ["ubuntu-latest"]
|
|
160
|
+
|
|
161
|
+
# 使用字符串拼接而非 YAML 库(零依赖)
|
|
162
|
+
lines = [
|
|
163
|
+
"name: gitinstall CI",
|
|
164
|
+
"",
|
|
165
|
+
"on:",
|
|
166
|
+
" push:",
|
|
167
|
+
" branches: [main, master]",
|
|
168
|
+
" pull_request:",
|
|
169
|
+
" branches: [main, master]",
|
|
170
|
+
" workflow_dispatch:",
|
|
171
|
+
"",
|
|
172
|
+
"jobs:",
|
|
173
|
+
" install-test:",
|
|
174
|
+
f" runs-on: ${{{{ matrix.os }}}}",
|
|
175
|
+
" strategy:",
|
|
176
|
+
" matrix:",
|
|
177
|
+
f" os: [{', '.join(os_list)}]",
|
|
178
|
+
" fail-fast: false",
|
|
179
|
+
"",
|
|
180
|
+
" steps:",
|
|
181
|
+
" - uses: actions/checkout@v4",
|
|
182
|
+
"",
|
|
183
|
+
f" - name: Set up Python {python_version}",
|
|
184
|
+
" uses: actions/setup-python@v5",
|
|
185
|
+
" with:",
|
|
186
|
+
f" python-version: '{python_version}'",
|
|
187
|
+
"",
|
|
188
|
+
" - name: Install gitinstall",
|
|
189
|
+
" run: pip install gitinstall",
|
|
190
|
+
"",
|
|
191
|
+
]
|
|
192
|
+
|
|
193
|
+
if cache_enabled:
|
|
194
|
+
lines.extend([
|
|
195
|
+
" - name: Cache gitinstall data",
|
|
196
|
+
" uses: actions/cache@v4",
|
|
197
|
+
" with:",
|
|
198
|
+
" path: ~/.cache/gitinstall",
|
|
199
|
+
" key: gitinstall-${{ runner.os }}-${{ hashFiles('**/requirements*.txt') }}",
|
|
200
|
+
" restore-keys: |",
|
|
201
|
+
" gitinstall-${{ runner.os }}-",
|
|
202
|
+
"",
|
|
203
|
+
])
|
|
204
|
+
|
|
205
|
+
if audit_enabled:
|
|
206
|
+
lines.extend([
|
|
207
|
+
" - name: Security audit",
|
|
208
|
+
" run: gitinstall audit --format json --output audit-report.json",
|
|
209
|
+
"",
|
|
210
|
+
])
|
|
211
|
+
|
|
212
|
+
# 安装各仓库
|
|
213
|
+
for repo in repos:
|
|
214
|
+
safe_name = repo.replace("/", "-").replace(".", "-")
|
|
215
|
+
lines.extend([
|
|
216
|
+
f" - name: Install {repo}",
|
|
217
|
+
f" run: gitinstall install {repo} --ci --json-report install-{safe_name}.json",
|
|
218
|
+
"",
|
|
219
|
+
])
|
|
220
|
+
|
|
221
|
+
if sbom_export:
|
|
222
|
+
lines.extend([
|
|
223
|
+
" - name: Generate SBOM",
|
|
224
|
+
" run: gitinstall sbom --format cyclonedx --output sbom.cdx.json",
|
|
225
|
+
"",
|
|
226
|
+
" - name: Upload SBOM",
|
|
227
|
+
" uses: actions/upload-artifact@v4",
|
|
228
|
+
" with:",
|
|
229
|
+
" name: sbom-${{ matrix.os }}",
|
|
230
|
+
" path: sbom.cdx.json",
|
|
231
|
+
"",
|
|
232
|
+
])
|
|
233
|
+
|
|
234
|
+
# 上传报告
|
|
235
|
+
lines.extend([
|
|
236
|
+
" - name: Upload install reports",
|
|
237
|
+
" if: always()",
|
|
238
|
+
" uses: actions/upload-artifact@v4",
|
|
239
|
+
" with:",
|
|
240
|
+
" name: install-reports-${{ matrix.os }}",
|
|
241
|
+
" path: |",
|
|
242
|
+
" install-*.json",
|
|
243
|
+
" audit-report.json",
|
|
244
|
+
"",
|
|
245
|
+
])
|
|
246
|
+
|
|
247
|
+
return "\n".join(lines)
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def generate_gitlab_ci(
|
|
251
|
+
repos: list[str],
|
|
252
|
+
python_version: str = "3.12",
|
|
253
|
+
audit_enabled: bool = True,
|
|
254
|
+
) -> str:
|
|
255
|
+
"""生成 GitLab CI 配置"""
|
|
256
|
+
lines = [
|
|
257
|
+
f"image: python:{python_version}-slim",
|
|
258
|
+
"",
|
|
259
|
+
"variables:",
|
|
260
|
+
" PIP_CACHE_DIR: $CI_PROJECT_DIR/.pip-cache",
|
|
261
|
+
"",
|
|
262
|
+
"cache:",
|
|
263
|
+
" paths:",
|
|
264
|
+
" - .pip-cache/",
|
|
265
|
+
" - .cache/gitinstall/",
|
|
266
|
+
"",
|
|
267
|
+
"stages:",
|
|
268
|
+
" - audit",
|
|
269
|
+
" - install",
|
|
270
|
+
"",
|
|
271
|
+
]
|
|
272
|
+
|
|
273
|
+
if audit_enabled:
|
|
274
|
+
lines.extend([
|
|
275
|
+
"security-audit:",
|
|
276
|
+
" stage: audit",
|
|
277
|
+
" script:",
|
|
278
|
+
" - pip install gitinstall",
|
|
279
|
+
" - gitinstall audit --format json --output audit-report.json",
|
|
280
|
+
" artifacts:",
|
|
281
|
+
" reports:",
|
|
282
|
+
" dependency_scanning: audit-report.json",
|
|
283
|
+
" expire_in: 1 week",
|
|
284
|
+
"",
|
|
285
|
+
])
|
|
286
|
+
|
|
287
|
+
for repo in repos:
|
|
288
|
+
safe_name = repo.replace("/", "-").replace(".", "-")
|
|
289
|
+
lines.extend([
|
|
290
|
+
f"install-{safe_name}:",
|
|
291
|
+
" stage: install",
|
|
292
|
+
" script:",
|
|
293
|
+
" - pip install gitinstall",
|
|
294
|
+
f" - gitinstall install {repo} --ci",
|
|
295
|
+
" allow_failure: true",
|
|
296
|
+
"",
|
|
297
|
+
])
|
|
298
|
+
|
|
299
|
+
return "\n".join(lines)
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
# ─────────────────────────────────────────────
|
|
303
|
+
# 安装锁文件(可复现安装)
|
|
304
|
+
# ─────────────────────────────────────────────
|
|
305
|
+
|
|
306
|
+
@dataclass
|
|
307
|
+
class InstallLockEntry:
|
|
308
|
+
"""锁文件中的单条记录"""
|
|
309
|
+
repo_url: str
|
|
310
|
+
commit_sha: str = "" # 锁定的 commit
|
|
311
|
+
tag: str = "" # 锁定的 tag
|
|
312
|
+
install_commands: list[str] = field(default_factory=list)
|
|
313
|
+
checksum: str = "" # 仓库文件的 hash
|
|
314
|
+
installed_at: float = 0.0
|
|
315
|
+
environment: dict = field(default_factory=dict) # os, python, arch
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
def generate_install_lock(
|
|
319
|
+
installs: list[dict],
|
|
320
|
+
output_path: str = "gitinstall.lock.json",
|
|
321
|
+
) -> str:
|
|
322
|
+
"""
|
|
323
|
+
生成安装锁文件,确保可复现安装。
|
|
324
|
+
|
|
325
|
+
Args:
|
|
326
|
+
installs: 安装记录列表 [{"repo": "...", "commit": "...", "commands": [...]}]
|
|
327
|
+
output_path: 输出路径
|
|
328
|
+
|
|
329
|
+
Returns:
|
|
330
|
+
输出文件路径
|
|
331
|
+
"""
|
|
332
|
+
import platform
|
|
333
|
+
|
|
334
|
+
lock = {
|
|
335
|
+
"version": 1,
|
|
336
|
+
"generated_at": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
|
|
337
|
+
"generator": "gitinstall/1.1.0",
|
|
338
|
+
"environment": {
|
|
339
|
+
"os": platform.system().lower(),
|
|
340
|
+
"arch": platform.machine(),
|
|
341
|
+
"python": platform.python_version(),
|
|
342
|
+
},
|
|
343
|
+
"installs": [],
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
for inst in installs:
|
|
347
|
+
entry = {
|
|
348
|
+
"repo": inst.get("repo", ""),
|
|
349
|
+
"commit_sha": inst.get("commit", ""),
|
|
350
|
+
"tag": inst.get("tag", ""),
|
|
351
|
+
"commands": inst.get("commands", []),
|
|
352
|
+
"installed_at": inst.get("installed_at", time.time()),
|
|
353
|
+
}
|
|
354
|
+
# 生成 checksum
|
|
355
|
+
content = json.dumps(entry, sort_keys=True)
|
|
356
|
+
entry["checksum"] = hashlib.sha256(content.encode()).hexdigest()[:16]
|
|
357
|
+
lock["installs"].append(entry)
|
|
358
|
+
|
|
359
|
+
with open(output_path, "w", encoding="utf-8") as f:
|
|
360
|
+
json.dump(lock, f, indent=2, ensure_ascii=False)
|
|
361
|
+
|
|
362
|
+
return output_path
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
def load_install_lock(lock_path: str = "gitinstall.lock.json") -> list[dict]:
|
|
366
|
+
"""加载安装锁文件"""
|
|
367
|
+
if not os.path.isfile(lock_path):
|
|
368
|
+
return []
|
|
369
|
+
with open(lock_path, encoding="utf-8") as f:
|
|
370
|
+
lock = json.load(f)
|
|
371
|
+
return lock.get("installs", [])
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
# ─────────────────────────────────────────────
|
|
375
|
+
# JUnit XML 报告生成(CI 友好)
|
|
376
|
+
# ─────────────────────────────────────────────
|
|
377
|
+
|
|
378
|
+
def generate_junit_report(
|
|
379
|
+
results: list[dict],
|
|
380
|
+
output_path: str = "gitinstall-results.xml",
|
|
381
|
+
) -> str:
|
|
382
|
+
"""
|
|
383
|
+
将安装结果转换为 JUnit XML 格式。
|
|
384
|
+
|
|
385
|
+
CI 平台(GitHub Actions, GitLab, Jenkins)都支持 JUnit 报告。
|
|
386
|
+
|
|
387
|
+
Args:
|
|
388
|
+
results: [{"repo": "...", "success": bool, "duration": float, "error": "..."}]
|
|
389
|
+
output_path: 输出路径
|
|
390
|
+
"""
|
|
391
|
+
total = len(results)
|
|
392
|
+
failures = sum(1 for r in results if not r.get("success"))
|
|
393
|
+
total_time = sum(r.get("duration", 0) for r in results)
|
|
394
|
+
|
|
395
|
+
lines = [
|
|
396
|
+
'<?xml version="1.0" encoding="UTF-8"?>',
|
|
397
|
+
f'<testsuites tests="{total}" failures="{failures}" time="{total_time:.2f}">',
|
|
398
|
+
f' <testsuite name="gitinstall" tests="{total}" failures="{failures}" time="{total_time:.2f}">',
|
|
399
|
+
]
|
|
400
|
+
|
|
401
|
+
for r in results:
|
|
402
|
+
repo = _xml_escape(r.get("repo", "unknown"))
|
|
403
|
+
duration = r.get("duration", 0)
|
|
404
|
+
lines.append(f' <testcase name="{repo}" time="{duration:.2f}">')
|
|
405
|
+
if not r.get("success"):
|
|
406
|
+
error = _xml_escape(r.get("error", "Unknown error"))
|
|
407
|
+
lines.append(f' <failure message="Installation failed">{error}</failure>')
|
|
408
|
+
lines.append(" </testcase>")
|
|
409
|
+
|
|
410
|
+
lines.extend([
|
|
411
|
+
" </testsuite>",
|
|
412
|
+
"</testsuites>",
|
|
413
|
+
])
|
|
414
|
+
|
|
415
|
+
xml = "\n".join(lines)
|
|
416
|
+
with open(output_path, "w", encoding="utf-8") as f:
|
|
417
|
+
f.write(xml)
|
|
418
|
+
|
|
419
|
+
return output_path
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
def _xml_escape(s: str) -> str:
|
|
423
|
+
"""XML 字符转义"""
|
|
424
|
+
return (s.replace("&", "&").replace("<", "<")
|
|
425
|
+
.replace(">", ">").replace('"', """)
|
|
426
|
+
.replace("'", "'"))
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
# ─────────────────────────────────────────────
|
|
430
|
+
# 批量安装(CI 模式)
|
|
431
|
+
# ─────────────────────────────────────────────
|
|
432
|
+
|
|
433
|
+
@dataclass
|
|
434
|
+
class BatchInstallResult:
|
|
435
|
+
"""批量安装结果"""
|
|
436
|
+
total: int = 0
|
|
437
|
+
success: int = 0
|
|
438
|
+
failed: int = 0
|
|
439
|
+
skipped: int = 0
|
|
440
|
+
results: list[dict] = field(default_factory=list)
|
|
441
|
+
duration: float = 0.0
|
|
442
|
+
|
|
443
|
+
|
|
444
|
+
def plan_batch_install(
|
|
445
|
+
repos: list[str],
|
|
446
|
+
parallelism: int = 1,
|
|
447
|
+
fail_fast: bool = False,
|
|
448
|
+
skip_audit: bool = False,
|
|
449
|
+
) -> dict:
|
|
450
|
+
"""
|
|
451
|
+
规划批量安装策略。
|
|
452
|
+
|
|
453
|
+
Args:
|
|
454
|
+
repos: 仓库列表
|
|
455
|
+
parallelism: 并行度
|
|
456
|
+
fail_fast: 失败时立即停止
|
|
457
|
+
skip_audit: 跳过安全审计
|
|
458
|
+
|
|
459
|
+
Returns:
|
|
460
|
+
安装计划 dict
|
|
461
|
+
"""
|
|
462
|
+
ci = detect_ci_environment()
|
|
463
|
+
|
|
464
|
+
plan = {
|
|
465
|
+
"ci_detected": ci.is_ci,
|
|
466
|
+
"ci_platform": ci.platform,
|
|
467
|
+
"total_repos": len(repos),
|
|
468
|
+
"parallelism": min(parallelism, len(repos)),
|
|
469
|
+
"fail_fast": fail_fast,
|
|
470
|
+
"skip_audit": skip_audit,
|
|
471
|
+
"phases": [],
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
# Phase 1: 安全审计(如果启用)
|
|
475
|
+
if not skip_audit:
|
|
476
|
+
plan["phases"].append({
|
|
477
|
+
"name": "security_audit",
|
|
478
|
+
"description": "安全审计扫描",
|
|
479
|
+
"repos": repos,
|
|
480
|
+
})
|
|
481
|
+
|
|
482
|
+
# Phase 2: 分批安装
|
|
483
|
+
batch_size = max(1, parallelism)
|
|
484
|
+
for i in range(0, len(repos), batch_size):
|
|
485
|
+
batch = repos[i:i + batch_size]
|
|
486
|
+
plan["phases"].append({
|
|
487
|
+
"name": f"install_batch_{i // batch_size + 1}",
|
|
488
|
+
"description": f"安装批次 {i // batch_size + 1}",
|
|
489
|
+
"repos": batch,
|
|
490
|
+
})
|
|
491
|
+
|
|
492
|
+
# Phase 3: 报告生成
|
|
493
|
+
plan["phases"].append({
|
|
494
|
+
"name": "report",
|
|
495
|
+
"description": "生成安装报告",
|
|
496
|
+
"outputs": ["junit_xml", "json_report"],
|
|
497
|
+
})
|
|
498
|
+
|
|
499
|
+
return plan
|