sci-agent 0.2.0__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.
Files changed (46) hide show
  1. sci_agent-0.2.0/PKG-INFO +11 -0
  2. sci_agent-0.2.0/README.md +129 -0
  3. sci_agent-0.2.0/pyproject.toml +33 -0
  4. sci_agent-0.2.0/setup.cfg +4 -0
  5. sci_agent-0.2.0/src/sci_agent.egg-info/PKG-INFO +11 -0
  6. sci_agent-0.2.0/src/sci_agent.egg-info/SOURCES.txt +44 -0
  7. sci_agent-0.2.0/src/sci_agent.egg-info/dependency_links.txt +1 -0
  8. sci_agent-0.2.0/src/sci_agent.egg-info/entry_points.txt +2 -0
  9. sci_agent-0.2.0/src/sci_agent.egg-info/requires.txt +6 -0
  10. sci_agent-0.2.0/src/sci_agent.egg-info/top_level.txt +1 -0
  11. sci_agent-0.2.0/src/simple_ci/__init__.py +1 -0
  12. sci_agent-0.2.0/src/simple_ci/__main__.py +42 -0
  13. sci_agent-0.2.0/src/simple_ci/actions/__init__.py +136 -0
  14. sci_agent-0.2.0/src/simple_ci/actions/base.py +318 -0
  15. sci_agent-0.2.0/src/simple_ci/actions/build.py +31 -0
  16. sci_agent-0.2.0/src/simple_ci/actions/deploy.py +23 -0
  17. sci_agent-0.2.0/src/simple_ci/actions/hooks.py +153 -0
  18. sci_agent-0.2.0/src/simple_ci/actions/image.py +28 -0
  19. sci_agent-0.2.0/src/simple_ci/actions/source.py +32 -0
  20. sci_agent-0.2.0/src/simple_ci/actions/target.py +63 -0
  21. sci_agent-0.2.0/src/simple_ci/cli/__init__.py +1 -0
  22. sci_agent-0.2.0/src/simple_ci/cli/client.py +104 -0
  23. sci_agent-0.2.0/src/simple_ci/cli/config.py +276 -0
  24. sci_agent-0.2.0/src/simple_ci/cli/history.py +82 -0
  25. sci_agent-0.2.0/src/simple_ci/cli/main.py +333 -0
  26. sci_agent-0.2.0/src/simple_ci/cli/render.py +111 -0
  27. sci_agent-0.2.0/src/simple_ci/errors.py +37 -0
  28. sci_agent-0.2.0/src/simple_ci/models.py +134 -0
  29. sci_agent-0.2.0/src/simple_ci/server/__init__.py +1 -0
  30. sci_agent-0.2.0/src/simple_ci/server/app.py +118 -0
  31. sci_agent-0.2.0/src/simple_ci/server/auth.py +40 -0
  32. sci_agent-0.2.0/src/simple_ci/server/executor.py +304 -0
  33. sci_agent-0.2.0/src/simple_ci/server/main.py +37 -0
  34. sci_agent-0.2.0/src/simple_ci/server/store.py +152 -0
  35. sci_agent-0.2.0/src/simple_ci/ui/__init__.py +1 -0
  36. sci_agent-0.2.0/src/simple_ci/ui/server.py +285 -0
  37. sci_agent-0.2.0/src/simple_ci/ui/static/index.html +271 -0
  38. sci_agent-0.2.0/tests/test_actions_commands.py +539 -0
  39. sci_agent-0.2.0/tests/test_cli.py +905 -0
  40. sci_agent-0.2.0/tests/test_config_loader.py +408 -0
  41. sci_agent-0.2.0/tests/test_executor.py +465 -0
  42. sci_agent-0.2.0/tests/test_history.py +108 -0
  43. sci_agent-0.2.0/tests/test_hooks.py +133 -0
  44. sci_agent-0.2.0/tests/test_render.py +134 -0
  45. sci_agent-0.2.0/tests/test_server_api.py +353 -0
  46. sci_agent-0.2.0/tests/test_version.py +5 -0
