pyrig 2.2.6__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.
- pyrig/__init__.py +1 -0
- pyrig/dev/__init__.py +6 -0
- pyrig/dev/builders/__init__.py +1 -0
- pyrig/dev/builders/base/__init__.py +5 -0
- pyrig/dev/builders/base/base.py +256 -0
- pyrig/dev/builders/pyinstaller.py +229 -0
- pyrig/dev/cli/__init__.py +5 -0
- pyrig/dev/cli/cli.py +95 -0
- pyrig/dev/cli/commands/__init__.py +1 -0
- pyrig/dev/cli/commands/build_artifacts.py +16 -0
- pyrig/dev/cli/commands/create_root.py +25 -0
- pyrig/dev/cli/commands/create_tests.py +244 -0
- pyrig/dev/cli/commands/init_project.py +160 -0
- pyrig/dev/cli/commands/make_inits.py +27 -0
- pyrig/dev/cli/commands/protect_repo.py +145 -0
- pyrig/dev/cli/shared_subcommands.py +20 -0
- pyrig/dev/cli/subcommands.py +73 -0
- pyrig/dev/configs/__init__.py +1 -0
- pyrig/dev/configs/base/__init__.py +5 -0
- pyrig/dev/configs/base/base.py +826 -0
- pyrig/dev/configs/containers/__init__.py +1 -0
- pyrig/dev/configs/containers/container_file.py +111 -0
- pyrig/dev/configs/dot_env.py +95 -0
- pyrig/dev/configs/dot_python_version.py +88 -0
- pyrig/dev/configs/git/__init__.py +5 -0
- pyrig/dev/configs/git/gitignore.py +181 -0
- pyrig/dev/configs/git/pre_commit.py +170 -0
- pyrig/dev/configs/licence.py +112 -0
- pyrig/dev/configs/markdown/__init__.py +1 -0
- pyrig/dev/configs/markdown/docs/__init__.py +1 -0
- pyrig/dev/configs/markdown/docs/index.py +38 -0
- pyrig/dev/configs/markdown/readme.py +132 -0
- pyrig/dev/configs/py_typed.py +28 -0
- pyrig/dev/configs/pyproject.py +436 -0
- pyrig/dev/configs/python/__init__.py +5 -0
- pyrig/dev/configs/python/builders_init.py +27 -0
- pyrig/dev/configs/python/configs_init.py +28 -0
- pyrig/dev/configs/python/dot_experiment.py +46 -0
- pyrig/dev/configs/python/main.py +59 -0
- pyrig/dev/configs/python/resources_init.py +27 -0
- pyrig/dev/configs/python/shared_subcommands.py +29 -0
- pyrig/dev/configs/python/src_init.py +27 -0
- pyrig/dev/configs/python/subcommands.py +27 -0
- pyrig/dev/configs/testing/__init__.py +5 -0
- pyrig/dev/configs/testing/conftest.py +64 -0
- pyrig/dev/configs/testing/fixtures_init.py +27 -0
- pyrig/dev/configs/testing/main_test.py +74 -0
- pyrig/dev/configs/testing/zero_test.py +43 -0
- pyrig/dev/configs/workflows/__init__.py +5 -0
- pyrig/dev/configs/workflows/base/__init__.py +5 -0
- pyrig/dev/configs/workflows/base/base.py +1662 -0
- pyrig/dev/configs/workflows/build.py +106 -0
- pyrig/dev/configs/workflows/health_check.py +133 -0
- pyrig/dev/configs/workflows/publish.py +68 -0
- pyrig/dev/configs/workflows/release.py +90 -0
- pyrig/dev/tests/__init__.py +5 -0
- pyrig/dev/tests/conftest.py +40 -0
- pyrig/dev/tests/fixtures/__init__.py +1 -0
- pyrig/dev/tests/fixtures/assertions.py +147 -0
- pyrig/dev/tests/fixtures/autouse/__init__.py +5 -0
- pyrig/dev/tests/fixtures/autouse/class_.py +42 -0
- pyrig/dev/tests/fixtures/autouse/module.py +40 -0
- pyrig/dev/tests/fixtures/autouse/session.py +589 -0
- pyrig/dev/tests/fixtures/factories.py +118 -0
- pyrig/dev/utils/__init__.py +1 -0
- pyrig/dev/utils/cli.py +17 -0
- pyrig/dev/utils/git.py +312 -0
- pyrig/dev/utils/packages.py +93 -0
- pyrig/dev/utils/resources.py +77 -0
- pyrig/dev/utils/testing.py +66 -0
- pyrig/dev/utils/versions.py +268 -0
- pyrig/main.py +9 -0
- pyrig/py.typed +0 -0
- pyrig/resources/GITIGNORE +216 -0
- pyrig/resources/LATEST_PYTHON_VERSION +1 -0
- pyrig/resources/MIT_LICENSE_TEMPLATE +21 -0
- pyrig/resources/__init__.py +1 -0
- pyrig/src/__init__.py +1 -0
- pyrig/src/git/__init__.py +6 -0
- pyrig/src/git/git.py +146 -0
- pyrig/src/graph.py +255 -0
- pyrig/src/iterate.py +107 -0
- pyrig/src/modules/__init__.py +22 -0
- pyrig/src/modules/class_.py +369 -0
- pyrig/src/modules/function.py +189 -0
- pyrig/src/modules/inspection.py +148 -0
- pyrig/src/modules/module.py +658 -0
- pyrig/src/modules/package.py +452 -0
- pyrig/src/os/__init__.py +6 -0
- pyrig/src/os/os.py +121 -0
- pyrig/src/project/__init__.py +5 -0
- pyrig/src/project/mgt.py +83 -0
- pyrig/src/resource.py +58 -0
- pyrig/src/string.py +100 -0
- pyrig/src/testing/__init__.py +6 -0
- pyrig/src/testing/assertions.py +66 -0
- pyrig/src/testing/convention.py +203 -0
- pyrig-2.2.6.dist-info/METADATA +174 -0
- pyrig-2.2.6.dist-info/RECORD +102 -0
- pyrig-2.2.6.dist-info/WHEEL +4 -0
- pyrig-2.2.6.dist-info/entry_points.txt +3 -0
- pyrig-2.2.6.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,1662 @@
|
|
|
1
|
+
"""Base class for GitHub Actions workflow configuration.
|
|
2
|
+
|
|
3
|
+
This module provides the Workflow base class that all workflow
|
|
4
|
+
configuration files inherit from. It includes utilities for
|
|
5
|
+
building jobs, steps, triggers, and matrix strategies.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from abc import abstractmethod
|
|
9
|
+
from collections.abc import Callable
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
import pyrig
|
|
14
|
+
from pyrig.dev.builders.base.base import Builder
|
|
15
|
+
from pyrig.dev.configs.base.base import YamlConfigFile
|
|
16
|
+
from pyrig.dev.configs.pyproject import PyprojectConfigFile
|
|
17
|
+
from pyrig.dev.utils.packages import get_src_package
|
|
18
|
+
from pyrig.src.modules.package import (
|
|
19
|
+
DependencyGraph,
|
|
20
|
+
)
|
|
21
|
+
from pyrig.src.project.mgt import (
|
|
22
|
+
PROJECT_MGT,
|
|
23
|
+
PROJECT_MGT_RUN_SCRIPT,
|
|
24
|
+
get_project_mgt_run_pyrig_cli_cmd_script,
|
|
25
|
+
)
|
|
26
|
+
from pyrig.src.string import (
|
|
27
|
+
make_name_from_obj,
|
|
28
|
+
split_on_uppercase,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class Workflow(YamlConfigFile):
|
|
33
|
+
"""Abstract base class for GitHub Actions workflow configuration.
|
|
34
|
+
|
|
35
|
+
Provides a declarative API for building workflow YAML files with
|
|
36
|
+
jobs, steps, triggers, and matrix strategies. Subclasses must
|
|
37
|
+
implement get_jobs() to define the workflow's jobs.
|
|
38
|
+
|
|
39
|
+
Attributes:
|
|
40
|
+
UBUNTU_LATEST: Runner label for Ubuntu.
|
|
41
|
+
WINDOWS_LATEST: Runner label for Windows.
|
|
42
|
+
MACOS_LATEST: Runner label for macOS.
|
|
43
|
+
ARTIFACTS_DIR_NAME: Directory name for build artifacts.
|
|
44
|
+
ARTIFACTS_PATTERN: Glob pattern for artifact files.
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
UBUNTU_LATEST = "ubuntu-latest"
|
|
48
|
+
WINDOWS_LATEST = "windows-latest"
|
|
49
|
+
MACOS_LATEST = "macos-latest"
|
|
50
|
+
|
|
51
|
+
ARTIFACTS_DIR_NAME = Builder.ARTIFACTS_DIR_NAME
|
|
52
|
+
ARTIFACTS_PATTERN = f"{ARTIFACTS_DIR_NAME}/*"
|
|
53
|
+
|
|
54
|
+
@classmethod
|
|
55
|
+
def load(cls) -> dict[str, Any]:
|
|
56
|
+
"""Load and parse the workflow configuration file.
|
|
57
|
+
|
|
58
|
+
Returns:
|
|
59
|
+
The parsed workflow configuration as a dict.
|
|
60
|
+
"""
|
|
61
|
+
content = super().load()
|
|
62
|
+
if not isinstance(content, dict):
|
|
63
|
+
msg = f"Expected dict, got {type(content)}"
|
|
64
|
+
raise TypeError(msg)
|
|
65
|
+
return content
|
|
66
|
+
|
|
67
|
+
@classmethod
|
|
68
|
+
def get_configs(cls) -> dict[str, Any]:
|
|
69
|
+
"""Build the complete workflow configuration.
|
|
70
|
+
|
|
71
|
+
Returns:
|
|
72
|
+
Dict with name, triggers, permissions, defaults, env, and jobs.
|
|
73
|
+
"""
|
|
74
|
+
return {
|
|
75
|
+
"name": cls.get_workflow_name(),
|
|
76
|
+
"on": cls.get_workflow_triggers(),
|
|
77
|
+
"permissions": cls.get_permissions(),
|
|
78
|
+
"run-name": cls.get_run_name(),
|
|
79
|
+
"defaults": cls.get_defaults(),
|
|
80
|
+
"env": cls.get_global_env(),
|
|
81
|
+
"jobs": cls.get_jobs(),
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
@classmethod
|
|
85
|
+
def get_parent_path(cls) -> Path:
|
|
86
|
+
"""Get the parent directory for workflow files.
|
|
87
|
+
|
|
88
|
+
Returns:
|
|
89
|
+
Path to .github/workflows directory.
|
|
90
|
+
"""
|
|
91
|
+
return Path(".github/workflows")
|
|
92
|
+
|
|
93
|
+
@classmethod
|
|
94
|
+
def is_correct(cls) -> bool:
|
|
95
|
+
"""Check if the workflow configuration is correct.
|
|
96
|
+
|
|
97
|
+
Handles the special case where workflow files cannot be empty.
|
|
98
|
+
If empty, writes a minimal valid workflow that never triggers.
|
|
99
|
+
|
|
100
|
+
Returns:
|
|
101
|
+
True if configuration matches expected state.
|
|
102
|
+
"""
|
|
103
|
+
correct = super().is_correct()
|
|
104
|
+
|
|
105
|
+
if cls.get_path().read_text(encoding="utf-8") == "":
|
|
106
|
+
config = cls.get_configs()
|
|
107
|
+
jobs = config["jobs"]
|
|
108
|
+
for job in jobs.values():
|
|
109
|
+
job["steps"] = [cls.step_opt_out_of_workflow()]
|
|
110
|
+
cls.dump(config)
|
|
111
|
+
|
|
112
|
+
config = cls.load()
|
|
113
|
+
jobs = config["jobs"]
|
|
114
|
+
opted_out = all(
|
|
115
|
+
job["steps"] == [cls.step_opt_out_of_workflow()] for job in jobs.values()
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
return correct or opted_out
|
|
119
|
+
|
|
120
|
+
# Overridable Workflow Parts
|
|
121
|
+
# ----------------------------------------------------------------------------
|
|
122
|
+
@classmethod
|
|
123
|
+
@abstractmethod
|
|
124
|
+
def get_jobs(cls) -> dict[str, Any]:
|
|
125
|
+
"""Get the workflow jobs.
|
|
126
|
+
|
|
127
|
+
Subclasses must implement this to define their jobs.
|
|
128
|
+
|
|
129
|
+
Returns:
|
|
130
|
+
Dict mapping job IDs to job configurations.
|
|
131
|
+
"""
|
|
132
|
+
|
|
133
|
+
@classmethod
|
|
134
|
+
def get_workflow_triggers(cls) -> dict[str, Any]:
|
|
135
|
+
"""Get the workflow triggers.
|
|
136
|
+
|
|
137
|
+
Override to customize when the workflow runs.
|
|
138
|
+
Default is manual workflow_dispatch only.
|
|
139
|
+
|
|
140
|
+
Returns:
|
|
141
|
+
Dict of trigger configurations.
|
|
142
|
+
"""
|
|
143
|
+
return cls.on_workflow_dispatch()
|
|
144
|
+
|
|
145
|
+
@classmethod
|
|
146
|
+
def get_permissions(cls) -> dict[str, Any]:
|
|
147
|
+
"""Get the workflow permissions.
|
|
148
|
+
|
|
149
|
+
Override to request additional permissions.
|
|
150
|
+
Default is no extra permissions.
|
|
151
|
+
|
|
152
|
+
Returns:
|
|
153
|
+
Dict of permission settings.
|
|
154
|
+
"""
|
|
155
|
+
return {}
|
|
156
|
+
|
|
157
|
+
@classmethod
|
|
158
|
+
def get_defaults(cls) -> dict[str, Any]:
|
|
159
|
+
"""Get the workflow defaults.
|
|
160
|
+
|
|
161
|
+
Override to customize default settings.
|
|
162
|
+
Default uses bash shell.
|
|
163
|
+
|
|
164
|
+
Returns:
|
|
165
|
+
Dict of default settings.
|
|
166
|
+
"""
|
|
167
|
+
return {"run": {"shell": "bash"}}
|
|
168
|
+
|
|
169
|
+
@classmethod
|
|
170
|
+
def get_global_env(cls) -> dict[str, Any]:
|
|
171
|
+
"""Get the global environment variables.
|
|
172
|
+
|
|
173
|
+
Override to add environment variables.
|
|
174
|
+
Default disables Python bytecode writing.
|
|
175
|
+
|
|
176
|
+
Returns:
|
|
177
|
+
Dict of environment variables.
|
|
178
|
+
"""
|
|
179
|
+
return {"PYTHONDONTWRITEBYTECODE": 1, "UV_NO_SYNC": 1}
|
|
180
|
+
|
|
181
|
+
# Workflow Conventions
|
|
182
|
+
# ----------------------------------------------------------------------------
|
|
183
|
+
@classmethod
|
|
184
|
+
def get_workflow_name(cls) -> str:
|
|
185
|
+
"""Generate a human-readable workflow name from the class name.
|
|
186
|
+
|
|
187
|
+
Returns:
|
|
188
|
+
Class name split on uppercase letters and joined with spaces.
|
|
189
|
+
"""
|
|
190
|
+
name = cls.__name__.removesuffix(Workflow.__name__)
|
|
191
|
+
return " ".join(split_on_uppercase(name))
|
|
192
|
+
|
|
193
|
+
@classmethod
|
|
194
|
+
def get_run_name(cls) -> str:
|
|
195
|
+
"""Get the display name for workflow runs.
|
|
196
|
+
|
|
197
|
+
Returns:
|
|
198
|
+
The workflow name by default.
|
|
199
|
+
"""
|
|
200
|
+
return cls.get_workflow_name()
|
|
201
|
+
|
|
202
|
+
# Build Utilities
|
|
203
|
+
# ----------------------------------------------------------------------------
|
|
204
|
+
@classmethod
|
|
205
|
+
def get_job( # noqa: PLR0913
|
|
206
|
+
cls,
|
|
207
|
+
job_func: Callable[..., Any],
|
|
208
|
+
needs: list[str] | None = None,
|
|
209
|
+
strategy: dict[str, Any] | None = None,
|
|
210
|
+
permissions: dict[str, Any] | None = None,
|
|
211
|
+
runs_on: str = UBUNTU_LATEST,
|
|
212
|
+
if_condition: str | None = None,
|
|
213
|
+
steps: list[dict[str, Any]] | None = None,
|
|
214
|
+
job: dict[str, Any] | None = None,
|
|
215
|
+
) -> dict[str, Any]:
|
|
216
|
+
"""Build a job configuration.
|
|
217
|
+
|
|
218
|
+
Args:
|
|
219
|
+
job_func: Function representing the job, used to generate the ID.
|
|
220
|
+
needs: List of job IDs this job depends on.
|
|
221
|
+
strategy: Matrix or other strategy configuration.
|
|
222
|
+
permissions: Job-level permissions.
|
|
223
|
+
runs_on: Runner label. Defaults to ubuntu-latest.
|
|
224
|
+
if_condition: Conditional expression for job execution.
|
|
225
|
+
steps: List of step configurations.
|
|
226
|
+
job: Existing job dict to update.
|
|
227
|
+
|
|
228
|
+
Returns:
|
|
229
|
+
Dict mapping job ID to job configuration.
|
|
230
|
+
"""
|
|
231
|
+
name = cls.make_id_from_func(job_func)
|
|
232
|
+
if job is None:
|
|
233
|
+
job = {}
|
|
234
|
+
job_config: dict[str, Any] = {}
|
|
235
|
+
if needs is not None:
|
|
236
|
+
job_config["needs"] = needs
|
|
237
|
+
if strategy is not None:
|
|
238
|
+
job_config["strategy"] = strategy
|
|
239
|
+
if permissions is not None:
|
|
240
|
+
job_config["permissions"] = permissions
|
|
241
|
+
job_config["runs-on"] = runs_on
|
|
242
|
+
if if_condition is not None:
|
|
243
|
+
job_config["if"] = if_condition
|
|
244
|
+
if steps is not None:
|
|
245
|
+
job_config["steps"] = steps
|
|
246
|
+
job_config.update(job)
|
|
247
|
+
return {name: job_config}
|
|
248
|
+
|
|
249
|
+
@classmethod
|
|
250
|
+
def make_name_from_func(cls, func: Callable[..., Any]) -> str:
|
|
251
|
+
"""Generate a human-readable name from a function.
|
|
252
|
+
|
|
253
|
+
Args:
|
|
254
|
+
func: Function to extract name from.
|
|
255
|
+
|
|
256
|
+
Returns:
|
|
257
|
+
Formatted name with prefix removed.
|
|
258
|
+
"""
|
|
259
|
+
name = make_name_from_obj(func, split_on="_", join_on=" ", capitalize=True)
|
|
260
|
+
prefix = split_on_uppercase(name)[0]
|
|
261
|
+
return name.removeprefix(prefix).strip()
|
|
262
|
+
|
|
263
|
+
@classmethod
|
|
264
|
+
def make_id_from_func(cls, func: Callable[..., Any]) -> str:
|
|
265
|
+
"""Generate a job/step ID from a function name.
|
|
266
|
+
|
|
267
|
+
Args:
|
|
268
|
+
func: Function to extract ID from.
|
|
269
|
+
|
|
270
|
+
Returns:
|
|
271
|
+
Function name with prefix removed.
|
|
272
|
+
"""
|
|
273
|
+
name = getattr(func, "__name__", "")
|
|
274
|
+
if not name:
|
|
275
|
+
msg = f"Cannot extract name from {func}"
|
|
276
|
+
raise ValueError(msg)
|
|
277
|
+
prefix = name.split("_")[0]
|
|
278
|
+
return name.removeprefix(f"{prefix}_")
|
|
279
|
+
|
|
280
|
+
# triggers
|
|
281
|
+
@classmethod
|
|
282
|
+
def on_workflow_dispatch(cls) -> dict[str, Any]:
|
|
283
|
+
"""Create a manual workflow dispatch trigger.
|
|
284
|
+
|
|
285
|
+
Returns:
|
|
286
|
+
Trigger configuration for manual runs.
|
|
287
|
+
"""
|
|
288
|
+
return {"workflow_dispatch": {}}
|
|
289
|
+
|
|
290
|
+
@classmethod
|
|
291
|
+
def on_push(cls, branches: list[str] | None = None) -> dict[str, Any]:
|
|
292
|
+
"""Create a push trigger.
|
|
293
|
+
|
|
294
|
+
Args:
|
|
295
|
+
branches: Branches to trigger on. Defaults to ["main"].
|
|
296
|
+
|
|
297
|
+
Returns:
|
|
298
|
+
Trigger configuration for push events.
|
|
299
|
+
"""
|
|
300
|
+
if branches is None:
|
|
301
|
+
branches = ["main"]
|
|
302
|
+
return {"push": {"branches": branches}}
|
|
303
|
+
|
|
304
|
+
@classmethod
|
|
305
|
+
def on_schedule(cls, cron: str) -> dict[str, Any]:
|
|
306
|
+
"""Create a scheduled trigger.
|
|
307
|
+
|
|
308
|
+
Args:
|
|
309
|
+
cron: Cron expression for the schedule.
|
|
310
|
+
|
|
311
|
+
Returns:
|
|
312
|
+
Trigger configuration for scheduled runs.
|
|
313
|
+
"""
|
|
314
|
+
return {"schedule": [{"cron": cron}]}
|
|
315
|
+
|
|
316
|
+
@classmethod
|
|
317
|
+
def on_pull_request(cls, types: list[str] | None = None) -> dict[str, Any]:
|
|
318
|
+
"""Create a pull request trigger.
|
|
319
|
+
|
|
320
|
+
Args:
|
|
321
|
+
types: PR event types. Defaults to opened, synchronize, reopened.
|
|
322
|
+
|
|
323
|
+
Returns:
|
|
324
|
+
Trigger configuration for pull request events.
|
|
325
|
+
"""
|
|
326
|
+
if types is None:
|
|
327
|
+
types = ["opened", "synchronize", "reopened"]
|
|
328
|
+
return {"pull_request": {"types": types}}
|
|
329
|
+
|
|
330
|
+
@classmethod
|
|
331
|
+
def on_workflow_run(
|
|
332
|
+
cls, workflows: list[str] | None = None, branches: list[str] | None = None
|
|
333
|
+
) -> dict[str, Any]:
|
|
334
|
+
"""Create a workflow run trigger.
|
|
335
|
+
|
|
336
|
+
Args:
|
|
337
|
+
workflows: Workflow names to trigger on. Defaults to this workflow.
|
|
338
|
+
branches: Branches to filter on.
|
|
339
|
+
|
|
340
|
+
Returns:
|
|
341
|
+
Trigger configuration for workflow completion events.
|
|
342
|
+
"""
|
|
343
|
+
if workflows is None:
|
|
344
|
+
workflows = [cls.get_workflow_name()]
|
|
345
|
+
config: dict[str, Any] = {"workflows": workflows, "types": ["completed"]}
|
|
346
|
+
if branches is not None:
|
|
347
|
+
config["branches"] = branches
|
|
348
|
+
return {"workflow_run": config}
|
|
349
|
+
|
|
350
|
+
# permissions
|
|
351
|
+
@classmethod
|
|
352
|
+
def permission_content(cls, permission: str = "read") -> dict[str, Any]:
|
|
353
|
+
"""Create a contents permission configuration.
|
|
354
|
+
|
|
355
|
+
Args:
|
|
356
|
+
permission: Permission level (read, write, none).
|
|
357
|
+
|
|
358
|
+
Returns:
|
|
359
|
+
Dict with contents permission.
|
|
360
|
+
"""
|
|
361
|
+
return {"contents": permission}
|
|
362
|
+
|
|
363
|
+
# Steps
|
|
364
|
+
@classmethod
|
|
365
|
+
def get_step( # noqa: PLR0913
|
|
366
|
+
cls,
|
|
367
|
+
step_func: Callable[..., Any],
|
|
368
|
+
run: str | None = None,
|
|
369
|
+
if_condition: str | None = None,
|
|
370
|
+
uses: str | None = None,
|
|
371
|
+
with_: dict[str, Any] | None = None,
|
|
372
|
+
env: dict[str, Any] | None = None,
|
|
373
|
+
step: dict[str, Any] | None = None,
|
|
374
|
+
) -> dict[str, Any]:
|
|
375
|
+
"""Build a step configuration.
|
|
376
|
+
|
|
377
|
+
Args:
|
|
378
|
+
step_func: Function representing the step, used to generate name/ID.
|
|
379
|
+
run: Shell command to execute.
|
|
380
|
+
if_condition: Conditional expression for step execution.
|
|
381
|
+
uses: GitHub Action to use.
|
|
382
|
+
with_: Input parameters for the action.
|
|
383
|
+
env: Environment variables for the step.
|
|
384
|
+
step: Existing step dict to update.
|
|
385
|
+
|
|
386
|
+
Returns:
|
|
387
|
+
Step configuration dict.
|
|
388
|
+
"""
|
|
389
|
+
if step is None:
|
|
390
|
+
step = {}
|
|
391
|
+
# make name from setup function name if name is a function
|
|
392
|
+
name = cls.make_name_from_func(step_func)
|
|
393
|
+
id_ = cls.make_id_from_func(step_func)
|
|
394
|
+
step_config: dict[str, Any] = {"name": name, "id": id_}
|
|
395
|
+
if run is not None:
|
|
396
|
+
step_config["run"] = run
|
|
397
|
+
if if_condition is not None:
|
|
398
|
+
step_config["if"] = if_condition
|
|
399
|
+
if uses is not None:
|
|
400
|
+
step_config["uses"] = uses
|
|
401
|
+
if with_ is not None:
|
|
402
|
+
step_config["with"] = with_
|
|
403
|
+
if env is not None:
|
|
404
|
+
step_config["env"] = env
|
|
405
|
+
|
|
406
|
+
step_config.update(step)
|
|
407
|
+
|
|
408
|
+
return step_config
|
|
409
|
+
|
|
410
|
+
# Strategy
|
|
411
|
+
@classmethod
|
|
412
|
+
def strategy_matrix_os_and_python_version(
|
|
413
|
+
cls,
|
|
414
|
+
os: list[str] | None = None,
|
|
415
|
+
python_version: list[str] | None = None,
|
|
416
|
+
matrix: dict[str, list[Any]] | None = None,
|
|
417
|
+
strategy: dict[str, Any] | None = None,
|
|
418
|
+
) -> dict[str, Any]:
|
|
419
|
+
"""Create a strategy with OS and Python version matrix.
|
|
420
|
+
|
|
421
|
+
Args:
|
|
422
|
+
os: List of OS runners. Defaults to all major platforms.
|
|
423
|
+
python_version: List of Python versions. Defaults to supported versions.
|
|
424
|
+
matrix: Additional matrix dimensions.
|
|
425
|
+
strategy: Additional strategy options.
|
|
426
|
+
|
|
427
|
+
Returns:
|
|
428
|
+
Strategy configuration with OS and Python matrix.
|
|
429
|
+
"""
|
|
430
|
+
return cls.strategy_matrix(
|
|
431
|
+
matrix=cls.matrix_os_and_python_version(
|
|
432
|
+
os=os, python_version=python_version, matrix=matrix
|
|
433
|
+
),
|
|
434
|
+
strategy=strategy,
|
|
435
|
+
)
|
|
436
|
+
|
|
437
|
+
@classmethod
|
|
438
|
+
def strategy_matrix_python_version(
|
|
439
|
+
cls,
|
|
440
|
+
python_version: list[str] | None = None,
|
|
441
|
+
matrix: dict[str, list[Any]] | None = None,
|
|
442
|
+
strategy: dict[str, Any] | None = None,
|
|
443
|
+
) -> dict[str, Any]:
|
|
444
|
+
"""Create a strategy with Python version matrix.
|
|
445
|
+
|
|
446
|
+
Args:
|
|
447
|
+
python_version: List of Python versions. Defaults to supported versions.
|
|
448
|
+
matrix: Additional matrix dimensions.
|
|
449
|
+
strategy: Additional strategy options.
|
|
450
|
+
|
|
451
|
+
Returns:
|
|
452
|
+
Strategy configuration with Python version matrix.
|
|
453
|
+
"""
|
|
454
|
+
return cls.strategy_matrix(
|
|
455
|
+
matrix=cls.matrix_python_version(
|
|
456
|
+
python_version=python_version, matrix=matrix
|
|
457
|
+
),
|
|
458
|
+
strategy=strategy,
|
|
459
|
+
)
|
|
460
|
+
|
|
461
|
+
@classmethod
|
|
462
|
+
def strategy_matrix_os(
|
|
463
|
+
cls,
|
|
464
|
+
os: list[str] | None = None,
|
|
465
|
+
matrix: dict[str, list[Any]] | None = None,
|
|
466
|
+
strategy: dict[str, Any] | None = None,
|
|
467
|
+
) -> dict[str, Any]:
|
|
468
|
+
"""Create a strategy with OS matrix.
|
|
469
|
+
|
|
470
|
+
Args:
|
|
471
|
+
os: List of OS runners. Defaults to all major platforms.
|
|
472
|
+
matrix: Additional matrix dimensions.
|
|
473
|
+
strategy: Additional strategy options.
|
|
474
|
+
|
|
475
|
+
Returns:
|
|
476
|
+
Strategy configuration with OS matrix.
|
|
477
|
+
"""
|
|
478
|
+
return cls.strategy_matrix(
|
|
479
|
+
matrix=cls.matrix_os(os=os, matrix=matrix), strategy=strategy
|
|
480
|
+
)
|
|
481
|
+
|
|
482
|
+
@classmethod
|
|
483
|
+
def strategy_matrix(
|
|
484
|
+
cls,
|
|
485
|
+
*,
|
|
486
|
+
strategy: dict[str, Any] | None = None,
|
|
487
|
+
matrix: dict[str, list[Any]] | None = None,
|
|
488
|
+
) -> dict[str, Any]:
|
|
489
|
+
"""Create a matrix strategy configuration.
|
|
490
|
+
|
|
491
|
+
Args:
|
|
492
|
+
strategy: Base strategy options.
|
|
493
|
+
matrix: Matrix dimensions.
|
|
494
|
+
|
|
495
|
+
Returns:
|
|
496
|
+
Strategy configuration with matrix.
|
|
497
|
+
"""
|
|
498
|
+
if strategy is None:
|
|
499
|
+
strategy = {}
|
|
500
|
+
if matrix is None:
|
|
501
|
+
matrix = {}
|
|
502
|
+
strategy["matrix"] = matrix
|
|
503
|
+
return cls.get_strategy(strategy=strategy)
|
|
504
|
+
|
|
505
|
+
@classmethod
|
|
506
|
+
def get_strategy(
|
|
507
|
+
cls,
|
|
508
|
+
*,
|
|
509
|
+
strategy: dict[str, Any],
|
|
510
|
+
) -> dict[str, Any]:
|
|
511
|
+
"""Finalize a strategy configuration.
|
|
512
|
+
|
|
513
|
+
Args:
|
|
514
|
+
strategy: Strategy configuration to finalize.
|
|
515
|
+
|
|
516
|
+
Returns:
|
|
517
|
+
Strategy with fail-fast defaulting to True.
|
|
518
|
+
"""
|
|
519
|
+
strategy["fail-fast"] = strategy.pop("fail-fast", True)
|
|
520
|
+
return strategy
|
|
521
|
+
|
|
522
|
+
@classmethod
|
|
523
|
+
def matrix_os_and_python_version(
|
|
524
|
+
cls,
|
|
525
|
+
os: list[str] | None = None,
|
|
526
|
+
python_version: list[str] | None = None,
|
|
527
|
+
matrix: dict[str, list[Any]] | None = None,
|
|
528
|
+
) -> dict[str, Any]:
|
|
529
|
+
"""Create a matrix with OS and Python version dimensions.
|
|
530
|
+
|
|
531
|
+
Args:
|
|
532
|
+
os: List of OS runners. Defaults to all major platforms.
|
|
533
|
+
python_version: List of Python versions. Defaults to supported versions.
|
|
534
|
+
matrix: Additional matrix dimensions.
|
|
535
|
+
|
|
536
|
+
Returns:
|
|
537
|
+
Matrix configuration with os and python-version.
|
|
538
|
+
"""
|
|
539
|
+
if matrix is None:
|
|
540
|
+
matrix = {}
|
|
541
|
+
os_matrix = cls.matrix_os(os=os, matrix=matrix)["os"]
|
|
542
|
+
python_version_matrix = cls.matrix_python_version(
|
|
543
|
+
python_version=python_version, matrix=matrix
|
|
544
|
+
)["python-version"]
|
|
545
|
+
matrix["os"] = os_matrix
|
|
546
|
+
matrix["python-version"] = python_version_matrix
|
|
547
|
+
return cls.get_matrix(matrix=matrix)
|
|
548
|
+
|
|
549
|
+
@classmethod
|
|
550
|
+
def matrix_os(
|
|
551
|
+
cls,
|
|
552
|
+
*,
|
|
553
|
+
os: list[str] | None = None,
|
|
554
|
+
matrix: dict[str, list[Any]] | None = None,
|
|
555
|
+
) -> dict[str, Any]:
|
|
556
|
+
"""Create a matrix with OS dimension.
|
|
557
|
+
|
|
558
|
+
Args:
|
|
559
|
+
os: List of OS runners. Defaults to Ubuntu, Windows, macOS.
|
|
560
|
+
matrix: Additional matrix dimensions.
|
|
561
|
+
|
|
562
|
+
Returns:
|
|
563
|
+
Matrix configuration with os.
|
|
564
|
+
"""
|
|
565
|
+
if os is None:
|
|
566
|
+
os = [cls.UBUNTU_LATEST, cls.WINDOWS_LATEST, cls.MACOS_LATEST]
|
|
567
|
+
if matrix is None:
|
|
568
|
+
matrix = {}
|
|
569
|
+
matrix["os"] = os
|
|
570
|
+
return cls.get_matrix(matrix=matrix)
|
|
571
|
+
|
|
572
|
+
@classmethod
|
|
573
|
+
def matrix_python_version(
|
|
574
|
+
cls,
|
|
575
|
+
*,
|
|
576
|
+
python_version: list[str] | None = None,
|
|
577
|
+
matrix: dict[str, list[Any]] | None = None,
|
|
578
|
+
) -> dict[str, Any]:
|
|
579
|
+
"""Create a matrix with Python version dimension.
|
|
580
|
+
|
|
581
|
+
Args:
|
|
582
|
+
python_version: List of Python versions. Defaults to supported versions.
|
|
583
|
+
matrix: Additional matrix dimensions.
|
|
584
|
+
|
|
585
|
+
Returns:
|
|
586
|
+
Matrix configuration with python-version.
|
|
587
|
+
"""
|
|
588
|
+
if python_version is None:
|
|
589
|
+
python_version = [
|
|
590
|
+
str(v) for v in PyprojectConfigFile.get_supported_python_versions()
|
|
591
|
+
]
|
|
592
|
+
if matrix is None:
|
|
593
|
+
matrix = {}
|
|
594
|
+
matrix["python-version"] = python_version
|
|
595
|
+
return cls.get_matrix(matrix=matrix)
|
|
596
|
+
|
|
597
|
+
@classmethod
|
|
598
|
+
def get_matrix(cls, matrix: dict[str, list[Any]]) -> dict[str, Any]:
|
|
599
|
+
"""Return the matrix configuration.
|
|
600
|
+
|
|
601
|
+
Args:
|
|
602
|
+
matrix: Matrix dimensions.
|
|
603
|
+
|
|
604
|
+
Returns:
|
|
605
|
+
The matrix configuration unchanged.
|
|
606
|
+
"""
|
|
607
|
+
return matrix
|
|
608
|
+
|
|
609
|
+
# Workflow Steps
|
|
610
|
+
# ----------------------------------------------------------------------------
|
|
611
|
+
# Combined Steps
|
|
612
|
+
@classmethod
|
|
613
|
+
def steps_core_setup(
|
|
614
|
+
cls, python_version: str | None = None, *, repo_token: bool = False
|
|
615
|
+
) -> list[dict[str, Any]]:
|
|
616
|
+
"""Get the core setup steps for any workflow.
|
|
617
|
+
|
|
618
|
+
Args:
|
|
619
|
+
python_version: Python version to use. Defaults to latest supported.
|
|
620
|
+
repo_token: Whether to use REPO_TOKEN for checkout.
|
|
621
|
+
|
|
622
|
+
Returns:
|
|
623
|
+
List with checkout and project management setup steps.
|
|
624
|
+
"""
|
|
625
|
+
if python_version is None:
|
|
626
|
+
python_version = str(
|
|
627
|
+
PyprojectConfigFile.get_latest_possible_python_version(level="minor")
|
|
628
|
+
)
|
|
629
|
+
return [
|
|
630
|
+
cls.step_checkout_repository(repo_token=repo_token),
|
|
631
|
+
cls.step_setup_git(),
|
|
632
|
+
cls.step_setup_project_mgt(python_version=python_version),
|
|
633
|
+
]
|
|
634
|
+
|
|
635
|
+
@classmethod
|
|
636
|
+
def steps_core_installed_setup(
|
|
637
|
+
cls,
|
|
638
|
+
*,
|
|
639
|
+
no_dev: bool = False,
|
|
640
|
+
python_version: str | None = None,
|
|
641
|
+
repo_token: bool = False,
|
|
642
|
+
) -> list[dict[str, Any]]:
|
|
643
|
+
"""Get core setup steps with dependency installation.
|
|
644
|
+
|
|
645
|
+
Args:
|
|
646
|
+
python_version: Python version to use. Defaults to latest supported.
|
|
647
|
+
repo_token: Whether to use REPO_TOKEN for checkout.
|
|
648
|
+
no_dev: Whether to install dev dependencies.
|
|
649
|
+
|
|
650
|
+
Returns:
|
|
651
|
+
List with setup, install, and dependency update steps.
|
|
652
|
+
"""
|
|
653
|
+
return [
|
|
654
|
+
*cls.steps_core_setup(python_version=python_version, repo_token=repo_token),
|
|
655
|
+
cls.step_patch_version(),
|
|
656
|
+
cls.step_install_python_dependencies(no_dev=no_dev),
|
|
657
|
+
cls.step_add_dependency_updates_to_git(),
|
|
658
|
+
]
|
|
659
|
+
|
|
660
|
+
@classmethod
|
|
661
|
+
def steps_core_matrix_setup(
|
|
662
|
+
cls,
|
|
663
|
+
*,
|
|
664
|
+
no_dev: bool = False,
|
|
665
|
+
python_version: str | None = None,
|
|
666
|
+
repo_token: bool = False,
|
|
667
|
+
) -> list[dict[str, Any]]:
|
|
668
|
+
"""Get core setup steps for matrix jobs.
|
|
669
|
+
|
|
670
|
+
Args:
|
|
671
|
+
python_version: Python version to use. Defaults to matrix value.
|
|
672
|
+
repo_token: Whether to use REPO_TOKEN for checkout.
|
|
673
|
+
no_dev: Whether to install dev dependencies.
|
|
674
|
+
|
|
675
|
+
Returns:
|
|
676
|
+
List with full setup steps for matrix execution.
|
|
677
|
+
"""
|
|
678
|
+
return [
|
|
679
|
+
*cls.steps_core_installed_setup(
|
|
680
|
+
python_version=python_version,
|
|
681
|
+
repo_token=repo_token,
|
|
682
|
+
no_dev=no_dev,
|
|
683
|
+
),
|
|
684
|
+
]
|
|
685
|
+
|
|
686
|
+
@classmethod
|
|
687
|
+
def steps_configure_keyring_if_needed(cls) -> list[dict[str, Any]]:
|
|
688
|
+
"""Get keyring configuration steps if keyring is a dependency.
|
|
689
|
+
|
|
690
|
+
Returns:
|
|
691
|
+
List with keyring setup step if needed, empty otherwise.
|
|
692
|
+
"""
|
|
693
|
+
steps: list[dict[str, Any]] = []
|
|
694
|
+
if "keyring" in DependencyGraph.get_all_dependencies():
|
|
695
|
+
steps.append(cls.step_setup_keyring())
|
|
696
|
+
return steps
|
|
697
|
+
|
|
698
|
+
# Single Step
|
|
699
|
+
@classmethod
|
|
700
|
+
def step_opt_out_of_workflow(
|
|
701
|
+
cls,
|
|
702
|
+
*,
|
|
703
|
+
step: dict[str, Any] | None = None,
|
|
704
|
+
) -> dict[str, Any]:
|
|
705
|
+
"""Create a step that opts out of the workflow.
|
|
706
|
+
|
|
707
|
+
Args:
|
|
708
|
+
step: Existing step dict to update.
|
|
709
|
+
|
|
710
|
+
Returns:
|
|
711
|
+
Step that echoes an opt-out message.
|
|
712
|
+
"""
|
|
713
|
+
return cls.get_step(
|
|
714
|
+
step_func=cls.step_opt_out_of_workflow,
|
|
715
|
+
run=f"echo 'Opting out of {cls.get_workflow_name()} workflow.'",
|
|
716
|
+
step=step,
|
|
717
|
+
)
|
|
718
|
+
|
|
719
|
+
@classmethod
|
|
720
|
+
def step_aggregate_matrix_results(
|
|
721
|
+
cls,
|
|
722
|
+
*,
|
|
723
|
+
step: dict[str, Any] | None = None,
|
|
724
|
+
) -> dict[str, Any]:
|
|
725
|
+
"""Create a step that aggregates matrix job results.
|
|
726
|
+
|
|
727
|
+
Args:
|
|
728
|
+
step: Existing step dict to update.
|
|
729
|
+
|
|
730
|
+
Returns:
|
|
731
|
+
Step configuration for result aggregation.
|
|
732
|
+
"""
|
|
733
|
+
return cls.get_step(
|
|
734
|
+
step_func=cls.step_aggregate_matrix_results,
|
|
735
|
+
run="echo 'Aggregating matrix results into one job.'",
|
|
736
|
+
step=step,
|
|
737
|
+
)
|
|
738
|
+
|
|
739
|
+
@classmethod
|
|
740
|
+
def step_no_builder_defined(
|
|
741
|
+
cls,
|
|
742
|
+
*,
|
|
743
|
+
step: dict[str, Any] | None = None,
|
|
744
|
+
) -> dict[str, Any]:
|
|
745
|
+
"""Create a placeholder step when no builders are defined.
|
|
746
|
+
|
|
747
|
+
Args:
|
|
748
|
+
step: Existing step dict to update.
|
|
749
|
+
|
|
750
|
+
Returns:
|
|
751
|
+
Step that echoes a skip message.
|
|
752
|
+
"""
|
|
753
|
+
return cls.get_step(
|
|
754
|
+
step_func=cls.step_no_builder_defined,
|
|
755
|
+
run="echo 'No non-abstract builders defined. Skipping build.'",
|
|
756
|
+
step=step,
|
|
757
|
+
)
|
|
758
|
+
|
|
759
|
+
@classmethod
|
|
760
|
+
def step_install_container_engine(
|
|
761
|
+
cls,
|
|
762
|
+
*,
|
|
763
|
+
step: dict[str, Any] | None = None,
|
|
764
|
+
) -> dict[str, Any]:
|
|
765
|
+
"""Create a step that installs the container engine.
|
|
766
|
+
|
|
767
|
+
We use podman as the container engine.
|
|
768
|
+
|
|
769
|
+
Args:
|
|
770
|
+
step: Existing step dict to update.
|
|
771
|
+
|
|
772
|
+
Returns:
|
|
773
|
+
Step that installs podman.
|
|
774
|
+
"""
|
|
775
|
+
return cls.get_step(
|
|
776
|
+
step_func=cls.step_install_container_engine,
|
|
777
|
+
uses="redhat-actions/podman-install@main",
|
|
778
|
+
with_={"github-token": cls.insert_github_token()},
|
|
779
|
+
step=step,
|
|
780
|
+
)
|
|
781
|
+
|
|
782
|
+
@classmethod
|
|
783
|
+
def step_build_container_image(
|
|
784
|
+
cls,
|
|
785
|
+
*,
|
|
786
|
+
step: dict[str, Any] | None = None,
|
|
787
|
+
) -> dict[str, Any]:
|
|
788
|
+
"""Create a step that builds the container image.
|
|
789
|
+
|
|
790
|
+
Args:
|
|
791
|
+
step: Existing step dict to update.
|
|
792
|
+
|
|
793
|
+
Returns:
|
|
794
|
+
Step that builds the container image.
|
|
795
|
+
"""
|
|
796
|
+
return cls.get_step(
|
|
797
|
+
step_func=cls.step_build_container_image,
|
|
798
|
+
run=f"podman build -t {PyprojectConfigFile.get_project_name()} .",
|
|
799
|
+
step=step,
|
|
800
|
+
)
|
|
801
|
+
|
|
802
|
+
@classmethod
|
|
803
|
+
def step_save_container_image(
|
|
804
|
+
cls,
|
|
805
|
+
*,
|
|
806
|
+
step: dict[str, Any] | None = None,
|
|
807
|
+
) -> dict[str, Any]:
|
|
808
|
+
"""Create a step that saves the container image to a file.
|
|
809
|
+
|
|
810
|
+
Args:
|
|
811
|
+
step: Existing step dict to update.
|
|
812
|
+
|
|
813
|
+
Returns:
|
|
814
|
+
Step that saves the container image.
|
|
815
|
+
"""
|
|
816
|
+
image_file = Path(f"{PyprojectConfigFile.get_project_name()}.tar")
|
|
817
|
+
image_path = Path(cls.ARTIFACTS_DIR_NAME) / image_file
|
|
818
|
+
return cls.get_step(
|
|
819
|
+
step_func=cls.step_save_container_image,
|
|
820
|
+
run=f"podman save -o {image_path.as_posix()} {image_file.stem}",
|
|
821
|
+
step=step,
|
|
822
|
+
)
|
|
823
|
+
|
|
824
|
+
@classmethod
|
|
825
|
+
def step_make_dist_folder(
|
|
826
|
+
cls,
|
|
827
|
+
*,
|
|
828
|
+
step: dict[str, Any] | None = None,
|
|
829
|
+
) -> dict[str, Any]:
|
|
830
|
+
"""Create a step that makes the dist folder.
|
|
831
|
+
|
|
832
|
+
Creates only if it does not exist.
|
|
833
|
+
|
|
834
|
+
Args:
|
|
835
|
+
step: Existing step dict to update.
|
|
836
|
+
|
|
837
|
+
Returns:
|
|
838
|
+
Step that makes the dist folder.
|
|
839
|
+
"""
|
|
840
|
+
return cls.get_step(
|
|
841
|
+
step_func=cls.step_make_dist_folder,
|
|
842
|
+
run=f"mkdir -p {Builder.ARTIFACTS_DIR_NAME}",
|
|
843
|
+
step=step,
|
|
844
|
+
)
|
|
845
|
+
|
|
846
|
+
@classmethod
|
|
847
|
+
def step_run_tests(
|
|
848
|
+
cls,
|
|
849
|
+
*,
|
|
850
|
+
step: dict[str, Any] | None = None,
|
|
851
|
+
) -> dict[str, Any]:
|
|
852
|
+
"""Create a step that runs pytest.
|
|
853
|
+
|
|
854
|
+
Args:
|
|
855
|
+
step: Existing step dict to update.
|
|
856
|
+
|
|
857
|
+
Returns:
|
|
858
|
+
Step configuration for running tests.
|
|
859
|
+
"""
|
|
860
|
+
if step is None:
|
|
861
|
+
step = {}
|
|
862
|
+
if PyprojectConfigFile.get_package_name() == pyrig.__name__:
|
|
863
|
+
step.setdefault("env", {})["REPO_TOKEN"] = cls.insert_repo_token()
|
|
864
|
+
run = f"{PROJECT_MGT_RUN_SCRIPT} pytest --log-cli-level=INFO --cov-report=xml"
|
|
865
|
+
return cls.get_step(
|
|
866
|
+
step_func=cls.step_run_tests,
|
|
867
|
+
run=run,
|
|
868
|
+
step=step,
|
|
869
|
+
)
|
|
870
|
+
|
|
871
|
+
@classmethod
|
|
872
|
+
def step_upload_coverage_report(
|
|
873
|
+
cls,
|
|
874
|
+
*,
|
|
875
|
+
step: dict[str, Any] | None = None,
|
|
876
|
+
) -> dict[str, Any]:
|
|
877
|
+
"""Create a step that uploads the coverage report.
|
|
878
|
+
|
|
879
|
+
If the repository is private, the workflow will fail and
|
|
880
|
+
a Codecov token has to be added to the repository secrets.
|
|
881
|
+
You need an account on Codecov for this.
|
|
882
|
+
This is why we fail_ci_if_error is not set to true.
|
|
883
|
+
This is an optional service and should not break the workflow.
|
|
884
|
+
|
|
885
|
+
Args:
|
|
886
|
+
step: Existing step dict to update.
|
|
887
|
+
|
|
888
|
+
Returns:
|
|
889
|
+
Step configuration for uploading coverage report.
|
|
890
|
+
"""
|
|
891
|
+
return cls.get_step(
|
|
892
|
+
step_func=cls.step_upload_coverage_report,
|
|
893
|
+
uses="codecov/codecov-action@main",
|
|
894
|
+
with_={
|
|
895
|
+
"files": "coverage.xml",
|
|
896
|
+
"token": cls.insert_codecov_token(),
|
|
897
|
+
},
|
|
898
|
+
step=step,
|
|
899
|
+
)
|
|
900
|
+
|
|
901
|
+
@classmethod
|
|
902
|
+
def step_patch_version(
|
|
903
|
+
cls,
|
|
904
|
+
*,
|
|
905
|
+
step: dict[str, Any] | None = None,
|
|
906
|
+
) -> dict[str, Any]:
|
|
907
|
+
"""Create a step that bumps the patch version.
|
|
908
|
+
|
|
909
|
+
Args:
|
|
910
|
+
step: Existing step dict to update.
|
|
911
|
+
|
|
912
|
+
Returns:
|
|
913
|
+
Step that increments version and stages pyproject.toml.
|
|
914
|
+
"""
|
|
915
|
+
return cls.get_step(
|
|
916
|
+
step_func=cls.step_patch_version,
|
|
917
|
+
run=f"{PROJECT_MGT} version --bump patch && git add pyproject.toml",
|
|
918
|
+
step=step,
|
|
919
|
+
)
|
|
920
|
+
|
|
921
|
+
@classmethod
|
|
922
|
+
def step_add_dependency_updates_to_git(
|
|
923
|
+
cls,
|
|
924
|
+
*,
|
|
925
|
+
step: dict[str, Any] | None = None,
|
|
926
|
+
) -> dict[str, Any]:
|
|
927
|
+
"""Create a step that stages dependency file changes.
|
|
928
|
+
|
|
929
|
+
Args:
|
|
930
|
+
step: Existing step dict to update.
|
|
931
|
+
|
|
932
|
+
Returns:
|
|
933
|
+
Step that stages pyproject.toml and uv.lock.
|
|
934
|
+
"""
|
|
935
|
+
return cls.get_step(
|
|
936
|
+
step_func=cls.step_add_dependency_updates_to_git,
|
|
937
|
+
run=f"git add pyproject.toml {PROJECT_MGT}.lock",
|
|
938
|
+
step=step,
|
|
939
|
+
)
|
|
940
|
+
|
|
941
|
+
@classmethod
|
|
942
|
+
def step_checkout_repository(
|
|
943
|
+
cls,
|
|
944
|
+
*,
|
|
945
|
+
step: dict[str, Any] | None = None,
|
|
946
|
+
fetch_depth: int | None = None,
|
|
947
|
+
repo_token: bool = False,
|
|
948
|
+
) -> dict[str, Any]:
|
|
949
|
+
"""Create a step that checks out the repository.
|
|
950
|
+
|
|
951
|
+
Args:
|
|
952
|
+
step: Existing step dict to update.
|
|
953
|
+
fetch_depth: Git fetch depth. None for full history.
|
|
954
|
+
repo_token: Whether to use REPO_TOKEN for authentication.
|
|
955
|
+
|
|
956
|
+
Returns:
|
|
957
|
+
Step using actions/checkout.
|
|
958
|
+
"""
|
|
959
|
+
if step is None:
|
|
960
|
+
step = {}
|
|
961
|
+
if fetch_depth is not None:
|
|
962
|
+
step.setdefault("with", {})["fetch-depth"] = fetch_depth
|
|
963
|
+
if repo_token:
|
|
964
|
+
step.setdefault("with", {})["token"] = cls.insert_repo_token()
|
|
965
|
+
return cls.get_step(
|
|
966
|
+
step_func=cls.step_checkout_repository,
|
|
967
|
+
uses="actions/checkout@main",
|
|
968
|
+
step=step,
|
|
969
|
+
)
|
|
970
|
+
|
|
971
|
+
@classmethod
|
|
972
|
+
def step_setup_git(
|
|
973
|
+
cls,
|
|
974
|
+
*,
|
|
975
|
+
step: dict[str, Any] | None = None,
|
|
976
|
+
) -> dict[str, Any]:
|
|
977
|
+
"""Create a step that configures git user for commits.
|
|
978
|
+
|
|
979
|
+
Args:
|
|
980
|
+
step: Existing step dict to update.
|
|
981
|
+
|
|
982
|
+
Returns:
|
|
983
|
+
Step that sets git user.email and user.name.
|
|
984
|
+
"""
|
|
985
|
+
return cls.get_step(
|
|
986
|
+
step_func=cls.step_setup_git,
|
|
987
|
+
run='git config --global user.email "github-actions[bot]@users.noreply.github.com" && git config --global user.name "github-actions[bot]"', # noqa: E501
|
|
988
|
+
step=step,
|
|
989
|
+
)
|
|
990
|
+
|
|
991
|
+
@classmethod
|
|
992
|
+
def step_setup_python(
|
|
993
|
+
cls,
|
|
994
|
+
*,
|
|
995
|
+
step: dict[str, Any] | None = None,
|
|
996
|
+
python_version: str | None = None,
|
|
997
|
+
) -> dict[str, Any]:
|
|
998
|
+
"""Create a step that sets up Python.
|
|
999
|
+
|
|
1000
|
+
Args:
|
|
1001
|
+
step: Existing step dict to update.
|
|
1002
|
+
python_version: Python version to install. Defaults to latest.
|
|
1003
|
+
|
|
1004
|
+
Returns:
|
|
1005
|
+
Step using actions/setup-python.
|
|
1006
|
+
"""
|
|
1007
|
+
if step is None:
|
|
1008
|
+
step = {}
|
|
1009
|
+
if python_version is None:
|
|
1010
|
+
python_version = str(
|
|
1011
|
+
PyprojectConfigFile.get_latest_possible_python_version(level="minor")
|
|
1012
|
+
)
|
|
1013
|
+
|
|
1014
|
+
step.setdefault("with", {})["python-version"] = python_version
|
|
1015
|
+
return cls.get_step(
|
|
1016
|
+
step_func=cls.step_setup_python,
|
|
1017
|
+
uses="actions/setup-python@main",
|
|
1018
|
+
step=step,
|
|
1019
|
+
)
|
|
1020
|
+
|
|
1021
|
+
@classmethod
|
|
1022
|
+
def step_setup_project_mgt(
|
|
1023
|
+
cls,
|
|
1024
|
+
*,
|
|
1025
|
+
python_version: str | None = None,
|
|
1026
|
+
step: dict[str, Any] | None = None,
|
|
1027
|
+
) -> dict[str, Any]:
|
|
1028
|
+
"""Create a step that sets up the project management tool (uv).
|
|
1029
|
+
|
|
1030
|
+
Args:
|
|
1031
|
+
python_version: Python version to configure.
|
|
1032
|
+
step: Existing step dict to update.
|
|
1033
|
+
|
|
1034
|
+
Returns:
|
|
1035
|
+
Step using astral-sh/setup-uv.
|
|
1036
|
+
"""
|
|
1037
|
+
return cls.get_step(
|
|
1038
|
+
step_func=cls.step_setup_project_mgt,
|
|
1039
|
+
uses="astral-sh/setup-uv@main",
|
|
1040
|
+
with_={"python-version": python_version},
|
|
1041
|
+
step=step,
|
|
1042
|
+
)
|
|
1043
|
+
|
|
1044
|
+
@classmethod
|
|
1045
|
+
def step_build_wheel(
|
|
1046
|
+
cls,
|
|
1047
|
+
*,
|
|
1048
|
+
step: dict[str, Any] | None = None,
|
|
1049
|
+
) -> dict[str, Any]:
|
|
1050
|
+
"""Create a step that builds the Python wheel.
|
|
1051
|
+
|
|
1052
|
+
Args:
|
|
1053
|
+
step: Existing step dict to update.
|
|
1054
|
+
|
|
1055
|
+
Returns:
|
|
1056
|
+
Step that runs uv build.
|
|
1057
|
+
"""
|
|
1058
|
+
return cls.get_step(
|
|
1059
|
+
step_func=cls.step_build_wheel,
|
|
1060
|
+
run=f"{PROJECT_MGT} build",
|
|
1061
|
+
step=step,
|
|
1062
|
+
)
|
|
1063
|
+
|
|
1064
|
+
@classmethod
|
|
1065
|
+
def step_publish_to_pypi(
|
|
1066
|
+
cls,
|
|
1067
|
+
*,
|
|
1068
|
+
step: dict[str, Any] | None = None,
|
|
1069
|
+
) -> dict[str, Any]:
|
|
1070
|
+
"""Create a step that publishes the package to PyPI.
|
|
1071
|
+
|
|
1072
|
+
Args:
|
|
1073
|
+
step: Existing step dict to update.
|
|
1074
|
+
|
|
1075
|
+
Returns:
|
|
1076
|
+
Step that runs uv publish with PYPI_TOKEN.
|
|
1077
|
+
"""
|
|
1078
|
+
return cls.get_step(
|
|
1079
|
+
step_func=cls.step_publish_to_pypi,
|
|
1080
|
+
run=f"{PROJECT_MGT} publish --token {cls.insert_pypi_token()}",
|
|
1081
|
+
step=step,
|
|
1082
|
+
)
|
|
1083
|
+
|
|
1084
|
+
@classmethod
|
|
1085
|
+
def step_install_python_dependencies(
|
|
1086
|
+
cls,
|
|
1087
|
+
*,
|
|
1088
|
+
no_dev: bool = False,
|
|
1089
|
+
step: dict[str, Any] | None = None,
|
|
1090
|
+
) -> dict[str, Any]:
|
|
1091
|
+
"""Create a step that installs Python dependencies.
|
|
1092
|
+
|
|
1093
|
+
Args:
|
|
1094
|
+
step: Existing step dict to update.
|
|
1095
|
+
no_dev: Whether to install dev dependencies.
|
|
1096
|
+
|
|
1097
|
+
Returns:
|
|
1098
|
+
Step that runs uv sync.
|
|
1099
|
+
"""
|
|
1100
|
+
upgrade = f"{PROJECT_MGT} lock --upgrade"
|
|
1101
|
+
install = f"{PROJECT_MGT} sync"
|
|
1102
|
+
if no_dev:
|
|
1103
|
+
install += " --no-group dev"
|
|
1104
|
+
run = f"{upgrade} && {install}"
|
|
1105
|
+
|
|
1106
|
+
return cls.get_step(
|
|
1107
|
+
step_func=cls.step_install_python_dependencies,
|
|
1108
|
+
run=run,
|
|
1109
|
+
step=step,
|
|
1110
|
+
)
|
|
1111
|
+
|
|
1112
|
+
@classmethod
|
|
1113
|
+
def step_setup_keyring(
|
|
1114
|
+
cls,
|
|
1115
|
+
*,
|
|
1116
|
+
step: dict[str, Any] | None = None,
|
|
1117
|
+
) -> dict[str, Any]:
|
|
1118
|
+
"""Create a step that configures keyring for CI.
|
|
1119
|
+
|
|
1120
|
+
Args:
|
|
1121
|
+
step: Existing step dict to update.
|
|
1122
|
+
|
|
1123
|
+
Returns:
|
|
1124
|
+
Step that sets up PlaintextKeyring for CI environments.
|
|
1125
|
+
"""
|
|
1126
|
+
return cls.get_step(
|
|
1127
|
+
step_func=cls.step_setup_keyring,
|
|
1128
|
+
run=f'{PROJECT_MGT_RUN_SCRIPT} python -c "import keyring; from keyrings.alt.file import PlaintextKeyring; keyring.set_keyring(PlaintextKeyring());"', # noqa: E501
|
|
1129
|
+
step=step,
|
|
1130
|
+
)
|
|
1131
|
+
|
|
1132
|
+
@classmethod
|
|
1133
|
+
def step_protect_repository(
|
|
1134
|
+
cls,
|
|
1135
|
+
*,
|
|
1136
|
+
step: dict[str, Any] | None = None,
|
|
1137
|
+
) -> dict[str, Any]:
|
|
1138
|
+
"""Create a step that applies repository protection rules.
|
|
1139
|
+
|
|
1140
|
+
Args:
|
|
1141
|
+
step: Existing step dict to update.
|
|
1142
|
+
|
|
1143
|
+
Returns:
|
|
1144
|
+
Step that runs the pyrig protect-repo command.
|
|
1145
|
+
"""
|
|
1146
|
+
from pyrig.dev.cli.subcommands import protect_repo # noqa: PLC0415
|
|
1147
|
+
|
|
1148
|
+
return cls.get_step(
|
|
1149
|
+
step_func=cls.step_protect_repository,
|
|
1150
|
+
run=get_project_mgt_run_pyrig_cli_cmd_script(protect_repo),
|
|
1151
|
+
env={"REPO_TOKEN": cls.insert_repo_token()},
|
|
1152
|
+
step=step,
|
|
1153
|
+
)
|
|
1154
|
+
|
|
1155
|
+
@classmethod
|
|
1156
|
+
def step_run_pre_commit_hooks(
|
|
1157
|
+
cls,
|
|
1158
|
+
*,
|
|
1159
|
+
step: dict[str, Any] | None = None,
|
|
1160
|
+
) -> dict[str, Any]:
|
|
1161
|
+
"""Create a step that runs pre-commit hooks.
|
|
1162
|
+
|
|
1163
|
+
Ensures code quality checks pass before commits. Also useful
|
|
1164
|
+
for ensuring git stash pop doesn't fail when there are no changes.
|
|
1165
|
+
|
|
1166
|
+
Args:
|
|
1167
|
+
step: Existing step dict to update.
|
|
1168
|
+
|
|
1169
|
+
Returns:
|
|
1170
|
+
Step that runs pre-commit on all files.
|
|
1171
|
+
"""
|
|
1172
|
+
return cls.get_step(
|
|
1173
|
+
step_func=cls.step_run_pre_commit_hooks,
|
|
1174
|
+
run=f"{PROJECT_MGT_RUN_SCRIPT} pre-commit run --all-files",
|
|
1175
|
+
step=step,
|
|
1176
|
+
)
|
|
1177
|
+
|
|
1178
|
+
@classmethod
|
|
1179
|
+
def step_commit_added_changes(
|
|
1180
|
+
cls,
|
|
1181
|
+
*,
|
|
1182
|
+
step: dict[str, Any] | None = None,
|
|
1183
|
+
) -> dict[str, Any]:
|
|
1184
|
+
"""Create a step that commits staged changes.
|
|
1185
|
+
|
|
1186
|
+
Args:
|
|
1187
|
+
step: Existing step dict to update.
|
|
1188
|
+
|
|
1189
|
+
Returns:
|
|
1190
|
+
Step that commits with [skip ci] prefix.
|
|
1191
|
+
"""
|
|
1192
|
+
return cls.get_step(
|
|
1193
|
+
step_func=cls.step_commit_added_changes,
|
|
1194
|
+
run="git commit --no-verify -m '[skip ci] CI/CD: Committing possible added changes (e.g.: pyproject.toml)'", # noqa: E501
|
|
1195
|
+
step=step,
|
|
1196
|
+
)
|
|
1197
|
+
|
|
1198
|
+
@classmethod
|
|
1199
|
+
def step_push_commits(
|
|
1200
|
+
cls,
|
|
1201
|
+
*,
|
|
1202
|
+
step: dict[str, Any] | None = None,
|
|
1203
|
+
) -> dict[str, Any]:
|
|
1204
|
+
"""Create a step that pushes commits to the remote.
|
|
1205
|
+
|
|
1206
|
+
Args:
|
|
1207
|
+
step: Existing step dict to update.
|
|
1208
|
+
|
|
1209
|
+
Returns:
|
|
1210
|
+
Step that runs git push.
|
|
1211
|
+
"""
|
|
1212
|
+
return cls.get_step(
|
|
1213
|
+
step_func=cls.step_push_commits,
|
|
1214
|
+
run="git push",
|
|
1215
|
+
step=step,
|
|
1216
|
+
)
|
|
1217
|
+
|
|
1218
|
+
@classmethod
|
|
1219
|
+
def step_create_and_push_tag(
|
|
1220
|
+
cls,
|
|
1221
|
+
*,
|
|
1222
|
+
step: dict[str, Any] | None = None,
|
|
1223
|
+
) -> dict[str, Any]:
|
|
1224
|
+
"""Create a step that creates and pushes a version tag.
|
|
1225
|
+
|
|
1226
|
+
Args:
|
|
1227
|
+
step: Existing step dict to update.
|
|
1228
|
+
|
|
1229
|
+
Returns:
|
|
1230
|
+
Step that creates a git tag and pushes it.
|
|
1231
|
+
"""
|
|
1232
|
+
return cls.get_step(
|
|
1233
|
+
step_func=cls.step_create_and_push_tag,
|
|
1234
|
+
run=f"git tag {cls.insert_version()} && git push origin {cls.insert_version()}", # noqa: E501
|
|
1235
|
+
step=step,
|
|
1236
|
+
)
|
|
1237
|
+
|
|
1238
|
+
@classmethod
|
|
1239
|
+
def step_create_folder(
|
|
1240
|
+
cls,
|
|
1241
|
+
*,
|
|
1242
|
+
folder: str,
|
|
1243
|
+
step: dict[str, Any] | None = None,
|
|
1244
|
+
) -> dict[str, Any]:
|
|
1245
|
+
"""Create a step that creates a directory.
|
|
1246
|
+
|
|
1247
|
+
Args:
|
|
1248
|
+
folder: Directory name to create.
|
|
1249
|
+
step: Existing step dict to update.
|
|
1250
|
+
|
|
1251
|
+
Returns:
|
|
1252
|
+
Step that runs mkdir (cross-platform).
|
|
1253
|
+
"""
|
|
1254
|
+
# should work on all OSs
|
|
1255
|
+
return cls.get_step(
|
|
1256
|
+
step_func=cls.step_create_folder,
|
|
1257
|
+
run=f"mkdir {folder}",
|
|
1258
|
+
step=step,
|
|
1259
|
+
)
|
|
1260
|
+
|
|
1261
|
+
@classmethod
|
|
1262
|
+
def step_create_artifacts_folder(
|
|
1263
|
+
cls,
|
|
1264
|
+
*,
|
|
1265
|
+
folder: str = Builder.ARTIFACTS_DIR_NAME,
|
|
1266
|
+
step: dict[str, Any] | None = None,
|
|
1267
|
+
) -> dict[str, Any]:
|
|
1268
|
+
"""Create a step that creates the artifacts directory.
|
|
1269
|
+
|
|
1270
|
+
Args:
|
|
1271
|
+
folder: Directory name. Defaults to ARTIFACTS_DIR_NAME.
|
|
1272
|
+
step: Existing step dict to update.
|
|
1273
|
+
|
|
1274
|
+
Returns:
|
|
1275
|
+
Step that creates the artifacts folder.
|
|
1276
|
+
"""
|
|
1277
|
+
return cls.step_create_folder(folder=folder, step=step)
|
|
1278
|
+
|
|
1279
|
+
@classmethod
|
|
1280
|
+
def step_upload_artifacts(
|
|
1281
|
+
cls,
|
|
1282
|
+
*,
|
|
1283
|
+
name: str | None = None,
|
|
1284
|
+
path: str | Path = ARTIFACTS_DIR_NAME,
|
|
1285
|
+
step: dict[str, Any] | None = None,
|
|
1286
|
+
) -> dict[str, Any]:
|
|
1287
|
+
"""Create a step that uploads build artifacts.
|
|
1288
|
+
|
|
1289
|
+
Args:
|
|
1290
|
+
name: Artifact name. Defaults to package-os format.
|
|
1291
|
+
path: Path to upload. Defaults to artifacts directory.
|
|
1292
|
+
step: Existing step dict to update.
|
|
1293
|
+
|
|
1294
|
+
Returns:
|
|
1295
|
+
Step using actions/upload-artifact.
|
|
1296
|
+
"""
|
|
1297
|
+
if name is None:
|
|
1298
|
+
name = cls.insert_artifact_name()
|
|
1299
|
+
return cls.get_step(
|
|
1300
|
+
step_func=cls.step_upload_artifacts,
|
|
1301
|
+
uses="actions/upload-artifact@main",
|
|
1302
|
+
with_={"name": name, "path": str(path)},
|
|
1303
|
+
step=step,
|
|
1304
|
+
)
|
|
1305
|
+
|
|
1306
|
+
@classmethod
|
|
1307
|
+
def step_build_artifacts(
|
|
1308
|
+
cls,
|
|
1309
|
+
*,
|
|
1310
|
+
step: dict[str, Any] | None = None,
|
|
1311
|
+
) -> dict[str, Any]:
|
|
1312
|
+
"""Create a step that builds project artifacts.
|
|
1313
|
+
|
|
1314
|
+
Args:
|
|
1315
|
+
step: Existing step dict to update.
|
|
1316
|
+
|
|
1317
|
+
Returns:
|
|
1318
|
+
Step that runs the pyrig build command.
|
|
1319
|
+
"""
|
|
1320
|
+
from pyrig.dev.cli.subcommands import build # noqa: PLC0415
|
|
1321
|
+
|
|
1322
|
+
return cls.get_step(
|
|
1323
|
+
step_func=cls.step_build_artifacts,
|
|
1324
|
+
run=get_project_mgt_run_pyrig_cli_cmd_script(build),
|
|
1325
|
+
step=step,
|
|
1326
|
+
)
|
|
1327
|
+
|
|
1328
|
+
@classmethod
|
|
1329
|
+
def step_download_artifacts(
|
|
1330
|
+
cls,
|
|
1331
|
+
*,
|
|
1332
|
+
name: str | None = None,
|
|
1333
|
+
path: str | Path = ARTIFACTS_DIR_NAME,
|
|
1334
|
+
step: dict[str, Any] | None = None,
|
|
1335
|
+
) -> dict[str, Any]:
|
|
1336
|
+
"""Create a step that downloads build artifacts.
|
|
1337
|
+
|
|
1338
|
+
Args:
|
|
1339
|
+
name: Artifact name to download. None downloads all.
|
|
1340
|
+
path: Path to download to. Defaults to artifacts directory.
|
|
1341
|
+
step: Existing step dict to update.
|
|
1342
|
+
|
|
1343
|
+
Returns:
|
|
1344
|
+
Step using actions/download-artifact.
|
|
1345
|
+
"""
|
|
1346
|
+
# omit name downloads all by default
|
|
1347
|
+
with_: dict[str, Any] = {"path": str(path)}
|
|
1348
|
+
if name is not None:
|
|
1349
|
+
with_["name"] = name
|
|
1350
|
+
with_["merge-multiple"] = "true"
|
|
1351
|
+
return cls.get_step(
|
|
1352
|
+
step_func=cls.step_download_artifacts,
|
|
1353
|
+
uses="actions/download-artifact@main",
|
|
1354
|
+
with_=with_,
|
|
1355
|
+
step=step,
|
|
1356
|
+
)
|
|
1357
|
+
|
|
1358
|
+
@classmethod
|
|
1359
|
+
def step_download_artifacts_from_workflow_run(
|
|
1360
|
+
cls,
|
|
1361
|
+
*,
|
|
1362
|
+
name: str | None = None,
|
|
1363
|
+
path: str | Path = ARTIFACTS_DIR_NAME,
|
|
1364
|
+
step: dict[str, Any] | None = None,
|
|
1365
|
+
) -> dict[str, Any]:
|
|
1366
|
+
"""Create a step that downloads artifacts from triggering workflow run.
|
|
1367
|
+
|
|
1368
|
+
Uses the github.event.workflow_run.id to download artifacts from
|
|
1369
|
+
the workflow that triggered this workflow (via workflow_run event).
|
|
1370
|
+
|
|
1371
|
+
Args:
|
|
1372
|
+
name: Artifact name to download. None downloads all.
|
|
1373
|
+
path: Path to download to. Defaults to artifacts directory.
|
|
1374
|
+
step: Existing step dict to update.
|
|
1375
|
+
|
|
1376
|
+
Returns:
|
|
1377
|
+
Step using actions/download-artifact with run-id parameter.
|
|
1378
|
+
"""
|
|
1379
|
+
with_: dict[str, Any] = {
|
|
1380
|
+
"path": str(path),
|
|
1381
|
+
"run-id": cls.insert_workflow_run_id(),
|
|
1382
|
+
"github-token": cls.insert_github_token(),
|
|
1383
|
+
}
|
|
1384
|
+
if name is not None:
|
|
1385
|
+
with_["name"] = name
|
|
1386
|
+
with_["merge-multiple"] = "true"
|
|
1387
|
+
return cls.get_step(
|
|
1388
|
+
step_func=cls.step_download_artifacts_from_workflow_run,
|
|
1389
|
+
uses="actions/download-artifact@main",
|
|
1390
|
+
with_=with_,
|
|
1391
|
+
step=step,
|
|
1392
|
+
)
|
|
1393
|
+
|
|
1394
|
+
@classmethod
|
|
1395
|
+
def step_build_changelog(
|
|
1396
|
+
cls,
|
|
1397
|
+
*,
|
|
1398
|
+
step: dict[str, Any] | None = None,
|
|
1399
|
+
) -> dict[str, Any]:
|
|
1400
|
+
"""Create a step that generates a changelog.
|
|
1401
|
+
|
|
1402
|
+
Args:
|
|
1403
|
+
step: Existing step dict to update.
|
|
1404
|
+
|
|
1405
|
+
Returns:
|
|
1406
|
+
Step using release-changelog-builder-action.
|
|
1407
|
+
"""
|
|
1408
|
+
return cls.get_step(
|
|
1409
|
+
step_func=cls.step_build_changelog,
|
|
1410
|
+
uses="mikepenz/release-changelog-builder-action@develop",
|
|
1411
|
+
with_={"token": cls.insert_github_token()},
|
|
1412
|
+
step=step,
|
|
1413
|
+
)
|
|
1414
|
+
|
|
1415
|
+
@classmethod
|
|
1416
|
+
def step_extract_version(
|
|
1417
|
+
cls,
|
|
1418
|
+
*,
|
|
1419
|
+
step: dict[str, Any] | None = None,
|
|
1420
|
+
) -> dict[str, Any]:
|
|
1421
|
+
"""Create a step that extracts the version to GITHUB_OUTPUT.
|
|
1422
|
+
|
|
1423
|
+
Args:
|
|
1424
|
+
step: Existing step dict to update.
|
|
1425
|
+
|
|
1426
|
+
Returns:
|
|
1427
|
+
Step that outputs the version for later steps.
|
|
1428
|
+
"""
|
|
1429
|
+
return cls.get_step(
|
|
1430
|
+
step_func=cls.step_extract_version,
|
|
1431
|
+
run=f'echo "version={cls.insert_version()}" >> $GITHUB_OUTPUT',
|
|
1432
|
+
step=step,
|
|
1433
|
+
)
|
|
1434
|
+
|
|
1435
|
+
@classmethod
|
|
1436
|
+
def step_create_release(
|
|
1437
|
+
cls,
|
|
1438
|
+
*,
|
|
1439
|
+
step: dict[str, Any] | None = None,
|
|
1440
|
+
artifacts_pattern: str = ARTIFACTS_PATTERN,
|
|
1441
|
+
) -> dict[str, Any]:
|
|
1442
|
+
"""Create a step that creates a GitHub release.
|
|
1443
|
+
|
|
1444
|
+
Args:
|
|
1445
|
+
step: Existing step dict to update.
|
|
1446
|
+
artifacts_pattern: Glob pattern for release artifacts.
|
|
1447
|
+
|
|
1448
|
+
Returns:
|
|
1449
|
+
Step using ncipollo/release-action.
|
|
1450
|
+
"""
|
|
1451
|
+
version = cls.insert_version_from_extract_version_step()
|
|
1452
|
+
return cls.get_step(
|
|
1453
|
+
step_func=cls.step_create_release,
|
|
1454
|
+
uses="ncipollo/release-action@main",
|
|
1455
|
+
with_={
|
|
1456
|
+
"tag": version,
|
|
1457
|
+
"name": f"{cls.insert_repository_name()} {version}",
|
|
1458
|
+
"body": cls.insert_changelog(),
|
|
1459
|
+
"artifacts": artifacts_pattern,
|
|
1460
|
+
},
|
|
1461
|
+
step=step,
|
|
1462
|
+
)
|
|
1463
|
+
|
|
1464
|
+
# Insertions
|
|
1465
|
+
# ----------------------------------------------------------------------------
|
|
1466
|
+
@classmethod
|
|
1467
|
+
def insert_repo_token(cls) -> str:
|
|
1468
|
+
"""Get the GitHub expression for REPO_TOKEN secret.
|
|
1469
|
+
|
|
1470
|
+
Returns:
|
|
1471
|
+
GitHub Actions expression for secrets.REPO_TOKEN.
|
|
1472
|
+
"""
|
|
1473
|
+
return "${{ secrets.REPO_TOKEN }}"
|
|
1474
|
+
|
|
1475
|
+
@classmethod
|
|
1476
|
+
def insert_pypi_token(cls) -> str:
|
|
1477
|
+
"""Get the GitHub expression for PYPI_TOKEN secret.
|
|
1478
|
+
|
|
1479
|
+
Returns:
|
|
1480
|
+
GitHub Actions expression for secrets.PYPI_TOKEN.
|
|
1481
|
+
"""
|
|
1482
|
+
return "${{ secrets.PYPI_TOKEN }}"
|
|
1483
|
+
|
|
1484
|
+
@classmethod
|
|
1485
|
+
def insert_version(cls) -> str:
|
|
1486
|
+
"""Get a shell expression for the current version.
|
|
1487
|
+
|
|
1488
|
+
Returns:
|
|
1489
|
+
Shell command that outputs the version with v prefix.
|
|
1490
|
+
"""
|
|
1491
|
+
return f"v$({PROJECT_MGT} version --short)"
|
|
1492
|
+
|
|
1493
|
+
@classmethod
|
|
1494
|
+
def insert_version_from_extract_version_step(cls) -> str:
|
|
1495
|
+
"""Get the GitHub expression for version from extract step.
|
|
1496
|
+
|
|
1497
|
+
Returns:
|
|
1498
|
+
GitHub Actions expression referencing the extract_version output.
|
|
1499
|
+
"""
|
|
1500
|
+
# make dynamic with cls.make_id_from_func(cls.step_extract_version)
|
|
1501
|
+
return (
|
|
1502
|
+
"${{ "
|
|
1503
|
+
f"steps.{cls.make_id_from_func(cls.step_extract_version)}.outputs.version"
|
|
1504
|
+
" }}"
|
|
1505
|
+
)
|
|
1506
|
+
|
|
1507
|
+
@classmethod
|
|
1508
|
+
def insert_changelog(cls) -> str:
|
|
1509
|
+
"""Get the GitHub expression for changelog from build step.
|
|
1510
|
+
|
|
1511
|
+
Returns:
|
|
1512
|
+
GitHub Actions expression referencing the build_changelog output.
|
|
1513
|
+
"""
|
|
1514
|
+
return (
|
|
1515
|
+
"${{ "
|
|
1516
|
+
f"steps.{cls.make_id_from_func(cls.step_build_changelog)}.outputs.changelog"
|
|
1517
|
+
" }}"
|
|
1518
|
+
)
|
|
1519
|
+
|
|
1520
|
+
@classmethod
|
|
1521
|
+
def insert_github_token(cls) -> str:
|
|
1522
|
+
"""Get the GitHub expression for GITHUB_TOKEN.
|
|
1523
|
+
|
|
1524
|
+
Returns:
|
|
1525
|
+
GitHub Actions expression for secrets.GITHUB_TOKEN.
|
|
1526
|
+
"""
|
|
1527
|
+
return "${{ secrets.GITHUB_TOKEN }}"
|
|
1528
|
+
|
|
1529
|
+
@classmethod
|
|
1530
|
+
def insert_codecov_token(cls) -> str:
|
|
1531
|
+
"""Get the GitHub expression for CODECOV_TOKEN.
|
|
1532
|
+
|
|
1533
|
+
Returns:
|
|
1534
|
+
GitHub Actions expression for secrets.CODECOV_TOKEN.
|
|
1535
|
+
"""
|
|
1536
|
+
return "${{ secrets.CODECOV_TOKEN }}"
|
|
1537
|
+
|
|
1538
|
+
@classmethod
|
|
1539
|
+
def insert_repository_name(cls) -> str:
|
|
1540
|
+
"""Get the GitHub expression for repository name.
|
|
1541
|
+
|
|
1542
|
+
Returns:
|
|
1543
|
+
GitHub Actions expression for the repository name.
|
|
1544
|
+
"""
|
|
1545
|
+
return "${{ github.event.repository.name }}"
|
|
1546
|
+
|
|
1547
|
+
@classmethod
|
|
1548
|
+
def insert_ref_name(cls) -> str:
|
|
1549
|
+
"""Get the GitHub expression for the ref name.
|
|
1550
|
+
|
|
1551
|
+
Returns:
|
|
1552
|
+
GitHub Actions expression for github.ref_name.
|
|
1553
|
+
"""
|
|
1554
|
+
return "${{ github.ref_name }}"
|
|
1555
|
+
|
|
1556
|
+
@classmethod
|
|
1557
|
+
def insert_repository_owner(cls) -> str:
|
|
1558
|
+
"""Get the GitHub expression for repository owner.
|
|
1559
|
+
|
|
1560
|
+
Returns:
|
|
1561
|
+
GitHub Actions expression for github.repository_owner.
|
|
1562
|
+
"""
|
|
1563
|
+
return "${{ github.repository_owner }}"
|
|
1564
|
+
|
|
1565
|
+
@classmethod
|
|
1566
|
+
def insert_workflow_run_id(cls) -> str:
|
|
1567
|
+
"""Get the GitHub expression for triggering workflow run ID.
|
|
1568
|
+
|
|
1569
|
+
Used when downloading artifacts from the workflow that triggered
|
|
1570
|
+
this workflow via workflow_run event.
|
|
1571
|
+
|
|
1572
|
+
Returns:
|
|
1573
|
+
GitHub Actions expression for github.event.workflow_run.id.
|
|
1574
|
+
"""
|
|
1575
|
+
return "${{ github.event.workflow_run.id }}"
|
|
1576
|
+
|
|
1577
|
+
@classmethod
|
|
1578
|
+
def insert_os(cls) -> str:
|
|
1579
|
+
"""Get the GitHub expression for runner OS.
|
|
1580
|
+
|
|
1581
|
+
Returns:
|
|
1582
|
+
GitHub Actions expression for runner.os.
|
|
1583
|
+
"""
|
|
1584
|
+
return "${{ runner.os }}"
|
|
1585
|
+
|
|
1586
|
+
@classmethod
|
|
1587
|
+
def insert_matrix_os(cls) -> str:
|
|
1588
|
+
"""Get the GitHub expression for matrix OS value.
|
|
1589
|
+
|
|
1590
|
+
Returns:
|
|
1591
|
+
GitHub Actions expression for matrix.os.
|
|
1592
|
+
"""
|
|
1593
|
+
return "${{ matrix.os }}"
|
|
1594
|
+
|
|
1595
|
+
@classmethod
|
|
1596
|
+
def insert_matrix_python_version(cls) -> str:
|
|
1597
|
+
"""Get the GitHub expression for matrix Python version.
|
|
1598
|
+
|
|
1599
|
+
Returns:
|
|
1600
|
+
GitHub Actions expression for matrix.python-version.
|
|
1601
|
+
"""
|
|
1602
|
+
return "${{ matrix.python-version }}"
|
|
1603
|
+
|
|
1604
|
+
@classmethod
|
|
1605
|
+
def insert_artifact_name(cls) -> str:
|
|
1606
|
+
"""Generate an artifact name based on package and OS.
|
|
1607
|
+
|
|
1608
|
+
Returns:
|
|
1609
|
+
Artifact name in format: package-os.
|
|
1610
|
+
"""
|
|
1611
|
+
return f"{get_src_package().__name__}-{cls.insert_os()}"
|
|
1612
|
+
|
|
1613
|
+
# ifs
|
|
1614
|
+
# ----------------------------------------------------------------------------
|
|
1615
|
+
@classmethod
|
|
1616
|
+
def combined_if(cls, *conditions: str) -> str:
|
|
1617
|
+
"""Combine multiple conditions with logical AND.
|
|
1618
|
+
|
|
1619
|
+
Args:
|
|
1620
|
+
*conditions: Individual condition expressions.
|
|
1621
|
+
|
|
1622
|
+
Returns:
|
|
1623
|
+
Combined condition expression.
|
|
1624
|
+
"""
|
|
1625
|
+
bare_conditions = [
|
|
1626
|
+
condition.strip().removeprefix("${{").removesuffix("}}").strip()
|
|
1627
|
+
for condition in conditions
|
|
1628
|
+
]
|
|
1629
|
+
return cls.if_condition(" && ".join(bare_conditions))
|
|
1630
|
+
|
|
1631
|
+
@classmethod
|
|
1632
|
+
def if_condition(cls, condition: str) -> str:
|
|
1633
|
+
"""Wrap a condition in GitHub Actions expression syntax.
|
|
1634
|
+
|
|
1635
|
+
Args:
|
|
1636
|
+
condition: Condition expression to wrap.
|
|
1637
|
+
|
|
1638
|
+
Returns:
|
|
1639
|
+
GitHub Actions expression for the condition.
|
|
1640
|
+
"""
|
|
1641
|
+
return f"${{{{ {condition} }}}}"
|
|
1642
|
+
|
|
1643
|
+
@classmethod
|
|
1644
|
+
def if_matrix_is_not_os(cls, os: str) -> str:
|
|
1645
|
+
"""Create a condition for not matching a specific OS.
|
|
1646
|
+
|
|
1647
|
+
Args:
|
|
1648
|
+
os: OS runner label to not match.
|
|
1649
|
+
|
|
1650
|
+
Returns:
|
|
1651
|
+
Condition expression for matrix.os comparison.
|
|
1652
|
+
"""
|
|
1653
|
+
return cls.if_condition(f"matrix.os != '{os}'")
|
|
1654
|
+
|
|
1655
|
+
@classmethod
|
|
1656
|
+
def if_workflow_run_is_success(cls) -> str:
|
|
1657
|
+
"""Create a condition for successful workflow run.
|
|
1658
|
+
|
|
1659
|
+
Returns:
|
|
1660
|
+
GitHub Actions expression checking workflow_run conclusion.
|
|
1661
|
+
"""
|
|
1662
|
+
return cls.if_condition("github.event.workflow_run.conclusion == 'success'")
|