3
from __future__ import annotations
8
from pathlib import Path
9
from typing import Any, Dict
10
from typing_extensions import TypedDict # Python 3.11+
18
class Script(TypedDict):
23
def extract(step: Step) -> Script | None:
26
# https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions#using-a-specific-shell
27
shell = step.get("shell", "bash")
37
is_gh_script = step.get("uses", "").startswith("actions/github-script@")
38
gh_script = step.get("with", {}).get("script")
40
if run is not None and extension is not None:
42
"bash": f"#!/usr/bin/env bash\nset -eo pipefail\n{run}",
43
"sh": f"#!/usr/bin/env sh\nset -e\n{run}",
45
return {"extension": extension, "script": script}
46
elif is_gh_script and gh_script is not None:
47
return {"extension": ".js", "script": gh_script}
53
parser = argparse.ArgumentParser()
54
parser.add_argument("--out", required=True)
55
args = parser.parse_args()
59
sys.exit(f"{out} already exists; aborting to avoid overwriting")
61
gha_expressions_found = False
63
for p in Path(".github/workflows").iterdir():
64
with open(p, "rb") as f:
65
workflow = yaml.safe_load(f)
67
for job_name, job in workflow["jobs"].items():
68
job_dir = out / p / job_name
69
if "steps" not in job:
72
index_chars = len(str(len(steps) - 1))
73
for i, step in enumerate(steps, start=1):
74
extracted = extract(step)
76
script = extracted["script"]
77
step_name = step.get("name", "")
79
gha_expressions_found = True
81
f"{p} job `{job_name}` step {i}: {step_name}",
85
job_dir.mkdir(parents=True, exist_ok=True)
92
extension = extracted["extension"]
93
filename = f"{i:0{index_chars}}{sanitized}{extension}"
94
(job_dir / filename).write_text(script)
96
if gha_expressions_found:
98
"Each of the above scripts contains a GitHub Actions "
99
"${{ <expression> }} which must be replaced with an `env` variable"
100
" for security reasons."
104
if __name__ == "__main__":