#!/usr/bin/env python3 import argparse import re import sys from pathlib import Path from typing import Any, Dict, Optional import yaml from typing_extensions import TypedDict Step = Dict[str, Any] class Script(TypedDict): extension: str script: str def extract(step: Step) -> Optional[Script]: run = step.get('run') # https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions#using-a-specific-shell shell = step.get('shell', 'bash') extension = { 'bash': '.sh', 'pwsh': '.ps1', 'python': '.py', 'sh': '.sh', 'cmd': '.cmd', 'powershell': '.ps1', }.get(shell) is_gh_script = step.get('uses', '').startswith('actions/github-script@') gh_script = step.get('with', {}).get('script') if run is not None and extension is not None: script = { 'bash': f'#!/usr/bin/env bash\nset -eo pipefail\n{run}', 'sh': f'#!/usr/bin/env sh\nset -e\n{run}', }.get(shell, run) return {'extension': extension, 'script': script} elif is_gh_script and gh_script is not None: return {'extension': '.js', 'script': gh_script} else: return None def main() -> None: parser = argparse.ArgumentParser() parser.add_argument('--out', required=True) args = parser.parse_args() out = Path(args.out) if out.exists(): sys.exit(f'{out} already exists; aborting to avoid overwriting') gha_expressions_found = False for p in Path('.github/workflows').iterdir(): with open(p) as f: workflow = yaml.safe_load(f) for job_name, job in workflow['jobs'].items(): job_dir = out / p / job_name steps = job['steps'] index_chars = len(str(len(steps) - 1)) for i, step in enumerate(steps, start=1): extracted = extract(step) if extracted: script = extracted['script'] step_name = step.get('name', '') if '${{' in script: gha_expressions_found = True print( f'{p} job `{job_name}` step {i}: {step_name}', file=sys.stderr ) job_dir.mkdir(parents=True, exist_ok=True) sanitized = re.sub( '[^a-zA-Z_]+', '_', f'_{step_name}', ).rstrip('_') extension = extracted['extension'] filename = f'{i:0{index_chars}}{sanitized}{extension}' (job_dir / filename).write_text(script) if gha_expressions_found: sys.exit( 'Each of the above scripts contains a GitHub Actions ' '${{ }} which must be replaced with an `env` variable' ' for security reasons.' ) if __name__ == '__main__': main()