|
| 1 | +//! For pull requests that have changed the triagebot.toml, validate that the |
| 2 | +//! changes are a valid configuration file. |
| 3 | +//! It won't validate anything unless the PR is open and has changed. |
| 4 | +
|
| 5 | +use crate::{ |
| 6 | + config::{ValidateConfig, CONFIG_FILE_NAME}, |
| 7 | + handlers::{Context, IssuesEvent}, |
| 8 | +}; |
| 9 | +use tracing as log; |
| 10 | + |
| 11 | +pub(super) async fn parse_input( |
| 12 | + ctx: &Context, |
| 13 | + event: &IssuesEvent, |
| 14 | + _config: Option<&ValidateConfig>, |
| 15 | +) -> Result<Option<()>, String> { |
| 16 | + // All processing needs to be done in parse_input (instead of |
| 17 | + // handle_input) because we want this to *always* run. handle_input |
| 18 | + // requires the config to exist in triagebot.toml, but we want this to run |
| 19 | + // even if it isn't configured. As a consequence, error handling needs to |
| 20 | + // be a little more cautious here, since we don't want to relay |
| 21 | + // un-actionable errors to the user. |
| 22 | + let diff = match event.issue.diff(&ctx.github).await { |
| 23 | + Ok(Some(diff)) => diff, |
| 24 | + Ok(None) => return Ok(None), |
| 25 | + Err(e) => { |
| 26 | + log::error!("failed to get diff {e}"); |
| 27 | + return Ok(None); |
| 28 | + } |
| 29 | + }; |
| 30 | + if !diff.iter().any(|diff| diff.path == CONFIG_FILE_NAME) { |
| 31 | + return Ok(None); |
| 32 | + } |
| 33 | + |
| 34 | + let Some(pr_source) = &event.issue.head else { |
| 35 | + log::error!("expected head commit in {event:?}"); |
| 36 | + return Ok(None); |
| 37 | + }; |
| 38 | + let triagebot_content = match ctx |
| 39 | + .github |
| 40 | + .raw_file(&pr_source.repo.full_name, &pr_source.sha, CONFIG_FILE_NAME) |
| 41 | + .await |
| 42 | + { |
| 43 | + Ok(Some(c)) => c, |
| 44 | + Ok(None) => { |
| 45 | + log::error!("{CONFIG_FILE_NAME} modified, but failed to get content"); |
| 46 | + return Ok(None); |
| 47 | + } |
| 48 | + Err(e) => { |
| 49 | + log::error!("failed to get {CONFIG_FILE_NAME}: {e}"); |
| 50 | + return Ok(None); |
| 51 | + } |
| 52 | + }; |
| 53 | + |
| 54 | + let triagebot_content = String::from_utf8_lossy(&*triagebot_content); |
| 55 | + if let Err(e) = toml::from_str::<crate::handlers::Config>(&triagebot_content) { |
| 56 | + let position = match e.span() { |
| 57 | + // toml sometimes gives bad spans, see https://github.com/toml-rs/toml/issues/589 |
| 58 | + Some(span) if span != (0..0) => { |
| 59 | + let (line, col) = translate_position(&triagebot_content, span.start); |
| 60 | + let url = format!( |
| 61 | + "https://github.com/{}/blob/{}/{CONFIG_FILE_NAME}#L{line}", |
| 62 | + pr_source.repo.full_name, pr_source.sha |
| 63 | + ); |
| 64 | + format!(" at position [{line}:{col}]({url})",) |
| 65 | + } |
| 66 | + Some(_) | None => String::new(), |
| 67 | + }; |
| 68 | + |
| 69 | + return Err(format!( |
| 70 | + "Invalid `triagebot.toml`{position}:\n\ |
| 71 | + `````\n\ |
| 72 | + {e}\n\ |
| 73 | + `````", |
| 74 | + )); |
| 75 | + } |
| 76 | + Ok(None) |
| 77 | +} |
| 78 | + |
| 79 | +pub(super) async fn handle_input( |
| 80 | + _ctx: &Context, |
| 81 | + _config: &ValidateConfig, |
| 82 | + _event: &IssuesEvent, |
| 83 | + _input: (), |
| 84 | +) -> anyhow::Result<()> { |
| 85 | + Ok(()) |
| 86 | +} |
| 87 | + |
| 88 | +/// Helper to translate a toml span to a `(line_no, col_no)` (1-based). |
| 89 | +fn translate_position(input: &str, index: usize) -> (usize, usize) { |
| 90 | + if input.is_empty() { |
| 91 | + return (0, index); |
| 92 | + } |
| 93 | + |
| 94 | + let safe_index = index.min(input.len() - 1); |
| 95 | + let column_offset = index - safe_index; |
| 96 | + |
| 97 | + let nl = input[0..safe_index] |
| 98 | + .as_bytes() |
| 99 | + .iter() |
| 100 | + .rev() |
| 101 | + .enumerate() |
| 102 | + .find(|(_, b)| **b == b'\n') |
| 103 | + .map(|(nl, _)| safe_index - nl - 1); |
| 104 | + let line_start = match nl { |
| 105 | + Some(nl) => nl + 1, |
| 106 | + None => 0, |
| 107 | + }; |
| 108 | + let line = input[0..line_start] |
| 109 | + .as_bytes() |
| 110 | + .iter() |
| 111 | + .filter(|c| **c == b'\n') |
| 112 | + .count(); |
| 113 | + let column = input[line_start..=safe_index].chars().count() - 1; |
| 114 | + let column = column + column_offset; |
| 115 | + |
| 116 | + (line + 1, column + 1) |
| 117 | +} |
0 commit comments