@@ -74,6 +74,38 @@ pub struct GitHubComment {
7474 pub updated_at : String ,
7575}
7676
77+ /// GitHub milestone (REST API format)
78+ #[ derive( Debug , Clone , Serialize , Deserialize ) ]
79+ pub struct GitHubMilestone {
80+ pub number : u64 ,
81+ pub id : u64 ,
82+ pub title : String ,
83+ #[ serde( default ) ]
84+ pub description : Option < String > ,
85+ pub state : String , // "open" or "closed"
86+ #[ serde( default ) ]
87+ pub due_on : Option < String > ,
88+ pub html_url : String ,
89+ pub created_at : String ,
90+ pub updated_at : String ,
91+ #[ serde( default ) ]
92+ pub closed_at : Option < String > ,
93+ pub open_issues : u32 ,
94+ pub closed_issues : u32 ,
95+ }
96+
97+ /// Request to create/update a milestone
98+ #[ derive( Debug , Clone , Serialize ) ]
99+ pub struct MilestoneRequest {
100+ pub title : String ,
101+ #[ serde( skip_serializing_if = "Option::is_none" ) ]
102+ pub description : Option < String > ,
103+ #[ serde( skip_serializing_if = "Option::is_none" ) ]
104+ pub due_on : Option < String > ,
105+ #[ serde( skip_serializing_if = "Option::is_none" ) ]
106+ pub state : Option < String > ,
107+ }
108+
77109/// GraphQL response wrapper
78110#[ derive( Debug , Clone , Deserialize ) ]
79111struct GraphQLResponse < T > {
@@ -771,6 +803,243 @@ impl GitHubAdapter {
771803 } ) ,
772804 }
773805 }
806+
807+ // ========================================================================
808+ // Milestone Operations
809+ // ========================================================================
810+
811+ /// List all milestones for a repository
812+ pub async fn list_milestones (
813+ & self ,
814+ repo : & str ,
815+ state : Option < & str > ,
816+ ) -> Result < Vec < GitHubMilestone > > {
817+ let url = format ! (
818+ "{}/repos/{}/{}/milestones" ,
819+ self . rest_base_url, self . config. owner, repo
820+ ) ;
821+
822+ debug ! ( repo = %repo, state = ?state, "Listing GitHub milestones" ) ;
823+
824+ let mut request = self . client . get ( & url) ;
825+ if let Some ( state) = state {
826+ request = request. query ( & [ ( "state" , state) ] ) ;
827+ }
828+ request = request. query ( & [ ( "sort" , "due_on" ) , ( "direction" , "asc" ) ] ) ;
829+
830+ if let Some ( ref token) = self . auth_token {
831+ request = request. bearer_auth ( token) ;
832+ }
833+
834+ let response = request. timeout ( GET_TIMEOUT ) . send ( ) . await ?;
835+
836+ match response. status ( ) {
837+ StatusCode :: OK => {
838+ let milestones: Vec < GitHubMilestone > = response. json ( ) . await ?;
839+ info ! ( count = milestones. len( ) , "Fetched GitHub milestones" ) ;
840+ Ok ( milestones)
841+ }
842+ StatusCode :: NOT_FOUND => Err ( crate :: AllBeadsError :: Integration ( format ! (
843+ "Repository not found: {}/{}" ,
844+ self . config. owner, repo
845+ ) ) ) ,
846+ status => {
847+ let error_body = response. text ( ) . await . unwrap_or_default ( ) ;
848+ Err ( crate :: AllBeadsError :: Integration ( format ! (
849+ "GitHub milestones failed: HTTP {}: {}" ,
850+ status, error_body
851+ ) ) )
852+ }
853+ }
854+ }
855+
856+ /// Get a single milestone by number
857+ pub async fn get_milestone ( & self , repo : & str , number : u64 ) -> Result < GitHubMilestone > {
858+ let url = format ! (
859+ "{}/repos/{}/{}/milestones/{}" ,
860+ self . rest_base_url, self . config. owner, repo, number
861+ ) ;
862+
863+ debug ! ( repo = %repo, number = %number, "Fetching GitHub milestone" ) ;
864+
865+ let mut request = self . client . get ( & url) ;
866+ if let Some ( ref token) = self . auth_token {
867+ request = request. bearer_auth ( token) ;
868+ }
869+
870+ let response = request. timeout ( GET_TIMEOUT ) . send ( ) . await ?;
871+
872+ match response. status ( ) {
873+ StatusCode :: OK => Ok ( response. json ( ) . await ?) ,
874+ StatusCode :: NOT_FOUND => Err ( crate :: AllBeadsError :: Integration ( format ! (
875+ "Milestone not found: {}/{}#{}" ,
876+ self . config. owner, repo, number
877+ ) ) ) ,
878+ status => {
879+ let error_body = response. text ( ) . await . unwrap_or_default ( ) ;
880+ Err ( crate :: AllBeadsError :: Integration ( format ! (
881+ "GitHub milestone fetch failed: HTTP {}: {}" ,
882+ status, error_body
883+ ) ) )
884+ }
885+ }
886+ }
887+
888+ /// Create a new milestone
889+ pub async fn create_milestone (
890+ & self ,
891+ repo : & str ,
892+ request : MilestoneRequest ,
893+ ) -> Result < GitHubMilestone > {
894+ let url = format ! (
895+ "{}/repos/{}/{}/milestones" ,
896+ self . rest_base_url, self . config. owner, repo
897+ ) ;
898+
899+ info ! ( repo = %repo, title = %request. title, "Creating GitHub milestone" ) ;
900+
901+ let mut http_request = self . client . post ( & url) . json ( & request) ;
902+ if let Some ( ref token) = self . auth_token {
903+ http_request = http_request. bearer_auth ( token) ;
904+ }
905+
906+ let response = http_request. timeout ( WRITE_TIMEOUT ) . send ( ) . await ?;
907+
908+ match response. status ( ) {
909+ StatusCode :: CREATED => {
910+ let milestone: GitHubMilestone = response. json ( ) . await ?;
911+ info ! ( number = milestone. number, "GitHub milestone created" ) ;
912+ Ok ( milestone)
913+ }
914+ StatusCode :: UNPROCESSABLE_ENTITY => {
915+ let error_body = response. text ( ) . await . unwrap_or_default ( ) ;
916+ Err ( crate :: AllBeadsError :: Integration ( format ! (
917+ "Invalid milestone data: {}" ,
918+ error_body
919+ ) ) )
920+ }
921+ status => {
922+ let error_body = response. text ( ) . await . unwrap_or_default ( ) ;
923+ Err ( crate :: AllBeadsError :: Integration ( format ! (
924+ "GitHub milestone creation failed: HTTP {}: {}" ,
925+ status, error_body
926+ ) ) )
927+ }
928+ }
929+ }
930+
931+ /// Update an existing milestone
932+ pub async fn update_milestone (
933+ & self ,
934+ repo : & str ,
935+ number : u64 ,
936+ request : MilestoneRequest ,
937+ ) -> Result < GitHubMilestone > {
938+ let url = format ! (
939+ "{}/repos/{}/{}/milestones/{}" ,
940+ self . rest_base_url, self . config. owner, repo, number
941+ ) ;
942+
943+ info ! ( repo = %repo, number = %number, "Updating GitHub milestone" ) ;
944+
945+ let mut http_request = self . client . patch ( & url) . json ( & request) ;
946+ if let Some ( ref token) = self . auth_token {
947+ http_request = http_request. bearer_auth ( token) ;
948+ }
949+
950+ let response = http_request. timeout ( WRITE_TIMEOUT ) . send ( ) . await ?;
951+
952+ match response. status ( ) {
953+ StatusCode :: OK => Ok ( response. json ( ) . await ?) ,
954+ StatusCode :: NOT_FOUND => Err ( crate :: AllBeadsError :: Integration ( format ! (
955+ "Milestone not found: {}/{}#{}" ,
956+ self . config. owner, repo, number
957+ ) ) ) ,
958+ status => {
959+ let error_body = response. text ( ) . await . unwrap_or_default ( ) ;
960+ Err ( crate :: AllBeadsError :: Integration ( format ! (
961+ "GitHub milestone update failed: HTTP {}: {}" ,
962+ status, error_body
963+ ) ) )
964+ }
965+ }
966+ }
967+
968+ /// Delete a milestone
969+ pub async fn delete_milestone ( & self , repo : & str , number : u64 ) -> Result < ( ) > {
970+ let url = format ! (
971+ "{}/repos/{}/{}/milestones/{}" ,
972+ self . rest_base_url, self . config. owner, repo, number
973+ ) ;
974+
975+ info ! ( repo = %repo, number = %number, "Deleting GitHub milestone" ) ;
976+
977+ let mut request = self . client . delete ( & url) ;
978+ if let Some ( ref token) = self . auth_token {
979+ request = request. bearer_auth ( token) ;
980+ }
981+
982+ let response = request. timeout ( WRITE_TIMEOUT ) . send ( ) . await ?;
983+
984+ match response. status ( ) {
985+ StatusCode :: NO_CONTENT => Ok ( ( ) ) ,
986+ StatusCode :: NOT_FOUND => Err ( crate :: AllBeadsError :: Integration ( format ! (
987+ "Milestone not found: {}/{}#{}" ,
988+ self . config. owner, repo, number
989+ ) ) ) ,
990+ status => {
991+ let error_body = response. text ( ) . await . unwrap_or_default ( ) ;
992+ Err ( crate :: AllBeadsError :: Integration ( format ! (
993+ "GitHub milestone deletion failed: HTTP {}: {}" ,
994+ status, error_body
995+ ) ) )
996+ }
997+ }
998+ }
999+
1000+ /// Assign an issue to a milestone
1001+ pub async fn assign_issue_to_milestone (
1002+ & self ,
1003+ repo : & str ,
1004+ issue_number : u64 ,
1005+ milestone_number : u64 ,
1006+ ) -> Result < GitHubIssue > {
1007+ let url = format ! (
1008+ "{}/repos/{}/{}/issues/{}" ,
1009+ self . rest_base_url, self . config. owner, repo, issue_number
1010+ ) ;
1011+
1012+ info ! (
1013+ repo = %repo,
1014+ issue = %issue_number,
1015+ milestone = %milestone_number,
1016+ "Assigning issue to milestone"
1017+ ) ;
1018+
1019+ let body = serde_json:: json!( { "milestone" : milestone_number } ) ;
1020+
1021+ let mut request = self . client . patch ( & url) . json ( & body) ;
1022+ if let Some ( ref token) = self . auth_token {
1023+ request = request. bearer_auth ( token) ;
1024+ }
1025+
1026+ let response = request. timeout ( WRITE_TIMEOUT ) . send ( ) . await ?;
1027+
1028+ match response. status ( ) {
1029+ StatusCode :: OK => Ok ( response. json ( ) . await ?) ,
1030+ StatusCode :: NOT_FOUND => Err ( crate :: AllBeadsError :: Integration ( format ! (
1031+ "Issue or milestone not found: {}/{}#{}" ,
1032+ self . config. owner, repo, issue_number
1033+ ) ) ) ,
1034+ status => {
1035+ let error_body = response. text ( ) . await . unwrap_or_default ( ) ;
1036+ Err ( crate :: AllBeadsError :: Integration ( format ! (
1037+ "GitHub issue update failed: HTTP {}: {}" ,
1038+ status, error_body
1039+ ) ) )
1040+ }
1041+ }
1042+ }
7741043}
7751044
7761045#[ cfg( test) ]
0 commit comments