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
17 changes: 17 additions & 0 deletions .github/workflows/smoke-call-workflow.lock.yml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

29 changes: 29 additions & 0 deletions actions/setup/js/collect_ndjson_output.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,13 @@ describe("collect_ndjson_output.cjs", () => {
inputs: { type: "object" },
},
},
call_workflow: {
defaultMax: 1,
fields: {
workflow_name: { required: !0, type: "string", sanitize: !0, minLength: 1, maxLength: 256, pattern: ".*\\S.*", patternError: "must not be empty" },
inputs: { type: "object" },
},
},
})
));
}),
Expand Down Expand Up @@ -261,6 +268,28 @@ describe("collect_ndjson_output.cjs", () => {
const parsedOutput = JSON.parse(outputCall[1]);
expect(parsedOutput.errors).toHaveLength(1);
}),
it("should preserve call_workflow workflow_name and inputs during ingestion (regression for github/gh-aw#55176)", async () => {
// Samples-mode replay (and the live dynamic call_workflow MCP tool) emits a
// canonical message with both workflow_name and inputs set. Before the fix,
// call_workflow had no ValidationConfig entry, so ingestion fell back to
// validateItemWithSafeJobConfig, which dropped every field except "type"
// because the call_workflow safe-outputs config lacks an "inputs" key.
const testFile = "/tmp/gh-aw/test-ndjson-output.txt",
ndjsonContent = '{"type": "call_workflow", "workflow_name": "test-copilot-call-worker", "inputs": {"sentinel": "hello-sentinel"}}';
Comment thread
dsyme marked this conversation as resolved.
(fs.writeFileSync(testFile, ndjsonContent), (process.env.GH_AW_SAFE_OUTPUTS = testFile));
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"}}}',
configPath = "/tmp/gh-aw/safeoutputs/config.json";
(fs.mkdirSync("/tmp/gh-aw/safeoutputs", { recursive: !0 }), fs.writeFileSync(configPath, __config), await eval(`(async () => { ${collectScript}; await main(); })()`));
const setOutputCalls = mockCore.setOutput.mock.calls,
outputCall = setOutputCalls.find(call => "output" === call[0]);
expect(outputCall).toBeDefined();
const parsedOutput = JSON.parse(outputCall[1]);
(expect(parsedOutput.items).toHaveLength(1),
expect(parsedOutput.items[0].type).toBe("call_workflow"),
expect(parsedOutput.items[0].workflow_name).toBe("test-copilot-call-worker"),
expect(parsedOutput.items[0].inputs).toEqual({ sentinel: "hello-sentinel" }),
expect(parsedOutput.errors).toHaveLength(0));
}),
it("should preserve Slack mrkdwn links in custom safe-job string inputs", async () => {
const testFile = "/tmp/gh-aw/test-ndjson-output.txt";
const slackText = "Tracking issue: <https://github.com/octo-org/octo-repo/issues/123|Build failure — Build github/gh-aw#456>";
Expand Down
42 changes: 42 additions & 0 deletions pkg/workflow/safe_output_validation_config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,48 @@ func TestApproveWorkflowRunValidationConfig(t *testing.T) {
}
}

// TestCallWorkflowValidationConfigPreservesWorkflowName is a regression test for
// samples-mode call-workflow replay dropping the workflow name (github/gh-aw#55176):
// without a "call_workflow" entry in ValidationConfig, collect_ndjson_output.cjs
// fell back to validateItemWithSafeJobConfig, which drops every field except
// "type" because the call_workflow safe-outputs config has no "inputs" key. This
// test verifies the compiler emits a validation config that declares
// "workflow_name" (required) and "inputs" for call_workflow, mirroring
// dispatch_workflow, so the field survives ingestion.
func TestCallWorkflowValidationConfigPreservesWorkflowName(t *testing.T) {
config, ok := ValidationConfig["call_workflow"]
if !ok {
t.Fatal("call_workflow not found in ValidationConfig")
}
if config.DefaultMax != 1 {
t.Errorf("call_workflow DefaultMax = %d, want 1", config.DefaultMax)
}
workflowName, ok := config.Fields["workflow_name"]
if !ok || !workflowName.Required || workflowName.Type != "string" {
t.Errorf("call_workflow workflow_name = %+v, want required string", workflowName)
}
inputs, ok := config.Fields["inputs"]
if !ok || inputs.Type != "object" {
t.Errorf("call_workflow inputs = %+v, want object", inputs)
}

jsonStr, err := GetValidationConfigJSONWithDataSchema([]string{"call_workflow"}, nil, false, nil)
if err != nil {
t.Fatalf("GetValidationConfigJSONWithDataSchema() error = %v", err)
}
var parsed map[string]TypeValidationConfig
if err := json.Unmarshal([]byte(jsonStr), &parsed); err != nil {
t.Fatalf("Failed to parse validation config JSON: %v", err)
}
parsedConfig, ok := parsed["call_workflow"]
if len(parsed) != 1 || !ok || parsedConfig.DefaultMax != 1 {
t.Errorf("call_workflow validation config = %#v, want defaultMax 1", parsedConfig)
}
if workflowName := parsedConfig.Fields["workflow_name"]; !workflowName.Required || workflowName.Type != "string" {
t.Errorf("generated call_workflow workflow_name = %+v, want required string", workflowName)
}
}

func TestDismissPullRequestReviewValidationConfigSupportsAutoReviewID(t *testing.T) {
config, ok := ValidationConfig["dismiss_pull_request_review"]
if !ok {
Expand Down
7 changes: 7 additions & 0 deletions pkg/workflow/safe_outputs_validation_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -450,6 +450,13 @@ var ValidationConfig = map[string]TypeValidationConfig{
"ref": {Type: "string", MinLength: 1, MaxLength: 256, Pattern: "^[^\\x00-\\x20\\x7f~^:?*\\[\\\\]+$", PatternError: "must be a valid git ref"},
},
},
"call_workflow": {
DefaultMax: 1,
Fields: map[string]FieldValidation{
"workflow_name": {Required: true, Type: "string", Sanitize: true, MinLength: 1, MaxLength: 256, Pattern: ".*\\S.*", PatternError: "must not be empty"},
"inputs": {Type: "object"},
},
},
"missing_tool": {
DefaultMax: 20,
Fields: map[string]FieldValidation{
Expand Down
Loading