Conversation
…h events Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> Agent-Logs-Url: https://github.com/github/gh-aw/sessions/a736f787-d406-4bb4-a907-eab2bc837100
dispatch_repository safe-output type for repository_dispatch events
|
@copilot add integration tests in pkg/cli/workflows to ensure the compilation is properly happening at all levels |
…g/cli Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> Agent-Logs-Url: https://github.com/github/gh-aw/sessions/af6e709f-e41e-4903-b24c-b758b791c058
Added 4 integration tests in
Warning Firewall rules blocked me from connecting to one or more addresses (expand for details)I tried to connect to the following addresses, but was blocked by firewall rules:
If you need me to access, download, or install something from one of these locations, you can either:
|
|
@copilot make sure github actions expression are also allowed. |
…h_repository Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> Agent-Logs-Url: https://github.com/github/gh-aw/sessions/dcbd0a0c-b195-4656-9d1f-0a7f80c4b49e
Added two integration tests in commit c6b5a47:
The validation code already skips format checking when Warning Firewall rules blocked me from connecting to one or more addresses (expand for details)I tried to connect to the following addresses, but was blocked by firewall rules:
If you need me to access, download, or install something from one of these locations, you can either:
|
|
Hey The PR checks all the boxes:
This PR looks ready for maintainer review. 🎉
|
|
@copilot make it expiremental |
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> Agent-Logs-Url: https://github.com/github/gh-aw/sessions/76ae354e-e256-438b-8b3f-784d8dc4f3dc
Done in commit 2b1dc42. Warning Firewall rules blocked me from connecting to one or more addresses (expand for details)I tried to connect to the following addresses, but was blocked by firewall rules:
If you need me to access, download, or install something from one of these locations, you can either:
|
dispatch_repository safe-output type for repository_dispatch eventsdispatch_repository safe-output type for repository_dispatch events (experimental)
There was a problem hiding this comment.
Pull request overview
Adds an experimental dispatch_repository safe-output type to enable agent tools that trigger GitHub repository_dispatch events against external repositories, with compile-time schema/validation and runtime handler wiring.
Changes:
- Go compiler support: parse/validate
dispatch_repository, generate dynamic MCP tools, serialize handler config, and emit an experimental-feature warning. - JSON Schema updates for
safe-outputs.dispatch_repositoryplus new canonical workflow + Go unit/integration tests. - JS runtime support: new
dispatch_repositoryhandler and registration in the safe-outputs MCP server/tool loader.
Reviewed changes
Copilot reviewed 19 out of 19 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| pkg/workflow/unified_prompt_step.go | Adds dispatch_repository to the prompt tools hint list when configured. |
| pkg/workflow/safe_outputs_tools_filtering.go | Generates dynamic dispatch_repository tool definitions (legacy + meta strategies). |
| pkg/workflow/safe_outputs_state.go | Wires DispatchRepository into safe-output state reflection mapping. |
| pkg/workflow/safe_outputs_config_generation.go | Serializes dispatch_repository runtime config (per-tool map) into handler config JSON. |
| pkg/workflow/safe_outputs_config.go | Parses dispatch_repository from frontmatter during safe-outputs extraction. |
| pkg/workflow/dispatch_repository_validation.go | New compiler validation for tool fields + repo slug format (with expression bypass). |
| pkg/workflow/dispatch_repository_test.go | Unit tests for parsing/validation/tool generation/config serialization + compilation. |
| pkg/workflow/dispatch_repository_experimental_warning_test.go | Integration tests asserting experimental warning emission. |
| pkg/workflow/dispatch_repository.go | New config structs + parser + dynamic tool schema generation. |
| pkg/workflow/compiler_types.go | Adds DispatchRepository to SafeOutputsConfig. |
| pkg/workflow/compiler_safe_outputs_config.go | Adds handler-config serialization entry for dispatch_repository. |
| pkg/workflow/compiler.go | Emits experimental warning + hooks compiler validation for dispatch_repository. |
| pkg/parser/schemas/main_workflow_schema.json | Adds JSON Schema for safe-outputs.dispatch_repository. |
| pkg/cli/workflows/test-copilot-dispatch-repository.md | Adds a canonical example workflow using dispatch_repository. |
| pkg/cli/compile_integration_test.go | Adds CLI-level compilation integration tests for dispatch_repository. |
| actions/setup/js/safe_outputs_tools_loader.cjs | Attaches/registers dynamic dispatch tools with _dispatch_repository_tool metadata. |
| actions/setup/js/safe_outputs_mcp_server_http.cjs | Registers dispatch_repository tools in HTTP server tool registration logic. |
| actions/setup/js/safe_output_handler_manager.cjs | Adds dispatch_repository to the handler module map. |
| actions/setup/js/dispatch_repository.cjs | New runtime handler that dispatches repository_dispatch events with allowlist + staged support. |
Comments suppressed due to low confidence (5)
actions/setup/js/dispatch_repository.cjs:105
- Cross-repo allowlist enforcement is effectively optional here: when
targetRepoSlug !== contextRepoSlugandallowed_repositoriesis empty/omitted, the code skipsvalidateTargetRepoentirely and will dispatch to the configuredrepository(or any message-provided repo). This differs from the SEC-005 default-deny behavior used indispatch_workflow.cjsand contradicts the intended “enforces cross-repo allowlist” contract.
Consider default-denying cross-repo dispatch when no allowlist is configured (return an E004-style error), or otherwise ensuring the resolved target is always validated against an explicit allowlist before calling the GitHub API.
// Validate cross-repo dispatch (SEC-005 pattern)
const isCrossRepo = targetRepoSlug !== contextRepoSlug;
if (isCrossRepo && allowedRepos.size > 0) {
const repoValidation = validateTargetRepo(targetRepoSlug, contextRepoSlug, allowedRepos);
if (!repoValidation.valid) {
core.warning(`dispatch_repository: cross-repo check failed for "${targetRepoSlug}": ${repoValidation.error}`);
return {
success: false,
error: `E004: ${repoValidation.error}`,
};
}
}
actions/setup/js/dispatch_repository.cjs:93
message.repositoryis treated as an override for choosing the dispatch target, but the dynamic tool wrapper added insafe_outputs_tools_loader.cjsonly forwards tool arguments undermessage.inputs(and the generated tool schema disallows extra properties). As a result, callers can’t actually setmessage.repository, and tools configured with onlyallowed_repositorieswill silently default to the first entry (which may be a glob likeorg/*and will then fail slug parsing).
To make multi-repo selection work, either read the target from message.inputs.repository (and add repository to the tool input schema), or update the wrapper to pass a top-level repository field and ensure the tool schema allows it.
// Resolve target repository
// Prefer message.repository > toolConfig.repository > first allowed_repository
const messageRepo = message.repository || "";
const configuredRepo = toolConfig.repository || "";
const allowedReposConfig = toolConfig.allowed_repositories || [];
const allowedRepos = parseAllowedRepos(allowedReposConfig);
let targetRepoSlug = messageRepo || configuredRepo;
if (!targetRepoSlug && allowedReposConfig.length > 0) {
// Default to first allowed repository if no specific target given
targetRepoSlug = allowedReposConfig[0];
}
if (!targetRepoSlug) {
core.warning(`dispatch_repository: no target repository for tool "${toolName}"`);
return {
success: false,
error: `E001: No target repository configured for tool "${toolName}"`,
};
}
actions/setup/js/dispatch_repository.cjs:70
maxis described/validated as capped at 50 at compile time, but at runtimemaxCountis parsed and used as-is. Ifmaxis templated (e.g.${{ inputs.max }}) and resolves to >50, this handler will permit more than 50 dispatches, bypassing the intended execution limit.
Consider capping maxCount to 50 in the handler after parsing (and potentially returning a validation error if it exceeds the cap) so the limit is enforced even when max is expression-driven.
const maxCount = typeof toolConfig.max === "number" ? toolConfig.max : parseInt(String(toolConfig.max || "1"), 10) || 1;
const currentCount = dispatchCounts[toolName] || 0;
if (currentCount >= maxCount) {
core.warning(`dispatch_repository: max count of ${maxCount} reached for tool "${toolName}", skipping`);
return {
success: false,
error: `E002: Max count of ${maxCount} reached for tool "${toolName}"`,
};
}
pkg/parser/schemas/main_workflow_schema.json:6919
- The repository slug validation in this schema doesn’t match the compiler’s validation logic in
pkg/workflow/dispatch_repository_validation.go: the schema’srepository.patternallows dots (and underscores) in the owner segment ([a-zA-Z0-9._-]+/...), while the Go validator comment explicitly disallows dots in owners and uses a different regex. This can lead to confusing behavior where a value passes schema validation but fails compilation (or vice versa if the validator is relaxed).
Align the schema pattern with the actual accepted slug format (and/or update the Go validator) so users get consistent validation errors.
"repository": {
"type": "string",
"description": "Target repository in 'owner/repo' format. Dispatches to this repository when no 'allowed_repositories' list is given.",
"pattern": "^[a-zA-Z0-9._-]+/[a-zA-Z0-9._-]+$|^\\$\\{\\{.*\\}\\}$"
},
actions/setup/js/dispatch_repository.cjs:28
- Per-tool
github-tokenoverrides won’t take effect with the current implementation. The handler createsgithubClientonce from the top-level handler config (createAuthenticatedGitHubClient(config)), butdispatch_repositorystoresgithub-tokenunder eachtools[toolName]entry. As a result,toolConfig["github-token"]is ignored and all dispatches use the step-level token (or any hypothetical handler-level token).
Either (a) support only a single handler-level github-token for dispatch_repository and remove the per-tool field from schema/config generation, or (b) create/select an authenticated client per message based on the selected tool’s github-token.
async function main(config = {}) {
const tools = config.tools || {};
const githubClient = await createAuthenticatedGitHubClient(config);
const isStaged = isStagedMode(config);
const contextRepoSlug = `${context.repo.owner}/${context.repo.repo}`;
core.info(`dispatch_repository handler initialized: tools=${Object.keys(tools).join(", ")}, context_repo=${contextRepoSlug}`);
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| const contextRepoSlug = `${context.repo.owner}/${context.repo.repo}`; | ||
| core.info(`dispatch_repository handler initialized: tools=${Object.keys(tools).join(", ")}, context_repo=${contextRepoSlug}`); | ||
|
|
||
| // Per-tool dispatch counters for max enforcement |
There was a problem hiding this comment.
There’s no JS unit test coverage for the new dispatch_repository handler, while similar handlers (e.g. dispatch_workflow.cjs) have dedicated tests covering max enforcement, cross-repo allowlists, staged mode, and error paths. Given the security-sensitive nature of cross-repo dispatch, adding a dispatch_repository.test.cjs (and possibly MCP tool registration tests) would help prevent regressions.
This issue also appears in the following locations of the same file:
- line 94
- line 72
- line 61
- line 22
| // Per-tool dispatch counters for max enforcement | |
| // Per-tool dispatch counters for max enforcement; this state underpins security-sensitive | |
| // rate/usage limits and is expected to be covered by dedicated unit tests (similar to | |
| // dispatch_workflow handler tests). |
| "dispatch_repository": { | ||
| "type": "object", | ||
| "description": "Dispatch repository_dispatch events to external repositories. Each sub-key defines a named dispatch tool with its own event_type, target repository, input schema, and execution limits.", | ||
| "additionalProperties": { | ||
| "type": "object", | ||
| "description": "Configuration for a single repository dispatch tool", |
There was a problem hiding this comment.
The parser supports both dispatch_repository and the dispatch-repository alias, but the schema only defines dispatch_repository. Workflows using the dash alias will likely fail JSON Schema frontmatter validation even though the compiler can parse them.
Either add a dispatch-repository schema entry (matching this object schema) or drop the alias to keep schema and parsing behavior aligned.
This issue also appears on line 6915 of the same file.
| // repoSlugPattern matches a valid owner/repo GitHub repository slug. | ||
| // Owner names: alphanumerics and hyphens (no dots - GitHub usernames/org names cannot have dots). | ||
| // Repository names: alphanumerics, hyphens, dots and underscores. | ||
| var repoSlugPattern = regexp.MustCompile(`^[a-zA-Z0-9_-]+/[a-zA-Z0-9._-]+$`) |
There was a problem hiding this comment.
The comment says owner names are “alphanumerics and hyphens”, but the actual regex allows underscores in the owner segment ([a-zA-Z0-9_-]+). Either update the regex to match the documented constraint, or adjust the comment (and consider aligning with the JSON Schema slug pattern too) so validation rules are consistent and predictable.
| var repoSlugPattern = regexp.MustCompile(`^[a-zA-Z0-9_-]+/[a-zA-Z0-9._-]+$`) | |
| var repoSlugPattern = regexp.MustCompile(`^[a-zA-Z0-9-]+/[a-zA-Z0-9._-]+$`) |
| // Build input schema from the tool's inputs definition | ||
| properties := make(map[string]any) | ||
| required := []string{} | ||
|
|
||
| for inputName, inputDef := range toolConfig.Inputs { | ||
| inputDefMap, ok := inputDef.(map[string]any) | ||
| if !ok { | ||
| continue | ||
| } | ||
|
|
||
| inputType := "string" | ||
| inputDescription := "Input parameter '" + inputName + "'" | ||
| inputRequired := false | ||
|
|
||
| if desc, ok := inputDefMap["description"].(string); ok && desc != "" { | ||
| inputDescription = desc | ||
| } | ||
| if req, ok := inputDefMap["required"].(bool); ok { | ||
| inputRequired = req | ||
| } | ||
|
|
||
| // Map input types to JSON Schema types | ||
| if typeStr, ok := inputDefMap["type"].(string); ok { | ||
| switch typeStr { | ||
| case "number": | ||
| inputType = "number" | ||
| case "boolean": | ||
| inputType = "boolean" | ||
| case "choice": | ||
| inputType = "string" | ||
| if options, ok := inputDefMap["options"].([]any); ok && len(options) > 0 { | ||
| prop := map[string]any{ | ||
| "type": inputType, | ||
| "description": inputDescription, | ||
| "enum": options, | ||
| } | ||
| if defaultVal, ok := inputDefMap["default"]; ok { | ||
| prop["default"] = defaultVal | ||
| } | ||
| properties[inputName] = prop | ||
| if inputRequired { | ||
| required = append(required, inputName) | ||
| } | ||
| continue | ||
| } | ||
| case "environment": | ||
| inputType = "string" | ||
| } | ||
| } | ||
|
|
||
| prop := map[string]any{ | ||
| "type": inputType, | ||
| "description": inputDescription, | ||
| } | ||
| if defaultVal, ok := inputDefMap["default"]; ok { | ||
| prop["default"] = defaultVal | ||
| } | ||
| properties[inputName] = prop | ||
|
|
||
| if inputRequired { | ||
| required = append(required, inputName) | ||
| } | ||
| } | ||
|
|
||
| tool := map[string]any{ | ||
| "name": toolName, | ||
| "description": description, | ||
| "_dispatch_repository_tool": toolKey, // Internal metadata for handler routing | ||
| "inputSchema": map[string]any{ | ||
| "type": "object", | ||
| "properties": properties, | ||
| "additionalProperties": false, | ||
| }, | ||
| } |
There was a problem hiding this comment.
The generated MCP tool schema only exposes inputs fields and sets additionalProperties: false, but the runtime handler supports selecting a target repo via message.repository (and the config supports allowed_repositories). With the current tool schema, agents can’t provide a repository override when multiple repositories are allowed, which effectively makes allowed_repositories either (a) just an allowlist for a fixed repository, or (b) a list where the handler will default to the first entry.
If per-invocation repo selection is intended, consider adding an optional repository (or repo) field to the tool’s inputSchema when allowed_repositories is configured (and update the handler/wrapper to read it consistently).
- Fix broken architecture link in index.mdx (closes #22226) - Add dispatch_repository safe-output type documentation (from #22315) - Add engine feature comparison table to engines reference (closes #22147) - Add frontmatter common field name mistakes to troubleshooting (addresses #22092) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Introduces a new
dispatch_repositorysafe-output using the map-of-tools pattern, enabling agentic workflows to trigger GitHubrepository_dispatchevents against external repositories with structured input validation and execution limits.Go (compile-time)
dispatch_repository.go—DispatchRepositoryConfig+DispatchRepositoryToolConfigstructs; parser supports bothdispatch_repositoryanddispatch-repositoryYAML keysdispatch_repository_validation.go— validatesworkflow,event_type, and at least one repo target per tool; validatesowner/reposlug format with GitHub Actions expressions bypass (values containing${{skip format validation)compiler.go— emits"Using experimental feature: dispatch_repository"warning and increments warning count whendispatch_repositoryis configured (same pattern asrate-limitanddependencies (APM))main_workflow_schema.json) — fulldispatch_repositoryJSON Schema entry with per-tooladditionalPropertiesvalidationSafeOutputsConfig, state reflection map, tools filtering (both JSON strategies), config serialization, compiler validation hook, and prompt tools listJavaScript (runtime)
dispatch_repository.cjs— handler factory; resolves target repo per message, enforces cross-repo allowlist (SEC-005), buildsclient_payloadfrom inputs +workflowidentifier, callsrepos.createDispatchEvent; supports staged modesafe_output_handler_manager.cjs(HANDLER_MAP),safe_outputs_tools_loader.cjs(_dispatch_repository_toolmetadata attach + register), andsafe_outputs_mcp_server_http.cjs(HTTP server registration)Tests
dispatch_repository_test.go) — 19 tests covering parsing, validation, tool generation, config serialization, and end-to-end compilation; includesTestValidateDispatchRepository_GitHubExpressionverifying expressions bypass slug format checksdispatch_repository_experimental_warning_test.go) — 3 integration tests verifying the warning is emitted whendispatch_repositoryis configured and absent otherwisepkg/cli/compile_integration_test.go) — 6 new CLI-level integration tests that compile real workflows and assert on the generated lock file:TestCompileDispatchRepository— single-tool compile; asserts_dispatch_repository_toolmetadata and handler config fieldsTestCompileDispatchRepositoryMultipleTools— two tools with inputs andallowed_repositoriesTestCompileDispatchRepositoryValidationFailure— missingworkflowfield causes compilation failure with descriptive errorTestCompileDispatchRepositoryWorkflowFile— end-to-end compilation of the canonicalpkg/cli/workflows/test-copilot-dispatch-repository.mdTestCompileDispatchRepositoryGitHubActionsExpression—${{ inputs.target_repo }}inrepositoryfield compiles successfully and is preserved verbatim in the handler configTestCompileDispatchRepositoryGitHubActionsExpressionAllowedRepos— mixed static slugs and${{ vars.DYNAMIC_REPO }}expressions inallowed_repositoriescompile and are preserved intactpkg/cli/workflows/test-copilot-dispatch-repository.md) — reference workflow with two tools (trigger_ciwith singlerepository,notify_servicewithallowed_repositoriesandinputs)Example
Each tool becomes a named MCP tool exposed to the agent. At runtime the handler resolves the target repository, validates the allowlist, and sends
repository_dispatchwithclient_payloadcontaining all resolved inputs plus theworkflowidentifier for downstream routing. Bothrepositoryandallowed_repositoriesaccept staticowner/reposlugs or GitHub Actions expressions — expressions are passed through without format validation since their values are only known at runtime.📱 Kick off Copilot coding agent tasks wherever you are with GitHub Mobile, available on iOS and Android.