omdev 0.0.0.dev210__py3-none-any.whl → 0.0.0.dev211__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.
- omdev/ci/cache.py +137 -10
- omdev/ci/ci.py +110 -75
- omdev/ci/cli.py +41 -7
- omdev/ci/compose.py +31 -15
- omdev/ci/{dockertars.py → docker.py} +43 -30
- omdev/ci/github/__init__.py +0 -0
- omdev/ci/github/bootstrap.py +11 -0
- omdev/ci/github/cache.py +355 -0
- omdev/ci/github/cacheapi.py +207 -0
- omdev/ci/github/cli.py +39 -0
- omdev/ci/requirements.py +1 -0
- omdev/ci/shell.py +42 -0
- omdev/ci/utils.py +49 -0
- omdev/scripts/ci.py +1737 -485
- omdev/scripts/interp.py +22 -22
- omdev/scripts/pyproject.py +22 -22
- {omdev-0.0.0.dev210.dist-info → omdev-0.0.0.dev211.dist-info}/METADATA +2 -2
- {omdev-0.0.0.dev210.dist-info → omdev-0.0.0.dev211.dist-info}/RECORD +22 -16
- {omdev-0.0.0.dev210.dist-info → omdev-0.0.0.dev211.dist-info}/LICENSE +0 -0
- {omdev-0.0.0.dev210.dist-info → omdev-0.0.0.dev211.dist-info}/WHEEL +0 -0
- {omdev-0.0.0.dev210.dist-info → omdev-0.0.0.dev211.dist-info}/entry_points.txt +0 -0
- {omdev-0.0.0.dev210.dist-info → omdev-0.0.0.dev211.dist-info}/top_level.txt +0 -0
omdev/ci/cache.py
CHANGED
@@ -2,21 +2,24 @@
|
|
2
2
|
# @omlish-lite
|
3
3
|
import abc
|
4
4
|
import os.path
|
5
|
+
import shlex
|
5
6
|
import shutil
|
6
7
|
import typing as ta
|
7
8
|
|
9
|
+
from .shell import ShellCmd
|
8
10
|
|
9
|
-
|
11
|
+
|
12
|
+
##
|
10
13
|
|
11
14
|
|
12
15
|
@abc.abstractmethod
|
13
16
|
class FileCache(abc.ABC):
|
14
17
|
@abc.abstractmethod
|
15
|
-
def get_file(self,
|
18
|
+
def get_file(self, key: str) -> ta.Optional[str]:
|
16
19
|
raise NotImplementedError
|
17
20
|
|
18
21
|
@abc.abstractmethod
|
19
|
-
def put_file(self,
|
22
|
+
def put_file(self, key: str, file_path: str) -> ta.Optional[str]:
|
20
23
|
raise NotImplementedError
|
21
24
|
|
22
25
|
|
@@ -29,13 +32,137 @@ class DirectoryFileCache(FileCache):
|
|
29
32
|
|
30
33
|
self._dir = dir
|
31
34
|
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
+
#
|
36
|
+
|
37
|
+
def get_cache_file_path(
|
38
|
+
self,
|
39
|
+
key: str,
|
40
|
+
*,
|
41
|
+
make_dirs: bool = False,
|
42
|
+
) -> str:
|
43
|
+
if make_dirs:
|
44
|
+
os.makedirs(self._dir, exist_ok=True)
|
45
|
+
return os.path.join(self._dir, key)
|
46
|
+
|
47
|
+
def format_incomplete_file(self, f: str) -> str:
|
48
|
+
return os.path.join(os.path.dirname(f), f'_{os.path.basename(f)}.incomplete')
|
49
|
+
|
50
|
+
#
|
51
|
+
|
52
|
+
def get_file(self, key: str) -> ta.Optional[str]:
|
53
|
+
cache_file_path = self.get_cache_file_path(key)
|
54
|
+
if not os.path.exists(cache_file_path):
|
35
55
|
return None
|
36
|
-
return
|
56
|
+
return cache_file_path
|
37
57
|
|
38
|
-
def put_file(self, file_path: str) -> None:
|
39
|
-
|
40
|
-
cache_file_path = os.path.join(self._dir, os.path.basename(file_path))
|
58
|
+
def put_file(self, key: str, file_path: str) -> None:
|
59
|
+
cache_file_path = self.get_cache_file_path(key, make_dirs=True)
|
41
60
|
shutil.copyfile(file_path, cache_file_path)
|
61
|
+
|
62
|
+
|
63
|
+
##
|
64
|
+
|
65
|
+
|
66
|
+
class ShellCache(abc.ABC):
|
67
|
+
@abc.abstractmethod
|
68
|
+
def get_file_cmd(self, key: str) -> ta.Optional[ShellCmd]:
|
69
|
+
raise NotImplementedError
|
70
|
+
|
71
|
+
class PutFileCmdContext(abc.ABC):
|
72
|
+
def __init__(self) -> None:
|
73
|
+
super().__init__()
|
74
|
+
|
75
|
+
self._state: ta.Literal['open', 'committed', 'aborted'] = 'open'
|
76
|
+
|
77
|
+
@property
|
78
|
+
def state(self) -> ta.Literal['open', 'committed', 'aborted']:
|
79
|
+
return self._state
|
80
|
+
|
81
|
+
#
|
82
|
+
|
83
|
+
@property
|
84
|
+
@abc.abstractmethod
|
85
|
+
def cmd(self) -> ShellCmd:
|
86
|
+
raise NotImplementedError
|
87
|
+
|
88
|
+
#
|
89
|
+
|
90
|
+
def __enter__(self):
|
91
|
+
return self
|
92
|
+
|
93
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
94
|
+
if exc_val is None:
|
95
|
+
self.commit()
|
96
|
+
else:
|
97
|
+
self.abort()
|
98
|
+
|
99
|
+
#
|
100
|
+
|
101
|
+
@abc.abstractmethod
|
102
|
+
def _commit(self) -> None:
|
103
|
+
raise NotImplementedError
|
104
|
+
|
105
|
+
def commit(self) -> None:
|
106
|
+
if self._state == 'committed':
|
107
|
+
return
|
108
|
+
elif self._state == 'open':
|
109
|
+
self._commit()
|
110
|
+
self._state = 'committed'
|
111
|
+
else:
|
112
|
+
raise RuntimeError(self._state)
|
113
|
+
|
114
|
+
#
|
115
|
+
|
116
|
+
@abc.abstractmethod
|
117
|
+
def _abort(self) -> None:
|
118
|
+
raise NotImplementedError
|
119
|
+
|
120
|
+
def abort(self) -> None:
|
121
|
+
if self._state == 'aborted':
|
122
|
+
return
|
123
|
+
elif self._state == 'open':
|
124
|
+
self._abort()
|
125
|
+
self._state = 'committed'
|
126
|
+
else:
|
127
|
+
raise RuntimeError(self._state)
|
128
|
+
|
129
|
+
@abc.abstractmethod
|
130
|
+
def put_file_cmd(self, key: str) -> PutFileCmdContext:
|
131
|
+
raise NotImplementedError
|
132
|
+
|
133
|
+
|
134
|
+
#
|
135
|
+
|
136
|
+
|
137
|
+
class DirectoryShellCache(ShellCache):
|
138
|
+
def __init__(self, dfc: DirectoryFileCache) -> None:
|
139
|
+
super().__init__()
|
140
|
+
|
141
|
+
self._dfc = dfc
|
142
|
+
|
143
|
+
def get_file_cmd(self, key: str) -> ta.Optional[ShellCmd]:
|
144
|
+
f = self._dfc.get_file(key)
|
145
|
+
if f is None:
|
146
|
+
return None
|
147
|
+
return ShellCmd(f'cat {shlex.quote(f)}')
|
148
|
+
|
149
|
+
class _PutFileCmdContext(ShellCache.PutFileCmdContext): # noqa
|
150
|
+
def __init__(self, tf: str, f: str) -> None:
|
151
|
+
super().__init__()
|
152
|
+
|
153
|
+
self._tf = tf
|
154
|
+
self._f = f
|
155
|
+
|
156
|
+
@property
|
157
|
+
def cmd(self) -> ShellCmd:
|
158
|
+
return ShellCmd(f'cat > {shlex.quote(self._tf)}')
|
159
|
+
|
160
|
+
def _commit(self) -> None:
|
161
|
+
os.replace(self._tf, self._f)
|
162
|
+
|
163
|
+
def _abort(self) -> None:
|
164
|
+
os.unlink(self._tf)
|
165
|
+
|
166
|
+
def put_file_cmd(self, key: str) -> ShellCache.PutFileCmdContext:
|
167
|
+
f = self._dfc.get_cache_file_path(key, make_dirs=True)
|
168
|
+
return self._PutFileCmdContext(self._dfc.format_incomplete_file(f), f)
|
omdev/ci/ci.py
CHANGED
@@ -13,19 +13,19 @@ from omlish.lite.contextmanagers import ExitStacked
|
|
13
13
|
from omlish.lite.contextmanagers import defer
|
14
14
|
|
15
15
|
from .cache import FileCache
|
16
|
+
from .cache import ShellCache
|
16
17
|
from .compose import DockerComposeRun
|
17
18
|
from .compose import get_compose_service_dependencies
|
18
|
-
from .
|
19
|
-
from .
|
20
|
-
from .
|
21
|
-
from .
|
22
|
-
from .
|
23
|
-
from .
|
19
|
+
from .docker import build_docker_file_hash
|
20
|
+
from .docker import build_docker_image
|
21
|
+
from .docker import is_docker_image_present
|
22
|
+
from .docker import load_docker_tar_cmd
|
23
|
+
from .docker import pull_docker_image
|
24
|
+
from .docker import save_docker_tar_cmd
|
24
25
|
from .requirements import build_requirements_hash
|
25
26
|
from .requirements import download_requirements
|
26
|
-
|
27
|
-
|
28
|
-
##
|
27
|
+
from .shell import ShellCmd
|
28
|
+
from .utils import log_timing_context
|
29
29
|
|
30
30
|
|
31
31
|
class Ci(ExitStacked):
|
@@ -40,8 +40,12 @@ class Ci(ExitStacked):
|
|
40
40
|
compose_file: str
|
41
41
|
service: str
|
42
42
|
|
43
|
+
cmd: ShellCmd
|
44
|
+
|
43
45
|
requirements_txts: ta.Optional[ta.Sequence[str]] = None
|
44
46
|
|
47
|
+
always_pull: bool = False
|
48
|
+
|
45
49
|
def __post_init__(self) -> None:
|
46
50
|
check.not_isinstance(self.requirements_txts, str)
|
47
51
|
|
@@ -49,40 +53,61 @@ class Ci(ExitStacked):
|
|
49
53
|
self,
|
50
54
|
cfg: Config,
|
51
55
|
*,
|
56
|
+
shell_cache: ta.Optional[ShellCache] = None,
|
52
57
|
file_cache: ta.Optional[FileCache] = None,
|
53
58
|
) -> None:
|
54
59
|
super().__init__()
|
55
60
|
|
56
61
|
self._cfg = cfg
|
62
|
+
self._shell_cache = shell_cache
|
57
63
|
self._file_cache = file_cache
|
58
64
|
|
59
65
|
#
|
60
66
|
|
61
|
-
def
|
62
|
-
if
|
67
|
+
def _load_cache_docker_image(self, key: str) -> ta.Optional[str]:
|
68
|
+
if self._shell_cache is None:
|
69
|
+
return None
|
70
|
+
|
71
|
+
get_cache_cmd = self._shell_cache.get_file_cmd(key)
|
72
|
+
if get_cache_cmd is None:
|
73
|
+
return None
|
74
|
+
|
75
|
+
get_cache_cmd = dc.replace(get_cache_cmd, s=f'{get_cache_cmd.s} | zstd -cd --long') # noqa
|
76
|
+
|
77
|
+
return load_docker_tar_cmd(get_cache_cmd)
|
78
|
+
|
79
|
+
def _save_cache_docker_image(self, key: str, image: str) -> None:
|
80
|
+
if self._shell_cache is None:
|
81
|
+
return
|
82
|
+
|
83
|
+
with self._shell_cache.put_file_cmd(key) as put_cache:
|
84
|
+
put_cache_cmd = put_cache.cmd
|
85
|
+
|
86
|
+
put_cache_cmd = dc.replace(put_cache_cmd, s=f'zstd | {put_cache_cmd.s}')
|
87
|
+
|
88
|
+
save_docker_tar_cmd(image, put_cache_cmd)
|
89
|
+
|
90
|
+
#
|
91
|
+
|
92
|
+
def _load_docker_image(self, image: str) -> None:
|
93
|
+
if not self._cfg.always_pull and is_docker_image_present(image):
|
63
94
|
return
|
64
95
|
|
65
96
|
dep_suffix = image
|
66
97
|
for c in '/:.-_':
|
67
98
|
dep_suffix = dep_suffix.replace(c, '-')
|
68
99
|
|
69
|
-
|
70
|
-
|
71
|
-
if self._file_cache is not None and (cache_tar_file := self._file_cache.get_file(tar_file_name)):
|
72
|
-
load_docker_tar(cache_tar_file)
|
100
|
+
cache_key = f'docker-{dep_suffix}'
|
101
|
+
if self._load_cache_docker_image(cache_key) is not None:
|
73
102
|
return
|
74
103
|
|
75
|
-
|
76
|
-
with defer(lambda: shutil.rmtree(temp_dir)):
|
77
|
-
temp_tar_file = os.path.join(temp_dir, tar_file_name)
|
104
|
+
pull_docker_image(image)
|
78
105
|
|
79
|
-
|
80
|
-
image,
|
81
|
-
temp_tar_file,
|
82
|
-
)
|
106
|
+
self._save_cache_docker_image(cache_key, image)
|
83
107
|
|
84
|
-
|
85
|
-
|
108
|
+
def load_docker_image(self, image: str) -> None:
|
109
|
+
with log_timing_context(f'Load docker image: {image}'):
|
110
|
+
self._load_docker_image(image)
|
86
111
|
|
87
112
|
@cached_nullary
|
88
113
|
def load_compose_service_dependencies(self) -> None:
|
@@ -96,46 +121,46 @@ class Ci(ExitStacked):
|
|
96
121
|
|
97
122
|
#
|
98
123
|
|
99
|
-
|
100
|
-
def build_ci_image(self) -> str:
|
124
|
+
def _resolve_ci_image(self) -> str:
|
101
125
|
docker_file_hash = build_docker_file_hash(self._cfg.docker_file)[:self.FILE_NAME_HASH_LEN]
|
102
126
|
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
image_id = read_docker_tar_image_id(cache_tar_file)
|
107
|
-
load_docker_tar(cache_tar_file)
|
108
|
-
return image_id
|
127
|
+
cache_key = f'ci-{docker_file_hash}'
|
128
|
+
if (cache_image_id := self._load_cache_docker_image(cache_key)) is not None:
|
129
|
+
return cache_image_id
|
109
130
|
|
110
|
-
|
111
|
-
|
112
|
-
|
131
|
+
image_id = build_docker_image(
|
132
|
+
self._cfg.docker_file,
|
133
|
+
cwd=self._cfg.project_dir,
|
134
|
+
)
|
113
135
|
|
114
|
-
|
115
|
-
self._cfg.docker_file,
|
116
|
-
temp_tar_file,
|
117
|
-
cwd=self._cfg.project_dir,
|
118
|
-
)
|
136
|
+
self._save_cache_docker_image(cache_key, image_id)
|
119
137
|
|
120
|
-
|
121
|
-
self._file_cache.put_file(temp_tar_file)
|
138
|
+
return image_id
|
122
139
|
|
140
|
+
@cached_nullary
|
141
|
+
def resolve_ci_image(self) -> str:
|
142
|
+
with log_timing_context('Resolve ci image') as ltc:
|
143
|
+
image_id = self._resolve_ci_image()
|
144
|
+
ltc.set_description(f'Resolve ci image: {image_id}')
|
123
145
|
return image_id
|
124
146
|
|
125
147
|
#
|
126
148
|
|
127
|
-
|
128
|
-
|
129
|
-
|
149
|
+
def _resolve_requirements_dir(self) -> str:
|
150
|
+
requirements_txts = [
|
151
|
+
os.path.join(self._cfg.project_dir, rf)
|
152
|
+
for rf in check.not_none(self._cfg.requirements_txts)
|
153
|
+
]
|
130
154
|
|
131
155
|
requirements_hash = build_requirements_hash(requirements_txts)[:self.FILE_NAME_HASH_LEN]
|
132
156
|
|
133
|
-
|
157
|
+
tar_file_key = f'requirements-{requirements_hash}'
|
158
|
+
tar_file_name = f'{tar_file_key}.tar'
|
134
159
|
|
135
160
|
temp_dir = tempfile.mkdtemp()
|
136
161
|
self._enter_context(defer(lambda: shutil.rmtree(temp_dir))) # noqa
|
137
162
|
|
138
|
-
if self._file_cache is not None and (cache_tar_file := self._file_cache.get_file(
|
163
|
+
if self._file_cache is not None and (cache_tar_file := self._file_cache.get_file(tar_file_key)):
|
139
164
|
with tarfile.open(cache_tar_file) as tar:
|
140
165
|
tar.extractall(path=temp_dir) # noqa
|
141
166
|
|
@@ -145,7 +170,7 @@ class Ci(ExitStacked):
|
|
145
170
|
os.makedirs(temp_requirements_dir)
|
146
171
|
|
147
172
|
download_requirements(
|
148
|
-
self.
|
173
|
+
self.resolve_ci_image(),
|
149
174
|
temp_requirements_dir,
|
150
175
|
requirements_txts,
|
151
176
|
)
|
@@ -160,21 +185,20 @@ class Ci(ExitStacked):
|
|
160
185
|
arcname=requirement_file,
|
161
186
|
)
|
162
187
|
|
163
|
-
self._file_cache.put_file(temp_tar_file)
|
188
|
+
self._file_cache.put_file(os.path.basename(tar_file_key), temp_tar_file)
|
164
189
|
|
165
190
|
return temp_requirements_dir
|
166
191
|
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
requirements_dir = self.build_requirements_dir()
|
192
|
+
@cached_nullary
|
193
|
+
def resolve_requirements_dir(self) -> str:
|
194
|
+
with log_timing_context('Resolve requirements dir') as ltc:
|
195
|
+
requirements_dir = self._resolve_requirements_dir()
|
196
|
+
ltc.set_description(f'Resolve requirements dir: {requirements_dir}')
|
197
|
+
return requirements_dir
|
175
198
|
|
176
|
-
|
199
|
+
#
|
177
200
|
|
201
|
+
def _run_compose_(self) -> None:
|
178
202
|
setup_cmds = [
|
179
203
|
'pip install --root-user-action ignore --find-links /requirements --no-index uv',
|
180
204
|
(
|
@@ -185,30 +209,41 @@ class Ci(ExitStacked):
|
|
185
209
|
|
186
210
|
#
|
187
211
|
|
188
|
-
|
189
|
-
|
190
|
-
|
212
|
+
ci_cmd = dc.replace(self._cfg.cmd, s=' && '.join([
|
213
|
+
*setup_cmds,
|
214
|
+
f'({self._cfg.cmd.s})',
|
215
|
+
]))
|
191
216
|
|
192
217
|
#
|
193
218
|
|
194
|
-
bash_src = ' && '.join([
|
195
|
-
*setup_cmds,
|
196
|
-
*test_cmds,
|
197
|
-
])
|
198
|
-
|
199
219
|
with DockerComposeRun(DockerComposeRun.Config(
|
200
|
-
|
201
|
-
|
220
|
+
compose_file=self._cfg.compose_file,
|
221
|
+
service=self._cfg.service,
|
202
222
|
|
203
|
-
|
223
|
+
image=self.resolve_ci_image(),
|
204
224
|
|
205
|
-
|
225
|
+
cmd=ci_cmd,
|
206
226
|
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
227
|
+
run_options=[
|
228
|
+
'-v', f'{os.path.abspath(self._cfg.project_dir)}:/project',
|
229
|
+
'-v', f'{os.path.abspath(self.resolve_requirements_dir())}:/requirements',
|
230
|
+
],
|
211
231
|
|
212
|
-
|
232
|
+
cwd=self._cfg.project_dir,
|
213
233
|
)) as ci_compose_run:
|
214
234
|
ci_compose_run.run()
|
235
|
+
|
236
|
+
def _run_compose(self) -> None:
|
237
|
+
with log_timing_context('Run compose'):
|
238
|
+
self._run_compose_()
|
239
|
+
|
240
|
+
#
|
241
|
+
|
242
|
+
def run(self) -> None:
|
243
|
+
self.load_compose_service_dependencies()
|
244
|
+
|
245
|
+
self.resolve_ci_image()
|
246
|
+
|
247
|
+
self.resolve_requirements_dir()
|
248
|
+
|
249
|
+
self._run_compose()
|
omdev/ci/cli.py
CHANGED
@@ -20,15 +20,18 @@ from omlish.argparse.cli import ArgparseCli
|
|
20
20
|
from omlish.argparse.cli import argparse_arg
|
21
21
|
from omlish.argparse.cli import argparse_cmd
|
22
22
|
from omlish.lite.check import check
|
23
|
+
from omlish.logs.standard import configure_standard_logging
|
23
24
|
|
24
25
|
from .cache import DirectoryFileCache
|
26
|
+
from .cache import DirectoryShellCache
|
25
27
|
from .cache import FileCache
|
28
|
+
from .cache import ShellCache
|
26
29
|
from .ci import Ci
|
27
30
|
from .compose import get_compose_service_dependencies
|
31
|
+
from .github.cache import GithubShellCache
|
32
|
+
from .github.cli import GithubCli
|
28
33
|
from .requirements import build_requirements_hash
|
29
|
-
|
30
|
-
|
31
|
-
##
|
34
|
+
from .shell import ShellCmd
|
32
35
|
|
33
36
|
|
34
37
|
class CiCli(ArgparseCli):
|
@@ -59,23 +62,32 @@ class CiCli(ArgparseCli):
|
|
59
62
|
|
60
63
|
#
|
61
64
|
|
65
|
+
@argparse_cmd(
|
66
|
+
accepts_unknown=True,
|
67
|
+
)
|
68
|
+
def github(self) -> ta.Optional[int]:
|
69
|
+
return GithubCli(self.unknown_args).cli_run()
|
70
|
+
|
71
|
+
#
|
72
|
+
|
62
73
|
@argparse_cmd(
|
63
74
|
argparse_arg('project-dir'),
|
64
75
|
argparse_arg('service'),
|
65
76
|
argparse_arg('--docker-file'),
|
66
77
|
argparse_arg('--compose-file'),
|
67
78
|
argparse_arg('-r', '--requirements-txt', action='append'),
|
79
|
+
argparse_arg('--github-cache', action='store_true'),
|
68
80
|
argparse_arg('--cache-dir'),
|
81
|
+
argparse_arg('--always-pull', action='store_true'),
|
69
82
|
)
|
70
83
|
async def run(self) -> None:
|
71
|
-
await asyncio.sleep(1)
|
72
|
-
|
73
84
|
project_dir = self.args.project_dir
|
74
85
|
docker_file = self.args.docker_file
|
75
86
|
compose_file = self.args.compose_file
|
76
87
|
service = self.args.service
|
77
88
|
requirements_txts = self.args.requirements_txt
|
78
89
|
cache_dir = self.args.cache_dir
|
90
|
+
always_pull = self.args.always_pull
|
79
91
|
|
80
92
|
#
|
81
93
|
|
@@ -85,7 +97,7 @@ class CiCli(ArgparseCli):
|
|
85
97
|
|
86
98
|
def find_alt_file(*alts: str) -> ta.Optional[str]:
|
87
99
|
for alt in alts:
|
88
|
-
alt_file = os.path.join(project_dir, alt)
|
100
|
+
alt_file = os.path.abspath(os.path.join(project_dir, alt))
|
89
101
|
if os.path.isfile(alt_file):
|
90
102
|
return alt_file
|
91
103
|
return None
|
@@ -121,24 +133,44 @@ class CiCli(ArgparseCli):
|
|
121
133
|
|
122
134
|
#
|
123
135
|
|
136
|
+
shell_cache: ta.Optional[ShellCache] = None
|
124
137
|
file_cache: ta.Optional[FileCache] = None
|
125
138
|
if cache_dir is not None:
|
126
139
|
if not os.path.exists(cache_dir):
|
127
140
|
os.makedirs(cache_dir)
|
128
141
|
check.state(os.path.isdir(cache_dir))
|
129
|
-
|
142
|
+
|
143
|
+
directory_file_cache = DirectoryFileCache(cache_dir)
|
144
|
+
|
145
|
+
file_cache = directory_file_cache
|
146
|
+
|
147
|
+
if self.args.github_cache:
|
148
|
+
shell_cache = GithubShellCache(cache_dir)
|
149
|
+
else:
|
150
|
+
shell_cache = DirectoryShellCache(directory_file_cache)
|
130
151
|
|
131
152
|
#
|
132
153
|
|
133
154
|
with Ci(
|
134
155
|
Ci.Config(
|
135
156
|
project_dir=project_dir,
|
157
|
+
|
136
158
|
docker_file=docker_file,
|
159
|
+
|
137
160
|
compose_file=compose_file,
|
138
161
|
service=service,
|
162
|
+
|
139
163
|
requirements_txts=requirements_txts,
|
164
|
+
|
165
|
+
cmd=ShellCmd(' && '.join([
|
166
|
+
'cd /project',
|
167
|
+
'python3 -m pytest -svv test.py',
|
168
|
+
])),
|
169
|
+
|
170
|
+
always_pull=always_pull,
|
140
171
|
),
|
141
172
|
file_cache=file_cache,
|
173
|
+
shell_cache=shell_cache,
|
142
174
|
) as ci:
|
143
175
|
ci.run()
|
144
176
|
|
@@ -148,6 +180,8 @@ async def _async_main() -> ta.Optional[int]:
|
|
148
180
|
|
149
181
|
|
150
182
|
def _main() -> None:
|
183
|
+
configure_standard_logging('DEBUG')
|
184
|
+
|
151
185
|
sys.exit(rc if isinstance(rc := asyncio.run(_async_main()), int) else 0)
|
152
186
|
|
153
187
|
|
omdev/ci/compose.py
CHANGED
@@ -4,8 +4,11 @@
|
|
4
4
|
TODO:
|
5
5
|
- fix rmi - only when not referenced anymore
|
6
6
|
"""
|
7
|
+
import contextlib
|
7
8
|
import dataclasses as dc
|
9
|
+
import itertools
|
8
10
|
import os.path
|
11
|
+
import shlex
|
9
12
|
import typing as ta
|
10
13
|
|
11
14
|
from omlish.lite.cached import cached_nullary
|
@@ -15,6 +18,7 @@ from omlish.lite.contextmanagers import defer
|
|
15
18
|
from omlish.lite.json import json_dumps_pretty
|
16
19
|
from omlish.subprocesses import subprocesses
|
17
20
|
|
21
|
+
from .shell import ShellCmd
|
18
22
|
from .utils import make_temp_file
|
19
23
|
from .utils import read_yaml_file
|
20
24
|
|
@@ -50,7 +54,7 @@ class DockerComposeRun(ExitStacked):
|
|
50
54
|
|
51
55
|
image: str
|
52
56
|
|
53
|
-
|
57
|
+
cmd: ShellCmd
|
54
58
|
|
55
59
|
#
|
56
60
|
|
@@ -60,9 +64,11 @@ class DockerComposeRun(ExitStacked):
|
|
60
64
|
|
61
65
|
#
|
62
66
|
|
63
|
-
|
64
|
-
|
67
|
+
no_dependency_cleanup: bool = False
|
68
|
+
|
69
|
+
#
|
65
70
|
|
71
|
+
def __post_init__(self) -> None:
|
66
72
|
check.not_isinstance(self.run_options, str)
|
67
73
|
|
68
74
|
def __init__(self, cfg: Config) -> None:
|
@@ -171,28 +177,38 @@ class DockerComposeRun(ExitStacked):
|
|
171
177
|
|
172
178
|
#
|
173
179
|
|
180
|
+
def _cleanup_dependencies(self) -> None:
|
181
|
+
subprocesses.check_call(
|
182
|
+
'docker',
|
183
|
+
'compose',
|
184
|
+
'-f', self.rewrite_compose_file(),
|
185
|
+
'down',
|
186
|
+
)
|
187
|
+
|
174
188
|
def run(self) -> None:
|
175
189
|
self.tag_image()
|
176
190
|
|
177
191
|
compose_file = self.rewrite_compose_file()
|
178
192
|
|
179
|
-
|
180
|
-
|
193
|
+
with contextlib.ExitStack() as es:
|
194
|
+
if not self._cfg.no_dependency_cleanup:
|
195
|
+
es.enter_context(defer(self._cleanup_dependencies)) # noqa
|
196
|
+
|
197
|
+
sh_cmd = ' '.join([
|
181
198
|
'docker',
|
182
199
|
'compose',
|
183
200
|
'-f', compose_file,
|
184
201
|
'run',
|
185
202
|
'--rm',
|
186
|
-
*self._cfg.
|
203
|
+
*itertools.chain.from_iterable(['-e', k] for k in (self._cfg.cmd.env or [])),
|
204
|
+
*(self._cfg.run_options or []),
|
187
205
|
self._cfg.service,
|
188
|
-
|
189
|
-
|
190
|
-
)
|
206
|
+
'sh', '-c', shlex.quote(self._cfg.cmd.s),
|
207
|
+
])
|
191
208
|
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
'down',
|
209
|
+
run_cmd = dc.replace(self._cfg.cmd, s=sh_cmd)
|
210
|
+
|
211
|
+
run_cmd.run(
|
212
|
+
subprocesses.check_call,
|
213
|
+
**self._subprocess_kwargs,
|
198
214
|
)
|