pullup 0.0.5__tar.gz → 0.0.6__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.
- pullup-0.0.6/.gitignore +153 -0
- {pullup-0.0.5/pullup.egg-info → pullup-0.0.6}/PKG-INFO +14 -15
- {pullup-0.0.5 → pullup-0.0.6}/pullup/__init__.py +1 -1
- {pullup-0.0.5 → pullup-0.0.6}/pyproject.toml +10 -7
- pullup-0.0.5/MANIFEST.in +0 -5
- pullup-0.0.5/PKG-INFO +0 -163
- pullup-0.0.5/pullup.egg-info/SOURCES.txt +0 -26
- pullup-0.0.5/pullup.egg-info/dependency_links.txt +0 -1
- pullup-0.0.5/pullup.egg-info/entry_points.txt +0 -2
- pullup-0.0.5/pullup.egg-info/requires.txt +0 -9
- pullup-0.0.5/pullup.egg-info/top_level.txt +0 -1
- pullup-0.0.5/setup.cfg +0 -4
- pullup-0.0.5/tests/test_env.py +0 -54
- pullup-0.0.5/tests/test_pipeline.py +0 -103
- pullup-0.0.5/tests/test_project.py +0 -56
- pullup-0.0.5/tests/test_stack.py +0 -55
- pullup-0.0.5/tests/test_workflows.py +0 -59
- {pullup-0.0.5 → pullup-0.0.6}/LICENSE +0 -0
- {pullup-0.0.5 → pullup-0.0.6}/README.md +0 -0
- {pullup-0.0.5 → pullup-0.0.6}/pullup/_modidx.py +0 -0
- {pullup-0.0.5 → pullup-0.0.6}/pullup/deploy.py +0 -0
- {pullup-0.0.5 → pullup-0.0.6}/pullup/drive.py +0 -0
- {pullup-0.0.5 → pullup-0.0.6}/pullup/env.py +0 -0
- {pullup-0.0.5 → pullup-0.0.6}/pullup/infra.py +0 -0
- {pullup-0.0.5 → pullup-0.0.6}/pullup/pipeline.py +0 -0
- {pullup-0.0.5 → pullup-0.0.6}/pullup/project.py +0 -0
- {pullup-0.0.5 → pullup-0.0.6}/pullup/stack.py +0 -0
- {pullup-0.0.5 → pullup-0.0.6}/pullup/wfbuild.py +0 -0
- {pullup-0.0.5 → pullup-0.0.6}/pullup/workflows.py +0 -0
pullup-0.0.6/.gitignore
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
_docs/
|
|
2
|
+
_proc/
|
|
3
|
+
sidebar.yml
|
|
4
|
+
|
|
5
|
+
*.bak
|
|
6
|
+
.gitattributes
|
|
7
|
+
.last_checked
|
|
8
|
+
.gitconfig
|
|
9
|
+
*.bak
|
|
10
|
+
*.log
|
|
11
|
+
*~
|
|
12
|
+
~*
|
|
13
|
+
_tmp*
|
|
14
|
+
tmp*
|
|
15
|
+
tags
|
|
16
|
+
*.pkg
|
|
17
|
+
|
|
18
|
+
# Byte-compiled / optimized / DLL files
|
|
19
|
+
__pycache__/
|
|
20
|
+
*.py[cod]
|
|
21
|
+
*$py.class
|
|
22
|
+
|
|
23
|
+
# C extensions
|
|
24
|
+
*.so
|
|
25
|
+
|
|
26
|
+
# Distribution / packaging
|
|
27
|
+
.Python
|
|
28
|
+
env/
|
|
29
|
+
build/
|
|
30
|
+
conda/
|
|
31
|
+
develop-eggs/
|
|
32
|
+
dist/
|
|
33
|
+
downloads/
|
|
34
|
+
eggs/
|
|
35
|
+
.eggs/
|
|
36
|
+
lib/
|
|
37
|
+
lib64/
|
|
38
|
+
parts/
|
|
39
|
+
sdist/
|
|
40
|
+
var/
|
|
41
|
+
wheels/
|
|
42
|
+
*.egg-info/
|
|
43
|
+
.installed.cfg
|
|
44
|
+
*.egg
|
|
45
|
+
|
|
46
|
+
# PyInstaller
|
|
47
|
+
# Usually these files are written by a python script from a template
|
|
48
|
+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
|
49
|
+
*.manifest
|
|
50
|
+
*.spec
|
|
51
|
+
|
|
52
|
+
# Installer logs
|
|
53
|
+
pip-log.txt
|
|
54
|
+
pip-delete-this-directory.txt
|
|
55
|
+
|
|
56
|
+
# Unit test / coverage reports
|
|
57
|
+
htmlcov/
|
|
58
|
+
.tox/
|
|
59
|
+
.coverage
|
|
60
|
+
.coverage.*
|
|
61
|
+
.cache
|
|
62
|
+
nosetests.xml
|
|
63
|
+
coverage.xml
|
|
64
|
+
*.cover
|
|
65
|
+
.hypothesis/
|
|
66
|
+
|
|
67
|
+
# Translations
|
|
68
|
+
*.mo
|
|
69
|
+
*.pot
|
|
70
|
+
|
|
71
|
+
# Django stuff:
|
|
72
|
+
*.log
|
|
73
|
+
local_settings.py
|
|
74
|
+
|
|
75
|
+
# Flask stuff:
|
|
76
|
+
instance/
|
|
77
|
+
.webassets-cache
|
|
78
|
+
|
|
79
|
+
# Scrapy stuff:
|
|
80
|
+
.scrapy
|
|
81
|
+
|
|
82
|
+
# Sphinx documentation
|
|
83
|
+
docs/_build/
|
|
84
|
+
|
|
85
|
+
# PyBuilder
|
|
86
|
+
target/
|
|
87
|
+
|
|
88
|
+
# Jupyter Notebook
|
|
89
|
+
.ipynb_checkpoints
|
|
90
|
+
|
|
91
|
+
# pyenv
|
|
92
|
+
.python-version
|
|
93
|
+
|
|
94
|
+
# celery beat schedule file
|
|
95
|
+
celerybeat-schedule
|
|
96
|
+
|
|
97
|
+
# SageMath parsed files
|
|
98
|
+
*.sage.py
|
|
99
|
+
|
|
100
|
+
# dotenv
|
|
101
|
+
.env
|
|
102
|
+
|
|
103
|
+
# virtualenv
|
|
104
|
+
.venv
|
|
105
|
+
venv/
|
|
106
|
+
ENV/
|
|
107
|
+
|
|
108
|
+
# Spyder project settings
|
|
109
|
+
.spyderproject
|
|
110
|
+
.spyproject
|
|
111
|
+
|
|
112
|
+
# Rope project settings
|
|
113
|
+
.ropeproject
|
|
114
|
+
|
|
115
|
+
# mkdocs documentation
|
|
116
|
+
/site
|
|
117
|
+
|
|
118
|
+
# mypy
|
|
119
|
+
.mypy_cache/
|
|
120
|
+
|
|
121
|
+
.vscode
|
|
122
|
+
*.swp
|
|
123
|
+
|
|
124
|
+
# osx generated files
|
|
125
|
+
.DS_Store
|
|
126
|
+
.DS_Store?
|
|
127
|
+
.Trashes
|
|
128
|
+
ehthumbs.db
|
|
129
|
+
Thumbs.db
|
|
130
|
+
.idea
|
|
131
|
+
|
|
132
|
+
# pytest
|
|
133
|
+
.pytest_cache
|
|
134
|
+
|
|
135
|
+
# tools/trust-doc-nbs
|
|
136
|
+
docs_src/.last_checked
|
|
137
|
+
|
|
138
|
+
# symlinks to fastai
|
|
139
|
+
docs_src/fastai
|
|
140
|
+
tools/fastai
|
|
141
|
+
|
|
142
|
+
# link checker
|
|
143
|
+
checklink/cookies.txt
|
|
144
|
+
|
|
145
|
+
# .gitconfig is now autogenerated
|
|
146
|
+
.gitconfig
|
|
147
|
+
|
|
148
|
+
# Quarto installer
|
|
149
|
+
.deb
|
|
150
|
+
.pkg
|
|
151
|
+
|
|
152
|
+
# Quarto
|
|
153
|
+
.quarto
|
|
@@ -1,30 +1,29 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: pullup
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.6
|
|
4
4
|
Summary: build, release, and deploy projects
|
|
5
|
-
Author-email: Karthik Rajgopal <karthik.rajgopal@hotmail.com>
|
|
6
|
-
License: Apache-2.0
|
|
7
5
|
Project-URL: Repository, https://github.com/vedicreader/pullup
|
|
8
6
|
Project-URL: Documentation, https://vedicreader.github.io/pullup
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
7
|
+
Author-email: Karthik Rajgopal <karthik.rajgopal@hotmail.com>
|
|
8
|
+
License: Apache-2.0
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Keywords: jupyter,nbdev,notebook,python
|
|
12
11
|
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Natural Language :: English
|
|
13
14
|
Classifier: Programming Language :: Python :: 3
|
|
14
15
|
Classifier: Programming Language :: Python :: 3 :: Only
|
|
15
16
|
Requires-Python: >=3.11
|
|
16
|
-
|
|
17
|
-
|
|
17
|
+
Requires-Dist: cfeasy>=0.0.4
|
|
18
|
+
Requires-Dist: dockeasy>=0.0.5
|
|
18
19
|
Requires-Dist: fastcore>=1.8.17
|
|
20
|
+
Requires-Dist: gheasy>=0.0.10
|
|
21
|
+
Requires-Dist: keyring>=25.0.0
|
|
19
22
|
Requires-Dist: ptymini>=0.0.5
|
|
23
|
+
Requires-Dist: python-dotenv>=1.0.0
|
|
20
24
|
Requires-Dist: pyyaml>=6.0
|
|
21
|
-
Requires-Dist: dockeasy>=0.0.5
|
|
22
|
-
Requires-Dist: gheasy>=0.0.10
|
|
23
25
|
Requires-Dist: vpseasy>=0.0.18
|
|
24
|
-
|
|
25
|
-
Requires-Dist: python-dotenv>=1.0.0
|
|
26
|
-
Requires-Dist: keyring>=25.0.0
|
|
27
|
-
Dynamic: license-file
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
28
27
|
|
|
29
28
|
# pullup
|
|
30
29
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[build-system]
|
|
2
|
-
requires = ["
|
|
3
|
-
build-backend = "
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "pullup"
|
|
@@ -23,11 +23,14 @@ Documentation = "https://vedicreader.github.io/pullup"
|
|
|
23
23
|
[project.entry-points.nbdev]
|
|
24
24
|
pullup = "pullup._modidx:d"
|
|
25
25
|
|
|
26
|
-
[tool.
|
|
27
|
-
|
|
26
|
+
[tool.hatch.version]
|
|
27
|
+
path = "pullup/__init__.py"
|
|
28
28
|
|
|
29
|
-
[tool.
|
|
30
|
-
|
|
29
|
+
[tool.hatch.build.targets.wheel]
|
|
30
|
+
packages = ["pullup"]
|
|
31
|
+
|
|
32
|
+
[tool.hatch.build.targets.sdist]
|
|
33
|
+
include = ["/pullup", "/README.md", "/pyproject.toml"]
|
|
31
34
|
|
|
32
35
|
[tool.nbdev]
|
|
33
36
|
doc_host = "https://vedicreader.github.io"
|
|
@@ -38,4 +41,4 @@ testpaths = ["tests"]
|
|
|
38
41
|
asyncio_mode = "auto"
|
|
39
42
|
|
|
40
43
|
[dependency-groups]
|
|
41
|
-
dev = ['pytest>=8.0.0', 'pytest-asyncio>=1.0.0', 'nbdev>=3.3.11']
|
|
44
|
+
dev = ['pytest>=8.0.0', 'pytest-asyncio>=1.0.0', 'nbdev>=3.3.11']
|
pullup-0.0.5/MANIFEST.in
DELETED
pullup-0.0.5/PKG-INFO
DELETED
|
@@ -1,163 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: pullup
|
|
3
|
-
Version: 0.0.5
|
|
4
|
-
Summary: build, release, and deploy projects
|
|
5
|
-
Author-email: Karthik Rajgopal <karthik.rajgopal@hotmail.com>
|
|
6
|
-
License: Apache-2.0
|
|
7
|
-
Project-URL: Repository, https://github.com/vedicreader/pullup
|
|
8
|
-
Project-URL: Documentation, https://vedicreader.github.io/pullup
|
|
9
|
-
Keywords: nbdev,jupyter,notebook,python
|
|
10
|
-
Classifier: Natural Language :: English
|
|
11
|
-
Classifier: Intended Audience :: Developers
|
|
12
|
-
Classifier: Development Status :: 3 - Alpha
|
|
13
|
-
Classifier: Programming Language :: Python :: 3
|
|
14
|
-
Classifier: Programming Language :: Python :: 3 :: Only
|
|
15
|
-
Requires-Python: >=3.11
|
|
16
|
-
Description-Content-Type: text/markdown
|
|
17
|
-
License-File: LICENSE
|
|
18
|
-
Requires-Dist: fastcore>=1.8.17
|
|
19
|
-
Requires-Dist: ptymini>=0.0.5
|
|
20
|
-
Requires-Dist: pyyaml>=6.0
|
|
21
|
-
Requires-Dist: dockeasy>=0.0.5
|
|
22
|
-
Requires-Dist: gheasy>=0.0.10
|
|
23
|
-
Requires-Dist: vpseasy>=0.0.18
|
|
24
|
-
Requires-Dist: cfeasy>=0.0.4
|
|
25
|
-
Requires-Dist: python-dotenv>=1.0.0
|
|
26
|
-
Requires-Dist: keyring>=25.0.0
|
|
27
|
-
Dynamic: license-file
|
|
28
|
-
|
|
29
|
-
# pullup
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
<!-- WARNING: THIS FILE WAS AUTOGENERATED! DO NOT EDIT! -->
|
|
33
|
-
|
|
34
|
-
## Install
|
|
35
|
-
|
|
36
|
-
``` sh
|
|
37
|
-
pip install pullup
|
|
38
|
-
```
|
|
39
|
-
|
|
40
|
-
Cloud integrations load only when used.
|
|
41
|
-
|
|
42
|
-
## Project detection
|
|
43
|
-
|
|
44
|
-
[`Project`](https://vedicreader.github.io/pullup/project.html#project) detects the release flow from files in the project directory.
|
|
45
|
-
|
|
46
|
-
``` python
|
|
47
|
-
project = Project(root)
|
|
48
|
-
project.kind, [step.id for step in project.steps()]
|
|
49
|
-
```
|
|
50
|
-
|
|
51
|
-
('nbdev', ['prepare', 'bump', 'gh', 'pypi'])
|
|
52
|
-
|
|
53
|
-
## Running steps
|
|
54
|
-
|
|
55
|
-
Steps run in one PTY. Automatic execution stops at the first failure.
|
|
56
|
-
|
|
57
|
-
``` python
|
|
58
|
-
pipeline = Pipeline(root, dir='.demo')
|
|
59
|
-
pipeline.save([{'id': 'hello', 'cmd': 'say hello',
|
|
60
|
-
'argv': [sys.executable, '-c', 'print("ready")']}])
|
|
61
|
-
await pipeline.start()
|
|
62
|
-
pipeline.state()['done'], pipeline.tail().splitlines()[-2:]
|
|
63
|
-
```
|
|
64
|
-
|
|
65
|
-
(False, ['', '❯ say hello'])
|
|
66
|
-
|
|
67
|
-
## Plan storage
|
|
68
|
-
|
|
69
|
-
Plans are written only below the project directory.
|
|
70
|
-
|
|
71
|
-
``` python
|
|
72
|
-
custom = Pipeline(root, dir='.myapp')
|
|
73
|
-
custom.save([{'id': 'show', 'cmd': 'show', 'argv': [sys.executable, '-c', 'print("saved")']}])
|
|
74
|
-
custom.path.relative_to(root)
|
|
75
|
-
```
|
|
76
|
-
|
|
77
|
-
Path('.myapp/pipeline.json')
|
|
78
|
-
|
|
79
|
-
## Deployment pipelines
|
|
80
|
-
|
|
81
|
-
[`Deploy`](https://vedicreader.github.io/pullup/deploy.html#deploy) runs a project’s deployment script. [`Drive`](https://vedicreader.github.io/pullup/drive.html#drive) exposes each operation as a library call.
|
|
82
|
-
|
|
83
|
-
``` python
|
|
84
|
-
Deploy(root).state()['kind'], Drive(root).state()['kind']
|
|
85
|
-
```
|
|
86
|
-
|
|
87
|
-
('deploy', 'drive')
|
|
88
|
-
|
|
89
|
-
## Workflows
|
|
90
|
-
|
|
91
|
-
Local workflow inspection needs no GitHub credentials.
|
|
92
|
-
|
|
93
|
-
``` python
|
|
94
|
-
workflow_dir = root/'.github'/'workflows'
|
|
95
|
-
workflow_dir.mkdir(parents=True)
|
|
96
|
-
(workflow_dir/'ci.yml').write_text('''name: CI
|
|
97
|
-
on: push
|
|
98
|
-
jobs:
|
|
99
|
-
test:
|
|
100
|
-
runs-on: ubuntu-latest
|
|
101
|
-
steps:
|
|
102
|
-
- run: pytest
|
|
103
|
-
''')
|
|
104
|
-
workflows = Workflows(root)
|
|
105
|
-
[(row['name'], [job['id'] for job in row['jobs']]) for row in workflows.parsed()]
|
|
106
|
-
```
|
|
107
|
-
|
|
108
|
-
[('CI', ['test'])]
|
|
109
|
-
|
|
110
|
-
## Environment
|
|
111
|
-
|
|
112
|
-
A pipeline injects required values into the child without changing the parent environment.
|
|
113
|
-
|
|
114
|
-
``` python
|
|
115
|
-
class Values:
|
|
116
|
-
def get(self, key, secret=True): return 'demo-token' if key == 'DEMO_TOKEN' else ''
|
|
117
|
-
def values(self, keys): return {key: self.get(key) for key in keys if self.get(key)}
|
|
118
|
-
|
|
119
|
-
with_values = Pipeline(root, env=Values(), dir='.env-demo')
|
|
120
|
-
with_values.save([{'id': 'env', 'cmd': 'read environment', 'needs': ['DEMO_TOKEN'],
|
|
121
|
-
'argv': [sys.executable, '-c', 'import os; print(os.environ["DEMO_TOKEN"])']}])
|
|
122
|
-
await with_values.start()
|
|
123
|
-
with_values.tail().splitlines()[-2:]
|
|
124
|
-
```
|
|
125
|
-
|
|
126
|
-
['', '❯ read environment']
|
|
127
|
-
|
|
128
|
-
## Package errors
|
|
129
|
-
|
|
130
|
-
[`blame`](https://vedicreader.github.io/pullup/stack.html#blame) identifies the deepest selected-package frame.
|
|
131
|
-
|
|
132
|
-
``` python
|
|
133
|
-
trace = '''Traceback (most recent call last):
|
|
134
|
-
File "/tmp/pullup/demo.py", line 7, in release
|
|
135
|
-
raise RuntimeError("stopped")
|
|
136
|
-
RuntimeError: stopped
|
|
137
|
-
'''
|
|
138
|
-
blame(trace, family=['pullup'])
|
|
139
|
-
```
|
|
140
|
-
|
|
141
|
-
{'package': 'pullup',
|
|
142
|
-
'file': '/tmp/pullup/demo.py',
|
|
143
|
-
'line': 7,
|
|
144
|
-
'fn': 'release',
|
|
145
|
-
'error': 'RuntimeError: stopped',
|
|
146
|
-
'open': '/tmp/pullup/demo.py',
|
|
147
|
-
'checkout': '',
|
|
148
|
-
'version': '0.0.4'}
|
|
149
|
-
|
|
150
|
-
## Infrastructure
|
|
151
|
-
|
|
152
|
-
[`Infra.status`](https://vedicreader.github.io/pullup/infra.html#infra.status) reports provider and token availability without making a request.
|
|
153
|
-
|
|
154
|
-
``` python
|
|
155
|
-
infra = Infra(env={})
|
|
156
|
-
infra.status()
|
|
157
|
-
```
|
|
158
|
-
|
|
159
|
-
{'vpseasy': True,
|
|
160
|
-
'cfeasy': True,
|
|
161
|
-
'hcloud_token': True,
|
|
162
|
-
'cf_token': False,
|
|
163
|
-
'keys': ['HCLOUD_TOKEN', 'CLOUDFLARE_API_TOKEN']}
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
LICENSE
|
|
2
|
-
MANIFEST.in
|
|
3
|
-
README.md
|
|
4
|
-
pyproject.toml
|
|
5
|
-
pullup/__init__.py
|
|
6
|
-
pullup/_modidx.py
|
|
7
|
-
pullup/deploy.py
|
|
8
|
-
pullup/drive.py
|
|
9
|
-
pullup/env.py
|
|
10
|
-
pullup/infra.py
|
|
11
|
-
pullup/pipeline.py
|
|
12
|
-
pullup/project.py
|
|
13
|
-
pullup/stack.py
|
|
14
|
-
pullup/wfbuild.py
|
|
15
|
-
pullup/workflows.py
|
|
16
|
-
pullup.egg-info/PKG-INFO
|
|
17
|
-
pullup.egg-info/SOURCES.txt
|
|
18
|
-
pullup.egg-info/dependency_links.txt
|
|
19
|
-
pullup.egg-info/entry_points.txt
|
|
20
|
-
pullup.egg-info/requires.txt
|
|
21
|
-
pullup.egg-info/top_level.txt
|
|
22
|
-
tests/test_env.py
|
|
23
|
-
tests/test_pipeline.py
|
|
24
|
-
tests/test_project.py
|
|
25
|
-
tests/test_stack.py
|
|
26
|
-
tests/test_workflows.py
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
pullup
|
pullup-0.0.5/setup.cfg
DELETED
pullup-0.0.5/tests/test_env.py
DELETED
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
"The environment a project's commands run in."
|
|
2
|
-
import os
|
|
3
|
-
from pathlib import Path
|
|
4
|
-
import pytest
|
|
5
|
-
from pullup.env import BUNDLE_ONLY, EnvStore, clean_env, env_value, strip_bundle, venv_env
|
|
6
|
-
from pullup.deploy import Deploy
|
|
7
|
-
from pullup.drive import Drive
|
|
8
|
-
|
|
9
|
-
def test_outside_a_bundle_the_environment_is_handed_back_untouched():
|
|
10
|
-
"A caller that claimed nothing about an environment still claims nothing."
|
|
11
|
-
given = {'PYTHONHOME': '/somewhere', 'PATH': '/bin'}
|
|
12
|
-
assert strip_bundle(dict(given), frozen=False) == given
|
|
13
|
-
|
|
14
|
-
def test_inside_one_the_hosts_interpreter_redirection_is_dropped():
|
|
15
|
-
"""py2app points PYTHONHOME at the bundle. A child that keeps it imports the bundle's standard
|
|
16
|
-
library under another interpreter and dies somewhere that names nothing to do with the cause."""
|
|
17
|
-
out = strip_bundle({n: 'x' for n in BUNDLE_ONLY} | {'PATH': '/bin'}, frozen=True)
|
|
18
|
-
assert not (set(out) & set(BUNDLE_ONLY)) and out['PATH'] == '/bin'
|
|
19
|
-
assert out['PYTHONUTF8'] == '1'
|
|
20
|
-
|
|
21
|
-
def test_naming_an_interpreter_puts_its_environment_in_front(tmp_path):
|
|
22
|
-
venv = tmp_path/'.venv'; (venv/'bin').mkdir(parents=True)
|
|
23
|
-
py = venv/'bin'/'python'; py.write_text('')
|
|
24
|
-
env = venv_env(py, env={'PATH': '/usr/bin', 'UV_PROJECT_ENVIRONMENT': '/elsewhere'})
|
|
25
|
-
assert env['VIRTUAL_ENV'] == str(venv)
|
|
26
|
-
assert env['PATH'].startswith(str(venv/'bin') + os.pathsep)
|
|
27
|
-
assert 'UV_PROJECT_ENVIRONMENT' not in env, 'it is read where the process ends up, not here'
|
|
28
|
-
assert 'PYTHONHOME' not in env
|
|
29
|
-
|
|
30
|
-
def test_naming_no_interpreter_still_gives_a_usable_environment():
|
|
31
|
-
assert venv_env(None, env={'PATH': '/bin'}) == {'PATH': '/bin'}
|
|
32
|
-
assert 'PATH' in venv_env()
|
|
33
|
-
|
|
34
|
-
def test_a_value_that_cannot_be_read_falls_back_rather_than_raising():
|
|
35
|
-
"A missing dockeasy is a reason to use the default, not to fail the step that wanted it."
|
|
36
|
-
class Broken:
|
|
37
|
-
def get(self, key, secret=False): raise RuntimeError('no store here')
|
|
38
|
-
assert env_value(Broken(), 'ANYTHING', 'fallback') == 'fallback'
|
|
39
|
-
assert env_value(Broken(), 'ANYTHING') == ''
|
|
40
|
-
|
|
41
|
-
def test_a_store_with_no_dockeasy_says_which_extra_installs_it():
|
|
42
|
-
with pytest.raises(Exception, match='pullup'):
|
|
43
|
-
EnvStore().get('SOMETHING') if not _dockeasy_installed() else pytest.skip('dockeasy is here')
|
|
44
|
-
|
|
45
|
-
def _dockeasy_installed():
|
|
46
|
-
from importlib.util import find_spec
|
|
47
|
-
try: return find_spec('dockeasy') is not None
|
|
48
|
-
except (ImportError, ValueError): return False
|
|
49
|
-
|
|
50
|
-
def test_every_pipeline_kind_writes_under_the_directory_it_was_given(tmp_path):
|
|
51
|
-
"Three kinds, one rule: the caller names the folder, and nothing writes outside it."
|
|
52
|
-
for kind, name in ((Deploy, 'deploy.json'), (Drive, 'drive.json')):
|
|
53
|
-
p = kind(tmp_path, dir='.somewhere')
|
|
54
|
-
assert p.path == tmp_path/'.somewhere'/name
|
|
@@ -1,103 +0,0 @@
|
|
|
1
|
-
"The plan, the state, and a step that actually runs on a pty."
|
|
2
|
-
import asyncio, json
|
|
3
|
-
import pytest
|
|
4
|
-
from pullup.project import Step
|
|
5
|
-
from pullup.pipeline import Pipeline, PipelineError, resolve_script, uv_argv
|
|
6
|
-
|
|
7
|
-
class Steps(Pipeline):
|
|
8
|
-
"A pipeline whose plan is given rather than sniffed from the folder."
|
|
9
|
-
plan = ()
|
|
10
|
-
@classmethod
|
|
11
|
-
def defaults(cls, root): return list(cls.plan)
|
|
12
|
-
|
|
13
|
-
def mk(tmp_path, steps, **kw):
|
|
14
|
-
Steps.plan = steps
|
|
15
|
-
return Steps(tmp_path, **kw)
|
|
16
|
-
|
|
17
|
-
def test_the_plan_is_written_where_the_caller_says_and_nowhere_else(tmp_path):
|
|
18
|
-
"A package that wrote into a directory named after one of its callers would be one to fork."
|
|
19
|
-
p = mk(tmp_path, [Step('a', 'a', 'true')], dir='.mine')
|
|
20
|
-
assert p.path == tmp_path/'.mine'/'pipeline.json'
|
|
21
|
-
p.save([{'id': 'b', 'label': 'b', 'cmd': 'false'}])
|
|
22
|
-
assert json.loads(p.path.read_text())['steps'][0]['cmd'] == 'false'
|
|
23
|
-
assert [s.id for s in Steps(tmp_path, dir='.mine').steps] == ['b'], 'and it is read back'
|
|
24
|
-
assert not (tmp_path/'.leela').exists() and not (tmp_path/'.pullup').exists()
|
|
25
|
-
|
|
26
|
-
def test_a_plan_with_no_command_in_it_is_refused(tmp_path):
|
|
27
|
-
p = mk(tmp_path, [Step('a', 'a', 'true')])
|
|
28
|
-
with pytest.raises(PipelineError, match='at least one step'): p.save([{'id': 'x', 'cmd': ' '}])
|
|
29
|
-
with pytest.raises(PipelineError, match='at least one step'): p.save([])
|
|
30
|
-
|
|
31
|
-
def test_an_unreadable_plan_falls_back_to_the_default_rather_than_raising(tmp_path):
|
|
32
|
-
"A half-written file is a reason to offer the default plan, not to make the panel unopenable."
|
|
33
|
-
(tmp_path/'.pullup').mkdir()
|
|
34
|
-
(tmp_path/'.pullup'/'pipeline.json').write_text('{ not json')
|
|
35
|
-
assert [s.id for s in mk(tmp_path, [Step('a', 'a', 'true')]).steps] == ['a']
|
|
36
|
-
|
|
37
|
-
def test_the_state_says_what_ran_what_is_running_and_what_is_still_wanted(tmp_path):
|
|
38
|
-
p = mk(tmp_path, [Step('a', 'a', 'true'), Step('b', 'b', 'true', needs=['NOPE_NOT_SET'])])
|
|
39
|
-
st = p.state()
|
|
40
|
-
assert st['kind'] == 'pipeline' and not st['configured'] and not st['running']
|
|
41
|
-
assert [r['status'] for r in st['steps']] == ['pending', 'pending']
|
|
42
|
-
assert st['needs'] == ['NOPE_NOT_SET'] and st['steps'][1]['missing'] == ['NOPE_NOT_SET']
|
|
43
|
-
assert not st['done'] and st['failed'] == ''
|
|
44
|
-
p.skip('a'); p.skip('b')
|
|
45
|
-
assert p.state()['done'], 'skipped counts as done: it is a decision, not a gap'
|
|
46
|
-
p.reset()
|
|
47
|
-
assert [r['status'] for r in p.state()['steps']] == ['pending', 'pending']
|
|
48
|
-
|
|
49
|
-
def test_an_unknown_step_is_named_rather_than_silently_skipped(tmp_path):
|
|
50
|
-
p = mk(tmp_path, [Step('a', 'a', 'true')])
|
|
51
|
-
with pytest.raises(PipelineError, match='unknown pipeline step: zzz'): p.step('zzz')
|
|
52
|
-
|
|
53
|
-
async def test_a_passing_run_walks_the_plan_and_a_failing_one_stops_it(tmp_path):
|
|
54
|
-
"""Auto-advance is what makes a release one button. It has to stop at the first failure, or a
|
|
55
|
-
project would publish from a tree whose tests never passed."""
|
|
56
|
-
p = mk(tmp_path, [Step('one', 'one', 'echo first'),
|
|
57
|
-
Step('two', 'two', 'sh -c "exit 3"'),
|
|
58
|
-
Step('three', 'three', 'echo never')])
|
|
59
|
-
await p.start()
|
|
60
|
-
for _ in range(200):
|
|
61
|
-
if p.state()['failed']: break
|
|
62
|
-
await asyncio.sleep(.05)
|
|
63
|
-
st = p.state()
|
|
64
|
-
assert st['failed'] == 'two'
|
|
65
|
-
got = {r['id']: (r['status'], r['code']) for r in st['steps']}
|
|
66
|
-
assert got['one'] == ('passed', 0) and got['two'] == ('failed', 3)
|
|
67
|
-
assert got['three'][0] == 'pending', 'the step after a failure must not have run'
|
|
68
|
-
assert 'first' in p.tail() and 'never' not in p.tail()
|
|
69
|
-
assert '❯ echo first' in p.tail(), 'the transcript says which command produced what'
|
|
70
|
-
|
|
71
|
-
async def test_a_viewer_that_joins_late_is_primed_with_what_it_missed(tmp_path):
|
|
72
|
-
"A browser attaching after a step has scrolled by should still see it."
|
|
73
|
-
p = mk(tmp_path, [Step('one', 'one', 'echo hello-from-the-past')])
|
|
74
|
-
await p.start()
|
|
75
|
-
for _ in range(200):
|
|
76
|
-
if p.state()['steps'][0]['status'] == 'passed': break
|
|
77
|
-
await asyncio.sleep(.05)
|
|
78
|
-
q = p.subscribe()
|
|
79
|
-
assert b'hello-from-the-past' in q.get_nowait()
|
|
80
|
-
p.unsubscribe(q)
|
|
81
|
-
|
|
82
|
-
async def test_a_second_start_is_refused_while_one_is_running(tmp_path):
|
|
83
|
-
p = mk(tmp_path, [Step('slow', 'slow', 'sleep 5')])
|
|
84
|
-
await p.start()
|
|
85
|
-
with pytest.raises(PipelineError, match='is still running'): await p.start()
|
|
86
|
-
with pytest.raises(PipelineError, match='is still running'): p.reset()
|
|
87
|
-
p.stop()
|
|
88
|
-
assert p.state()['steps'][0]['status'] == 'failed' and not p.state()['running']
|
|
89
|
-
|
|
90
|
-
async def test_a_step_with_no_command_says_so_instead_of_spawning_a_shell(tmp_path):
|
|
91
|
-
p = mk(tmp_path, [Step('empty', 'empty', ' ')])
|
|
92
|
-
with pytest.raises(PipelineError, match='no command to run'): await p.start()
|
|
93
|
-
|
|
94
|
-
def test_a_hyphenated_command_is_found_under_either_spelling(tmp_path):
|
|
95
|
-
"nbdev installs `nbdev_export`; the plan spells it `nbdev-export`. Both have to resolve."
|
|
96
|
-
bindir = tmp_path/'bin'; bindir.mkdir()
|
|
97
|
-
(bindir/'my_tool').write_text('#!/bin/sh\n'); (bindir/'my_tool').chmod(0o755)
|
|
98
|
-
argv = resolve_script(['my-tool', '--flag'], python=bindir/'python')
|
|
99
|
-
assert argv[0].endswith('my_tool') and argv[1] == '--flag'
|
|
100
|
-
assert resolve_script(['definitely-not-here'])[0] == 'definitely-not-here', 'unfound is unchanged'
|
|
101
|
-
|
|
102
|
-
def test_uv_only_claims_a_project_it_actually_owns(tmp_path):
|
|
103
|
-
assert uv_argv(tmp_path, ['pytest']) is None, 'no lock, so not uv'
|
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
"What kind of project a folder holds, and the steps that follow from it."
|
|
2
|
-
import pytest
|
|
3
|
-
from pullup.project import (Project, Step, app_project, default_steps, fastship_project,
|
|
4
|
-
maturin_project, nbdev_project, release_flow, rust_project)
|
|
5
|
-
|
|
6
|
-
def test_an_nbdev_project_is_recognised_by_either_file_it_keeps_its_config_in(tmp_path):
|
|
7
|
-
"nbdev 2 writes settings.ini; a migrated project writes `[tool.nbdev]` instead."
|
|
8
|
-
assert not nbdev_project(tmp_path)
|
|
9
|
-
(tmp_path/'settings.ini').write_text('[DEFAULT]\nlib_name = x\nnbs_path = nbs\n')
|
|
10
|
-
assert nbdev_project(tmp_path) and release_flow(tmp_path) == 'nbdev'
|
|
11
|
-
(tmp_path/'settings.ini').unlink()
|
|
12
|
-
(tmp_path/'pyproject.toml').write_text('[project]\nname = "x"\n\n[tool.nbdev]\n')
|
|
13
|
-
assert nbdev_project(tmp_path), 'a migrated project is still an nbdev project'
|
|
14
|
-
|
|
15
|
-
def test_a_settings_ini_that_is_not_nbdevs_does_not_claim_the_folder(tmp_path):
|
|
16
|
-
"`[DEFAULT]` alone is any old ini file; nbdev's has its paths in it."
|
|
17
|
-
(tmp_path/'settings.ini').write_text('[DEFAULT]\ncolour = blue\n')
|
|
18
|
-
assert not nbdev_project(tmp_path)
|
|
19
|
-
|
|
20
|
-
def test_rust_is_asked_before_fastship_because_a_maturin_crate_has_a_pyproject_too(tmp_path):
|
|
21
|
-
"""The fastship flow would `ship-pypi` a wheel only the runners can build, so a crate that is
|
|
22
|
-
also a Python package has to be recognised as the crate it is."""
|
|
23
|
-
(tmp_path/'Cargo.toml').write_text('[package]\nname = "x"\n')
|
|
24
|
-
assert rust_project(tmp_path) and release_flow(tmp_path) == 'crate'
|
|
25
|
-
(tmp_path/'pyproject.toml').write_text('[build-system]\nbuild-backend = "maturin"\n')
|
|
26
|
-
assert maturin_project(tmp_path) and release_flow(tmp_path) == 'maturin'
|
|
27
|
-
assert fastship_project(tmp_path), 'it does have a pyproject; the order is what decides'
|
|
28
|
-
|
|
29
|
-
def test_a_plain_folder_and_a_plain_package_get_the_flows_that_fit_them(tmp_path):
|
|
30
|
-
assert release_flow(tmp_path) == 'plain'
|
|
31
|
-
(tmp_path/'pyproject.toml').write_text('[project]\nname = "x"\n')
|
|
32
|
-
assert release_flow(tmp_path) == 'fastship'
|
|
33
|
-
|
|
34
|
-
def test_the_app_bundle_lands_after_the_last_publish_and_before_the_bump(tmp_path):
|
|
35
|
-
"""`nbdev_bump_version` raises the version for the next cycle, so a bundle built after it
|
|
36
|
-
carries a version nothing was released under."""
|
|
37
|
-
(tmp_path/'settings.ini').write_text('[DEFAULT]\nlib_name = x\nnbs_path = nbs\n')
|
|
38
|
-
assert [s.id for s in default_steps(tmp_path)] == ['prepare', 'bump', 'gh', 'pypi']
|
|
39
|
-
(tmp_path/'setup_app.py').write_text('')
|
|
40
|
-
(tmp_path/'tools').mkdir(); (tmp_path/'tools'/'build_release.py').write_text('')
|
|
41
|
-
assert app_project(tmp_path)
|
|
42
|
-
ids = [s.id for s in default_steps(tmp_path)]
|
|
43
|
-
assert ids == ['prepare', 'bump', 'gh', 'pypi', 'app', 'assets']
|
|
44
|
-
assert ids.index('app') > ids.index('pypi')
|
|
45
|
-
|
|
46
|
-
def test_a_project_says_what_it_is_without_the_caller_asking_five_questions(tmp_path):
|
|
47
|
-
(tmp_path/'pyproject.toml').write_text('[project]\nname = "x"\n')
|
|
48
|
-
p = Project(tmp_path)
|
|
49
|
-
assert p.kind == 'fastship' and not p.packages_an_app
|
|
50
|
-
assert [s.id for s in p.steps()] == ['test', 'changelog', 'gh', 'pypi', 'bump']
|
|
51
|
-
assert p.dict()['kind'] == 'fastship' and p.dict()['steps'][0]['cmd'].startswith('python -m pytest')
|
|
52
|
-
|
|
53
|
-
def test_a_caller_can_bring_its_own_flow(tmp_path):
|
|
54
|
-
"`FLOWS` is data, so a project kind pullup does not ship a plan for is still reachable."
|
|
55
|
-
mine = {'plain': [Step('only', 'the one step', 'true', 'nothing else runs')]}
|
|
56
|
-
assert [s.id for s in default_steps(tmp_path, flows=mine)] == ['only']
|
pullup-0.0.5/tests/test_stack.py
DELETED
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
"Which of your own packages raised, and where to open it."
|
|
2
|
-
from pullup.stack import blame, frames, checkout_for, survey
|
|
3
|
-
|
|
4
|
-
TB = '''Traceback (most recent call last):
|
|
5
|
-
File "/x/.venv/lib/python3.12/site-packages/nbdev/cli.py", line 10, in main
|
|
6
|
-
run()
|
|
7
|
-
File "/x/.venv/lib/python3.12/site-packages/gheasy/repo.py", line 42, in push
|
|
8
|
-
raise GitError("no upstream")
|
|
9
|
-
GitError: no upstream
|
|
10
|
-
'''
|
|
11
|
-
|
|
12
|
-
def test_the_deepest_package_of_yours_is_the_one_blamed():
|
|
13
|
-
"The top of the traceback is whoever called; the bottom is whoever actually raised."
|
|
14
|
-
r = blame(TB, family=['nbdev', 'gheasy'])
|
|
15
|
-
assert r['package'] == 'gheasy' and r['line'] == 42 and r['fn'] == 'push'
|
|
16
|
-
assert r['error'] == 'GitError: no upstream'
|
|
17
|
-
|
|
18
|
-
def test_a_traceback_with_none_of_your_packages_in_it_still_reports_the_error():
|
|
19
|
-
"A failure in somebody else's code is not a failure to describe."
|
|
20
|
-
r = blame(TB, family=['pullup'])
|
|
21
|
-
assert r['package'] == '' and r['error'] == 'GitError: no upstream'
|
|
22
|
-
|
|
23
|
-
def test_nothing_at_all_is_answered_with_blanks_not_an_exception():
|
|
24
|
-
assert blame('')['package'] == '' and blame(None)['error'] == ''
|
|
25
|
-
|
|
26
|
-
def test_frames_reads_a_traceback_in_the_order_the_terminal_printed_it():
|
|
27
|
-
got = frames(TB)
|
|
28
|
-
assert [f['fn'] for f in got] == ['main', 'push'] and got[0]['line'] == 10
|
|
29
|
-
|
|
30
|
-
def test_an_open_checkout_is_matched_by_what_is_inside_it_not_by_its_folder_name(tmp_path):
|
|
31
|
-
"A repository called `gheasy-fork` still holds the `gheasy` package, and that is what counts."
|
|
32
|
-
root = tmp_path/'gheasy-fork'; (root/'gheasy').mkdir(parents=True)
|
|
33
|
-
(root/'gheasy'/'__init__.py').write_text('')
|
|
34
|
-
assert checkout_for('gheasy', [tmp_path]) == ''
|
|
35
|
-
assert checkout_for('gheasy', [root]) == str(root)
|
|
36
|
-
assert checkout_for('nothere', [root]) == ''
|
|
37
|
-
|
|
38
|
-
def test_a_src_layout_checkout_is_found_too(tmp_path):
|
|
39
|
-
root = tmp_path/'thing'; (root/'src'/'thing').mkdir(parents=True)
|
|
40
|
-
(root/'src'/'thing'/'__init__.py').write_text('')
|
|
41
|
-
assert checkout_for('thing', [root]) == str(root)
|
|
42
|
-
|
|
43
|
-
def test_the_blamed_file_is_reopened_in_your_checkout_when_you_have_one(tmp_path):
|
|
44
|
-
"""Site-packages is a copy. Pointing an editor at it means editing something a reinstall
|
|
45
|
-
discards, so the checkout wins where the same file exists in it."""
|
|
46
|
-
root = tmp_path/'gheasy'; (root/'gheasy').mkdir(parents=True)
|
|
47
|
-
(root/'gheasy'/'__init__.py').write_text('')
|
|
48
|
-
(root/'gheasy'/'repo.py').write_text('')
|
|
49
|
-
r = blame(TB, checkouts=[root], family=['gheasy'])
|
|
50
|
-
assert r['open'] == str(root/'gheasy'/'repo.py') and r['checkout'] == str(root)
|
|
51
|
-
assert r['file'].startswith('/x/'), 'and the traceback still says where it actually ran'
|
|
52
|
-
|
|
53
|
-
def test_survey_answers_for_a_package_that_is_not_installed():
|
|
54
|
-
row = next(r for r in survey(family=['definitely_not_installed_xyz']))
|
|
55
|
-
assert row['installed'] is False and row['source'] == '' and row['version'] == ''
|
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
"Reading a GitHub Actions workflow without running one."
|
|
2
|
-
from pullup.workflows import Workflows, levels
|
|
3
|
-
|
|
4
|
-
def test_jobs_are_arranged_into_the_rows_a_graph_is_drawn_in():
|
|
5
|
-
"Everything that can start now, then what follows it, by longest path."
|
|
6
|
-
jobs = [{'id': 'build', 'needs': []}, {'id': 'test', 'needs': ['build']},
|
|
7
|
-
{'id': 'lint', 'needs': []}, {'id': 'ship', 'needs': ['test', 'lint']}]
|
|
8
|
-
assert levels(jobs) == [['build', 'lint'], ['test'], ['ship']]
|
|
9
|
-
|
|
10
|
-
def test_a_job_needing_something_that_is_not_there_still_gets_a_row():
|
|
11
|
-
"A workflow naming a job it does not define is a workflow you have to be able to look at."
|
|
12
|
-
assert levels([{'id': 'a', 'needs': ['ghost']}]) == [['a']]
|
|
13
|
-
|
|
14
|
-
def test_a_cycle_is_drawn_rather_than_hung_on():
|
|
15
|
-
got = levels([{'id': 'a', 'needs': ['b']}, {'id': 'b', 'needs': ['a']}])
|
|
16
|
-
assert got and sorted(sum(got, [])) == ['a', 'b']
|
|
17
|
-
|
|
18
|
-
def test_a_repository_with_no_workflows_reads_as_empty_not_as_an_error(tmp_path):
|
|
19
|
-
w = Workflows(tmp_path)
|
|
20
|
-
assert w.files() == [] and w.parsed() == []
|
|
21
|
-
|
|
22
|
-
def test_one_workflow_is_read_into_its_triggers_jobs_and_rows(tmp_path):
|
|
23
|
-
d = tmp_path/'.github'/'workflows'; d.mkdir(parents=True)
|
|
24
|
-
(d/'ci.yml').write_text('''name: CI
|
|
25
|
-
on:
|
|
26
|
-
push:
|
|
27
|
-
branches: [main]
|
|
28
|
-
workflow_dispatch:
|
|
29
|
-
jobs:
|
|
30
|
-
build:
|
|
31
|
-
runs-on: ubuntu-latest
|
|
32
|
-
steps:
|
|
33
|
-
- uses: actions/checkout@v4
|
|
34
|
-
- name: test
|
|
35
|
-
run: pytest -q
|
|
36
|
-
ship:
|
|
37
|
-
needs: build
|
|
38
|
-
runs-on: ubuntu-latest
|
|
39
|
-
steps:
|
|
40
|
-
- run: echo ship
|
|
41
|
-
''')
|
|
42
|
-
row = Workflows(tmp_path).parse(d/'ci.yml')
|
|
43
|
-
assert row['error'] == '' and row['name'] == 'CI'
|
|
44
|
-
assert row['dispatchable'], 'a dispatchable workflow is one you can start from a button'
|
|
45
|
-
assert {t['event'] for t in row['triggers']} == {'push', 'workflow_dispatch'}
|
|
46
|
-
assert [j['id'] for j in row['jobs']] == ['build', 'ship']
|
|
47
|
-
assert row['jobs'][1]['needs'] == ['build'] and row['levels'] == [['build'], ['ship']]
|
|
48
|
-
assert [s['name'] for s in row['jobs'][0]['steps']] == ['actions/checkout@v4', 'test']
|
|
49
|
-
|
|
50
|
-
def test_a_file_that_is_not_a_workflow_reports_why_rather_than_raising(tmp_path):
|
|
51
|
-
d = tmp_path/'.github'/'workflows'; d.mkdir(parents=True)
|
|
52
|
-
(d/'nope.yml').write_text('just a string\n')
|
|
53
|
-
assert Workflows(tmp_path).parse(d/'nope.yml')['error'] == 'this file is not a workflow'
|
|
54
|
-
|
|
55
|
-
def test_broken_yaml_is_an_error_on_the_row_not_an_exception(tmp_path):
|
|
56
|
-
d = tmp_path/'.github'/'workflows'; d.mkdir(parents=True)
|
|
57
|
-
(d/'bad.yml').write_text('name: [unclosed\n')
|
|
58
|
-
row = Workflows(tmp_path).parse(d/'bad.yml')
|
|
59
|
-
assert row['error'] and row['jobs'] == []
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|