@@ -0,0 +1,11 @@
1
+ Metadata-Version: 2.4
2
+ Name: sci-agent
3
+ Version: 0.2.0
4
+ Summary: Agent-first lightweight CI/CD tool
5
+ Requires-Python: >=3.12
6
+ Requires-Dist: fastapi>=0.110.0
7
+ Requires-Dist: uvicorn>=0.29.0
8
+ Requires-Dist: httpx>=0.27.0
9
+ Requires-Dist: pyyaml>=6.0
10
+ Requires-Dist: jinja2>=3.1.0
11
+ Requires-Dist: python-dotenv>=1.0.0
@@ -0,0 +1,129 @@
1
+ # sci-agent
2
+
3
+ sci-agent (`sci-agent`) is an agent-first lightweight CI/CD tool. It can run pipelines from the command line, expose a protected target execution server, and provide a local Web UI for browsing configuration and run history.
4
+
5
+ ## Quick start
6
+
7
+ Open the local Web UI from a workspace that contains `sci/`:
8
+
9
+ ```powershell
10
+ uvx --from sci-agent sci-agent
11
+ ```
12
+
13
+ Run the example `ai-threat` release pipeline on target `10-50-10-18`:
14
+
15
+ ```powershell
16
+ uvx --from sci-agent sci-agent run --env 10-50-10-18 --project ai-threat --pipeline server-openapi --yes
17
+ ```
18
+
19
+ Start a target execution server:
20
+
21
+ ```powershell
22
+ uvx --from sci-agent sci-agent server --port 9090 --token xxx
23
+ ```
24
+
25
+ For production-like usage, prefer `SCI_AGENT_TOKEN` over passing the token on the command line.
26
+
27
+ ## Configuration layout
28
+
29
+ A workspace is configured with a `sci/` directory and an optional root `.env` file:
30
+
31
+ ```text
32
+ .
33
+ ├── .env
34
+ ├── .sci-agent/
35
+ │ └── history.jsonl
36
+ └── sci/
37
+ ├── config.toml
38
+ ├── targets/
39
+ │ └── 10-50-10-18.toml
40
+ └── projects/
41
+ └── ai-threat/
42
+ ├── config.toml
43
+ └── pipeline/
44
+ └── server-openapi/
45
+ ├── config.toml
46
+ └── pipeline.yml
47
+ ```
48
+
49
+ IDs are derived from paths:
50
+
51
+ - Target ID: `sci/targets/10-50-10-18.toml` -> `10-50-10-18`.
52
+ - Project ID: `sci/projects/ai-threat/` -> `ai-threat`.
53
+ - Pipeline ID: `sci/projects/ai-threat/pipeline/server-openapi/` -> `server-openapi`.
54
+
55
+ ## Git credential model
56
+
57
+ Git credentials are split between non-secret configuration and secret environment values.
58
+
59
+ `sci/config.toml` defines credential IDs and which environment variables contain the secret values:
60
+
61
+ ```toml
62
+ [credentials.gitlab-ai-threat]
63
+ type = "git_http_basic"
64
+ username_env = "GITLAB_AI_THREAT_USERNAME"
65
+ password_env = "GITLAB_AI_THREAT_PASSWORD"
66
+ ```
67
+
68
+ The root `.env` file stores the actual username and password:
69
+
70
+ ```dotenv
71
+ GITLAB_AI_THREAT_USERNAME=your-user
72
+ GITLAB_AI_THREAT_PASSWORD=your-password-or-token
73
+ SERVER_10_50_10_18_URL=http://10.50.10.18:9090
74
+ SERVER_10_50_10_18_TOKEN=xxx
75
+ ```
76
+
77
+ The project config references the credential ID, not the secret values:
78
+
79
+ ```toml
80
+ [source]
81
+ repo = "https://gitlab.example.com/ai-threat/ai-threat.git"
82
+ branch = "release"
83
+ credential = "gitlab-ai-threat"
84
+ ```
85
+
86
+ ## Running pipelines
87
+
88
+ Inspect the resolved plan without executing it:
89
+
90
+ ```powershell
91
+ uvx --from sci-agent sci-agent run --env 10-50-10-18 --project ai-threat --pipeline server-openapi --dry-run
92
+ ```
93
+
94
+ Execute without interactive confirmation:
95
+
96
+ ```powershell
97
+ uvx --from sci-agent sci-agent run --env 10-50-10-18 --project ai-threat --pipeline server-openapi --yes
98
+ ```
99
+
100
+ View local run status and logs:
101
+
102
+ ```powershell
103
+ uvx --from sci-agent sci-agent run --status
104
+ uvx --from sci-agent sci-agent run --status run-xxxxxxxxxxxx
105
+ uvx --from sci-agent sci-agent run --logs run-xxxxxxxxxxxx
106
+ ```
107
+
108
+ Dry-run output, confirmation summaries, logs, Web UI responses, and history records are sanitized before display or persistence. Tokens, passwords, credentials, and known secret values are redacted.
109
+
110
+ ## Web UI, history, and streaming
111
+
112
+ - `uvx --from sci-agent sci-agent` starts a local Web UI for browsing targets, projects, pipelines, and `.sci-agent/history.jsonl`.
113
+ - Web UI APIs return sanitized configuration and sanitized history only.
114
+ - Agents use polling when waiting for target server runs, which keeps CLI behavior simple and resilient.
115
+ - The Web UI can use Server-Sent Events (SSE) for run detail updates via the local UI streaming endpoint.
116
+
117
+ ## Publishing
118
+
119
+ Copy `.env.publish.example` to `.env.publish`, fill in `UV_PUBLISH_TOKEN`, then run:
120
+
121
+ ```powershell
122
+ .\scripts\publish.ps1
123
+ ```
124
+
125
+ Build only without requiring a publish token:
126
+
127
+ ```powershell
128
+ .\scripts\publish.ps1 -BuildOnly
129
+ ```
@@ -0,0 +1,33 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "sci-agent"
7
+ version = "0.2.0"
8
+ description = "Agent-first lightweight CI/CD tool"
9
+ requires-python = ">=3.12"
10
+ dependencies = [
11
+ "fastapi>=0.110.0",
12
+ "uvicorn>=0.29.0",
13
+ "httpx>=0.27.0",
14
+ "pyyaml>=6.0",
15
+ "jinja2>=3.1.0",
16
+ "python-dotenv>=1.0.0",
17
+ ]
18
+
19
+ [dependency-groups]
20
+ dev = [
21
+ "pytest>=8.0.0",
22
+ "pytest-asyncio>=0.23.0",
23
+ ]
24
+
25
+ [project.scripts]
26
+ sci-agent = "simple_ci.__main__:main"
27
+
28
+ [tool.setuptools.packages.find]
29
+ where = ["src"]
30
+ include = ["simple_ci*"]
31
+
32
+ [tool.setuptools.package-data]
33
+ simple_ci = ["ui/static/*.html", "ui/static/*.js", "ui/static/*.css"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,11 @@
1
+ Metadata-Version: 2.4
2
+ Name: sci-agent
3
+ Version: 0.2.0
4
+ Summary: Agent-first lightweight CI/CD tool
5
+ Requires-Python: >=3.12
6
+ Requires-Dist: fastapi>=0.110.0
7
+ Requires-Dist: uvicorn>=0.29.0
8
+ Requires-Dist: httpx>=0.27.0
9
+ Requires-Dist: pyyaml>=6.0
10
+ Requires-Dist: jinja2>=3.1.0
11
+ Requires-Dist: python-dotenv>=1.0.0
@@ -0,0 +1,44 @@
1
+ README.md
2
+ pyproject.toml
3
+ src/sci_agent.egg-info/PKG-INFO
4
+ src/sci_agent.egg-info/SOURCES.txt
5
+ src/sci_agent.egg-info/dependency_links.txt
6
+ src/sci_agent.egg-info/entry_points.txt
7
+ src/sci_agent.egg-info/requires.txt
8
+ src/sci_agent.egg-info/top_level.txt
9
+ src/simple_ci/__init__.py
10
+ src/simple_ci/__main__.py
11
+ src/simple_ci/errors.py
12
+ src/simple_ci/models.py
13
+ src/simple_ci/actions/__init__.py
14
+ src/simple_ci/actions/base.py
15
+ src/simple_ci/actions/build.py
16
+ src/simple_ci/actions/deploy.py
17
+ src/simple_ci/actions/hooks.py
18
+ src/simple_ci/actions/image.py
19
+ src/simple_ci/actions/source.py
20
+ src/simple_ci/actions/target.py
21
+ src/simple_ci/cli/__init__.py
22
+ src/simple_ci/cli/client.py
23
+ src/simple_ci/cli/config.py
24
+ src/simple_ci/cli/history.py
25
+ src/simple_ci/cli/main.py
26
+ src/simple_ci/cli/render.py
27
+ src/simple_ci/server/__init__.py
28
+ src/simple_ci/server/app.py
29
+ src/simple_ci/server/auth.py
30
+ src/simple_ci/server/executor.py
31
+ src/simple_ci/server/main.py
32
+ src/simple_ci/server/store.py
33
+ src/simple_ci/ui/__init__.py
34
+ src/simple_ci/ui/server.py
35
+ src/simple_ci/ui/static/index.html
36
+ tests/test_actions_commands.py
37
+ tests/test_cli.py
38
+ tests/test_config_loader.py
39
+ tests/test_executor.py
40
+ tests/test_history.py
41
+ tests/test_hooks.py
42
+ tests/test_render.py
43
+ tests/test_server_api.py
44
+ tests/test_version.py
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ sci-agent = simple_ci.__main__:main
@@ -0,0 +1,6 @@
1
+ fastapi>=0.110.0
2
+ uvicorn>=0.29.0
3
+ httpx>=0.27.0
4
+ pyyaml>=6.0
5
+ jinja2>=3.1.0
6
+ python-dotenv>=1.0.0
@@ -0,0 +1 @@
1
+ simple_ci
@@ -0,0 +1 @@
1
+ __version__ = "0.2.0"
@@ -0,0 +1,42 @@
1
+ from __future__ import annotations
2
+
3
+ import ctypes
4
+ import sys
5
+
6
+
7
+ def _configure_stdio_encoding() -> None:
8
+ if sys.platform == "win32":
9
+ try:
10
+ kernel32 = ctypes.windll.kernel32
11
+ kernel32.SetConsoleOutputCP(65001)
12
+ kernel32.SetConsoleCP(65001)
13
+ except Exception:
14
+ pass
15
+
16
+ for stream in (sys.stdout, sys.stderr, sys.stdin):
17
+ try:
18
+ stream.reconfigure(encoding="utf-8", errors="replace")
19
+ except Exception:
20
+ pass
21
+
22
+
23
+ def main() -> None:
24
+ _configure_stdio_encoding()
25
+
26
+ argv = sys.argv[1:]
27
+ if argv and argv[0] == "server":
28
+ from simple_ci.server.main import handle_server
29
+
30
+ handle_server(argv[1:])
31
+ elif argv and argv[0] == "run":
32
+ from simple_ci.cli.main import handle_run
33
+
34
+ handle_run(argv[1:])
35
+ else:
36
+ from simple_ci.ui.server import run_ui
37
+
38
+ run_ui(argv)
39
+
40
+
41
+ if __name__ == "__main__":
42
+ main()
@@ -0,0 +1,136 @@
1
+ """内置动作入口与调度注册表。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Callable
6
+ from typing import Any
7
+
8
+ from simple_ci.actions import base
9
+ from simple_ci.actions.build import build_maven_command
10
+ from simple_ci.actions.deploy import build_compose_command
11
+ from simple_ci.actions.image import build_image_command, build_push_command
12
+ from simple_ci.actions.source import build_git_pull_command
13
+ from simple_ci.actions.target import build_target_shell_command
14
+
15
+ ActionHandler = Callable[[dict[str, Any], dict[str, Any]], base.ActionResult]
16
+
17
+
18
+ def dispatch_action(step: dict[str, Any], payload: dict[str, Any]) -> dict[str, Any]:
19
+ """根据 step.uses 分发内置动作,并返回可 JSON 序列化的动作结果。"""
20
+
21
+ action_name = str(step.get("uses") or step.get("action") or "")
22
+ handler = ACTION_REGISTRY.get(action_name)
23
+ if handler is None:
24
+ return base.ActionResult(
25
+ rendered_command="",
26
+ exit_code=1,
27
+ stderr_tail=f"Unknown action: {action_name or '<missing>'}",
28
+ ).to_dict()
29
+ return handler(step, payload).to_dict()
30
+
31
+
32
+ def _params(step: dict[str, Any]) -> dict[str, Any]:
33
+ """兼容配置文件中的 with 字段与 Python 调用中的 with_ 字段。"""
34
+
35
+ value = step.get("with", step.get("with_", {}))
36
+ return dict(value) if isinstance(value, dict) else {}
37
+
38
+
39
+ def _run_built_command(
40
+ command: list[str] | list[list[str]] | str,
41
+ params: dict[str, Any],
42
+ *,
43
+ allow_shell: bool = False,
44
+ ) -> base.ActionResult:
45
+ """统一执行命令并注入动作参数中的敏感值用于结果脱敏。"""
46
+
47
+ return base.run_command(
48
+ command,
49
+ cwd=params.get("cwd") or params.get("workdir"),
50
+ env=params.get("env"),
51
+ secret_values=base.collect_secret_values(params),
52
+ timeout_seconds=float(params.get("timeout_seconds", 300)),
53
+ allow_shell=allow_shell,
54
+ )
55
+
56
+
57
+ def _source_git_pull(step: dict[str, Any], _payload: dict[str, Any]) -> base.ActionResult:
58
+ params = _params(step)
59
+ if isinstance(params.get("credential"), str):
60
+ message = "Unresolved credential id for source.git_pull; CLI must send a credential dict."
61
+ return base.ActionResult(rendered_command="", exit_code=1, stderr_tail=message, error=message)
62
+ return _run_built_command(build_git_pull_command(params), params)
63
+
64
+
65
+ def _build_maven(step: dict[str, Any], _payload: dict[str, Any]) -> base.ActionResult:
66
+ params = _params(step)
67
+ return _run_built_command(build_maven_command(params), params)
68
+
69
+
70
+ def _shell_action(step: dict[str, Any], _payload: dict[str, Any]) -> base.ActionResult:
71
+ params = _params(step)
72
+ command = params.get("command") or step.get("command")
73
+ if not isinstance(command, (str, list)):
74
+ return base.ActionResult(rendered_command="", exit_code=1, stderr_tail="Missing shell command.")
75
+ return _run_built_command(command, params, allow_shell=True)
76
+
77
+
78
+ def _target_shell(step: dict[str, Any], payload: dict[str, Any]) -> base.ActionResult:
79
+ """执行目标脚本或 inline 命令;script 模式使用 argv,inline 字符串保留 shell 能力。"""
80
+
81
+ params = _params(step)
82
+ try:
83
+ command = build_target_shell_command(params, payload)
84
+ except ValueError as exc:
85
+ return base.ActionResult(rendered_command="", exit_code=1, stderr_tail=str(exc), error=str(exc))
86
+ return _run_built_command(command, params, allow_shell=isinstance(command, str))
87
+
88
+
89
+ def _image_build(step: dict[str, Any], _payload: dict[str, Any]) -> base.ActionResult:
90
+ params = _params(step)
91
+ result = _run_built_command(build_image_command(params), params)
92
+ if result.exit_code == 0:
93
+ result.image_tag = f"{params['image']}:{params.get('tag', 'latest')}"
94
+ return result
95
+
96
+
97
+ def _image_push(step: dict[str, Any], _payload: dict[str, Any]) -> base.ActionResult:
98
+ params = _params(step)
99
+ return _run_built_command(build_push_command(params), params)
100
+
101
+
102
+ def _deploy_compose(step: dict[str, Any], _payload: dict[str, Any]) -> base.ActionResult:
103
+ params = _params(step)
104
+ return _run_built_command(build_compose_command(params), params)
105
+
106
+
107
+ def _deploy_healthcheck(step: dict[str, Any], _payload: dict[str, Any]) -> base.ActionResult:
108
+ """执行 HTTP 健康检查;失败时以非零退出码进入流水线短路。"""
109
+
110
+ params = _params(step)
111
+ return base.run_healthcheck(
112
+ str(params.get("url", "")),
113
+ timeout=float(params.get("timeout", 10)),
114
+ expected_status=int(params.get("expected_status", 200)),
115
+ retries=int(params.get("retries", 0)),
116
+ interval_seconds=float(params.get("interval_seconds", 0)),
117
+ expect_body_contains=(
118
+ str(params["expect_body_contains"]) if params.get("expect_body_contains") is not None else None
119
+ ),
120
+ secret_values=base.collect_secret_values(params),
121
+ )
122
+
123
+
124
+ ACTION_REGISTRY: dict[str, ActionHandler] = {
125
+ "source.git_pull": _source_git_pull,
126
+ "build.maven": _build_maven,
127
+ "build.shell": _shell_action,
128
+ "image.build": _image_build,
129
+ "image.push": _image_push,
130
+ "deploy.compose": _deploy_compose,
131
+ "deploy.healthcheck": _deploy_healthcheck,
132
+ "deploy.shell": _shell_action,
133
+ "target.shell": _target_shell,
134
+ }
135
+
136
+ __all__ = ["ACTION_REGISTRY", "dispatch_action"]