ominfra 0.0.0.dev7__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.
Files changed (44) hide show
  1. ominfra/__about__.py +27 -0
  2. ominfra/__init__.py +0 -0
  3. ominfra/bootstrap/__init__.py +0 -0
  4. ominfra/bootstrap/bootstrap.py +8 -0
  5. ominfra/cmds.py +83 -0
  6. ominfra/deploy/__init__.py +0 -0
  7. ominfra/deploy/_executor.py +1036 -0
  8. ominfra/deploy/configs.py +19 -0
  9. ominfra/deploy/executor/__init__.py +1 -0
  10. ominfra/deploy/executor/base.py +115 -0
  11. ominfra/deploy/executor/concerns/__init__.py +0 -0
  12. ominfra/deploy/executor/concerns/dirs.py +28 -0
  13. ominfra/deploy/executor/concerns/nginx.py +47 -0
  14. ominfra/deploy/executor/concerns/repo.py +17 -0
  15. ominfra/deploy/executor/concerns/supervisor.py +46 -0
  16. ominfra/deploy/executor/concerns/systemd.py +88 -0
  17. ominfra/deploy/executor/concerns/user.py +25 -0
  18. ominfra/deploy/executor/concerns/venv.py +22 -0
  19. ominfra/deploy/executor/main.py +119 -0
  20. ominfra/deploy/poly/__init__.py +1 -0
  21. ominfra/deploy/poly/_main.py +725 -0
  22. ominfra/deploy/poly/base.py +179 -0
  23. ominfra/deploy/poly/configs.py +38 -0
  24. ominfra/deploy/poly/deploy.py +25 -0
  25. ominfra/deploy/poly/main.py +18 -0
  26. ominfra/deploy/poly/nginx.py +60 -0
  27. ominfra/deploy/poly/repo.py +41 -0
  28. ominfra/deploy/poly/runtime.py +39 -0
  29. ominfra/deploy/poly/site.py +11 -0
  30. ominfra/deploy/poly/supervisor.py +64 -0
  31. ominfra/deploy/poly/venv.py +52 -0
  32. ominfra/deploy/remote.py +91 -0
  33. ominfra/pyremote/__init__.py +0 -0
  34. ominfra/pyremote/_runcommands.py +824 -0
  35. ominfra/pyremote/bootstrap.py +149 -0
  36. ominfra/pyremote/runcommands.py +56 -0
  37. ominfra/ssh.py +191 -0
  38. ominfra/tools/__init__.py +0 -0
  39. ominfra/tools/listresources.py +256 -0
  40. ominfra-0.0.0.dev7.dist-info/LICENSE +21 -0
  41. ominfra-0.0.0.dev7.dist-info/METADATA +19 -0
  42. ominfra-0.0.0.dev7.dist-info/RECORD +44 -0
  43. ominfra-0.0.0.dev7.dist-info/WHEEL +5 -0
  44. ominfra-0.0.0.dev7.dist-info/top_level.txt +1 -0
