aagt_core/skills/compiler/
github.rs1use serde::{Deserialize, Serialize};
2use std::time::Duration;
3use tokio::time::sleep;
4use tracing::info;
5use std::sync::Arc;
6use crate::infra::notification::{Notifier, NotifyChannel};
7use crate::error::{Error, Result};
8
9#[derive(Clone)]
10pub struct GithubCompiler {
11 repo: String, token: String,
13 workflow_id: String, notifier: Option<Arc<dyn Notifier>>,
15}
16
17impl std::fmt::Debug for GithubCompiler {
18 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
19 f.debug_struct("GithubCompiler")
20 .field("repo", &self.repo)
21 .field("workflow_id", &self.workflow_id)
22 .field("notifier", &self.notifier.as_ref().map(|_| "Some(Notifier)"))
23 .finish()
24 }
25}
26
27#[derive(Serialize)]
28struct WorkflowDispatch {
29 #[serde(rename = "ref")]
30 branch: String,
31 inputs: serde_json::Value,
32}
33
34#[derive(Deserialize)]
35struct WorkflowRun {
36 id: u64,
37 status: String,
38 conclusion: Option<String>,
39}
40
41#[derive(Deserialize)]
42struct WorkflowRunsResponse {
43 workflow_runs: Vec<WorkflowRun>,
44}
45
46#[derive(Deserialize)]
47struct Artifact {
48 #[allow(dead_code)]
49 id: u64,
50 name: String,
51 archive_download_url: String,
52}
53
54#[derive(Deserialize)]
55struct ArtifactsResponse {
56 artifacts: Vec<Artifact>,
57}
58
59impl GithubCompiler {
60 pub fn new(repo: String, token: String, notifier: Option<Arc<dyn Notifier>>) -> Self {
61 Self {
62 repo,
63 token,
64 workflow_id: "compiler.yml".to_string(),
65 notifier,
66 }
67 }
68
69 pub async fn compile(&self, skill_name: &str, source_code: &str) -> Result<Vec<u8>> {
71 let client = reqwest::Client::new();
72 let url = format!("https://api.github.com/repos/{}/actions/workflows/{}/dispatches", self.repo, self.workflow_id);
73
74 if let Some(notifier) = &self.notifier {
75 let _ = notifier.notify(NotifyChannel::Log, &format!("GitHub: Starting compilation for {}", skill_name)).await;
76 }
77
78 info!("Triggering GitHub compilation for skill: {}", skill_name);
79
80 let response = client.post(&url)
82 .header("Authorization", format!("Bearer {}", self.token))
83 .header("Accept", "application/vnd.github+json")
84 .header("User-Agent", "AAGT-Agent")
85 .json(&WorkflowDispatch {
86 branch: "main".to_string(),
87 inputs: serde_json::json!({
88 "source_code": source_code,
89 "skill_name": skill_name,
90 }),
91 })
92 .send()
93 .await
94 .map_err(|e| Error::ToolExecution {
95 tool_name: "github_compiler".to_string(),
96 message: format!("Failed to trigger workflow: {}", e)
97 })?;
98
99 if !response.status().is_success() {
100 let status = response.status();
101 let body = response.text().await.unwrap_or_default();
102 return Err(Error::ToolExecution {
103 tool_name: "github_compiler".to_string(),
104 message: format!("GitHub API error ({}): {}", status, body)
105 }.into());
106 }
107
108 sleep(Duration::from_secs(5)).await;
110
111 let run_id = self.wait_for_run(&client, skill_name).await?;
112
113 let result_run = self.wait_for_completion(&client, run_id).await?;
115
116 if result_run.conclusion.as_deref() != Some("success") {
117 return Err(Error::ToolExecution {
118 tool_name: "github_compiler".to_string(),
119 message: format!("Compilation failed on GitHub. Conclusion: {:?}", result_run.conclusion)
120 }.into());
121 }
122
123 self.download_artifact(&client, run_id, skill_name).await
125 }
126
127 async fn wait_for_run(&self, client: &reqwest::Client, _skill_name: &str) -> Result<u64> {
128 let url = format!("https://api.github.com/repos/{}/actions/runs", self.repo);
129
130 for _ in 0..12 { let response = client.get(&url)
132 .header("Authorization", format!("Bearer {}", self.token))
133 .header("User-Agent", "AAGT-Agent")
134 .send()
135 .await
136 .map_err(|e| Error::ToolExecution {
137 tool_name: "github_compiler".to_string(),
138 message: format!("Failed to poll runs: {}", e)
139 })?;
140
141 let body: WorkflowRunsResponse = response.json().await.map_err(|e| Error::ToolExecution {
142 tool_name: "github_compiler".to_string(),
143 message: format!("Failed to parse runs: {}", e)
144 })?;
145
146 if let Some(run) = body.workflow_runs.first() {
147 return Ok(run.id);
149 }
150 sleep(Duration::from_secs(5)).await;
151 }
152
153 Err(Error::ToolExecution {
154 tool_name: "github_compiler".to_string(),
155 message: "Timed out waiting for workflow job to start".to_string()
156 }.into())
157 }
158
159 async fn wait_for_completion(&self, client: &reqwest::Client, run_id: u64) -> Result<WorkflowRun> {
160 let url = format!("https://api.github.com/repos/{}/actions/runs/{}", self.repo, run_id);
161
162 for _ in 0..60 { let response = client.get(&url)
164 .header("Authorization", format!("Bearer {}", self.token))
165 .header("User-Agent", "AAGT-Agent")
166 .send()
167 .await
168 .map_err(|e| Error::ToolExecution {
169 tool_name: "github_compiler".to_string(),
170 message: format!("Failed to check run status: {}", e)
171 })?;
172
173 let run: WorkflowRun = response.json().await.map_err(|e| Error::ToolExecution {
174 tool_name: "github_compiler".to_string(),
175 message: format!("Failed to parse run status: {}", e)
176 })?;
177
178 if run.status == "completed" {
179 return Ok(run);
180 }
181
182 if let Some(notifier) = &self.notifier {
183 let _ = notifier.notify(NotifyChannel::Log, &format!("GitHub: Build in progress (Run {})", run_id)).await;
184 }
185
186 info!("Waiting for GitHub build (Run ID: {})...", run_id);
187 sleep(Duration::from_secs(10)).await;
188 }
189
190 Err(Error::ToolExecution {
191 tool_name: "github_compiler".to_string(),
192 message: "Timed out waiting for workflow completion".to_string()
193 }.into())
194 }
195
196 async fn download_artifact(&self, client: &reqwest::Client, run_id: u64, skill_name: &str) -> Result<Vec<u8>> {
197 let url = format!("https://api.github.com/repos/{}/actions/runs/{}/artifacts", self.repo, run_id);
198
199 let response = client.get(&url)
200 .header("Authorization", format!("Bearer {}", self.token))
201 .header("User-Agent", "AAGT-Agent")
202 .send()
203 .await
204 .map_err(|e| Error::ToolExecution {
205 tool_name: "github_compiler".to_string(),
206 message: format!("Failed to list artifacts: {}", e)
207 })?;
208
209 let body: ArtifactsResponse = response.json().await.map_err(|e| Error::ToolExecution {
210 tool_name: "github_compiler".to_string(),
211 message: format!("Failed to parse artifacts: {}", e)
212 })?;
213
214 let artifact = body.artifacts.iter()
215 .find(|a| a.name == skill_name)
216 .ok_or_else(|| Error::ToolExecution {
217 tool_name: "github_compiler".to_string(),
218 message: format!("Artifact '{}' not found in run {}", skill_name, run_id)
219 })?;
220
221 info!("Downloading artifact: {} from {}", artifact.name, artifact.archive_download_url);
222
223 let download_response = client.get(&artifact.archive_download_url)
224 .header("Authorization", format!("Bearer {}", self.token))
225 .header("User-Agent", "AAGT-Agent")
226 .send()
227 .await
228 .map_err(|e| Error::ToolExecution {
229 tool_name: "github_compiler".to_string(),
230 message: format!("Failed to download artifact zip: {}", e)
231 })?;
232
233 let zip_data = download_response.bytes().await.map_err(|e| Error::ToolExecution {
234 tool_name: "github_compiler".to_string(),
235 message: format!("Failed to read artifact bytes: {}", e)
236 })?;
237
238 use std::io::Read;
239 let mut archive = zip::ZipArchive::new(std::io::Cursor::new(zip_data))
240 .map_err(|e| Error::ToolExecution {
241 tool_name: "github_compiler".to_string(),
242 message: format!("Failed to open artifact zip: {}", e)
243 })?;
244
245 let mut file = archive.by_index(0).map_err(|e| Error::ToolExecution {
246 tool_name: "github_compiler".to_string(),
247 message: format!("Failed to find file in zip: {}", e)
248 })?;
249
250 let mut buffer = Vec::new();
251 file.read_to_end(&mut buffer).map_err(|e| Error::ToolExecution {
252 tool_name: "github_compiler".to_string(),
253 message: format!("Failed to read file from zip: {}", e)
254 })?;
255
256 Ok(buffer)
257 }
258}