cdt-release 0.4.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- cdt/__init__.py +1 -0
- cdt/__main__.py +3 -0
- cdt/agent_release.py +316 -0
- cdt/agent_release_worker.py +46 -0
- cdt/artifacts.py +37 -0
- cdt/cdt.schema.json +1512 -0
- cdt/cli.py +590 -0
- cdt/config.py +38 -0
- cdt/doctor.py +81 -0
- cdt/flows/__init__.py +5 -0
- cdt/flows/deploy_flow.py +50 -0
- cdt/flows/ios_flow.py +52 -0
- cdt/flows/prod_flow.py +236 -0
- cdt/flows/testing_flow.py +296 -0
- cdt/init_project.py +95 -0
- cdt/pipeline/__init__.py +15 -0
- cdt/pipeline/builtins.py +213 -0
- cdt/pipeline/config.py +254 -0
- cdt/pipeline/context.py +164 -0
- cdt/pipeline/executor.py +180 -0
- cdt/pipeline/planning.py +314 -0
- cdt/pipeline/preflight.py +53 -0
- cdt/pipeline/registry.py +196 -0
- cdt/pipeline/runner.py +180 -0
- cdt/pipeline/step.py +10 -0
- cdt/pipeline/validation.py +152 -0
- cdt/platforms/__init__.py +1 -0
- cdt/platforms/android.py +137 -0
- cdt/platforms/flutter_build.py +60 -0
- cdt/platforms/ios_flutter.py +66 -0
- cdt/platforms/ios_xcode.py +214 -0
- cdt/platforms/web.py +104 -0
- cdt/py.typed +0 -0
- cdt/runner.py +108 -0
- cdt/runs.py +243 -0
- cdt/schema.py +182 -0
- cdt/sdk.py +78 -0
- cdt/self_update.py +394 -0
- cdt/services/__init__.py +1 -0
- cdt/services/appstore.py +244 -0
- cdt/services/firebase.py +54 -0
- cdt/services/notify.py +112 -0
- cdt/services/tracker.py +43 -0
- cdt/sounds.py +76 -0
- cdt/steps/__init__.py +1 -0
- cdt/steps/android.py +73 -0
- cdt/steps/appstore.py +29 -0
- cdt/steps/artifact.py +29 -0
- cdt/steps/firebase.py +39 -0
- cdt/steps/flutter.py +32 -0
- cdt/steps/git.py +46 -0
- cdt/steps/hook.py +93 -0
- cdt/steps/ios.py +83 -0
- cdt/steps/notify.py +53 -0
- cdt/steps/tracker.py +19 -0
- cdt/steps/web.py +126 -0
- cdt/ui.py +120 -0
- cdt/versioning.py +60 -0
- cdt_release-0.4.0.dist-info/METADATA +215 -0
- cdt_release-0.4.0.dist-info/RECORD +64 -0
- cdt_release-0.4.0.dist-info/WHEEL +5 -0
- cdt_release-0.4.0.dist-info/entry_points.txt +2 -0
- cdt_release-0.4.0.dist-info/licenses/LICENSE +21 -0
- cdt_release-0.4.0.dist-info/top_level.txt +1 -0
cdt/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.4.0"
|
cdt/__main__.py
ADDED
cdt/agent_release.py
ADDED
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import signal
|
|
5
|
+
import subprocess
|
|
6
|
+
import sys
|
|
7
|
+
import time
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
import yaml
|
|
12
|
+
|
|
13
|
+
from .runs import (
|
|
14
|
+
RUN_SCHEMA_VERSION,
|
|
15
|
+
RunPaths,
|
|
16
|
+
create_run,
|
|
17
|
+
now,
|
|
18
|
+
read_json,
|
|
19
|
+
resolve_run,
|
|
20
|
+
write_exit_code,
|
|
21
|
+
write_json_atomic,
|
|
22
|
+
write_text_atomic,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def artifact_paths(pipeline: str) -> dict[str, Path]:
|
|
27
|
+
"""Return legacy pipeline-named paths for compatibility with older callers."""
|
|
28
|
+
base = Path.cwd() / ".cdt"
|
|
29
|
+
stem = f"agent-release-{pipeline}"
|
|
30
|
+
return {
|
|
31
|
+
"dir": base,
|
|
32
|
+
"log": base / f"{stem}.log",
|
|
33
|
+
"pid": base / f"{stem}.pid",
|
|
34
|
+
"meta": base / f"{stem}.meta.json",
|
|
35
|
+
"exit": base / f"{stem}.exit",
|
|
36
|
+
"status": base / f"{stem}.status.json",
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def start_release(
|
|
41
|
+
pipeline: str,
|
|
42
|
+
ids: list[str] | None = None,
|
|
43
|
+
*,
|
|
44
|
+
run_id: str | None = None,
|
|
45
|
+
confirm: str | None = None,
|
|
46
|
+
) -> dict[str, Any]:
|
|
47
|
+
ids = ids or []
|
|
48
|
+
cwd = Path.cwd()
|
|
49
|
+
command = ["cdt", "run", pipeline]
|
|
50
|
+
for task_id in ids:
|
|
51
|
+
command.extend(["--id", task_id])
|
|
52
|
+
if confirm is not None:
|
|
53
|
+
command.extend(["--confirm", confirm])
|
|
54
|
+
paths = create_run(cwd, pipeline, ids=ids, run_id=run_id, command=command, detached=True)
|
|
55
|
+
|
|
56
|
+
worker_cmd = [
|
|
57
|
+
sys.executable,
|
|
58
|
+
"-m",
|
|
59
|
+
"cdt.agent_release_worker",
|
|
60
|
+
"--pipeline",
|
|
61
|
+
pipeline,
|
|
62
|
+
"--run-id",
|
|
63
|
+
paths.run_id,
|
|
64
|
+
"--log",
|
|
65
|
+
str(paths.log),
|
|
66
|
+
"--exit-file",
|
|
67
|
+
str(paths.exit),
|
|
68
|
+
"--status-file",
|
|
69
|
+
str(paths.status),
|
|
70
|
+
]
|
|
71
|
+
for task_id in ids:
|
|
72
|
+
worker_cmd.extend(["--id", task_id])
|
|
73
|
+
if confirm is not None:
|
|
74
|
+
worker_cmd.extend(["--confirm", confirm])
|
|
75
|
+
|
|
76
|
+
try:
|
|
77
|
+
process = subprocess.Popen(
|
|
78
|
+
worker_cmd,
|
|
79
|
+
cwd=cwd,
|
|
80
|
+
start_new_session=True,
|
|
81
|
+
stdout=subprocess.DEVNULL,
|
|
82
|
+
stderr=subprocess.DEVNULL,
|
|
83
|
+
)
|
|
84
|
+
except Exception as exc:
|
|
85
|
+
payload = read_json(paths.status) or {}
|
|
86
|
+
payload.update({"status": "failed", "error": f"Failed to start release worker: {exc}", "finished_at": now()})
|
|
87
|
+
payload["updated_at"] = now()
|
|
88
|
+
write_json_atomic(paths.status, payload)
|
|
89
|
+
write_exit_code(paths.exit, 1)
|
|
90
|
+
return release_status(run_id=paths.run_id)
|
|
91
|
+
|
|
92
|
+
write_text_atomic(paths.pid, f"{process.pid}\n")
|
|
93
|
+
manifest = read_json(paths.manifest) or {}
|
|
94
|
+
manifest.update({"pid": process.pid, "worker_command": worker_cmd})
|
|
95
|
+
write_json_atomic(paths.manifest, manifest)
|
|
96
|
+
return release_status(run_id=paths.run_id)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def release_status(pipeline: str | None = None, *, run_id: str | None = None) -> dict[str, Any]:
|
|
100
|
+
paths, legacy = _resolve_paths(pipeline=pipeline, run_id=run_id)
|
|
101
|
+
if paths is None:
|
|
102
|
+
return {
|
|
103
|
+
"schema_version": RUN_SCHEMA_VERSION,
|
|
104
|
+
"status": "unknown",
|
|
105
|
+
"run_id": run_id,
|
|
106
|
+
"pipeline": pipeline,
|
|
107
|
+
"pid": None,
|
|
108
|
+
"exit_code": None,
|
|
109
|
+
"log": None,
|
|
110
|
+
"status_file": None,
|
|
111
|
+
"last_log_update": None,
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
pid = _read_pid(paths.pid)
|
|
115
|
+
exit_code = _read_exit(paths.exit)
|
|
116
|
+
running = _pid_running(pid) if pid is not None else False
|
|
117
|
+
status_payload = read_json(paths.status) or {}
|
|
118
|
+
manifest = read_json(paths.manifest) or {}
|
|
119
|
+
recorded_status = status_payload.get("status")
|
|
120
|
+
|
|
121
|
+
if exit_code == 0:
|
|
122
|
+
status = "success"
|
|
123
|
+
elif exit_code is not None:
|
|
124
|
+
status = "failed" if recorded_status != "cancelled" else "cancelled"
|
|
125
|
+
elif running:
|
|
126
|
+
status = "running"
|
|
127
|
+
elif recorded_status in {"success", "failed", "cancelled", "blocked"}:
|
|
128
|
+
status = str(recorded_status)
|
|
129
|
+
elif pid is not None:
|
|
130
|
+
status = "stale"
|
|
131
|
+
elif recorded_status == "queued":
|
|
132
|
+
status = "queued"
|
|
133
|
+
else:
|
|
134
|
+
status = "unknown"
|
|
135
|
+
|
|
136
|
+
payload: dict[str, Any] = {
|
|
137
|
+
"schema_version": RUN_SCHEMA_VERSION,
|
|
138
|
+
"status": status,
|
|
139
|
+
"run_id": None if legacy else paths.run_id,
|
|
140
|
+
"pipeline": status_payload.get("pipeline") or manifest.get("pipeline") or pipeline,
|
|
141
|
+
"pid": pid,
|
|
142
|
+
"exit_code": exit_code,
|
|
143
|
+
"log": str(paths.log),
|
|
144
|
+
"status_file": str(paths.status),
|
|
145
|
+
"last_log_update": _mtime(paths.log),
|
|
146
|
+
}
|
|
147
|
+
status_keys = (
|
|
148
|
+
"current_step",
|
|
149
|
+
"completed_steps",
|
|
150
|
+
"running_steps",
|
|
151
|
+
"parallel_completed",
|
|
152
|
+
"parallel_failed",
|
|
153
|
+
"failed_step",
|
|
154
|
+
"error",
|
|
155
|
+
"artifacts",
|
|
156
|
+
"old_version",
|
|
157
|
+
"new_version",
|
|
158
|
+
"started_at",
|
|
159
|
+
"finished_at",
|
|
160
|
+
"updated_at",
|
|
161
|
+
)
|
|
162
|
+
for key in status_keys:
|
|
163
|
+
if key in status_payload:
|
|
164
|
+
payload[key] = status_payload[key]
|
|
165
|
+
return payload
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def wait_for_release(
|
|
169
|
+
pipeline: str | None = None,
|
|
170
|
+
timeout_seconds: int | None = None,
|
|
171
|
+
interval_seconds: float = 5.0,
|
|
172
|
+
*,
|
|
173
|
+
run_id: str | None = None,
|
|
174
|
+
) -> dict[str, Any]:
|
|
175
|
+
deadline = time.monotonic() + timeout_seconds if timeout_seconds is not None else None
|
|
176
|
+
while True:
|
|
177
|
+
payload = release_status(pipeline, run_id=run_id)
|
|
178
|
+
if payload["status"] in {"success", "failed", "cancelled", "blocked", "stale", "unknown"}:
|
|
179
|
+
return payload
|
|
180
|
+
if deadline is not None and time.monotonic() >= deadline:
|
|
181
|
+
payload["wait_status"] = "timeout"
|
|
182
|
+
return payload
|
|
183
|
+
time.sleep(interval_seconds)
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def stop_release(
|
|
187
|
+
pipeline: str | None = None,
|
|
188
|
+
timeout_seconds: int = 30,
|
|
189
|
+
*,
|
|
190
|
+
run_id: str | None = None,
|
|
191
|
+
) -> dict[str, Any]:
|
|
192
|
+
paths, _ = _resolve_paths(pipeline=pipeline, run_id=run_id)
|
|
193
|
+
if paths is None:
|
|
194
|
+
payload = release_status(pipeline, run_id=run_id)
|
|
195
|
+
payload["stop_result"] = "missing_pid"
|
|
196
|
+
return payload
|
|
197
|
+
manifest = read_json(paths.manifest) or {}
|
|
198
|
+
if manifest.get("detached") is False:
|
|
199
|
+
payload = release_status(pipeline, run_id=run_id)
|
|
200
|
+
payload["stop_result"] = "not_detached"
|
|
201
|
+
return payload
|
|
202
|
+
pid = _read_pid(paths.pid)
|
|
203
|
+
if pid is None:
|
|
204
|
+
payload = release_status(pipeline, run_id=run_id)
|
|
205
|
+
payload["stop_result"] = "missing_pid"
|
|
206
|
+
return payload
|
|
207
|
+
if not _pid_running(pid):
|
|
208
|
+
payload = release_status(pipeline, run_id=run_id)
|
|
209
|
+
payload["stop_result"] = "not_running"
|
|
210
|
+
return payload
|
|
211
|
+
|
|
212
|
+
try:
|
|
213
|
+
os.killpg(pid, signal.SIGTERM)
|
|
214
|
+
except ProcessLookupError:
|
|
215
|
+
payload = release_status(pipeline, run_id=run_id)
|
|
216
|
+
payload["stop_result"] = "not_running"
|
|
217
|
+
return payload
|
|
218
|
+
|
|
219
|
+
deadline = time.monotonic() + timeout_seconds
|
|
220
|
+
while time.monotonic() < deadline:
|
|
221
|
+
if not _pid_running(pid):
|
|
222
|
+
_mark_cancelled(paths)
|
|
223
|
+
payload = release_status(pipeline, run_id=run_id)
|
|
224
|
+
payload["stop_result"] = "terminated"
|
|
225
|
+
return payload
|
|
226
|
+
time.sleep(0.5)
|
|
227
|
+
|
|
228
|
+
try:
|
|
229
|
+
os.killpg(pid, signal.SIGKILL)
|
|
230
|
+
except ProcessLookupError:
|
|
231
|
+
pass
|
|
232
|
+
_mark_cancelled(paths)
|
|
233
|
+
payload = release_status(pipeline, run_id=run_id)
|
|
234
|
+
payload["stop_result"] = "killed"
|
|
235
|
+
return payload
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def parse_duration(value: str) -> int:
|
|
239
|
+
raw = value.strip().lower()
|
|
240
|
+
if raw.endswith("ms"):
|
|
241
|
+
return max(1, int(raw[:-2]) // 1000)
|
|
242
|
+
if raw.endswith("s"):
|
|
243
|
+
return int(raw[:-1])
|
|
244
|
+
if raw.endswith("m"):
|
|
245
|
+
return int(raw[:-1]) * 60
|
|
246
|
+
if raw.endswith("h"):
|
|
247
|
+
return int(raw[:-1]) * 3600
|
|
248
|
+
return int(raw)
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def format_yamlish(payload: dict[str, Any]) -> str:
|
|
252
|
+
return yaml.safe_dump(payload, allow_unicode=True, sort_keys=False).strip()
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def _resolve_paths(*, pipeline: str | None, run_id: str | None) -> tuple[RunPaths | None, bool]:
|
|
256
|
+
cwd = Path.cwd()
|
|
257
|
+
paths = resolve_run(cwd, run_id=run_id, pipeline=pipeline)
|
|
258
|
+
if paths is not None:
|
|
259
|
+
return paths, False
|
|
260
|
+
if pipeline is None:
|
|
261
|
+
return None, False
|
|
262
|
+
legacy = artifact_paths(pipeline)
|
|
263
|
+
if not any(legacy[key].exists() for key in ("pid", "exit", "status", "log")):
|
|
264
|
+
return None, False
|
|
265
|
+
return (
|
|
266
|
+
RunPaths(
|
|
267
|
+
run_id=pipeline,
|
|
268
|
+
root=legacy["dir"],
|
|
269
|
+
manifest=legacy["meta"],
|
|
270
|
+
status=legacy["status"],
|
|
271
|
+
log=legacy["log"],
|
|
272
|
+
exit=legacy["exit"],
|
|
273
|
+
pid=legacy["pid"],
|
|
274
|
+
),
|
|
275
|
+
True,
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def _mark_cancelled(paths: RunPaths) -> None:
|
|
280
|
+
payload = read_json(paths.status) or {}
|
|
281
|
+
payload.update({"status": "cancelled", "finished_at": now(), "updated_at": now()})
|
|
282
|
+
write_json_atomic(paths.status, payload)
|
|
283
|
+
write_exit_code(paths.exit, 130)
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def _read_pid(path: Path) -> int | None:
|
|
287
|
+
try:
|
|
288
|
+
return int(path.read_text(encoding="utf-8").strip())
|
|
289
|
+
except (OSError, ValueError):
|
|
290
|
+
return None
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def _read_exit(path: Path) -> int | None:
|
|
294
|
+
try:
|
|
295
|
+
return int(path.read_text(encoding="utf-8").strip())
|
|
296
|
+
except (OSError, ValueError):
|
|
297
|
+
return None
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def _pid_running(pid: int | None) -> bool:
|
|
301
|
+
if pid is None:
|
|
302
|
+
return False
|
|
303
|
+
try:
|
|
304
|
+
os.kill(pid, 0)
|
|
305
|
+
except ProcessLookupError:
|
|
306
|
+
return False
|
|
307
|
+
except PermissionError:
|
|
308
|
+
return True
|
|
309
|
+
return True
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
def _mtime(path: Path) -> str | None:
|
|
313
|
+
try:
|
|
314
|
+
return time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime(path.stat().st_mtime))
|
|
315
|
+
except OSError:
|
|
316
|
+
return None
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import subprocess
|
|
3
|
+
import sys
|
|
4
|
+
import traceback
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def main() -> int:
|
|
9
|
+
parser = argparse.ArgumentParser()
|
|
10
|
+
parser.add_argument("--pipeline", required=True)
|
|
11
|
+
parser.add_argument("--run-id")
|
|
12
|
+
parser.add_argument("--log", required=True)
|
|
13
|
+
parser.add_argument("--exit-file", required=True)
|
|
14
|
+
parser.add_argument("--status-file", required=True)
|
|
15
|
+
parser.add_argument("--id", action="append", default=[])
|
|
16
|
+
parser.add_argument("--confirm")
|
|
17
|
+
args = parser.parse_args()
|
|
18
|
+
|
|
19
|
+
cmd = [sys.executable, "-m", "cdt", "run", args.pipeline, "--status-file", args.status_file]
|
|
20
|
+
if args.run_id is not None:
|
|
21
|
+
cmd.extend(["--run-id", args.run_id])
|
|
22
|
+
for task_id in args.id:
|
|
23
|
+
cmd.extend(["--id", task_id])
|
|
24
|
+
if args.confirm is not None:
|
|
25
|
+
cmd.extend(["--confirm", args.confirm])
|
|
26
|
+
|
|
27
|
+
log_path = Path(args.log)
|
|
28
|
+
log_path.parent.mkdir(parents=True, exist_ok=True)
|
|
29
|
+
exit_file = Path(args.exit_file)
|
|
30
|
+
exit_file.parent.mkdir(parents=True, exist_ok=True)
|
|
31
|
+
exit_code = 1
|
|
32
|
+
try:
|
|
33
|
+
with log_path.open("ab") as log:
|
|
34
|
+
process = subprocess.Popen(cmd, stdout=log, stderr=subprocess.STDOUT)
|
|
35
|
+
exit_code = process.wait()
|
|
36
|
+
except Exception:
|
|
37
|
+
with log_path.open("a", encoding="utf-8") as log:
|
|
38
|
+
log.write("\nagent_release_worker failed before or during cdt run startup:\n")
|
|
39
|
+
log.write(traceback.format_exc())
|
|
40
|
+
finally:
|
|
41
|
+
exit_file.write_text(f"{exit_code}\n", encoding="utf-8")
|
|
42
|
+
return exit_code
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
if __name__ == "__main__":
|
|
46
|
+
raise SystemExit(main())
|
cdt/artifacts.py
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from enum import Enum
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class ArtifactKind(str, Enum):
|
|
7
|
+
APK = "apk"
|
|
8
|
+
AAB = "aab"
|
|
9
|
+
IPA = "ipa"
|
|
10
|
+
WEB = "web"
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass(frozen=True)
|
|
14
|
+
class BuildArtifact:
|
|
15
|
+
kind: ArtifactKind
|
|
16
|
+
path: Path
|
|
17
|
+
label: str
|
|
18
|
+
step: str | None = None
|
|
19
|
+
|
|
20
|
+
def to_json(self, name: str) -> dict[str, str]:
|
|
21
|
+
payload = {
|
|
22
|
+
"name": name,
|
|
23
|
+
"path": str(self.path),
|
|
24
|
+
"kind": self.kind.value,
|
|
25
|
+
}
|
|
26
|
+
if self.step:
|
|
27
|
+
payload["step"] = self.step
|
|
28
|
+
return payload
|
|
29
|
+
|
|
30
|
+
@classmethod
|
|
31
|
+
def from_json(cls, payload: dict[str, str]) -> "BuildArtifact":
|
|
32
|
+
return cls(
|
|
33
|
+
kind=ArtifactKind(payload["kind"]),
|
|
34
|
+
path=Path(payload["path"]),
|
|
35
|
+
label=payload.get("label") or payload.get("name") or payload["kind"],
|
|
36
|
+
step=payload.get("step"),
|
|
37
|
+
)
|