@@ -0,0 +1,179 @@
1
+ # ruff: noqa: UP006 UP007
2
+ import abc
3
+ import dataclasses as dc
4
+ import typing as ta
5
+
6
+ from .configs import DeployConcernConfig # noqa
7
+ from .configs import DeployConfig
8
+ from .configs import SiteConcernConfig # noqa
9
+ from .configs import SiteConfig
10
+
11
+
12
+ T = ta.TypeVar('T')
13
+ ConcernT = ta.TypeVar('ConcernT')
14
+ ConfigT = ta.TypeVar('ConfigT')
15
+
16
+
17
+ ##
18
+
19
+
20
+ @dc.dataclass(frozen=True)
21
+ class FsItem(abc.ABC):
22
+ path: str
23
+
24
+ @property
25
+ @abc.abstractmethod
26
+ def is_dir(self) -> bool:
27
+ raise NotImplementedError
28
+
29
+
30
+ @dc.dataclass(frozen=True)
31
+ class FsFile(FsItem):
32
+ @property
33
+ def is_dir(self) -> bool:
34
+ return False
35
+
36
+
37
+ @dc.dataclass(frozen=True)
38
+ class FsDir(FsItem):
39
+ @property
40
+ def is_dir(self) -> bool:
41
+ return True
42
+
43
+
44
+ ##
45
+
46
+
47
+ class Runtime(abc.ABC):
48
+ class Stat(ta.NamedTuple):
49
+ path: str
50
+ is_dir: bool
51
+
52
+ @abc.abstractmethod
53
+ def stat(self, p: str) -> ta.Optional[Stat]:
54
+ raise NotImplementedError
55
+
56
+ @abc.abstractmethod
57
+ def make_dirs(self, p: str, exist_ok: bool = False) -> None:
58
+ raise NotImplementedError
59
+
60
+ @abc.abstractmethod
61
+ def write_file(self, p: str, c: ta.Union[str, bytes]) -> None:
62
+ raise NotImplementedError
63
+
64
+ @abc.abstractmethod
65
+ def sh(self, *ss: str) -> None:
66
+ raise NotImplementedError
67
+
68
+
69
+ ##
70
+
71
+
72
+ class ConcernsContainer(abc.ABC, ta.Generic[ConcernT, ConfigT]):
73
+ concern_cls: ta.ClassVar[type]
74
+
75
+ def __init__(
76
+ self,
77
+ config: ConfigT,
78
+ ) -> None:
79
+ super().__init__()
80
+ self._config = config
81
+
82
+ concern_cls_dct = self._concern_cls_by_config_cls()
83
+ self._concerns = [
84
+ concern_cls_dct[type(c)](c, self) # type: ignore
85
+ for c in config.concerns # type: ignore
86
+ ]
87
+ self._concerns_by_cls: ta.Dict[ta.Type[ConcernT], ConcernT] = {}
88
+ for c in self._concerns:
89
+ if type(c) in self._concerns_by_cls:
90
+ raise TypeError(f'Duplicate concern type: {c}')
91
+ self._concerns_by_cls[type(c)] = c
92
+
93
+ @classmethod
94
+ def _concern_cls_by_config_cls(cls) -> ta.Mapping[type, ta.Type[ConcernT]]:
95
+ return { # noqa
96
+ c.Config: c # type: ignore
97
+ for c in cls.concern_cls.__subclasses__()
98
+ }
99
+
100
+ @property
101
+ def config(self) -> ConfigT:
102
+ return self._config
103
+
104
+ @property
105
+ def concerns(self) -> ta.List[ConcernT]:
106
+ return self._concerns
107
+
108
+ def concern(self, cls: ta.Type[T]) -> T:
109
+ return self._concerns_by_cls[cls] # type: ignore
110
+
111
+
112
+ ##
113
+
114
+
115
+ SiteConcernT = ta.TypeVar('SiteConcernT', bound='SiteConcern')
116
+ SiteConcernConfigT = ta.TypeVar('SiteConcernConfigT', bound='SiteConcernConfig')
117
+
118
+
119
+ class SiteConcern(abc.ABC, ta.Generic[SiteConcernConfigT]):
120
+ def __init__(self, config: SiteConcernConfigT, site: 'Site') -> None:
121
+ super().__init__()
122
+ self._config = config
123
+ self._site = site
124
+
125
+ @property
126
+ def config(self) -> SiteConcernConfigT:
127
+ return self._config
128
+
129
+ @abc.abstractmethod
130
+ def run(self, runtime: Runtime) -> None:
131
+ raise NotImplementedError
132
+
133
+
134
+ ##
135
+
136
+
137
+ class Site(ConcernsContainer[SiteConcern, SiteConfig]):
138
+ @abc.abstractmethod
139
+ def run(self, runtime: Runtime) -> None:
140
+ raise NotImplementedError
141
+
142
+
143
+ ##
144
+
145
+
146
+ DeployConcernT = ta.TypeVar('DeployConcernT', bound='DeployConcern')
147
+ DeployConcernConfigT = ta.TypeVar('DeployConcernConfigT', bound='DeployConcernConfig')
148
+
149
+
150
+ class DeployConcern(abc.ABC, ta.Generic[DeployConcernConfigT]):
151
+ def __init__(self, config: DeployConcernConfigT, deploy: 'Deploy') -> None:
152
+ super().__init__()
153
+ self._config = config
154
+ self._deploy = deploy
155
+
156
+ @property
157
+ def config(self) -> DeployConcernConfigT:
158
+ return self._config
159
+
160
+ def fs_items(self) -> ta.Sequence[FsItem]:
161
+ return []
162
+
163
+ @abc.abstractmethod
164
+ def run(self, runtime: Runtime) -> None:
165
+ raise NotImplementedError
166
+
167
+
168
+ ##
169
+
170
+
171
+ class Deploy(ConcernsContainer[DeployConcern, DeployConfig]):
172
+ @property
173
+ @abc.abstractmethod
174
+ def site(self) -> Site:
175
+ raise NotImplementedError
176
+
177
+ @abc.abstractmethod
178
+ def run(self, runtime: Runtime) -> None:
179
+ raise NotImplementedError
@@ -0,0 +1,38 @@
1
+ # ruff: noqa: UP006
2
+ import abc
3
+ import dataclasses as dc
4
+ import typing as ta
5
+
6
+
7
+ ##
8
+
9
+
10
+ @dc.dataclass(frozen=True)
11
+ class SiteConcernConfig(abc.ABC): # noqa
12
+ pass
13
+
14
+
15
+ @dc.dataclass(frozen=True)
16
+ class SiteConfig:
17
+ user = 'omlish'
18
+
19
+ root_dir: str = '~/deploy'
20
+
21
+ concerns: ta.List[SiteConcernConfig] = dc.field(default_factory=list)
22
+
23
+
24
+ ##
25
+
26
+
27
+ @dc.dataclass(frozen=True)
28
+ class DeployConcernConfig(abc.ABC): # noqa
29
+ pass
30
+
31
+
32
+ @dc.dataclass(frozen=True)
33
+ class DeployConfig:
34
+ site: SiteConfig
35
+
36
+ name: str
37
+
38
+ concerns: ta.List[DeployConcernConfig] = dc.field(default_factory=list)
@@ -0,0 +1,25 @@
1
+ from .base import Deploy
2
+ from .base import DeployConcern
3
+ from .base import Runtime
4
+ from .base import Site
5
+ from .configs import DeployConfig
6
+
7
+
8
+ class DeployImpl(Deploy):
9
+ concern_cls = DeployConcern
10
+
11
+ def __init__(
12
+ self,
13
+ config: DeployConfig,
14
+ site: Site,
15
+ ) -> None:
16
+ super().__init__(config)
17
+ self._site = site
18
+
19
+ @property
20
+ def site(self) -> Site:
21
+ return self._site
22
+
23
+ def run(self, runtime: Runtime) -> None:
24
+ for c in self._concerns:
25
+ c.run(runtime)
@@ -0,0 +1,18 @@
1
+ #!/usr/bin/env python3
2
+ # @omdev-amalg ./_main.py
3
+ from .deploy import DeployImpl # noqa
4
+ from .nginx import NginxDeployConcern # noqa
5
+ from .nginx import NginxSiteConcern # noqa
6
+ from .repo import RepoDeployConcern # noqa
7
+ from .runtime import RuntimeImpl # noqa
8
+ from .site import SiteImpl # noqa
9
+ from .supervisor import SupervisorDeployConcern # noqa
10
+ from .venv import VenvDeployConcern # noqa
11
+
12
+
13
+ def _main() -> None:
14
+ pass
15
+
16
+
17
+ if __name__ == '__main__':
18
+ _main()
@@ -0,0 +1,60 @@
1
+ import dataclasses as dc
2
+ import os.path
3
+ import textwrap
4
+ import typing as ta
5
+
6
+ from omlish.lite.cached import cached_nullary
7
+
8
+ from .base import DeployConcern
9
+ from .base import FsFile
10
+ from .base import FsItem
11
+ from .base import Runtime
12
+ from .base import SiteConcern
13
+ from .configs import DeployConcernConfig
14
+ from .configs import SiteConcernConfig
15
+
16
+
17
+ class NginxSiteConcern(SiteConcern['NginxSiteConcern.Config']):
18
+ @dc.dataclass(frozen=True)
19
+ class Config(SiteConcernConfig):
20
+ global_conf_file: str = '/etc/nginx/sites-enabled/omlish.conf'
21
+
22
+ @cached_nullary
23
+ def confs_dir(self) -> str:
24
+ return os.path.join(self._site.config.root_dir, 'conf', 'nginx')
25
+
26
+ def run(self, runtime: Runtime) -> None:
27
+ if runtime.stat(self._config.global_conf_file) is None:
28
+ runtime.write_file(
29
+ self._config.global_conf_file,
30
+ f'include {self.confs_dir()}/*.conf;\n',
31
+ )
32
+
33
+
34
+ class NginxDeployConcern(DeployConcern['NginxDeployConcern.Config']):
35
+ @dc.dataclass(frozen=True)
36
+ class Config(DeployConcernConfig):
37
+ listen_port: int = 80
38
+ proxy_port: int = 8000
39
+
40
+ @cached_nullary
41
+ def conf_file(self) -> str:
42
+ return os.path.join(self._deploy.site.concern(NginxSiteConcern).confs_dir(), self._deploy.config.name + '.conf')
43
+
44
+ @cached_nullary
45
+ def fs_items(self) -> ta.Sequence[FsItem]:
46
+ return [FsFile(self.conf_file())]
47
+
48
+ def run(self, runtime: Runtime) -> None:
49
+ runtime.make_dirs(os.path.dirname(self.conf_file()))
50
+
51
+ conf = textwrap.dedent(f"""
52
+ server {{
53
+ listen {self._config.listen_port};
54
+ location / {{
55
+ proxy_pass http://127.0.0.1:{self._config.proxy_port}/;
56
+ }}
57
+ }}
58
+ """)
59
+
60
+ runtime.write_file(self.conf_file(), conf)
@@ -0,0 +1,41 @@
1
+ import dataclasses as dc
2
+ import os.path
3
+ import typing as ta
4
+
5
+ from omlish.lite.cached import cached_nullary
6
+
7
+ from .base import DeployConcern
8
+ from .base import FsDir
9
+ from .base import FsItem
10
+ from .base import Runtime
11
+ from .configs import DeployConcernConfig
12
+
13
+
14
+ class RepoDeployConcern(DeployConcern['RepoDeployConcern.Config']):
15
+ @dc.dataclass(frozen=True)
16
+ class Config(DeployConcernConfig):
17
+ url: str
18
+ revision: str = 'master'
19
+ init_submodules: bool = False
20
+
21
+ @cached_nullary
22
+ def repo_dir(self) -> str:
23
+ return os.path.join(self._deploy.site.config.root_dir, 'repos', self._deploy.config.name)
24
+
25
+ @cached_nullary
26
+ def fs_items(self) -> ta.Sequence[FsItem]:
27
+ return [FsDir(self.repo_dir())]
28
+
29
+ def run(self, runtime: Runtime) -> None:
30
+ runtime.make_dirs(self.repo_dir())
31
+
32
+ runtime.sh(
33
+ f'cd {self.repo_dir()}',
34
+ 'git init',
35
+ f'git remote add origin {self._config.url}',
36
+ f'git fetch --depth 1 origin {self._config.revision}',
37
+ 'git checkout FETCH_HEAD',
38
+ *([
39
+ 'git submodule update --init',
40
+ ] if self._config.init_submodules else []),
41
+ )
@@ -0,0 +1,39 @@
1
+ # ruff: noqa: UP007
2
+ import os.path
3
+ import stat
4
+ import typing as ta
5
+
6
+ from omlish.lite.logs import log
7
+ from omlish.lite.subprocesses import subprocess_check_call
8
+
9
+ from .base import Runtime
10
+
11
+
12
+ class RuntimeImpl(Runtime):
13
+ def __init__(self) -> None:
14
+ super().__init__()
15
+
16
+ def stat(self, p: str) -> ta.Optional[Runtime.Stat]:
17
+ try:
18
+ st = os.stat(p)
19
+ except FileNotFoundError:
20
+ return None
21
+ else:
22
+ return Runtime.Stat(
23
+ path=p,
24
+ is_dir=bool(st.st_mode & stat.S_IFDIR),
25
+ )
26
+
27
+ def make_dirs(self, p: str, exist_ok: bool = False) -> None:
28
+ os.makedirs(p, exist_ok=exist_ok)
29
+
30
+ def write_file(self, p: str, c: ta.Union[str, bytes]) -> None:
31
+ if os.path.exists(p):
32
+ raise RuntimeError(f'Path exists: {p}')
33
+ with open(p, 'w' if isinstance(c, str) else 'wb') as f:
34
+ f.write(c)
35
+
36
+ def sh(self, *ss: str) -> None:
37
+ s = ' && '.join(ss)
38
+ log.info('Executing: %s', s)
39
+ subprocess_check_call(s, shell=True)
@@ -0,0 +1,11 @@
1
+ from .base import Runtime
2
+ from .base import Site
3
+ from .base import SiteConcern
4
+
5
+
6
+ class SiteImpl(Site):
7
+ concern_cls = SiteConcern
8
+
9
+ def run(self, runtime: Runtime) -> None:
10
+ for c in self._concerns:
11
+ c.run(runtime)
@@ -0,0 +1,64 @@
1
+ import dataclasses as dc
2
+ import os.path
3
+ import textwrap
4
+ import typing as ta
5
+
6
+ from omlish.lite.cached import cached_nullary
7
+
8
+ from .base import DeployConcern
9
+ from .base import FsFile
10
+ from .base import FsItem
11
+ from .base import Runtime
12
+ from .configs import DeployConcernConfig
13
+ from .repo import RepoDeployConcern
14
+ from .venv import VenvDeployConcern
15
+
16
+
17
+ # class SupervisorSiteConcern(SiteConcern['SupervisorSiteConcern.Config']):
18
+ # @dc.dataclass(frozen=True)
19
+ # class Config(DeployConcern.Config):
20
+ # global_conf_file: str = '/etc/supervisor/conf.d/supervisord.conf'
21
+ #
22
+ # def run(self) -> None:
23
+ # sup_conf_dir = os.path.join(self._d.home_dir(), 'conf/supervisor')
24
+ # with open(self._d.host_cfg.global_supervisor_conf_file_path) as f:
25
+ # glo_sup_conf = f.read()
26
+ # if sup_conf_dir not in glo_sup_conf:
27
+ # log.info('Updating global supervisor conf at %s', self._d.host_cfg.global_supervisor_conf_file_path) # noqa
28
+ # glo_sup_conf += textwrap.dedent(f"""
29
+ # [include]
30
+ # files = {self._d.home_dir()}/conf/supervisor/*.conf
31
+ # """)
32
+ # with open(self._d.host_cfg.global_supervisor_conf_file_path, 'w') as f:
33
+ # f.write(glo_sup_conf)
34
+
35
+
36
+ class SupervisorDeployConcern(DeployConcern['SupervisorDeployConcern.Config']):
37
+ @dc.dataclass(frozen=True)
38
+ class Config(DeployConcernConfig):
39
+ entrypoint: str
40
+
41
+ @cached_nullary
42
+ def conf_file(self) -> str:
43
+ return os.path.join(self._deploy.site.config.root_dir, 'conf', 'supervisor', self._deploy.config.name + '.conf')
44
+
45
+ @cached_nullary
46
+ def fs_items(self) -> ta.Sequence[FsItem]:
47
+ return [FsFile(self.conf_file())]
48
+
49
+ def run(self, runtime: Runtime) -> None:
50
+ runtime.make_dirs(os.path.dirname(self.conf_file()))
51
+
52
+ rd = self._deploy.concern(RepoDeployConcern).repo_dir()
53
+ vx = self._deploy.concern(VenvDeployConcern).exe()
54
+
55
+ conf = textwrap.dedent(f"""
56
+ [program:{self._deploy.config.name}]
57
+ command={vx} -m {self._config.entrypoint}
58
+ directory={rd}
59
+ user={self._deploy.site.config.user}
60
+ autostart=true
61
+ autorestart=true
62
+ """)
63
+
64
+ runtime.write_file(self.conf_file(), conf)
@@ -0,0 +1,52 @@
1
+ import dataclasses as dc
2
+ import os.path
3
+ import typing as ta
4
+
5
+ from omlish.lite.cached import cached_nullary
6
+
7
+ from .base import DeployConcern
8
+ from .base import FsDir
9
+ from .base import FsItem
10
+ from .base import Runtime
11
+ from .configs import DeployConcernConfig
12
+ from .repo import RepoDeployConcern
13
+
14
+
15
+ class VenvDeployConcern(DeployConcern['VenvDeployConcern.Config']):
16
+ @dc.dataclass(frozen=True)
17
+ class Config(DeployConcernConfig):
18
+ interp_version: str
19
+ requirements_txt: str = 'requirements.txt'
20
+
21
+ @cached_nullary
22
+ def venv_dir(self) -> str:
23
+ return os.path.join(self._deploy.site.config.root_dir, 'venvs', self._deploy.config.name)
24
+
25
+ @cached_nullary
26
+ def fs_items(self) -> ta.Sequence[FsItem]:
27
+ return [FsDir(self.venv_dir())]
28
+
29
+ @cached_nullary
30
+ def exe(self) -> str:
31
+ return os.path.join(self.venv_dir(), 'bin', 'python')
32
+
33
+ def run(self, runtime: Runtime) -> None:
34
+ runtime.make_dirs(self.venv_dir())
35
+
36
+ rd = self._deploy.concern(RepoDeployConcern).repo_dir()
37
+
38
+ l, r = os.path.split(self.venv_dir())
39
+
40
+ # FIXME: lol
41
+ py_exe = 'python3'
42
+
43
+ runtime.sh(
44
+ f'cd {l}',
45
+ f'{py_exe} -mvenv {r}',
46
+
47
+ # https://stackoverflow.com/questions/77364550/attributeerror-module-pkgutil-has-no-attribute-impimporter-did-you-mean
48
+ f'{self.exe()} -m ensurepip',
49
+ f'{self.exe()} -mpip install --upgrade setuptools pip',
50
+
51
+ f'{self.exe()} -mpip install -r {rd}/{self._config.requirements_txt}', # noqa
52
+ )
@@ -0,0 +1,91 @@
1
+ """
2
+ lookit:
3
+ - piku, obviously
4
+ - https://github.com/mitogen-hq/mitogen/
5
+
6
+ git init
7
+ git remote add local ~/src/wrmsr/omlish/.git
8
+ git fetch --depth=1 local master
9
+ git remote add origin https://github.com/wrmsr/omlish
10
+ git fetch --depth=1 origin master
11
+ git checkout origin/master
12
+
13
+ {base_path}/{deploys}/
14
+ current ->
15
+ previous ->
16
+ 20240522T120000_{rev}
17
+ """
18
+ import asyncio
19
+ import itertools
20
+ import os.path
21
+ import shlex
22
+ import tempfile
23
+
24
+ from omlish import check
25
+
26
+ from .. import cmds
27
+
28
+
29
+ def render_script(*cs: list[str] | tuple[str, ...]) -> str:
30
+ return ' '.join(itertools.chain.from_iterable(
31
+ [
32
+ *(['&&'] if i > 0 else []),
33
+ shlex.join(check.not_isinstance(l, str)),
34
+ ]
35
+ for i, l in enumerate(cs)
36
+ ))
37
+
38
+
39
+ async def do_remote_deploy(
40
+ cr: cmds.CommandRunner,
41
+ rev: str = 'master',
42
+ *,
43
+ local_repo_path: str | None = None,
44
+ skip_submodules: bool = False,
45
+ ) -> None:
46
+ clone_script = [
47
+ ['git', 'init'],
48
+
49
+ *([
50
+ ['git', 'remote', 'add', 'local', local_repo_path],
51
+ ['git', 'fetch', '--depth=1', 'local', rev],
52
+ ] if local_repo_path is not None else ()),
53
+
54
+ ['git', 'remote', 'add', 'origin', 'https://github.com/wrmsr/omlish'],
55
+ ['git', 'fetch', '--depth=1', 'origin', rev],
56
+ ['git', 'checkout', f'origin/{rev}'],
57
+
58
+ *([['git', 'submodule', 'update', '--init']] if not skip_submodules else ()),
59
+
60
+ ['make', 'venv-deploy'],
61
+ ]
62
+
63
+ res = await cr.run_command(cr.Command([
64
+ 'sh', '-c', render_script(
65
+ ['mkdir', 'omlish'],
66
+ ['cd', 'omlish'],
67
+ *clone_script,
68
+ ),
69
+ ]))
70
+ res.check()
71
+
72
+
73
+ async def _a_main():
74
+ cwd = tempfile.mkdtemp()
75
+ print(cwd)
76
+
77
+ bootstrap_git_path = os.path.join(os.getcwd(), '.git')
78
+ check.state(os.path.isdir(bootstrap_git_path))
79
+
80
+ cr: cmds.CommandRunner = cmds.LocalCommandRunner(cmds.LocalCommandRunner.Config(
81
+ cwd=cwd,
82
+ ))
83
+
84
+ await do_remote_deploy(
85
+ cr,
86
+ local_repo_path=os.path.expanduser('~/src/wrmsr/omlish/.git'),
87
+ )
88
+
89
+
90
+ if __name__ == '__main__':
91
+ asyncio.run(_a_main())
File without changes