Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
137 changes: 94 additions & 43 deletions pkg/parser/import_field_extractor.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,9 @@ type importAccumulator struct {
topLevelGitHubApp string // JSON-encoded GitHubAppConfig
// Checkout configs from all imported files (append in order; main workflow's checkouts take precedence)
checkouts []string // JSON-encoded checkout values, one per import
// First engine.mcp.tool-timeout / engine.mcp.session-timeout found across all imported files (first-wins strategy)
mergedEngineMCPToolTimeout string // Go duration string (e.g. "10m", "30s")
mergedEngineMCPSessionTimeout string // Go duration string (e.g. "4h", "30m")
// Best-effort sub-agent frontmatter warnings collected during BFS traversal.
warnings []string
}
Expand Down Expand Up @@ -225,11 +228,57 @@ func (acc *importAccumulator) extractAllImportFields(content []byte, item import
}
}

// Extract engines from imported file
engineContent, err := extractFieldJSONFromMap(fm, "engine", "")
if err == nil && engineContent != "" {
// Extract engines from imported file.
// Engine configs with only `mcp` sub-keys (no `id` or `runtime`) are not counted
// as engine specifications — they only carry MCP gateway settings. This prevents
// "multiple engine fields" errors when a shared workflow declares engine.mcp.*
// without specifying an engine ID.
if engineVal, hasEngine := fm["engine"]; hasEngine {
log.Printf("Found engine config in import: %s", item.fullPath)
acc.engines = append(acc.engines, engineContent)

switch v := engineVal.(type) {
case string:
// String engine (e.g. "copilot") — always counts as an engine spec.
if engineJSON, merr := json.Marshal(v); merr == nil {
acc.engines = append(acc.engines, string(engineJSON))
}
case map[string]any:
// Object engine — extract engine.mcp.* settings first, then decide
// whether to add to engines based on whether an engine ID is present.
if mcpVal, hasMCP := v["mcp"]; hasMCP {
if mcpMap, ok := mcpVal.(map[string]any); ok {
// Extract tool-timeout (first-wins across all imports)
if acc.mergedEngineMCPToolTimeout == "" {
if ttStr, ok := mcpMap["tool-timeout"].(string); ok && ttStr != "" {
acc.mergedEngineMCPToolTimeout = ttStr
log.Printf("Extracted engine.mcp.tool-timeout from import %s: %s", item.fullPath, ttStr)
}
}
// Extract session-timeout (first-wins across all imports)
if acc.mergedEngineMCPSessionTimeout == "" {
if stStr, ok := mcpMap["session-timeout"].(string); ok && stStr != "" {
acc.mergedEngineMCPSessionTimeout = stStr
log.Printf("Extracted engine.mcp.session-timeout from import %s: %s", item.fullPath, stStr)
}
}
}
}
// Only add to engines list if this config specifies an actual engine.
// Configs with only `mcp` settings are purely MCP gateway configuration
// and must not trigger the "multiple engine fields" validation error.
_, hasMCP := v["mcp"]
isMCPOnly := hasMCP && len(v) == 1
if !isMCPOnly {
if engineJSON, merr := json.Marshal(v); merr == nil {
acc.engines = append(acc.engines, string(engineJSON))
}
}
default:
// Unexpected type — marshal and add to preserve existing behavior.
if engineJSON, merr := json.Marshal(engineVal); merr == nil {
acc.engines = append(acc.engines, string(engineJSON))
}
}
}

// Extract mcp-servers from imported file
Expand Down Expand Up @@ -506,45 +555,47 @@ func (acc *importAccumulator) toImportsResult(topologicalOrder []string) *Import
log.Printf("Building ImportsResult: importedFiles=%d, importPaths=%d, engines=%d, bots=%d, labels=%d",
len(topologicalOrder), len(acc.importPaths), len(acc.engines), len(acc.bots), len(acc.labels))
return &ImportsResult{
MergedTools: acc.toolsBuilder.String(),
MergedMCPServers: acc.mcpServersBuilder.String(),
MergedEngines: acc.engines,
MergedSafeOutputs: acc.safeOutputs,
MergedMCPScripts: acc.mcpScripts,
MergedMarkdown: acc.markdownBuilder.String(),
ImportPaths: acc.importPaths,
MergedSteps: acc.stepsBuilder.String(),
CopilotSetupSteps: acc.copilotSetupStepsBuilder.String(),
MergedPreSteps: acc.preStepsBuilder.String(),
MergedPreAgentSteps: acc.preAgentStepsBuilder.String(),
MergedRuntimes: acc.runtimesBuilder.String(),
MergedRunInstallScripts: acc.runInstallScripts,
MergedServices: acc.servicesBuilder.String(),
MergedNetwork: acc.networkBuilder.String(),
MergedPermissions: acc.permissionsBuilder.String(),
MergedSecretMasking: acc.secretMaskingBuilder.String(),
MergedBots: acc.bots,
MergedSkipRoles: acc.skipRoles,
MergedSkipBots: acc.skipBots,
MergedPostSteps: acc.postStepsBuilder.String(),
MergedLabels: acc.labels,
MergedCaches: acc.caches,
MergedJobs: acc.jobsBuilder.String(),
MergedEnv: acc.envBuilder.String(),
MergedEnvSources: acc.envSources,
MergedFeatures: acc.features,
MergedModels: acc.models,
MergedObservability: mergeObservabilityConfigs(acc.observabilityConfigs),
ImportedFiles: topologicalOrder,
AgentFile: acc.agentFile,
AgentImportSpec: acc.agentImportSpec,
RepositoryImports: acc.repositoryImports,
ImportInputs: acc.importInputs,
MergedActivationGitHubToken: acc.activationGitHubToken,
MergedActivationGitHubApp: acc.activationGitHubApp,
MergedTopLevelGitHubApp: acc.topLevelGitHubApp,
MergedCheckout: strings.Join(acc.checkouts, "\n"),
Warnings: acc.warnings,
MergedTools: acc.toolsBuilder.String(),
MergedMCPServers: acc.mcpServersBuilder.String(),
MergedEngines: acc.engines,
MergedSafeOutputs: acc.safeOutputs,
MergedMCPScripts: acc.mcpScripts,
MergedMarkdown: acc.markdownBuilder.String(),
ImportPaths: acc.importPaths,
MergedSteps: acc.stepsBuilder.String(),
CopilotSetupSteps: acc.copilotSetupStepsBuilder.String(),
MergedPreSteps: acc.preStepsBuilder.String(),
MergedPreAgentSteps: acc.preAgentStepsBuilder.String(),
MergedRuntimes: acc.runtimesBuilder.String(),
MergedRunInstallScripts: acc.runInstallScripts,
MergedServices: acc.servicesBuilder.String(),
MergedNetwork: acc.networkBuilder.String(),
MergedPermissions: acc.permissionsBuilder.String(),
MergedSecretMasking: acc.secretMaskingBuilder.String(),
MergedBots: acc.bots,
MergedSkipRoles: acc.skipRoles,
MergedSkipBots: acc.skipBots,
MergedPostSteps: acc.postStepsBuilder.String(),
MergedLabels: acc.labels,
MergedCaches: acc.caches,
MergedJobs: acc.jobsBuilder.String(),
MergedEnv: acc.envBuilder.String(),
MergedEnvSources: acc.envSources,
MergedFeatures: acc.features,
MergedModels: acc.models,
MergedObservability: mergeObservabilityConfigs(acc.observabilityConfigs),
ImportedFiles: topologicalOrder,
AgentFile: acc.agentFile,
AgentImportSpec: acc.agentImportSpec,
RepositoryImports: acc.repositoryImports,
ImportInputs: acc.importInputs,
MergedActivationGitHubToken: acc.activationGitHubToken,
MergedActivationGitHubApp: acc.activationGitHubApp,
MergedTopLevelGitHubApp: acc.topLevelGitHubApp,
MergedCheckout: strings.Join(acc.checkouts, "\n"),
MergedEngineMCPToolTimeout: acc.mergedEngineMCPToolTimeout,
MergedEngineMCPSessionTimeout: acc.mergedEngineMCPSessionTimeout,
Warnings: acc.warnings,
}
}

