Skip to main content

elizaos_plugin_github/actions/
push_code.rs

1#![allow(missing_docs)]
2
3use async_trait::async_trait;
4use serde_json::json;
5
6use super::{ActionContext, ActionResult, GitHubAction};
7use crate::error::Result;
8use crate::types::{CreateCommitParams, FileChange};
9use crate::GitHubService;
10
11pub struct PushCodeAction;
12
13#[async_trait]
14impl GitHubAction for PushCodeAction {
15    fn name(&self) -> &str {
16        "PUSH_GITHUB_CODE"
17    }
18
19    fn description(&self) -> &str {
20        "Creates a commit with file changes and pushes to a GitHub branch."
21    }
22
23    fn similes(&self) -> Vec<&str> {
24        vec![
25            "COMMIT_CODE",
26            "PUSH_CHANGES",
27            "COMMIT_FILES",
28            "PUSH_FILES",
29            "GIT_PUSH",
30            "SAVE_CODE",
31        ]
32    }
33
34    async fn validate(&self, context: &ActionContext) -> Result<bool> {
35        let text = context
36            .message
37            .get("content")
38            .and_then(|c| c.get("text"))
39            .and_then(|t| t.as_str())
40            .unwrap_or("")
41            .to_lowercase();
42
43        Ok(text.contains("push")
44            || text.contains("commit")
45            || text.contains("save")
46            || text.contains("upload"))
47    }
48
49    async fn handler(
50        &self,
51        context: &ActionContext,
52        service: &GitHubService,
53    ) -> Result<ActionResult> {
54        let text = context
55            .message
56            .get("content")
57            .and_then(|c| c.get("text"))
58            .and_then(|t| t.as_str())
59            .unwrap_or("");
60
61        let message = context
62            .state
63            .get("message")
64            .and_then(|m| m.as_str())
65            .map(|s| s.to_string())
66            .unwrap_or_else(|| text.chars().take(100).collect());
67
68        let branch = context
69            .state
70            .get("branch")
71            .and_then(|b| b.as_str())
72            .map(|s| s.to_string())
73            .unwrap_or_else(|| "main".to_string());
74
75        let files: Vec<FileChange> = context
76            .state
77            .get("files")
78            .and_then(|f| serde_json::from_value(f.clone()).ok())
79            .unwrap_or_default();
80
81        if files.is_empty() {
82            return Ok(ActionResult::error("No files to commit"));
83        }
84
85        let author_name = context
86            .state
87            .get("authorName")
88            .and_then(|a| a.as_str())
89            .map(|s| s.to_string());
90
91        let author_email = context
92            .state
93            .get("authorEmail")
94            .and_then(|a| a.as_str())
95            .map(|s| s.to_string());
96
97        let params = CreateCommitParams {
98            owner: context.owner.clone(),
99            repo: context.repo.clone(),
100            message,
101            files,
102            branch,
103            parent_sha: None,
104            author_name,
105            author_email,
106        };
107
108        let commit = service.create_commit(params).await?;
109        let short_sha = commit.sha.chars().take(7).collect::<String>();
110
111        Ok(ActionResult::success(
112            format!("Pushed commit {} to {}", short_sha, &commit.html_url),
113            json!({
114                "sha": commit.sha,
115                "html_url": commit.html_url,
116                "message": commit.message,
117            }),
118        ))
119    }
120}
121
122#[cfg(test)]
123mod tests {
124    use super::*;
125    use serde_json::json;
126
127    #[tokio::test]
128    async fn test_validate_with_push_keyword() {
129        let action = PushCodeAction;
130        let context = ActionContext {
131            message: json!({
132                "content": { "text": "Push the changes to the repository" }
133            }),
134            owner: "test".to_string(),
135            repo: "test".to_string(),
136            state: json!({}),
137        };
138
139        assert!(action.validate(&context).await.unwrap());
140    }
141
142    #[tokio::test]
143    async fn test_validate_with_commit_keyword() {
144        let action = PushCodeAction;
145        let context = ActionContext {
146            message: json!({
147                "content": { "text": "Commit these files to the repository" }
148            }),
149            owner: "test".to_string(),
150            repo: "test".to_string(),
151            state: json!({}),
152        };
153
154        assert!(action.validate(&context).await.unwrap());
155    }
156
157    #[tokio::test]
158    async fn test_validate_without_keywords() {
159        let action = PushCodeAction;
160        let context = ActionContext {
161            message: json!({
162                "content": { "text": "Hello world" }
163            }),
164            owner: "test".to_string(),
165            repo: "test".to_string(),
166            state: json!({}),
167        };
168
169        assert!(!action.validate(&context).await.unwrap());
170    }
171
172    #[test]
173    fn test_action_properties() {
174        let action = PushCodeAction;
175        assert_eq!(action.name(), "PUSH_GITHUB_CODE");
176        assert!(!action.description().is_empty());
177        assert!(action.similes().contains(&"COMMIT_CODE"));
178        assert!(action.similes().contains(&"PUSH_CHANGES"));
179    }
180}