Skip to content
552 linesCodeBlameRaw

Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.

Merge Actions runs: summaries, attempts and re-runs, graceful cancel, log downloads, badges (actions 0009)1//! Reading: workflows, runs (any attempt), a run's jobs, a job's log and
2//! its steps' summaries.
GitHub Actions on g1t, part two: running workflows3
4use g1t_actions::workflow::{self, Severity};
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look5use g1t_contracts::access::Capability;
Merge checks: statuses and check runs on every commit6use g1t_contracts::checks::{ActionsChecksArgs, MAX_COMMITS};
GitHub Actions on g1t, part two: running workflows7use g1t_contracts::actions::{
Merge Actions runs: summaries, attempts and re-runs, graceful cancel, log downloads, badges (actions 0009)8 Annotation, Job, JobLog, JobLogText, JobLogTextArgs, JobSummary, LogChunk, LogsArgs, MAX_RUN_LOG_BYTES, RunArgs, RunAttempt, RunDetail,
9 RunLogsArgs, RunsArgs, SetWorkflowEnabledArgs, StepState, StepSummary, SummariesArgs, Workflow, WorkflowNote, WorkflowRun, WorkflowsArgs,
GitHub Actions on g1t, part two: running workflows10};
11use g1t_contracts::{FailureCode, Outcome};
12use serde::Deserialize;
13use worker::Result;
14
15use crate::plan::{JobRow, RunRow};
16use crate::sync::WorkflowRow;
17use crate::{Actions, check, fail};
18
19const RUNS_SHOWN: u32 = 50;
20
21fn notes(source: &str) -> Vec<WorkflowNote> {
22 workflow::parse(source)
23 .map(|w| {
24 w.notes
25 .into_iter()
26 .map(|note| WorkflowNote {
27 severity: match note.severity {
28 Severity::Info => "info",
29 Severity::Warning => "warning",
30 Severity::Unsupported => "unsupported",
31 }
32 .to_owned(),
33 job: note.job,
34 message: note.message,
35 })
36 .collect()
37 })
38 .unwrap_or_default()
39}
40
Merge Actions runs: summaries, attempts and re-runs, graceful cancel, log downloads, badges (actions 0009)41/// A job of an earlier attempt, as it ended (migration 0009).
42#[derive(Clone, Deserialize)]
43pub struct AttemptJobRow {
44 pub id: String,
45 pub run_id: String,
46 pub log_id: String,
47 pub key: String,
48 pub name: String,
49 pub needs: String,
50 pub status: String,
51 pub conclusion: Option<String>,
52 pub steps: String,
53 pub annotations: String,
54 pub reason: Option<String>,
55 pub environment: Option<String>,
56 pub labels: Option<String>,
57 pub runner_name: Option<String>,
58 pub started_at: Option<String>,
59 pub finished_at: Option<String>,
60}
61
62fn attempt_job_view(row: AttemptJobRow) -> Job {
63 Job {
64 needs: serde_json::from_str(&row.needs).unwrap_or_default(),
65 id: row.id,
66 run_id: row.run_id,
67 key: row.key,
68 name: row.name,
69 status: row.status,
70 conclusion: row.conclusion,
71 steps: serde_json::from_str::<Vec<StepState>>(&row.steps).unwrap_or_default(),
72 annotations: serde_json::from_str::<Vec<Annotation>>(&row.annotations).unwrap_or_default(),
73 reason: row.reason,
74 started_at: row.started_at,
75 finished_at: row.finished_at,
76 environment: row.environment,
77 self_hosted: row.labels.is_some(),
78 runner: row.runner_name,
79 cancelling: false,
80 }
81}
82
83/// An earlier attempt of a run, as it ended.
84#[derive(Clone, Deserialize)]
85struct AttemptRow {
86 attempt: u64,
87 conclusion: Option<String>,
88 actor: Option<String>,
89 debug: u32,
90 started_at: Option<String>,
91 finished_at: Option<String>,
92}
93
94/// One attempt's jobs, each with where its logs and summary are kept.
95struct AttemptJobs {
96 jobs: Vec<Job>,
97 log_ids: Vec<String>,
98}
99
GitHub Actions on g1t, part two: running workflows100fn job_view(row: JobRow) -> Job {
101 let needs = row.needs();
Merge Actions runs: summaries, attempts and re-runs, graceful cancel, log downloads, badges (actions 0009)102 let cancelling = row.cancel_requested_at.is_some() && row.status != "completed";
GitHub Actions on g1t, part two: running workflows103 Job {
Merge Actions runs: summaries, attempts and re-runs, graceful cancel, log downloads, badges (actions 0009)104 cancelling,
GitHub Actions on g1t, part two: running workflows105 id: row.id,
106 run_id: row.run_id,
107 key: row.key,
108 name: row.name,
109 needs,
110 status: row.status,
111 conclusion: row.conclusion,
112 steps: serde_json::from_str::<Vec<StepState>>(&row.steps).unwrap_or_default(),
113 annotations: serde_json::from_str::<Vec<Annotation>>(&row.annotations).unwrap_or_default(),
114 reason: row.reason,
115 started_at: row.started_at,
116 finished_at: row.finished_at,
Merge branch 'worktree-agent-a3abfcce648e87dca'117 environment: row.environment,
Fast pages, required checks on the branch, self-hosted runners, honest incidents118 self_hosted: row.labels.is_some(),
119 runner: row.runner_name,
GitHub Actions on g1t, part two: running workflows120 }
121}
122
123impl Actions {
124 async fn summary(&self, row: &WorkflowRow) -> Result<Workflow> {
125 let last_run = self
126 .db
127 .prepare("SELECT * FROM runs WHERE workflow_id = ? ORDER BY id DESC LIMIT 1")
128 .bind(&[row.id.as_str().into()])?
129 .first::<RunRow>(None)
130 .await?
131 .map(|run| run.summary());
132 let parsed = workflow::parse(&row.source).ok();
133 Ok(Workflow {
134 id: row.id.clone(),
135 path: row.path.clone(),
136 name: row.name.clone(),
137 events: serde_json::from_str(&row.events).unwrap_or_default(),
138 state: row.state.clone(),
139 error: row.error.clone(),
140 notes: notes(&row.source),
141 dispatch: parsed
142 .as_ref()
143 .filter(|_| row.error.is_none())
144 .and_then(|w| w.trigger("workflow_dispatch"))
145 .map(|t| serde_json::Value::Object(t.inputs.clone())),
146 last_run,
147 })
148 }
149
150 pub async fn workflows(&self, a: WorkflowsArgs) -> Result<Outcome<Vec<Workflow>>> {
151 let Some(repo) = self.visible_repo(&a.repo, &a.viewer).await? else {
152 return Ok(fail(FailureCode::NotFound, "There is no such repository."));
153 };
154 if !self.synced(&repo.id).await?
155 && let Some(ws) = self.workspace_actor(&repo.namespace).await?
156 {
157 self.sync(&repo, &ws).await?;
158 }
One CI in the workflow list: hide files from before .github stopped being read, and gone files a live one replaced159 // A file that is gone stays listed while it has runs, unless one in
160 // the folder now has its name: then it would read as a second copy.
161 // Files outside the folder are from before g1t stopped reading
162 // `.github`; their runs stay under All workflows.
GitHub Actions on g1t, part two: running workflows163 let rows = self
164 .db
165 .prepare(
One CI in the workflow list: hide files from before .github stopped being read, and gone files a live one replaced166 "SELECT * FROM workflows w WHERE w.repo_id = ?1 AND w.path LIKE ?2
167 AND (w.error IS NULL OR w.error NOT LIKE 'Its file is%'
168 OR (w.id IN (SELECT workflow_id FROM runs WHERE repo_id = ?1)
169 AND NOT EXISTS (SELECT 1 FROM workflows o WHERE o.repo_id = ?1 AND o.id <> w.id
170 AND o.name = w.name AND (o.error IS NULL OR o.error NOT LIKE 'Its file is%'))))
171 ORDER BY w.name",
GitHub Actions on g1t, part two: running workflows172 )
One CI in the workflow list: hide files from before .github stopped being read, and gone files a live one replaced173 .bind(&[repo.id.as_str().into(), format!("{}/%", g1t_actions::workflow::FOLDER).into()])?
GitHub Actions on g1t, part two: running workflows174 .all()
175 .await?
176 .results::<WorkflowRow>()?;
177 let mut out = Vec::with_capacity(rows.len());
178 for row in &rows {
179 out.push(self.summary(row).await?);
180 }
181 Ok(Outcome::Ok(out))
182 }
183
184 pub async fn runs(&self, a: RunsArgs) -> Result<Outcome<Vec<WorkflowRun>>> {
185 let Some(repo) = self.visible_repo(&a.repo, &a.viewer).await? else {
186 return Ok(fail(FailureCode::NotFound, "There is no such repository."));
187 };
188 let mut sql = "SELECT * FROM runs WHERE repo_id = ?".to_owned();
189 let mut binds: Vec<worker::wasm_bindgen::JsValue> = vec![repo.id.as_str().into()];
190 if let Some(workflow) = &a.workflow {
191 sql.push_str(" AND (workflow_id = ? OR path = ? OR path = ?)");
192 binds.push(workflow.as_str().into());
193 binds.push(workflow.as_str().into());
194 binds.push(format!("{}/{workflow}", g1t_actions::workflow::FOLDER).into());
195 }
196 if let Some(branch) = &a.branch {
197 sql.push_str(" AND (git_ref = ? OR json_extract(info, '$.headRef') = ?)");
198 binds.push(format!("refs/heads/{branch}").into());
199 binds.push(branch.as_str().into());
200 }
201 if let Some(event) = &a.event {
202 sql.push_str(" AND event = ?");
203 binds.push(event.as_str().into());
204 }
205 if let Some(pull) = a.pull {
206 sql.push_str(" AND pull = ?");
207 binds.push(pull.into());
208 }
209 if let Some(sha) = &a.sha {
210 sql.push_str(" AND sha = ?");
211 binds.push(sha.as_str().into());
212 }
213 sql.push_str(" ORDER BY id DESC LIMIT ?");
214 binds.push(a.limit.unwrap_or(RUNS_SHOWN).clamp(1, 100).into());
215 let rows = self.db.prepare(sql).bind(&binds)?.all().await?.results::<RunRow>()?;
216 Ok(Outcome::Ok(rows.iter().map(RunRow::summary).collect()))
217 }
218
Merge Actions runs: summaries, attempts and re-runs, graceful cancel, log downloads, badges (actions 0009)219 /// Every attempt of a run, oldest first, the current one last.
220 async fn attempts_of(&self, run: &RunRow) -> Result<Vec<RunAttempt>> {
221 let mut attempts: Vec<RunAttempt> = self
222 .db
223 .prepare("SELECT attempt, conclusion, actor, debug, started_at, finished_at FROM run_attempts WHERE run_id = ? ORDER BY attempt")
224 .bind(&[run.id.as_str().into()])?
225 .all()
226 .await?
227 .results::<AttemptRow>()?
228 .into_iter()
229 .filter(|row| row.attempt < run.attempt)
230 .map(|row| RunAttempt {
231 attempt: row.attempt,
232 status: "completed".to_owned(),
233 conclusion: row.conclusion,
234 actor: row.actor,
235 debug: row.debug != 0,
236 started_at: row.started_at,
237 finished_at: row.finished_at,
238 })
239 .collect();
240 attempts.push(RunAttempt {
241 attempt: run.attempt,
242 status: run.status.clone(),
243 conclusion: run.conclusion.clone().filter(|_| run.status == "completed"),
244 actor: run.triggering_actor.clone().or_else(|| run.actor.clone()),
245 debug: run.debug != 0,
246 started_at: run.started_at.clone(),
247 finished_at: run.finished_at.clone(),
248 });
249 Ok(attempts)
250 }
251
252 /// The jobs of one attempt: the live ones for the current attempt, the
253 /// kept ones for an earlier attempt. None for an attempt it never had.
254 async fn attempt_jobs(&self, run: &RunRow, attempt: Option<u64>) -> Result<Option<AttemptJobs>> {
255 match attempt.filter(|n| *n != run.attempt) {
256 None => {
257 let jobs: Vec<Job> = self.job_rows(&run.id).await?.into_iter().map(job_view).collect();
258 let log_ids = jobs.iter().map(|job| job.id.clone()).collect();
259 Ok(Some(AttemptJobs { jobs, log_ids }))
260 }
261 Some(n) if n == 0 || n > run.attempt => Ok(None),
262 Some(n) => {
263 let rows = self
264 .db
265 .prepare("SELECT * FROM job_attempts WHERE run_id = ? AND attempt = ? ORDER BY rowid")
266 .bind(&[run.id.as_str().into(), (n as f64).into()])?
267 .all()
268 .await?
269 .results::<AttemptJobRow>()?;
270 let log_ids = rows.iter().map(|row| row.log_id.clone()).collect();
271 Ok(Some(AttemptJobs { jobs: rows.into_iter().map(attempt_job_view).collect(), log_ids }))
272 }
273 }
274 }
275
GitHub Actions on g1t, part two: running workflows276 pub async fn run(&self, a: RunArgs) -> Result<Outcome<RunDetail>> {
277 if self.visible_repo(&a.repo, &a.viewer).await?.is_none() {
278 return Ok(fail(FailureCode::NotFound, "There is no such repository."));
279 }
280 let run = check!(self.run_in(&a.repo, &a.id).await?);
Merge Actions runs: summaries, attempts and re-runs, graceful cancel, log downloads, badges (actions 0009)281 let Some(AttemptJobs { jobs, .. }) = self.attempt_jobs(&run, a.attempt).await? else {
282 return Ok(fail(FailureCode::NotFound, format!("This run has no attempt {}.", a.attempt.unwrap_or_default())));
283 };
284 let attempts = self.attempts_of(&run).await?;
Merge branch 'worktree-agent-a3abfcce648e87dca'285 let mut summary = run.summary();
Merge Actions runs: summaries, attempts and re-runs, graceful cancel, log downloads, badges (actions 0009)286 // An earlier attempt: as it ended, and nothing waits for it.
287 if let Some(earlier) = a.attempt.filter(|n| *n != run.attempt).and_then(|n| attempts.iter().find(|at| at.attempt == n)) {
288 summary.attempt = earlier.attempt;
289 summary.status = "completed".to_owned();
290 summary.conclusion = earlier.conclusion.clone();
291 summary.started_at = earlier.started_at.clone();
292 summary.finished_at = earlier.finished_at.clone();
293 return Ok(Outcome::Ok(RunDetail {
294 notes: notes(&run.source),
295 run: summary,
296 jobs,
297 approval: run.approval(),
298 pending_deployments: Vec::new(),
299 attempts,
300 }));
301 }
302 let pending_deployments = self.pending_for(&run, &a.viewer).await?;
Merge branch 'worktree-agent-a3abfcce648e87dca'303 // Jobs held at an environment's rules: the run waits, as on GitHub.
304 if matches!(summary.status.as_str(), "queued" | "in_progress") && jobs.iter().any(|job| job.status == "pending") {
305 summary.status = "waiting".to_owned();
306 }
GitHub Actions on g1t, part two: running workflows307 Ok(Outcome::Ok(RunDetail {
308 notes: notes(&run.source),
Merge branch 'worktree-agent-a3abfcce648e87dca'309 run: summary,
GitHub Actions on g1t, part two: running workflows310 jobs,
Merge branch 'worktree-agent-a3abfcce648e87dca'311 approval: run.approval(),
312 pending_deployments,
Merge Actions runs: summaries, attempts and re-runs, graceful cancel, log downloads, badges (actions 0009)313 attempts,
GitHub Actions on g1t, part two: running workflows314 }))
315 }
316
Merge Actions runs: summaries, attempts and re-runs, graceful cancel, log downloads, badges (actions 0009)317 /// A job of the repository's runs by the id a run's page gives it: a
318 /// live job, or one of an earlier attempt. Its name, steps, where its
319 /// logs and summary are kept, and whether it has finished.
320 async fn job_for_log(&self, repo: &g1t_contracts::repos::RepoPath, id: &str) -> Result<Option<(Job, String, bool)>> {
321 let full = format!("{}/{}", repo.namespace, repo.name);
322 let live = self
GitHub Actions on g1t, part two: running workflows323 .db
324 .prepare("SELECT jobs.* FROM jobs JOIN runs ON runs.id = jobs.run_id WHERE jobs.id = ? AND lower(runs.repo) = lower(?)")
Merge Actions runs: summaries, attempts and re-runs, graceful cancel, log downloads, badges (actions 0009)325 .bind(&[id.into(), full.as_str().into()])?
GitHub Actions on g1t, part two: running workflows326 .first::<JobRow>(None)
327 .await?;
Merge Actions runs: summaries, attempts and re-runs, graceful cancel, log downloads, badges (actions 0009)328 if let Some(job) = live {
329 let done = job.status == "completed";
330 let view = job_view(job);
331 let log_id = view.id.clone();
332 return Ok(Some((view, log_id, done)));
333 }
334 let kept = self
335 .db
336 .prepare(
337 "SELECT job_attempts.* FROM job_attempts JOIN runs ON runs.id = job_attempts.run_id
338 WHERE job_attempts.id = ? AND lower(runs.repo) = lower(?)",
339 )
340 .bind(&[id.into(), full.as_str().into()])?
341 .first::<AttemptJobRow>(None)
342 .await?;
343 Ok(kept.map(|row| {
344 let log_id = row.log_id.clone();
345 (attempt_job_view(row), log_id, true)
346 }))
347 }
348
349 async fn log_chunks(&self, log_id: &str, after: u64, limit: u32) -> Result<Vec<LogChunk>> {
GitHub Actions on g1t, part two: running workflows350 #[derive(Deserialize)]
351 struct Row {
352 seq: u64,
353 step: u32,
354 text: String,
355 }
Merge Actions runs: summaries, attempts and re-runs, graceful cancel, log downloads, badges (actions 0009)356 Ok(self
GitHub Actions on g1t, part two: running workflows357 .db
Merge Actions runs: summaries, attempts and re-runs, graceful cancel, log downloads, badges (actions 0009)358 .prepare("SELECT seq, step, text FROM logs WHERE job_id = ? AND seq > ? ORDER BY seq LIMIT ?")
359 .bind(&[log_id.into(), (after as f64).into(), limit.into()])?
GitHub Actions on g1t, part two: running workflows360 .all()
361 .await?
362 .results::<Row>()?
363 .into_iter()
364 .map(|row| LogChunk { seq: row.seq, step: row.step, text: row.text })
Merge Actions runs: summaries, attempts and re-runs, graceful cancel, log downloads, badges (actions 0009)365 .collect())
366 }
367
368 /// Every chunk of a job's log, a page at a time.
369 async fn whole_log(&self, log_id: &str) -> Result<Vec<LogChunk>> {
370 let mut chunks: Vec<LogChunk> = Vec::new();
371 loop {
372 let after = chunks.last().map_or(0, |chunk| chunk.seq);
373 let page = self.log_chunks(log_id, after, 500).await?;
374 let more = page.len() == 500;
375 chunks.extend(page);
376 if !more {
377 return Ok(chunks);
378 }
379 }
380 }
381
382 pub async fn logs(&self, a: LogsArgs) -> Result<Outcome<JobLog>> {
383 if self.visible_repo(&a.repo, &a.viewer).await?.is_none() {
384 return Ok(fail(FailureCode::NotFound, "There is no such repository."));
385 }
386 let Some((_, log_id, done)) = self.job_for_log(&a.repo, &a.job).await? else {
387 return Ok(fail(FailureCode::NotFound, "No such job."));
388 };
389 let chunks = self.log_chunks(&log_id, a.after, 500).await?;
390 Ok(Outcome::Ok(JobLog { chunks, done }))
391 }
392
393 /// `job_log_text`: a job's whole log, to download.
394 pub async fn job_log_text(&self, a: JobLogTextArgs) -> Result<Outcome<JobLogText>> {
395 if self.visible_repo(&a.repo, &a.viewer).await?.is_none() {
396 return Ok(fail(FailureCode::NotFound, "There is no such repository."));
397 }
398 let Some((job, log_id, done)) = self.job_for_log(&a.repo, &a.job).await? else {
399 return Ok(fail(FailureCode::NotFound, "No such job."));
400 };
401 let chunks = self.whole_log(&log_id).await?;
402 Ok(Outcome::Ok(JobLogText { job_id: job.id, name: job.name, steps: job.steps, chunks, done, omitted: false }))
403 }
404
405 /// `run_logs`: every job's whole log for one attempt, to download as one
406 /// archive.
407 pub async fn run_logs(&self, a: RunLogsArgs) -> Result<Outcome<Vec<JobLogText>>> {
408 if self.visible_repo(&a.repo, &a.viewer).await?.is_none() {
409 return Ok(fail(FailureCode::NotFound, "There is no such repository."));
410 }
411 let run = check!(self.run_in(&a.repo, &a.id).await?);
412 let Some(AttemptJobs { jobs, log_ids }) = self.attempt_jobs(&run, a.attempt).await? else {
413 return Ok(fail(FailureCode::NotFound, format!("This run has no attempt {}.", a.attempt.unwrap_or_default())));
414 };
415 let mut out = Vec::with_capacity(jobs.len());
416 let mut total = 0usize;
417 for (job, log_id) in jobs.into_iter().zip(log_ids) {
418 let done = job.status == "completed";
419 let omitted = total >= MAX_RUN_LOG_BYTES;
420 let chunks = if omitted { Vec::new() } else { self.whole_log(&log_id).await? };
421 total += chunks.iter().map(|chunk| chunk.text.len()).sum::<usize>();
422 out.push(JobLogText { job_id: job.id, name: job.name, steps: job.steps, chunks, done, omitted });
423 }
424 Ok(Outcome::Ok(out))
425 }
426
427 /// `summaries`: what each job's steps wrote to `$GITHUB_STEP_SUMMARY`.
428 pub async fn summaries(&self, a: SummariesArgs) -> Result<Outcome<Vec<JobSummary>>> {
429 if self.visible_repo(&a.repo, &a.viewer).await?.is_none() {
430 return Ok(fail(FailureCode::NotFound, "There is no such repository."));
431 }
432 let run = check!(self.run_in(&a.repo, &a.id).await?);
433 let Some(AttemptJobs { jobs, log_ids }) = self.attempt_jobs(&run, a.attempt).await? else {
434 return Ok(fail(FailureCode::NotFound, format!("This run has no attempt {}.", a.attempt.unwrap_or_default())));
435 };
436 #[derive(Deserialize)]
437 struct Row {
438 job_id: String,
439 step: u32,
440 markdown: String,
441 }
442 let rows = self
443 .db
444 .prepare("SELECT job_id, step, markdown FROM job_summaries WHERE job_id IN (SELECT value FROM json_each(?)) ORDER BY step")
445 .bind(&[serde_json::to_string(&log_ids)?.into()])?
446 .all()
447 .await?
448 .results::<Row>()?;
449 Ok(Outcome::Ok(
450 jobs.into_iter()
451 .zip(log_ids)
452 .filter_map(|(job, log_id)| {
453 let steps: Vec<StepSummary> = rows
454 .iter()
455 .filter(|row| row.job_id == log_id)
456 .map(|row| StepSummary { step: row.step, markdown: row.markdown.clone() })
457 .collect();
458 (!steps.is_empty()).then_some(JobSummary { job_id: job.id, name: job.name, steps })
459 })
460 .collect(),
461 ))
GitHub Actions on g1t, part two: running workflows462 }
463
464 pub async fn set_workflow_enabled(&self, a: SetWorkflowEnabledArgs) -> Result<Outcome<Workflow>> {
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look465 if let Outcome::Fail(refused) = self.may(&a.actor, &a.repo, Capability::ManageSettings).await? {
GitHub Actions on g1t, part two: running workflows466 return Ok(Outcome::Fail(refused));
467 }
GitHub Actions on g1t, part three: .g1t/workflows, the pages, the docs468 let wanted = a.workflow.trim_start_matches(".g1t/workflows/");
GitHub Actions on g1t, part two: running workflows469 let row = self
470 .db
471 .prepare("SELECT * FROM workflows WHERE lower(repo) = lower(?) AND (id = ? OR path = ?)")
472 .bind(&[
473 format!("{}/{}", a.repo.namespace, a.repo.name).into(),
474 a.workflow.as_str().into(),
475 format!("{}/{wanted}", g1t_actions::workflow::FOLDER).into(),
476 ])?
477 .first::<WorkflowRow>(None)
478 .await?;
479 let Some(row) = row else {
480 return Ok(fail(FailureCode::NotFound, "No such workflow."));
481 };
482 let state = if a.enabled { "active" } else { "disabled" };
483 self.db.prepare("UPDATE workflows SET state = ? WHERE id = ?").bind(&[state.into(), row.id.as_str().into()])?.run().await?;
484 let row = WorkflowRow { state: state.to_owned(), ..row };
485 Ok(Outcome::Ok(self.summary(&row).await?))
486 }
Merge checks: statuses and check runs on every commit487
488 /// `check_runs`: workflow runs with their jobs, by commits, by a run's
489 /// id or by one of its jobs' ids, newest first, for the work service to
490 /// show as check suites and check runs. It decides who may see them.
491 pub async fn check_runs(&self, a: ActionsChecksArgs) -> Result<Vec<RunDetail>> {
492 let runs: Vec<RunRow> = if let Some(job) = &a.job_id {
493 self.db
494 .prepare("SELECT runs.* FROM runs JOIN jobs ON jobs.run_id = runs.id WHERE jobs.id = ? AND runs.repo_id = ?")
495 .bind(&[job.as_str().into(), a.repo_id.as_str().into()])?
496 .all()
497 .await?
498 .results::<RunRow>()?
499 } else if let Some(run) = &a.run_id {
500 self.db
501 .prepare("SELECT * FROM runs WHERE id = ? AND repo_id = ?")
502 .bind(&[run.as_str().into(), a.repo_id.as_str().into()])?
503 .all()
504 .await?
505 .results::<RunRow>()?
506 } else if a.shas.is_empty() {
507 Vec::new()
508 } else {
509 let shas: Vec<&String> = a.shas.iter().take(MAX_COMMITS).collect();
510 let marks = vec!["?"; shas.len()].join(", ");
511 let mut binds: Vec<worker::wasm_bindgen::JsValue> = vec![a.repo_id.as_str().into()];
512 binds.extend(shas.iter().map(|sha| sha.as_str().into()));
513 binds.push(CHECK_RUNS_LIMIT.into());
514 self.db
515 .prepare(format!("SELECT * FROM runs WHERE repo_id = ? AND sha IN ({marks}) ORDER BY id DESC LIMIT ?"))
516 .bind(&binds)?
517 .all()
518 .await?
519 .results::<RunRow>()?
520 };
521 if runs.is_empty() {
522 return Ok(Vec::new());
523 }
524 let marks = vec!["?"; runs.len()].join(", ");
525 let ids: Vec<worker::wasm_bindgen::JsValue> = runs.iter().map(|run| run.id.as_str().into()).collect();
526 let jobs = self
527 .db
528 .prepare(format!("SELECT * FROM jobs WHERE run_id IN ({marks}) ORDER BY rowid"))
529 .bind(&ids)?
530 .all()
531 .await?
532 .results::<JobRow>()?;
533 let mut by_run: std::collections::HashMap<String, Vec<Job>> = std::collections::HashMap::new();
534 for job in jobs {
535 by_run.entry(job.run_id.clone()).or_default().push(job_view(job));
536 }
537 Ok(runs
538 .iter()
Merge branch 'worktree-agent-a3abfcce648e87dca'539 .map(|run| RunDetail {
540 run: run.summary(),
541 jobs: by_run.remove(&run.id).unwrap_or_default(),
542 notes: Vec::new(),
543 approval: run.approval(),
544 pending_deployments: Vec::new(),
Merge Actions runs: summaries, attempts and re-runs, graceful cancel, log downloads, badges (actions 0009)545 attempts: Vec::new(),
Merge branch 'worktree-agent-a3abfcce648e87dca'546 })
Merge checks: statuses and check runs on every commit547 .collect())
548 }
GitHub Actions on g1t, part two: running workflows549}
Merge checks: statuses and check runs on every commit550
551/// The most runs `check_runs` reads for a set of commits.
552const CHECK_RUNS_LIMIT: u32 = 200;

This file's history is long; its oldest lines are credited to the oldest commit read.