1+ using System . Net . Http . Headers ;
2+ using System . Text ;
3+ using System . Text . Json ;
4+ using System . Text . RegularExpressions ;
5+ using Newtonsoft . Json ;
6+
7+ namespace TelegramBuildBot ;
8+
9+ public class Builder
10+ {
11+ public Builder ( string gitHubToken )
12+ {
13+ GitHubToken = gitHubToken ;
14+ }
15+
16+ private static readonly Dictionary < string , string ? > RepoToWorkflow = new ( )
17+ {
18+ { "ethpandaops/armiarma" , "build-push-armiarma.yml" } ,
19+ { "dapplion/beacon-metrics-gazer" , "build-push-beacon-metrics-gazer.yml" } ,
20+ { "hyperledger/besu" , "build-push-besu.yml" } ,
21+ { "ralexstokes/ethereum_consensus_monitor" , "build-push-consensus-monitor.yml" } ,
22+ { "sigp/eleel" , "build-push-eleel.yml" } ,
23+ { "ledgerwatch/erigon" , "build-push-erigon.yml" } ,
24+ { "ethpandaops/ethereum-genesis-generator" , "build-push-genesis-generator.yml" } ,
25+ { "ethereumjs/ethereumjs-monorepo" , "build-push-ethereumjs.yml" } ,
26+ { "ethereum/nodemonitor" , "build-push-execution-monitor.yml" } ,
27+ { "flashbots/builder" , "build-push-flashbots-builder.yml" } ,
28+ { "ethereum/go-ethereum" , "build-push-geth.yml" } ,
29+ { "ethpandaops/goomy-blob" , "build-push-goomy-blob.yml" } ,
30+ { "migalabs/goteth" , "build-push-goteth.yml" } ,
31+ { "grandinetech/grandine" , "build-push-grandine.yml" } ,
32+ { "sigp/lighthouse" , "build-push-lighthouse.yml" } ,
33+ { "chainsafe/lodestar" , "build-push-lodestar.yml" } ,
34+ { "ralexstokes/mev-rs" , "build-push-mev-rs.yml" } ,
35+ { "nethermindeth/nethermind" , "build-push-nethermind.yml" } ,
36+ { "status-im/nimbus-eth1" , "build-push-nimbus-eth1.yml" } ,
37+ { "status-im/nimbus-eth2" , "build-push-nimbus-eth2.yml" } ,
38+ { "prysmaticlabs/prysm" , "build-push-prysm.yml" } ,
39+ { "paradigmxyz/reth" , "build-push-reth.yml" } ,
40+ { "consensys/teku" , "build-push-teku.yml" } ,
41+ { "mariusvanderwijden/tx-fuzz" , "build-push-tx-fuzz.yml" } ,
42+ } ;
43+
44+ public string GitHubToken { get ; set ; }
45+
46+ public async Task < string ? > ProcessMessage ( string messageText )
47+ {
48+
49+ if ( messageText . StartsWith ( "/build" ) || messageText . StartsWith ( "/barnabas" ) )
50+ {
51+ string [ ] msgSegments = messageText . Split ( ' ' ) ;
52+ if ( msgSegments . Length < 2 )
53+ {
54+ Console . WriteLine ( "No repo link supplied" ) ;
55+ return "You need to supply a repository link with a branch\\ ." ;
56+ }
57+
58+ string url = msgSegments [ 1 ] ;
59+
60+ var regex = new Regex ( @"https:\/\/github\.com\/(.*?)\/tree\/(.*)" ) ;
61+ var match = regex . Match ( url ) ;
62+
63+ if ( match is { Success : true , Groups . Count : 3 } )
64+ {
65+ string ? repo = match . Groups [ 1 ] . Value ;
66+ string branch = match . Groups [ 2 ] . Value ;
67+ ( bool triggerSuccess , List < string > dockerImages , string runUrl ) = await TriggerGitHubWorkflow ( repo , branch ) ;
68+ if ( triggerSuccess )
69+ {
70+ Console . WriteLine ( $ "Build triggered for { repo } /{ branch } [Run URL: { runUrl } | DockerImage: { String . Join ( ':' , dockerImages ) } ]") ;
71+ return $ "Your build was triggered\\ . [View run on GitHub]({ runUrl } )\n Docker Image\\ (s\\ ) once run completed:\n { string . Join ( '\n ' , dockerImages . Select ( x => $ "`{ x } `") ) } ";
72+ }
73+
74+ Console . WriteLine ( "Unable to trigger build." ) ;
75+ return "Sorry\\ . Was unable to trigger your build\\ . Likely the repository is not supported\\ ." ;
76+
77+ }
78+
79+ Console . WriteLine ( "Unable to parse repo" ) ;
80+ return "Sorry\\ . Was unable to get the repository and branch from the URL\\ . Check your URL and try again\\ ." ;
81+ }
82+
83+ return null ;
84+ }
85+ private async Task < ( bool IsSuccessStatusCode , List < string > dockerImageUrls , string ? runUrl ) > TriggerGitHubWorkflow ( string ? repo , string branch )
86+ {
87+
88+ // Register a runner with github
89+ using HttpClient client = new ( ) ;
90+ client . DefaultRequestHeaders . Accept . Add ( new MediaTypeWithQualityHeaderValue ( "application/vnd.github+json" ) ) ;
91+ client . DefaultRequestHeaders . Add ( "X-GitHub-Api-Version" , "2022-11-28" ) ;
92+ client . DefaultRequestHeaders . Authorization = AuthenticationHeaderValue . Parse ( $ "Bearer { GitHubToken } ") ;
93+ client . DefaultRequestHeaders . UserAgent . Add ( new ProductInfoHeaderValue ( "build-bot" , "1" ) ) ;
94+
95+ var requestData = new {
96+ @ref = "master" , // Ref of the build repo
97+ inputs = new {
98+ repository = repo ,
99+ @ref = branch
100+ }
101+ } ;
102+
103+ bool isFork = false ;
104+
105+ // Check if given repo is a direct map
106+ if ( ! RepoToWorkflow . TryGetValue ( repo . ToLower ( ) , out string ? workflowId ) )
107+ {
108+ Console . WriteLine ( $ "Repo { repo } not in the workflow map. checking for fork.") ;
109+
110+ // check for fork
111+ var forkResp = await client . GetAsync ( $ "https://api.github.com/repos/{ repo } ") ;
112+ if ( forkResp . IsSuccessStatusCode )
113+ {
114+ // Got repo meta
115+ string forkContent = await forkResp . Content . ReadAsStringAsync ( ) ;
116+ JsonDocument forkJson = System . Text . Json . JsonSerializer . Deserialize < JsonDocument > ( forkContent ) ;
117+
118+ // check if fork
119+ isFork = forkJson . RootElement . GetProperty ( "fork" ) . GetBoolean ( ) ;
120+ if ( ! isFork )
121+ {
122+ // Not a fork - irgnore.
123+ return ( false , null , String . Empty ) ! ;
124+ }
125+
126+ string ? parentRepo = forkJson . RootElement . GetProperty ( "parent" ) . GetProperty ( "full_name" ) . GetString ( ) ;
127+
128+ // Check if we have a workflow for the parent
129+ if ( ! RepoToWorkflow . TryGetValue ( parentRepo . ToLower ( ) , out workflowId ) )
130+ {
131+ return ( false , null , String . Empty ) ! ;
132+ }
133+
134+ Console . WriteLine ( $ "Found valid parent repo at { parentRepo } ") ;
135+ }
136+ else
137+ {
138+ // Unable to grab repo meta - return error
139+ return ( false , null , String . Empty ) ! ;
140+ }
141+
142+ }
143+
144+ // Build tag url
145+
146+ // Grab the docker base from the workflow
147+ string dockerBase = Regex . Match ( workflowId , @"build-push-(.+)\.yml" ) . Groups [ 1 ] . Value ;
148+ List < string > dockerImageUrls = new ( ) ;
149+
150+ //string dockerhubPrefix = "{dockerhubPrefix}";
151+ string dockerhubPrefix = "" ;
152+ if ( dockerBase == "prysm" )
153+ {
154+ if ( isFork )
155+ {
156+ string forkUser = repo . Split ( '/' ) [ 0 ] ;
157+ dockerImageUrls . Add ( $ "{ dockerhubPrefix } ethpandaops/prysm-beacon-chain:{ forkUser } -{ branch } ") ;
158+ dockerImageUrls . Add ( $ "{ dockerhubPrefix } ethpandaops/prysm-validator:{ forkUser } -{ branch } ") ;
159+ }
160+ else
161+ {
162+ dockerImageUrls . Add ( $ "{ dockerhubPrefix } ethpandaops/prysm-beacon-chain:{ branch } ") ;
163+ dockerImageUrls . Add ( $ "{ dockerhubPrefix } ethpandaops/prysm-validator:{ branch } ") ;
164+ }
165+ }
166+ else if ( dockerBase == "nimbus-eth2" )
167+ {
168+ if ( isFork )
169+ {
170+ string forkUser = repo . Split ( '/' ) [ 0 ] ;
171+ dockerImageUrls . Add ( $ "{ dockerhubPrefix } ethpandaops/nimbus-eth2:{ forkUser } -{ branch } ") ;
172+ dockerImageUrls . Add ( $ "{ dockerhubPrefix } ethpandaops/nimbus-validator-client:{ forkUser } -{ branch } ") ;
173+ }
174+ else
175+ {
176+ dockerImageUrls . Add ( $ "{ dockerhubPrefix } ethpandaops/nimbus-eth2:{ branch } ") ;
177+ dockerImageUrls . Add ( $ "{ dockerhubPrefix } ethpandaops/nimbus-validator-client:{ branch } ") ;
178+ }
179+
180+ }
181+ else
182+ {
183+ if ( isFork )
184+ {
185+ string forkUser = repo . Split ( '/' ) [ 0 ] ;
186+ dockerImageUrls . Add ( $ "{ dockerhubPrefix } ethpandaops/{ dockerBase } :{ forkUser } -{ branch } ") ;
187+ }
188+ else
189+ {
190+ dockerImageUrls . Add ( $ "{ dockerhubPrefix } ethpandaops/{ dockerBase } :{ branch } ") ;
191+ }
192+ }
193+
194+ // Trigger job
195+ var content = new StringContent (
196+ JsonConvert . SerializeObject ( requestData ) ,
197+ Encoding . UTF8 ,
198+ "application/json" ) ;
199+
200+ HttpResponseMessage response = await client . PostAsync (
201+ $ "https://api.github.com/repos/ethpandaops/eth-client-docker-image-builder/actions/workflows/{ workflowId } /dispatches", content ) ;
202+
203+ // Grab job url from GH
204+ await Task . Delay ( 1500 ) ;
205+ HttpResponseMessage runsResponse = await client . GetAsync (
206+ $ "https://api.github.com/repos/ethpandaops/eth-client-docker-image-builder/actions/runs") ;
207+
208+ string ? runUrl = String . Empty ;
209+ if ( response . IsSuccessStatusCode )
210+ {
211+ string runsContent = await runsResponse . Content . ReadAsStringAsync ( ) ;
212+ JsonDocument ? responseObject = System . Text . Json . JsonSerializer . Deserialize < JsonDocument > ( runsContent ) ;
213+ if ( responseObject != null )
214+ {
215+ var firstRun = responseObject . RootElement . GetProperty ( "workflow_runs" ) . EnumerateArray ( ) . FirstOrDefault ( ) ;
216+ runUrl = firstRun . GetProperty ( "html_url" ) . GetString ( ) ;
217+ }
218+ }
219+
220+ return ( response . IsSuccessStatusCode , dockerImageUrls , runUrl ) ;
221+ }
222+
223+ }
0 commit comments