plz / ci / ci_routes.v
285 lines · 240 sloc · 6.62 KB · c1be7a1408ed30cff2e8e359cf7e1ef69d66e988
Raw
1module main
2
3import veb
4import api
5import x.json2 as json
6import net.http
7import time
8import git
9
10struct CiStatusCallback {
11 run_id string
12 repo_id string
13 commit_hash string
14 branch string
15 status string
16}
17
18// POST /api/v1/ci/status - Callback endpoint for gitly_ci to report status updates
19@['/api/v1/ci/status'; post]
20pub fn (mut app App) handle_ci_status_callback() veb.Result {
21 body := ctx.req.data
22 callback := json.decode[CiStatusCallback](body) or {
23 return ctx.json_error('Invalid request body')
24 }
25
26 repo_id := callback.repo_id.int()
27 ci_run_id := callback.run_id.int()
28 status := ci_status_from_string(callback.status)
29
30 app.upsert_ci_status(repo_id, callback.commit_hash, callback.branch, status, ci_run_id) or {
31 return ctx.json_error('Failed to update CI status: ${err}')
32 }
33
34 return ctx.json(api.ApiSuccessResponse[string]{
35 success: true
36 result: 'ok'
37 })
38}
39
40// GET /:username/:repo_name/ci - CI runs list page
41@['/:username/:repo_name/ci']
42pub fn (mut app App) ci_runs(username string, repo_name string) veb.Result {
43 repo := app.find_repo_by_name_and_username(repo_name, username) or { return ctx.not_found() }
44
45 if !repo.is_public {
46 if repo.user_id != ctx.user.id {
47 return ctx.not_found()
48 }
49 }
50
51 // Check if .gitly-ci.yml exists in the repo
52 has_ci_file := git.Git.exec_in_dir(repo.git_dir,
53 ['show', '${repo.primary_branch}:.gitly-ci.yml']).exit_code == 0
54
55 // Fetch runs from gitly_ci service for a complete list
56 mut ci_runs := []CiRunListItem{}
57 mut ci_service_error := false
58 if app.config.ci_service_url != '' {
59 runs_url := '${app.config.ci_service_url}/api/v1/runs/repo/${repo.id}'
60 response := http.get(runs_url) or {
61 ci_service_error = true
62 http.Response{}
63 }
64 if !ci_service_error && response.status_code == 200 {
65 runs_resp := json.decode[CiApiRunListResponse](response.body) or {
66 CiApiRunListResponse{}
67 }
68 if runs_resp.success {
69 for r in runs_resp.result {
70 ci_runs << CiRunListItem{
71 ci_run_id: r.id
72 status: ci_status_from_string(r.status)
73 commit_hash: r.commit_hash
74 branch: r.branch
75 created_at: r.created_at
76 finished_at: r.finished_at
77 }
78 }
79 }
80 } else if !ci_service_error && response.status_code != 200 {
81 ci_service_error = true
82 }
83 }
84
85 return $veb.html()
86}
87
88// GET /:username/:repo_name/ci/:run_id_str - CI run detail page
89@['/:username/:repo_name/ci/:run_id_str']
90pub fn (mut app App) ci_run_detail(username string, repo_name string, run_id_str string) veb.Result {
91 repo := app.find_repo_by_name_and_username(repo_name, username) or { return ctx.not_found() }
92
93 if !repo.is_public {
94 if repo.user_id != ctx.user.id {
95 return ctx.not_found()
96 }
97 }
98
99 ci_run_id := run_id_str.int()
100
101 // Fetch run details from gitly_ci service
102 if app.config.ci_service_url == '' {
103 return ctx.not_found()
104 }
105
106 ci_url := '${app.config.ci_service_url}/api/v1/runs/${ci_run_id}'
107 response := http.get(ci_url) or { return ctx.not_found() }
108
109 if response.status_code != 200 {
110 return ctx.not_found()
111 }
112
113 ci_run_json := response.body
114
115 // Parse the response to display
116 run_data := json.decode[CiApiRunResponse](ci_run_json) or { return ctx.not_found() }
117
118 ci_run := run_data.result
119
120 return $veb.html()
121}
122
123// POST /:username/:repo_name/ci/:run_id_str/restart - Restart a CI run
124@['/:username/:repo_name/ci/:run_id_str/restart'; post]
125pub fn (mut app App) ci_restart_run(username string, repo_name string, run_id_str string) veb.Result {
126 repo := app.find_repo_by_name_and_username(repo_name, username) or { return ctx.not_found() }
127
128 // Only repo owner can restart
129 if repo.user_id != ctx.user.id {
130 return ctx.not_found()
131 }
132
133 ci_run_id := run_id_str.int()
134
135 if app.config.ci_service_url == '' {
136 return ctx.not_found()
137 }
138
139 // Call gitly_ci restart API
140 restart_url := '${app.config.ci_service_url}/api/v1/runs/${ci_run_id}/restart'
141 response := http.post(restart_url, '') or { return ctx.not_found() }
142
143 if response.status_code != 200 {
144 return ctx.not_found()
145 }
146
147 result := json.decode[CiApiRunResponse](response.body) or { return ctx.not_found() }
148
149 if result.success {
150 new_run := result.result
151 // Update local CI status
152 app.upsert_ci_status(repo.id, new_run.commit_hash, new_run.branch, .pending, new_run.id) or {}
153 // Redirect to new run
154 return ctx.redirect('/${username}/${repo_name}/ci/${new_run.id}')
155 }
156
157 return ctx.redirect('/${username}/${repo_name}/ci/${ci_run_id}')
158}
159
160// Structs for parsing gitly_ci API responses
161
162struct CiApiRunListResponse {
163 success bool
164 result []CiRunListResponseItem
165}
166
167struct CiRunListResponseItem {
168 id int
169 status string
170 commit_hash string
171 branch string
172 created_at int
173 finished_at int
174}
175
176struct CiRunListItem {
177 ci_run_id int
178 status CiStatusEnum
179 commit_hash string
180 branch string
181 created_at int
182 finished_at int
183}
184
185fn (ci &CiRunListItem) relative_time() string {
186 if ci.finished_at > 0 {
187 return time.unix(ci.finished_at).relative()
188 }
189 if ci.created_at > 0 {
190 return time.unix(ci.created_at).relative()
191 }
192 return ''
193}
194
195struct CiApiRunResponse {
196 success bool
197 result CiRunDetail
198}
199
200struct CiRunDetail {
201 id int
202 status string
203 commit_hash string
204 branch string
205 created_at int
206 finished_at int
207 jobs []CiJobDetail
208}
209
210struct CiJobDetail {
211 id int
212 name string
213 status string
214 exit_code int
215 started_at int
216 finished_at int
217 steps []CiStepDetail
218}
219
220struct CiStepDetail {
221 id int
222 name string
223 command string
224 status string
225 output string
226 exit_code int
227}
228
229fn (r &CiRunDetail) status_css_class() string {
230 return match r.status {
231 'success' { 'ci-success' }
232 'failure' { 'ci-failure' }
233 'running' { 'ci-running' }
234 'cancelled' { 'ci-cancelled' }
235 else { 'ci-pending' }
236 }
237}
238
239fn (r &CiRunDetail) created_relative() string {
240 if r.created_at == 0 {
241 return ''
242 }
243 return time.unix(r.created_at).relative()
244}
245
246fn (r &CiRunDetail) duration() string {
247 if r.finished_at == 0 || r.created_at == 0 {
248 return 'running...'
249 }
250 d := r.finished_at - r.created_at
251 if d < 60 {
252 return '${d}s'
253 }
254 return '${d / 60}m ${d % 60}s'
255}
256
257fn (j &CiJobDetail) status_css_class() string {
258 return match j.status {
259 'success' { 'ci-success' }
260 'failure' { 'ci-failure' }
261 'running' { 'ci-running' }
262 'cancelled' { 'ci-cancelled' }
263 else { 'ci-pending' }
264 }
265}
266
267fn (s &CiStepDetail) status_css_class() string {
268 return match s.status {
269 'success' { 'ci-success' }
270 'failure' { 'ci-failure' }
271 'running' { 'ci-running' }
272 'cancelled' { 'ci-cancelled' }
273 else { 'ci-pending' }
274 }
275}
276
277fn (s &CiStepDetail) status_icon() string {
278 return match s.status {
279 'success' { '✓' }
280 'failure' { '✗' }
281 'running' { '⟳' }
282 'cancelled' { '⊘' }
283 else { '○' }
284 }
285}
286