renderStepFromMap renders a GitHub Actions step from a map to YAML
(out *strings.Builder, step map[string]any, data *WorkflowData, indent string)
| 106 | |
| 107 | // renderStepFromMap renders a GitHub Actions step from a map to YAML |
| 108 | func (c *Compiler) renderStepFromMap(out *strings.Builder, step map[string]any, data *WorkflowData, indent string) { |
| 109 | // Before rendering, extract any ${{ ... }} expressions from the run: field into |
| 110 | // env: variables to prevent shell injection attacks. A compiler warning is emitted |
| 111 | // for every expression that is moved so that authors know their script was changed. |
| 112 | if sanitized, warnings, changed := sanitizeRunStepExpressions(step); changed { |
| 113 | stepConversionLog.Printf("Sanitized run-step expressions: %d warning(s) emitted", len(warnings)) |
| 114 | for _, w := range warnings { |
| 115 | fmt.Fprintln(os.Stderr, console.FormatWarningMessage(w)) |
| 116 | c.IncrementWarningCount() |
| 117 | } |
| 118 | step = sanitized |
| 119 | } |
| 120 | |
| 121 | stepName, _ := step["name"].(string) |
| 122 | stepConversionLog.Printf("Rendering step from map: name=%q, fields=%d", stepName, len(step)) |
| 123 | // Start the step with a dash |
| 124 | out.WriteString(indent + "- ") |
| 125 | |
| 126 | // Track if we've written the first line |
| 127 | firstField := true |
| 128 | |
| 129 | // Order of fields to write (matches GitHub Actions convention) |
| 130 | fieldOrder := []string{"name", "id", "if", "uses", "with", "run", "env", "working-directory", "continue-on-error", "timeout-minutes", "shell"} |
| 131 | |
| 132 | for _, field := range fieldOrder { |
| 133 | if value, exists := step[field]; exists { |
| 134 | // Add proper indentation for non-first fields |
| 135 | if !firstField { |
| 136 | out.WriteString(indent + " ") |
| 137 | } |
| 138 | firstField = false |
| 139 | |
| 140 | // Render the field based on its type |
| 141 | switch v := value.(type) { |
| 142 | case string: |
| 143 | // Handle multi-line strings (especially for 'run' field) |
| 144 | if field == "run" && strings.Contains(v, "\n") { |
| 145 | fmt.Fprintf(out, "%s: |\n", field) |
| 146 | lines := strings.SplitSeq(v, "\n") |
| 147 | for line := range lines { |
| 148 | fmt.Fprintf(out, "%s %s\n", indent, line) |
| 149 | } |
| 150 | } else { |
| 151 | fmt.Fprintf(out, "%s: %s\n", field, v) |
| 152 | } |
| 153 | case map[string]any: |
| 154 | // For complex fields like "with" or "env" — sort keys for stable output. |
| 155 | fmt.Fprintf(out, "%s:\n", field) |
| 156 | for _, key := range sliceutil.SortedKeys(v) { |
| 157 | if field == "env" { |
| 158 | fmt.Fprintf(out, "%s %s: %s\n", indent, key, formatStepEnvValueForYAML(v[key])) |
| 159 | } else { |
| 160 | fmt.Fprintf(out, "%s %s: %v\n", indent, key, v[key]) |
| 161 | } |
| 162 | } |
| 163 | default: |
| 164 | fmt.Fprintf(out, "%s: %v\n", field, v) |
| 165 | } |
no test coverage detected