bb-run 1.0.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.
- bb_run-1.0.0.dist-info/METADATA +248 -0
- bb_run-1.0.0.dist-info/RECORD +11 -0
- bb_run-1.0.0.dist-info/WHEEL +5 -0
- bb_run-1.0.0.dist-info/entry_points.txt +2 -0
- bb_run-1.0.0.dist-info/licenses/LICENSE +21 -0
- bb_run-1.0.0.dist-info/top_level.txt +1 -0
- bbrun/__init__.py +13 -0
- bbrun/cli.py +186 -0
- bbrun/docker.py +224 -0
- bbrun/host.py +186 -0
- bbrun/validator.py +107 -0
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: bb-run
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Run Bitbucket Pipelines locally
|
|
5
|
+
Author-email: Karl Hill <karlhillx@gmail.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/karlhillx/bb-run
|
|
8
|
+
Requires-Python: >=3.12
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Requires-Dist: PyYAML>=6.0
|
|
12
|
+
Provides-Extra: test
|
|
13
|
+
Requires-Dist: pytest>=8.0; extra == "test"
|
|
14
|
+
Requires-Dist: pytest-cov>=4.0; extra == "test"
|
|
15
|
+
Provides-Extra: dev
|
|
16
|
+
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
17
|
+
Requires-Dist: pytest-cov>=4.0; extra == "dev"
|
|
18
|
+
Requires-Dist: ruff>=0.4; extra == "dev"
|
|
19
|
+
Dynamic: license-file
|
|
20
|
+
|
|
21
|
+
# bb-run
|
|
22
|
+
|
|
23
|
+
[](https://github.com/karlhillx/bb-run/blob/main/pyproject.toml)
|
|
24
|
+
[](https://opensource.org/licenses/MIT)
|
|
25
|
+
[](https://github.com/karlhillx/bb-run/blob/main/pyproject.toml)
|
|
26
|
+
[](https://github.com/karlhillx/bb-run/actions/workflows/test.yml)
|
|
27
|
+
|
|
28
|
+
**Run Bitbucket Pipelines locally.** bb-run faithfully executes your `bitbucket-pipelines.yml` on your local machine using Docker or directly on your host.
|
|
29
|
+
|
|
30
|
+
## Why bb-run?
|
|
31
|
+
|
|
32
|
+
- **Test before pushing** - Catch CI failures locally before committing
|
|
33
|
+
- **Fast iteration** - No waiting for Bitbucket's pipeline queue
|
|
34
|
+
- **Debug easily** - Run in verbose mode, inspect outputs directly
|
|
35
|
+
- **Two modes** - Docker for Bitbucket-accurate execution, Host for quick local testing
|
|
36
|
+
- **No dependencies** - Works with just Python and Docker (optional)
|
|
37
|
+
|
|
38
|
+
## Installation
|
|
39
|
+
|
|
40
|
+
### via pip
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
pip install bb-run
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### via Homebrew
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
brew install karlhillx/tap/bb-run
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### from source
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
git clone https://github.com/karlhillx/bb-run.git
|
|
56
|
+
cd bb-run
|
|
57
|
+
pip install -e .
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Quick Start
|
|
61
|
+
|
|
62
|
+
### Validate a pipeline (instant)
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
bb-run --validate
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### Run the default pipeline
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
bb-run
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### Run a specific branch
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
bb-run --target branches.main
|
|
78
|
+
bb-run -t branches.main
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### Simulate a feature branch
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
bb-run --branch feature/my-work
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### Run on your host (no Docker)
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
bb-run --mode host
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### Pass variables
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
bb-run -v ENVIRONMENT=staging -v API_KEY=secret
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### List available targets
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
bb-run --list-targets
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## Modes
|
|
106
|
+
|
|
107
|
+
### Docker Mode (default)
|
|
108
|
+
|
|
109
|
+
Runs steps in Docker containers matching Bitbucket's build environment.
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
bb-run --mode docker
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
**Pros:** Faithful reproduction of Bitbucket's environment
|
|
116
|
+
**Cons:** Requires Docker, images may take time to download
|
|
117
|
+
|
|
118
|
+
### Host Mode
|
|
119
|
+
|
|
120
|
+
Runs steps directly on your local machine.
|
|
121
|
+
|
|
122
|
+
```bash
|
|
123
|
+
bb-run --mode host
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
**Pros:** Fast, no image downloads
|
|
127
|
+
**Cons:** May differ from Bitbucket's environment (Python vs Python3, etc.)
|
|
128
|
+
|
|
129
|
+
## Examples
|
|
130
|
+
|
|
131
|
+
### Python project
|
|
132
|
+
|
|
133
|
+
```bash
|
|
134
|
+
cd my-python-project
|
|
135
|
+
bb-run
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
### Node.js project
|
|
139
|
+
|
|
140
|
+
```bash
|
|
141
|
+
cd my-node-project
|
|
142
|
+
bb-run --target branches.main
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
### Run with verbose output
|
|
146
|
+
|
|
147
|
+
```bash
|
|
148
|
+
bb-run --verbose
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
## Configuration
|
|
152
|
+
|
|
153
|
+
bb-run automatically looks for `bitbucket-pipelines.yml` in your current directory. Use `--repo` to specify a different path:
|
|
154
|
+
|
|
155
|
+
```bash
|
|
156
|
+
bb-run --repo /path/to/repo
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
## Requirements
|
|
160
|
+
|
|
161
|
+
- Python 3.12+
|
|
162
|
+
- PyYAML
|
|
163
|
+
- Docker (for Docker mode)
|
|
164
|
+
|
|
165
|
+
### Local development (virtualenv)
|
|
166
|
+
|
|
167
|
+
Use Python 3.12 for the project venv so `python --version` matches `requires-python` in `pyproject.toml`:
|
|
168
|
+
|
|
169
|
+
```bash
|
|
170
|
+
# macOS (Homebrew)
|
|
171
|
+
brew install python@3.12
|
|
172
|
+
"$(brew --prefix python@3.12)/bin/python3.12" -m venv .venv
|
|
173
|
+
source .venv/bin/activate
|
|
174
|
+
pip install -e ".[dev]"
|
|
175
|
+
# or tests + coverage only: pip install -e ".[test]"
|
|
176
|
+
python -m pytest
|
|
177
|
+
python -m pytest --cov=bbrun --cov-report=xml tests/
|
|
178
|
+
ruff check bbrun
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
## Environment Variables
|
|
182
|
+
|
|
183
|
+
bb-run sets these Bitbucket-specific environment variables:
|
|
184
|
+
|
|
185
|
+
| Variable | Description |
|
|
186
|
+
|----------|-------------|
|
|
187
|
+
| `BITBUCKET_BUILD_NUMBER` | Build number (set to "1") |
|
|
188
|
+
| `BITBUCKET_CLONE_DIR` | Repository path |
|
|
189
|
+
| `BITBUCKET_COMMIT` | Git commit SHA |
|
|
190
|
+
| `BITBUCKET_BRANCH` | Branch name |
|
|
191
|
+
| `BITBUCKET_REPO_SLUG` | Repository name |
|
|
192
|
+
| `BITBUCKET_REPO_UUID` | Unique run ID |
|
|
193
|
+
| `BITBUCKET_WORKSPACE` | Workspace (set to "local") |
|
|
194
|
+
|
|
195
|
+
## Troubleshooting
|
|
196
|
+
|
|
197
|
+
### "Docker is not available"
|
|
198
|
+
|
|
199
|
+
Use `--mode host` to run on your local machine instead of in Docker:
|
|
200
|
+
|
|
201
|
+
```bash
|
|
202
|
+
bb-run --mode host
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
### "pip: command not found"
|
|
206
|
+
|
|
207
|
+
bb-run automatically translates `pip` to `pip3` and adds `--break-system-packages` for PEP 668 environments.
|
|
208
|
+
|
|
209
|
+
### `pytest: error: unrecognized arguments: --cov=...`
|
|
210
|
+
|
|
211
|
+
Coverage flags come from the **pytest-cov** plugin. Install the `test` or `dev` extra, then use the same interpreter for pytest:
|
|
212
|
+
|
|
213
|
+
```bash
|
|
214
|
+
pip install -e ".[dev]"
|
|
215
|
+
# or: pip install -e ".[test]"
|
|
216
|
+
python -m pytest --cov=bbrun --cov-report=xml tests/
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
### Image pull failures
|
|
220
|
+
|
|
221
|
+
Docker Hub rate limits may cause image downloads to fail. Try:
|
|
222
|
+
1. Waiting and retrying later
|
|
223
|
+
2. Using `--mode host` temporarily
|
|
224
|
+
3. Configuring a Docker mirror
|
|
225
|
+
|
|
226
|
+
## License
|
|
227
|
+
|
|
228
|
+
MIT License - see [LICENSE](LICENSE) for details.
|
|
229
|
+
|
|
230
|
+
## Contributing
|
|
231
|
+
|
|
232
|
+
Contributions welcome! Please open an issue or submit a PR.
|
|
233
|
+
|
|
234
|
+
## Publishing to PyPI
|
|
235
|
+
|
|
236
|
+
The package name **`bb-run`** must exist on PyPI (first upload is manual or via this workflow after [trusted publishing](https://docs.pypi.org/trusted-publishers/adding-a-publisher/) is configured).
|
|
237
|
+
|
|
238
|
+
1. In PyPI, add a **pending publisher** for this GitHub repo and workflow `publish.yml`, environment **`pypi`**.
|
|
239
|
+
2. In GitHub ā **Settings ā Environments**, create environment **`pypi`** (no secrets needed for trusted publishing).
|
|
240
|
+
3. Bump `version` in `pyproject.toml`, merge, then **create a GitHub Release** (or run the workflow manually after a release).
|
|
241
|
+
|
|
242
|
+
Badges in this README use **GitHub** (version from `pyproject.toml` on `main`) so they stay valid before the first PyPI release. After publishing, you can add e.g. `https://img.shields.io/pypi/v/bb-run.svg`.
|
|
243
|
+
|
|
244
|
+
## Links
|
|
245
|
+
|
|
246
|
+
- [PyPI project](https://pypi.org/project/bb-run/) (live after first successful upload)
|
|
247
|
+
- [GitHub Repository](https://github.com/karlhillx/bb-run)
|
|
248
|
+
- [Issue Tracker](https://github.com/karlhillx/bb-run/issues)
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
bb_run-1.0.0.dist-info/licenses/LICENSE,sha256=w7gLMqk8rKX33vCkMQJFyR-H8zvFG7BXDWeqjUbhNAQ,1065
|
|
2
|
+
bbrun/__init__.py,sha256=o8_eaSXjj9zhhn23Ya2dTS_84tz7VsKWIFKaO0HAjGY,249
|
|
3
|
+
bbrun/cli.py,sha256=lewHyEHX3b1Rxb2HhJ8i2Oq_w7jREkof8DOINhRMjJQ,4879
|
|
4
|
+
bbrun/docker.py,sha256=MP7JWO7UaoHtGDwhCMt3LNC6Zd6N1uP1a6MB7dZTS7E,7111
|
|
5
|
+
bbrun/host.py,sha256=Uf_UCNsSEmtPTvSHarUiguQhVAKuJzOwVVaRtdsdj2s,6048
|
|
6
|
+
bbrun/validator.py,sha256=FxdOt-MwNnWtoDT0_A5qS5PUjgrFJLUkmCPIsQu_KxY,3389
|
|
7
|
+
bb_run-1.0.0.dist-info/METADATA,sha256=jnxgLWOJEmpCvdGQc_yO9Ij9cvL8MyBe8vfrJR-3sug,6338
|
|
8
|
+
bb_run-1.0.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
9
|
+
bb_run-1.0.0.dist-info/entry_points.txt,sha256=M4-RuBKtKFsdQBGHvloxQNAzSzcgEjCUefJU4eX0YfM,42
|
|
10
|
+
bb_run-1.0.0.dist-info/top_level.txt,sha256=x1SUojzkaTzH4xk5aFHKUrjL3nbgJGaK3fIxv8QdSY4,6
|
|
11
|
+
bb_run-1.0.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Karl Hill
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
bbrun
|
bbrun/__init__.py
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""
|
|
2
|
+
bb-run - Bitbucket Pipelines Local Runner
|
|
3
|
+
|
|
4
|
+
Faithfully runs bitbucket-pipelines.yml locally using Docker or your host environment.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from .cli import main
|
|
8
|
+
|
|
9
|
+
__version__ = "0.1.0"
|
|
10
|
+
__author__ = "Karl Hill"
|
|
11
|
+
__license__ = "MIT"
|
|
12
|
+
|
|
13
|
+
__all__ = ['main']
|
bbrun/cli.py
ADDED
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
bb-run CLI - Bitbucket Pipelines Local Runner
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import sys
|
|
7
|
+
import argparse
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
from .validator import PipelineValidator
|
|
11
|
+
from .docker import DockerRunner
|
|
12
|
+
from .host import HostRunner
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def list_targets(repo_path: Path) -> int:
|
|
16
|
+
"""List available pipeline targets."""
|
|
17
|
+
validator = PipelineValidator(repo_path)
|
|
18
|
+
config = validator.load()
|
|
19
|
+
|
|
20
|
+
if not config:
|
|
21
|
+
return 1
|
|
22
|
+
|
|
23
|
+
print("Available pipeline targets:")
|
|
24
|
+
print("\n default")
|
|
25
|
+
|
|
26
|
+
pipelines = config.get('pipelines', {})
|
|
27
|
+
|
|
28
|
+
branches = pipelines.get('branches', {})
|
|
29
|
+
for branch in sorted(branches.keys()):
|
|
30
|
+
print(f" branches.{branch}")
|
|
31
|
+
|
|
32
|
+
tags = pipelines.get('tags', {})
|
|
33
|
+
for tag in sorted(tags.keys()):
|
|
34
|
+
print(f" tags.{tag}")
|
|
35
|
+
|
|
36
|
+
for name in pipelines:
|
|
37
|
+
if name not in ['default', 'branches', 'tags']:
|
|
38
|
+
print(f" {name}")
|
|
39
|
+
|
|
40
|
+
image = config.get('image', 'atlassian/default-image:latest')
|
|
41
|
+
print(f"\nDefault image: {image}")
|
|
42
|
+
|
|
43
|
+
return 0
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def run_pipeline(
|
|
47
|
+
repo_path: Path,
|
|
48
|
+
target: str,
|
|
49
|
+
branch: str,
|
|
50
|
+
variables: dict,
|
|
51
|
+
mode: str,
|
|
52
|
+
verbose: bool
|
|
53
|
+
) -> int:
|
|
54
|
+
"""Run a pipeline in the specified mode."""
|
|
55
|
+
|
|
56
|
+
if mode == 'docker':
|
|
57
|
+
runner = DockerRunner(repo_path)
|
|
58
|
+
else:
|
|
59
|
+
runner = HostRunner(repo_path)
|
|
60
|
+
|
|
61
|
+
success = runner.run(
|
|
62
|
+
target=target,
|
|
63
|
+
branch=branch,
|
|
64
|
+
variables=variables,
|
|
65
|
+
verbose=verbose
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
return 0 if success else 1
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def validate(repo_path: Path) -> int:
|
|
72
|
+
"""Validate a pipeline YAML file."""
|
|
73
|
+
validator = PipelineValidator(repo_path)
|
|
74
|
+
|
|
75
|
+
if validator.validate():
|
|
76
|
+
print("ā
Valid bitbucket-pipelines.yml")
|
|
77
|
+
validator.show_summary()
|
|
78
|
+
return 0
|
|
79
|
+
else:
|
|
80
|
+
print("ā Invalid or missing bitbucket-pipelines.yml")
|
|
81
|
+
return 1
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def main():
|
|
85
|
+
parser = argparse.ArgumentParser(
|
|
86
|
+
prog='bb-run',
|
|
87
|
+
description='Run Bitbucket Pipelines locally',
|
|
88
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
89
|
+
epilog="""
|
|
90
|
+
Examples:
|
|
91
|
+
bb-run # Run default pipeline
|
|
92
|
+
bb-run --target master # Run master branch pipeline
|
|
93
|
+
bb-run --repo /path/to/repo # Run in specific repo
|
|
94
|
+
bb-run --branch feature-x # Simulate running on a branch
|
|
95
|
+
bb-run --mode host # Run on host (no Docker)
|
|
96
|
+
bb-run --mode docker # Run in Docker (default)
|
|
97
|
+
bb-run -v KEY=VALUE # Pass variables
|
|
98
|
+
bb-run --list-targets # List available targets
|
|
99
|
+
bb-run --validate # Validate YAML only
|
|
100
|
+
"""
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
parser.add_argument(
|
|
104
|
+
'--repo', '-r',
|
|
105
|
+
default='.',
|
|
106
|
+
help='Path to repository (default: current directory)'
|
|
107
|
+
)
|
|
108
|
+
parser.add_argument(
|
|
109
|
+
'--target', '-t',
|
|
110
|
+
default='default',
|
|
111
|
+
help='Pipeline target (default: default)'
|
|
112
|
+
)
|
|
113
|
+
parser.add_argument(
|
|
114
|
+
'--branch', '-b',
|
|
115
|
+
default='LOCAL',
|
|
116
|
+
help='Branch name to simulate (default: LOCAL)'
|
|
117
|
+
)
|
|
118
|
+
parser.add_argument(
|
|
119
|
+
'--mode', '-m',
|
|
120
|
+
choices=['docker', 'host'],
|
|
121
|
+
default='docker',
|
|
122
|
+
help='Execution mode (default: docker)'
|
|
123
|
+
)
|
|
124
|
+
parser.add_argument(
|
|
125
|
+
'--variables', '-v',
|
|
126
|
+
action='append',
|
|
127
|
+
help='Variables in KEY=VALUE format'
|
|
128
|
+
)
|
|
129
|
+
parser.add_argument(
|
|
130
|
+
'--list-targets',
|
|
131
|
+
action='store_true',
|
|
132
|
+
help='List available pipeline targets and exit'
|
|
133
|
+
)
|
|
134
|
+
parser.add_argument(
|
|
135
|
+
'--validate',
|
|
136
|
+
action='store_true',
|
|
137
|
+
help='Validate YAML only, do not run'
|
|
138
|
+
)
|
|
139
|
+
parser.add_argument(
|
|
140
|
+
'--verbose',
|
|
141
|
+
action='store_true',
|
|
142
|
+
help='Verbose output'
|
|
143
|
+
)
|
|
144
|
+
parser.add_argument(
|
|
145
|
+
'--version',
|
|
146
|
+
action='version',
|
|
147
|
+
version='%(prog)s 0.1.0'
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
args = parser.parse_args()
|
|
151
|
+
|
|
152
|
+
# Parse variables
|
|
153
|
+
variables = {}
|
|
154
|
+
if args.variables:
|
|
155
|
+
for var in args.variables:
|
|
156
|
+
if '=' in var:
|
|
157
|
+
key, value = var.split('=', 1)
|
|
158
|
+
variables[key] = value
|
|
159
|
+
|
|
160
|
+
repo_path = Path(args.repo).resolve()
|
|
161
|
+
|
|
162
|
+
# Validate repo has a pipeline file
|
|
163
|
+
pipeline_file = repo_path / 'bitbucket-pipelines.yml'
|
|
164
|
+
if not pipeline_file.exists() and not args.list_targets:
|
|
165
|
+
print(f"Error: bitbucket-pipelines.yml not found in {repo_path}")
|
|
166
|
+
return 1
|
|
167
|
+
|
|
168
|
+
# Execute
|
|
169
|
+
if args.list_targets:
|
|
170
|
+
return list_targets(repo_path)
|
|
171
|
+
|
|
172
|
+
if args.validate:
|
|
173
|
+
return validate(repo_path)
|
|
174
|
+
|
|
175
|
+
return run_pipeline(
|
|
176
|
+
repo_path=repo_path,
|
|
177
|
+
target=args.target,
|
|
178
|
+
branch=args.branch,
|
|
179
|
+
variables=variables,
|
|
180
|
+
mode=args.mode,
|
|
181
|
+
verbose=args.verbose
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
if __name__ == '__main__':
|
|
186
|
+
sys.exit(main())
|
bbrun/docker.py
ADDED
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Docker Runner - Executes pipeline steps in Docker containers
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import subprocess
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Dict, List, Optional
|
|
9
|
+
|
|
10
|
+
from .validator import PipelineValidator
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class DockerRunner:
|
|
14
|
+
"""Runs pipeline steps in Docker containers."""
|
|
15
|
+
|
|
16
|
+
def __init__(self, repo_path: Path):
|
|
17
|
+
self.repo_path = Path(repo_path)
|
|
18
|
+
self.pipeline_file = self.repo_path / "bitbucket-pipelines.yml"
|
|
19
|
+
self.variables = {}
|
|
20
|
+
self.validator = PipelineValidator(repo_path)
|
|
21
|
+
|
|
22
|
+
def _docker_available(self) -> bool:
|
|
23
|
+
"""Check if Docker is available."""
|
|
24
|
+
try:
|
|
25
|
+
result = subprocess.run(
|
|
26
|
+
['docker', 'info'],
|
|
27
|
+
capture_output=True,
|
|
28
|
+
timeout=10
|
|
29
|
+
)
|
|
30
|
+
return result.returncode == 0
|
|
31
|
+
except (subprocess.TimeoutExpired, FileNotFoundError):
|
|
32
|
+
return False
|
|
33
|
+
|
|
34
|
+
def _image_exists(self, image: str) -> bool:
|
|
35
|
+
"""Check if Docker image exists locally."""
|
|
36
|
+
result = subprocess.run(
|
|
37
|
+
['docker', 'image', 'inspect', image],
|
|
38
|
+
capture_output=True
|
|
39
|
+
)
|
|
40
|
+
return result.returncode == 0
|
|
41
|
+
|
|
42
|
+
def _pull_image(self, image: str) -> bool:
|
|
43
|
+
"""Pull a Docker image."""
|
|
44
|
+
print(f"Pulling Docker image: {image}")
|
|
45
|
+
result = subprocess.run(
|
|
46
|
+
['docker', 'pull', image],
|
|
47
|
+
capture_output=True,
|
|
48
|
+
text=True
|
|
49
|
+
)
|
|
50
|
+
if result.returncode != 0:
|
|
51
|
+
print(f"Warning: Could not pull {image}: {result.stderr}")
|
|
52
|
+
return result.returncode == 0
|
|
53
|
+
|
|
54
|
+
def _build_env(self, branch: str) -> Dict[str, str]:
|
|
55
|
+
"""Build environment variables for the container."""
|
|
56
|
+
env = dict(os.environ)
|
|
57
|
+
|
|
58
|
+
# Get git commit
|
|
59
|
+
try:
|
|
60
|
+
commit = subprocess.run(
|
|
61
|
+
['git', 'rev-parse', 'HEAD'],
|
|
62
|
+
capture_output=True,
|
|
63
|
+
text=True,
|
|
64
|
+
cwd=self.repo_path
|
|
65
|
+
).stdout.strip()
|
|
66
|
+
except Exception:
|
|
67
|
+
commit = 'local'
|
|
68
|
+
|
|
69
|
+
env.update({
|
|
70
|
+
'BITBUCKET_BUILD_NUMBER': '1',
|
|
71
|
+
'BITBUCKET_CLONE_DIR': '/opt/atlassian/pipelines/agent/build',
|
|
72
|
+
'BITBUCKET_COMMIT': commit,
|
|
73
|
+
'BITBUCKET_BRANCH': branch,
|
|
74
|
+
'BITBUCKET_REPO_SLUG': self.repo_path.name,
|
|
75
|
+
'BITBUCKET_REPO_UUID': f'bb-run-{os.getpid()}',
|
|
76
|
+
'BITBUCKET_WORKSPACE': 'local',
|
|
77
|
+
'HOME': '/root',
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
# Add user variables
|
|
81
|
+
env.update(self.variables)
|
|
82
|
+
|
|
83
|
+
return env
|
|
84
|
+
|
|
85
|
+
def _run_step(self, step: Dict, step_name: str, default_image: str, env: Dict) -> bool:
|
|
86
|
+
"""Execute a single pipeline step in Docker."""
|
|
87
|
+
print(f"\n{'='*60}")
|
|
88
|
+
print(f"Step: {step_name}")
|
|
89
|
+
print(f"{'='*60}")
|
|
90
|
+
|
|
91
|
+
# Resolve image
|
|
92
|
+
image = step.get('image', default_image)
|
|
93
|
+
|
|
94
|
+
# Check/pull image
|
|
95
|
+
if not self._image_exists(image):
|
|
96
|
+
print(f"Image not found locally: {image}")
|
|
97
|
+
if not self._pull_image(image):
|
|
98
|
+
print(f"Failed to pull image {image}")
|
|
99
|
+
return False
|
|
100
|
+
|
|
101
|
+
# Build docker command
|
|
102
|
+
docker_cmd = [
|
|
103
|
+
'docker', 'run', '--rm',
|
|
104
|
+
'-w', '/opt/atlassian/pipelines/agent/build',
|
|
105
|
+
'-v', f'{self.repo_path}:/opt/atlassian/pipelines/agent/build:rw'
|
|
106
|
+
]
|
|
107
|
+
|
|
108
|
+
# Add environment variables
|
|
109
|
+
for key, value in env.items():
|
|
110
|
+
docker_cmd.extend(['-e', f'{key}={value}'])
|
|
111
|
+
|
|
112
|
+
docker_cmd.append(image)
|
|
113
|
+
|
|
114
|
+
# Handle script vs pipe
|
|
115
|
+
if 'script' in step:
|
|
116
|
+
script = step['script']
|
|
117
|
+
if isinstance(script, list):
|
|
118
|
+
bash_cmd = ' && '.join(script)
|
|
119
|
+
else:
|
|
120
|
+
bash_cmd = script
|
|
121
|
+
|
|
122
|
+
docker_cmd.extend(['/bin/bash', '-c', bash_cmd])
|
|
123
|
+
print(f"Executing: {bash_cmd[:60]}...")
|
|
124
|
+
elif 'pipe' in step:
|
|
125
|
+
pipe = step['pipe']
|
|
126
|
+
print(f"Pipe: {pipe}")
|
|
127
|
+
print("Note: Pipes are not executed in Docker mode (simplified)")
|
|
128
|
+
return True
|
|
129
|
+
else:
|
|
130
|
+
print("Warning: Step has no script or pipe")
|
|
131
|
+
return True
|
|
132
|
+
|
|
133
|
+
# Run
|
|
134
|
+
result = subprocess.run(
|
|
135
|
+
docker_cmd,
|
|
136
|
+
cwd=self.repo_path,
|
|
137
|
+
env=env
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
if result.returncode != 0:
|
|
141
|
+
print(f"ā Failed with exit code {result.returncode}")
|
|
142
|
+
return False
|
|
143
|
+
|
|
144
|
+
return True
|
|
145
|
+
|
|
146
|
+
def run(
|
|
147
|
+
self,
|
|
148
|
+
target: str = 'default',
|
|
149
|
+
branch: str = 'LOCAL',
|
|
150
|
+
variables: Optional[Dict] = None,
|
|
151
|
+
verbose: bool = False
|
|
152
|
+
) -> bool:
|
|
153
|
+
"""Run the pipeline for a given target."""
|
|
154
|
+
if variables:
|
|
155
|
+
self.variables.update(variables)
|
|
156
|
+
|
|
157
|
+
# Check Docker
|
|
158
|
+
if not self._docker_available():
|
|
159
|
+
print("Error: Docker is not available")
|
|
160
|
+
print("Use --mode host to run on your host machine instead")
|
|
161
|
+
return False
|
|
162
|
+
|
|
163
|
+
# Load pipeline
|
|
164
|
+
config = self.validator.load()
|
|
165
|
+
if not config:
|
|
166
|
+
print("Error: Could not load pipeline")
|
|
167
|
+
return False
|
|
168
|
+
|
|
169
|
+
default_image = config.get('image', 'atlassian/default-image:latest')
|
|
170
|
+
|
|
171
|
+
print(f"Repository: {self.repo_path}")
|
|
172
|
+
print(f"Target: {target}")
|
|
173
|
+
print(f"Branch: {branch}")
|
|
174
|
+
print("Mode: DOCKER")
|
|
175
|
+
print(f"Image: {default_image}")
|
|
176
|
+
|
|
177
|
+
# Get steps
|
|
178
|
+
steps = self._get_steps(config, target)
|
|
179
|
+
if not steps:
|
|
180
|
+
print(f"No steps found for target: {target}")
|
|
181
|
+
return False
|
|
182
|
+
|
|
183
|
+
# Run steps
|
|
184
|
+
env = self._build_env(branch)
|
|
185
|
+
all_passed = True
|
|
186
|
+
|
|
187
|
+
for i, item in enumerate(steps):
|
|
188
|
+
step = item.get('step', item)
|
|
189
|
+
step_name = step.get('name', f'Step {i+1}')
|
|
190
|
+
|
|
191
|
+
if not self._run_step(step, step_name, default_image, env):
|
|
192
|
+
all_passed = False
|
|
193
|
+
break
|
|
194
|
+
|
|
195
|
+
if all_passed:
|
|
196
|
+
print(f"\n{'='*60}")
|
|
197
|
+
print("ā
All steps completed successfully!")
|
|
198
|
+
print(f"{'='*60}")
|
|
199
|
+
else:
|
|
200
|
+
print(f"\n{'='*60}")
|
|
201
|
+
print("ā Pipeline failed!")
|
|
202
|
+
print(f"{'='*60}")
|
|
203
|
+
|
|
204
|
+
return all_passed
|
|
205
|
+
|
|
206
|
+
def _get_steps(self, config: Dict, target: str) -> List:
|
|
207
|
+
"""Get steps for a given target."""
|
|
208
|
+
pipelines = config.get('pipelines', {})
|
|
209
|
+
|
|
210
|
+
if target == 'default':
|
|
211
|
+
return pipelines.get('default', [])
|
|
212
|
+
|
|
213
|
+
if target.startswith('branches.'):
|
|
214
|
+
branch_name = target.split('.', 1)[1]
|
|
215
|
+
return pipelines.get('branches', {}).get(branch_name, [])
|
|
216
|
+
|
|
217
|
+
if target.startswith('tags.'):
|
|
218
|
+
tag_name = target.split('.', 1)[1]
|
|
219
|
+
return pipelines.get('tags', {}).get(tag_name, [])
|
|
220
|
+
|
|
221
|
+
if target in pipelines:
|
|
222
|
+
return pipelines[target]
|
|
223
|
+
|
|
224
|
+
return []
|
bbrun/host.py
ADDED
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Host Runner - Executes pipeline steps directly on the host machine
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import shutil
|
|
7
|
+
import subprocess
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Dict, List, Optional
|
|
10
|
+
|
|
11
|
+
from .validator import PipelineValidator
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class HostRunner:
|
|
15
|
+
"""Runs pipeline steps directly on the host machine."""
|
|
16
|
+
|
|
17
|
+
def __init__(self, repo_path: Path):
|
|
18
|
+
self.repo_path = Path(repo_path)
|
|
19
|
+
self.pipeline_file = self.repo_path / "bitbucket-pipelines.yml"
|
|
20
|
+
self.variables = {}
|
|
21
|
+
self.validator = PipelineValidator(repo_path)
|
|
22
|
+
|
|
23
|
+
def _build_env(self, branch: str) -> Dict[str, str]:
|
|
24
|
+
"""Build environment variables."""
|
|
25
|
+
env = dict(os.environ)
|
|
26
|
+
|
|
27
|
+
# Get git commit
|
|
28
|
+
try:
|
|
29
|
+
commit = subprocess.run(
|
|
30
|
+
['git', 'rev-parse', 'HEAD'],
|
|
31
|
+
capture_output=True,
|
|
32
|
+
text=True,
|
|
33
|
+
cwd=self.repo_path
|
|
34
|
+
).stdout.strip()
|
|
35
|
+
except Exception:
|
|
36
|
+
commit = 'local'
|
|
37
|
+
|
|
38
|
+
env.update({
|
|
39
|
+
'BITBUCKET_BUILD_NUMBER': '1',
|
|
40
|
+
'BITBUCKET_CLONE_DIR': str(self.repo_path),
|
|
41
|
+
'BITBUCKET_COMMIT': commit,
|
|
42
|
+
'BITBUCKET_BRANCH': branch,
|
|
43
|
+
'BITBUCKET_REPO_SLUG': self.repo_path.name,
|
|
44
|
+
'BITBUCKET_REPO_UUID': f'bb-run-{os.getpid()}',
|
|
45
|
+
'BITBUCKET_WORKSPACE': 'local',
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
env.update(self.variables)
|
|
49
|
+
|
|
50
|
+
return env
|
|
51
|
+
|
|
52
|
+
def _translate_command(self, cmd: str) -> str:
|
|
53
|
+
"""Translate commands for host compatibility."""
|
|
54
|
+
# Translate 'python' to 'python3' if python isn't available
|
|
55
|
+
if not shutil.which('python') and cmd.startswith('python '):
|
|
56
|
+
cmd = 'python3' + cmd[6:]
|
|
57
|
+
|
|
58
|
+
# Translate 'pip ' to 'pip3 ' if pip isn't available
|
|
59
|
+
if not shutil.which('pip') and cmd.startswith('pip ') and not cmd.startswith('pip3 '):
|
|
60
|
+
cmd = 'pip3 ' + cmd[4:]
|
|
61
|
+
|
|
62
|
+
# Add --break-system-packages for PEP 668
|
|
63
|
+
if 'pip3 install' in cmd and '--break-system-packages' not in cmd:
|
|
64
|
+
cmd = cmd.replace('pip3 install', 'pip3 install --break-system-packages')
|
|
65
|
+
print(" (added --break-system-packages for PEP 668)")
|
|
66
|
+
|
|
67
|
+
return cmd
|
|
68
|
+
|
|
69
|
+
def _run_step(self, step: Dict, step_name: str, env: Dict) -> bool:
|
|
70
|
+
"""Execute a single pipeline step on the host."""
|
|
71
|
+
print(f"\n{'='*60}")
|
|
72
|
+
print(f"Step: {step_name}")
|
|
73
|
+
print(f"{'='*60}")
|
|
74
|
+
|
|
75
|
+
if 'script' in step:
|
|
76
|
+
return self._run_script(step['script'], env)
|
|
77
|
+
elif 'pipe' in step:
|
|
78
|
+
return self._run_pipe(step)
|
|
79
|
+
else:
|
|
80
|
+
print("Warning: Step has no script or pipe")
|
|
81
|
+
return True
|
|
82
|
+
|
|
83
|
+
def _run_script(self, script: List[str], env: Dict) -> bool:
|
|
84
|
+
"""Run a script step."""
|
|
85
|
+
if isinstance(script, list):
|
|
86
|
+
commands = script
|
|
87
|
+
else:
|
|
88
|
+
commands = [script]
|
|
89
|
+
|
|
90
|
+
for cmd in commands:
|
|
91
|
+
translated = self._translate_command(cmd)
|
|
92
|
+
print(f"$ {translated}")
|
|
93
|
+
|
|
94
|
+
result = subprocess.run(
|
|
95
|
+
translated,
|
|
96
|
+
shell=True,
|
|
97
|
+
cwd=self.repo_path,
|
|
98
|
+
env=env
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
if result.returncode != 0:
|
|
102
|
+
print(f"ā Failed with exit code {result.returncode}")
|
|
103
|
+
return False
|
|
104
|
+
|
|
105
|
+
return True
|
|
106
|
+
|
|
107
|
+
def _run_pipe(self, step: Dict) -> bool:
|
|
108
|
+
"""Handle a pipe step (not executed in host mode)."""
|
|
109
|
+
pipe = step.get('pipe', '')
|
|
110
|
+
print(f"ā ļø Pipe: {pipe}")
|
|
111
|
+
print(" (pipes not executed in host mode)")
|
|
112
|
+
return True
|
|
113
|
+
|
|
114
|
+
def run(
|
|
115
|
+
self,
|
|
116
|
+
target: str = 'default',
|
|
117
|
+
branch: str = 'LOCAL',
|
|
118
|
+
variables: Optional[Dict] = None,
|
|
119
|
+
verbose: bool = False
|
|
120
|
+
) -> bool:
|
|
121
|
+
"""Run the pipeline for a given target."""
|
|
122
|
+
if variables:
|
|
123
|
+
self.variables.update(variables)
|
|
124
|
+
|
|
125
|
+
# Load pipeline
|
|
126
|
+
config = self.validator.load()
|
|
127
|
+
if not config:
|
|
128
|
+
print("Error: Could not load pipeline")
|
|
129
|
+
return False
|
|
130
|
+
|
|
131
|
+
image = config.get('image', 'atlassian/default-image:latest')
|
|
132
|
+
|
|
133
|
+
print(f"Repository: {self.repo_path}")
|
|
134
|
+
print(f"Target: {target}")
|
|
135
|
+
print(f"Branch: {branch}")
|
|
136
|
+
print("Mode: HOST (runs on your machine)")
|
|
137
|
+
print(f"Note: Uses '{image}' as reference for command mapping")
|
|
138
|
+
|
|
139
|
+
# Get steps
|
|
140
|
+
steps = self._get_steps(config, target)
|
|
141
|
+
if not steps:
|
|
142
|
+
print(f"No steps found for target: {target}")
|
|
143
|
+
return False
|
|
144
|
+
|
|
145
|
+
# Run steps
|
|
146
|
+
env = self._build_env(branch)
|
|
147
|
+
all_passed = True
|
|
148
|
+
|
|
149
|
+
for i, item in enumerate(steps):
|
|
150
|
+
step = item.get('step', item)
|
|
151
|
+
step_name = step.get('name', f'Step {i+1}')
|
|
152
|
+
|
|
153
|
+
if not self._run_step(step, step_name, env):
|
|
154
|
+
all_passed = False
|
|
155
|
+
break
|
|
156
|
+
|
|
157
|
+
if all_passed:
|
|
158
|
+
print(f"\n{'='*60}")
|
|
159
|
+
print("ā
All steps completed successfully!")
|
|
160
|
+
print(f"{'='*60}")
|
|
161
|
+
else:
|
|
162
|
+
print(f"\n{'='*60}")
|
|
163
|
+
print("ā Pipeline failed!")
|
|
164
|
+
print(f"{'='*60}")
|
|
165
|
+
|
|
166
|
+
return all_passed
|
|
167
|
+
|
|
168
|
+
def _get_steps(self, config: Dict, target: str) -> List:
|
|
169
|
+
"""Get steps for a given target."""
|
|
170
|
+
pipelines = config.get('pipelines', {})
|
|
171
|
+
|
|
172
|
+
if target == 'default':
|
|
173
|
+
return pipelines.get('default', [])
|
|
174
|
+
|
|
175
|
+
if target.startswith('branches.'):
|
|
176
|
+
branch_name = target.split('.', 1)[1]
|
|
177
|
+
return pipelines.get('branches', {}).get(branch_name, [])
|
|
178
|
+
|
|
179
|
+
if target.startswith('tags.'):
|
|
180
|
+
tag_name = target.split('.', 1)[1]
|
|
181
|
+
return pipelines.get('tags', {}).get(tag_name, [])
|
|
182
|
+
|
|
183
|
+
if target in pipelines:
|
|
184
|
+
return pipelines[target]
|
|
185
|
+
|
|
186
|
+
return []
|
bbrun/validator.py
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Pipeline YAML Validator
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import yaml
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Dict, Optional
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class PipelineValidator:
|
|
11
|
+
"""Validates and parses bitbucket-pipelines.yml"""
|
|
12
|
+
|
|
13
|
+
def __init__(self, repo_path: Path):
|
|
14
|
+
self.repo_path = Path(repo_path)
|
|
15
|
+
self.pipeline_file = self.repo_path / "bitbucket-pipelines.yml"
|
|
16
|
+
self._config: Optional[Dict] = None
|
|
17
|
+
|
|
18
|
+
def load(self) -> Optional[Dict]:
|
|
19
|
+
"""Load and parse the pipeline YAML."""
|
|
20
|
+
if not self.pipeline_file.exists():
|
|
21
|
+
return None
|
|
22
|
+
|
|
23
|
+
try:
|
|
24
|
+
with open(self.pipeline_file, 'r') as f:
|
|
25
|
+
self._config = yaml.safe_load(f)
|
|
26
|
+
return self._config
|
|
27
|
+
except yaml.YAMLError as e:
|
|
28
|
+
print(f"YAML parse error: {e}")
|
|
29
|
+
return None
|
|
30
|
+
|
|
31
|
+
def validate(self) -> bool:
|
|
32
|
+
"""Validate the pipeline configuration."""
|
|
33
|
+
config = self.load()
|
|
34
|
+
|
|
35
|
+
if not config:
|
|
36
|
+
return False
|
|
37
|
+
|
|
38
|
+
# Check for required 'pipelines' key
|
|
39
|
+
if 'pipelines' not in config:
|
|
40
|
+
print("Error: Missing 'pipelines' key")
|
|
41
|
+
return False
|
|
42
|
+
|
|
43
|
+
return True
|
|
44
|
+
|
|
45
|
+
def show_summary(self) -> None:
|
|
46
|
+
"""Print a summary of the pipeline."""
|
|
47
|
+
if not self._config:
|
|
48
|
+
return
|
|
49
|
+
|
|
50
|
+
image = self._config.get('image', 'atlassian/default-image:latest')
|
|
51
|
+
print(f"\nImage: {image}")
|
|
52
|
+
|
|
53
|
+
pipelines = self._config.get('pipelines', {})
|
|
54
|
+
|
|
55
|
+
# Default pipeline
|
|
56
|
+
if 'default' in pipelines:
|
|
57
|
+
print("\nš¦ default:")
|
|
58
|
+
for item in pipelines['default']:
|
|
59
|
+
self._show_step(item)
|
|
60
|
+
|
|
61
|
+
# Branches
|
|
62
|
+
branches = pipelines.get('branches', {})
|
|
63
|
+
if branches:
|
|
64
|
+
print("\nšæ branches:")
|
|
65
|
+
for branch, items in branches.items():
|
|
66
|
+
print(f" {branch}:")
|
|
67
|
+
for item in items:
|
|
68
|
+
self._show_step(item, indent=4)
|
|
69
|
+
|
|
70
|
+
# Tags
|
|
71
|
+
tags = pipelines.get('tags', {})
|
|
72
|
+
if tags:
|
|
73
|
+
print("\nš·ļø tags:")
|
|
74
|
+
for tag, items in tags.items():
|
|
75
|
+
print(f" {tag}:")
|
|
76
|
+
for item in items:
|
|
77
|
+
self._show_step(item, indent=4)
|
|
78
|
+
|
|
79
|
+
def _show_step(self, item: Dict, indent: int = 2) -> None:
|
|
80
|
+
"""Show details of a single step."""
|
|
81
|
+
step = item.get('step', item)
|
|
82
|
+
name = step.get('name', 'unnamed')
|
|
83
|
+
prefix = " " * indent
|
|
84
|
+
|
|
85
|
+
suffix = ""
|
|
86
|
+
if step.get('deployment'):
|
|
87
|
+
suffix += f" [{step['deployment']}]"
|
|
88
|
+
if step.get('trigger'):
|
|
89
|
+
suffix += f" ({step['trigger']})"
|
|
90
|
+
|
|
91
|
+
print(f"{prefix}⢠{name}{suffix}")
|
|
92
|
+
|
|
93
|
+
for cmd in step.get('script', []):
|
|
94
|
+
if isinstance(cmd, str):
|
|
95
|
+
display = cmd[:70] + "..." if len(cmd) > 70 else cmd
|
|
96
|
+
print(f"{prefix} ā {display}")
|
|
97
|
+
elif isinstance(cmd, dict) and 'pipe' in cmd:
|
|
98
|
+
pipe_name = cmd['pipe']
|
|
99
|
+
vars_str = ""
|
|
100
|
+
if 'variables' in cmd:
|
|
101
|
+
vars_str = f" ({cmd['variables']})"
|
|
102
|
+
print(f"{prefix} ā pipe: {pipe_name}{vars_str}")
|
|
103
|
+
|
|
104
|
+
@property
|
|
105
|
+
def config(self) -> Optional[Dict]:
|
|
106
|
+
"""Get the loaded configuration."""
|
|
107
|
+
return self._config
|