blob: 35e4c3f9599d6ad66709c2e2b93574349d8dc34f [file]
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
}
}