Skip to main content

aagt_core/skills/compiler/
github.rs

1use 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, // e.g., "undead-undead/aagt-compiler"
12    token: String,
13    workflow_id: String, // e.g., "compiler.yml"
14    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    /// Trigger compilation and wait for result
70    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        // 1. Trigger workflow
81        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        // 2. Poll for the latest run
109        sleep(Duration::from_secs(5)).await; 
110        
111        let run_id = self.wait_for_run(&client, skill_name).await?;
112        
113        // 3. Wait for completion
114        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        // 4. Download artifact
124        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 { // Try for 1 minute
131            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 the first one for now, assuming it's ours if it was just created
148                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 { // Try for 10 minutes
163            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}