| 1 | //! Checks over REST and MCP: statuses on commits, and check runs and |
| 2 | //! check suites, in GitHub's shapes so that existing integrations and |
| 3 | //! actions report to g1t unchanged. |
| 4 | //! |
| 5 | //! The work service keeps them and decides who may read and report them |
| 6 | //! (`g1t_contracts::checks`). A g1t Actions job is a check run here too, |
| 7 | //! and its workflow run the suite. |
| 8 | |
| 9 | use g1t_contracts::checks::*; |
| 10 | use g1t_contracts::{FailureCode, Outcome, Viewer}; |
| 11 | use serde::Serialize; |
| 12 | use serde::de::DeserializeOwned; |
| 13 | use serde_json::{Map, Value, json}; |
| 14 | use worker::Result; |
| 15 | |
| 16 | use crate::operations::Services; |
| 17 | |
| 18 | /// One operation on checks. |
| 19 | #[derive(Clone, Copy, Debug, PartialEq, Eq)] |
| 20 | pub enum ChecksOp { |
| 21 | CreateCommitStatus, |
| 22 | ListCommitStatuses, |
| 23 | GetCombinedStatus, |
| 24 | CreateCheckRun, |
| 25 | UpdateCheckRun, |
| 26 | GetCheckRun, |
| 27 | ListCheckRunAnnotations, |
| 28 | RerequestCheckRun, |
| 29 | ListCheckRunsForRef, |
| 30 | ListCheckSuitesForRef, |
| 31 | GetCheckSuite, |
| 32 | RerequestCheckSuite, |
| 33 | } |
| 34 | |
| 35 | impl ChecksOp { |
| 36 | /// Every one: `Op::ALL` lists each as `Op::Checks(…)`, which a test |
| 37 | /// checks against this. |
| 38 | #[cfg(test)] |
| 39 | pub const ALL: [ChecksOp; 12] = [ |
| 40 | ChecksOp::CreateCommitStatus, |
| 41 | ChecksOp::ListCommitStatuses, |
| 42 | ChecksOp::GetCombinedStatus, |
| 43 | ChecksOp::CreateCheckRun, |
| 44 | ChecksOp::UpdateCheckRun, |
| 45 | ChecksOp::GetCheckRun, |
| 46 | ChecksOp::ListCheckRunAnnotations, |
| 47 | ChecksOp::RerequestCheckRun, |
| 48 | ChecksOp::ListCheckRunsForRef, |
| 49 | ChecksOp::ListCheckSuitesForRef, |
| 50 | ChecksOp::GetCheckSuite, |
| 51 | ChecksOp::RerequestCheckSuite, |
| 52 | ]; |
| 53 | |
| 54 | pub fn name(self) -> &'static str { |
| 55 | match self { |
| 56 | ChecksOp::CreateCommitStatus => "create_commit_status", |
| 57 | ChecksOp::ListCommitStatuses => "list_commit_statuses", |
| 58 | ChecksOp::GetCombinedStatus => "get_combined_status", |
| 59 | ChecksOp::CreateCheckRun => "create_check_run", |
| 60 | ChecksOp::UpdateCheckRun => "update_check_run", |
| 61 | ChecksOp::GetCheckRun => "get_check_run", |
| 62 | ChecksOp::ListCheckRunAnnotations => "list_check_run_annotations", |
| 63 | ChecksOp::RerequestCheckRun => "rerequest_check_run", |
| 64 | ChecksOp::ListCheckRunsForRef => "list_check_runs_for_ref", |
| 65 | ChecksOp::ListCheckSuitesForRef => "list_check_suites_for_ref", |
| 66 | ChecksOp::GetCheckSuite => "get_check_suite", |
| 67 | ChecksOp::RerequestCheckSuite => "rerequest_check_suite", |
| 68 | } |
| 69 | } |
| 70 | |
| 71 | /// For the API reference. |
| 72 | pub fn title(self) -> &'static str { |
| 73 | match self { |
| 74 | ChecksOp::CreateCommitStatus => "Create a commit status", |
| 75 | ChecksOp::ListCommitStatuses => "List a commit's statuses", |
| 76 | ChecksOp::GetCombinedStatus => "Get a commit's combined status", |
| 77 | ChecksOp::CreateCheckRun => "Create a check run", |
| 78 | ChecksOp::UpdateCheckRun => "Update a check run", |
| 79 | ChecksOp::GetCheckRun => "Get a check run", |
| 80 | ChecksOp::ListCheckRunAnnotations => "List a check run's annotations", |
| 81 | ChecksOp::RerequestCheckRun => "Rerequest a check run", |
| 82 | ChecksOp::ListCheckRunsForRef => "List a commit's check runs", |
| 83 | ChecksOp::ListCheckSuitesForRef => "List a commit's check suites", |
| 84 | ChecksOp::GetCheckSuite => "Get a check suite", |
| 85 | ChecksOp::RerequestCheckSuite => "Rerequest a check suite", |
| 86 | } |
| 87 | } |
| 88 | |
| 89 | pub fn description(self) -> &'static str { |
| 90 | match self { |
| 91 | ChecksOp::CreateCommitStatus => "Set a status on a commit: state (pending, success, failure or error), context (what reports it, such as ci/build; default by default), description (at most 140 characters) and target_url (where to see more). A context has one status per commit: setting it again replaces it. A status is a check: a required check or a ruleset's required status check of its context is met by it. Needs the Write role; publishes status.created.", |
| 92 | ChecksOp::ListCommitStatuses => "List the statuses on a commit, named by its SHA, a branch or a tag: one per context, newest first. Check runs are listed by list_check_runs_for_ref instead.", |
| 93 | ChecksOp::GetCombinedStatus => "A commit's statuses, one per context, and the state they add up to: failure if any failed or errored, pending if any is pending or there are none, success otherwise. Named by its SHA, a branch or a tag.", |
| 94 | ChecksOp::CreateCheckRun => "Report a check run on a commit: name and head_sha are required; status (queued, in_progress or completed; queued by default), conclusion (success, failure, neutral, cancelled, skipped, timed_out or action_required; it makes the run completed), started_at and completed_at (RFC 3339; filled in when left out), details_url (your page for it), external_id (your id for it), output (title, summary and text in Markdown, and up to 50 annotations: path, start_line, end_line, start_column, end_column, annotation_level notice, warning or failure, message, title, raw_details) and actions (up to 3 buttons: label, description, identifier). app names who reports it, by default your token's name. Runs are grouped per reporter and commit into a check suite. A check run is a check: a required check of its name is met by it, a cancelled one failing. Needs the Write role; publishes check_run.created, and check_run.completed when it is created completed.", |
| 95 | ChecksOp::UpdateCheckRun => "Change a check run reported through the API, by id (cr_…). Fields left out stay as they are; output annotations are added to the ones it has (at most 1000 in all); actions, when given, replace its buttons. Giving a conclusion completes it. Publishes check_run.completed when it completes. A g1t Actions job's check run is its workflow's and cannot be changed.", |
| 96 | ChecksOp::GetCheckRun => "Get a check run by id: cr_… for one reported through the API, or a g1t Actions job's id (job_…), whose workflow run is its suite and whose workflow says its name, run and event in workflow.", |
| 97 | ChecksOp::ListCheckRunAnnotations => "List a check run's annotations in the order they were reported: path, start_line, end_line, start_column, end_column, annotation_level (notice, warning or failure), message, title and raw_details.", |
| 98 | ChecksOp::RerequestCheckRun => "Ask for a check run to run again. For one reported through the API, its reporter is sent check_run.rerequested; for a g1t Actions job, its workflow run runs again (which also needs workflows:write). Needs the Write role.", |
| 99 | ChecksOp::ListCheckRunsForRef => "List a commit's check runs, named by its SHA, a branch or a tag: those reported through the API and each job of its g1t Actions workflow runs. filter latest (the default) gives each name's latest run and each workflow's latest run per event; all gives every one. Narrow with check_name, status and app (a reporter's slug; actions for g1t Actions).", |
| 100 | ChecksOp::ListCheckSuitesForRef => "List a commit's check suites, named by its SHA, a branch or a tag: one per reporter that reported check runs on it through the API, and one per g1t Actions workflow run, with its status and conclusion worked out from its latest check runs. Narrow with app and check_name.", |
| 101 | ChecksOp::GetCheckSuite => "Get a check suite by id: cs_… for a reporter's, or a g1t Actions workflow run's id (run_…).", |
| 102 | ChecksOp::RerequestCheckSuite => "Ask for a check suite to run again: its reporter is sent check_suite.rerequested, or a g1t Actions workflow run runs again (which also needs workflows:write). Needs the Write role.", |
| 103 | } |
| 104 | } |
| 105 | |
| 106 | /// Whether it only reads, which anyone who can see the repository may. |
| 107 | pub fn reads(self) -> bool { |
| 108 | !matches!( |
| 109 | self, |
| 110 | ChecksOp::CreateCommitStatus |
| 111 | | ChecksOp::CreateCheckRun |
| 112 | | ChecksOp::UpdateCheckRun |
| 113 | | ChecksOp::RerequestCheckRun |
| 114 | | ChecksOp::RerequestCheckSuite |
| 115 | ) |
| 116 | } |
| 117 | |
| 118 | pub fn input(self) -> Value { |
| 119 | let repo = || json!({ "type": "string", "description": "Repository as \"owner/name\", e.g. \"flagon-io/hello\"." }); |
| 120 | let git_ref = || json!({ "type": "string", "description": "The commit: its SHA, a branch or a tag." }); |
| 121 | let run_id = || json!({ "type": "string", "description": "The check run's id: cr_…, or a g1t Actions job's job_…" }); |
| 122 | let suite_id = || json!({ "type": "string", "description": "The check suite's id: cs_…, or a g1t Actions run's run_…" }); |
| 123 | let run_fields = |mut properties: Value, creating: bool| { |
| 124 | properties["name"] = json!({ "type": "string", "description": "The check's name, at most 100 characters, such as lint or coverage." }); |
| 125 | if creating { |
| 126 | properties["head_sha"] = json!({ "type": "string", "description": "The commit's full SHA (or a branch or tag, read as the commit it points to now)." }); |
| 127 | properties["app"] = json!({ "type": "string", "description": "Who reports it, shown with it and grouping its check suite: by default your token's name." }); |
| 128 | } |
| 129 | properties["status"] = json!({ "type": "string", "enum": STATUSES, "description": "Where it is: queued, in_progress or completed." }); |
| 130 | properties["conclusion"] = json!({ "type": "string", "enum": CONCLUSIONS, "description": "How it came out; giving one completes it." }); |
| 131 | properties["started_at"] = json!({ "type": "string", "description": "When it started, RFC 3339." }); |
| 132 | properties["completed_at"] = json!({ "type": "string", "description": "When it completed, RFC 3339." }); |
| 133 | properties["details_url"] = json!({ "type": "string", "description": "Your page for it, http or https." }); |
| 134 | properties["external_id"] = json!({ "type": "string", "description": "Your id for it." }); |
| 135 | properties["output"] = json!({ |
| 136 | "type": "object", |
| 137 | "description": "Its report: a title, a Markdown summary and text, and annotations on lines of files (at most 50 a request).", |
| 138 | "properties": { |
| 139 | "title": { "type": "string" }, |
| 140 | "summary": { "type": "string" }, |
| 141 | "text": { "type": "string" }, |
| 142 | "annotations": { |
| 143 | "type": "array", |
| 144 | "items": { |
| 145 | "type": "object", |
| 146 | "properties": { |
| 147 | "path": { "type": "string" }, |
| 148 | "start_line": { "type": "integer" }, |
| 149 | "end_line": { "type": "integer" }, |
| 150 | "start_column": { "type": "integer" }, |
| 151 | "end_column": { "type": "integer" }, |
| 152 | "annotation_level": { "type": "string", "enum": ANNOTATION_LEVELS }, |
| 153 | "message": { "type": "string" }, |
| 154 | "title": { "type": "string" }, |
| 155 | "raw_details": { "type": "string" }, |
| 156 | }, |
| 157 | "required": ["path", "start_line", "end_line", "annotation_level", "message"], |
| 158 | }, |
| 159 | }, |
| 160 | }, |
| 161 | }); |
| 162 | properties["actions"] = json!({ |
| 163 | "type": "array", |
| 164 | "description": "Up to 3 buttons on its page. Pressing one sends you check_run.requested_action with its identifier.", |
| 165 | "items": { |
| 166 | "type": "object", |
| 167 | "properties": { |
| 168 | "label": { "type": "string", "description": "At most 20 characters." }, |
| 169 | "description": { "type": "string", "description": "At most 40 characters." }, |
| 170 | "identifier": { "type": "string", "description": "At most 20 characters." }, |
| 171 | }, |
| 172 | "required": ["label", "description", "identifier"], |
| 173 | }, |
| 174 | }); |
| 175 | properties |
| 176 | }; |
| 177 | let (properties, required): (Value, &[&str]) = match self { |
| 178 | ChecksOp::CreateCommitStatus => ( |
| 179 | json!({ |
| 180 | "repo": repo(), |
| 181 | "sha": { "type": "string", "description": "The commit's full SHA." }, |
| 182 | "state": { "type": "string", "enum": STATUS_STATES, "description": "pending, success, failure or error." }, |
| 183 | "context": { "type": "string", "description": "What reports it, such as ci/build; default when left out." }, |
| 184 | "description": { "type": "string", "description": "A short word on it, at most 140 characters." }, |
| 185 | "target_url": { "type": "string", "description": "Where to see more, http or https." }, |
| 186 | }), |
| 187 | &["repo", "sha", "state"], |
| 188 | ), |
| 189 | ChecksOp::ListCommitStatuses | ChecksOp::GetCombinedStatus => (json!({ "repo": repo(), "ref": git_ref() }), &["repo", "ref"]), |
| 190 | ChecksOp::CreateCheckRun => (run_fields(json!({ "repo": repo() }), true), &["repo", "name", "head_sha"]), |
| 191 | ChecksOp::UpdateCheckRun => (run_fields(json!({ "repo": repo(), "id": run_id() }), false), &["repo", "id"]), |
| 192 | ChecksOp::GetCheckRun | ChecksOp::ListCheckRunAnnotations | ChecksOp::RerequestCheckRun => { |
| 193 | (json!({ "repo": repo(), "id": run_id() }), &["repo", "id"]) |
| 194 | } |
| 195 | ChecksOp::ListCheckRunsForRef => ( |
| 196 | json!({ |
| 197 | "repo": repo(), |
| 198 | "ref": git_ref(), |
| 199 | "check_name": { "type": "string", "description": "Only runs of this name." }, |
| 200 | "status": { "type": "string", "enum": STATUSES, "description": "Only runs in this status." }, |
| 201 | "app": { "type": "string", "description": "Only this reporter's runs, by slug: actions for g1t Actions." }, |
| 202 | "filter": { "type": "string", "enum": ["latest", "all"], "description": "latest (the default) or all." }, |
| 203 | }), |
| 204 | &["repo", "ref"], |
| 205 | ), |
| 206 | ChecksOp::ListCheckSuitesForRef => ( |
| 207 | json!({ |
| 208 | "repo": repo(), |
| 209 | "ref": git_ref(), |
| 210 | "app": { "type": "string", "description": "Only this reporter's suites, by slug." }, |
| 211 | "check_name": { "type": "string", "description": "Only suites with a run of this name." }, |
| 212 | }), |
| 213 | &["repo", "ref"], |
| 214 | ), |
| 215 | ChecksOp::GetCheckSuite | ChecksOp::RerequestCheckSuite => (json!({ "repo": repo(), "id": suite_id() }), &["repo", "id"]), |
| 216 | }; |
| 217 | json!({ "type": "object", "properties": properties, "required": required }) |
| 218 | } |
| 219 | } |
| 220 | |
| 221 | fn text(input: &Value, key: &str) -> Option<String> { |
| 222 | match &input[key] { |
| 223 | Value::String(text) => Some(text.trim().to_owned()).filter(|text| !text.is_empty()), |
| 224 | _ => None, |
| 225 | } |
| 226 | } |
| 227 | |
| 228 | /// A key in `camelCase`, as the contracts read it: `start_line` is `startLine`. |
| 229 | fn camel_key(key: &str) -> String { |
| 230 | let mut out = String::with_capacity(key.len()); |
| 231 | let mut upper = false; |
| 232 | for c in key.chars() { |
| 233 | if c == '_' { |
| 234 | upper = true; |
| 235 | } else if upper { |
| 236 | out.extend(c.to_uppercase()); |
| 237 | upper = false; |
| 238 | } else { |
| 239 | out.push(c); |
| 240 | } |
| 241 | } |
| 242 | out |
| 243 | } |
| 244 | |
| 245 | fn camel(value: &Value) -> Value { |
| 246 | match value { |
| 247 | Value::Object(fields) => Value::Object(fields.iter().map(|(key, value)| (camel_key(key), camel(value))).collect::<Map<_, _>>()), |
| 248 | Value::Array(items) => Value::Array(items.iter().map(camel).collect()), |
| 249 | other => other.clone(), |
| 250 | } |
| 251 | } |
| 252 | |
| 253 | /// A check run's fields from a request body. |
| 254 | pub(crate) fn run_input(input: &Value) -> std::result::Result<CheckRunInput, String> { |
| 255 | const FIELDS: [&str; 10] = |
| 256 | ["name", "head_sha", "status", "conclusion", "started_at", "completed_at", "details_url", "external_id", "output", "actions"]; |
| 257 | let mut fields = Map::new(); |
| 258 | for key in FIELDS { |
| 259 | if let Some(value) = input.get(key).filter(|value| !value.is_null()) { |
| 260 | fields.insert(camel_key(key), camel(value)); |
| 261 | } |
| 262 | } |
| 263 | serde_json::from_value(Value::Object(fields)).map_err(|error| format!("The check run could not be read: {error}")) |
| 264 | } |
| 265 | |
| 266 | /// Pages on the site, which the work service names by path, as full |
| 267 | /// addresses. |
| 268 | fn absolute(value: Value, site: &str) -> Value { |
| 269 | match value { |
| 270 | Value::Object(fields) => Value::Object( |
| 271 | fields |
| 272 | .into_iter() |
| 273 | .map(|(key, value)| { |
| 274 | let value = match value { |
| 275 | Value::String(path) if matches!(key.as_str(), "htmlUrl" | "detailsUrl" | "targetUrl") && path.starts_with('/') => { |
| 276 | Value::String(format!("{site}{path}")) |
| 277 | } |
| 278 | other => absolute(other, site), |
| 279 | }; |
| 280 | (key, value) |
| 281 | }) |
| 282 | .collect(), |
| 283 | ), |
| 284 | Value::Array(items) => Value::Array(items.into_iter().map(|item| absolute(item, site)).collect()), |
| 285 | other => other, |
| 286 | } |
| 287 | } |
| 288 | |
| 289 | async fn call<A: Serialize, T: DeserializeOwned + Serialize>(services: &Services, method: &str, args: &A) -> Result<Outcome<Value>> { |
| 290 | let found: Outcome<T> = g1t_kit::call(&services.work, method, args).await?; |
| 291 | Ok(match found { |
| 292 | Outcome::Ok(value) => Outcome::Ok(absolute(serde_json::to_value(value)?, &services.addresses.site)), |
| 293 | Outcome::Fail(failure) => Outcome::Fail(failure), |
| 294 | }) |
| 295 | } |
| 296 | |
| 297 | pub async fn run(op: ChecksOp, services: &Services, viewer: &Viewer, input: &Value) -> Result<Outcome<Value>> { |
| 298 | let Some(repo) = crate::operations::repo_path(input) else { |
| 299 | return Ok(Outcome::fail(FailureCode::Invalid, "Give the repository as \"owner/name\".")); |
| 300 | }; |
| 301 | let actor = || viewer.clone().unwrap_or_default(); |
| 302 | let id = || text(input, "id").unwrap_or_default(); |
| 303 | let git_ref = || text(input, "ref").unwrap_or_default(); |
| 304 | match op { |
| 305 | ChecksOp::CreateCommitStatus => { |
| 306 | let args = CreateStatusArgs { |
| 307 | actor: actor(), |
| 308 | repo, |
| 309 | sha: text(input, "sha").unwrap_or_default(), |
| 310 | state: text(input, "state").unwrap_or_default(), |
| 311 | context: text(input, "context"), |
| 312 | description: text(input, "description"), |
| 313 | target_url: text(input, "target_url"), |
| 314 | }; |
| 315 | call::<_, g1t_contracts::work::CommitStatus>(services, "create_commit_status", &args).await |
| 316 | } |
| 317 | ChecksOp::ListCommitStatuses => { |
| 318 | call::<_, Vec<g1t_contracts::work::CommitStatus>>(services, "commit_statuses", &RefArgs { viewer: viewer.clone(), repo, git_ref: git_ref() }).await |
| 319 | } |
| 320 | ChecksOp::GetCombinedStatus => { |
| 321 | call::<_, CombinedStatus>(services, "combined_status", &RefArgs { viewer: viewer.clone(), repo, git_ref: git_ref() }).await |
| 322 | } |
| 323 | ChecksOp::CreateCheckRun | ChecksOp::UpdateCheckRun => { |
| 324 | let run = match run_input(input) { |
| 325 | Ok(run) => run, |
| 326 | Err(message) => return Ok(Outcome::fail(FailureCode::Invalid, message)), |
| 327 | }; |
| 328 | if op == ChecksOp::CreateCheckRun { |
| 329 | let args = CreateCheckRunArgs { actor: actor(), repo, app: text(input, "app"), run }; |
| 330 | call::<_, CommitCheckRun>(services, "create_check_run", &args).await |
| 331 | } else { |
| 332 | call::<_, CommitCheckRun>(services, "update_check_run", &UpdateCheckRunArgs { actor: actor(), repo, id: id(), run }).await |
| 333 | } |
| 334 | } |
| 335 | ChecksOp::GetCheckRun => call::<_, CommitCheckRun>(services, "get_check_run", &CheckIdArgs { viewer: viewer.clone(), repo, id: id() }).await, |
| 336 | ChecksOp::ListCheckRunAnnotations => { |
| 337 | call::<_, Vec<CheckAnnotation>>(services, "check_run_annotations", &CheckIdArgs { viewer: viewer.clone(), repo, id: id() }).await |
| 338 | } |
| 339 | ChecksOp::GetCheckSuite => call::<_, CommitCheckSuite>(services, "get_check_suite", &CheckIdArgs { viewer: viewer.clone(), repo, id: id() }).await, |
| 340 | ChecksOp::RerequestCheckRun | ChecksOp::RerequestCheckSuite => { |
| 341 | let method = if op == ChecksOp::RerequestCheckRun { "rerequest_check_run" } else { "rerequest_check_suite" }; |
| 342 | let done: Outcome<bool> = g1t_kit::call(&services.work, method, &RerequestArgs { actor: actor(), repo, id: id() }).await?; |
| 343 | Ok(match done { |
| 344 | Outcome::Ok(_) => Outcome::Ok(json!({ "rerequested": true })), |
| 345 | Outcome::Fail(failure) => Outcome::Fail(failure), |
| 346 | }) |
| 347 | } |
| 348 | ChecksOp::ListCheckRunsForRef => { |
| 349 | let args = RefCheckRunsArgs { |
| 350 | viewer: viewer.clone(), |
| 351 | repo, |
| 352 | git_ref: git_ref(), |
| 353 | check_name: text(input, "check_name"), |
| 354 | status: text(input, "status"), |
| 355 | app: text(input, "app"), |
| 356 | filter: text(input, "filter"), |
| 357 | }; |
| 358 | call::<_, CheckRunList>(services, "ref_check_runs", &args).await |
| 359 | } |
| 360 | ChecksOp::ListCheckSuitesForRef => { |
| 361 | let args = RefCheckSuitesArgs { viewer: viewer.clone(), repo, git_ref: git_ref(), app: text(input, "app"), check_name: text(input, "check_name") }; |
| 362 | call::<_, CheckSuiteList>(services, "ref_check_suites", &args).await |
| 363 | } |
| 364 | } |
| 365 | } |
| 366 | |
| 367 | #[cfg(test)] |
| 368 | mod tests { |
| 369 | use super::*; |
| 370 | |
| 371 | #[test] |
| 372 | fn a_body_in_snake_case_is_a_check_run() { |
| 373 | let body = json!({ |
| 374 | "repo": "acme/web", |
| 375 | "name": "lint", |
| 376 | "head_sha": "a".repeat(40), |
| 377 | "status": "completed", |
| 378 | "conclusion": "failure", |
| 379 | "output": { |
| 380 | "title": "2 problems", |
| 381 | "summary": "**2** problems", |
| 382 | "annotations": [{ "path": "src/a.rs", "start_line": 3, "end_line": 3, "annotation_level": "warning", "message": "unused" }] |
| 383 | }, |
| 384 | "actions": [{ "label": "Fix", "description": "Fix it", "identifier": "fix" }] |
| 385 | }); |
| 386 | let run = run_input(&body).unwrap(); |
| 387 | assert_eq!(run.head_sha.as_deref(), Some("a".repeat(40).as_str())); |
| 388 | let output = run.output.unwrap(); |
| 389 | assert_eq!(output.annotations[0].start_line, 3); |
| 390 | assert_eq!(output.annotations[0].annotation_level, "warning"); |
| 391 | assert_eq!(run.actions.unwrap()[0].identifier, "fix"); |
| 392 | assert!(run_input(&json!({ "output": { "annotations": "no" } })).is_err()); |
| 393 | } |
| 394 | |
| 395 | #[test] |
| 396 | fn pages_on_the_site_become_full_addresses() { |
| 397 | let value = json!({ "checkRuns": [{ "htmlUrl": "/acme/web/checks/cr_1", "detailsUrl": "https://ci.example.com/1", "name": "/x" }] }); |
| 398 | let out = absolute(value, "https://g1t.sh"); |
| 399 | assert_eq!(out["checkRuns"][0]["htmlUrl"], "https://g1t.sh/acme/web/checks/cr_1"); |
| 400 | assert_eq!(out["checkRuns"][0]["detailsUrl"], "https://ci.example.com/1"); |
| 401 | assert_eq!(out["checkRuns"][0]["name"], "/x"); |
| 402 | } |
| 403 | |
| 404 | #[test] |
| 405 | fn each_operation_is_described_with_a_schema() { |
| 406 | for op in ChecksOp::ALL { |
| 407 | assert!(crate::operations::Op::ALL.contains(&crate::operations::Op::Checks(op)), "{}", op.name()); |
| 408 | assert!(!op.title().is_empty() && op.description().len() > 40, "{}", op.name()); |
| 409 | assert!(op.input()["required"].as_array().unwrap().contains(&json!("repo")), "{}", op.name()); |
| 410 | assert_eq!(op.reads(), g1t_contracts::scopes::scope_for(op.name()).unwrap().level() == g1t_contracts::scopes::Level::Read, "{}", op.name()); |
| 411 | } |
| 412 | } |
| 413 | } |