| use std::collections::HashMap; | |
| use anyhow::Context; | |
| use build_helper::metrics::{CiMetadata, JsonRoot}; | |
| pub struct GitHubClient; | |
| impl GitHubClient { | |
| fn get_workflow_run_jobs( | |
| &self, | |
| repo: &str, | |
| workflow_run_id: u64, | |
| ) -> anyhow::Result<Vec<GitHubJob>> { | |
| let req = ureq::get(format!( | |
| "https://api.github.com/repos/{repo}/actions/runs/{workflow_run_id}/jobs?per_page=100" | |
| )) | |
| .header("User-Agent", "rust-lang/rust/citool") | |
| .header("Accept", "application/vnd.github+json") | |
| .header("X-GitHub-Api-Version", "2022-11-28") | |
| .call() | |
| .context("cannot get workflow job list")?; | |
| let status = req.status(); | |
| let mut body = req.into_body(); | |
| if status.is_success() { | |
| // This API response is actually paged, but we assume for now that there are at | |
| // most 100 jobs per workflow. | |
| let response = body | |
| .read_json::<WorkflowRunJobsResponse>() | |
| .context("cannot deserialize workflow run jobs response")?; | |
| // The CI job names have a prefix, e.g. `auto - foo`. We remove the prefix here to | |
| // normalize the job name. | |
| Ok(response | |
| .jobs | |
| .into_iter() | |
| .map(|mut job| { | |
| job.name = job | |
| .name | |
| .split_once(" - ") | |
| .map(|res| res.1.to_string()) | |
| .unwrap_or_else(|| job.name); | |
| job | |
| }) | |
| .collect()) | |
| } else { | |
| Err(anyhow::anyhow!( | |
| "Cannot get jobs of workflow run {workflow_run_id}: {status}\n{}", | |
| body.read_to_string()? | |
| )) | |
| } | |
| } | |
| } | |
| #[derive(serde::Deserialize)] | |
| struct WorkflowRunJobsResponse { | |
| jobs: Vec<GitHubJob>, | |
| } | |
| #[derive(serde::Deserialize)] | |
| struct GitHubJob { | |
| name: String, | |
| id: u64, | |
| } | |
| /// Can be used to resolve information about GitHub Actions jobs. | |
| /// Caches results internally to avoid too unnecessary GitHub API calls. | |
| pub struct JobInfoResolver { | |
| client: GitHubClient, | |
| // Workflow run ID -> jobs | |
| workflow_job_cache: HashMap<u64, Vec<GitHubJob>>, | |
| } | |
| impl JobInfoResolver { | |
| pub fn new() -> Self { | |
| Self { client: GitHubClient, workflow_job_cache: Default::default() } | |
| } | |
| /// Get a link to a job summary for the given job name and bootstrap execution. | |
| pub fn get_job_summary_link(&mut self, job_name: &str, metrics: &JsonRoot) -> Option<String> { | |
| metrics.ci_metadata.as_ref().and_then(|metadata| { | |
| self.get_job_id(metadata, job_name).map(|job_id| { | |
| format!( | |
| "https://github.com/{}/actions/runs/{}#summary-{job_id}", | |
| metadata.repository, metadata.workflow_run_id | |
| ) | |
| }) | |
| }) | |
| } | |
| fn get_job_id(&mut self, ci_metadata: &CiMetadata, job_name: &str) -> Option<u64> { | |
| if let Some(job) = self | |
| .workflow_job_cache | |
| .get(&ci_metadata.workflow_run_id) | |
| .and_then(|jobs| jobs.iter().find(|j| j.name == job_name)) | |
| { | |
| return Some(job.id); | |
| } | |
| let jobs = self | |
| .client | |
| .get_workflow_run_jobs(&ci_metadata.repository, ci_metadata.workflow_run_id) | |
| .inspect_err(|e| eprintln!("Cannot download workflow jobs: {e:?}")) | |
| .ok()?; | |
| let job_id = jobs.iter().find(|j| j.name == job_name).map(|j| j.id); | |
| // Save the cache even if the job name was not found, it could be useful for further lookups | |
| self.workflow_job_cache.insert(ci_metadata.workflow_run_id, jobs); | |
| job_id | |
| } | |
| } |