Skip to content

Commit 93dcf0e

Browse files
Copilotdsyme
andauthored
Fix call_workflow samples replay dropping workflow_name during ingestion (#58113)
* Initial plan * Add call_workflow validation config to preserve workflow_name during ingestion Co-authored-by: dsyme <7204669+dsyme@users.noreply.github.com> * Add regression tests and recompile smoke-call-workflow lock file Co-authored-by: dsyme <7204669+dsyme@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: dsyme <7204669+dsyme@users.noreply.github.com>
1 parent b4fadaa commit 93dcf0e

4 files changed

Lines changed: 95 additions & 0 deletions

File tree

.github/workflows/smoke-call-workflow.lock.yml

Lines changed: 17 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

actions/setup/js/collect_ndjson_output.test.cjs

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,13 @@ describe("collect_ndjson_output.cjs", () => {
133133
inputs: { type: "object" },
134134
},
135135
},
136+
call_workflow: {
137+
defaultMax: 1,
138+
fields: {
139+
workflow_name: { required: !0, type: "string", sanitize: !0, minLength: 1, maxLength: 256, pattern: ".*\\S.*", patternError: "must not be empty" },
140+
inputs: { type: "object" },
141+
},
142+
},
136143
})
137144
));
138145
}),
@@ -261,6 +268,28 @@ describe("collect_ndjson_output.cjs", () => {
261268
const parsedOutput = JSON.parse(outputCall[1]);
262269
expect(parsedOutput.errors).toHaveLength(1);
263270
}),
271+
it("should preserve call_workflow workflow_name and inputs during ingestion (regression for github/gh-aw#55176)", async () => {
272+
// Samples-mode replay (and the live dynamic call_workflow MCP tool) emits a
273+
// canonical message with both workflow_name and inputs set. Before the fix,
274+
// call_workflow had no ValidationConfig entry, so ingestion fell back to
275+
// validateItemWithSafeJobConfig, which dropped every field except "type"
276+
// because the call_workflow safe-outputs config lacks an "inputs" key.
277+
const testFile = "/tmp/gh-aw/test-ndjson-output.txt",
278+
ndjsonContent = '{"type": "call_workflow", "workflow_name": "test-copilot-call-worker", "inputs": {"sentinel": "hello-sentinel"}}';
279+
(fs.writeFileSync(testFile, ndjsonContent), (process.env.GH_AW_SAFE_OUTPUTS = testFile));
280+
const __config = '{"call_workflow":{"max":1,"workflows":["test-copilot-call-worker"],"workflow_files":{"test-copilot-call-worker":"./.github/workflows/test-copilot-call-worker.lock.yml"}}}',
281+
configPath = "/tmp/gh-aw/safeoutputs/config.json";
282+
(fs.mkdirSync("/tmp/gh-aw/safeoutputs", { recursive: !0 }), fs.writeFileSync(configPath, __config), await eval(`(async () => { ${collectScript}; await main(); })()`));
283+
const setOutputCalls = mockCore.setOutput.mock.calls,
284+
outputCall = setOutputCalls.find(call => "output" === call[0]);
285+
expect(outputCall).toBeDefined();
286+
const parsedOutput = JSON.parse(outputCall[1]);
287+
(expect(parsedOutput.items).toHaveLength(1),
288+
expect(parsedOutput.items[0].type).toBe("call_workflow"),
289+
expect(parsedOutput.items[0].workflow_name).toBe("test-copilot-call-worker"),
290+
expect(parsedOutput.items[0].inputs).toEqual({ sentinel: "hello-sentinel" }),
291+
expect(parsedOutput.errors).toHaveLength(0));
292+
}),
264293
it("should preserve Slack mrkdwn links in custom safe-job string inputs", async () => {
265294
const testFile = "/tmp/gh-aw/test-ndjson-output.txt";
266295
const slackText = "Tracking issue: <https://github.com/octo-org/octo-repo/issues/123|Build failure — Build github/gh-aw#456>";

pkg/workflow/safe_output_validation_config_test.go

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,48 @@ func TestApproveWorkflowRunValidationConfig(t *testing.T) {
102102
}
103103
}
104104

105+
// TestCallWorkflowValidationConfigPreservesWorkflowName is a regression test for
106+
// samples-mode call-workflow replay dropping the workflow name (github/gh-aw#55176):
107+
// without a "call_workflow" entry in ValidationConfig, collect_ndjson_output.cjs
108+
// fell back to validateItemWithSafeJobConfig, which drops every field except
109+
// "type" because the call_workflow safe-outputs config has no "inputs" key. This
110+
// test verifies the compiler emits a validation config that declares
111+
// "workflow_name" (required) and "inputs" for call_workflow, mirroring
112+
// dispatch_workflow, so the field survives ingestion.
113+
func TestCallWorkflowValidationConfigPreservesWorkflowName(t *testing.T) {
114+
config, ok := ValidationConfig["call_workflow"]
115+
if !ok {
116+
t.Fatal("call_workflow not found in ValidationConfig")
117+
}
118+
if config.DefaultMax != 1 {
119+
t.Errorf("call_workflow DefaultMax = %d, want 1", config.DefaultMax)
120+
}
121+
workflowName, ok := config.Fields["workflow_name"]
122+
if !ok || !workflowName.Required || workflowName.Type != "string" {
123+
t.Errorf("call_workflow workflow_name = %+v, want required string", workflowName)
124+
}
125+
inputs, ok := config.Fields["inputs"]
126+
if !ok || inputs.Type != "object" {
127+
t.Errorf("call_workflow inputs = %+v, want object", inputs)
128+
}
129+
130+
jsonStr, err := GetValidationConfigJSONWithDataSchema([]string{"call_workflow"}, nil, false, nil)
131+
if err != nil {
132+
t.Fatalf("GetValidationConfigJSONWithDataSchema() error = %v", err)
133+
}
134+
var parsed map[string]TypeValidationConfig
135+
if err := json.Unmarshal([]byte(jsonStr), &parsed); err != nil {
136+
t.Fatalf("Failed to parse validation config JSON: %v", err)
137+
}
138+
parsedConfig, ok := parsed["call_workflow"]
139+
if len(parsed) != 1 || !ok || parsedConfig.DefaultMax != 1 {
140+
t.Errorf("call_workflow validation config = %#v, want defaultMax 1", parsedConfig)
141+
}
142+
if workflowName := parsedConfig.Fields["workflow_name"]; !workflowName.Required || workflowName.Type != "string" {
143+
t.Errorf("generated call_workflow workflow_name = %+v, want required string", workflowName)
144+
}
145+
}
146+
105147
func TestDismissPullRequestReviewValidationConfigSupportsAutoReviewID(t *testing.T) {
106148
config, ok := ValidationConfig["dismiss_pull_request_review"]
107149
if !ok {

pkg/workflow/safe_outputs_validation_config.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -450,6 +450,13 @@ var ValidationConfig = map[string]TypeValidationConfig{
450450
"ref": {Type: "string", MinLength: 1, MaxLength: 256, Pattern: "^[^\\x00-\\x20\\x7f~^:?*\\[\\\\]+$", PatternError: "must be a valid git ref"},
451451
},
452452
},
453+
"call_workflow": {
454+
DefaultMax: 1,
455+
Fields: map[string]FieldValidation{
456+
"workflow_name": {Required: true, Type: "string", Sanitize: true, MinLength: 1, MaxLength: 256, Pattern: ".*\\S.*", PatternError: "must not be empty"},
457+
"inputs": {Type: "object"},
458+
},
459+
},
453460
"missing_tool": {
454461
DefaultMax: 20,
455462
Fields: map[string]FieldValidation{

0 commit comments

Comments
 (0)