1*da0073e9SAndroid Build Coastguard Worker#!/usr/bin/env python3 2*da0073e9SAndroid Build Coastguard Worker 3*da0073e9SAndroid Build Coastguard Workerfrom __future__ import annotations 4*da0073e9SAndroid Build Coastguard Worker 5*da0073e9SAndroid Build Coastguard Workerimport argparse 6*da0073e9SAndroid Build Coastguard Workerimport re 7*da0073e9SAndroid Build Coastguard Workerimport sys 8*da0073e9SAndroid Build Coastguard Workerfrom pathlib import Path 9*da0073e9SAndroid Build Coastguard Workerfrom typing import Any, Dict 10*da0073e9SAndroid Build Coastguard Workerfrom typing_extensions import TypedDict # Python 3.11+ 11*da0073e9SAndroid Build Coastguard Worker 12*da0073e9SAndroid Build Coastguard Workerimport yaml 13*da0073e9SAndroid Build Coastguard Worker 14*da0073e9SAndroid Build Coastguard Worker 15*da0073e9SAndroid Build Coastguard WorkerStep = Dict[str, Any] 16*da0073e9SAndroid Build Coastguard Worker 17*da0073e9SAndroid Build Coastguard Worker 18*da0073e9SAndroid Build Coastguard Workerclass Script(TypedDict): 19*da0073e9SAndroid Build Coastguard Worker extension: str 20*da0073e9SAndroid Build Coastguard Worker script: str 21*da0073e9SAndroid Build Coastguard Worker 22*da0073e9SAndroid Build Coastguard Worker 23*da0073e9SAndroid Build Coastguard Workerdef extract(step: Step) -> Script | None: 24*da0073e9SAndroid Build Coastguard Worker run = step.get("run") 25*da0073e9SAndroid Build Coastguard Worker 26*da0073e9SAndroid Build Coastguard Worker # https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions#using-a-specific-shell 27*da0073e9SAndroid Build Coastguard Worker shell = step.get("shell", "bash") 28*da0073e9SAndroid Build Coastguard Worker extension = { 29*da0073e9SAndroid Build Coastguard Worker "bash": ".sh", 30*da0073e9SAndroid Build Coastguard Worker "pwsh": ".ps1", 31*da0073e9SAndroid Build Coastguard Worker "python": ".py", 32*da0073e9SAndroid Build Coastguard Worker "sh": ".sh", 33*da0073e9SAndroid Build Coastguard Worker "cmd": ".cmd", 34*da0073e9SAndroid Build Coastguard Worker "powershell": ".ps1", 35*da0073e9SAndroid Build Coastguard Worker }.get(shell) 36*da0073e9SAndroid Build Coastguard Worker 37*da0073e9SAndroid Build Coastguard Worker is_gh_script = step.get("uses", "").startswith("actions/github-script@") 38*da0073e9SAndroid Build Coastguard Worker gh_script = step.get("with", {}).get("script") 39*da0073e9SAndroid Build Coastguard Worker 40*da0073e9SAndroid Build Coastguard Worker if run is not None and extension is not None: 41*da0073e9SAndroid Build Coastguard Worker script = { 42*da0073e9SAndroid Build Coastguard Worker "bash": f"#!/usr/bin/env bash\nset -eo pipefail\n{run}", 43*da0073e9SAndroid Build Coastguard Worker "sh": f"#!/usr/bin/env sh\nset -e\n{run}", 44*da0073e9SAndroid Build Coastguard Worker }.get(shell, run) 45*da0073e9SAndroid Build Coastguard Worker return {"extension": extension, "script": script} 46*da0073e9SAndroid Build Coastguard Worker elif is_gh_script and gh_script is not None: 47*da0073e9SAndroid Build Coastguard Worker return {"extension": ".js", "script": gh_script} 48*da0073e9SAndroid Build Coastguard Worker else: 49*da0073e9SAndroid Build Coastguard Worker return None 50*da0073e9SAndroid Build Coastguard Worker 51*da0073e9SAndroid Build Coastguard Worker 52*da0073e9SAndroid Build Coastguard Workerdef main() -> None: 53*da0073e9SAndroid Build Coastguard Worker parser = argparse.ArgumentParser() 54*da0073e9SAndroid Build Coastguard Worker parser.add_argument("--out", required=True) 55*da0073e9SAndroid Build Coastguard Worker args = parser.parse_args() 56*da0073e9SAndroid Build Coastguard Worker 57*da0073e9SAndroid Build Coastguard Worker out = Path(args.out) 58*da0073e9SAndroid Build Coastguard Worker if out.exists(): 59*da0073e9SAndroid Build Coastguard Worker sys.exit(f"{out} already exists; aborting to avoid overwriting") 60*da0073e9SAndroid Build Coastguard Worker 61*da0073e9SAndroid Build Coastguard Worker gha_expressions_found = False 62*da0073e9SAndroid Build Coastguard Worker 63*da0073e9SAndroid Build Coastguard Worker for p in Path(".github/workflows").iterdir(): 64*da0073e9SAndroid Build Coastguard Worker with open(p, "rb") as f: 65*da0073e9SAndroid Build Coastguard Worker workflow = yaml.safe_load(f) 66*da0073e9SAndroid Build Coastguard Worker 67*da0073e9SAndroid Build Coastguard Worker for job_name, job in workflow["jobs"].items(): 68*da0073e9SAndroid Build Coastguard Worker job_dir = out / p / job_name 69*da0073e9SAndroid Build Coastguard Worker if "steps" not in job: 70*da0073e9SAndroid Build Coastguard Worker continue 71*da0073e9SAndroid Build Coastguard Worker steps = job["steps"] 72*da0073e9SAndroid Build Coastguard Worker index_chars = len(str(len(steps) - 1)) 73*da0073e9SAndroid Build Coastguard Worker for i, step in enumerate(steps, start=1): 74*da0073e9SAndroid Build Coastguard Worker extracted = extract(step) 75*da0073e9SAndroid Build Coastguard Worker if extracted: 76*da0073e9SAndroid Build Coastguard Worker script = extracted["script"] 77*da0073e9SAndroid Build Coastguard Worker step_name = step.get("name", "") 78*da0073e9SAndroid Build Coastguard Worker if "${{" in script: 79*da0073e9SAndroid Build Coastguard Worker gha_expressions_found = True 80*da0073e9SAndroid Build Coastguard Worker print( 81*da0073e9SAndroid Build Coastguard Worker f"{p} job `{job_name}` step {i}: {step_name}", 82*da0073e9SAndroid Build Coastguard Worker file=sys.stderr, 83*da0073e9SAndroid Build Coastguard Worker ) 84*da0073e9SAndroid Build Coastguard Worker 85*da0073e9SAndroid Build Coastguard Worker job_dir.mkdir(parents=True, exist_ok=True) 86*da0073e9SAndroid Build Coastguard Worker 87*da0073e9SAndroid Build Coastguard Worker sanitized = re.sub( 88*da0073e9SAndroid Build Coastguard Worker "[^a-zA-Z_]+", 89*da0073e9SAndroid Build Coastguard Worker "_", 90*da0073e9SAndroid Build Coastguard Worker f"_{step_name}", 91*da0073e9SAndroid Build Coastguard Worker ).rstrip("_") 92*da0073e9SAndroid Build Coastguard Worker extension = extracted["extension"] 93*da0073e9SAndroid Build Coastguard Worker filename = f"{i:0{index_chars}}{sanitized}{extension}" 94*da0073e9SAndroid Build Coastguard Worker (job_dir / filename).write_text(script) 95*da0073e9SAndroid Build Coastguard Worker 96*da0073e9SAndroid Build Coastguard Worker if gha_expressions_found: 97*da0073e9SAndroid Build Coastguard Worker sys.exit( 98*da0073e9SAndroid Build Coastguard Worker "Each of the above scripts contains a GitHub Actions " 99*da0073e9SAndroid Build Coastguard Worker "${{ <expression> }} which must be replaced with an `env` variable" 100*da0073e9SAndroid Build Coastguard Worker " for security reasons." 101*da0073e9SAndroid Build Coastguard Worker ) 102*da0073e9SAndroid Build Coastguard Worker 103*da0073e9SAndroid Build Coastguard Worker 104*da0073e9SAndroid Build Coastguard Workerif __name__ == "__main__": 105*da0073e9SAndroid Build Coastguard Worker main() 106