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/aw/test-dispatcher.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
---
on: issues
engine: copilot
permissions:
contents: read
issues: read
safe-outputs:
dispatch-workflow:
workflows:
- test-workflow
max: 1
---

# Test Dispatcher Workflow

This workflow demonstrates the dispatch-workflow safe output capability.
The agent can trigger the test-workflow using the test_workflow tool.
1,022 changes: 1,022 additions & 0 deletions .github/workflows/test-dispatcher.lock.yml

Large diffs are not rendered by default.

17 changes: 17 additions & 0 deletions .github/workflows/test-dispatcher.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
---
on: issues
engine: copilot
permissions:
contents: read
issues: read
safe-outputs:
dispatch-workflow:
workflows:
- test-workflow
max: 1
---

# Test Dispatcher Workflow

This workflow demonstrates the dispatch-workflow safe output capability.
The agent can trigger the test-workflow using the test_workflow tool.
15 changes: 15 additions & 0 deletions .github/workflows/test-workflow.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
name: Test Workflow
on:
workflow_dispatch:
inputs:
test_param:
description: 'Test parameter'
type: string
required: false
permissions:
contents: read
jobs:
test:
runs-on: ubuntu-latest
steps:
- run: echo "Test workflow"
9 changes: 9 additions & 0 deletions actions/setup/js/safe_outputs_tools_loader.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -86,8 +86,17 @@ function attachHandlers(tools, handlers) {
*/
function registerPredefinedTools(server, tools, config, registerTool, normalizeTool) {
tools.forEach(tool => {
// Check if this is a regular tool matching a config key
if (Object.keys(config).find(configKey => normalizeTool(configKey) === tool.name)) {
registerTool(server, tool);
return;
}

// Check if this is a dispatch_workflow tool (has _workflow_name metadata)
// These tools are dynamically generated with workflow-specific names
if (tool._workflow_name && config.dispatch_workflow) {
registerTool(server, tool);
return;
}
});
}
Expand Down
112 changes: 112 additions & 0 deletions actions/setup/js/safe_outputs_tools_loader.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,56 @@ describe("safe_outputs_tools_loader", () => {

expect(result[0].handler).toBeUndefined();
});

it("should attach dispatch_workflow handler for tools with _workflow_name", () => {
const tools = [{ name: "test_workflow", description: "Test workflow", _workflow_name: "test-workflow" }];
const defaultHandler = vi.fn(type => vi.fn());
const handlers = {
createPullRequestHandler: vi.fn(),
pushToPullRequestBranchHandler: vi.fn(),
uploadAssetHandler: vi.fn(),
defaultHandler: defaultHandler,
};

const result = attachHandlers(tools, handlers);

// Handler should be attached
expect(result[0].handler).toBeDefined();
expect(typeof result[0].handler).toBe("function");

// Call the handler to verify it uses dispatch_workflow type
const mockArgs = { test_param: "value" };
result[0].handler(mockArgs);

// Verify defaultHandler was called with dispatch_workflow type
expect(defaultHandler).toHaveBeenCalledWith("dispatch_workflow");
});

it("should include workflow_name in dispatch_workflow handler args", () => {
const tools = [{ name: "ci_workflow", description: "CI workflow", _workflow_name: "ci" }];
const mockHandlerFunction = vi.fn();
const defaultHandler = vi.fn(() => mockHandlerFunction);
const handlers = {
createPullRequestHandler: vi.fn(),
pushToPullRequestBranchHandler: vi.fn(),
uploadAssetHandler: vi.fn(),
defaultHandler: defaultHandler,
};

const result = attachHandlers(tools, handlers);

// Call the handler
const mockArgs = { input1: "value1" };
result[0].handler(mockArgs);

// Verify the handler function was called with workflow_name
expect(mockHandlerFunction).toHaveBeenCalledWith(
expect.objectContaining({
workflow_name: "ci",
input1: "value1",
})
);
});
});

