-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathdag_mermaid.go
More file actions
160 lines (137 loc) · 4.31 KB
/
Copy pathdag_mermaid.go
File metadata and controls
160 lines (137 loc) · 4.31 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
package probe
import (
"fmt"
"regexp"
"strings"
)
// DagMermaidRenderer renders workflow graphs in Mermaid format
type DagMermaidRenderer struct {
DagRendererBase
jobIDToIdx map[string]int
}
// NewDagMermaidRenderer creates a new DagMermaidRenderer
func NewDagMermaidRenderer(w *Workflow) *DagMermaidRenderer {
r := &DagMermaidRenderer{
DagRendererBase: NewDagRendererBase(w),
jobIDToIdx: make(map[string]int),
}
// Build job ID to index mapping
for i, job := range w.Jobs {
id := job.ID
if id == "" {
id = job.Name
}
r.jobIDToIdx[id] = i
}
return r
}
// Render generates the Mermaid flowchart representation
func (r *DagMermaidRenderer) Render() string {
if len(r.workflow.Jobs) == 0 {
return ""
}
var sb strings.Builder
// Mermaid flowchart header (left-right direction)
sb.WriteString("flowchart LR\n")
// Render node definitions with subgraphs for steps
for _, job := range r.workflow.Jobs {
jobID := r.getJobID(&job)
safeID := r.sanitizeID(jobID)
displayName := r.escapeLabel(job.Name)
// Create subgraph for job with steps
if len(job.Steps) > 0 {
fmt.Fprintf(&sb, " subgraph %s[\"%s\"]\n", safeID, displayName)
stepIndex := 0
for _, step := range job.Steps {
stepName := step.Name
if stepName == "" {
stepName = step.Uses
}
stepID := fmt.Sprintf("%s_step%d", safeID, stepIndex)
stepLabel := r.escapeLabel(stepName)
fmt.Fprintf(&sb, " %s[\"%s\"]\n", stepID, stepLabel)
stepIndex++
// Render embedded steps if this is an embedded action
if step.Uses == "embedded" {
if pathVal, ok := step.With["path"]; ok {
if pathStr, ok := pathVal.(string); ok {
// Expand template variables in path and resolve relative to workflow directory
expandedPath := r.ExpandPath(pathStr)
resolvedPath := r.ResolvePath(expandedPath)
embeddedJob, err := LoadEmbeddedJob(resolvedPath)
if err == nil && len(embeddedJob.Steps) > 0 {
// Render each embedded step
for _, embStep := range embeddedJob.Steps {
embStepName := embStep.Name
if embStepName == "" {
embStepName = embStep.Uses
}
embStepID := fmt.Sprintf("%s_step%d", safeID, stepIndex)
embStepLabel := r.escapeLabel(embStepName)
fmt.Fprintf(&sb, " %s[\"%s\"]\n", embStepID, embStepLabel)
stepIndex++
}
}
}
}
}
}
sb.WriteString(" end\n")
} else {
// Job without steps - simple node
fmt.Fprintf(&sb, " %s[\"%s\"]\n", safeID, displayName)
}
}
sb.WriteString("\n")
// Render edges (dependencies)
for _, job := range r.workflow.Jobs {
if len(job.Needs) == 0 {
continue
}
jobID := r.getJobID(&job)
safeJobID := r.sanitizeID(jobID)
for _, need := range job.Needs {
safeNeedID := r.sanitizeID(need)
fmt.Fprintf(&sb, " %s --> %s\n", safeNeedID, safeJobID)
}
}
return sb.String()
}
// getJobID returns the job ID or Name if ID is empty
func (r *DagMermaidRenderer) getJobID(job *Job) string {
if job.ID != "" {
return job.ID
}
return job.Name
}
// sanitizeID converts a job ID/name to a valid Mermaid node ID
// Mermaid IDs should be alphanumeric with underscores
//
// NOTE: Original job/step IDs are validated for uniqueness by validateIDs() in probe.go.
// However, sanitized IDs may collide (e.g., "unit-test" and "unit.test" both become "unit_test").
// This is acceptable as such naming conflicts are rare in practice.
func (r *DagMermaidRenderer) sanitizeID(id string) string {
// Replace non-alphanumeric characters with underscores
reg := regexp.MustCompile(`[^a-zA-Z0-9_]`)
sanitized := reg.ReplaceAllString(id, "_")
// Ensure it starts with a letter (prepend 'n' if it starts with a number)
if len(sanitized) > 0 && sanitized[0] >= '0' && sanitized[0] <= '9' {
sanitized = "n" + sanitized
}
// Ensure it's not empty
if sanitized == "" {
sanitized = "node"
}
return sanitized
}
// escapeLabel escapes special characters for Mermaid labels
func (r *DagMermaidRenderer) escapeLabel(label string) string {
// Escape double quotes
label = strings.ReplaceAll(label, "\"", "#quot;")
return label
}
// RenderDagMermaid returns the Mermaid format representation of workflow job dependencies
func (w *Workflow) RenderDagMermaid() string {
renderer := NewDagMermaidRenderer(w)
return renderer.Render()
}