Repository navigation
Expand file tree
/
Copy pathworkflow_steps.py
More file actions
93 lines (78 loc) · 3.43 KB
/
Copy pathworkflow_steps.py
File metadata and controls
93 lines (78 loc) · 3.43 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
"""Read one named step out of a workflow or composite action, bounded to that step.
The readers work by indentation. A step is sliced out before any key is looked for in it. A step missing a key therefore reads as missing
rather than as the next step's key.
"""
from __future__ import annotations
import re
from pathlib import Path
def step_script_text(text: str, name: str) -> str:
"""The `run` block of the step called `name` in `text`, dedented as a runner reads it.
The `run:` key is looked for only at the step's own key level. A step without one therefore fails
here rather than reading the next step's script.
"""
step = yaml_step(text, name)
key_level = len(step) - len(step.lstrip())
match = re.search(rf"(?m)^ {{{key_level}}}run: \|[-+]?\n", step)
if match is None:
raise AssertionError(f"no run block in the step named {name!r}")
indent = " " * (key_level + 2)
script_lines = []
for line in step[match.end() :].splitlines():
if line.startswith(indent):
script_lines.append(line.removeprefix(indent))
elif not line:
script_lines.append(line)
else:
break
return "\n".join(script_lines)
def step_script(path: Path, name: str) -> str:
"""The `run` block of the step called `name` in the workflow or action at `path`."""
return step_script_text(path.read_text(encoding="utf-8"), name)
def yaml_step(text: str, name: str) -> str:
"""The one sequence item named `name`, its dash blanked so its keys read as a mapping.
Read by indentation. The item ends at the first line at or left of its dash, so a key under
a sibling step is never this step's.
"""
lines = text.splitlines()
starts = [
i for i, line in enumerate(lines) if re.fullmatch(rf"( *)- name: {re.escape(name)}", line)
]
if len(starts) != 1:
raise AssertionError(f"{len(starts)} steps named {name!r}")
first = lines[starts[0]]
dash = len(first) - len(first.lstrip())
item = [first[:dash] + " " + first[dash + 2 :]]
for line in lines[starts[0] + 1 :]:
if line.strip() and len(line) - len(line.lstrip()) <= dash:
break
item.append(line)
return "\n".join(item)
def yaml_value(text: str, *keys: str) -> str | None:
"""The text after the last key's colon at the nested mapping path `keys`, or None where absent.
The text is raw rather than a parsed value, so quotes and a trailing comment are kept.
Each key is looked for only among its parent's direct children, the shallowest lines under it.
A comment line is no key at all. A commented-out or relocated entry therefore reads as absent.
"""
lines = [
line for line in text.splitlines() if line.strip() and not line.lstrip().startswith("#")
]
value = None
for key in keys:
if not lines:
return None
level = min(len(line) - len(line.lstrip()) for line in lines)
found = None
for i, line in enumerate(lines):
if len(line) - len(line.lstrip()) == level and line[level:].startswith(f"{key}:"):
found = i
break
if found is None:
return None
value = lines[found][level + len(key) + 1 :].strip()
children = []
for line in lines[found + 1 :]:
if len(line) - len(line.lstrip()) <= level:
break
children.append(line)
lines = children
return value