@elvis1513/auto-coding-skill 1.0.0 → 1.0.1
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.
- package/README.md +18 -2
- package/cli/assets/skill/SKILL.md +17 -2
- package/cli/assets/skill/data/templates/ENGINEERING.md +21 -15
- package/cli/assets/skill/scripts/ap.py +98 -113
- package/cli/src/index.js +0 -17
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -7,6 +7,8 @@ Engineering workflow skill for:
|
|
|
7
7
|
|
|
8
8
|
This skill targets Go backend + frontend monorepo projects that rely on Jenkins for build and deployment. The optimized default flow is lightweight locally, then Jenkins-first and target-environment-first for real verification.
|
|
9
9
|
|
|
10
|
+
`docs/ENGINEERING.md` is intentionally Git-tracked. The environment fields kept in that file are mandatory, must be filled with real values, and are committed as part of project maintenance. Unused environment items should be removed instead of being kept as placeholders.
|
|
11
|
+
|
|
10
12
|
## Install
|
|
11
13
|
|
|
12
14
|
```bash
|
|
@@ -108,6 +110,7 @@ python3 .claude/skills/auto-coding-skill/scripts/ap.py --repo . install
|
|
|
108
110
|
- `docs/ENGINEERING.md` frontmatter
|
|
109
111
|
|
|
110
112
|
This frontmatter is the only manual config source.
|
|
113
|
+
It must be committed to Git. Do not add it to `.gitignore`.
|
|
111
114
|
|
|
112
115
|
重点字段:
|
|
113
116
|
- `commands.*`
|
|
@@ -121,13 +124,24 @@ This frontmatter is the only manual config source.
|
|
|
121
124
|
- `commands.quick_test` 或 `commands.test`
|
|
122
125
|
- `commands.lint` 或 `commands.typecheck`
|
|
123
126
|
- `target_env.name`
|
|
127
|
+
- `target_env.frontend_base_url`
|
|
128
|
+
- `target_env.frontend_username`
|
|
129
|
+
- `target_env.frontend_password`
|
|
130
|
+
- `target_env.backend_base_url`
|
|
131
|
+
- `target_env.backend_username`
|
|
132
|
+
- `target_env.backend_password`
|
|
124
133
|
- `target_env.health_base_url`
|
|
125
134
|
- `target_env.health_path`
|
|
135
|
+
- `jenkins.base_url`
|
|
136
|
+
- `jenkins.ui_username`
|
|
137
|
+
- `jenkins.ui_password`
|
|
138
|
+
- `jenkins.api_user`
|
|
139
|
+
- `jenkins.api_password`
|
|
126
140
|
- `jenkins.trigger_branch`
|
|
127
141
|
- `jenkins.image_repository`
|
|
128
142
|
- `jenkins.image_tag_strategy`
|
|
129
143
|
- `jenkins.deploy_env`
|
|
130
|
-
- `jenkins.job_url`
|
|
144
|
+
- `jenkins.job_url`
|
|
131
145
|
|
|
132
146
|
4. Start AI development by constraints:
|
|
133
147
|
|
|
@@ -185,7 +199,9 @@ This frontmatter is the only manual config source.
|
|
|
185
199
|
- Purpose: single source of project config + workflow rules.
|
|
186
200
|
- How to record:
|
|
187
201
|
- Fill YAML frontmatter once.
|
|
188
|
-
- Keep target env
|
|
202
|
+
- Keep target env front/backend usernames and passwords, Jenkins UI/API usernames and passwords, commands, docs paths here only.
|
|
203
|
+
- This file is expected to be committed to Git and maintained in plaintext for this workflow.
|
|
204
|
+
- Remaining environment keys are all mandatory; blank values, TODO-like placeholders, and incorrect URL/path formats are treated as blocking errors by `doctor`.
|
|
189
205
|
- Do not duplicate config elsewhere.
|
|
190
206
|
|
|
191
207
|
### 2) docs/tasks/taskbook.md
|
|
@@ -7,6 +7,8 @@ description: Use for a lightweight Jenkins-first engineering workflow in Claude/
|
|
|
7
7
|
|
|
8
8
|
This skill is for Go backend + frontend monorepo projects that rely on Jenkins to build and deploy after push. It supports both Claude and Codex. The default workflow is lightweight locally, then uses Jenkins and the real target environment as the authoritative verification path.
|
|
9
9
|
|
|
10
|
+
`docs/ENGINEERING.md` is intentionally Git-tracked in this workflow. The remaining environment fields in that file are mandatory, must be filled with real values, and are committed as part of the project baseline. Unused environment keys should be removed from the template instead of being left as placeholders.
|
|
11
|
+
|
|
10
12
|
Prefer already available MCP servers, installed skills, plugins, and app connectors over ad-hoc manual work whenever they can complete the task reliably.
|
|
11
13
|
|
|
12
14
|
Default to multi-agent execution when the client supports it. Break work into independent design, research, implementation, validation, and documentation subtasks so Claude/Codex can run them in parallel whenever that reduces cycle time without weakening control of the main task.
|
|
@@ -71,6 +73,7 @@ This contains all manual fields:
|
|
|
71
73
|
- `docs.*`
|
|
72
74
|
|
|
73
75
|
Do not duplicate config in other md/yaml files.
|
|
76
|
+
Do not hide `docs/ENGINEERING.md` in `.gitignore`.
|
|
74
77
|
|
|
75
78
|
Minimum required config for the default flow:
|
|
76
79
|
- `project.name`
|
|
@@ -78,13 +81,24 @@ Minimum required config for the default flow:
|
|
|
78
81
|
- `commands.quick_test` or `commands.test`
|
|
79
82
|
- `commands.lint` or `commands.typecheck`
|
|
80
83
|
- `target_env.name`
|
|
84
|
+
- `target_env.frontend_base_url`
|
|
85
|
+
- `target_env.frontend_username`
|
|
86
|
+
- `target_env.frontend_password`
|
|
87
|
+
- `target_env.backend_base_url`
|
|
88
|
+
- `target_env.backend_username`
|
|
89
|
+
- `target_env.backend_password`
|
|
81
90
|
- `target_env.health_base_url`
|
|
82
91
|
- `target_env.health_path`
|
|
92
|
+
- `jenkins.base_url`
|
|
93
|
+
- `jenkins.ui_username`
|
|
94
|
+
- `jenkins.ui_password`
|
|
95
|
+
- `jenkins.api_user`
|
|
96
|
+
- `jenkins.api_password`
|
|
83
97
|
- `jenkins.trigger_branch`
|
|
84
98
|
- `jenkins.image_repository`
|
|
85
99
|
- `jenkins.image_tag_strategy`
|
|
86
100
|
- `jenkins.deploy_env`
|
|
87
|
-
- `jenkins.job_url`
|
|
101
|
+
- `jenkins.job_url`
|
|
88
102
|
|
|
89
103
|
## Branch policy
|
|
90
104
|
|
|
@@ -134,8 +148,9 @@ python3 docs/tools/autopipeline/ap.py gen-summary <TASK_ID>
|
|
|
134
148
|
## Quality policy
|
|
135
149
|
|
|
136
150
|
- Default local gate is lightweight only: build, unit/quick test, lint, typecheck, API docs, Jenkinsfile / script syntax, `git diff --check`.
|
|
137
|
-
- `doctor` should be used early to catch missing config before the first implementation loop.
|
|
151
|
+
- `doctor` should be used early to catch missing or invalid config before the first implementation loop.
|
|
138
152
|
- `light-gate` now fails if the required default commands are not configured.
|
|
153
|
+
- `doctor`, `light-gate`, and `commit-push` all fail when required environment fields are missing, placeholder-like, or syntactically invalid.
|
|
139
154
|
- Do not require local Docker Compose or full local regression for every small change.
|
|
140
155
|
- Jenkins and target environment verification are more valuable than repeated local simulation of deploy-only problems.
|
|
141
156
|
- `verify-target` should be used for real target-environment API/page checks when the task touches user-visible or deploy-sensitive behavior.
|
|
@@ -45,28 +45,19 @@ target_env:
|
|
|
45
45
|
backend_password: ""
|
|
46
46
|
health_base_url: ""
|
|
47
47
|
health_path: ""
|
|
48
|
-
verify_notes: ""
|
|
49
48
|
|
|
50
49
|
jenkins:
|
|
51
50
|
base_url: ""
|
|
52
51
|
ui_username: ""
|
|
53
52
|
ui_password: ""
|
|
54
|
-
crumb_url: ""
|
|
55
|
-
job_name: ""
|
|
56
53
|
job_url: ""
|
|
57
|
-
multibranch_root_job: ""
|
|
58
|
-
branch_name: ""
|
|
59
54
|
trigger_branch: ""
|
|
60
55
|
image_repository: ""
|
|
61
56
|
image_tag_strategy: ""
|
|
62
57
|
deploy_env: ""
|
|
63
58
|
deploy_timeout_sec: 1800
|
|
64
59
|
api_user: ""
|
|
65
|
-
api_token: ""
|
|
66
60
|
api_password: ""
|
|
67
|
-
api_user_env: "JENKINS_USER"
|
|
68
|
-
api_token_env: "JENKINS_TOKEN"
|
|
69
|
-
api_password_env: "JENKINS_PASSWORD"
|
|
70
61
|
|
|
71
62
|
docs:
|
|
72
63
|
taskbook: "docs/tasks/taskbook.md"
|
|
@@ -97,6 +88,9 @@ docs:
|
|
|
97
88
|
补充规则:
|
|
98
89
|
- 每次任务闭环后,必须清理临时文件、临时目录、日志、截图、构建缓存等非必要产物;仅明确需要保留的本地诊断目录允许保留。
|
|
99
90
|
- 所有手工填写信息,只维护在本文件 frontmatter 中,其他文档不得重复配置。
|
|
91
|
+
- `docs/ENGINEERING.md` 必须提交到 Git 管理,不允许写入 `.gitignore`。
|
|
92
|
+
- 本 workflow 明确允许在 `docs/ENGINEERING.md` 中明文维护平台账号、密码,并随 Git 一起版本化。
|
|
93
|
+
- 未参与默认流程的环境项不要保留占位;模板中未保留的字段视为已清理,不再额外配置。
|
|
100
94
|
|
|
101
95
|
---
|
|
102
96
|
|
|
@@ -104,14 +98,14 @@ docs:
|
|
|
104
98
|
|
|
105
99
|
先填写 `docs/ENGINEERING.md` frontmatter 中的所有空值。重点包括:
|
|
106
100
|
- `commands.*`:本地轻量校验命令
|
|
107
|
-
- `target_env
|
|
108
|
-
- `jenkins.*`:Jenkins UI/API
|
|
101
|
+
- `target_env.*`:目标环境前端 / 后端地址、用户名、密码,必须全部填写且真实可用
|
|
102
|
+
- `jenkins.*`:Jenkins UI/API 用户名、密码、Job、分支、镜像、部署环境,必须全部填写且真实可用
|
|
109
103
|
|
|
110
104
|
字段说明:
|
|
111
105
|
- `target_env.backend_username` / `target_env.backend_password`:目标环境后台账号
|
|
112
|
-
- `target_env.frontend_username` / `target_env.frontend_password
|
|
106
|
+
- `target_env.frontend_username` / `target_env.frontend_password`:目标环境前端登录账号
|
|
113
107
|
- `jenkins.ui_username` / `jenkins.ui_password`:Jenkins 页面登录账号
|
|
114
|
-
- `jenkins.api_user`
|
|
108
|
+
- `jenkins.api_user` / `jenkins.api_password`:Jenkins API 用户名 / 密码
|
|
115
109
|
|
|
116
110
|
默认必填:
|
|
117
111
|
- `project.name`
|
|
@@ -119,18 +113,27 @@ docs:
|
|
|
119
113
|
- `commands.quick_test` 或 `commands.test`
|
|
120
114
|
- `commands.lint` 或 `commands.typecheck`
|
|
121
115
|
- `target_env.name`
|
|
116
|
+
- `target_env.frontend_base_url`
|
|
117
|
+
- `target_env.frontend_username`
|
|
118
|
+
- `target_env.frontend_password`
|
|
119
|
+
- `target_env.backend_base_url`
|
|
120
|
+
- `target_env.backend_username`
|
|
121
|
+
- `target_env.backend_password`
|
|
122
122
|
- `target_env.health_base_url`
|
|
123
123
|
- `target_env.health_path`
|
|
124
|
+
- `jenkins.ui_username`
|
|
125
|
+
- `jenkins.ui_password`
|
|
126
|
+
- `jenkins.api_user`
|
|
127
|
+
- `jenkins.api_password`
|
|
124
128
|
- `jenkins.trigger_branch`
|
|
125
129
|
- `jenkins.image_repository`
|
|
126
130
|
- `jenkins.image_tag_strategy`
|
|
127
131
|
- `jenkins.deploy_env`
|
|
128
|
-
- `jenkins.job_url`
|
|
132
|
+
- `jenkins.job_url`
|
|
129
133
|
|
|
130
134
|
按需填写:
|
|
131
135
|
- `runtime.*`:仅在本地运行诊断时使用
|
|
132
136
|
- `commands.compose_up` / `commands.compose_down` / `commands.smoke` / `commands.regression`
|
|
133
|
-
- `target_env.frontend_*` / `target_env.backend_*`:仅在需要额外页面/API验证或受保护资源校验时使用
|
|
134
137
|
|
|
135
138
|
---
|
|
136
139
|
|
|
@@ -217,6 +220,9 @@ docs:
|
|
|
217
220
|
9. 闭环记录
|
|
218
221
|
每个任务必须留下轻量闭环记录:任务 ID、提交号、Jenkins Build URL、目标环境验证结果、是否通过、遗留问题。
|
|
219
222
|
|
|
223
|
+
10. 配置入库
|
|
224
|
+
`docs/ENGINEERING.md` 中保留下来的环境信息、前端/后端账号、Jenkins 账号与密码必须 100% 填写、正确填写,并提交 Git 作为项目权威配置持续维护。
|
|
225
|
+
|
|
220
226
|
---
|
|
221
227
|
|
|
222
228
|
## 3. 高风险变更(必须补强验证)
|
|
@@ -8,7 +8,6 @@ import argparse
|
|
|
8
8
|
import base64
|
|
9
9
|
import datetime as _dt
|
|
10
10
|
import json
|
|
11
|
-
import os
|
|
12
11
|
import time
|
|
13
12
|
import urllib.parse
|
|
14
13
|
import urllib.error
|
|
@@ -20,6 +19,7 @@ from core import APError, ensure_git_repo, copy_tree, run, load_yaml, find_confi
|
|
|
20
19
|
|
|
21
20
|
|
|
22
21
|
_JENKINS_CRUMB_CACHE: dict[str, dict[str, str]] = {}
|
|
22
|
+
_INVALID_PLACEHOLDERS = {"N/A", "TODO", "TBD", "CHANGEME", "CHANGE_ME", "FILL_ME", "FILL-ME", "PLACEHOLDER", "XXX"}
|
|
23
23
|
|
|
24
24
|
|
|
25
25
|
def _skill_root() -> Path:
|
|
@@ -60,27 +60,21 @@ def _run_configured_command(repo: Path, cfg: dict, name: str) -> bool:
|
|
|
60
60
|
|
|
61
61
|
def _jenkins_basic_auth_headers(cfg: dict) -> dict:
|
|
62
62
|
jenkins_cfg = (cfg.get("jenkins") or {})
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
or os.getenv(password_env)
|
|
75
|
-
or os.getenv("JENKINS_TOKEN")
|
|
76
|
-
or os.getenv("JENKINS_PASSWORD")
|
|
77
|
-
)
|
|
78
|
-
if not user or not token:
|
|
63
|
+
user_candidates = [
|
|
64
|
+
jenkins_cfg.get("api_user"),
|
|
65
|
+
jenkins_cfg.get("ui_username"),
|
|
66
|
+
]
|
|
67
|
+
secret_candidates = [
|
|
68
|
+
jenkins_cfg.get("api_password"),
|
|
69
|
+
jenkins_cfg.get("ui_password"),
|
|
70
|
+
]
|
|
71
|
+
user = next((_text(v) for v in user_candidates if _is_explicit_fill(v)), "")
|
|
72
|
+
secret = next((_text(v) for v in secret_candidates if _is_explicit_fill(v)), "")
|
|
73
|
+
if not user or not secret:
|
|
79
74
|
raise APError(
|
|
80
|
-
"Missing Jenkins API credentials. Fill jenkins.api_user
|
|
81
|
-
f"in docs/ENGINEERING.md, or set env vars {user_env}, {token_env}, {password_env}."
|
|
75
|
+
"Missing Jenkins API credentials. Fill jenkins.api_user and jenkins.api_password in docs/ENGINEERING.md."
|
|
82
76
|
)
|
|
83
|
-
raw = f"{user}:{
|
|
77
|
+
raw = f"{user}:{secret}".encode("utf-8")
|
|
84
78
|
auth = base64.b64encode(raw).decode("ascii")
|
|
85
79
|
return {"Authorization": f"Basic {auth}"}
|
|
86
80
|
|
|
@@ -122,10 +116,6 @@ def _jenkins_root_url(cfg: dict, job_url: str = "") -> str:
|
|
|
122
116
|
|
|
123
117
|
|
|
124
118
|
def _jenkins_crumb_api_url(cfg: dict, job_url: str = "") -> str:
|
|
125
|
-
jenkins_cfg = (cfg.get("jenkins") or {})
|
|
126
|
-
explicit = str(jenkins_cfg.get("crumb_url") or "").strip()
|
|
127
|
-
if explicit:
|
|
128
|
-
return explicit
|
|
129
119
|
root = _jenkins_root_url(cfg, job_url=job_url)
|
|
130
120
|
if not root:
|
|
131
121
|
return ""
|
|
@@ -203,12 +193,12 @@ def _jenkins_api_get_json(url: str, cfg: dict, timeout_s: int = 15, allow_404: b
|
|
|
203
193
|
except Exception as retry_exc:
|
|
204
194
|
raise APError(f"Jenkins API request failed after crumb retry: {url}\n{retry_exc}") from retry_exc
|
|
205
195
|
else:
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
"Fill jenkins.base_url
|
|
211
|
-
|
|
196
|
+
raise APError(
|
|
197
|
+
f"Jenkins API request failed: {url}\n"
|
|
198
|
+
f"HTTP 403\n{body or '(empty response body)'}\n"
|
|
199
|
+
"Jenkins may require crumb/CSRF handling, but no crumb issuer endpoint was available. "
|
|
200
|
+
"Fill jenkins.base_url in docs/ENGINEERING.md if needed."
|
|
201
|
+
) from exc
|
|
212
202
|
else:
|
|
213
203
|
raise APError(
|
|
214
204
|
f"Jenkins API request failed: {url}\n"
|
|
@@ -296,15 +286,12 @@ def _resolve_jenkins_job_url(cfg: dict, job_name: str = "", job_url: str = "") -
|
|
|
296
286
|
jenkins_cfg = (cfg.get("jenkins") or {})
|
|
297
287
|
explicit_url = str(job_url or "").strip()
|
|
298
288
|
requested_name = str(job_name or "").strip()
|
|
299
|
-
configured_name = str(jenkins_cfg.get("job_name") or "").strip()
|
|
300
289
|
configured_url = str(jenkins_cfg.get("job_url") or "").strip()
|
|
301
290
|
base_url = str(jenkins_cfg.get("base_url") or "").strip()
|
|
302
291
|
|
|
303
292
|
if explicit_url:
|
|
304
293
|
return explicit_url.rstrip("/")
|
|
305
294
|
if requested_name:
|
|
306
|
-
if configured_name and requested_name == configured_name and configured_url:
|
|
307
|
-
return configured_url.rstrip("/")
|
|
308
295
|
if base_url:
|
|
309
296
|
return _jenkins_job_url_from_name(base_url, requested_name)
|
|
310
297
|
raise APError(
|
|
@@ -313,11 +300,9 @@ def _resolve_jenkins_job_url(cfg: dict, job_name: str = "", job_url: str = "") -
|
|
|
313
300
|
)
|
|
314
301
|
if configured_url:
|
|
315
302
|
return configured_url.rstrip("/")
|
|
316
|
-
if configured_name and base_url:
|
|
317
|
-
return _jenkins_job_url_from_name(base_url, configured_name)
|
|
318
303
|
raise APError(
|
|
319
|
-
"Missing Jenkins job location. Fill jenkins.job_url
|
|
320
|
-
"
|
|
304
|
+
"Missing Jenkins job location. Fill jenkins.job_url in docs/ENGINEERING.md, "
|
|
305
|
+
"or pass --job-url / --job-name explicitly."
|
|
321
306
|
)
|
|
322
307
|
|
|
323
308
|
|
|
@@ -331,17 +316,16 @@ def _resolve_jenkins_job_candidates(
|
|
|
331
316
|
branch_name: str = "",
|
|
332
317
|
) -> List[str]:
|
|
333
318
|
jenkins_cfg = (cfg.get("jenkins") or {})
|
|
334
|
-
effective_branch = str(branch_name or
|
|
319
|
+
effective_branch = str(branch_name or "").strip()
|
|
335
320
|
if not effective_branch:
|
|
336
321
|
inferred_branch = _resolve_git_branch_name(repo, git_ref or "HEAD")
|
|
337
322
|
if inferred_branch:
|
|
338
323
|
effective_branch = inferred_branch
|
|
339
324
|
|
|
340
|
-
effective_root = str(multibranch_root_job or
|
|
325
|
+
effective_root = str(multibranch_root_job or "").strip()
|
|
341
326
|
explicit_url = str(job_url or "").strip()
|
|
342
327
|
explicit_name = str(job_name or "").strip()
|
|
343
328
|
configured_url = str(jenkins_cfg.get("job_url") or "").strip()
|
|
344
|
-
configured_name = str(jenkins_cfg.get("job_name") or "").strip()
|
|
345
329
|
|
|
346
330
|
if effective_branch:
|
|
347
331
|
if explicit_url:
|
|
@@ -354,12 +338,9 @@ def _resolve_jenkins_job_candidates(
|
|
|
354
338
|
return _jenkins_branch_job_urls(_jenkins_job_url_from_name(base_url, explicit_name), effective_branch)
|
|
355
339
|
if configured_url:
|
|
356
340
|
return _jenkins_branch_job_urls(configured_url, effective_branch)
|
|
357
|
-
if configured_name:
|
|
358
|
-
base_url = str(jenkins_cfg.get("base_url") or "").strip()
|
|
359
|
-
return _jenkins_branch_job_urls(_jenkins_job_url_from_name(base_url, configured_name), effective_branch)
|
|
360
341
|
raise APError(
|
|
361
|
-
"Missing Jenkins multibranch root job location.
|
|
362
|
-
"or pass --
|
|
342
|
+
"Missing Jenkins multibranch root job location. Pass --job-url / --job-name together with "
|
|
343
|
+
"--branch-name, or pass --multibranch-root-job with jenkins.base_url."
|
|
363
344
|
)
|
|
364
345
|
|
|
365
346
|
return [_resolve_jenkins_job_url(cfg, job_name=job_name, job_url=job_url)]
|
|
@@ -404,17 +385,8 @@ def cmd_install(args: argparse.Namespace) -> None:
|
|
|
404
385
|
copy_tree(Path(__file__).resolve().parent / "core.py", tools_dir / "core.py")
|
|
405
386
|
copy_tree(Path(__file__).resolve().parent / "http_checks.py", tools_dir / "http_checks.py")
|
|
406
387
|
|
|
407
|
-
gi = repo / ".gitignore"
|
|
408
|
-
secret_line = "docs/ENGINEERING.md"
|
|
409
|
-
if gi.exists():
|
|
410
|
-
txt = gi.read_text(encoding="utf-8")
|
|
411
|
-
if secret_line not in txt:
|
|
412
|
-
gi.write_text(txt.rstrip() + "\n" + secret_line + "\n", encoding="utf-8")
|
|
413
|
-
else:
|
|
414
|
-
gi.write_text(secret_line + "\n", encoding="utf-8")
|
|
415
|
-
|
|
416
388
|
print(f"[install] OK: scaffold installed into {repo}")
|
|
417
|
-
print("[install] Next: edit docs/ENGINEERING.md frontmatter and
|
|
389
|
+
print("[install] Next: edit docs/ENGINEERING.md frontmatter, fill all platform credentials, and commit that file into Git.")
|
|
418
390
|
|
|
419
391
|
|
|
420
392
|
def _infer_title(taskbook: Path, task_id: str) -> str:
|
|
@@ -568,12 +540,35 @@ def _load_cfg(repo: Path) -> dict:
|
|
|
568
540
|
return load_yaml(cfg_path)
|
|
569
541
|
|
|
570
542
|
|
|
571
|
-
def
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
return
|
|
543
|
+
def _text(value: object) -> str:
|
|
544
|
+
return str(value or "").strip()
|
|
545
|
+
|
|
546
|
+
|
|
547
|
+
def _is_placeholder(value: object) -> bool:
|
|
548
|
+
return _text(value).upper() in _INVALID_PLACEHOLDERS
|
|
549
|
+
|
|
550
|
+
|
|
551
|
+
def _is_explicit_fill(value: object) -> bool:
|
|
552
|
+
return bool(_text(value)) and not _is_placeholder(value)
|
|
553
|
+
|
|
554
|
+
|
|
555
|
+
def _validate_url_field(errors: List[str], field: str, value: object) -> None:
|
|
556
|
+
raw = _text(value)
|
|
557
|
+
parsed = urllib.parse.urlparse(raw)
|
|
558
|
+
if parsed.scheme not in {"http", "https"} or not parsed.netloc:
|
|
559
|
+
errors.append(f"{field} must be a valid http/https URL")
|
|
560
|
+
|
|
561
|
+
|
|
562
|
+
def _validate_path_field(errors: List[str], field: str, value: object) -> None:
|
|
563
|
+
raw = _text(value)
|
|
564
|
+
if not raw.startswith("/"):
|
|
565
|
+
errors.append(f"{field} must start with '/'")
|
|
566
|
+
|
|
567
|
+
|
|
568
|
+
def _require_explicit_field(missing: List[str], field: str, value: object) -> None:
|
|
569
|
+
raw = _text(value)
|
|
570
|
+
if not _is_explicit_fill(raw):
|
|
571
|
+
missing.append(f"{field} (must be explicitly filled, not blank/TODO)")
|
|
577
572
|
|
|
578
573
|
|
|
579
574
|
def _run_git_diff_check(repo: Path, cfg: dict) -> None:
|
|
@@ -611,6 +606,7 @@ def cmd_run(args: argparse.Namespace) -> None:
|
|
|
611
606
|
|
|
612
607
|
def cmd_light_gate(args: argparse.Namespace) -> None:
|
|
613
608
|
repo = Path(args.repo).resolve()
|
|
609
|
+
cmd_doctor(argparse.Namespace(repo=str(repo)))
|
|
614
610
|
cfg = _load_cfg(repo)
|
|
615
611
|
commands = (cfg.get("commands") or {})
|
|
616
612
|
|
|
@@ -770,10 +766,7 @@ def cmd_verify_target(args: argparse.Namespace) -> None:
|
|
|
770
766
|
raise APError(f"Frontend target verification failed: {url} -> {status}\n{body[:400]}")
|
|
771
767
|
checks.append(f"frontend:{url}->{status}")
|
|
772
768
|
|
|
773
|
-
note = str(target_cfg.get("verify_notes") or "").strip()
|
|
774
769
|
summary = ", ".join(checks) if checks else "health-only"
|
|
775
|
-
if note:
|
|
776
|
-
summary = summary + f" | notes={note}"
|
|
777
770
|
print(f"[verify-target] OK: {summary}")
|
|
778
771
|
|
|
779
772
|
|
|
@@ -788,6 +781,8 @@ def cmd_verify_jenkins(args: argparse.Namespace) -> None:
|
|
|
788
781
|
raise APError(f"Jenkinsfile not found: {jenkinsfile}")
|
|
789
782
|
|
|
790
783
|
required = [
|
|
784
|
+
("jenkins.base_url", jenkins_cfg.get("base_url")),
|
|
785
|
+
("jenkins.job_url", jenkins_cfg.get("job_url")),
|
|
791
786
|
("jenkins.trigger_branch", jenkins_cfg.get("trigger_branch")),
|
|
792
787
|
("jenkins.image_repository", jenkins_cfg.get("image_repository")),
|
|
793
788
|
("jenkins.image_tag_strategy", jenkins_cfg.get("image_tag_strategy")),
|
|
@@ -796,15 +791,6 @@ def cmd_verify_jenkins(args: argparse.Namespace) -> None:
|
|
|
796
791
|
("target_env.health_path", target_cfg.get("health_path")),
|
|
797
792
|
]
|
|
798
793
|
missing = [name for name, value in required if not str(value or "").strip()]
|
|
799
|
-
if not str(jenkins_cfg.get("job_url") or "").strip():
|
|
800
|
-
base_url = str(jenkins_cfg.get("base_url") or "").strip()
|
|
801
|
-
job_name = str(jenkins_cfg.get("job_name") or "").strip()
|
|
802
|
-
multibranch_root = str(jenkins_cfg.get("multibranch_root_job") or "").strip()
|
|
803
|
-
if not (base_url and job_name) and not (base_url and multibranch_root):
|
|
804
|
-
missing.append(
|
|
805
|
-
"jenkins.job_url (or jenkins.base_url + jenkins.job_name, "
|
|
806
|
-
"or jenkins.base_url + jenkins.multibranch_root_job)"
|
|
807
|
-
)
|
|
808
794
|
if missing:
|
|
809
795
|
raise APError("Missing Jenkins config: " + ", ".join(missing))
|
|
810
796
|
print(f"[verify-jenkins] OK: {jenkinsfile}")
|
|
@@ -831,27 +817,26 @@ def cmd_doctor(args: argparse.Namespace) -> None:
|
|
|
831
817
|
missing.append("commands.quick_test or commands.test")
|
|
832
818
|
if not (str(commands.get("lint") or "").strip() or str(commands.get("typecheck") or "").strip()):
|
|
833
819
|
missing.append("commands.lint or commands.typecheck")
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
missing.append("jenkins.job_url or (jenkins.base_url + jenkins.job_name) or (jenkins.base_url + jenkins.multibranch_root_job)")
|
|
820
|
+
_require_explicit_field(missing, "target_env.name", target_cfg.get("name"))
|
|
821
|
+
_require_explicit_field(missing, "target_env.frontend_base_url", target_cfg.get("frontend_base_url"))
|
|
822
|
+
_require_explicit_field(missing, "target_env.frontend_username", target_cfg.get("frontend_username"))
|
|
823
|
+
_require_explicit_field(missing, "target_env.frontend_password", target_cfg.get("frontend_password"))
|
|
824
|
+
_require_explicit_field(missing, "target_env.backend_base_url", target_cfg.get("backend_base_url"))
|
|
825
|
+
_require_explicit_field(missing, "target_env.backend_username", target_cfg.get("backend_username"))
|
|
826
|
+
_require_explicit_field(missing, "target_env.backend_password", target_cfg.get("backend_password"))
|
|
827
|
+
_require_explicit_field(missing, "target_env.health_base_url", target_cfg.get("health_base_url"))
|
|
828
|
+
_require_explicit_field(missing, "target_env.health_path", target_cfg.get("health_path"))
|
|
829
|
+
|
|
830
|
+
_require_explicit_field(missing, "jenkins.base_url", jenkins_cfg.get("base_url"))
|
|
831
|
+
_require_explicit_field(missing, "jenkins.ui_username", jenkins_cfg.get("ui_username"))
|
|
832
|
+
_require_explicit_field(missing, "jenkins.ui_password", jenkins_cfg.get("ui_password"))
|
|
833
|
+
_require_explicit_field(missing, "jenkins.job_url", jenkins_cfg.get("job_url"))
|
|
834
|
+
_require_explicit_field(missing, "jenkins.trigger_branch", jenkins_cfg.get("trigger_branch"))
|
|
835
|
+
_require_explicit_field(missing, "jenkins.image_repository", jenkins_cfg.get("image_repository"))
|
|
836
|
+
_require_explicit_field(missing, "jenkins.image_tag_strategy", jenkins_cfg.get("image_tag_strategy"))
|
|
837
|
+
_require_explicit_field(missing, "jenkins.deploy_env", jenkins_cfg.get("deploy_env"))
|
|
838
|
+
_require_explicit_field(missing, "jenkins.api_user", jenkins_cfg.get("api_user"))
|
|
839
|
+
_require_explicit_field(missing, "jenkins.api_password", jenkins_cfg.get("api_password"))
|
|
855
840
|
|
|
856
841
|
repo_docs = {
|
|
857
842
|
"docs.taskbook": Path(repo, str(docs_cfg.get("taskbook", "docs/tasks/taskbook.md"))),
|
|
@@ -863,30 +848,31 @@ def cmd_doctor(args: argparse.Namespace) -> None:
|
|
|
863
848
|
if not path.exists():
|
|
864
849
|
warnings.append(f"{key} missing on disk: {path}")
|
|
865
850
|
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
api_user = str(jenkins_cfg.get("api_user") or "").strip()
|
|
874
|
-
api_secret = str(jenkins_cfg.get("api_token") or jenkins_cfg.get("api_password") or "").strip()
|
|
875
|
-
if _pair_state(api_user, api_secret) == "partial":
|
|
876
|
-
warnings.append("jenkins.api_user with jenkins.api_token/api_password is partial")
|
|
851
|
+
_validate_url_field(warnings, "target_env.frontend_base_url", target_cfg.get("frontend_base_url"))
|
|
852
|
+
_validate_url_field(warnings, "target_env.backend_base_url", target_cfg.get("backend_base_url"))
|
|
853
|
+
_validate_url_field(warnings, "target_env.health_base_url", target_cfg.get("health_base_url"))
|
|
854
|
+
_validate_path_field(warnings, "target_env.health_path", target_cfg.get("health_path"))
|
|
855
|
+
_validate_url_field(warnings, "jenkins.base_url", jenkins_cfg.get("base_url"))
|
|
856
|
+
_validate_url_field(warnings, "jenkins.job_url", jenkins_cfg.get("job_url"))
|
|
877
857
|
|
|
878
858
|
runtime_enabled = any(str(runtime_cfg.get(key) or "").strip() for key in ["docker_compose_file", "docker_service", "health_base_url", "health_path"])
|
|
879
859
|
if runtime_enabled and not (str(commands.get("compose_up") or "").strip() or str(runtime_cfg.get("docker_compose_file") or "").strip()):
|
|
880
860
|
warnings.append("runtime config is partially enabled but compose_up or docker_compose_file is missing")
|
|
881
861
|
|
|
862
|
+
try:
|
|
863
|
+
timeout_s = int(jenkins_cfg.get("deploy_timeout_sec") or 0)
|
|
864
|
+
if timeout_s <= 0:
|
|
865
|
+
warnings.append("jenkins.deploy_timeout_sec must be a positive integer")
|
|
866
|
+
except Exception:
|
|
867
|
+
warnings.append("jenkins.deploy_timeout_sec must be a positive integer")
|
|
868
|
+
|
|
869
|
+
if warnings:
|
|
870
|
+
missing.extend([f"invalid {item}" for item in warnings])
|
|
871
|
+
|
|
882
872
|
if missing:
|
|
883
|
-
raise APError("Doctor found blocking config issues:\n- " + "\n- ".join(missing)
|
|
873
|
+
raise APError("Doctor found blocking config issues:\n- " + "\n- ".join(missing))
|
|
884
874
|
|
|
885
875
|
print("[doctor] OK")
|
|
886
|
-
if warnings:
|
|
887
|
-
print("[doctor] Warnings:")
|
|
888
|
-
for item in warnings:
|
|
889
|
-
print(f"- {item}")
|
|
890
876
|
|
|
891
877
|
|
|
892
878
|
def cmd_verify_jenkins_build(args: argparse.Namespace) -> None:
|
|
@@ -908,12 +894,10 @@ def cmd_verify_jenkins_build(args: argparse.Namespace) -> None:
|
|
|
908
894
|
timeout_s = int(args.timeout_sec or 300)
|
|
909
895
|
poll_s = int(args.poll_sec or 5)
|
|
910
896
|
inferred_branch = _resolve_git_branch_name(repo, git_ref)
|
|
911
|
-
branch_hint = str(args.branch_name or
|
|
897
|
+
branch_hint = str(args.branch_name or inferred_branch or "").strip()
|
|
912
898
|
root_hint = str(
|
|
913
899
|
args.multibranch_root_job
|
|
914
|
-
or jenkins_cfg.get("multibranch_root_job")
|
|
915
900
|
or args.job_name
|
|
916
|
-
or jenkins_cfg.get("job_name")
|
|
917
901
|
or args.job_url
|
|
918
902
|
or jenkins_cfg.get("job_url")
|
|
919
903
|
or ""
|
|
@@ -1064,6 +1048,7 @@ def cmd_record_closure(args: argparse.Namespace) -> None:
|
|
|
1064
1048
|
def cmd_commit_push(args: argparse.Namespace) -> None:
|
|
1065
1049
|
repo = Path(args.repo).resolve()
|
|
1066
1050
|
ensure_git_repo(repo)
|
|
1051
|
+
cmd_doctor(argparse.Namespace(repo=str(repo)))
|
|
1067
1052
|
|
|
1068
1053
|
msg = args.msg
|
|
1069
1054
|
|
package/cli/src/index.js
CHANGED
|
@@ -62,19 +62,6 @@ function resolveTargetDir(ai, mode, destOverride){
|
|
|
62
62
|
die(`unknown ai: ${ai}`);
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
-
function ensureGitignore(projectDir){
|
|
66
|
-
const gi = path.join(projectDir, ".gitignore");
|
|
67
|
-
const line = "docs/ENGINEERING.md";
|
|
68
|
-
if (!exists(gi)) {
|
|
69
|
-
fs.writeFileSync(gi, `${line}\n`, "utf-8");
|
|
70
|
-
return;
|
|
71
|
-
}
|
|
72
|
-
const txt = fs.readFileSync(gi, "utf-8");
|
|
73
|
-
if (!txt.includes(line)) {
|
|
74
|
-
fs.appendFileSync(gi, (txt.endsWith("\n") ? "" : "\n") + line + "\n");
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
|
|
78
65
|
function main(){
|
|
79
66
|
const args = parseArgs(process.argv);
|
|
80
67
|
|
|
@@ -104,8 +91,6 @@ Examples:
|
|
|
104
91
|
const assetSkill = path.resolve(here, "..", "assets", "skill");
|
|
105
92
|
if (!exists(assetSkill)) die(`missing assets at ${assetSkill}`);
|
|
106
93
|
|
|
107
|
-
const proj = projectRoot();
|
|
108
|
-
|
|
109
94
|
for (const t of targets) {
|
|
110
95
|
const dstOverride = args.dest
|
|
111
96
|
? (targets.length > 1 ? path.join(args.dest, t) : args.dest)
|
|
@@ -119,8 +104,6 @@ Examples:
|
|
|
119
104
|
console.log(`[autocoding] installed skill to: ${dst}`);
|
|
120
105
|
}
|
|
121
106
|
|
|
122
|
-
if (args.mode === "project") ensureGitignore(proj);
|
|
123
|
-
|
|
124
107
|
console.log("[autocoding] done.");
|
|
125
108
|
}
|
|
126
109
|
|
package/package.json
CHANGED