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.
| Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API | 1 | //! Every response the REST routes give, run through the converter the API |
| 2 | //! sends them with, checked for `camelCase` that would leak out. | |
| 3 | //! | |
| 4 | //! The samples are the reference's example responses, put back into the | |
| 5 | //! `camelCase` the services send (as serde's `rename_all` writes it) and, | |
| 6 | //! where an operation returns a contract type, decoded into that type and | |
| 7 | //! encoded again, so that every field the type has is sent, not only the | |
| 8 | //! ones an example shows. | |
| 9 | ||
| Merge checks: statuses and check runs on every commit | 10 | use g1t_contracts::{access, actions, checks, codeowners, integrations, repos, rules, search, teams, webhooks, work}; |
| Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API | 11 | use g1t_kit::wire::{self, USER_KEYED}; |
| 12 | use serde::Serialize; | |
| 13 | use serde::de::DeserializeOwned; | |
| 14 | use serde_json::{Map, Value, json}; | |
| 15 | ||
| 16 | use crate::openapi::document; | |
| 17 | use crate::operations::Op; | |
| Merge checks: statuses and check runs on every commit | 18 | use crate::checks::ChecksOp; |
| Merge rulesets: branch and tag rules, agent-first, enforced on push and merge | 19 | use crate::rules::RulesOp; |
| Merge branch 'worktree-agent-a5a5de74d8863d554' into worktree-agent-a16631325aecf58ca | 20 | use crate::deploy_keys::DeployKeysOp; |
| Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API | 21 | |
| 22 | /// A key as `#[serde(rename_all = "camelCase")]` writes it. | |
| 23 | fn camel_key(key: &str) -> String { | |
| 24 | let mut out = String::with_capacity(key.len()); | |
| 25 | let mut upper = false; | |
| 26 | for c in key.chars() { | |
| 27 | if c == '_' { | |
| 28 | upper = true; | |
| 29 | } else if upper { | |
| 30 | out.extend(c.to_uppercase()); | |
| 31 | upper = false; | |
| 32 | } else { | |
| 33 | out.push(c); | |
| 34 | } | |
| 35 | } | |
| 36 | out | |
| 37 | } | |
| 38 | ||
| 39 | /// A response as the services send it: `camelCase`, but for the maps the | |
| 40 | /// converter passes through, which are data. | |
| 41 | fn as_services_send(value: &Value) -> Value { | |
| 42 | match value { | |
| 43 | Value::Object(fields) => { | |
| 44 | let mut out = Map::new(); | |
| 45 | for (key, value) in fields { | |
| 46 | let user_keyed = value.is_object() | |
| 47 | && (USER_KEYED.contains(&key.as_str()) || key.starts_with("by_")); | |
| 48 | let value = if user_keyed { value.clone() } else { as_services_send(value) }; | |
| 49 | // A `by_…` map keeps its name in the converter's spelling. | |
| 50 | let key = if key.starts_with("by_") { key.clone() } else { camel_key(key) }; | |
| 51 | out.insert(key, value); | |
| 52 | } | |
| 53 | Value::Object(out) | |
| 54 | } | |
| 55 | Value::Array(items) => Value::Array(items.iter().map(as_services_send).collect()), | |
| 56 | other => other.clone(), | |
| 57 | } | |
| 58 | } | |
| 59 | ||
| 60 | /// `value` decoded as `T` and encoded again, as the service would send it. | |
| 61 | fn through<T: DeserializeOwned + Serialize>(op: Op, value: Value) -> Value { | |
| 62 | let decoded: T = serde_json::from_value(value) | |
| 63 | .unwrap_or_else(|error| panic!("{}: the example is not a {}: {error}", op.name(), std::any::type_name::<T>())); | |
| 64 | serde_json::to_value(decoded).unwrap() | |
| 65 | } | |
| 66 | ||
| 67 | /// What the service behind an operation sends, from its example. | |
| 68 | fn sample(op: Op, example: &Value) -> Value { | |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 69 | // Types serde already writes in `snake_case`: a person, and who has |
| 70 | // access. Sent as they are. | |
| 71 | let as_is = example.clone(); | |
| 72 | match op { | |
| 73 | Op::Whoami => return through::<g1t_contracts::User>(op, as_is), | |
| 74 | Op::ListCollaborators => return through::<access::RepoAccess>(op, as_is), | |
| 75 | Op::AddCollaborator => return through::<access::Added>(op, as_is), | |
| 76 | Op::UpdateCollaborator => return through::<access::Collaborator>(op, as_is), | |
| 77 | Op::GetCollaboratorPermission => return through::<access::PermissionInfo>(op, as_is), | |
| 78 | Op::ListRepoInvitations | Op::ListMyRepoInvitations => { | |
| 79 | return through::<Vec<access::RepoInvitation>>(op, as_is); | |
| 80 | } | |
| 81 | Op::RevokeRepoInvitation | Op::AcceptRepoInvitation | Op::DeclineRepoInvitation => { | |
| 82 | return through::<access::RepoInvitation>(op, as_is); | |
| 83 | } | |
| 84 | Op::ListOutsideCollaborators => return through::<Vec<access::OutsideCollaborator>>(op, as_is), | |
| Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar | 85 | // Teams and code owners, also `snake_case`. |
| 86 | Op::ListTeams | Op::ListChildTeams | Op::ListUserTeams => return through::<Vec<teams::Team>>(op, as_is), | |
| 87 | Op::GetTeam | Op::CreateTeam | Op::UpdateTeam | Op::SetTeamReviewAssignment => { | |
| 88 | return through::<teams::Team>(op, as_is); | |
| 89 | } | |
| 90 | Op::ListTeamMembers => return through::<Vec<teams::TeamMember>>(op, as_is), | |
| 91 | Op::SetTeamMember => return through::<teams::TeamMember>(op, as_is), | |
| 92 | Op::ListTeamRepos => return through::<Vec<teams::TeamRepo>>(op, as_is), | |
| 93 | Op::SetTeamRepo => return through::<teams::TeamRepo>(op, as_is), | |
| 94 | Op::DeleteTeam | Op::RemoveTeamMember | Op::RemoveTeamRepo => return through::<bool>(op, as_is), | |
| 95 | Op::GetCodeownersErrors => return through::<codeowners::CodeOwnersReport>(op, as_is), | |
| Merge rulesets: branch and tag rules, agent-first, enforced on push and merge | 96 | // Rulesets travel in `snake_case` between services too. |
| 97 | Op::Rules(RulesOp::ListRepoRulesets | RulesOp::ListWorkspaceRulesets) => { | |
| 98 | return through::<Vec<rules::Ruleset>>(op, as_is); | |
| 99 | } | |
| 100 | Op::Rules( | |
| 101 | RulesOp::GetRepoRuleset | |
| 102 | | RulesOp::CreateRepoRuleset | |
| 103 | | RulesOp::UpdateRepoRuleset | |
| 104 | | RulesOp::GetWorkspaceRuleset | |
| 105 | | RulesOp::CreateWorkspaceRuleset | |
| 106 | | RulesOp::UpdateWorkspaceRuleset, | |
| 107 | ) => return through::<rules::Ruleset>(op, as_is), | |
| 108 | Op::Rules(RulesOp::GetBranchRules) => return through::<rules::EffectiveRules>(op, as_is), | |
| 109 | Op::Rules(RulesOp::ListRuleEvaluations | RulesOp::ListWorkspaceRuleEvaluations) => { | |
| 110 | return through::<rules::EvaluationPage>(op, as_is); | |
| 111 | } | |
| 112 | // Built by the API itself. | |
| 113 | Op::Rules(RulesOp::DeleteRepoRuleset | RulesOp::DeleteWorkspaceRuleset) => return as_is, | |
| Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97 | 114 | // Deployments travel in `snake_case` between services too. |
| 115 | Op::Deployments(_) => return as_is, | |
| Actions: OIDC tokens, the toolkit's cache and artifact services, and artifacts in R2 | 116 | // Artifacts are shaped by the API itself, in `snake_case`. |
| 117 | Op::Artifacts(_) => return as_is, | |
| Merge branch 'worktree-agent-a5a5de74d8863d554' into worktree-agent-a16631325aecf58ca | 118 | // Deploy keys travel in `snake_case` from identity. |
| 119 | Op::DeployKeys(DeployKeysOp::ListDeployKeys) => return through::<Vec<g1t_contracts::deploy_keys::DeployKey>>(op, as_is), | |
| 120 | Op::DeployKeys(DeployKeysOp::GetDeployKey | DeployKeysOp::CreateDeployKey) => { | |
| 121 | return through::<g1t_contracts::deploy_keys::DeployKey>(op, as_is); | |
| 122 | } | |
| 123 | Op::DeployKeys(DeployKeysOp::DeleteDeployKey) => return through::<bool>(op, as_is), | |
| Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily | 124 | // Built by the API itself, in `snake_case`. |
| 125 | Op::ListSecurityAlerts => return through::<Vec<crate::alerts::SecurityAlert>>(op, as_is), | |
| 126 | Op::DismissSecurityAlert | Op::ReopenSecurityAlert => { | |
| 127 | return through::<crate::alerts::SecurityAlert>(op, as_is); | |
| 128 | } | |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 129 | _ => {} |
| 130 | } | |
| Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar | 131 | let mut sent = as_services_send(example); |
| 132 | // A pull request's code owners are `snake_case` inside it. | |
| 133 | if op == Op::GetPullRequest | |
| 134 | && let Some(code_owners) = example.get("code_owners") | |
| 135 | { | |
| 136 | sent["codeOwners"] = code_owners.clone(); | |
| 137 | } | |
| Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API | 138 | match op { |
| Merge branch 'worktree-agent-ad7c6d88d93adc817' | 139 | Op::GetWorkspace | Op::CreateWorkspace | Op::UpdateWorkspace => through::<g1t_contracts::identity::Workspace>(op, sent), |
| Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API | 140 | Op::ListRepos => through::<Vec<repos::Repo>>(op, sent), |
| Search across all of g1t, Explore, and a command palette | 141 | Op::Search => through::<search::SearchResults>(op, sent), |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 142 | Op::GetRepo |
| 143 | | Op::CreateRepo | |
| 144 | | Op::UpdateRepo | |
| 145 | | Op::TransferRepo | |
| 146 | | Op::RenameRepo | |
| 147 | | Op::RenameBranch | |
| 148 | | Op::ArchiveRepo | |
| 149 | | Op::UnarchiveRepo | |
| 150 | | Op::SetRepoVisibility | |
| 151 | | Op::RestoreRepo => through::<repos::Repo>(op, sent), | |
| 152 | Op::DeleteRepo => through::<repos::DeletedRepo>(op, sent), | |
| 153 | Op::ListDeletedRepos => through::<Vec<repos::DeletedRepo>>(op, sent), | |
| Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API | 154 | Op::GetRepoSettings | Op::UpdateRepoSettings => through::<work::RepoSettings>(op, sent), |
| Fast pages, required checks on the branch, self-hosted runners, honest incidents | 155 | Op::ListCheckNames => through::<Vec<work::SeenCheck>>(op, sent), |
| Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API | 156 | Op::GetMergeQueue => through::<work::QueueView>(op, sent), |
| 157 | Op::ListIssues => through::<Vec<work::Issue>>(op, sent), | |
| 158 | Op::CreateIssue | Op::UpdateIssue | Op::CloseIssue | Op::ReopenIssue => { | |
| 159 | through::<work::Issue>(op, sent) | |
| 160 | } | |
| 161 | Op::GetIssue => through::<work::IssueDetail>(op, sent), | |
| Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step | 162 | Op::Delegate => through::<work::Delegated>(op, sent), |
| Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API | 163 | Op::ListPullRequests => through::<Vec<work::Pull>>(op, sent), |
| Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar | 164 | Op::UpdatePullRequest => through::<work::Pull>(op, sent), |
| 165 | Op::ListLabels | Op::AddDefaultLabels | Op::ListIssueLabels => through::<Vec<work::Label>>(op, sent), | |
| 166 | Op::CreateLabel | Op::UpdateLabel => through::<work::Label>(op, sent), | |
| 167 | Op::ListMilestones => through::<Vec<work::Milestone>>(op, sent), | |
| 168 | Op::CreateMilestone | Op::UpdateMilestone => through::<work::Milestone>(op, sent), | |
| 169 | Op::GetMilestone => through::<work::MilestoneDetail>(op, sent), | |
| Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API | 170 | Op::GetPullRequest => through::<work::PullDetail>(op, sent), |
| Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar | 171 | Op::MarkPullRequestReady |
| 172 | | Op::ClosePullRequest | |
| 173 | | Op::MergePullRequest | |
| 174 | | Op::AssignIssue | |
| 175 | | Op::RequestReviewers | |
| 176 | | Op::RemoveRequestedReviewers => { | |
| Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API | 177 | through::<work::Pull>(op, sent) |
| 178 | } | |
| 179 | Op::ListWorkflows => through::<Vec<actions::Workflow>>(op, sent), | |
| 180 | Op::ListWorkflowRuns => through::<Vec<actions::WorkflowRun>>(op, sent), | |
| 181 | Op::GetWorkflowRun => through::<actions::RunDetail>(op, sent), | |
| 182 | Op::GetJobLogs => through::<actions::JobLog>(op, sent), | |
| 183 | Op::DispatchWorkflow | Op::CancelWorkflowRun | Op::RerunWorkflowRun => { | |
| 184 | through::<actions::WorkflowRun>(op, sent) | |
| 185 | } | |
| 186 | Op::ListActionsSecrets | Op::ListActionsVariables => through::<Vec<actions::Setting>>(op, sent), | |
| Fast pages, required checks on the branch, self-hosted runners, honest incidents | 187 | Op::ListRunners => through::<Vec<g1t_contracts::runners::Runner>>(op, sent), |
| 188 | Op::ListRunnerGroups => through::<Vec<g1t_contracts::runners::RunnerGroup>>(op, sent), | |
| 189 | Op::CreateRunnerGroup | Op::UpdateRunnerGroup => through::<g1t_contracts::runners::RunnerGroup>(op, sent), | |
| 190 | Op::GetRunnerSettings | Op::UpdateRunnerSettings => through::<g1t_contracts::runners::RunnerSettings>(op, sent), | |
| 191 | Op::CreateRunnerRegistrationToken => through::<g1t_contracts::runners::RegistrationToken>(op, sent), | |
| Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API | 192 | Op::ListWebhooks => through::<Vec<webhooks::Hook>>(op, sent), |
| 193 | Op::ListIntegrations => through::<Vec<integrations::Connection>>(op, sent), | |
| 194 | Op::GetModelRoutes | Op::SetModelRoutes => through::<Vec<integrations::ModelRoute>>(op, sent), | |
| 195 | Op::ListEvents => through::<Vec<g1t_contracts::events::Event>>(op, sent), | |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 196 | Op::ListEmails | Op::AddEmail | Op::RemoveEmail | Op::UpdateEmailSettings => { |
| 197 | through::<g1t_contracts::accounts::AccountEmails>(op, sent) | |
| 198 | } | |
| 199 | Op::ListInvites => through::<g1t_contracts::identity::InvitesOverview>(op, sent), | |
| 200 | Op::CreateInvite | Op::RevokeInvite | Op::InviteMember | Op::RevokeWorkspaceInvite => { | |
| 201 | through::<g1t_contracts::identity::Invite>(op, sent) | |
| 202 | } | |
| 203 | Op::ListWorkspaceInvites => through::<Vec<g1t_contracts::identity::Invite>>(op, sent), | |
| API: notifications over REST and MCP, with notifications scopes | 204 | Op::ListNotifications => through::<g1t_contracts::inbox::InboxPage>(op, sent), |
| 205 | Op::GetNotificationThread | Op::MarkThreadRead | Op::MarkThreadDone | Op::SaveThread | Op::SnoozeThread => { | |
| 206 | through::<g1t_contracts::inbox::InboxThread>(op, sent) | |
| 207 | } | |
| 208 | Op::GetThreadSubscription | Op::SetThreadSubscription | Op::DeleteThreadSubscription => { | |
| 209 | through::<g1t_contracts::inbox::ThreadSubscription>(op, sent) | |
| 210 | } | |
| Merge checks: statuses and check runs on every commit | 211 | Op::Checks(ChecksOp::CreateCommitStatus) => through::<work::CommitStatus>(op, sent), |
| 212 | Op::Checks(ChecksOp::ListCommitStatuses) => through::<Vec<work::CommitStatus>>(op, sent), | |
| 213 | Op::Checks(ChecksOp::GetCombinedStatus) => through::<checks::CombinedStatus>(op, sent), | |
| 214 | Op::Checks(ChecksOp::CreateCheckRun | ChecksOp::UpdateCheckRun | ChecksOp::GetCheckRun) => { | |
| 215 | through::<checks::CommitCheckRun>(op, sent) | |
| 216 | } | |
| 217 | Op::Checks(ChecksOp::ListCheckRunAnnotations) => through::<Vec<checks::CheckAnnotation>>(op, sent), | |
| 218 | Op::Checks(ChecksOp::ListCheckRunsForRef) => through::<checks::CheckRunList>(op, sent), | |
| 219 | Op::Checks(ChecksOp::ListCheckSuitesForRef) => through::<checks::CheckSuiteList>(op, sent), | |
| 220 | Op::Checks(ChecksOp::GetCheckSuite) => through::<checks::CommitCheckSuite>(op, sent), | |
| Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API | 221 | _ => sent, |
| 222 | } | |
| 223 | } | |
| 224 | ||
| 225 | /// Every key of `example`, as paths, outside the maps passed through. | |
| 226 | fn paths(value: &Value, path: &str, out: &mut Vec<String>) { | |
| 227 | match value { | |
| 228 | Value::Object(fields) => { | |
| 229 | for (key, value) in fields { | |
| 230 | let here = format!("{path}.{key}"); | |
| 231 | out.push(here.clone()); | |
| 232 | let user_keyed = value.is_object() | |
| 233 | && (USER_KEYED.contains(&key.as_str()) || key.starts_with("by_")); | |
| 234 | if !user_keyed { | |
| 235 | paths(value, &here, out); | |
| 236 | } | |
| 237 | } | |
| 238 | } | |
| 239 | Value::Array(items) => { | |
| 240 | for item in items { | |
| 241 | paths(item, &format!("{path}[]"), out); | |
| 242 | } | |
| 243 | } | |
| 244 | _ => {} | |
| 245 | } | |
| 246 | } | |
| 247 | ||
| 248 | #[test] | |
| 249 | fn no_route_answers_with_camel_case() { | |
| 250 | let document = document(); | |
| 251 | let (mut checked, mut converted) = (0, 0); | |
| 252 | for (path, methods) in document["paths"].as_object().unwrap() { | |
| 253 | for (method, operation) in methods.as_object().unwrap() { | |
| 254 | let example = &operation["responses"]["200"]["content"]["application/json"]["example"]; | |
| Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step | 255 | let tool = operation["x-operation"].as_str().unwrap_or_default(); |
| Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API | 256 | let Some(op) = Op::by_name(tool) else { |
| 257 | // Device sign-in, which is written in `snake_case` by hand. | |
| 258 | assert!(wire::camel_case_keys(example).is_empty(), "{method} {path}"); | |
| 259 | continue; | |
| 260 | }; | |
| 261 | let sample = sample(op, example); | |
| 262 | converted += wire::camel_case_keys(&sample).len(); | |
| 263 | let sent = wire::snake_case(sample); | |
| 264 | let leaked = wire::camel_case_keys(&sent); | |
| 265 | assert!(leaked.is_empty(), "{method} {path} sends {leaked:?}"); | |
| 266 | // The reference shows what is sent: each of its names is one. | |
| 267 | let (mut shown, mut real) = (Vec::new(), Vec::new()); | |
| 268 | paths(example, "", &mut shown); | |
| 269 | paths(&sent, "", &mut real); | |
| 270 | for name in shown { | |
| 271 | assert!(real.contains(&name), "{method} {path}: the reference shows {name}, which is not sent"); | |
| 272 | } | |
| 273 | checked += 1; | |
| 274 | } | |
| 275 | } | |
| 276 | assert!(checked >= Op::ALL.len()); | |
| 277 | // The samples are in the services' spelling, so there was something to | |
| 278 | // convert. | |
| 279 | assert!(converted > 100, "{converted}"); | |
| 280 | } | |
| 281 | ||
| 282 | #[test] | |
| 283 | fn every_route_has_a_sample() { | |
| 284 | let document = document(); | |
| 285 | for route in crate::rest::ROUTES { | |
| 286 | let path = route | |
| 287 | .path | |
| 288 | .split('/') | |
| 289 | .map(|segment| match segment.strip_prefix(':') { | |
| 290 | Some(name) => format!("{{{name}}}"), | |
| 291 | None => segment.to_owned(), | |
| 292 | }) | |
| 293 | .collect::<Vec<_>>() | |
| 294 | .join("/"); | |
| 295 | let example = &document["paths"][&path][route.method.to_lowercase()]["responses"]["200"] | |
| 296 | ["content"]["application/json"]["example"]; | |
| 297 | assert!(!example.is_null(), "{} {path}", route.method); | |
| 298 | } | |
| 299 | } | |
| 300 | ||
| 301 | #[test] | |
| 302 | fn errors_and_reports_are_snake_case() { | |
| 303 | let failure = g1t_contracts::Failure { | |
| 304 | code: g1t_contracts::FailureCode::NotFound, | |
| 305 | message: "No such endpoint.".to_owned(), | |
| 306 | }; | |
| 307 | assert!(wire::camel_case_keys(&wire::snake_case(json!({ "error": failure }))).is_empty()); | |
| 308 | } | |
| 309 | ||
| 310 | #[test] | |
| 311 | fn a_job_spec_keeps_github_s_spelling() { | |
| 312 | let spec = json!({ | |
| 313 | "job": "job_1", | |
| 314 | "spec": { "runs-on": "ubuntu-latest", "timeoutMinutes": 5 }, | |
| 315 | "workflow": { "env": { "nodeEnv": "x" } }, | |
| 316 | "github": { "eventName": "push", "headRef": "" }, | |
| 317 | "event": { "pull_request": { "headSha": "x" } }, | |
| 318 | "contexts": { "inputs": { "dryRun": true }, "matrix": { "nodeVersion": 20 } }, | |
| 319 | "checkout": { "ref": "main" }, | |
| 320 | "timeoutMinutes": 30, | |
| 321 | "masks": [], | |
| 322 | }); | |
| 323 | let sent = wire::snake_case_keeping(spec.clone(), crate::JOB_SPEC_AS_GIVEN); | |
| 324 | assert_eq!(sent["timeout_minutes"], 30); | |
| 325 | assert!(sent.get("timeoutMinutes").is_none()); | |
| 326 | for kept in ["spec", "workflow", "github", "event", "contexts", "checkout"] { | |
| 327 | assert_eq!(sent[kept], spec[kept], "{kept}"); | |
| 328 | } | |
| 329 | } |
This file's history is long; its oldest lines are credited to the oldest commit read.