-
Notifications
You must be signed in to change notification settings - Fork 296
Description
Summary
On gh-aw v0.58.3, a workflow that uses consolidated safe_outputs plus call-workflow can still fail in the Process Safe Outputs step with No handler loaded for type 'call_workflow'. Current main already contains the adjacent HTTP registration fix from #21074 / #21124, but the consolidated safe-output handler-manager path is still missing call_workflow wiring in both the compiler and runtime. This means the feature can compile worker fan-out jobs and MCP tools, but the runtime step that consumes agent messages still skips the selected call_workflow message instead of invoking actions/setup/js/call_workflow.cjs.
Root Cause
This is not a duplicate of #21074. That issue fixed HTTP MCP tool registration for generated _call_workflow_name tools. The remaining bug is later in the execution pipeline.
There are three missing integration points on current main:
-
actions/setup/js/safe_output_handler_manager.cjsHANDLER_MAPincludesdispatch_workflowbut does not includecall_workflow.loadHandlers()only iteratesHANDLER_MAP, so it never requires./call_workflow.cjseven whenconfig.call_workflowis present.processMessages()then reaches the fallback branch and records:No handler loaded for type 'call_workflow'
-
pkg/workflow/compiler_safe_outputs_config.goaddHandlerManagerConfigEnvVar()buildsGH_AW_SAFE_OUTPUTS_HANDLER_CONFIGby iteratinghandlerRegistry.handlerRegistryhas a builder fordispatch_workflowbut no builder forcall_workflow.- Result: compiled workflows do not emit
call_workflowintoGH_AW_SAFE_OUTPUTS_HANDLER_CONFIG, so even if the runtime map were fixed, the handler would still not be enabled from compiled config.
-
pkg/workflow/compiler_safe_outputs_job.gohasHandlerManagerTypesincludesDispatchWorkflowbut does not includeCallWorkflow.- Result: a workflow that only routes via
call-workflowcan compile without anyProcess Safe Outputsstep at all, despitecall_workflow.cjsbeing the runtime bridge that setscall_workflow_nameandcall_workflow_payload.
Relevant current-source evidence:
// actions/setup/js/safe_output_handler_manager.cjs
const HANDLER_MAP = {
create_issue: "./create_issue.cjs",
...
dispatch_workflow: "./dispatch_workflow.cjs",
create_missing_tool_issue: "./create_missing_tool_issue.cjs",
missing_tool: "./missing_tool.cjs",
...
};// actions/setup/js/safe_output_handler_manager.cjs
if (!handler) {
core.warning(
`⚠️ No handler loaded for message type '${messageType}' (message ${i + 1}/${messages.length}). The message will be skipped...`
);
results.push({
type: messageType,
messageIndex: i,
success: false,
error: `No handler loaded for type '${messageType}'`,
});
continue;
}// pkg/workflow/compiler_safe_outputs_job.go
hasHandlerManagerTypes :=
data.SafeOutputs.CreateIssues != nil ||
...
data.SafeOutputs.DispatchWorkflow != nil ||
data.SafeOutputs.CreateCodeScanningAlerts != nil ||
data.SafeOutputs.AutofixCodeScanningAlert != nil ||
data.SafeOutputs.MissingTool != nil ||
data.SafeOutputs.MissingData != nil// pkg/workflow/compiler_safe_outputs_config.go
var handlerRegistry = map[string]handlerBuilder{
"create_issue": ...,
...
"dispatch_workflow": func(cfg *SafeOutputsConfig) map[string]any {
...
},
// missing: "call_workflow"
}For contrast, the already-fixed adjacent path on current main now does recognize _call_workflow_name in actions/setup/js/safe_outputs_mcp_server_http.cjs, so tool registration is no longer the blocker:
const isCallWorkflowTool = tool._call_workflow_name && typeof tool._call_workflow_name === "string" && tool._call_workflow_name.length > 0;
...
} else if (isCallWorkflowTool) {
logger.debug(`Found call_workflow tool: ${tool.name} (_call_workflow_name: ${tool._call_workflow_name})`);
if (!safeOutputsConfig.call_workflow) {
...
continue;
}
logger.debug(` call_workflow config exists, registering tool`);
}Affected Code
Broken compiled/runtime behavior appears in two ways.
1. Compiled handler-manager config omits call_workflow
Current compiler logic can emit a safe_outputs job and even generate conditional fan-out jobs, but the handler-manager config is missing the type that runtime needs to load:
# compiled safe_outputs job excerpt
- name: Process Safe Outputs
id: process_safe_outputs
env:
GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: '{"add_comment":{"max":1}}'
# BUG: call_workflow is omitted even when safe-outputs.call-workflow is configuredExpected shape:
- name: Process Safe Outputs
id: process_safe_outputs
env:
GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: '{"add_comment":{"max":1},"call_workflow":{"max":1,"workflows":["worker-a"],"workflow_files":{"worker-a":"./.github/workflows/worker-a.lock.yml"}}}'2. Runtime handler-manager skips the message even though call_workflow.cjs exists
actions/setup/js/call_workflow.cjs is already the correct runtime implementation for consolidated fan-out selection:
// actions/setup/js/call_workflow.cjs
core.setOutput("call_workflow_name", workflowName);
core.setOutput("call_workflow_payload", payloadJson);
return {
success: true,
workflow_name: workflowName,
payload: payloadJson,
};But safe_output_handler_manager.cjs never loads it, so the message falls through to the no-handler path instead of invoking this module.
Proposed Fix
Wire call_workflow into the same consolidated path that already handles dispatch_workflow.
1. Add call_workflow to the handler-manager runtime map
Replace the relevant section in actions/setup/js/safe_output_handler_manager.cjs with:
const HANDLER_MAP = {
create_issue: "./create_issue.cjs",
add_comment: "./add_comment.cjs",
create_discussion: "./create_discussion.cjs",
close_issue: "./close_issue.cjs",
close_discussion: "./close_discussion.cjs",
add_labels: "./add_labels.cjs",
remove_labels: "./remove_labels.cjs",
update_issue: "./update_issue.cjs",
update_discussion: "./update_discussion.cjs",
link_sub_issue: "./link_sub_issue.cjs",
update_release: "./update_release.cjs",
create_pull_request_review_comment: "./create_pr_review_comment.cjs",
submit_pull_request_review: "./submit_pr_review.cjs",
reply_to_pull_request_review_comment: "./reply_to_pr_review_comment.cjs",
resolve_pull_request_review_thread: "./resolve_pr_review_thread.cjs",
create_pull_request: "./create_pull_request.cjs",
push_to_pull_request_branch: "./push_to_pull_request_branch.cjs",
update_pull_request: "./update_pull_request.cjs",
close_pull_request: "./close_pull_request.cjs",
mark_pull_request_as_ready_for_review: "./mark_pull_request_as_ready_for_review.cjs",
hide_comment: "./hide_comment.cjs",
set_issue_type: "./set_issue_type.cjs",
add_reviewer: "./add_reviewer.cjs",
assign_milestone: "./assign_milestone.cjs",
assign_to_user: "./assign_to_user.cjs",
unassign_from_user: "./unassign_from_user.cjs",
create_code_scanning_alert: "./create_code_scanning_alert.cjs",
autofix_code_scanning_alert: "./autofix_code_scanning_alert.cjs",
dispatch_workflow: "./dispatch_workflow.cjs",
call_workflow: "./call_workflow.cjs",
create_missing_tool_issue: "./create_missing_tool_issue.cjs",
missing_tool: "./missing_tool.cjs",
create_missing_data_issue: "./create_missing_data_issue.cjs",
missing_data: "./missing_data.cjs",
noop: "./noop_handler.cjs",
create_project: "./create_project.cjs",
create_project_status_update: "./create_project_status_update.cjs",
update_project: "./update_project.cjs",
};2. Add call_workflow to compiler handler-manager config generation
Add a call_workflow builder adjacent to dispatch_workflow in pkg/workflow/compiler_safe_outputs_config.go:
"call_workflow": func(cfg *SafeOutputsConfig) map[string]any {
if cfg.CallWorkflow == nil {
return nil
}
c := cfg.CallWorkflow
builder := newHandlerConfigBuilder().
AddTemplatableInt("max", c.Max).
AddStringSlice("workflows", c.Workflows)
if len(c.WorkflowFiles) > 0 {
builder.AddDefault("workflow_files", c.WorkflowFiles)
}
return builder.Build()
},3. Ensure the handler-manager step is generated when call-workflow is configured
Update pkg/workflow/compiler_safe_outputs_job.go:
hasHandlerManagerTypes :=
data.SafeOutputs.CreateIssues != nil ||
data.SafeOutputs.CreateDiscussions != nil ||
data.SafeOutputs.CloseIssues != nil ||
data.SafeOutputs.CloseDiscussions != nil ||
data.SafeOutputs.AddLabels != nil ||
data.SafeOutputs.RemoveLabels != nil ||
data.SafeOutputs.UpdateIssues != nil ||
data.SafeOutputs.UpdateDiscussions != nil ||
data.SafeOutputs.LinkSubIssues != nil ||
data.SafeOutputs.UpdateRelease != nil ||
data.SafeOutputs.CreatePullRequestReviewComments != nil ||
data.SafeOutputs.SubmitPullRequestReview != nil ||
data.SafeOutputs.ReplyToPullRequestReviewComments != nil ||
data.SafeOutputs.ResolvePullRequestReviewThreads != nil ||
data.SafeOutputs.CreatePullRequests != nil ||
data.SafeOutputs.PushToPullRequestBranch != nil ||
data.SafeOutputs.UpdatePullRequests != nil ||
data.SafeOutputs.ClosePullRequests != nil ||
data.SafeOutputs.MarkPullRequestAsReadyForReview != nil ||
data.SafeOutputs.HideComment != nil ||
data.SafeOutputs.SetIssueType != nil ||
data.SafeOutputs.AddReviewer != nil ||
data.SafeOutputs.AssignMilestone != nil ||
data.SafeOutputs.AssignToUser != nil ||
data.SafeOutputs.UnassignFromUser != nil ||
data.SafeOutputs.DispatchWorkflow != nil ||
data.SafeOutputs.CallWorkflow != nil ||
data.SafeOutputs.CreateCodeScanningAlerts != nil ||
data.SafeOutputs.AutofixCodeScanningAlert != nil ||
data.SafeOutputs.MissingTool != nil ||
data.SafeOutputs.MissingData != nilThis keeps the execution model consistent with the existing design:
- MCP server registers workflow-specific
call_workflowtools - agent emits a
call_workflowmessage - consolidated handler manager loads
call_workflow.cjs call_workflow.cjssetscall_workflow_name/call_workflow_payload- compiler-generated conditional
uses:jobs consume those outputs
Implementation Plan
-
Update runtime handler loading
- Edit
actions/setup/js/safe_output_handler_manager.cjs. - Add
call_workflow: "./call_workflow.cjs"toHANDLER_MAP. - Keep
call_workflowin the handler-manager path, notSTANDALONE_STEP_TYPES, becausecall_workflow.cjsis the module that sets the downstream action outputs.
- Edit
-
Update compiler handler config generation
- Edit
pkg/workflow/compiler_safe_outputs_config.go. - Add a
handlerRegistry["call_workflow"]builder that emits:maxworkflowsworkflow_files
- Do not add
target-repo/target-reffields here; those belong todispatch_workflow, notcall_workflow.
- Edit
-
Update consolidated safe_outputs job gating
- Edit
pkg/workflow/compiler_safe_outputs_job.go. - Include
data.SafeOutputs.CallWorkflow != nilinhasHandlerManagerTypesso workflows that only route viacall-workflowstill compile aProcess Safe Outputsstep.
- Edit
-
Add compiler regression tests
- Edit
pkg/workflow/compiler_safe_outputs_config_test.go. - Add
TestAddHandlerManagerConfigEnvVar_CallWorkflowasserting thatGH_AW_SAFE_OUTPUTS_HANDLER_CONFIGcontainscall_workflow,workflows, andworkflow_fileswhenSafeOutputs.CallWorkflowis configured. - Edit
pkg/workflow/safe_outputs_call_workflow_test.goorpkg/workflow/compiler_safe_outputs_job_test.go. - Add
TestCallWorkflowOnly_UsesHandlerManagerStepasserting that a workflow with onlycall-workflowstill includes theProcess Safe Outputsstep.
- Edit
-
Add runtime regression tests
- Edit
actions/setup/js/safe_output_handler_manager.test.cjs. - Add
it("loads call_workflow handler when config.call_workflow is present", ...)that callsloadHandlers()with{ call_workflow: { workflows: ["worker-a"], max: 1 } }and asserts the returned map containscall_workflow. - Add
it("processes call_workflow messages without no-handler warnings", ...)that uses a real loaded handler or a stubbed handler map entry and asserts there is noNo handler loaded for type 'call_workflow'result.
- Edit
-
Add end-to-end compile/runtime coverage for the exact bug shape
- Extend
pkg/workflow/call_workflow_compilation_test.gowith a case that compiles a workflow containing only:safe-outputs.call-workflow
- Assert the compiled lock file contains:
Process Safe OutputsGH_AW_SAFE_OUTPUTS_HANDLER_CONFIGcall_workflow- downstream
call-<worker>fan-out jobs
- Extend
-
Validate with the project-standard finish step
- Run
make testat minimum. - Prefer
make agent-finishbefore closing the issue implementation, since this change spans both Go compiler output and JS runtime behavior.
- Run
-
No duplicate issue; reference adjacent fixes instead
Reproduction
- Use
gh-aw v0.58.3. - Create a reusable “gateway” workflow in
<org>/<platform-repo>with:on: workflow_call- at least one ordinary consolidated safe output (for example
add-comment) or onlycall-workflow safe-outputs.call-workflowpointing at a reusable worker workflow in the same repository
- Compile the workflow and install the generated lock file into an application repository.
- Trigger the gateway from another repository via
workflow_callso the agent selects one of the generatedcall_workflowtools. - Observe the
Process Safe Outputsstep fail or warn instead of routing the worker selection. - The exact failing step/error from the reproduced run was:
- Step:
Process Safe Outputs - Error:
No handler loaded for type 'call_workflow'
- Step:
- Private failing run reference from the original investigation:
- run
23137133379 - job
67204124568 - omitted raw URL because the source repository is private
- run
- Compare with current
mainsource:- HTTP MCP registration already recognizes
_call_workflow_name - consolidated handler manager still does not load
call_workflow - compiler still does not emit
call_workflowintoGH_AW_SAFE_OUTPUTS_HANDLER_CONFIG
- HTTP MCP registration already recognizes