1panel-toolkit 0.3.0__tar.gz → 0.3.2__tar.gz
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.
- {1panel_toolkit-0.3.0 → 1panel_toolkit-0.3.2}/CHANGELOG.md +69 -0
- {1panel_toolkit-0.3.0 → 1panel_toolkit-0.3.2}/PKG-INFO +3 -3
- {1panel_toolkit-0.3.0 → 1panel_toolkit-0.3.2}/README.md +2 -2
- {1panel_toolkit-0.3.0 → 1panel_toolkit-0.3.2}/docs/generation-depth.md +4 -1
- {1panel_toolkit-0.3.0 → 1panel_toolkit-0.3.2}/pyproject.toml +4 -1
- {1panel_toolkit-0.3.0 → 1panel_toolkit-0.3.2}/src/p1toolkit/__init__.py +1 -1
- {1panel_toolkit-0.3.0 → 1panel_toolkit-0.3.2}/src/p1toolkit/cli.py +8 -5
- {1panel_toolkit-0.3.0 → 1panel_toolkit-0.3.2}/src/p1toolkit/depth.py +28 -11
- {1panel_toolkit-0.3.0 → 1panel_toolkit-0.3.2}/src/p1toolkit/evidence.py +26 -0
- {1panel_toolkit-0.3.0 → 1panel_toolkit-0.3.2}/src/p1toolkit/resolve/__init__.py +45 -31
- {1panel_toolkit-0.3.0 → 1panel_toolkit-0.3.2}/src/p1toolkit/resolve/analyze.py +29 -0
- {1panel_toolkit-0.3.0 → 1panel_toolkit-0.3.2}/src/p1toolkit/rules/data/lessons.yaml +2 -1
- {1panel_toolkit-0.3.0 → 1panel_toolkit-0.3.2}/tests/test_config.py +4 -4
- {1panel_toolkit-0.3.0 → 1panel_toolkit-0.3.2}/tests/test_depth.py +50 -1
- {1panel_toolkit-0.3.0 → 1panel_toolkit-0.3.2}/tests/test_evidence.py +19 -0
- {1panel_toolkit-0.3.0 → 1panel_toolkit-0.3.2}/tests/test_resolve.py +70 -0
- 1panel_toolkit-0.3.0/_debug_resolve.err +0 -0
- 1panel_toolkit-0.3.0/_debug_resolve.log +0 -21
- 1panel_toolkit-0.3.0/_debug_resolve.py +0 -18
- {1panel_toolkit-0.3.0 → 1panel_toolkit-0.3.2}/.gitignore +0 -0
- {1panel_toolkit-0.3.0 → 1panel_toolkit-0.3.2}/LICENSE +0 -0
- {1panel_toolkit-0.3.0 → 1panel_toolkit-0.3.2}/docs/patterns.md +0 -0
- {1panel_toolkit-0.3.0 → 1panel_toolkit-0.3.2}/examples/demo-db-spec.json +0 -0
- {1panel_toolkit-0.3.0 → 1panel_toolkit-0.3.2}/examples/demo-spec.json +0 -0
- {1panel_toolkit-0.3.0 → 1panel_toolkit-0.3.2}/skill/SKILL.md +0 -0
- {1panel_toolkit-0.3.0 → 1panel_toolkit-0.3.2}/src/p1toolkit/checks/__init__.py +0 -0
- {1panel_toolkit-0.3.0 → 1panel_toolkit-0.3.2}/src/p1toolkit/checks/compose.py +0 -0
- {1panel_toolkit-0.3.0 → 1panel_toolkit-0.3.2}/src/p1toolkit/checks/forms.py +0 -0
- {1panel_toolkit-0.3.0 → 1panel_toolkit-0.3.2}/src/p1toolkit/checks/hygiene.py +0 -0
- {1panel_toolkit-0.3.0 → 1panel_toolkit-0.3.2}/src/p1toolkit/checks/i18n.py +0 -0
- {1panel_toolkit-0.3.0 → 1panel_toolkit-0.3.2}/src/p1toolkit/checks/lessons.py +0 -0
- {1panel_toolkit-0.3.0 → 1panel_toolkit-0.3.2}/src/p1toolkit/checks/structure.py +0 -0
- {1panel_toolkit-0.3.0 → 1panel_toolkit-0.3.2}/src/p1toolkit/checks/substance.py +0 -0
- {1panel_toolkit-0.3.0 → 1panel_toolkit-0.3.2}/src/p1toolkit/config.py +0 -0
- {1panel_toolkit-0.3.0 → 1panel_toolkit-0.3.2}/src/p1toolkit/dependencies.py +0 -0
- {1panel_toolkit-0.3.0 → 1panel_toolkit-0.3.2}/src/p1toolkit/deploy.py +0 -0
- {1panel_toolkit-0.3.0 → 1panel_toolkit-0.3.2}/src/p1toolkit/errors.py +0 -0
- {1panel_toolkit-0.3.0 → 1panel_toolkit-0.3.2}/src/p1toolkit/fix.py +0 -0
- {1panel_toolkit-0.3.0 → 1panel_toolkit-0.3.2}/src/p1toolkit/gen.py +0 -0
- {1panel_toolkit-0.3.0 → 1panel_toolkit-0.3.2}/src/p1toolkit/lifecycle.py +0 -0
- {1panel_toolkit-0.3.0 → 1panel_toolkit-0.3.2}/src/p1toolkit/loader.py +0 -0
- {1panel_toolkit-0.3.0 → 1panel_toolkit-0.3.2}/src/p1toolkit/logo.py +0 -0
- {1panel_toolkit-0.3.0 → 1panel_toolkit-0.3.2}/src/p1toolkit/model.py +0 -0
- {1panel_toolkit-0.3.0 → 1panel_toolkit-0.3.2}/src/p1toolkit/net.py +0 -0
- {1panel_toolkit-0.3.0 → 1panel_toolkit-0.3.2}/src/p1toolkit/panel.py +0 -0
- {1panel_toolkit-0.3.0 → 1panel_toolkit-0.3.2}/src/p1toolkit/patterns/__init__.py +0 -0
- {1panel_toolkit-0.3.0 → 1panel_toolkit-0.3.2}/src/p1toolkit/patterns/catalog.py +0 -0
- {1panel_toolkit-0.3.0 → 1panel_toolkit-0.3.2}/src/p1toolkit/patterns/data/patterns.json +0 -0
- {1panel_toolkit-0.3.0 → 1panel_toolkit-0.3.2}/src/p1toolkit/patterns/extract.py +0 -0
- {1panel_toolkit-0.3.0 → 1panel_toolkit-0.3.2}/src/p1toolkit/remote.py +0 -0
- {1panel_toolkit-0.3.0 → 1panel_toolkit-0.3.2}/src/p1toolkit/report.py +0 -0
- {1panel_toolkit-0.3.0 → 1panel_toolkit-0.3.2}/src/p1toolkit/resolve/sources.py +0 -0
- {1panel_toolkit-0.3.0 → 1panel_toolkit-0.3.2}/src/p1toolkit/rules/__init__.py +0 -0
- {1panel_toolkit-0.3.0 → 1panel_toolkit-0.3.2}/src/p1toolkit/rules/data/decisions.yaml +0 -0
- {1panel_toolkit-0.3.0 → 1panel_toolkit-0.3.2}/src/p1toolkit/rules/data/dependencies.yaml +0 -0
- {1panel_toolkit-0.3.0 → 1panel_toolkit-0.3.2}/src/p1toolkit/rules/data/envkeys.yaml +0 -0
- {1panel_toolkit-0.3.0 → 1panel_toolkit-0.3.2}/src/p1toolkit/rules/data/form-design.yaml +0 -0
- {1panel_toolkit-0.3.0 → 1panel_toolkit-0.3.2}/src/p1toolkit/rules/data/i18n.yaml +0 -0
- {1panel_toolkit-0.3.0 → 1panel_toolkit-0.3.2}/src/p1toolkit/rules/data/store.yaml +0 -0
- {1panel_toolkit-0.3.0 → 1panel_toolkit-0.3.2}/src/p1toolkit/selftest.py +0 -0
- {1panel_toolkit-0.3.0 → 1panel_toolkit-0.3.2}/tests/conftest.py +0 -0
- {1panel_toolkit-0.3.0 → 1panel_toolkit-0.3.2}/tests/test_checks.py +0 -0
- {1panel_toolkit-0.3.0 → 1panel_toolkit-0.3.2}/tests/test_corpus.py +0 -0
- {1panel_toolkit-0.3.0 → 1panel_toolkit-0.3.2}/tests/test_dependencies.py +0 -0
- {1panel_toolkit-0.3.0 → 1panel_toolkit-0.3.2}/tests/test_deploy.py +0 -0
- {1panel_toolkit-0.3.0 → 1panel_toolkit-0.3.2}/tests/test_fix.py +0 -0
- {1panel_toolkit-0.3.0 → 1panel_toolkit-0.3.2}/tests/test_forms.py +0 -0
- {1panel_toolkit-0.3.0 → 1panel_toolkit-0.3.2}/tests/test_gen.py +0 -0
- {1panel_toolkit-0.3.0 → 1panel_toolkit-0.3.2}/tests/test_github_source.py +0 -0
- {1panel_toolkit-0.3.0 → 1panel_toolkit-0.3.2}/tests/test_lifecycle.py +0 -0
- {1panel_toolkit-0.3.0 → 1panel_toolkit-0.3.2}/tests/test_net.py +0 -0
- {1panel_toolkit-0.3.0 → 1panel_toolkit-0.3.2}/tests/test_panel.py +0 -0
- {1panel_toolkit-0.3.0 → 1panel_toolkit-0.3.2}/tests/test_patterns.py +0 -0
- {1panel_toolkit-0.3.0 → 1panel_toolkit-0.3.2}/tests/test_regressions.py +0 -0
- {1panel_toolkit-0.3.0 → 1panel_toolkit-0.3.2}/tests/test_remote.py +0 -0
- {1panel_toolkit-0.3.0 → 1panel_toolkit-0.3.2}/tests/test_remote_cli.py +0 -0
- {1panel_toolkit-0.3.0 → 1panel_toolkit-0.3.2}/tests/test_rules_data.py +0 -0
- {1panel_toolkit-0.3.0 → 1panel_toolkit-0.3.2}/tests/test_selftest.py +0 -0
|
@@ -1,5 +1,74 @@
|
|
|
1
1
|
# 变更记录
|
|
2
2
|
|
|
3
|
+
## 0.3.2 — 2026-09-20
|
|
4
|
+
|
|
5
|
+
`p1 depth` 的判定阈值校准。用 `p1 fix` 收敛用户自己的四个包时发现的误判。
|
|
6
|
+
|
|
7
|
+
### 判定:只缺一个维度不再是 thin
|
|
8
|
+
|
|
9
|
+
原来的 `assess()` 是"三个多样性维度(对外端口 / 持久化挂载 / 应用自有变量)里
|
|
10
|
+
缺任意一个 → thin"。于是 `xiaozhi-esp32-server` 被判成 `thin`——可它有 2 个对外端口、
|
|
11
|
+
2 个挂载点(其中一个还是模型文件 `model.pt`)、`security_opt`、`init.sh`,只是把配置
|
|
12
|
+
全放在挂载的 `data/.config.yaml` 里,本来就不该有应用变量。
|
|
13
|
+
|
|
14
|
+
现在:**缺两个及以上才算薄**;只缺一个仍是 `adapted`,但会打一条 `·` 说明;
|
|
15
|
+
与不相干官方包重合 ≥ 50% 仍是 `possibly-reskinned`。顺带 `p1 depth <包>`
|
|
16
|
+
不再需要 `--corpus` 也会给判定(只有"最接近的官方包"那节需要语料)。
|
|
17
|
+
|
|
18
|
+
四个真实包的判定:hindsight `adapted`、openviking `adapted`、
|
|
19
|
+
xiaozhi-esp32-server `adapted`(附一条"没有任何应用自有环境变量"的说明)、
|
|
20
|
+
xiaozhi-esp32-server-full `adapted`。
|
|
21
|
+
|
|
22
|
+
### 验证
|
|
23
|
+
|
|
24
|
+
- 新增 2 条测试(缺一个维度 → adapted + note;缺两个 → thin),`pytest` 257 passed。
|
|
25
|
+
- 收敛后的四个包 `p1 check`:hindsight `0 fail, 2 warn`、openviking `0 fail, 1 warn`、
|
|
26
|
+
xiaozhi-esp32-server `0 fail, 1 warn`、xiaozhi-esp32-server-full `OK no issues`,
|
|
27
|
+
与 README 表格一致;`p1 fix` 二次运行四个包都是"没有需要修复的内容"(幂等)。
|
|
28
|
+
|
|
29
|
+
## 0.3.1 — 2026-09-20
|
|
30
|
+
|
|
31
|
+
独立验证(另一个 session 的只读复现)挖出的三个真 bug,外加发布产物卫生。
|
|
32
|
+
其中第 1 条从 0.1.0 起就在**每一个已发布版本**里,PyPI 上的 0.3.0 同样中招。
|
|
33
|
+
|
|
34
|
+
### 修掉的 bug
|
|
35
|
+
|
|
36
|
+
- **`p1 resolve` 会被容器内挂载路径打崩**:`draft_spec` 里
|
|
37
|
+
`host, target = entry.split(":", 1)` 覆盖了同名函数参数,于是
|
|
38
|
+
`repo_key(target, ...)` 拿到的是 `/app/data` 这种容器路径而不是仓库路径。本地检出
|
|
39
|
+
的主服务 compose 只要带 `volumes`,`p1 resolve` 就直接
|
|
40
|
+
`error: cannot interpret '/otel-lgtm/...'`、exit 2、一个文件都不写;运气"好"时
|
|
41
|
+
(容器路径恰好能被当成路径解析)则静默算出错误的 `key`。0.2.0 修过
|
|
42
|
+
`for target in mounts` 那处同名覆盖,漏了 `primary_volumes` 这处。
|
|
43
|
+
- **多服务 compose 的挂载点被并进主服务**:主服务 `/app/data`(host `~/.app`)与
|
|
44
|
+
sidecar `./Caddyfile:/etc/caddy/Caddyfile` 会被配成 `~/.app:/etc/caddy/Caddyfile`,
|
|
45
|
+
等于把应用的宿主目录 bind 到另一个服务的容器路径上。这种包证据审计 0 fail、
|
|
46
|
+
`p1 check` 0 fail——因为那个容器路径确实在 D03 证据里,正是 D03 想拦的"挂错路径"。
|
|
47
|
+
新增 `analyze.service_mounts()` 按服务归属解析;draft 只描述主服务,其它服务的
|
|
48
|
+
挂载点写进 `notes`(并出现在 `decision-report.md`)交给人判断。
|
|
49
|
+
- **`mounts` 里缺 `target` 的条目被静默丢弃**:`mount_host` 写错会被阻塞,同样意图
|
|
50
|
+
写进 `mounts[]` 写错却 rc=0、无 volume、无提示。现在按 D03 阻塞;半坏不坏的
|
|
51
|
+
`mounts` 列表也不会被"修好一半"。
|
|
52
|
+
- 本地检出的产品名不再是一整条路径:`local:D:\...\OpenViking-main` 以前会原样进
|
|
53
|
+
`name`,现在取目录名。
|
|
54
|
+
|
|
55
|
+
### 产物卫生
|
|
56
|
+
|
|
57
|
+
- 发布物不再带真实服务器 IP(`cli.py` help 示例、`lessons.yaml` 的 L005 案例、
|
|
58
|
+
README 示例、`tests/test_config.py`)——统一换成文档用地址 `203.0.113.10`;
|
|
59
|
+
`tests/test_config.py` 里的私钥路径也换成占位值。
|
|
60
|
+
- `_debug_*` 与 `*.log` 加进构建排除列表,一次性排查脚本不会再被扫进 sdist。
|
|
61
|
+
注意 `1panel-toolkit\_debug_resolve.py/.log/.err` 三个文件仍在磁盘上
|
|
62
|
+
(本会话的删除命令被策略拦下),可手动删除。
|
|
63
|
+
|
|
64
|
+
### 验证
|
|
65
|
+
|
|
66
|
+
- 新增 5 条回归测试(容器路径不破坏 key、本地检出命名、按服务归属挂载、
|
|
67
|
+
`mounts` 缺 target 被拒、半坏 `mounts` 被拒),255 passed。
|
|
68
|
+
- 复现用例:带 `- ./x:/otel-lgtm/.../dash.yaml:ro` 的 5 行 compose,修复前
|
|
69
|
+
`p1 resolve` exit 2;修复后正常产出 spec 且 `key=repo`。openviking 真实检出:
|
|
70
|
+
修复前 `mounts` 两条且 host 错配,修复后只剩主服务那条 + 一条说明。
|
|
71
|
+
|
|
3
72
|
## 0.3.0 — 2026-09-20
|
|
4
73
|
|
|
5
74
|
补上 D09(生命周期脚本、首次启动配置)与 D10(依赖接入)两件"生成器一直在躲"的事。
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: 1panel-toolkit
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.2
|
|
4
4
|
Summary: Generate, check, fix and runtime-verify 1Panel app packages from a single spec.
|
|
5
5
|
Author: idkan
|
|
6
6
|
License: MIT License
|
|
@@ -47,7 +47,7 @@ agent 调用。
|
|
|
47
47
|
## 安装
|
|
48
48
|
|
|
49
49
|
```bash
|
|
50
|
-
uv tool install 1panel-toolkit
|
|
50
|
+
uv tool install 1panel-toolkit # PyPI 上当前是 0.3.0
|
|
51
51
|
p1 --version
|
|
52
52
|
p1 selftest # 验收:规则数据、写法库、生成→校验链路是否都正常
|
|
53
53
|
```
|
|
@@ -74,7 +74,7 @@ p1 check <app-package-dir>
|
|
|
74
74
|
p1 check <dir> --strict
|
|
75
75
|
p1 check <dir> --json
|
|
76
76
|
p1 rules lessons
|
|
77
|
-
p1 ports
|
|
77
|
+
p1 ports 203.0.113.10 10000,15000,8080
|
|
78
78
|
p1 patterns list
|
|
79
79
|
p1 patterns show compose/panel-db-env
|
|
80
80
|
p1 patterns find 数据库
|
|
@@ -13,7 +13,7 @@ agent 调用。
|
|
|
13
13
|
## 安装
|
|
14
14
|
|
|
15
15
|
```bash
|
|
16
|
-
uv tool install 1panel-toolkit
|
|
16
|
+
uv tool install 1panel-toolkit # PyPI 上当前是 0.3.0
|
|
17
17
|
p1 --version
|
|
18
18
|
p1 selftest # 验收:规则数据、写法库、生成→校验链路是否都正常
|
|
19
19
|
```
|
|
@@ -40,7 +40,7 @@ p1 check <app-package-dir>
|
|
|
40
40
|
p1 check <dir> --strict
|
|
41
41
|
p1 check <dir> --json
|
|
42
42
|
p1 rules lessons
|
|
43
|
-
p1 ports
|
|
43
|
+
p1 ports 203.0.113.10 10000,15000,8080
|
|
44
44
|
p1 patterns list
|
|
45
45
|
p1 patterns show compose/panel-db-env
|
|
46
46
|
p1 patterns find 数据库
|
|
@@ -99,7 +99,10 @@ p1 depth <包目录> --corpus <官方apps目录> # 单个包的判定 +
|
|
|
99
99
|
判定逻辑基于"应用特有特征指纹"(容器内端口、挂载点、应用变量名、镜像、额外运行时
|
|
100
100
|
字段),做 Jaccard 相似度:
|
|
101
101
|
|
|
102
|
-
-
|
|
102
|
+
- 三个"多样性维度"(对外端口 / 持久化挂载 / 应用自有变量)里**缺两个及以上** → `thin`
|
|
103
|
+
- 只缺一个 → 仍是 `adapted`,但会打一条 `·` 说明。应用完全可以把配置放在挂载的
|
|
104
|
+
配置文件里(xiaozhi 的 `data/.config.yaml`、openviking 的 `ov.conf`),这时
|
|
105
|
+
"没有应用变量"是设计而不是薄——把这种情况判成 thin 会逼人往 compose 里塞无意义的变量
|
|
103
106
|
- 与某个不相干的包重合度 ≥ 50% → `possibly-reskinned`
|
|
104
107
|
- 都不满足 → `adapted`
|
|
105
108
|
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "1panel-toolkit"
|
|
7
|
-
version = "0.3.
|
|
7
|
+
version = "0.3.2"
|
|
8
8
|
description = "Generate, check, fix and runtime-verify 1Panel app packages from a single spec."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -38,6 +38,9 @@ exclude = [
|
|
|
38
38
|
".pytest_cache/",
|
|
39
39
|
"*.egg-info/",
|
|
40
40
|
"*.pyc",
|
|
41
|
+
# Scratch files from debugging sessions must never ride along in the sdist.
|
|
42
|
+
"_debug_*",
|
|
43
|
+
"*.log",
|
|
41
44
|
]
|
|
42
45
|
|
|
43
46
|
[tool.pytest.ini_options]
|
|
@@ -268,11 +268,14 @@ def cmd_depth(args: argparse.Namespace) -> int:
|
|
|
268
268
|
print(f" - {value}")
|
|
269
269
|
if len(values) > 12:
|
|
270
270
|
print(f" ... 另外 {len(values) - 12} 项")
|
|
271
|
+
# The profile-based verdict needs no corpus; only the twin search does.
|
|
272
|
+
verdict = depth_module.assess(profile, corpus)
|
|
273
|
+
print(f"\n判定:{verdict.status}")
|
|
274
|
+
for reason in verdict.reasons:
|
|
275
|
+
print(f" ! {reason}")
|
|
276
|
+
for note in verdict.notes:
|
|
277
|
+
print(f" · {note}")
|
|
271
278
|
if corpus:
|
|
272
|
-
verdict = depth_module.assess(profile, corpus)
|
|
273
|
-
print(f"\n判定:{verdict.status}")
|
|
274
|
-
for reason in verdict.reasons:
|
|
275
|
-
print(f" ! {reason}")
|
|
276
279
|
print("\n与官方语料最接近的包:")
|
|
277
280
|
found = depth_module.nearest(profile, corpus)
|
|
278
281
|
if not found:
|
|
@@ -679,7 +682,7 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
679
682
|
"ports",
|
|
680
683
|
help="classify host ports as open / closed / filtered before a smoke test",
|
|
681
684
|
)
|
|
682
|
-
ports.add_argument("host", help="target host, e.g.
|
|
685
|
+
ports.add_argument("host", help="target host, e.g. 203.0.113.10")
|
|
683
686
|
ports.add_argument(
|
|
684
687
|
"ports", help="comma separated ports or ranges, e.g. 10000,15000 or 10000-10010"
|
|
685
688
|
)
|
|
@@ -366,6 +366,7 @@ class Verdict:
|
|
|
366
366
|
status: str
|
|
367
367
|
score: float
|
|
368
368
|
reasons: list[str] = field(default_factory=list)
|
|
369
|
+
notes: list[str] = field(default_factory=list)
|
|
369
370
|
twin: str | None = None
|
|
370
371
|
|
|
371
372
|
@property
|
|
@@ -380,18 +381,28 @@ def assess(
|
|
|
380
381
|
|
|
381
382
|
The dimensions that carry all the diversity in the store (container ports,
|
|
382
383
|
mount targets, application variable names) are exactly the ones a template
|
|
383
|
-
cannot answer. A package
|
|
384
|
-
|
|
384
|
+
cannot answer. A package missing *one* of them is a judgement call and only
|
|
385
|
+
gets a note — an app can legitimately carry its configuration in a mounted
|
|
386
|
+
file instead of environment variables. Missing two or more means the
|
|
387
|
+
package is a shell; a near-identical unrelated app means it is a reskin.
|
|
385
388
|
"""
|
|
386
389
|
reasons: list[str] = []
|
|
390
|
+
notes: list[str] = []
|
|
387
391
|
if not profile.images:
|
|
388
392
|
reasons.append("compose 里没有可识别的镜像")
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
393
|
+
missing = [
|
|
394
|
+
text
|
|
395
|
+
for present, text in (
|
|
396
|
+
(profile.port_pairs, "没有对外端口映射"),
|
|
397
|
+
(profile.mount_targets, "没有持久化挂载点(除非这个应用真的无状态)"),
|
|
398
|
+
(profile.app_env_keys, "没有任何应用自有环境变量(多数应用都有配置项)"),
|
|
399
|
+
)
|
|
400
|
+
if not present
|
|
401
|
+
]
|
|
402
|
+
if len(missing) >= 2:
|
|
403
|
+
reasons.extend(missing)
|
|
404
|
+
else:
|
|
405
|
+
notes.extend(missing)
|
|
395
406
|
twin = None
|
|
396
407
|
score = 0.0
|
|
397
408
|
found = nearest(profile, corpus, threshold=twin_threshold, limit=1)
|
|
@@ -404,10 +415,16 @@ def assess(
|
|
|
404
415
|
)
|
|
405
416
|
|
|
406
417
|
if not reasons:
|
|
407
|
-
return Verdict(status="adapted", score=score)
|
|
418
|
+
return Verdict(status="adapted", score=score, notes=notes)
|
|
408
419
|
if twin:
|
|
409
|
-
return Verdict(
|
|
410
|
-
|
|
420
|
+
return Verdict(
|
|
421
|
+
status="possibly-reskinned",
|
|
422
|
+
score=score,
|
|
423
|
+
reasons=reasons,
|
|
424
|
+
notes=notes,
|
|
425
|
+
twin=twin,
|
|
426
|
+
)
|
|
427
|
+
return Verdict(status="thin", score=score, reasons=reasons, notes=notes)
|
|
411
428
|
|
|
412
429
|
|
|
413
430
|
def to_json(data: dict[str, Any]) -> str:
|
|
@@ -193,6 +193,23 @@ def _mount_targets(service: dict[str, Any]) -> list[str]:
|
|
|
193
193
|
return []
|
|
194
194
|
|
|
195
195
|
|
|
196
|
+
def _mount_gaps(service: dict[str, Any]) -> list[str]:
|
|
197
|
+
"""``mounts`` entries the generator would silently drop.
|
|
198
|
+
|
|
199
|
+
A host path with no container path is refused (see below); the same mistake
|
|
200
|
+
written as a ``mounts`` entry used to vanish without a word.
|
|
201
|
+
"""
|
|
202
|
+
gaps: list[str] = []
|
|
203
|
+
mounts = service.get("mounts")
|
|
204
|
+
if isinstance(mounts, list):
|
|
205
|
+
for index, item in enumerate(mounts):
|
|
206
|
+
if not isinstance(item, dict):
|
|
207
|
+
gaps.append(f"mounts[{index}] 不是对象")
|
|
208
|
+
elif not item.get("target"):
|
|
209
|
+
gaps.append(f"mounts[{index}] 没有 target(host={item.get('host') or '—'})")
|
|
210
|
+
return gaps
|
|
211
|
+
|
|
212
|
+
|
|
196
213
|
def _mount_host(service: dict[str, Any]) -> str:
|
|
197
214
|
host = service.get("mount_host")
|
|
198
215
|
if host:
|
|
@@ -314,6 +331,15 @@ def _audit_services(audit: Audit, spec: dict[str, Any]) -> None:
|
|
|
314
331
|
)
|
|
315
332
|
|
|
316
333
|
# D03 — a host path without a container path silently drops persistence.
|
|
334
|
+
gaps = _mount_gaps(service)
|
|
335
|
+
if gaps and not audit.acknowledged.get("D03"):
|
|
336
|
+
audit.add(
|
|
337
|
+
"D03",
|
|
338
|
+
FAIL,
|
|
339
|
+
f"{name}: {';'.join(gaps)}",
|
|
340
|
+
"挂载点必须写出容器内路径;这条会被生成器静默丢弃,"
|
|
341
|
+
"装出来的包看着有挂载、实际没有",
|
|
342
|
+
)
|
|
317
343
|
if targets:
|
|
318
344
|
if d03:
|
|
319
345
|
unknown = [item for item in targets if item not in d03]
|
|
@@ -14,7 +14,7 @@ from pathlib import Path
|
|
|
14
14
|
from typing import Any
|
|
15
15
|
|
|
16
16
|
from .. import rules
|
|
17
|
-
from .analyze import Analysis, Evidence, analyze
|
|
17
|
+
from .analyze import Analysis, Evidence, analyze, service_mounts
|
|
18
18
|
from .sources import (
|
|
19
19
|
DEFAULT_FETCH_BUDGET,
|
|
20
20
|
GitHub,
|
|
@@ -281,38 +281,38 @@ def draft_spec(target: str, analysis: Analysis, decisions: list[Decision]) -> di
|
|
|
281
281
|
# value never reaches the generator.
|
|
282
282
|
images = analysis.values("image") or analysis.values("image_hint")
|
|
283
283
|
ports = analysis.values("container_port")
|
|
284
|
-
mounts = (
|
|
285
|
-
analysis.values("mount_target")
|
|
286
|
-
or analysis.values("mount_target_hint")
|
|
287
|
-
)
|
|
288
|
-
host_mounts = analysis.values("mount_host")
|
|
289
284
|
services = analysis.compose.get("services") or {}
|
|
290
285
|
primary = next(iter(services), "")
|
|
291
|
-
# Pair each container path with its own host path,
|
|
292
|
-
# a
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
)
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
286
|
+
# Pair each container path with its own host path, and only within one
|
|
287
|
+
# service: a compose file can mount a data directory for the app and a
|
|
288
|
+
# config file for a sidecar, and mixing them binds the wrong directory into
|
|
289
|
+
# the wrong service. The draft describes the primary service; mounts that
|
|
290
|
+
# belong to other services are reported for a human to decide.
|
|
291
|
+
per_service = service_mounts(analysis.compose)
|
|
292
|
+
primary_mounts = per_service.get(primary, [])
|
|
293
|
+
mount_pairs: list[dict[str, str]] = [
|
|
294
|
+
{"host": host, "target": container_path} for host, container_path in primary_mounts
|
|
295
|
+
]
|
|
296
|
+
seen_targets: set[str] = {container_path for _, container_path in primary_mounts}
|
|
297
|
+
# The primary service's own container paths. Sidecar mounts are deliberately
|
|
298
|
+
# not included: they belong to another service and another host path.
|
|
299
|
+
mounts = [container_path for _, container_path in primary_mounts]
|
|
300
|
+
# Weak evidence (a path-valued env default such as HALO_WORK_DIR) has no host
|
|
301
|
+
# of its own. It only fills in when the primary service mounts nothing, and
|
|
302
|
+
# it gets the primary service's first host, never a sidecar's.
|
|
303
|
+
if not mounts:
|
|
304
|
+
mounts = analysis.values("mount_target_hint")
|
|
305
|
+
hint_host = primary_mounts[0][0] if primary_mounts else "./data"
|
|
306
306
|
for container_path in mounts:
|
|
307
307
|
if container_path not in seen_targets:
|
|
308
308
|
seen_targets.add(container_path)
|
|
309
|
-
mount_pairs.append(
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
309
|
+
mount_pairs.append({"host": hint_host, "target": container_path})
|
|
310
|
+
# The product name, never the local checkout path.
|
|
311
|
+
label = str(source.label)
|
|
312
|
+
if label.startswith("local:"):
|
|
313
|
+
repo_name = Path(label.split(":", 1)[1]).name or "app"
|
|
314
|
+
else:
|
|
315
|
+
repo_name = label.split("/")[-1].split("@")[0] or "app"
|
|
316
316
|
# Values discovered for the application's own variables become compose
|
|
317
317
|
# defaults; the generator decides later which of them a user must see.
|
|
318
318
|
env_map: dict[str, Any] = env_map_of(analysis)
|
|
@@ -356,8 +356,20 @@ def draft_spec(target: str, analysis: Analysis, decisions: list[Decision]) -> di
|
|
|
356
356
|
# default: `p1 gen` refuses it, so only carry a host path when something is
|
|
357
357
|
# actually mounted.
|
|
358
358
|
if mounts:
|
|
359
|
-
service_spec["mount_host"] =
|
|
360
|
-
|
|
359
|
+
service_spec["mount_host"] = mount_pairs[0]["host"] if mount_pairs else hint_host
|
|
360
|
+
|
|
361
|
+
other_mounts = {
|
|
362
|
+
name: pairs for name, pairs in per_service.items() if name != primary and pairs
|
|
363
|
+
}
|
|
364
|
+
notes = list(source.notes) + list(analysis.notes)
|
|
365
|
+
if other_mounts:
|
|
366
|
+
described = "、".join(
|
|
367
|
+
f"{name}({', '.join(target for _, target in pairs)})"
|
|
368
|
+
for name, pairs in sorted(other_mounts.items())
|
|
369
|
+
)
|
|
370
|
+
notes.append(
|
|
371
|
+
f"compose 里还有其它服务的挂载点:{described};本 draft 只描述主服务 "
|
|
372
|
+
f"{primary or 'app'},这些挂载点要不要并进这个包由人工判断"
|
|
361
373
|
)
|
|
362
374
|
|
|
363
375
|
spec: dict[str, Any] = {
|
|
@@ -392,7 +404,7 @@ def draft_spec(target: str, analysis: Analysis, decisions: list[Decision]) -> di
|
|
|
392
404
|
"architectures 默认 amd64:容器镜像仓库在本机不可达,无法读取 manifest",
|
|
393
405
|
"type/tags 暂定 tool/Tool,需按应用实际形态确认",
|
|
394
406
|
],
|
|
395
|
-
"notes":
|
|
407
|
+
"notes": notes,
|
|
396
408
|
}
|
|
397
409
|
hints = dependency_hints(analysis, env_map)
|
|
398
410
|
if hints:
|
|
@@ -408,6 +420,8 @@ def render_report(target: str, analysis: Analysis, decisions: list[Decision]) ->
|
|
|
408
420
|
lines.append(f"- {key}: {value}")
|
|
409
421
|
for note in source.notes:
|
|
410
422
|
lines.append(f"- 注意:{note}")
|
|
423
|
+
for note in analysis.notes:
|
|
424
|
+
lines.append(f"- 注意:{note}")
|
|
411
425
|
lines.append("")
|
|
412
426
|
lines.append("| 决策 | 状态 | 答案 | 证据 |")
|
|
413
427
|
lines.append("| --- | --- | --- | --- |")
|
|
@@ -191,6 +191,35 @@ def _volume_split(entry: Any) -> tuple[str, str] | None:
|
|
|
191
191
|
return parts[0].strip(), parts[1].strip()
|
|
192
192
|
|
|
193
193
|
|
|
194
|
+
def service_mounts(compose: Any) -> dict[str, list[tuple[str, str]]]:
|
|
195
|
+
"""Bind mounts per service: ``{service: [(host, container_path), ...]}``.
|
|
196
|
+
|
|
197
|
+
Attribution matters. A compose file can mount a data directory for the app
|
|
198
|
+
and a config file for a sidecar; pairing every container path with the
|
|
199
|
+
first host path produces a package that binds the wrong directory into the
|
|
200
|
+
wrong service.
|
|
201
|
+
"""
|
|
202
|
+
if not isinstance(compose, dict):
|
|
203
|
+
return {}
|
|
204
|
+
services = compose.get("services")
|
|
205
|
+
if not isinstance(services, dict):
|
|
206
|
+
return {}
|
|
207
|
+
result: dict[str, list[tuple[str, str]]] = {}
|
|
208
|
+
for name, service in services.items():
|
|
209
|
+
if not isinstance(service, dict):
|
|
210
|
+
continue
|
|
211
|
+
pairs: list[tuple[str, str]] = []
|
|
212
|
+
for volume in service.get("volumes") or []:
|
|
213
|
+
split = _volume_split(volume)
|
|
214
|
+
# Only bind mounts with a container path; named volumes and paths
|
|
215
|
+
# relative to the container are a different decision.
|
|
216
|
+
if split and split[1].startswith("/") and split not in pairs:
|
|
217
|
+
pairs.append(split)
|
|
218
|
+
if pairs:
|
|
219
|
+
result[str(name)] = pairs
|
|
220
|
+
return result
|
|
221
|
+
|
|
222
|
+
|
|
194
223
|
def _env_map(service: dict[str, Any]) -> list[tuple[str, str]]:
|
|
195
224
|
env = service.get("environment")
|
|
196
225
|
if isinstance(env, dict):
|
|
@@ -58,7 +58,8 @@ lessons:
|
|
|
58
58
|
root_cause: 云厂商安全组只放行了部分端口,表单默认端口落在未放行区间。
|
|
59
59
|
fix: "先跑 `p1 ports <host> <候选端口>`:立即被拒绝(closed)= 安全组已放行且无人监听,可直接用;超时(filtered)= 被上游拦掉,换端口。冒烟测试只用 closed/open 的端口。"
|
|
60
60
|
cases:
|
|
61
|
-
- "
|
|
61
|
+
- "某云主机实测:部分端口返回 RST(安全组已放行且无人监听),另一些端口超时(被安全组拦截),
|
|
62
|
+
与打包无关"
|
|
62
63
|
applies_when: 在云服务器上做运行时验证。
|
|
63
64
|
|
|
64
65
|
- id: L006
|
|
@@ -45,13 +45,13 @@ def test_windows_paths_survive_a_round_trip(tmp_path: Path, monkeypatch) -> None
|
|
|
45
45
|
"""A backslash is a TOML escape; writing one raw corrupts the whole file."""
|
|
46
46
|
monkeypatch.setenv("P1_CONFIG", str(tmp_path / "config.toml"))
|
|
47
47
|
cfg = config_module.load()
|
|
48
|
-
cfg.set("ssh.key", r"C:\Users\
|
|
49
|
-
cfg.set("ssh.host", "
|
|
48
|
+
cfg.set("ssh.key", r"C:\Users\example\.ssh\panel_key")
|
|
49
|
+
cfg.set("ssh.host", "203.0.113.10")
|
|
50
50
|
cfg.save()
|
|
51
51
|
|
|
52
52
|
reloaded = config_module.load()
|
|
53
|
-
assert reloaded.get("ssh.key") == r"C:\Users\
|
|
54
|
-
assert reloaded.get("ssh.host") == "
|
|
53
|
+
assert reloaded.get("ssh.key") == r"C:\Users\example\.ssh\panel_key"
|
|
54
|
+
assert reloaded.get("ssh.host") == "203.0.113.10"
|
|
55
55
|
|
|
56
56
|
|
|
57
57
|
def test_quotes_in_values_survive(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
@@ -64,6 +64,56 @@ def test_adapted_package_passes(tmp_path: Path) -> None:
|
|
|
64
64
|
assert assess(profile, []).status == "adapted"
|
|
65
65
|
|
|
66
66
|
|
|
67
|
+
def test_missing_one_dimension_is_a_note_not_a_verdict(tmp_path: Path) -> None:
|
|
68
|
+
"""An app can carry its config in a mounted file instead of env vars.
|
|
69
|
+
|
|
70
|
+
xiaozhi-esp32-server is exactly that shape: two ports, a model-file mount,
|
|
71
|
+
security_opt, an init script, and zero application variables because
|
|
72
|
+
everything lives in `data/.config.yaml`. Calling that "thin" is wrong.
|
|
73
|
+
"""
|
|
74
|
+
compose = (
|
|
75
|
+
"services:\n"
|
|
76
|
+
" app:\n"
|
|
77
|
+
' image: "ghcr.io/example/app:1.0"\n'
|
|
78
|
+
" container_name: ${CONTAINER_NAME}\n"
|
|
79
|
+
" networks:\n"
|
|
80
|
+
" - 1panel-network\n"
|
|
81
|
+
" ports:\n"
|
|
82
|
+
' - "${PANEL_APP_PORT_HTTP}:8000"\n'
|
|
83
|
+
" volumes:\n"
|
|
84
|
+
" - ./data:/opt/app/data\n"
|
|
85
|
+
"networks:\n"
|
|
86
|
+
" 1panel-network:\n"
|
|
87
|
+
" external: true\n"
|
|
88
|
+
)
|
|
89
|
+
profile = profile_app(write_package(tmp_path / "demo", key="demo", compose=compose))
|
|
90
|
+
verdict = assess(profile, [])
|
|
91
|
+
assert verdict.status == "adapted"
|
|
92
|
+
assert not verdict.reasons
|
|
93
|
+
assert any("环境变量" in note for note in verdict.notes)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def test_two_missing_dimensions_are_still_thin(tmp_path: Path) -> None:
|
|
97
|
+
compose = (
|
|
98
|
+
"services:\n"
|
|
99
|
+
" app:\n"
|
|
100
|
+
' image: "ghcr.io/example/app:1.0"\n'
|
|
101
|
+
" container_name: ${CONTAINER_NAME}\n"
|
|
102
|
+
" networks:\n"
|
|
103
|
+
" - 1panel-network\n"
|
|
104
|
+
" ports:\n"
|
|
105
|
+
' - "${PANEL_APP_PORT_HTTP}:8000"\n'
|
|
106
|
+
"networks:\n"
|
|
107
|
+
" 1panel-network:\n"
|
|
108
|
+
" external: true\n"
|
|
109
|
+
)
|
|
110
|
+
profile = profile_app(write_package(tmp_path / "demo", key="demo", compose=compose))
|
|
111
|
+
verdict = assess(profile, [])
|
|
112
|
+
assert verdict.shallow
|
|
113
|
+
assert any("挂载" in reason for reason in verdict.reasons)
|
|
114
|
+
assert any("环境变量" in reason for reason in verdict.reasons)
|
|
115
|
+
|
|
116
|
+
|
|
67
117
|
def test_similarity_finds_a_reskin(tmp_path: Path) -> None:
|
|
68
118
|
first = profile_app(write_package(tmp_path / "a", key="a"))
|
|
69
119
|
second = profile_app(write_package(tmp_path / "b", key="b"))
|
|
@@ -131,4 +181,3 @@ def test_templateable_dimensions_documented() -> None:
|
|
|
131
181
|
templatable = data.get("templatable") or []
|
|
132
182
|
assert templatable
|
|
133
183
|
assert any("重启策略" in str(item.get("dimension")) for item in templatable)
|
|
134
|
-
|
|
@@ -136,6 +136,25 @@ def test_dangling_mount_host_is_refused() -> None:
|
|
|
136
136
|
assert "D03" in result.blocked_decisions
|
|
137
137
|
|
|
138
138
|
|
|
139
|
+
def test_mount_entry_without_a_target_is_refused() -> None:
|
|
140
|
+
"""The same mistake written into `mounts` used to vanish without a word."""
|
|
141
|
+
spec = make_spec()
|
|
142
|
+
spec["services"][0]["mounts"] = [{"host": "./data"}]
|
|
143
|
+
result = audit(spec)
|
|
144
|
+
assert not result.ok
|
|
145
|
+
assert "D03" in result.blocked_decisions
|
|
146
|
+
assert any("mounts[0]" in item.message for item in result.failures)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def test_partially_broken_mounts_are_refused_not_trimmed() -> None:
|
|
150
|
+
spec = make_spec()
|
|
151
|
+
spec["services"][0]["mounts"] = [
|
|
152
|
+
{"host": "./data", "target": "/app/data"},
|
|
153
|
+
{"host": "./extra"},
|
|
154
|
+
]
|
|
155
|
+
assert not audit(spec).ok
|
|
156
|
+
|
|
157
|
+
|
|
139
158
|
def test_port_declared_in_the_ledger_but_missing_from_the_spec_is_refused() -> None:
|
|
140
159
|
spec = make_spec()
|
|
141
160
|
spec["services"][0].pop("container_port")
|
|
@@ -230,6 +230,76 @@ def test_draft_spec_has_no_dangling_mount_host(tmp_path: Path) -> None:
|
|
|
230
230
|
assert "mount_host" not in service
|
|
231
231
|
|
|
232
232
|
|
|
233
|
+
def test_a_container_path_volume_does_not_break_the_repository_key(
|
|
234
|
+
tmp_path: Path,
|
|
235
|
+
) -> None:
|
|
236
|
+
"""A volume's container path used to overwrite `draft_spec`'s `target`
|
|
237
|
+
argument, so `repo_key` received `/otel-lgtm/...` and resolve died with
|
|
238
|
+
"cannot interpret ..." before writing anything."""
|
|
239
|
+
root = tmp_path / "repo"
|
|
240
|
+
root.mkdir()
|
|
241
|
+
(root / "docker-compose.yml").write_text(
|
|
242
|
+
"services:\n"
|
|
243
|
+
" grafana:\n"
|
|
244
|
+
" image: grafana/otel-lgtm:latest\n"
|
|
245
|
+
" volumes:\n"
|
|
246
|
+
" - ./x:/otel-lgtm/grafana/conf/provisioning/dashboards/dash.yaml:ro\n",
|
|
247
|
+
encoding="utf-8",
|
|
248
|
+
)
|
|
249
|
+
result = resolve(str(root))
|
|
250
|
+
assert result.spec["key"] == "repo"
|
|
251
|
+
assert result.spec["name"] == "repo"
|
|
252
|
+
assert result.spec["services"][0]["mounts"] == [
|
|
253
|
+
{
|
|
254
|
+
"host": "./x",
|
|
255
|
+
"target": "/otel-lgtm/grafana/conf/provisioning/dashboards/dash.yaml",
|
|
256
|
+
}
|
|
257
|
+
]
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def test_local_checkout_never_names_the_product_after_its_path(tmp_path: Path) -> None:
|
|
261
|
+
root = tmp_path / "My-App-main"
|
|
262
|
+
root.mkdir()
|
|
263
|
+
(root / "Dockerfile").write_text("FROM alpine\nEXPOSE 8080\n", encoding="utf-8")
|
|
264
|
+
spec = resolve(str(root)).spec
|
|
265
|
+
assert spec["name"] == "My-App-main"
|
|
266
|
+
assert spec["key"] == "my-app-main"
|
|
267
|
+
assert "\\\\" not in spec["name"] and "tmp" not in spec["key"]
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def test_each_service_keeps_its_own_mounts(tmp_path: Path) -> None:
|
|
271
|
+
"""A sidecar's config mount must not be paired with the app's data host.
|
|
272
|
+
|
|
273
|
+
Getting this wrong produces a package that binds the app's data directory
|
|
274
|
+
into the sidecar's container path, and every static check passes.
|
|
275
|
+
"""
|
|
276
|
+
root = tmp_path / "repo"
|
|
277
|
+
root.mkdir()
|
|
278
|
+
(root / "docker-compose.yml").write_text(
|
|
279
|
+
"services:\n"
|
|
280
|
+
" app:\n"
|
|
281
|
+
" image: example/app:1.0\n"
|
|
282
|
+
" ports:\n"
|
|
283
|
+
' - "1933:1933"\n'
|
|
284
|
+
" volumes:\n"
|
|
285
|
+
" - ~/.app:/app/data\n"
|
|
286
|
+
" caddy:\n"
|
|
287
|
+
" image: caddy:2\n"
|
|
288
|
+
" volumes:\n"
|
|
289
|
+
" - ./Caddyfile:/etc/caddy/Caddyfile:ro\n",
|
|
290
|
+
encoding="utf-8",
|
|
291
|
+
)
|
|
292
|
+
spec = resolve(str(root)).spec
|
|
293
|
+
service = spec["services"][0]
|
|
294
|
+
assert service["mounts"] == [{"host": "~/.app", "target": "/app/data"}]
|
|
295
|
+
assert service["mount_targets"] == ["/app/data"]
|
|
296
|
+
assert service["mount_host"] == "~/.app"
|
|
297
|
+
# The sidecar's mount is reported, not silently merged.
|
|
298
|
+
assert any("caddy" in note and "/etc/caddy/Caddyfile" in note for note in spec["notes"])
|
|
299
|
+
report = render_report("repo", resolve(str(root)).analysis, resolve(str(root)).decisions)
|
|
300
|
+
assert "/etc/caddy/Caddyfile" in report
|
|
301
|
+
|
|
302
|
+
|
|
233
303
|
def test_dependency_hints_point_at_the_upstream_variable_names(tmp_path: Path) -> None:
|
|
234
304
|
"""D10 的线索:把上游那几个数据库变量指出来,人不用自己抄。"""
|
|
235
305
|
root = tmp_path / "repo"
|
|
File without changes
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
GET https://api.github.com/repos/halo-dev/halo
|
|
2
|
-
-> 0.5 s
|
|
3
|
-
GET https://api.github.com/repos/halo-dev/halo/git/trees/main?recursive=1
|
|
4
|
-
-> 2.2 s
|
|
5
|
-
GET https://api.github.com/repos/halo-dev/halo/releases/latest
|
|
6
|
-
-> 0.5 s
|
|
7
|
-
GET https://raw.githubusercontent.com/halo-dev/halo/main/Dockerfile
|
|
8
|
-
-> 20.5 s
|
|
9
|
-
GET https://raw.githubusercontent.com/halo-dev/halo/main/README.md
|
|
10
|
-
-> 60.5 s
|
|
11
|
-
GET https://raw.githubusercontent.com/halo-dev/halo/main/docs/index/README.md
|
|
12
|
-
-> 60.0 s
|
|
13
|
-
GET https://raw.githubusercontent.com/halo-dev/halo/main/docs/notification/README.md
|
|
14
|
-
-> 36.4 s
|
|
15
|
-
GET https://raw.githubusercontent.com/halo-dev/halo/main/ui/tsconfig.json
|
|
16
|
-
-> 16.2 s
|
|
17
|
-
GET https://raw.githubusercontent.com/halo-dev/halo/main/openspec/config.yaml
|
|
18
|
-
-> 0.9 s
|
|
19
|
-
GET https://raw.githubusercontent.com/halo-dev/halo/main/ui/tsconfig.app.json
|
|
20
|
-
-> 0.9 s
|
|
21
|
-
total 198.7 s
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
import sys, time
|
|
2
|
-
sys.path.insert(0, "src")
|
|
3
|
-
from p1toolkit.resolve import sources
|
|
4
|
-
|
|
5
|
-
orig_get = sources.GitHub._get
|
|
6
|
-
def traced(self, url, limit=sources.MAX_FILE_BYTES):
|
|
7
|
-
started = time.time()
|
|
8
|
-
print("GET", url[:110], flush=True)
|
|
9
|
-
try:
|
|
10
|
-
return orig_get(self, url, limit)
|
|
11
|
-
finally:
|
|
12
|
-
print(" ->", round(time.time() - started, 1), "s", flush=True)
|
|
13
|
-
|
|
14
|
-
sources.GitHub._get = traced
|
|
15
|
-
from p1toolkit.resolve import resolve
|
|
16
|
-
started = time.time()
|
|
17
|
-
result = resolve("halo-dev/halo")
|
|
18
|
-
print("total", round(time.time() - started, 1), "s", flush=True)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|