describe("registerPredefinedTools", () => {
Expand Down Expand Up @@ -210,6 +260,68 @@ describe("safe_outputs_tools_loader", () => {

expect(registerTool).not.toHaveBeenCalled();
});

it("should register dispatch_workflow tools with _workflow_name metadata", () => {
const tools = [
{ name: "test_workflow", description: "Test workflow", _workflow_name: "test-workflow" },
{ name: "ci_workflow", description: "CI workflow", _workflow_name: "ci" },
{ name: "other_tool", description: "Other tool" },
];
const config = {
dispatch_workflow: {
workflows: ["test-workflow", "ci"],
max: 2,
},
};
const registerTool = vi.fn();
const normalizeTool = name => name.replace(/-/g, "_");

registerPredefinedTools(mockServer, tools, config, registerTool, normalizeTool);

// Should register both dispatch_workflow tools
expect(registerTool).toHaveBeenCalledTimes(2);
expect(registerTool).toHaveBeenCalledWith(mockServer, tools[0]);
expect(registerTool).toHaveBeenCalledWith(mockServer, tools[1]);
// Should NOT register the tool without _workflow_name
expect(registerTool).not.toHaveBeenCalledWith(mockServer, tools[2]);
});

it("should not register dispatch_workflow tools when dispatch_workflow is not in config", () => {
const tools = [{ name: "test_workflow", description: "Test workflow", _workflow_name: "test-workflow" }];
const config = {
create_issue: true,
};
const registerTool = vi.fn();
const normalizeTool = name => name.replace(/-/g, "_");

registerPredefinedTools(mockServer, tools, config, registerTool, normalizeTool);

// Should not register dispatch_workflow tool when config doesn't include it
expect(registerTool).not.toHaveBeenCalled();
});

it("should register both regular and dispatch_workflow tools", () => {
const tools = [
{ name: "create_pull_request", description: "Create PR" },
{ name: "test_workflow", description: "Test workflow", _workflow_name: "test-workflow" },
];
const config = {
create_pull_request: true,
dispatch_workflow: {
workflows: ["test-workflow"],
max: 1,
},
};
const registerTool = vi.fn();
const normalizeTool = name => name.replace(/-/g, "_");

registerPredefinedTools(mockServer, tools, config, registerTool, normalizeTool);

// Should register both the regular tool and dispatch_workflow tool
expect(registerTool).toHaveBeenCalledTimes(2);
expect(registerTool).toHaveBeenCalledWith(mockServer, tools[0]);
expect(registerTool).toHaveBeenCalledWith(mockServer, tools[1]);
});
});

describe("registerDynamicTools", () => {
Expand Down
1 change: 1 addition & 0 deletions docs/src/content/docs/agent-factory-status.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,7 @@ These are experimental agentic workflows used by the GitHub Next team to learn,
| [Super Linter Report](https://github.com/githubnext/gh-aw/blob/main/.github/workflows/super-linter.md) | copilot | [![Super Linter Report](https://github.com/githubnext/gh-aw/actions/workflows/super-linter.lock.yml/badge.svg)](https://github.com/githubnext/gh-aw/actions/workflows/super-linter.lock.yml) | `0 14 * * 1-5` | - |
| [Terminal Stylist](https://github.com/githubnext/gh-aw/blob/main/.github/workflows/terminal-stylist.md) | copilot | [![Terminal Stylist](https://github.com/githubnext/gh-aw/actions/workflows/terminal-stylist.lock.yml/badge.svg)](https://github.com/githubnext/gh-aw/actions/workflows/terminal-stylist.lock.yml) | - | - |
| [Test Create PR Error Handling](https://github.com/githubnext/gh-aw/blob/main/.github/workflows/test-create-pr-error-handling.md) | claude | [![Test Create PR Error Handling](https://github.com/githubnext/gh-aw/actions/workflows/test-create-pr-error-handling.lock.yml/badge.svg)](https://github.com/githubnext/gh-aw/actions/workflows/test-create-pr-error-handling.lock.yml) | - | - |
| [Test Dispatcher Workflow](https://github.com/githubnext/gh-aw/blob/main/.github/workflows/test-dispatcher.md) | copilot | [![Test Dispatcher Workflow](https://github.com/githubnext/gh-aw/actions/workflows/test-dispatcher.lock.yml/badge.svg)](https://github.com/githubnext/gh-aw/actions/workflows/test-dispatcher.lock.yml) | - | - |
| [Test Project URL Default](https://github.com/githubnext/gh-aw/blob/main/.github/workflows/test-project-url-default.md) | copilot | [![Test Project URL Default](https://github.com/githubnext/gh-aw/actions/workflows/test-project-url-default.lock.yml/badge.svg)](https://github.com/githubnext/gh-aw/actions/workflows/test-project-url-default.lock.yml) | - | - |
| [Test YAML Import](https://github.com/githubnext/gh-aw/blob/main/.github/workflows/test-yaml-import.md) | copilot | [![Test YAML Import](https://github.com/githubnext/gh-aw/actions/workflows/test-yaml-import.lock.yml/badge.svg)](https://github.com/githubnext/gh-aw/actions/workflows/test-yaml-import.lock.yml) | - | - |
| [The Daily Repository Chronicle](https://github.com/githubnext/gh-aw/blob/main/.github/workflows/daily-repo-chronicle.md) | copilot | [![The Daily Repository Chronicle](https://github.com/githubnext/gh-aw/actions/workflows/daily-repo-chronicle.lock.yml/badge.svg)](https://github.com/githubnext/gh-aw/actions/workflows/daily-repo-chronicle.lock.yml) | `0 16 * * 1-5` | - |
Expand Down
Loading