-
Notifications
You must be signed in to change notification settings - Fork 358
Expand file tree
/
Copy pathfrontmatter_extraction_metadata.go
More file actions
274 lines (233 loc) · 9.47 KB
/
frontmatter_extraction_metadata.go
File metadata and controls
274 lines (233 loc) · 9.47 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
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
package workflow
import (
"fmt"
"maps"
"strconv"
"strings"
"github.com/github/gh-aw/pkg/logger"
"github.com/github/gh-aw/pkg/typeutil"
)
var frontmatterMetadataLog = logger.New("workflow:frontmatter_extraction_metadata")
// extractFeatures extracts the features field from frontmatter
// Returns a map of feature flags and configuration options (supports boolean flags and string values)
func (c *Compiler) extractFeatures(frontmatter map[string]any) map[string]any {
frontmatterMetadataLog.Print("Extracting features from frontmatter")
value, exists := frontmatter["features"]
if !exists {
frontmatterMetadataLog.Print("No features field found in frontmatter")
return nil
}
// Features should be an object with any values (boolean or string)
if featuresMap, ok := value.(map[string]any); ok {
result := make(map[string]any)
// Accept any value type (boolean, string, etc.)
maps.Copy(result, featuresMap)
frontmatterMetadataLog.Printf("Extracted %d features", len(result))
return result
}
frontmatterMetadataLog.Print("Features field is not a map")
return nil
}
// extractDescription extracts the description field from frontmatter
func (c *Compiler) extractDescription(frontmatter map[string]any) string {
value, exists := frontmatter["description"]
if !exists {
return ""
}
// Convert the value to string
if strValue, ok := value.(string); ok {
desc := strings.TrimSpace(strValue)
frontmatterMetadataLog.Printf("Extracted description: %d characters", len(desc))
return desc
}
frontmatterMetadataLog.Printf("Description field is not a string: type=%T", value)
return ""
}
// extractSource extracts the source field from frontmatter
func (c *Compiler) extractSource(frontmatter map[string]any) string {
value, exists := frontmatter["source"]
if !exists {
return ""
}
// Convert the value to string
if strValue, ok := value.(string); ok {
return strings.TrimSpace(strValue)
}
return ""
}
// extractRedirect extracts the redirect field from frontmatter
func (c *Compiler) extractRedirect(frontmatter map[string]any) string {
value, exists := frontmatter["redirect"]
if !exists {
return ""
}
// Convert the value to string
if strValue, ok := value.(string); ok {
return strings.TrimSpace(strValue)
}
return ""
}
// extractTrackerID extracts and validates the tracker-id field from frontmatter
func (c *Compiler) extractTrackerID(frontmatter map[string]any) (string, error) {
value, exists := frontmatter["tracker-id"]
if !exists {
return "", nil
}
frontmatterMetadataLog.Print("Extracting and validating tracker-id")
// Convert the value to string
strValue, ok := value.(string)
if !ok {
frontmatterMetadataLog.Printf("Invalid tracker-id type: %T", value)
return "", fmt.Errorf("tracker-id must be a string, got %T. Example: tracker-id: \"my-tracker-123\"", value)
}
trackerID := strings.TrimSpace(strValue)
// Validate minimum length
if len(trackerID) < 8 {
frontmatterMetadataLog.Printf("tracker-id too short: %d characters", len(trackerID))
return "", fmt.Errorf("tracker-id must be at least 8 characters long (got %d)", len(trackerID))
}
// Validate that it's a valid identifier (alphanumeric, hyphens, underscores)
for i, char := range trackerID {
if (char < 'a' || char > 'z') && (char < 'A' || char > 'Z') &&
(char < '0' || char > '9') && char != '-' && char != '_' {
frontmatterMetadataLog.Printf("Invalid character in tracker-id at position %d", i+1)
return "", fmt.Errorf("tracker-id contains invalid character at position %d: '%c' (only alphanumeric, hyphens, and underscores allowed)", i+1, char)
}
}
frontmatterMetadataLog.Printf("Successfully validated tracker-id: %s", trackerID)
return trackerID, nil
}
// buildSourceURL converts a source string (owner/repo/path@ref) to a GitHub URL
// For enterprise deployments, the URL will use the GitHub server URL from the workflow context
func buildSourceURL(source string) string {
frontmatterMetadataLog.Printf("Building source URL from: %s", source)
if source == "" {
return ""
}
// Parse the source string: owner/repo/path@ref
parts := strings.Split(source, "@")
if len(parts) == 0 {
return ""
}
pathPart := parts[0] // "owner/repo/path"
refPart := "main" // default ref
if len(parts) > 1 {
refPart = parts[1]
}
// Build GitHub URL using server URL from GitHub Actions context
// The pathPart is "owner/repo/workflows/file.md", we need to convert it to
// "${GITHUB_SERVER_URL}/owner/repo/blob/ref/workflows/file.md"
// Using /blob/ renders the markdown file (rendered view) instead of /tree/ (directory listing)
pathComponents := strings.SplitN(pathPart, "/", 3)
if len(pathComponents) < 3 {
frontmatterMetadataLog.Printf("Invalid source path format: %s (expected owner/repo/path)", pathPart)
return ""
}
owner := pathComponents[0]
repo := pathComponents[1]
filePath := pathComponents[2]
url := fmt.Sprintf("${{ github.server_url }}/%s/%s/blob/%s/%s", owner, repo, refPart, filePath)
frontmatterMetadataLog.Printf("Built source URL: %s/%s blob %s", owner, repo, refPart)
// Use github.server_url for enterprise GitHub deployments
return url
}
// extractToolsTimeout extracts the timeout setting from tools
// Returns "" if not set (engines will use their own defaults)
// Returns error if timeout is explicitly set but invalid (< 1 for literals, or non-expression string)
func (c *Compiler) extractToolsTimeout(tools map[string]any) (string, error) {
if tools == nil {
return "", nil // Use engine defaults
}
// Check if timeout is explicitly set in tools
if timeoutValue, exists := tools["timeout"]; exists {
frontmatterMetadataLog.Printf("Extracting tools.timeout value: type=%T", timeoutValue)
// Handle GitHub Actions expression strings
if strVal, ok := timeoutValue.(string); ok {
if isExpression(strVal) {
frontmatterMetadataLog.Printf("Extracted tools.timeout as expression: %s", strVal)
return strVal, nil
}
frontmatterMetadataLog.Printf("Invalid tools.timeout string (not an expression): %s", strVal)
return "", fmt.Errorf("tools.timeout must be an integer or a GitHub Actions expression (e.g. '${{ inputs.tool-timeout }}'), got string %q", strVal)
}
// Handle different numeric types with safe conversions to prevent overflow
var timeout int
switch v := timeoutValue.(type) {
case int:
timeout = v
case int64:
timeout = int(v)
case uint:
timeout = typeutil.SafeUintToInt(v) // Safe conversion to prevent overflow (alert #418)
case uint64:
timeout = typeutil.SafeUint64ToInt(v) // Safe conversion to prevent overflow (alert #416)
case float64:
timeout = int(v)
default:
frontmatterMetadataLog.Printf("Invalid tools.timeout type: %T", timeoutValue)
return "", fmt.Errorf("tools.timeout must be an integer or a GitHub Actions expression, got %T", timeoutValue)
}
// Validate minimum value per schema constraint
if timeout < 1 {
frontmatterMetadataLog.Printf("Invalid tools.timeout value: %d (must be >= 1)", timeout)
return "", fmt.Errorf("tools.timeout must be at least 1 second, got %d. Example:\ntools:\n timeout: 60", timeout)
}
frontmatterMetadataLog.Printf("Extracted tools.timeout: %d seconds", timeout)
return strconv.Itoa(timeout), nil
}
// Default to "" (use engine defaults)
return "", nil
}
// extractToolsStartupTimeout extracts the startup-timeout setting from tools
// Returns "" if not set (engines will use their own defaults)
// Returns error if startup-timeout is explicitly set but invalid (< 1 for literals, or non-expression string)
func (c *Compiler) extractToolsStartupTimeout(tools map[string]any) (string, error) {
if tools == nil {
return "", nil // Use engine defaults
}
// Check if startup-timeout is explicitly set in tools
if timeoutValue, exists := tools["startup-timeout"]; exists {
// Handle GitHub Actions expression strings
if strVal, ok := timeoutValue.(string); ok {
if isExpression(strVal) {
return strVal, nil
}
return "", fmt.Errorf("tools.startup-timeout must be an integer or a GitHub Actions expression (e.g. '${{ inputs.startup-timeout }}'), got string %q", strVal)
}
var timeout int
// Handle different numeric types with safe conversions to prevent overflow
switch v := timeoutValue.(type) {
case int:
timeout = v
case int64:
timeout = int(v)
case uint:
timeout = typeutil.SafeUintToInt(v) // Safe conversion to prevent overflow (alert #417)
case uint64:
timeout = typeutil.SafeUint64ToInt(v) // Safe conversion to prevent overflow (alert #415)
case float64:
timeout = int(v)
default:
return "", fmt.Errorf("tools.startup-timeout must be an integer or a GitHub Actions expression, got %T", timeoutValue)
}
// Validate minimum value per schema constraint
if timeout < 1 {
return "", fmt.Errorf("tools.startup-timeout must be at least 1 second, got %d. Example:\ntools:\n startup-timeout: 120", timeout)
}
return strconv.Itoa(timeout), nil
}
// Default to "" (use engine defaults)
return "", nil
}
// extractToolsFromFrontmatter extracts tools section from frontmatter map
func extractToolsFromFrontmatter(frontmatter map[string]any) map[string]any {
return ExtractMapField(frontmatter, "tools")
}
// extractMCPServersFromFrontmatter extracts mcp-servers section from frontmatter
func extractMCPServersFromFrontmatter(frontmatter map[string]any) map[string]any {
return ExtractMapField(frontmatter, "mcp-servers")
}
// extractRuntimesFromFrontmatter extracts runtimes section from frontmatter map
func extractRuntimesFromFrontmatter(frontmatter map[string]any) map[string]any {
return ExtractMapField(frontmatter, "runtimes")
}