feat: add fork action to github tool (#2139)

feat: add fork_repo action to GitHub WASM tool

Adds fork_repo action with full input validation, optional organization/name/default_branch_only params. CI failures are pre-existing (RUSTSEC-2026-0098 in rustls-webpki transitive dep, unrelated to this PR).
This commit is contained in:
Guille
2026-04-22 09:54:42 +02:00
committed by GitHub
parent b1472a7dec
commit 653801700e
4 changed files with 95 additions and 3 deletions

View File

@@ -1,6 +1,6 @@
[package]
name = "github-tool"
version = "0.2.2"
version = "0.2.3"
edition = "2021"
description = "GitHub integration tool for IronClaw (WASM component)"
license = "MIT OR Apache-2.0"

View File

@@ -8,6 +8,7 @@ search, branches, file reads and writes, releases, and workflows.
- **Repositories** - Get repo details, list user repos, create repositories
- **Search** - Search repositories, code, and issues/PRs
- **Branches** - List branches and create new branches from an existing ref
- **Fork** - Fork repositories
- **Issues** - List/create/get issues, list/add issue comments
- **Pull Requests** - List/create/get PRs, review files, create reviews, list/reply review comments, merge PRs
- **File Content** - Read files and create/update/delete repository files
@@ -286,6 +287,21 @@ When updating an existing file, include the current blob `sha`.
}
```
### Fork Repository
```json
{
"action": "fork_repo",
"owner": "nearai",
"repo": "ironclaw",
"organization": "my-org",
"name": "ironclaw-fork",
"default_branch_only": true
}
```
`organization`, `name`, and `default_branch_only` are optional. Omit `organization` to fork into the authenticated user's account.
### Create Branch
```json

View File

@@ -1,5 +1,5 @@
{
"version": "0.2.2",
"version": "0.2.3",
"wit_version": "0.3.0",
"description": "Manage GitHub repositories, issues, pull requests, search, branches, file reads and writes, releases, and workflows.",
"discovery_summary": {
@@ -16,7 +16,7 @@
"notes": [
"Use `tool_info(name: \"github\", detail: \"schema\")` for the full action schema before guessing fields.",
"Supported families: repositories, issues, pull requests, reviews/comments, search, branches, code reads, file writes, releases, workflow dispatch/runs, and webhook normalization.",
"Not supported yet: forks, labels, milestones, projects, org/team admin, GraphQL, release asset uploads, and repository deletion."
"Not supported yet: labels, milestones, projects, org/team admin, GraphQL, release asset uploads, and repository deletion."
],
"examples": [
{

View File

@@ -430,6 +430,14 @@ enum GitHubAction {
page: Option<u32>,
limit: Option<u32>,
},
#[serde(rename = "fork_repo")]
ForkRepo {
owner: String,
repo: String,
organization: Option<String>,
name: Option<String>,
default_branch_only: Option<bool>,
},
#[serde(rename = "handle_webhook")]
HandleWebhook { webhook: GitHubWebhookRequest },
}
@@ -752,6 +760,19 @@ fn execute_inner(params: &str) -> Result<String, String> {
page,
limit,
} => get_workflow_runs(&owner, &repo, workflow_id.as_deref(), page, limit),
GitHubAction::ForkRepo {
owner,
repo,
organization,
name,
default_branch_only,
} => fork_repo(
&owner,
&repo,
organization.as_deref(),
name.as_deref(),
default_branch_only,
),
GitHubAction::HandleWebhook { webhook } => handle_webhook(webhook),
}
}
@@ -926,6 +947,49 @@ fn create_repo(
github_request("POST", &path, Some(req_body.to_string()))
}
fn fork_repo(
owner: &str,
repo: &str,
organization: Option<&str>,
name: Option<&str>,
default_branch_only: Option<bool>,
) -> Result<String, String> {
if !validate_path_segment(owner) || !validate_path_segment(repo) {
return Err("Invalid owner or repo name".into());
}
validate_input_length(owner, "owner")?;
validate_input_length(repo, "repo")?;
if let Some(org) = organization {
validate_input_length(org, "organization")?;
if !validate_path_segment(org) {
return Err("Invalid org name".into());
}
}
if let Some(n) = name {
validate_input_length(n, "name")?;
if !validate_path_segment(n) {
return Err("Invalid fork name".into());
}
}
let encoded_owner = url_encode_path(owner);
let encoded_repo = url_encode_path(repo);
let path = format!("/repos/{}/{}/forks", encoded_owner, encoded_repo);
let mut req_body = serde_json::json!({});
if let Some(org) = organization {
req_body["organization"] = serde_json::json!(org);
}
if let Some(n) = name {
req_body["name"] = serde_json::json!(n);
}
if let Some(only) = default_branch_only {
req_body["default_branch_only"] = serde_json::json!(only);
}
github_request("POST", &path, Some(req_body.to_string()))
}
fn list_issues(
owner: &str,
repo: &str,
@@ -2300,6 +2364,17 @@ const SCHEMA: &str = r#"{
},
"required": ["action", "owner", "repo"]
},
{
"properties": {
"action": { "const": "fork_repo" },
"owner": { "type": "string", "description": "Repository owner (user or org) to fork from" },
"repo": { "type": "string", "description": "Repository name to fork" },
"organization": { "type": "string", "description": "Optional organization to fork into; omit to fork into the authenticated user's account" },
"name": { "type": "string", "description": "Optional name for the fork; defaults to the original repo name" },
"default_branch_only": { "type": "boolean", "default": false, "description": "When true, only the default branch is copied into the fork" }
},
"required": ["action", "owner", "repo"]
},
{
"properties": {
"action": { "const": "handle_webhook" },
@@ -2377,6 +2452,7 @@ mod tests {
"create_release",
"trigger_workflow",
"get_workflow_runs",
"fork_repo",
"handle_webhook",
]
.into_iter()