sci-agent 0.2.0__tar.gz → 0.3.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.
- {sci_agent-0.2.0 → sci_agent-0.3.0}/PKG-INFO +2 -1
- {sci_agent-0.2.0 → sci_agent-0.3.0}/README.md +60 -0
- {sci_agent-0.2.0 → sci_agent-0.3.0}/pyproject.toml +3 -2
- {sci_agent-0.2.0 → sci_agent-0.3.0}/src/sci_agent.egg-info/PKG-INFO +2 -1
- {sci_agent-0.2.0 → sci_agent-0.3.0}/src/sci_agent.egg-info/SOURCES.txt +4 -0
- {sci_agent-0.2.0 → sci_agent-0.3.0}/src/sci_agent.egg-info/requires.txt +1 -0
- sci_agent-0.3.0/src/simple_ci/__init__.py +1 -0
- sci_agent-0.3.0/src/simple_ci/__main__.py +74 -0
- {sci_agent-0.2.0 → sci_agent-0.3.0}/src/simple_ci/actions/__init__.py +77 -19
- {sci_agent-0.2.0 → sci_agent-0.3.0}/src/simple_ci/actions/base.py +24 -0
- {sci_agent-0.2.0 → sci_agent-0.3.0}/src/simple_ci/cli/client.py +46 -0
- {sci_agent-0.2.0 → sci_agent-0.3.0}/src/simple_ci/cli/config.py +22 -1
- sci_agent-0.3.0/src/simple_ci/cli/tools.py +182 -0
- {sci_agent-0.2.0 → sci_agent-0.3.0}/src/simple_ci/models.py +16 -0
- {sci_agent-0.2.0 → sci_agent-0.3.0}/src/simple_ci/server/app.py +10 -4
- {sci_agent-0.2.0 → sci_agent-0.3.0}/src/simple_ci/server/executor.py +18 -3
- {sci_agent-0.2.0 → sci_agent-0.3.0}/src/simple_ci/server/main.py +15 -1
- sci_agent-0.3.0/src/simple_ci/server/tools.py +548 -0
- {sci_agent-0.2.0 → sci_agent-0.3.0}/tests/test_actions_commands.py +121 -0
- {sci_agent-0.2.0 → sci_agent-0.3.0}/tests/test_cli.py +118 -1
- {sci_agent-0.2.0 → sci_agent-0.3.0}/tests/test_config_loader.py +106 -0
- {sci_agent-0.2.0 → sci_agent-0.3.0}/tests/test_executor.py +38 -0
- sci_agent-0.3.0/tests/test_server_api.py +708 -0
- sci_agent-0.3.0/tests/test_server_tools.py +350 -0
- sci_agent-0.3.0/tests/test_tools_cli.py +594 -0
- {sci_agent-0.2.0 → sci_agent-0.3.0}/tests/test_version.py +1 -1
- sci_agent-0.2.0/src/simple_ci/__init__.py +0 -1
- sci_agent-0.2.0/src/simple_ci/__main__.py +0 -42
- sci_agent-0.2.0/tests/test_server_api.py +0 -353
- {sci_agent-0.2.0 → sci_agent-0.3.0}/setup.cfg +0 -0
- {sci_agent-0.2.0 → sci_agent-0.3.0}/src/sci_agent.egg-info/dependency_links.txt +0 -0
- {sci_agent-0.2.0 → sci_agent-0.3.0}/src/sci_agent.egg-info/entry_points.txt +0 -0
- {sci_agent-0.2.0 → sci_agent-0.3.0}/src/sci_agent.egg-info/top_level.txt +0 -0
- {sci_agent-0.2.0 → sci_agent-0.3.0}/src/simple_ci/actions/build.py +0 -0
- {sci_agent-0.2.0 → sci_agent-0.3.0}/src/simple_ci/actions/deploy.py +0 -0
- {sci_agent-0.2.0 → sci_agent-0.3.0}/src/simple_ci/actions/hooks.py +0 -0
- {sci_agent-0.2.0 → sci_agent-0.3.0}/src/simple_ci/actions/image.py +0 -0
- {sci_agent-0.2.0 → sci_agent-0.3.0}/src/simple_ci/actions/source.py +0 -0
- {sci_agent-0.2.0 → sci_agent-0.3.0}/src/simple_ci/actions/target.py +0 -0
- {sci_agent-0.2.0 → sci_agent-0.3.0}/src/simple_ci/cli/__init__.py +0 -0
- {sci_agent-0.2.0 → sci_agent-0.3.0}/src/simple_ci/cli/history.py +0 -0
- {sci_agent-0.2.0 → sci_agent-0.3.0}/src/simple_ci/cli/main.py +0 -0
- {sci_agent-0.2.0 → sci_agent-0.3.0}/src/simple_ci/cli/render.py +0 -0
- {sci_agent-0.2.0 → sci_agent-0.3.0}/src/simple_ci/errors.py +0 -0
- {sci_agent-0.2.0 → sci_agent-0.3.0}/src/simple_ci/server/__init__.py +0 -0
- {sci_agent-0.2.0 → sci_agent-0.3.0}/src/simple_ci/server/auth.py +0 -0
- {sci_agent-0.2.0 → sci_agent-0.3.0}/src/simple_ci/server/store.py +0 -0
- {sci_agent-0.2.0 → sci_agent-0.3.0}/src/simple_ci/ui/__init__.py +0 -0
- {sci_agent-0.2.0 → sci_agent-0.3.0}/src/simple_ci/ui/server.py +0 -0
- {sci_agent-0.2.0 → sci_agent-0.3.0}/src/simple_ci/ui/static/index.html +0 -0
- {sci_agent-0.2.0 → sci_agent-0.3.0}/tests/test_history.py +0 -0
- {sci_agent-0.2.0 → sci_agent-0.3.0}/tests/test_hooks.py +0 -0
- {sci_agent-0.2.0 → sci_agent-0.3.0}/tests/test_render.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sci-agent
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: Agent-first lightweight CI/CD tool
|
|
5
5
|
Requires-Python: >=3.12
|
|
6
6
|
Requires-Dist: fastapi>=0.110.0
|
|
@@ -8,4 +8,5 @@ Requires-Dist: uvicorn>=0.29.0
|
|
|
8
8
|
Requires-Dist: httpx>=0.27.0
|
|
9
9
|
Requires-Dist: pyyaml>=6.0
|
|
10
10
|
Requires-Dist: jinja2>=3.1.0
|
|
11
|
+
Requires-Dist: python-multipart>=0.0.20
|
|
11
12
|
Requires-Dist: python-dotenv>=1.0.0
|
|
@@ -23,6 +23,19 @@ uvx --from sci-agent sci-agent server --port 9090 --token xxx
|
|
|
23
23
|
```
|
|
24
24
|
|
|
25
25
|
For production-like usage, prefer `SCI_AGENT_TOKEN` over passing the token on the command line.
|
|
26
|
+
Set the target server workspace base directory with `--base-dir`:
|
|
27
|
+
|
|
28
|
+
```powershell
|
|
29
|
+
uvx --from sci-agent sci-agent server --port 9090 --token xxx --base-dir /opt/sci-agent
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Or with an environment variable:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
SCI_AGENT_BASE_DIR=/opt/sci-agent sci-agent server --port 9090 --token xxx
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
The priority is: `--base-dir > SCI_AGENT_BASE_DIR > current working directory`.
|
|
26
39
|
|
|
27
40
|
## Configuration layout
|
|
28
41
|
|
|
@@ -83,6 +96,53 @@ branch = "release"
|
|
|
83
96
|
credential = "gitlab-ai-threat"
|
|
84
97
|
```
|
|
85
98
|
|
|
99
|
+
## Target server tools
|
|
100
|
+
|
|
101
|
+
The target server can store uploaded tool archives, extract them under the server `base_dir`, maintain a manifest, and return stable paths relative to `<base_dir>`. This is useful for tools such as Maven or JDK that pipelines need to reference explicitly.
|
|
102
|
+
|
|
103
|
+
For `base_dir=/opt/sci-agent`, the server-managed layout is:
|
|
104
|
+
|
|
105
|
+
```text
|
|
106
|
+
/opt/sci-agent/
|
|
107
|
+
└── tools/
|
|
108
|
+
├── packages/
|
|
109
|
+
│ └── apache-maven-3.9.9-bin.tar.gz
|
|
110
|
+
├── installed/
|
|
111
|
+
│ ├── maven/
|
|
112
|
+
│ └── jdk/
|
|
113
|
+
└── manifest.json
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
Tool configuration belongs in the local target TOML. Do not put server `base_dir` in target tool configuration; the server decides `base_dir` only at startup.
|
|
117
|
+
|
|
118
|
+
```toml
|
|
119
|
+
[tools.maven]
|
|
120
|
+
archive = "packages/apache-maven-3.9.9-bin.tar.gz"
|
|
121
|
+
strip_root = true
|
|
122
|
+
|
|
123
|
+
[tools.maven.bins]
|
|
124
|
+
mvn = "bin/mvn"
|
|
125
|
+
|
|
126
|
+
[tools.jdk]
|
|
127
|
+
archive = "packages/jdk-8u333-linux-x64.tar.gz"
|
|
128
|
+
strip_root = true
|
|
129
|
+
|
|
130
|
+
[tools.jdk.bins]
|
|
131
|
+
java = "bin/java"
|
|
132
|
+
javac = "bin/javac"
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
Upload, list, and remove tools:
|
|
136
|
+
|
|
137
|
+
```powershell
|
|
138
|
+
uvx --from sci-agent sci-agent tools list --env 10-50-10-18
|
|
139
|
+
uvx --from sci-agent sci-agent tools upload --env 10-50-10-18 --tool maven
|
|
140
|
+
uvx --from sci-agent sci-agent tools upload --env 10-50-10-18 --all
|
|
141
|
+
uvx --from sci-agent sci-agent tools remove --env 10-50-10-18 --tool maven --purge-package
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
This feature does not automatically inject `PATH`, `JAVA_HOME`, or `MAVEN_HOME`. Pipelines should explicitly reference returned paths such as `tools/installed/maven/bin/mvn` or configure their own environment variables in pipeline steps.
|
|
145
|
+
|
|
86
146
|
## Running pipelines
|
|
87
147
|
|
|
88
148
|
Inspect the resolved plan without executing it:
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "sci-agent"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.3.0"
|
|
8
8
|
description = "Agent-first lightweight CI/CD tool"
|
|
9
9
|
requires-python = ">=3.12"
|
|
10
10
|
dependencies = [
|
|
@@ -13,6 +13,7 @@ dependencies = [
|
|
|
13
13
|
"httpx>=0.27.0",
|
|
14
14
|
"pyyaml>=6.0",
|
|
15
15
|
"jinja2>=3.1.0",
|
|
16
|
+
"python-multipart>=0.0.20",
|
|
16
17
|
"python-dotenv>=1.0.0",
|
|
17
18
|
]
|
|
18
19
|
|
|
@@ -30,4 +31,4 @@ where = ["src"]
|
|
|
30
31
|
include = ["simple_ci*"]
|
|
31
32
|
|
|
32
33
|
[tool.setuptools.package-data]
|
|
33
|
-
simple_ci = ["ui/static/*.html", "ui/static/*.js", "ui/static/*.css"]
|
|
34
|
+
simple_ci = ["ui/static/*.html", "ui/static/*.js", "ui/static/*.css"]
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sci-agent
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: Agent-first lightweight CI/CD tool
|
|
5
5
|
Requires-Python: >=3.12
|
|
6
6
|
Requires-Dist: fastapi>=0.110.0
|
|
@@ -8,4 +8,5 @@ Requires-Dist: uvicorn>=0.29.0
|
|
|
8
8
|
Requires-Dist: httpx>=0.27.0
|
|
9
9
|
Requires-Dist: pyyaml>=6.0
|
|
10
10
|
Requires-Dist: jinja2>=3.1.0
|
|
11
|
+
Requires-Dist: python-multipart>=0.0.20
|
|
11
12
|
Requires-Dist: python-dotenv>=1.0.0
|
|
@@ -24,12 +24,14 @@ src/simple_ci/cli/config.py
|
|
|
24
24
|
src/simple_ci/cli/history.py
|
|
25
25
|
src/simple_ci/cli/main.py
|
|
26
26
|
src/simple_ci/cli/render.py
|
|
27
|
+
src/simple_ci/cli/tools.py
|
|
27
28
|
src/simple_ci/server/__init__.py
|
|
28
29
|
src/simple_ci/server/app.py
|
|
29
30
|
src/simple_ci/server/auth.py
|
|
30
31
|
src/simple_ci/server/executor.py
|
|
31
32
|
src/simple_ci/server/main.py
|
|
32
33
|
src/simple_ci/server/store.py
|
|
34
|
+
src/simple_ci/server/tools.py
|
|
33
35
|
src/simple_ci/ui/__init__.py
|
|
34
36
|
src/simple_ci/ui/server.py
|
|
35
37
|
src/simple_ci/ui/static/index.html
|
|
@@ -41,4 +43,6 @@ tests/test_history.py
|
|
|
41
43
|
tests/test_hooks.py
|
|
42
44
|
tests/test_render.py
|
|
43
45
|
tests/test_server_api.py
|
|
46
|
+
tests/test_server_tools.py
|
|
47
|
+
tests/test_tools_cli.py
|
|
44
48
|
tests/test_version.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.3.0"
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import ctypes
|
|
4
|
+
import sys
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
TOP_LEVEL_HELP = """usage: sci-agent [-h] [--root ROOT] [--host HOST] [--port PORT] [--open]
|
|
8
|
+
sci-agent run [-h] [--env ENV] [--project PROJECT] [--pipeline PIPELINE] ...
|
|
9
|
+
sci-agent server [-h] [--host HOST] [--port PORT] [--token TOKEN]
|
|
10
|
+
sci-agent tools [-h] {list,upload,remove} ...
|
|
11
|
+
|
|
12
|
+
Agent-first lightweight CI/CD tool.
|
|
13
|
+
|
|
14
|
+
commands:
|
|
15
|
+
run Run CI pipelines, inspect configuration, and view local history.
|
|
16
|
+
server Run the target execution API server.
|
|
17
|
+
tools Manage target-side tool packages.
|
|
18
|
+
|
|
19
|
+
default:
|
|
20
|
+
Without a command, sci-agent starts the local Web UI.
|
|
21
|
+
|
|
22
|
+
common examples:
|
|
23
|
+
sci-agent
|
|
24
|
+
sci-agent run --targets
|
|
25
|
+
sci-agent run --env 10-50-10-18 --project ai-threat --pipeline server-openapi --yes
|
|
26
|
+
sci-agent server --host 0.0.0.0 --port 9090 --token <token>
|
|
27
|
+
sci-agent tools list --env 10-50-10-18
|
|
28
|
+
|
|
29
|
+
Use "sci-agent run --help", "sci-agent server --help", or "sci-agent tools --help" for command-specific options.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _configure_stdio_encoding() -> None:
|
|
34
|
+
if sys.platform == "win32":
|
|
35
|
+
try:
|
|
36
|
+
kernel32 = ctypes.windll.kernel32
|
|
37
|
+
kernel32.SetConsoleOutputCP(65001)
|
|
38
|
+
kernel32.SetConsoleCP(65001)
|
|
39
|
+
except Exception:
|
|
40
|
+
pass
|
|
41
|
+
|
|
42
|
+
for stream in (sys.stdout, sys.stderr, sys.stdin):
|
|
43
|
+
try:
|
|
44
|
+
stream.reconfigure(encoding="utf-8", errors="replace")
|
|
45
|
+
except Exception:
|
|
46
|
+
pass
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def main() -> None:
|
|
50
|
+
_configure_stdio_encoding()
|
|
51
|
+
|
|
52
|
+
argv = sys.argv[1:]
|
|
53
|
+
if argv and argv[0] in {"-h", "--help"}:
|
|
54
|
+
print(TOP_LEVEL_HELP)
|
|
55
|
+
elif argv and argv[0] == "server":
|
|
56
|
+
from simple_ci.server.main import handle_server
|
|
57
|
+
|
|
58
|
+
handle_server(argv[1:])
|
|
59
|
+
elif argv and argv[0] == "run":
|
|
60
|
+
from simple_ci.cli.main import handle_run
|
|
61
|
+
|
|
62
|
+
handle_run(argv[1:])
|
|
63
|
+
elif argv and argv[0] == "tools":
|
|
64
|
+
from simple_ci.cli.tools import handle_tools
|
|
65
|
+
|
|
66
|
+
handle_tools(argv[1:])
|
|
67
|
+
else:
|
|
68
|
+
from simple_ci.ui.server import run_ui
|
|
69
|
+
|
|
70
|
+
run_ui(argv)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
if __name__ == "__main__":
|
|
74
|
+
main()
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
from collections.abc import Callable
|
|
6
|
+
from pathlib import Path
|
|
6
7
|
from typing import Any
|
|
7
8
|
|
|
8
9
|
from simple_ci.actions import base
|
|
@@ -12,10 +13,18 @@ from simple_ci.actions.image import build_image_command, build_push_command
|
|
|
12
13
|
from simple_ci.actions.source import build_git_pull_command
|
|
13
14
|
from simple_ci.actions.target import build_target_shell_command
|
|
14
15
|
|
|
15
|
-
ActionHandler = Callable[
|
|
16
|
+
ActionHandler = Callable[
|
|
17
|
+
[dict[str, Any], dict[str, Any], str | Path | None],
|
|
18
|
+
base.ActionResult,
|
|
19
|
+
]
|
|
16
20
|
|
|
17
21
|
|
|
18
|
-
def dispatch_action(
|
|
22
|
+
def dispatch_action(
|
|
23
|
+
step: dict[str, Any],
|
|
24
|
+
payload: dict[str, Any],
|
|
25
|
+
*,
|
|
26
|
+
base_dir: str | Path | None = None,
|
|
27
|
+
) -> dict[str, Any]:
|
|
19
28
|
"""根据 step.uses 分发内置动作,并返回可 JSON 序列化的动作结果。"""
|
|
20
29
|
|
|
21
30
|
action_name = str(step.get("uses") or step.get("action") or "")
|
|
@@ -26,7 +35,7 @@ def dispatch_action(step: dict[str, Any], payload: dict[str, Any]) -> dict[str,
|
|
|
26
35
|
exit_code=1,
|
|
27
36
|
stderr_tail=f"Unknown action: {action_name or '<missing>'}",
|
|
28
37
|
).to_dict()
|
|
29
|
-
return handler(step, payload).to_dict()
|
|
38
|
+
return handler(step, payload, base_dir).to_dict()
|
|
30
39
|
|
|
31
40
|
|
|
32
41
|
def _params(step: dict[str, Any]) -> dict[str, Any]:
|
|
@@ -40,13 +49,15 @@ def _run_built_command(
|
|
|
40
49
|
command: list[str] | list[list[str]] | str,
|
|
41
50
|
params: dict[str, Any],
|
|
42
51
|
*,
|
|
52
|
+
base_dir: str | Path | None = None,
|
|
43
53
|
allow_shell: bool = False,
|
|
54
|
+
cwd_keys: tuple[str, ...] = ("cwd", "workdir", "work_dir", "working_dir"),
|
|
44
55
|
) -> base.ActionResult:
|
|
45
56
|
"""统一执行命令并注入动作参数中的敏感值用于结果脱敏。"""
|
|
46
57
|
|
|
47
58
|
return base.run_command(
|
|
48
59
|
command,
|
|
49
|
-
cwd=
|
|
60
|
+
cwd=base.resolve_process_cwd(params, base_dir=base_dir, cwd_keys=cwd_keys),
|
|
50
61
|
env=params.get("env"),
|
|
51
62
|
secret_values=base.collect_secret_values(params),
|
|
52
63
|
timeout_seconds=float(params.get("timeout_seconds", 300)),
|
|
@@ -54,28 +65,59 @@ def _run_built_command(
|
|
|
54
65
|
)
|
|
55
66
|
|
|
56
67
|
|
|
57
|
-
def _source_git_pull(
|
|
68
|
+
def _source_git_pull(
|
|
69
|
+
step: dict[str, Any],
|
|
70
|
+
_payload: dict[str, Any],
|
|
71
|
+
base_dir: str | Path | None,
|
|
72
|
+
) -> base.ActionResult:
|
|
58
73
|
params = _params(step)
|
|
59
74
|
if isinstance(params.get("credential"), str):
|
|
60
75
|
message = "Unresolved credential id for source.git_pull; CLI must send a credential dict."
|
|
61
76
|
return base.ActionResult(rendered_command="", exit_code=1, stderr_tail=message, error=message)
|
|
62
|
-
|
|
77
|
+
command_params = dict(params)
|
|
78
|
+
if "work_dir" in command_params and "directory" not in command_params and "checkout_dir" not in command_params:
|
|
79
|
+
command_params["directory"] = command_params["work_dir"]
|
|
80
|
+
process_cwd = base.resolve_process_cwd(
|
|
81
|
+
command_params,
|
|
82
|
+
base_dir=base_dir,
|
|
83
|
+
cwd_keys=("cwd", "workdir", "working_dir"),
|
|
84
|
+
)
|
|
85
|
+
if process_cwd is not None:
|
|
86
|
+
command_params["cwd"] = process_cwd
|
|
87
|
+
return _run_built_command(
|
|
88
|
+
build_git_pull_command(command_params),
|
|
89
|
+
command_params,
|
|
90
|
+
base_dir=base_dir,
|
|
91
|
+
cwd_keys=("cwd", "workdir", "working_dir"),
|
|
92
|
+
)
|
|
63
93
|
|
|
64
94
|
|
|
65
|
-
def _build_maven(
|
|
95
|
+
def _build_maven(
|
|
96
|
+
step: dict[str, Any],
|
|
97
|
+
_payload: dict[str, Any],
|
|
98
|
+
base_dir: str | Path | None,
|
|
99
|
+
) -> base.ActionResult:
|
|
66
100
|
params = _params(step)
|
|
67
|
-
return _run_built_command(build_maven_command(params), params)
|
|
101
|
+
return _run_built_command(build_maven_command(params), params, base_dir=base_dir)
|
|
68
102
|
|
|
69
103
|
|
|
70
|
-
def _shell_action(
|
|
104
|
+
def _shell_action(
|
|
105
|
+
step: dict[str, Any],
|
|
106
|
+
_payload: dict[str, Any],
|
|
107
|
+
base_dir: str | Path | None,
|
|
108
|
+
) -> base.ActionResult:
|
|
71
109
|
params = _params(step)
|
|
72
110
|
command = params.get("command") or step.get("command")
|
|
73
111
|
if not isinstance(command, (str, list)):
|
|
74
112
|
return base.ActionResult(rendered_command="", exit_code=1, stderr_tail="Missing shell command.")
|
|
75
|
-
return _run_built_command(command, params, allow_shell=True)
|
|
113
|
+
return _run_built_command(command, params, base_dir=base_dir, allow_shell=True)
|
|
76
114
|
|
|
77
115
|
|
|
78
|
-
def _target_shell(
|
|
116
|
+
def _target_shell(
|
|
117
|
+
step: dict[str, Any],
|
|
118
|
+
payload: dict[str, Any],
|
|
119
|
+
base_dir: str | Path | None,
|
|
120
|
+
) -> base.ActionResult:
|
|
79
121
|
"""执行目标脚本或 inline 命令;script 模式使用 argv,inline 字符串保留 shell 能力。"""
|
|
80
122
|
|
|
81
123
|
params = _params(step)
|
|
@@ -83,28 +125,44 @@ def _target_shell(step: dict[str, Any], payload: dict[str, Any]) -> base.ActionR
|
|
|
83
125
|
command = build_target_shell_command(params, payload)
|
|
84
126
|
except ValueError as exc:
|
|
85
127
|
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))
|
|
128
|
+
return _run_built_command(command, params, base_dir=base_dir, allow_shell=isinstance(command, str))
|
|
87
129
|
|
|
88
130
|
|
|
89
|
-
def _image_build(
|
|
131
|
+
def _image_build(
|
|
132
|
+
step: dict[str, Any],
|
|
133
|
+
_payload: dict[str, Any],
|
|
134
|
+
base_dir: str | Path | None,
|
|
135
|
+
) -> base.ActionResult:
|
|
90
136
|
params = _params(step)
|
|
91
|
-
result = _run_built_command(build_image_command(params), params)
|
|
137
|
+
result = _run_built_command(build_image_command(params), params, base_dir=base_dir)
|
|
92
138
|
if result.exit_code == 0:
|
|
93
139
|
result.image_tag = f"{params['image']}:{params.get('tag', 'latest')}"
|
|
94
140
|
return result
|
|
95
141
|
|
|
96
142
|
|
|
97
|
-
def _image_push(
|
|
143
|
+
def _image_push(
|
|
144
|
+
step: dict[str, Any],
|
|
145
|
+
_payload: dict[str, Any],
|
|
146
|
+
base_dir: str | Path | None,
|
|
147
|
+
) -> base.ActionResult:
|
|
98
148
|
params = _params(step)
|
|
99
|
-
return _run_built_command(build_push_command(params), params)
|
|
149
|
+
return _run_built_command(build_push_command(params), params, base_dir=base_dir)
|
|
100
150
|
|
|
101
151
|
|
|
102
|
-
def _deploy_compose(
|
|
152
|
+
def _deploy_compose(
|
|
153
|
+
step: dict[str, Any],
|
|
154
|
+
_payload: dict[str, Any],
|
|
155
|
+
base_dir: str | Path | None,
|
|
156
|
+
) -> base.ActionResult:
|
|
103
157
|
params = _params(step)
|
|
104
|
-
return _run_built_command(build_compose_command(params), params)
|
|
158
|
+
return _run_built_command(build_compose_command(params), params, base_dir=base_dir)
|
|
105
159
|
|
|
106
160
|
|
|
107
|
-
def _deploy_healthcheck(
|
|
161
|
+
def _deploy_healthcheck(
|
|
162
|
+
step: dict[str, Any],
|
|
163
|
+
_payload: dict[str, Any],
|
|
164
|
+
_base_dir: str | Path | None,
|
|
165
|
+
) -> base.ActionResult:
|
|
108
166
|
"""执行 HTTP 健康检查;失败时以非零退出码进入流水线短路。"""
|
|
109
167
|
|
|
110
168
|
params = _params(step)
|
|
@@ -6,6 +6,7 @@ import re
|
|
|
6
6
|
import subprocess
|
|
7
7
|
import time
|
|
8
8
|
from dataclasses import dataclass
|
|
9
|
+
from pathlib import Path
|
|
9
10
|
from typing import Any
|
|
10
11
|
from urllib.parse import quote, urlsplit, urlunsplit
|
|
11
12
|
|
|
@@ -145,6 +146,29 @@ def run_command(
|
|
|
145
146
|
)
|
|
146
147
|
|
|
147
148
|
|
|
149
|
+
def resolve_process_cwd(
|
|
150
|
+
params: dict[str, Any],
|
|
151
|
+
*,
|
|
152
|
+
base_dir: str | Path | None = None,
|
|
153
|
+
cwd_keys: tuple[str, ...] = ("cwd", "workdir", "work_dir", "working_dir"),
|
|
154
|
+
) -> str | None:
|
|
155
|
+
"""解析动作进程 cwd;未显式配置时默认使用服务端 base_dir。"""
|
|
156
|
+
|
|
157
|
+
configured_cwd = next(
|
|
158
|
+
(params[key] for key in cwd_keys if params.get(key) not in (None, "")),
|
|
159
|
+
None,
|
|
160
|
+
)
|
|
161
|
+
if configured_cwd is None:
|
|
162
|
+
return str(Path(base_dir).resolve()) if base_dir is not None else None
|
|
163
|
+
|
|
164
|
+
cwd_path = Path(str(configured_cwd))
|
|
165
|
+
if cwd_path.is_absolute():
|
|
166
|
+
return str(cwd_path.resolve())
|
|
167
|
+
if base_dir is not None:
|
|
168
|
+
return str((Path(base_dir) / cwd_path).resolve())
|
|
169
|
+
return str(cwd_path)
|
|
170
|
+
|
|
171
|
+
|
|
148
172
|
def _normalize_commands(command: list[str] | list[list[str]] | str) -> list[list[str] | str]:
|
|
149
173
|
"""把单命令或多命令统一为顺序执行列表。"""
|
|
150
174
|
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import json
|
|
3
4
|
import time
|
|
5
|
+
from pathlib import Path
|
|
4
6
|
from typing import Any
|
|
5
7
|
|
|
6
8
|
import httpx
|
|
@@ -50,6 +52,50 @@ class SimpleCiClient:
|
|
|
50
52
|
|
|
51
53
|
return self._request_json("GET", f"/api/v1/runs/{run_id}/logs")
|
|
52
54
|
|
|
55
|
+
def list_tools(self) -> dict[str, Any]:
|
|
56
|
+
"""查询服务端已安装工具列表。"""
|
|
57
|
+
|
|
58
|
+
return self._request_json("GET", "/api/v1/tools")
|
|
59
|
+
|
|
60
|
+
def get_tool(self, tool_id: str) -> dict[str, Any]:
|
|
61
|
+
"""查询指定 tool_id 的工具详情。"""
|
|
62
|
+
|
|
63
|
+
return self._request_json("GET", f"/api/v1/tools/{tool_id}")
|
|
64
|
+
|
|
65
|
+
def remove_tool(self, tool_id: str, purge_package: bool = False) -> dict[str, Any]:
|
|
66
|
+
"""删除指定工具;默认保留原始归档包。"""
|
|
67
|
+
|
|
68
|
+
return self._request_json(
|
|
69
|
+
"DELETE",
|
|
70
|
+
f"/api/v1/tools/{tool_id}",
|
|
71
|
+
params={"purge_package": str(purge_package).lower()},
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
def upload_tool(
|
|
75
|
+
self,
|
|
76
|
+
tool_id: str,
|
|
77
|
+
archive_path: str | Path,
|
|
78
|
+
strip_root: bool,
|
|
79
|
+
bins: dict[str, Any],
|
|
80
|
+
force: bool = False,
|
|
81
|
+
) -> dict[str, Any]:
|
|
82
|
+
"""以 multipart 方式上传工具归档包并安装。"""
|
|
83
|
+
|
|
84
|
+
archive = Path(archive_path)
|
|
85
|
+
data = {
|
|
86
|
+
"tool_id": tool_id,
|
|
87
|
+
"strip_root": str(strip_root).lower(),
|
|
88
|
+
"bins": json.dumps(bins, ensure_ascii=False),
|
|
89
|
+
"force": str(force).lower(),
|
|
90
|
+
}
|
|
91
|
+
with archive.open("rb") as archive_file:
|
|
92
|
+
return self._request_json(
|
|
93
|
+
"POST",
|
|
94
|
+
"/api/v1/tools/upload",
|
|
95
|
+
data=data,
|
|
96
|
+
files={"archive": (archive.name, archive_file, "application/octet-stream")},
|
|
97
|
+
)
|
|
98
|
+
|
|
53
99
|
def run_and_poll(
|
|
54
100
|
self,
|
|
55
101
|
payload: dict[str, Any],
|
|
@@ -17,7 +17,7 @@ import yaml
|
|
|
17
17
|
from dotenv import dotenv_values
|
|
18
18
|
|
|
19
19
|
from simple_ci.errors import ConfigError
|
|
20
|
-
from simple_ci.models import Credential, Pipeline, Project, ProjectSource, Step, Target, TargetServer
|
|
20
|
+
from simple_ci.models import Credential, Pipeline, Project, ProjectSource, Step, Target, TargetServer, TargetTool
|
|
21
21
|
|
|
22
22
|
|
|
23
23
|
@dataclass
|
|
@@ -109,10 +109,31 @@ def _load_targets(targets_dir: Path, env: Mapping[str, str]) -> dict[str, Target
|
|
|
109
109
|
vars=_optional_mapping(data, "vars", f"target {target_id}"),
|
|
110
110
|
description=_optional_str(data, "description"),
|
|
111
111
|
policy=_optional_mapping(data, "policy", f"target {target_id}"),
|
|
112
|
+
tools=_load_target_tools(data, f"target {target_id}"),
|
|
112
113
|
)
|
|
113
114
|
return targets
|
|
114
115
|
|
|
115
116
|
|
|
117
|
+
def _load_target_tools(data: Mapping[str, Any], context: str) -> dict[str, TargetTool]:
|
|
118
|
+
raw_tools = data.get("tools", {})
|
|
119
|
+
if not isinstance(raw_tools, dict):
|
|
120
|
+
raise ConfigError(f"{context} field tools must be a table")
|
|
121
|
+
|
|
122
|
+
tools: dict[str, TargetTool] = {}
|
|
123
|
+
for tool_id, raw_tool in raw_tools.items():
|
|
124
|
+
section = _require_mapping(raw_tool, f"{context}.tools.{tool_id}")
|
|
125
|
+
archive = _require_str(section, "archive", f"{context}.tools.{tool_id}")
|
|
126
|
+
strip_root = bool(section.get("strip_root", True))
|
|
127
|
+
bins = _optional_mapping(section, "bins", f"{context}.tools.{tool_id}")
|
|
128
|
+
tools[tool_id] = TargetTool(
|
|
129
|
+
id=tool_id,
|
|
130
|
+
archive=archive,
|
|
131
|
+
strip_root=strip_root,
|
|
132
|
+
bins={str(key): str(value) for key, value in bins.items()},
|
|
133
|
+
)
|
|
134
|
+
return tools
|
|
135
|
+
|
|
136
|
+
|
|
116
137
|
def _build_target_server(target_id: str, server: Mapping[str, Any], env: Mapping[str, str]) -> TargetServer:
|
|
117
138
|
normalized_id = _normalize_env_id(target_id)
|
|
118
139
|
url_env = _optional_str(server, "url_env") or f"SERVER_{normalized_id}_URL"
|