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.
Files changed (38) hide show
  1. mini_runbot-0.2.0/PKG-INFO +146 -0
  2. mini_runbot-0.2.0/README.md +107 -0
  3. mini_runbot-0.2.0/pyproject.toml +83 -0
  4. mini_runbot-0.2.0/setup.cfg +4 -0
  5. mini_runbot-0.2.0/src/mini_runbot/__init__.py +3 -0
  6. mini_runbot-0.2.0/src/mini_runbot/adapters/git/cli.py +165 -0
  7. mini_runbot-0.2.0/src/mini_runbot/adapters/persistence/sqlite.py +270 -0
  8. mini_runbot-0.2.0/src/mini_runbot/adapters/runtime/compose.py +323 -0
  9. mini_runbot-0.2.0/src/mini_runbot/adapters/runtime/local.py +30 -0
  10. mini_runbot-0.2.0/src/mini_runbot/adapters/runtime/ports.py +49 -0
  11. mini_runbot-0.2.0/src/mini_runbot/api/app.py +147 -0
  12. mini_runbot-0.2.0/src/mini_runbot/api/schemas.py +73 -0
  13. mini_runbot-0.2.0/src/mini_runbot/application/build_manager.py +390 -0
  14. mini_runbot-0.2.0/src/mini_runbot/application/executor.py +41 -0
  15. mini_runbot-0.2.0/src/mini_runbot/application/scheduler.py +60 -0
  16. mini_runbot-0.2.0/src/mini_runbot/bootstrap.py +28 -0
  17. mini_runbot-0.2.0/src/mini_runbot/cli/init_config.py +228 -0
  18. mini_runbot-0.2.0/src/mini_runbot/cli/main.py +346 -0
  19. mini_runbot-0.2.0/src/mini_runbot/cli/presentation.py +167 -0
  20. mini_runbot-0.2.0/src/mini_runbot/config.py +129 -0
  21. mini_runbot-0.2.0/src/mini_runbot/domain/enums.py +24 -0
  22. mini_runbot-0.2.0/src/mini_runbot/domain/errors.py +45 -0
  23. mini_runbot-0.2.0/src/mini_runbot/domain/models.py +72 -0
  24. mini_runbot-0.2.0/src/mini_runbot/domain/transitions.py +43 -0
  25. mini_runbot-0.2.0/src/mini_runbot/domain/validation.py +88 -0
  26. mini_runbot-0.2.0/src/mini_runbot/ports/repositories.py +23 -0
  27. mini_runbot-0.2.0/src/mini_runbot/ports/services.py +62 -0
  28. mini_runbot-0.2.0/src/mini_runbot/resources/config.example.yaml +22 -0
  29. mini_runbot-0.2.0/src/mini_runbot/templates/compose.yaml.j2 +49 -0
  30. mini_runbot-0.2.0/src/mini_runbot/web/app.js +247 -0
  31. mini_runbot-0.2.0/src/mini_runbot/web/index.html +94 -0
  32. mini_runbot-0.2.0/src/mini_runbot/web/styles.css +148 -0
  33. mini_runbot-0.2.0/src/mini_runbot.egg-info/PKG-INFO +146 -0
  34. mini_runbot-0.2.0/src/mini_runbot.egg-info/SOURCES.txt +36 -0
  35. mini_runbot-0.2.0/src/mini_runbot.egg-info/dependency_links.txt +1 -0
  36. mini_runbot-0.2.0/src/mini_runbot.egg-info/entry_points.txt +2 -0
  37. mini_runbot-0.2.0/src/mini_runbot.egg-info/requires.txt +19 -0
  38. 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
+ [![CI](https://github.com/rafnixg/odoo-platform-ephemeral-environment/actions/workflows/ci.yml/badge.svg)](https://github.com/rafnixg/odoo-platform-ephemeral-environment/actions/workflows/ci.yml)
43
+ [![Documentation](https://github.com/rafnixg/odoo-platform-ephemeral-environment/actions/workflows/docs.yml/badge.svg)](https://github.com/rafnixg/odoo-platform-ephemeral-environment/actions/workflows/docs.yml)
44
+ [![CodeQL](https://github.com/rafnixg/odoo-platform-ephemeral-environment/actions/workflows/codeql.yml/badge.svg)](https://github.com/rafnixg/odoo-platform-ephemeral-environment/actions/workflows/codeql.yml)
45
+ [![Ask DeepWiki](https://deepwiki.com/badge.svg)](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
+ [![CI](https://github.com/rafnixg/odoo-platform-ephemeral-environment/actions/workflows/ci.yml/badge.svg)](https://github.com/rafnixg/odoo-platform-ephemeral-environment/actions/workflows/ci.yml)
4
+ [![Documentation](https://github.com/rafnixg/odoo-platform-ephemeral-environment/actions/workflows/docs.yml/badge.svg)](https://github.com/rafnixg/odoo-platform-ephemeral-environment/actions/workflows/docs.yml)
5
+ [![CodeQL](https://github.com/rafnixg/odoo-platform-ephemeral-environment/actions/workflows/codeql.yml/badge.svg)](https://github.com/rafnixg/odoo-platform-ephemeral-environment/actions/workflows/codeql.yml)
6
+ [![Ask DeepWiki](https://deepwiki.com/badge.svg)](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,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,3 @@
1
+ """Mini-Runbot proof of concept."""
2
+
3
+ __version__ = "0.2.0"
@@ -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