Skip to content

Merge pull request #19310 from kenrowland/HPCC-32968 #9

Merge pull request #19310 from kenrowland/HPCC-32968

Merge pull request #19310 from kenrowland/HPCC-32968 #9

name: Generate Graphs for GitHub Action workflows
on:
push:
branches:
- master
paths:
- .github/workflows/*.yml
- .github/workflows/*.yaml
# pull_request: #trigger on pull_request is just for testing purpose
workflow_dispatch:
jobs:
main:
runs-on: ubuntu-22.04
steps:
- name: Checkout Repository
uses: actions/checkout@v4
- uses: "actions/setup-python@v5"
with:
python-version: "3.8"
- name: generate Graph
shell: python
run: |
import glob
class YamlFileObject():
def __init__(self, filename):
self.filename = filename
self.data = None
self.read()
def read(self):
with open(self.filename, 'r') as file:
self.data = file.readlines()
def getFileContent(self):
return iter(self.data)
def getNextLine(file):
try:
line = next(file)
while not line.strip("\n ") or line.strip("\n ").startswith('#'):
line = next(file)
except StopIteration :
return "EOF"
return line.strip('\n')
def getColumnNum(line): #gets the Column number of first word
i = 0
while line[i] == ' ':
i += 1
columnNum = i
return columnNum
def getWorkflowName(baseDir,file):
workflowDetails[file] = {'name':file.split('.')[0]}
# name: is not mandatory, so by default assign the name of the yaml file as the workflow name by removing the .yml/.yaml extension
yamlFile = YamlFileObject(baseDir+file)
fileContent = yamlFile.getFileContent()
line = getNextLine(fileContent)
while line != "EOF":
if line.startswith("name:"): # if 'name:' is present in the yaml file then update it
nameLine = line.split(":")[1]
name = nameLine.strip(" ")
workflowDetails[file] = {'name':name}
getWorkflowTriggers(yamlFile,file)
getWorkflowJobs(yamlFile,file)
break
line = getNextLine(fileContent)
def getWorkflowJobs(yamlFile,file):
fileContent = yamlFile.getFileContent()
ColumnNumOfJob = -1
workflowDetails[file]['jobs'] = {}
line = getNextLine(fileContent)
while line != "EOF" :
if line.startswith("jobs:"):
line = getNextLine(fileContent)
ColumnNumOfJob = getColumnNum(line)
break
line = getNextLine(fileContent)
while ColumnNumOfJob != -1 and line != "EOF" :
if line[ColumnNumOfJob] != ' ' :
jobName = line.strip(" ") #remove trailing spaces
jobName = jobName[:-1] #remove ':' at the end
workflowDetails[file]['jobs'][jobName] = {'name':jobName}
getJobDetails(yamlFile,file,jobName,ColumnNumOfJob)
line = getNextLine(fileContent)
def getJobDetails(yamlFile,file,jobName,columnNum):
fileContent = yamlFile.getFileContent()
for line in fileContent:
if line.strip("\n ") == jobName+':' :
line = getNextLine(fileContent)
subColumnNum = getColumnNum(line)
workflowDetails[file]['jobs'][jobName] = {'name':jobName,'uses':"",'needs':"",'inputs':[] }
job = workflowDetails[file]['jobs'][jobName]
while len(line) > columnNum and line[columnNum] == ' ':
if line.find('name:') != -1 and line[subColumnNum:subColumnNum+4]=='name':
name = line.split(':')[1]
name = name.strip()
job['name'] = name
elif line.find('uses:') != -1 :
uses = line.split(':')[1]
uses = uses.strip(' ')
job['uses'] = uses
elif line.find('needs:') != -1 :
needs = line.split(':')[1]
needs = needs.strip(" ")
job['needs'] = needs
elif line.find('with:') != -1 :
columnNumOfWith = line.find('with:')
line = getNextLine(fileContent)
while len(line) > columnNumOfWith and line[columnNumOfWith] == " " :
input = line.strip(" ")
if len(input) > 30:
input = input[:30]+'...'
job['inputs'].append(input)
line = getNextLine(fileContent)
if(line.find('steps:') != -1 or line == "EOF" or line[columnNum] != " "):
break
line = getNextLine(fileContent)
def getWorkflowTriggers(yamlFile,file):
workflowDetails[file]['trigger'] = []
fileContent = yamlFile.getFileContent()
for line in fileContent:
if line.strip("\n ").startswith("on:"):
line = line.split(':')[1]
line = line.strip("\n ")
if line and not line.startswith('#'): # This case handles triggers which are declared on the same line of "on:" key example: on: [push, pull_request]
line = line.replace("[","")
line = line.replace("]","")
line = line.replace(" ","")
line = line.replace('#',',#')
for trigger in line.split(','):
if not trigger.startswith('#'):
workflowDetails[file]['trigger'].append(trigger)
else : # Here it handles triggers that are declared on the next line of "on:" key
columnNumOfOn = 0
line = getNextLine(fileContent)
subColumnNum = getColumnNum(line)
while line[columnNumOfOn] == " ":
if line[subColumnNum] != " ":
trigger = line.strip(" ")
trigger = trigger.split(':')[0]
workflowDetails[file]['trigger'].append(trigger)
line = getNextLine(fileContent)
workflowsDir = '.github/workflows/'
yaml_files = glob.glob(workflowsDir+'*.y*ml')
workflowDetails = {} #This is the main dictionary where all the workflow details are stored
for file in sorted(yaml_files):
file = file.split('/')[-1]
getWorkflowName(workflowsDir,file)
markdown = workflowsDir+'ActionFlow.md'
with open( markdown ,'w') as markdownFile:
markdownFile.write('# GitHub Actions Workflow Overview\n\n')
sortedDictionary = dict(sorted(workflowDetails.items(),key = lambda item:item[1]['name']))
for file in sortedDictionary:
uniqFileName = workflowDetails[file]['name']
uniqFileName = uniqFileName.replace(' ','_') #replace spaces with underscore
uniqFileName = uniqFileName.replace('(','_') #replace parenthesis with underscore
uniqFileName = uniqFileName.replace(')','_')
uniqFileName = uniqFileName.upper()
fileBlock = f"{uniqFileName}[\"{workflowDetails[file]['name']}[{file}]\"]"
jobs = workflowDetails[file]['jobs']
markdownFile.write('```mermaid\n')
markdownFile.write('\nflowchart LR\n')
for job in jobs:
needs = jobs[job]['needs']
needs = needs.replace('[','')
needs = needs.replace(']','')
needs = needs.replace(' ','')
if len(needs) == 0:
markdownFile.write(f"\n {fileBlock} ---> {job}(\"{jobs[job]['name']}\")")
else:
needs = list(needs.split(','))
for need in needs:
markdownFile.write(f"\n {need}(\"{jobs[need]['name']}\") ---> {job}(\"{jobs[job]['name']}\")")
uses = jobs[job]['uses']
if uses != "":
uses = uses.split('/')[-1]
str = ","
inputs = str.join(jobs[job]['inputs'])
inputs = inputs.replace('\"','') #remove " (double quotes)
inputs = inputs.replace("\'",'') #remove ' (single quotes)
if len(inputs) != 0:
markdownFile.write(f"\n {job}(\"{jobs[job]['name']}\ninputs:[{inputs}]\") ---> {uses}(\"{workflowDetails[uses]['name']}[{uses}]\")")
else:
markdownFile.write(f"\n {job}(\"{jobs[job]['name']}\") ---> {uses}")
markdownFile.write('\n\n```\n\n')
markdownFile.write('## GitHub Actions Trigger Overview\n')
triggersList = set()
for file in workflowDetails:
for trigger in workflowDetails[file]['trigger']:
triggersList.add(trigger) #triggersList contains unique list of all types of triggers used in the workflow file
for trigger in sorted(triggersList):
markdownFile.write('\n```mermaid\n')
markdownFile.write('\nflowchart TB\n')
markdownFile.write(f'\n subgraph {trigger.upper()}')
for file in sortedDictionary:
if trigger in workflowDetails[file]['trigger']:
markdownFile.write(f'\n {trigger} ---> {file}[\"{workflowDetails[file]["name"]}[{file}]\"]')
markdownFile.write('\n end\n')
markdownFile.write('\n```\n')
- name: Upload Artifact
uses: actions/upload-artifact@v4
with:
name: Actions-workflow-MarkdownFile
path: .github/workflows/ActionFlow.md