Expand Down
76 changes: 39 additions & 37 deletions pkg/parser/import_processor.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,43 +14,45 @@ var importLog = logger.New("parser:import_processor")

// ImportsResult holds the result of processing imports from frontmatter
type ImportsResult struct {
MergedTools string // Merged tools configuration from all imports
MergedMCPServers string // Merged mcp-servers configuration from all imports
MergedEngines []string // Merged engine configurations from all imports
MergedSafeOutputs []string // Merged safe-outputs configurations from all imports
MergedMCPScripts []string // Merged mcp-scripts configurations from all imports
MergedMarkdown string // Only contains imports WITH inputs (for compile-time substitution)
ImportPaths []string // List of import file paths for runtime-import macro generation (replaces MergedMarkdown)
MergedSteps string // Merged steps configuration from all imports (excluding copilot-setup-steps)
CopilotSetupSteps string // Steps from copilot-setup-steps.yml (inserted at start)
MergedPreSteps string // Merged pre-steps configuration from all imports (prepended in order)
MergedPreAgentSteps string // Merged pre-agent-steps configuration from all imports (prepended in order)
MergedRuntimes string // Merged runtimes configuration from all imports
MergedRunInstallScripts bool // true if any imported workflow sets run-install-scripts: true (global or node-level)
MergedServices string // Merged services configuration from all imports
MergedNetwork string // Merged network configuration from all imports
MergedPermissions string // Merged permissions configuration from all imports
MergedSecretMasking string // Merged secret-masking steps from all imports
MergedBots []string // Merged bots list from all imports (union of bot names)
MergedSkipRoles []string // Merged skip-roles list from all imports (union of role names)
MergedSkipBots []string // Merged skip-bots list from all imports (union of usernames)
MergedActivationGitHubToken string // GitHub token from on.github-token in first imported workflow that defines it
MergedActivationGitHubApp string // JSON-encoded on.github-app from first imported workflow that defines it
MergedTopLevelGitHubApp string // JSON-encoded top-level github-app from first imported workflow that defines it
MergedCheckout string // JSON-encoded checkout configurations from imported workflows (one JSON value per line)
MergedPostSteps string // Merged post-steps configuration from all imports (appended in order)
MergedLabels []string // Merged labels from all imports (union of label names)
MergedCaches []string // Merged cache configurations from all imports (appended in order)
MergedJobs string // Merged jobs from imported YAML workflows (JSON format)
MergedEnv string // Merged env configuration from all imports (JSON format)
MergedEnvSources map[string]string // env var name → source import path (for conflict detection and lock file header listing)
MergedFeatures []map[string]any // Merged features configuration from all imports (parsed YAML structures)
MergedModels []map[string][]string // Merged model alias definitions from all imports (first import to define a key wins among imports)
MergedObservability string // Merged observability config (JSON) from all imports as an endpoint array (deduped by URL)
ImportedFiles []string // List of imported file paths (for manifest)
AgentFile string // Path to custom agent file (if imported)
AgentImportSpec string // Original import specification for agent file (e.g., "owner/repo/path@ref")
RepositoryImports []string // List of repository imports (format: "owner/repo@ref") for .github folder merging
MergedTools string // Merged tools configuration from all imports
MergedMCPServers string // Merged mcp-servers configuration from all imports
MergedEngines []string // Merged engine configurations from all imports
MergedSafeOutputs []string // Merged safe-outputs configurations from all imports
MergedMCPScripts []string // Merged mcp-scripts configurations from all imports
MergedMarkdown string // Only contains imports WITH inputs (for compile-time substitution)
ImportPaths []string // List of import file paths for runtime-import macro generation (replaces MergedMarkdown)
MergedSteps string // Merged steps configuration from all imports (excluding copilot-setup-steps)
CopilotSetupSteps string // Steps from copilot-setup-steps.yml (inserted at start)
MergedPreSteps string // Merged pre-steps configuration from all imports (prepended in order)
MergedPreAgentSteps string // Merged pre-agent-steps configuration from all imports (prepended in order)
MergedRuntimes string // Merged runtimes configuration from all imports
MergedRunInstallScripts bool // true if any imported workflow sets run-install-scripts: true (global or node-level)
MergedServices string // Merged services configuration from all imports
MergedNetwork string // Merged network configuration from all imports
MergedPermissions string // Merged permissions configuration from all imports
MergedSecretMasking string // Merged secret-masking steps from all imports
MergedBots []string // Merged bots list from all imports (union of bot names)
MergedSkipRoles []string // Merged skip-roles list from all imports (union of role names)
MergedSkipBots []string // Merged skip-bots list from all imports (union of usernames)
MergedActivationGitHubToken string // GitHub token from on.github-token in first imported workflow that defines it
MergedActivationGitHubApp string // JSON-encoded on.github-app from first imported workflow that defines it
MergedTopLevelGitHubApp string // JSON-encoded top-level github-app from first imported workflow that defines it
MergedCheckout string // JSON-encoded checkout configurations from imported workflows (one JSON value per line)
MergedPostSteps string // Merged post-steps configuration from all imports (appended in order)
MergedLabels []string // Merged labels from all imports (union of label names)
MergedCaches []string // Merged cache configurations from all imports (appended in order)
MergedJobs string // Merged jobs from imported YAML workflows (JSON format)
MergedEnv string // Merged env configuration from all imports (JSON format)
MergedEnvSources map[string]string // env var name → source import path (for conflict detection and lock file header listing)
MergedFeatures []map[string]any // Merged features configuration from all imports (parsed YAML structures)
MergedModels []map[string][]string // Merged model alias definitions from all imports (first import to define a key wins among imports)
MergedObservability string // Merged observability config (JSON) from all imports as an endpoint array (deduped by URL)
MergedEngineMCPToolTimeout string // First engine.mcp.tool-timeout found across all imports (Go duration string, e.g. "10m")
MergedEngineMCPSessionTimeout string // First engine.mcp.session-timeout found across all imports (Go duration string, e.g. "4h")
ImportedFiles []string // List of imported file paths (for manifest)
AgentFile string // Path to custom agent file (if imported)
AgentImportSpec string // Original import specification for agent file (e.g., "owner/repo/path@ref")
RepositoryImports []string // List of repository imports (format: "owner/repo@ref") for .github folder merging
// ImportInputs uses map[string]any because input values can be different types (string, number, boolean).
// This is parsed from YAML frontmatter where the structure is dynamic and not known at compile time.
// This is an appropriate use of 'any' for dynamic YAML/JSON data.
Expand Down
25 changes: 25 additions & 0 deletions pkg/parser/schemas/main_workflow_schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -10374,6 +10374,31 @@
},
"required": ["id", "display-name"],
"additionalProperties": false
},
{
"type": "object",
"description": "MCP gateway configuration for shared workflows. Declares engine.mcp settings (tool-timeout, session-timeout) that consumers inherit during import without specifying an engine identifier. The engine is always inherited from the importing workflow.",
"properties": {
"mcp": {
"type": "object",
"description": "Engine-level MCP gateway configuration. Settings here apply to the MCP gateway used by this engine.",
"properties": {
"session-timeout": {
"type": "string",
"description": "Session timeout for MCP gateway sessions as a Go duration string (e.g. \"30m\", \"4h\", \"24h\"). Must be at least 5m (no upper bound). Omitted or empty uses the effective gateway default (precedence: this field > MCP_GATEWAY_SESSION_TIMEOUT env var > built-in default 6h).",
"examples": ["30m", "1h", "4h", "6h", "12h"]
},
"tool-timeout": {
"type": "string",
"description": "Timeout for individual MCP tool calls as a Go duration string (e.g. \"30s\", \"2m\", \"10m\"). Must be between 10s and 600s inclusive. Omitted or empty uses the gateway built-in default (60s). Use a higher value for slow MCP backends such as full-text search over large indexes.",
"examples": ["30s", "2m", "5m", "10m"]
}
},
"additionalProperties": false
}
},
"required": ["mcp"],
"additionalProperties": false
}
]
},
Expand Down
Loading