mini-runbot 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.
- mini_runbot-0.2.0/PKG-INFO +146 -0
- mini_runbot-0.2.0/README.md +107 -0
- mini_runbot-0.2.0/pyproject.toml +83 -0
- mini_runbot-0.2.0/setup.cfg +4 -0
- mini_runbot-0.2.0/src/mini_runbot/__init__.py +3 -0
- mini_runbot-0.2.0/src/mini_runbot/adapters/git/cli.py +165 -0
- mini_runbot-0.2.0/src/mini_runbot/adapters/persistence/sqlite.py +270 -0
- mini_runbot-0.2.0/src/mini_runbot/adapters/runtime/compose.py +323 -0
- mini_runbot-0.2.0/src/mini_runbot/adapters/runtime/local.py +30 -0
- mini_runbot-0.2.0/src/mini_runbot/adapters/runtime/ports.py +49 -0
- mini_runbot-0.2.0/src/mini_runbot/api/app.py +147 -0
- mini_runbot-0.2.0/src/mini_runbot/api/schemas.py +73 -0
- mini_runbot-0.2.0/src/mini_runbot/application/build_manager.py +390 -0
- mini_runbot-0.2.0/src/mini_runbot/application/executor.py +41 -0
- mini_runbot-0.2.0/src/mini_runbot/application/scheduler.py +60 -0
- mini_runbot-0.2.0/src/mini_runbot/bootstrap.py +28 -0
- mini_runbot-0.2.0/src/mini_runbot/cli/init_config.py +228 -0
- mini_runbot-0.2.0/src/mini_runbot/cli/main.py +346 -0
- mini_runbot-0.2.0/src/mini_runbot/cli/presentation.py +167 -0
- mini_runbot-0.2.0/src/mini_runbot/config.py +129 -0
- mini_runbot-0.2.0/src/mini_runbot/domain/enums.py +24 -0
- mini_runbot-0.2.0/src/mini_runbot/domain/errors.py +45 -0
- mini_runbot-0.2.0/src/mini_runbot/domain/models.py +72 -0
- mini_runbot-0.2.0/src/mini_runbot/domain/transitions.py +43 -0
- mini_runbot-0.2.0/src/mini_runbot/domain/validation.py +88 -0
- mini_runbot-0.2.0/src/mini_runbot/ports/repositories.py +23 -0
- mini_runbot-0.2.0/src/mini_runbot/ports/services.py +62 -0
- mini_runbot-0.2.0/src/mini_runbot/resources/config.example.yaml +22 -0
- mini_runbot-0.2.0/src/mini_runbot/templates/compose.yaml.j2 +49 -0
- mini_runbot-0.2.0/src/mini_runbot/web/app.js +247 -0
- mini_runbot-0.2.0/src/mini_runbot/web/index.html +94 -0
- mini_runbot-0.2.0/src/mini_runbot/web/styles.css +148 -0
- mini_runbot-0.2.0/src/mini_runbot.egg-info/PKG-INFO +146 -0
- mini_runbot-0.2.0/src/mini_runbot.egg-info/SOURCES.txt +36 -0
- mini_runbot-0.2.0/src/mini_runbot.egg-info/dependency_links.txt +1 -0
- mini_runbot-0.2.0/src/mini_runbot.egg-info/entry_points.txt +2 -0
- mini_runbot-0.2.0/src/mini_runbot.egg-info/requires.txt +19 -0
- mini_runbot-0.2.0/src/mini_runbot.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: mini-runbot
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: A small, local build orchestrator proof of concept for Odoo 16
|
|
5
|
+
Author: rafnixg
|
|
6
|
+
Project-URL: Documentation, https://rafnixg.github.io/odoo-platform-ephemeral-environment/
|
|
7
|
+
Project-URL: Repository, https://github.com/rafnixg/odoo-platform-ephemeral-environment
|
|
8
|
+
Project-URL: Issues, https://github.com/rafnixg/odoo-platform-ephemeral-environment/issues
|
|
9
|
+
Project-URL: DeepWiki, https://deepwiki.com/rafnixg/odoo-platform-ephemeral-environment
|
|
10
|
+
Keywords: odoo,docker,runbot,testing,ephemeral-environments
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Environment :: Console
|
|
13
|
+
Classifier: Framework :: FastAPI
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: Operating System :: Microsoft :: Windows
|
|
16
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Topic :: Software Development :: Testing
|
|
20
|
+
Requires-Python: >=3.12
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
Requires-Dist: fastapi<1,>=0.115
|
|
23
|
+
Requires-Dist: jinja2<4,>=3.1
|
|
24
|
+
Requires-Dist: pydantic<3,>=2.9
|
|
25
|
+
Requires-Dist: pyyaml<7,>=6
|
|
26
|
+
Requires-Dist: rich<15,>=13
|
|
27
|
+
Requires-Dist: sqlalchemy<3,>=2.0
|
|
28
|
+
Requires-Dist: typer<1,>=0.12
|
|
29
|
+
Requires-Dist: uvicorn<1,>=0.30
|
|
30
|
+
Provides-Extra: dev
|
|
31
|
+
Requires-Dist: build<2,>=1; extra == "dev"
|
|
32
|
+
Requires-Dist: httpx<1,>=0.27; extra == "dev"
|
|
33
|
+
Requires-Dist: pytest<9,>=8; extra == "dev"
|
|
34
|
+
Requires-Dist: pytest-asyncio<1,>=0.24; extra == "dev"
|
|
35
|
+
Requires-Dist: pytest-cov<7,>=5; extra == "dev"
|
|
36
|
+
Requires-Dist: ruff<1,>=0.8; extra == "dev"
|
|
37
|
+
Provides-Extra: docs
|
|
38
|
+
Requires-Dist: mkdocs-material<10,>=9; extra == "docs"
|
|
39
|
+
|
|
40
|
+
# Mini-Runbot for Odoo 16 (PoC)
|
|
41
|
+
|
|
42
|
+
[](https://github.com/rafnixg/odoo-platform-ephemeral-environment/actions/workflows/ci.yml)
|
|
43
|
+
[](https://github.com/rafnixg/odoo-platform-ephemeral-environment/actions/workflows/docs.yml)
|
|
44
|
+
[](https://github.com/rafnixg/odoo-platform-ephemeral-environment/actions/workflows/codeql.yml)
|
|
45
|
+
[](https://deepwiki.com/rafnixg/odoo-platform-ephemeral-environment)
|
|
46
|
+
|
|
47
|
+
Mini-Runbot turns configured Git revisions and Odoo module lists into isolated, tested, temporary
|
|
48
|
+
Odoo 16 environments. It is a local, single-host proof of concept for trusted repositories, not a
|
|
49
|
+
security boundary for hostile code.
|
|
50
|
+
|
|
51
|
+
## Documentation
|
|
52
|
+
|
|
53
|
+
- [Documentación en español](https://rafnixg.github.io/odoo-platform-ephemeral-environment/)
|
|
54
|
+
- [English documentation](https://rafnixg.github.io/odoo-platform-ephemeral-environment/en/)
|
|
55
|
+
- [Documentation sources](gh-docs/es/index.md)
|
|
56
|
+
- [Explore the codebase in DeepWiki](https://deepwiki.com/rafnixg/odoo-platform-ephemeral-environment)
|
|
57
|
+
|
|
58
|
+
The bilingual portal is the canonical project documentation. It covers installation on Windows and
|
|
59
|
+
Linux, configuration, dashboard and CLI usage, the HTTP API, operations, security limits, testing,
|
|
60
|
+
the roadmap, and architecture decisions. Its technical guides connect behavior to the current
|
|
61
|
+
implementation:
|
|
62
|
+
|
|
63
|
+
- [Domain model and lifecycle](gh-docs/en/domain-model.md)
|
|
64
|
+
- [Eight-stage build pipeline](gh-docs/en/build-pipeline.md)
|
|
65
|
+
- [Concurrency, cleanup, and recovery](gh-docs/en/concurrency-recovery.md)
|
|
66
|
+
- [Ports and adapters](gh-docs/en/adapters.md)
|
|
67
|
+
- [Troubleshooting and operator runbook](gh-docs/en/troubleshooting.md)
|
|
68
|
+
- [CI/CD and automated security](gh-docs/en/ci-cd.md)
|
|
69
|
+
- [Glossary](gh-docs/en/glossary.md)
|
|
70
|
+
|
|
71
|
+
DeepWiki provides a generated, code-oriented view that complements the maintained portal; when the
|
|
72
|
+
two differ, the repository documentation and implementation are authoritative.
|
|
73
|
+
|
|
74
|
+
## Quick start
|
|
75
|
+
|
|
76
|
+
Python 3.12, Git, and Docker Compose v2 are required. On Windows, use Docker Desktop with Linux
|
|
77
|
+
containers; on Linux, use Docker Engine with the Compose v2 plugin.
|
|
78
|
+
|
|
79
|
+
### PyPI application
|
|
80
|
+
|
|
81
|
+
Mini-Runbot is packaged as a CLI application. After the first PyPI release, install it in an
|
|
82
|
+
isolated environment with `pipx`:
|
|
83
|
+
|
|
84
|
+
```console
|
|
85
|
+
pipx install mini-runbot
|
|
86
|
+
mini-runbot init
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Until that release, clone the repository and run `pipx install .`, or use the development setup
|
|
90
|
+
below. `init` asks for runtime and allowed-repository settings and refuses to overwrite an existing
|
|
91
|
+
`config.local.yaml`. Use `mini-runbot init --defaults` for the bundled example values.
|
|
92
|
+
|
|
93
|
+
### Development installation
|
|
94
|
+
|
|
95
|
+
#### Windows · PowerShell
|
|
96
|
+
|
|
97
|
+
```powershell
|
|
98
|
+
py -3.12 -m venv .venv
|
|
99
|
+
.venv\Scripts\Activate.ps1
|
|
100
|
+
python -m pip install -e ".[dev]"
|
|
101
|
+
mini-runbot init
|
|
102
|
+
$env:MINI_RUNBOT_CONFIG = (Resolve-Path config.local.yaml)
|
|
103
|
+
mini-runbot doctor
|
|
104
|
+
mini-runbot serve --reload
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
#### Linux · Bash
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
python3.12 -m venv .venv
|
|
111
|
+
source .venv/bin/activate
|
|
112
|
+
python -m pip install -e ".[dev]"
|
|
113
|
+
mini-runbot init
|
|
114
|
+
export MINI_RUNBOT_CONFIG="$(realpath config.local.yaml)"
|
|
115
|
+
mini-runbot doctor
|
|
116
|
+
mini-runbot serve --reload
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
Open `http://127.0.0.1:8000`, or create a build from the CLI:
|
|
120
|
+
|
|
121
|
+
```console
|
|
122
|
+
mini-runbot build create --repo custom --ref 16.0 --modules module_a,module_b --run
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
Repository inputs are configured aliases, never arbitrary URLs or filesystem paths supplied by a
|
|
126
|
+
client.
|
|
127
|
+
|
|
128
|
+
## Local checks
|
|
129
|
+
|
|
130
|
+
The same commands work in an activated PowerShell or Bash environment:
|
|
131
|
+
|
|
132
|
+
```console
|
|
133
|
+
python -m pip install -e ".[dev,docs]"
|
|
134
|
+
python -m pytest -m "not docker"
|
|
135
|
+
python -m ruff check .
|
|
136
|
+
node --check src/mini_runbot/web/app.js
|
|
137
|
+
python -m build
|
|
138
|
+
python scripts/check_distribution.py dist
|
|
139
|
+
python scripts/check_docs.py
|
|
140
|
+
python -m mkdocs build --strict --config-file gh-docs/mkdocs.es.yml --site-dir ../site
|
|
141
|
+
python -m mkdocs build --strict --config-file gh-docs/mkdocs.en.yml --site-dir ../site/en
|
|
142
|
+
git diff --check
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
Docker-marked end-to-end tests remain an explicit local check because they require a working daemon,
|
|
146
|
+
image registry access, and real Odoo startup.
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# Mini-Runbot for Odoo 16 (PoC)
|
|
2
|
+
|
|
3
|
+
[](https://github.com/rafnixg/odoo-platform-ephemeral-environment/actions/workflows/ci.yml)
|
|
4
|
+
[](https://github.com/rafnixg/odoo-platform-ephemeral-environment/actions/workflows/docs.yml)
|
|
5
|
+
[](https://github.com/rafnixg/odoo-platform-ephemeral-environment/actions/workflows/codeql.yml)
|
|
6
|
+
[](https://deepwiki.com/rafnixg/odoo-platform-ephemeral-environment)
|
|
7
|
+
|
|
8
|
+
Mini-Runbot turns configured Git revisions and Odoo module lists into isolated, tested, temporary
|
|
9
|
+
Odoo 16 environments. It is a local, single-host proof of concept for trusted repositories, not a
|
|
10
|
+
security boundary for hostile code.
|
|
11
|
+
|
|
12
|
+
## Documentation
|
|
13
|
+
|
|
14
|
+
- [Documentación en español](https://rafnixg.github.io/odoo-platform-ephemeral-environment/)
|
|
15
|
+
- [English documentation](https://rafnixg.github.io/odoo-platform-ephemeral-environment/en/)
|
|
16
|
+
- [Documentation sources](gh-docs/es/index.md)
|
|
17
|
+
- [Explore the codebase in DeepWiki](https://deepwiki.com/rafnixg/odoo-platform-ephemeral-environment)
|
|
18
|
+
|
|
19
|
+
The bilingual portal is the canonical project documentation. It covers installation on Windows and
|
|
20
|
+
Linux, configuration, dashboard and CLI usage, the HTTP API, operations, security limits, testing,
|
|
21
|
+
the roadmap, and architecture decisions. Its technical guides connect behavior to the current
|
|
22
|
+
implementation:
|
|
23
|
+
|
|
24
|
+
- [Domain model and lifecycle](gh-docs/en/domain-model.md)
|
|
25
|
+
- [Eight-stage build pipeline](gh-docs/en/build-pipeline.md)
|
|
26
|
+
- [Concurrency, cleanup, and recovery](gh-docs/en/concurrency-recovery.md)
|
|
27
|
+
- [Ports and adapters](gh-docs/en/adapters.md)
|
|
28
|
+
- [Troubleshooting and operator runbook](gh-docs/en/troubleshooting.md)
|
|
29
|
+
- [CI/CD and automated security](gh-docs/en/ci-cd.md)
|
|
30
|
+
- [Glossary](gh-docs/en/glossary.md)
|
|
31
|
+
|
|
32
|
+
DeepWiki provides a generated, code-oriented view that complements the maintained portal; when the
|
|
33
|
+
two differ, the repository documentation and implementation are authoritative.
|
|
34
|
+
|
|
35
|
+
## Quick start
|
|
36
|
+
|
|
37
|
+
Python 3.12, Git, and Docker Compose v2 are required. On Windows, use Docker Desktop with Linux
|
|
38
|
+
containers; on Linux, use Docker Engine with the Compose v2 plugin.
|
|
39
|
+
|
|
40
|
+
### PyPI application
|
|
41
|
+
|
|
42
|
+
Mini-Runbot is packaged as a CLI application. After the first PyPI release, install it in an
|
|
43
|
+
isolated environment with `pipx`:
|
|
44
|
+
|
|
45
|
+
```console
|
|
46
|
+
pipx install mini-runbot
|
|
47
|
+
mini-runbot init
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Until that release, clone the repository and run `pipx install .`, or use the development setup
|
|
51
|
+
below. `init` asks for runtime and allowed-repository settings and refuses to overwrite an existing
|
|
52
|
+
`config.local.yaml`. Use `mini-runbot init --defaults` for the bundled example values.
|
|
53
|
+
|
|
54
|
+
### Development installation
|
|
55
|
+
|
|
56
|
+
#### Windows · PowerShell
|
|
57
|
+
|
|
58
|
+
```powershell
|
|
59
|
+
py -3.12 -m venv .venv
|
|
60
|
+
.venv\Scripts\Activate.ps1
|
|
61
|
+
python -m pip install -e ".[dev]"
|
|
62
|
+
mini-runbot init
|
|
63
|
+
$env:MINI_RUNBOT_CONFIG = (Resolve-Path config.local.yaml)
|
|
64
|
+
mini-runbot doctor
|
|
65
|
+
mini-runbot serve --reload
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
#### Linux · Bash
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
python3.12 -m venv .venv
|
|
72
|
+
source .venv/bin/activate
|
|
73
|
+
python -m pip install -e ".[dev]"
|
|
74
|
+
mini-runbot init
|
|
75
|
+
export MINI_RUNBOT_CONFIG="$(realpath config.local.yaml)"
|
|
76
|
+
mini-runbot doctor
|
|
77
|
+
mini-runbot serve --reload
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Open `http://127.0.0.1:8000`, or create a build from the CLI:
|
|
81
|
+
|
|
82
|
+
```console
|
|
83
|
+
mini-runbot build create --repo custom --ref 16.0 --modules module_a,module_b --run
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Repository inputs are configured aliases, never arbitrary URLs or filesystem paths supplied by a
|
|
87
|
+
client.
|
|
88
|
+
|
|
89
|
+
## Local checks
|
|
90
|
+
|
|
91
|
+
The same commands work in an activated PowerShell or Bash environment:
|
|
92
|
+
|
|
93
|
+
```console
|
|
94
|
+
python -m pip install -e ".[dev,docs]"
|
|
95
|
+
python -m pytest -m "not docker"
|
|
96
|
+
python -m ruff check .
|
|
97
|
+
node --check src/mini_runbot/web/app.js
|
|
98
|
+
python -m build
|
|
99
|
+
python scripts/check_distribution.py dist
|
|
100
|
+
python scripts/check_docs.py
|
|
101
|
+
python -m mkdocs build --strict --config-file gh-docs/mkdocs.es.yml --site-dir ../site
|
|
102
|
+
python -m mkdocs build --strict --config-file gh-docs/mkdocs.en.yml --site-dir ../site/en
|
|
103
|
+
git diff --check
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Docker-marked end-to-end tests remain an explicit local check because they require a working daemon,
|
|
107
|
+
image registry access, and real Odoo startup.
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=75"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "mini-runbot"
|
|
7
|
+
version = "0.2.0"
|
|
8
|
+
description = "A small, local build orchestrator proof of concept for Odoo 16"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.12"
|
|
11
|
+
authors = [{ name = "rafnixg" }]
|
|
12
|
+
keywords = ["odoo", "docker", "runbot", "testing", "ephemeral-environments"]
|
|
13
|
+
classifiers = [
|
|
14
|
+
"Development Status :: 3 - Alpha",
|
|
15
|
+
"Environment :: Console",
|
|
16
|
+
"Framework :: FastAPI",
|
|
17
|
+
"Intended Audience :: Developers",
|
|
18
|
+
"Operating System :: Microsoft :: Windows",
|
|
19
|
+
"Operating System :: POSIX :: Linux",
|
|
20
|
+
"Programming Language :: Python :: 3",
|
|
21
|
+
"Programming Language :: Python :: 3.12",
|
|
22
|
+
"Topic :: Software Development :: Testing",
|
|
23
|
+
]
|
|
24
|
+
dependencies = [
|
|
25
|
+
"fastapi>=0.115,<1",
|
|
26
|
+
"jinja2>=3.1,<4",
|
|
27
|
+
"pydantic>=2.9,<3",
|
|
28
|
+
"pyyaml>=6,<7",
|
|
29
|
+
"rich>=13,<15",
|
|
30
|
+
"sqlalchemy>=2.0,<3",
|
|
31
|
+
"typer>=0.12,<1",
|
|
32
|
+
"uvicorn>=0.30,<1",
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
[project.optional-dependencies]
|
|
36
|
+
dev = [
|
|
37
|
+
"build>=1,<2",
|
|
38
|
+
"httpx>=0.27,<1",
|
|
39
|
+
"pytest>=8,<9",
|
|
40
|
+
"pytest-asyncio>=0.24,<1",
|
|
41
|
+
"pytest-cov>=5,<7",
|
|
42
|
+
"ruff>=0.8,<1",
|
|
43
|
+
]
|
|
44
|
+
docs = [
|
|
45
|
+
"mkdocs-material>=9,<10",
|
|
46
|
+
]
|
|
47
|
+
|
|
48
|
+
[project.scripts]
|
|
49
|
+
mini-runbot = "mini_runbot.cli.main:app"
|
|
50
|
+
|
|
51
|
+
[project.urls]
|
|
52
|
+
Documentation = "https://rafnixg.github.io/odoo-platform-ephemeral-environment/"
|
|
53
|
+
Repository = "https://github.com/rafnixg/odoo-platform-ephemeral-environment"
|
|
54
|
+
Issues = "https://github.com/rafnixg/odoo-platform-ephemeral-environment/issues"
|
|
55
|
+
DeepWiki = "https://deepwiki.com/rafnixg/odoo-platform-ephemeral-environment"
|
|
56
|
+
|
|
57
|
+
[tool.setuptools.packages.find]
|
|
58
|
+
where = ["src"]
|
|
59
|
+
|
|
60
|
+
[tool.setuptools.package-data]
|
|
61
|
+
mini_runbot = [
|
|
62
|
+
"resources/*.yaml",
|
|
63
|
+
"templates/*.j2",
|
|
64
|
+
"web/*.html",
|
|
65
|
+
"web/*.css",
|
|
66
|
+
"web/*.js",
|
|
67
|
+
]
|
|
68
|
+
|
|
69
|
+
[tool.pytest.ini_options]
|
|
70
|
+
testpaths = ["tests"]
|
|
71
|
+
addopts = "-q"
|
|
72
|
+
asyncio_default_fixture_loop_scope = "function"
|
|
73
|
+
markers = ["docker: requires an available Docker daemon"]
|
|
74
|
+
|
|
75
|
+
[tool.ruff]
|
|
76
|
+
line-length = 100
|
|
77
|
+
target-version = "py312"
|
|
78
|
+
|
|
79
|
+
[tool.ruff.lint]
|
|
80
|
+
select = ["E", "F", "I", "UP", "B"]
|
|
81
|
+
|
|
82
|
+
[tool.ruff.lint.per-file-ignores]
|
|
83
|
+
"**/__manifest__.py" = ["B018"]
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import subprocess
|
|
4
|
+
from dataclasses import replace
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from urllib.parse import urlsplit
|
|
7
|
+
|
|
8
|
+
from mini_runbot.config import Settings
|
|
9
|
+
from mini_runbot.domain.errors import GitOperationError, UnsafePathError
|
|
10
|
+
from mini_runbot.domain.models import RepositoryRevision
|
|
11
|
+
|
|
12
|
+
REMOTE_GIT_SCHEMES = {"https", "ssh"}
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def is_remote_git_url(value: str) -> bool:
|
|
16
|
+
"""Return whether a configured source is an allowed remote Git URL."""
|
|
17
|
+
parsed = urlsplit(value)
|
|
18
|
+
if parsed.scheme.lower() in REMOTE_GIT_SCHEMES:
|
|
19
|
+
return True
|
|
20
|
+
return value.startswith("git@") and ":" in value.partition("@")[2]
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def validate_remote_git_url(value: str) -> None:
|
|
24
|
+
parsed = urlsplit(value)
|
|
25
|
+
if parsed.scheme:
|
|
26
|
+
if parsed.scheme.lower() not in REMOTE_GIT_SCHEMES:
|
|
27
|
+
raise GitOperationError(
|
|
28
|
+
"Remote repository URL must use HTTPS or SSH"
|
|
29
|
+
)
|
|
30
|
+
if not parsed.hostname:
|
|
31
|
+
raise GitOperationError("Remote repository URL must include a host")
|
|
32
|
+
if parsed.scheme.lower() == "https" and (parsed.username or parsed.password):
|
|
33
|
+
raise GitOperationError(
|
|
34
|
+
"HTTPS repository URLs must not contain embedded credentials"
|
|
35
|
+
)
|
|
36
|
+
if parsed.password:
|
|
37
|
+
raise GitOperationError(
|
|
38
|
+
"SSH repository URLs must not contain an embedded password"
|
|
39
|
+
)
|
|
40
|
+
return
|
|
41
|
+
if value.startswith("git@") and ":" in value.partition("@")[2]:
|
|
42
|
+
return
|
|
43
|
+
raise GitOperationError("Remote repository URL must use HTTPS or SSH")
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class GitCliService:
|
|
47
|
+
def __init__(self, settings: Settings) -> None:
|
|
48
|
+
self.settings = settings
|
|
49
|
+
|
|
50
|
+
def checkout(
|
|
51
|
+
self,
|
|
52
|
+
revision: RepositoryRevision,
|
|
53
|
+
workspace_path: Path,
|
|
54
|
+
log_path: Path,
|
|
55
|
+
) -> RepositoryRevision:
|
|
56
|
+
config = self.settings.repository(revision.name, revision.requested_ref)
|
|
57
|
+
sources_root = (workspace_path / "sources").resolve()
|
|
58
|
+
target = (sources_root / config.target).resolve()
|
|
59
|
+
if target.parent != sources_root:
|
|
60
|
+
raise UnsafePathError(f"Checkout target escaped sources directory: {target}")
|
|
61
|
+
if target.exists():
|
|
62
|
+
raise GitOperationError(f"Checkout target already exists: {target}")
|
|
63
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
64
|
+
|
|
65
|
+
log_path.parent.mkdir(parents=True, exist_ok=True)
|
|
66
|
+
if is_remote_git_url(config.url) or "://" in config.url:
|
|
67
|
+
source = config.url
|
|
68
|
+
sha, output = self._checkout_remote(source, revision.requested_ref, target)
|
|
69
|
+
else:
|
|
70
|
+
source_path = Path(config.url).expanduser().resolve()
|
|
71
|
+
if not source_path.is_dir():
|
|
72
|
+
raise GitOperationError(
|
|
73
|
+
f"Configured repository does not exist: {source_path}"
|
|
74
|
+
)
|
|
75
|
+
source_check = self._run(
|
|
76
|
+
["git", "-C", str(source_path), "rev-parse", "--is-inside-work-tree"]
|
|
77
|
+
)
|
|
78
|
+
if source_check.strip() != "true":
|
|
79
|
+
raise GitOperationError(
|
|
80
|
+
f"Configured source is not a Git repository: {source_path}"
|
|
81
|
+
)
|
|
82
|
+
sha = self._run(
|
|
83
|
+
[
|
|
84
|
+
"git",
|
|
85
|
+
"-C",
|
|
86
|
+
str(source_path),
|
|
87
|
+
"rev-parse",
|
|
88
|
+
"--verify",
|
|
89
|
+
f"{revision.requested_ref}^{{commit}}",
|
|
90
|
+
]
|
|
91
|
+
).strip()
|
|
92
|
+
output = self._run(
|
|
93
|
+
["git", "clone", "--no-checkout", "--", str(source_path), str(target)]
|
|
94
|
+
)
|
|
95
|
+
output += self._run(
|
|
96
|
+
["git", "-C", str(target), "checkout", "--detach", sha]
|
|
97
|
+
)
|
|
98
|
+
source = str(source_path)
|
|
99
|
+
with log_path.open("a", encoding="utf-8") as log:
|
|
100
|
+
log.write(f"== {revision.name} @ {revision.requested_ref} ==\n")
|
|
101
|
+
log.write(output)
|
|
102
|
+
if output and not output.endswith("\n"):
|
|
103
|
+
log.write("\n")
|
|
104
|
+
addons_path = (target / config.addons_subpath).resolve()
|
|
105
|
+
if target not in {addons_path, *addons_path.parents} or not addons_path.is_dir():
|
|
106
|
+
raise GitOperationError(
|
|
107
|
+
f"Configured addons_subpath does not exist inside checkout: {config.addons_subpath}"
|
|
108
|
+
)
|
|
109
|
+
return replace(
|
|
110
|
+
revision,
|
|
111
|
+
source=source,
|
|
112
|
+
commit_sha=sha,
|
|
113
|
+
checkout_path=str(addons_path),
|
|
114
|
+
addons_priority=config.addons_priority,
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
def _checkout_remote(
|
|
118
|
+
self, source: str, requested_ref: str, target: Path
|
|
119
|
+
) -> tuple[str, str]:
|
|
120
|
+
validate_remote_git_url(source)
|
|
121
|
+
target.mkdir()
|
|
122
|
+
output = self._run(["git", "init", str(target)])
|
|
123
|
+
output += self._run(
|
|
124
|
+
["git", "-C", str(target), "remote", "add", "origin", source]
|
|
125
|
+
)
|
|
126
|
+
output += self._run(
|
|
127
|
+
[
|
|
128
|
+
"git",
|
|
129
|
+
"-C",
|
|
130
|
+
str(target),
|
|
131
|
+
"fetch",
|
|
132
|
+
"--depth=1",
|
|
133
|
+
"--no-tags",
|
|
134
|
+
"origin",
|
|
135
|
+
requested_ref,
|
|
136
|
+
]
|
|
137
|
+
)
|
|
138
|
+
sha = self._run(
|
|
139
|
+
[
|
|
140
|
+
"git",
|
|
141
|
+
"-C",
|
|
142
|
+
str(target),
|
|
143
|
+
"rev-parse",
|
|
144
|
+
"--verify",
|
|
145
|
+
"FETCH_HEAD^{commit}",
|
|
146
|
+
]
|
|
147
|
+
).strip()
|
|
148
|
+
output += self._run(
|
|
149
|
+
["git", "-C", str(target), "checkout", "--detach", sha]
|
|
150
|
+
)
|
|
151
|
+
return sha, output
|
|
152
|
+
|
|
153
|
+
@staticmethod
|
|
154
|
+
def _run(arguments: list[str]) -> str:
|
|
155
|
+
try:
|
|
156
|
+
result = subprocess.run(
|
|
157
|
+
arguments, capture_output=True, text=True, timeout=120, check=False
|
|
158
|
+
)
|
|
159
|
+
except (OSError, subprocess.TimeoutExpired) as exc:
|
|
160
|
+
raise GitOperationError(f"Git command could not run: {exc}") from exc
|
|
161
|
+
output = f"{result.stdout}{result.stderr}"
|
|
162
|
+
if result.returncode != 0:
|
|
163
|
+
safe_command = " ".join(arguments[:3])
|
|
164
|
+
raise GitOperationError(f"Git command failed ({safe_command}): {output[-500:]}")
|
|
165
|
+
return output
|