Skip to content

Commit ace4abb

Browse files
authored
Split frontmatter_types.go into types, parsing, and serialization files (#26305)
1 parent b048b08 commit ace4abb

3 files changed

Lines changed: 713 additions & 736 deletions

File tree

Lines changed: 275 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,275 @@
1+
package workflow
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
"strconv"
7+
)
8+
9+
// ParseFrontmatterConfig creates a FrontmatterConfig from a raw frontmatter map
10+
// This provides a single entry point for converting untyped frontmatter into
11+
// a structured configuration with better error handling.
12+
func ParseFrontmatterConfig(frontmatter map[string]any) (*FrontmatterConfig, error) {
13+
frontmatterTypesLog.Printf("Parsing frontmatter config with %d fields", len(frontmatter))
14+
var config FrontmatterConfig
15+
16+
// Use JSON marshaling for the entire frontmatter conversion.
17+
// TemplatableInt32.UnmarshalJSON transparently handles both integer literals
18+
// (e.g. timeout-minutes: 30) and GitHub Actions expressions
19+
// (e.g. timeout-minutes: ${{ inputs.timeout }}) during unmarshaling.
20+
jsonBytes, err := json.Marshal(frontmatter)
21+
if err != nil {
22+
frontmatterTypesLog.Printf("Failed to marshal frontmatter: %v", err)
23+
return nil, fmt.Errorf("failed to marshal frontmatter to JSON: %w", err)
24+
}
25+
26+
if err := json.Unmarshal(jsonBytes, &config); err != nil {
27+
frontmatterTypesLog.Printf("Failed to unmarshal frontmatter: %v", err)
28+
return nil, fmt.Errorf("failed to unmarshal frontmatter into config: %w", err)
29+
}
30+
31+
// Parse typed Runtimes field if runtimes exist
32+
if len(config.Runtimes) > 0 {
33+
runtimesTyped, err := parseRuntimesConfig(config.Runtimes)
34+
if err == nil {
35+
config.RuntimesTyped = runtimesTyped
36+
frontmatterTypesLog.Printf("Parsed typed runtimes config with %d runtimes", countRuntimes(runtimesTyped))
37+
}
38+
}
39+
40+
// Parse typed Permissions field if permissions exist
41+
if len(config.Permissions) > 0 {
42+
permissionsTyped, err := parsePermissionsConfig(config.Permissions)
43+
if err == nil {
44+
config.PermissionsTyped = permissionsTyped
45+
frontmatterTypesLog.Print("Parsed typed permissions config")
46+
}
47+
}
48+
49+
// Parse checkout field - supports single object, array of objects, or false to disable
50+
if config.Checkout != nil {
51+
if checkoutValue, ok := config.Checkout.(bool); ok && !checkoutValue {
52+
config.CheckoutDisabled = true
53+
frontmatterTypesLog.Print("Checkout disabled via checkout: false")
54+
} else {
55+
checkoutConfigs, err := ParseCheckoutConfigs(config.Checkout)
56+
if err == nil {
57+
config.CheckoutConfigs = checkoutConfigs
58+
frontmatterTypesLog.Printf("Parsed checkout config: %d entries", len(checkoutConfigs))
59+
}
60+
}
61+
}
62+
63+
frontmatterTypesLog.Printf("Successfully parsed frontmatter config: name=%s, engine=%v", config.Name, config.Engine)
64+
return &config, nil
65+
}
66+
67+
// parseRuntimesConfig converts a map[string]any to RuntimesConfig
68+
func parseRuntimesConfig(runtimes map[string]any) (*RuntimesConfig, error) {
69+
config := &RuntimesConfig{}
70+
71+
for runtimeID, configAny := range runtimes {
72+
configMap, ok := configAny.(map[string]any)
73+
if !ok {
74+
frontmatterTypesLog.Printf("Skipping runtime '%s': expected map, got %T", runtimeID, configAny)
75+
continue
76+
}
77+
78+
// Extract version (optional)
79+
var version string
80+
if versionAny, hasVersion := configMap["version"]; hasVersion {
81+
// Convert version to string
82+
switch v := versionAny.(type) {
83+
case string:
84+
version = v
85+
case int:
86+
version = strconv.Itoa(v)
87+
case float64:
88+
if v == float64(int(v)) {
89+
version = strconv.Itoa(int(v))
90+
} else {
91+
version = fmt.Sprintf("%g", v)
92+
}
93+
default:
94+
continue
95+
}
96+
}
97+
98+
// Extract if condition (optional)
99+
var ifCondition string
100+
if ifAny, hasIf := configMap["if"]; hasIf {
101+
if ifStr, ok := ifAny.(string); ok {
102+
ifCondition = ifStr
103+
}
104+
}
105+
106+
// Extract action-repo and action-version overrides (optional)
107+
actionRepo, _ := configMap["action-repo"].(string)
108+
actionVersion, _ := configMap["action-version"].(string)
109+
110+
// Extract run-install-scripts flag (optional)
111+
var runInstallScripts *bool
112+
if rsAny, hasRS := configMap["run-install-scripts"]; hasRS {
113+
if rsBool, ok := rsAny.(bool); ok {
114+
runInstallScripts = &rsBool
115+
}
116+
}
117+
118+
// Create runtime config with all fields
119+
runtimeConfig := &RuntimeConfig{
120+
Version: version,
121+
If: ifCondition,
122+
ActionRepo: actionRepo,
123+
ActionVersion: actionVersion,
124+
RunInstallScripts: runInstallScripts,
125+
}
126+
127+
// Map to specific runtime field
128+
switch runtimeID {
129+
case "node":
130+
config.Node = runtimeConfig
131+
case "python":
132+
config.Python = runtimeConfig
133+
case "go":
134+
config.Go = runtimeConfig
135+
case "uv":
136+
config.UV = runtimeConfig
137+
case "bun":
138+
config.Bun = runtimeConfig
139+
case "deno":
140+
config.Deno = runtimeConfig
141+
case "dotnet":
142+
config.Dotnet = runtimeConfig
143+
case "elixir":
144+
config.Elixir = runtimeConfig
145+
case "haskell":
146+
config.Haskell = runtimeConfig
147+
case "java":
148+
config.Java = runtimeConfig
149+
case "ruby":
150+
config.Ruby = runtimeConfig
151+
}
152+
}
153+
154+
return config, nil
155+
}
156+
157+
// parsePermissionsConfig converts a map[string]any to PermissionsConfig
158+
func parsePermissionsConfig(permissions map[string]any) (*PermissionsConfig, error) {
159+
config := &PermissionsConfig{}
160+
161+
// Check if it's a shorthand permission (single string value)
162+
if len(permissions) == 1 {
163+
for key, value := range permissions {
164+
if strValue, ok := value.(string); ok {
165+
shorthandPerms := []string{"read-all", "write-all", "read", "write", "none"}
166+
for _, shorthand := range shorthandPerms {
167+
if key == shorthand || strValue == shorthand {
168+
config.Shorthand = shorthand
169+
return config, nil
170+
}
171+
}
172+
}
173+
}
174+
}
175+
176+
// Parse detailed permissions
177+
for scope, level := range permissions {
178+
if levelStr, ok := level.(string); ok {
179+
switch scope {
180+
// GitHub Actions permission scopes
181+
case "actions":
182+
config.Actions = levelStr
183+
case "checks":
184+
config.Checks = levelStr
185+
case "contents":
186+
config.Contents = levelStr
187+
case "deployments":
188+
config.Deployments = levelStr
189+
case "id-token":
190+
config.IDToken = levelStr
191+
case "issues":
192+
config.Issues = levelStr
193+
case "discussions":
194+
config.Discussions = levelStr
195+
case "packages":
196+
config.Packages = levelStr
197+
case "pages":
198+
config.Pages = levelStr
199+
case "pull-requests":
200+
config.PullRequests = levelStr
201+
case "repository-projects":
202+
config.RepositoryProjects = levelStr
203+
case "security-events":
204+
config.SecurityEvents = levelStr
205+
case "statuses":
206+
config.Statuses = levelStr
207+
case "organization-projects":
208+
config.OrganizationProjects = levelStr
209+
// GitHub App-only permission scopes
210+
case "administration":
211+
config.Administration = levelStr
212+
case "environments":
213+
config.Environments = levelStr
214+
case "git-signing":
215+
config.GitSigning = levelStr
216+
case "vulnerability-alerts":
217+
config.VulnerabilityAlerts = levelStr
218+
case "workflows":
219+
config.Workflows = levelStr
220+
case "repository-hooks":
221+
config.RepositoryHooks = levelStr
222+
case "single-file":
223+
config.SingleFile = levelStr
224+
case "codespaces":
225+
config.Codespaces = levelStr
226+
case "repository-custom-properties":
227+
config.RepositoryCustomProperties = levelStr
228+
case "members":
229+
config.Members = levelStr
230+
case "organization-administration":
231+
config.OrganizationAdministration = levelStr
232+
case "team-discussions":
233+
config.TeamDiscussions = levelStr
234+
case "organization-hooks":
235+
config.OrganizationHooks = levelStr
236+
case "organization-members":
237+
config.OrganizationMembers = levelStr
238+
case "organization-packages":
239+
config.OrganizationPackages = levelStr
240+
case "organization-self-hosted-runners":
241+
config.OrganizationSelfHostedRunners = levelStr
242+
case "organization-custom-org-roles":
243+
config.OrganizationCustomOrgRoles = levelStr
244+
case "organization-custom-properties":
245+
config.OrganizationCustomProperties = levelStr
246+
case "organization-custom-repository-roles":
247+
config.OrganizationCustomRepositoryRoles = levelStr
248+
case "organization-announcement-banners":
249+
config.OrganizationAnnouncementBanners = levelStr
250+
case "organization-events":
251+
config.OrganizationEvents = levelStr
252+
case "organization-plan":
253+
config.OrganizationPlan = levelStr
254+
case "organization-user-blocking":
255+
config.OrganizationUserBlocking = levelStr
256+
case "organization-personal-access-token-requests":
257+
config.OrganizationPersonalAccessTokenReqs = levelStr
258+
case "organization-personal-access-tokens":
259+
config.OrganizationPersonalAccessTokens = levelStr
260+
case "organization-copilot":
261+
config.OrganizationCopilot = levelStr
262+
case "organization-codespaces":
263+
config.OrganizationCodespaces = levelStr
264+
case "email-addresses":
265+
config.EmailAddresses = levelStr
266+
case "codespaces-lifecycle-admin":
267+
config.CodespacesLifecycleAdmin = levelStr
268+
case "codespaces-metadata":
269+
config.CodespacesMetadata = levelStr
270+
}
271+
}
272+
}
273+
274+
return config, nil
275+
}

0 commit comments

Comments
 (0)