mirror of
https://github.com/hanzoai/dev.git
synced 2026-10-25 03:32:32 +00:00
feat(core/gh): auto-resolve gh_run_wait defaults
This commit is contained in:
+140
-48
@@ -8886,6 +8886,7 @@ async fn handle_gh_run_wait(
|
||||
) -> ResponseInputItem {
|
||||
use serde::Deserialize;
|
||||
use serde_json::Value;
|
||||
use std::path::Path;
|
||||
|
||||
#[derive(Deserialize, Clone)]
|
||||
struct Params {
|
||||
@@ -8935,6 +8936,55 @@ async fn handle_gh_run_wait(
|
||||
Ok(String::from_utf8_lossy(&output.stdout).trim().to_string())
|
||||
}
|
||||
|
||||
async fn run_git(cwd: &Path, args: &[&str]) -> Option<String> {
|
||||
let output = tokio::process::Command::new("git")
|
||||
.current_dir(cwd)
|
||||
.args(args)
|
||||
.output()
|
||||
.await
|
||||
.ok()?;
|
||||
if !output.status.success() {
|
||||
return None;
|
||||
}
|
||||
let value = String::from_utf8(output.stdout).ok()?;
|
||||
let trimmed = value.trim().to_string();
|
||||
if trimmed.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(trimmed)
|
||||
}
|
||||
}
|
||||
|
||||
async fn detect_branch(cwd: &Path) -> String {
|
||||
if let Some(branch) = run_git(cwd, &["rev-parse", "--abbrev-ref", "HEAD"]).await {
|
||||
if branch != "HEAD" {
|
||||
return branch;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(symref) = run_git(cwd, &["symbolic-ref", "--quiet", "refs/remotes/origin/HEAD"]).await {
|
||||
if let Some((_, name)) = symref.rsplit_once('/') {
|
||||
if !name.is_empty() {
|
||||
return name.to_string();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(show) = run_git(cwd, &["remote", "show", "origin"]).await {
|
||||
for line in show.lines() {
|
||||
let line = line.trim();
|
||||
if let Some(rest) = line.strip_prefix("HEAD branch:") {
|
||||
let name = rest.trim();
|
||||
if !name.is_empty() {
|
||||
return name.to_string();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
"main".to_string()
|
||||
}
|
||||
|
||||
let mut params_for_event = serde_json::from_str::<Value>(&arguments).ok();
|
||||
let parsed: Params = match serde_json::from_str(&arguments) {
|
||||
Ok(p) => p,
|
||||
@@ -8949,6 +8999,8 @@ async fn handle_gh_run_wait(
|
||||
}
|
||||
};
|
||||
|
||||
let cwd = sess.cwd.clone();
|
||||
|
||||
execute_custom_tool(
|
||||
sess,
|
||||
ctx,
|
||||
@@ -8969,69 +9021,108 @@ async fn handle_gh_run_wait(
|
||||
.map(|s| s.to_string());
|
||||
|
||||
let mut resolved_run_id = run_id;
|
||||
if resolved_run_id.is_none() {
|
||||
let workflow = match parsed.workflow.as_ref().map(|s| s.trim()).filter(|s| !s.is_empty()) {
|
||||
Some(name) => name.to_string(),
|
||||
None => {
|
||||
return ResponseInputItem::FunctionCallOutput {
|
||||
call_id,
|
||||
output: FunctionCallOutputPayload {
|
||||
content: "gh_run_wait requires run_id or workflow".to_string(),
|
||||
success: Some(false),
|
||||
},
|
||||
};
|
||||
}
|
||||
};
|
||||
let branch = parsed
|
||||
.branch
|
||||
.as_ref()
|
||||
.map(|s| s.trim())
|
||||
.filter(|s| !s.is_empty())
|
||||
.unwrap_or("main")
|
||||
.to_string();
|
||||
let mut resolved_workflow = parsed
|
||||
.workflow
|
||||
.as_ref()
|
||||
.map(|s| s.trim())
|
||||
.filter(|s| !s.is_empty())
|
||||
.map(|s| s.to_string());
|
||||
let branch = match parsed
|
||||
.branch
|
||||
.as_ref()
|
||||
.map(|s| s.trim())
|
||||
.filter(|s| !s.is_empty())
|
||||
{
|
||||
Some(value) => value.to_string(),
|
||||
None => detect_branch(&cwd).await,
|
||||
};
|
||||
|
||||
let json = match run_gh(&[
|
||||
"run",
|
||||
"list",
|
||||
"--workflow",
|
||||
&workflow,
|
||||
"--branch",
|
||||
&branch,
|
||||
"--limit",
|
||||
"1",
|
||||
"--json",
|
||||
"databaseId,displayTitle,workflowName,headBranch,status,conclusion",
|
||||
],
|
||||
repo.as_deref(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(out) => out,
|
||||
Err(err) => {
|
||||
return ResponseInputItem::FunctionCallOutput {
|
||||
call_id,
|
||||
output: FunctionCallOutputPayload {
|
||||
content: err,
|
||||
success: Some(false),
|
||||
},
|
||||
};
|
||||
}
|
||||
if resolved_run_id.is_none() {
|
||||
let (json, branch_label) = if let Some(workflow) = resolved_workflow.as_ref() {
|
||||
let json = match run_gh(&[
|
||||
"run",
|
||||
"list",
|
||||
"--workflow",
|
||||
workflow,
|
||||
"--branch",
|
||||
&branch,
|
||||
"--limit",
|
||||
"1",
|
||||
"--json",
|
||||
"databaseId,displayTitle,workflowName,headBranch,status,conclusion",
|
||||
],
|
||||
repo.as_deref(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(out) => out,
|
||||
Err(err) => {
|
||||
return ResponseInputItem::FunctionCallOutput {
|
||||
call_id,
|
||||
output: FunctionCallOutputPayload {
|
||||
content: err,
|
||||
success: Some(false),
|
||||
},
|
||||
};
|
||||
}
|
||||
};
|
||||
(json, branch.clone())
|
||||
} else {
|
||||
let json = match run_gh(&[
|
||||
"run",
|
||||
"list",
|
||||
"--branch",
|
||||
&branch,
|
||||
"--limit",
|
||||
"1",
|
||||
"--json",
|
||||
"databaseId,displayTitle,workflowName,headBranch,status,conclusion",
|
||||
],
|
||||
repo.as_deref(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(out) => out,
|
||||
Err(err) => {
|
||||
return ResponseInputItem::FunctionCallOutput {
|
||||
call_id,
|
||||
output: FunctionCallOutputPayload {
|
||||
content: err,
|
||||
success: Some(false),
|
||||
},
|
||||
};
|
||||
}
|
||||
};
|
||||
(json, branch.clone())
|
||||
};
|
||||
|
||||
let runs: Vec<Value> = serde_json::from_str(&json).unwrap_or_default();
|
||||
let run = runs.into_iter().next();
|
||||
let run_id = run
|
||||
.as_ref()
|
||||
.and_then(|item| item.get("databaseId").cloned())
|
||||
.and_then(|val| match val {
|
||||
Value::Number(num) => num.as_u64().map(|v| v.to_string()),
|
||||
Value::String(s) => Some(s),
|
||||
_ => None,
|
||||
});
|
||||
if resolved_workflow.is_none() {
|
||||
resolved_workflow = run
|
||||
.as_ref()
|
||||
.and_then(|item| item.get("workflowName"))
|
||||
.and_then(|value| value.as_str())
|
||||
.map(|value| value.to_string());
|
||||
}
|
||||
if run_id.is_none() {
|
||||
let detail = if let Some(workflow) = resolved_workflow.as_ref() {
|
||||
format!("workflow '{workflow}' on {branch_label}")
|
||||
} else {
|
||||
format!("branch {branch_label}")
|
||||
};
|
||||
return ResponseInputItem::FunctionCallOutput {
|
||||
call_id,
|
||||
output: FunctionCallOutputPayload {
|
||||
content: format!("No runs found for workflow '{workflow}' on {branch}"),
|
||||
content: format!("No runs found for {detail}"),
|
||||
success: Some(false),
|
||||
},
|
||||
};
|
||||
@@ -9125,9 +9216,10 @@ async fn handle_gh_run_wait(
|
||||
"run_id": run_id,
|
||||
"status": status,
|
||||
"conclusion": if conclusion.is_empty() { None::<String> } else { Some(conclusion.clone()) },
|
||||
"workflow": workflow_name,
|
||||
"workflow": workflow_name.or_else(|| resolved_workflow.clone()),
|
||||
"title": display_title,
|
||||
"url": html_url,
|
||||
"branch": branch,
|
||||
"jobs": {
|
||||
"total": total_jobs,
|
||||
"completed": completed_jobs,
|
||||
|
||||
@@ -870,7 +870,8 @@ pub fn create_gh_run_wait_tool() -> OpenAiTool {
|
||||
"branch".to_string(),
|
||||
JsonSchema::String {
|
||||
description: Some(
|
||||
"Branch to filter when selecting latest run (default: main).".to_string(),
|
||||
"Branch to filter when selecting latest run (default: current branch, falling back to main)."
|
||||
.to_string(),
|
||||
),
|
||||
allowed_values: None,
|
||||
},
|
||||
@@ -883,7 +884,7 @@ pub fn create_gh_run_wait_tool() -> OpenAiTool {
|
||||
);
|
||||
OpenAiTool::Function(ResponsesApiTool {
|
||||
name: "gh_run_wait".to_string(),
|
||||
description: "Wait for a GitHub Actions run to finish, using gh run view polling."
|
||||
description: "Wait for a GitHub Actions run to finish, using gh run view polling. If run_id is omitted, selects the latest run for the workflow/branch; if both are omitted, selects the latest run on the current branch."
|
||||
.to_string(),
|
||||
strict: false,
|
||||
parameters: JsonSchema::Object {
|
||||
|
||||
+118
-12
@@ -4,6 +4,7 @@
|
||||
# Usage examples:
|
||||
# scripts/wait-for-gh-run.sh --run 17901972778
|
||||
# scripts/wait-for-gh-run.sh --workflow Release --branch main
|
||||
# scripts/wait-for-gh-run.sh # picks latest run on current branch
|
||||
#
|
||||
# Dependencies: gh (GitHub CLI), jq.
|
||||
|
||||
@@ -16,12 +17,13 @@ Usage: wait-for-gh-run.sh [OPTIONS]
|
||||
Options:
|
||||
-r, --run ID Run ID to monitor.
|
||||
-w, --workflow NAME Workflow name or filename to pick the latest run.
|
||||
-b, --branch BRANCH Branch to filter when selecting a run (default: main).
|
||||
-b, --branch BRANCH Branch to filter when selecting a run (default: current branch).
|
||||
-i, --interval SECONDS Polling interval in seconds (default: 8).
|
||||
-L, --failure-logs Print logs for any job that does not finish successfully.
|
||||
-h, --help Show this help message.
|
||||
|
||||
Either --run or --workflow must be supplied.
|
||||
If neither --run nor --workflow is provided, the latest run on the current
|
||||
branch is selected automatically.
|
||||
EOF
|
||||
}
|
||||
|
||||
@@ -34,9 +36,10 @@ require_binary() {
|
||||
|
||||
RUN_ID=""
|
||||
WORKFLOW=""
|
||||
BRANCH="main"
|
||||
BRANCH=""
|
||||
INTERVAL="8"
|
||||
PRINT_FAILURE_LOGS=false
|
||||
AUTO_SELECTED_RUN=false
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
@@ -72,15 +75,38 @@ while [[ $# -gt 0 ]]; do
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ -z "$RUN_ID" && -z "$WORKFLOW" ]]; then
|
||||
echo "error: either --run or --workflow must be specified" >&2
|
||||
usage >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
require_binary gh
|
||||
require_binary jq
|
||||
|
||||
default_branch() {
|
||||
local branch=""
|
||||
if command -v git >/dev/null 2>&1; then
|
||||
if branch=$(git rev-parse --abbrev-ref HEAD 2>/dev/null); then
|
||||
if [[ -n "$branch" && "$branch" != "HEAD" ]]; then
|
||||
echo "$branch"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
|
||||
if branch=$(git symbolic-ref --quiet --short refs/remotes/origin/HEAD 2>/dev/null); then
|
||||
branch="${branch#origin/}"
|
||||
if [[ -n "$branch" ]]; then
|
||||
echo "$branch"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
|
||||
if branch=$(git remote show origin 2>/dev/null | awk '/HEAD branch/ {print $NF}'); then
|
||||
if [[ -n "$branch" ]]; then
|
||||
echo "$branch"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "main"
|
||||
}
|
||||
|
||||
select_latest_run() {
|
||||
local workflow="$1"
|
||||
local branch="$2"
|
||||
@@ -98,8 +124,49 @@ select_latest_run() {
|
||||
jq -r '.[0].databaseId' <<<"$json"
|
||||
}
|
||||
|
||||
select_latest_run_any() {
|
||||
local branch="$1"
|
||||
local json
|
||||
if ! json=$(gh run list --branch "$branch" --limit 1 --json databaseId,workflowName,displayTitle,headBranch 2>/dev/null); then
|
||||
echo "error: failed to list runs on branch '$branch'" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ $(jq 'length' <<<"$json") -eq 0 ]]; then
|
||||
echo "error: no runs found on branch '$branch'" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
WORKFLOW=$(jq -r '.[0].workflowName // ""' <<<"$json")
|
||||
jq -r '.[0].databaseId' <<<"$json"
|
||||
}
|
||||
|
||||
format_duration() {
|
||||
local total="$1"
|
||||
local hours=$((total / 3600))
|
||||
local minutes=$(((total % 3600) / 60))
|
||||
local seconds=$((total % 60))
|
||||
if [[ $hours -gt 0 ]]; then
|
||||
printf '%dh%02dm%02ds' "$hours" "$minutes" "$seconds"
|
||||
elif [[ $minutes -gt 0 ]]; then
|
||||
printf '%dm%02ds' "$minutes" "$seconds"
|
||||
else
|
||||
printf '%ds' "$seconds"
|
||||
fi
|
||||
}
|
||||
|
||||
if [[ -z "$BRANCH" ]]; then
|
||||
BRANCH=$(default_branch)
|
||||
fi
|
||||
|
||||
if [[ -z "$RUN_ID" ]]; then
|
||||
RUN_ID=$(select_latest_run "$WORKFLOW" "$BRANCH")
|
||||
if [[ -n "$WORKFLOW" ]]; then
|
||||
RUN_ID=$(select_latest_run "$WORKFLOW" "$BRANCH")
|
||||
AUTO_SELECTED_RUN=true
|
||||
else
|
||||
RUN_ID=$(select_latest_run_any "$BRANCH")
|
||||
AUTO_SELECTED_RUN=true
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ -z "$RUN_ID" ]]; then
|
||||
@@ -108,9 +175,19 @@ if [[ -z "$RUN_ID" ]]; then
|
||||
fi
|
||||
|
||||
echo "Waiting for GitHub Actions run $RUN_ID..." >&2
|
||||
if [[ "$AUTO_SELECTED_RUN" == true ]]; then
|
||||
if [[ -z "$WORKFLOW" ]]; then
|
||||
echo "Auto-selected latest run on branch '$BRANCH'." >&2
|
||||
else
|
||||
echo "Auto-selected latest '$WORKFLOW' run on branch '$BRANCH'." >&2
|
||||
fi
|
||||
elif [[ -n "$WORKFLOW" ]]; then
|
||||
echo "Using workflow '$WORKFLOW' on branch '$BRANCH'." >&2
|
||||
fi
|
||||
|
||||
last_status=""
|
||||
last_jobs_snapshot=""
|
||||
last_progress_snapshot=""
|
||||
|
||||
while true; do
|
||||
json=""
|
||||
@@ -143,6 +220,17 @@ while true; do
|
||||
last_jobs_snapshot="$jobs_snapshot"
|
||||
fi
|
||||
|
||||
total_jobs=$(jq -r '.jobs | length' <<<"$json")
|
||||
completed_jobs=$(jq -r '[.jobs[]? | select(.status == "completed")] | length' <<<"$json")
|
||||
in_progress_jobs=$(jq -r '[.jobs[]? | select(.status == "in_progress")] | length' <<<"$json")
|
||||
queued_jobs=$(jq -r '[.jobs[]? | select(.status == "queued")] | length' <<<"$json")
|
||||
progress_snapshot="$completed_jobs/$total_jobs/$in_progress_jobs/$queued_jobs"
|
||||
|
||||
if [[ "$status" != "completed" && "$total_jobs" != "0" && "$progress_snapshot" != "$last_progress_snapshot" ]]; then
|
||||
echo "$(date '+%Y-%m-%d %H:%M:%S') progress: $completed_jobs/$total_jobs completed ($in_progress_jobs in_progress, $queued_jobs queued)" >&2
|
||||
last_progress_snapshot="$progress_snapshot"
|
||||
fi
|
||||
|
||||
failing_jobs=$(jq -c '
|
||||
.jobs[]? | select(
|
||||
.status == "completed" and (.conclusion // "") != "" and
|
||||
@@ -177,8 +265,22 @@ while true; do
|
||||
fi
|
||||
|
||||
if [[ "$status" == "completed" ]]; then
|
||||
started_at=$(jq -r '.startedAt // ""' <<<"$json")
|
||||
updated_at=$(jq -r '.updatedAt // ""' <<<"$json")
|
||||
duration=""
|
||||
if [[ -n "$started_at" && -n "$updated_at" ]]; then
|
||||
start_epoch=$(date -d "$started_at" +%s 2>/dev/null || true)
|
||||
end_epoch=$(date -d "$updated_at" +%s 2>/dev/null || true)
|
||||
if [[ -n "$start_epoch" && -n "$end_epoch" && "$end_epoch" -ge "$start_epoch" ]]; then
|
||||
duration=$(format_duration $((end_epoch - start_epoch)))
|
||||
fi
|
||||
fi
|
||||
if [[ "$conclusion" == "success" ]]; then
|
||||
echo "Run $RUN_ID succeeded." >&2
|
||||
if [[ -n "$duration" ]]; then
|
||||
echo "Run $RUN_ID succeeded in $duration." >&2
|
||||
else
|
||||
echo "Run $RUN_ID succeeded." >&2
|
||||
fi
|
||||
exit 0
|
||||
else
|
||||
if [[ "$PRINT_FAILURE_LOGS" == true ]]; then
|
||||
@@ -193,7 +295,11 @@ while true; do
|
||||
echo "--- End logs for job: $job_name ---" >&2
|
||||
done
|
||||
fi
|
||||
echo "Run $RUN_ID finished with conclusion '$conclusion'." >&2
|
||||
if [[ -n "$duration" ]]; then
|
||||
echo "Run $RUN_ID finished with conclusion '$conclusion' in $duration." >&2
|
||||
else
|
||||
echo "Run $RUN_ID finished with conclusion '$conclusion'." >&2
|
||||
fi
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
Reference in New Issue
Block a user