Passed
Pull Request — master (#12)
by Konstantinos
01:20
created

visualize-ga-workflow.extract_job_dependencies()   B

Complexity

Conditions 6

Size

Total Lines 25
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 6
eloc 15
nop 1
dl 0
loc 25
rs 8.6666
c 0
b 0
f 0
1
#!/usr/bin/env python3
2
3
import argparse
4
import sys
5
import typing as t
6
from pathlib import Path
7
8
import yaml
9
10
# TYPES of Data as Read from Yaml Config
11
12
## job names are yaml keys
13
JobName = t.NewType('JobName', str)
14
15
## each job 'needs' key can be:
16
#   - missing -> python None
17
#   - a string value expected to be a job name -> python JobName
18
#   - a list value, with jobs names as items -> python List[JobName]
19
20
# OPT 2
21
# Define a new type for JobNeeds
22
# JobNeedsType = t.Union[JobName, t.List[JobName], None]
23
# JobNeeds = t.NewType('JobNeeds', JobNeedsType)
24
# # OPT 1
25
JobNeeds = t.Union[JobName, t.List[JobName], None]
26
27
28
ParsedYaml = t.Dict[str, t.Any]
29
30
# TYPES of Data Model
31
JobsNeedsValue = t.List[JobName]
32
33
34
# Parse the GitHub Actions YAML file
35
def parse_actions_config(filename: t.Union[str, Path]) -> t.Union[ParsedYaml, None]:
36
    with open(filename, 'r') as stream:
37
        try:
38
            return yaml.safe_load(stream)
39
        except yaml.YAMLError as exc:
40
            print(exc)
41
            return None
42
43
44
# Extract job names and their 'needs' sections
45
def extract_job_dependencies(config: ParsedYaml) -> t.Dict[str, JobsNeedsValue]:
46
    """Understand DAG of all Jobs"""
47
    # DAG representation
48
49
    # mapping of job names to their dependencies (previous steps in the dependency DAG)
50
    job_dependencies: t.Dict[str, JobsNeedsValue] = {}
51
52
    if not 'jobs' in config:
53
        print(f"[WARNGING] No 'jobs' section found in config file")
54
55
    else:
56
        for job_name, job_config in config['jobs'].items():
57
            needs: JobNeeds = job_config.get('needs')
58
59
            current_job_needs_value: JobsNeedsValue = []
60
            if isinstance(needs, str):  # single dependency
61
                current_job_needs_value = [needs]
62
            elif isinstance(needs, list):  # multiple dependencies
63
                current_job_needs_value = needs
64
            elif needs is not None:
65
                print(f"[WARNING] Unexpected 'needs' value: {needs}")
66
67
            job_dependencies[job_name] = current_job_needs_value
68
69
    return job_dependencies
70
71
72
# Generate Mermaid markdown from job dependencies
73
def generate_mermaid_markdown(job_dependencies: t.Dict[str, t.List[str]]) -> str:
74
    mermaid_code = 'graph LR;\n'
75
    for job_name, needs in job_dependencies.items():
76
        for need in needs:
77
            mermaid_code += f'  {need} --> {job_name}\n'
78
    return mermaid_code
79
80
81
def markdown_mermaid_from_yaml(filename: t.Union[str, Path]) -> str:
82
    config: ParsedYaml = parse_actions_config(filename)
83
    if config is None:
84
        print(f"[ERROR] Could not parse YAML file: {filename}")
85
        sys.exit(1)
86
    job_dependencies: t.Dict[str, JobsNeedsValue] = extract_job_dependencies(config)
87
    mermaid_code: str = generate_mermaid_markdown(job_dependencies)
88
89
    markdown: str = (
90
        # "## CI/CD Pipeline\n\n"
91
        # f"**CI Config File: {filename}**\n\n"
92
        f"```mermaid\n{mermaid_code}```\n"
93
    )
94
95
    return markdown
96
97
98
#### MAIN ####
99
def main():
100
    args = arg_parse()
101
102
    if args.input == "default-path":
103
        ci_config = Path.cwd() / ".github/workflows/test.yaml"
104
        # ci_config_file = Path(__file__).parent / "ci-config.yml"
105
        # input_data = sys.stdin.read()
106
    else:
107
        ci_config = Path(args.input)
108
109
    md: str = markdown_mermaid_from_yaml(
110
        ci_config,
111
    )
112
113
    if args.output:
114
        # Handle the case of writing to an output file
115
        output_file = Path(args.output)
116
        output_file.write_text(md)
117
    else:
118
        # Handle the case of streaming output to stdout
119
        sys.stdout.write(md)
120
        # print
121
122
123
# CLI
124
125
126
def arg_parse():
127
    parser = argparse.ArgumentParser(
128
        description="Command-line tool to handle input and output options."
129
    )
130
    parser.add_argument(
131
        "input",
132
        nargs="?",
133
        default="default-path",
134
        help="Input file path (default: 'default-path')",
135
    )
136
    parser.add_argument("-o", "--output", help="Output file path")
137
138
    args = parser.parse_args()
139
    return args
140
141
142
if __name__ == '__main__':
143
    main()
144