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.
| Merge rulesets: branch and tag rules, agent-first, enforced on push and merge | 1 | //! A repository's settings for how g1t's agents handle its pull requests, |
| 2 | //! and the branch protection settings that are now its "Default branch | |
| 3 | //! protection" ruleset (rulesets.rs). | |
| 4 | //! | |
| 5 | //! What a merge needs is decided by the rules of the branch it merges into | |
| 6 | //! (`Work::merge_gate`); `approvals_gap` asks them for what people must | |
| 7 | //! still do, as g1t sees it when it merges by itself. | |
| Agents as a team: lifecycle, merge queue, billing and a new shell | 8 | |
| 9 | use g1t_contracts::time::rfc3339; | |
| 10 | use g1t_contracts::work::*; | |
| 11 | use g1t_contracts::{FailureCode, Outcome}; | |
| 12 | use g1t_kit::now_ms; | |
| 13 | use serde::Deserialize; | |
| 14 | use worker::Result; | |
| 15 | ||
| 16 | use crate::Work; | |
| 17 | ||
| 18 | const MAX_REQUIRED_APPROVALS: u32 = 6; | |
| 19 | const MAX_REVISIONS: u32 = 5; | |
| Merge branch 'worktree-agent-ad8a36dfcd4176015' into spend-guardrails | 20 | /// The most times in a day people's mentions may send g1t back to one pull |
| 21 | /// request. They outrank `MAX_REVISIONS` and a stall, but not this. | |
| 22 | pub(crate) const MAX_MENTION_REVISIONS_PER_DAY: u32 = 10; | |
| Agents as a team: lifecycle, merge queue, billing and a new shell | 23 | |
| 24 | #[derive(Deserialize)] | |
| 25 | struct SettingsRow { | |
| 26 | auto_merge: u8, | |
| 27 | require_up_to_date: u8, | |
| 28 | required_approvals: u32, | |
| 29 | count_agent_approvals: u8, | |
| 30 | allow_ignoring_checks: u8, | |
| 31 | agent_review: u8, | |
| 32 | max_revisions: u32, | |
| 33 | #[serde(default)] | |
| 34 | merge_queue: u8, | |
| Fast pages, required checks on the branch, self-hosted runners, honest incidents | 35 | /// JSON array of names. |
| 36 | #[serde(default)] | |
| 37 | required_checks: Option<String>, | |
| Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar | 38 | #[serde(default)] |
| 39 | require_code_owner_review: u8, | |
| Agents as a team: lifecycle, merge queue, billing and a new shell | 40 | updated_by: String, |
| 41 | updated_at: String, | |
| 42 | } | |
| 43 | ||
| 44 | impl From<SettingsRow> for RepoSettings { | |
| 45 | fn from(row: SettingsRow) -> Self { | |
| 46 | RepoSettings { | |
| 47 | auto_merge: row.auto_merge != 0, | |
| Fast pages, required checks on the branch, self-hosted runners, honest incidents | 48 | required_checks: row |
| 49 | .required_checks | |
| 50 | .as_deref() | |
| 51 | .and_then(|names| serde_json::from_str(names).ok()) | |
| 52 | .unwrap_or_default(), | |
| Agents as a team: lifecycle, merge queue, billing and a new shell | 53 | require_up_to_date: row.require_up_to_date != 0, |
| 54 | required_approvals: row.required_approvals, | |
| 55 | count_agent_approvals: row.count_agent_approvals != 0, | |
| 56 | allow_ignoring_checks: row.allow_ignoring_checks != 0, | |
| 57 | agent_review: row.agent_review != 0, | |
| 58 | max_revisions: row.max_revisions, | |
| 59 | merge_queue: row.merge_queue != 0, | |
| Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step | 60 | // Kept in its own table (confidence.rs), read beside this row. |
| 61 | hold_low_confidence: true, | |
| Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar | 62 | require_code_owner_review: row.require_code_owner_review != 0, |
| Agents as a team: lifecycle, merge queue, billing and a new shell | 63 | updated_by: Some(row.updated_by), |
| 64 | updated_at: Some(row.updated_at), | |
| 65 | } | |
| 66 | } | |
| 67 | } | |
| 68 | ||
| 69 | impl Work { | |
| Merge rulesets: branch and tag rules, agent-first, enforced on push and merge | 70 | /// A repository's stored settings, by its id: how g1t's agents handle |
| 71 | /// its pull requests. Defaults if none were set. Branch protection is | |
| 72 | /// its rules': see `settings_on`. | |
| Agents as a team: lifecycle, merge queue, billing and a new shell | 73 | pub(crate) async fn settings(&self, repo_id: &str) -> Result<RepoSettings> { |
| Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily | 74 | if let Some(found) = self.prefetched_repo(repo_id) { |
| 75 | let row = found.first::<SettingsRow>(crate::prefetch::Slot::Settings)?; | |
| 76 | let hold = found | |
| 77 | .first::<crate::rows::NumberRow>(crate::prefetch::Slot::Hold)? | |
| 78 | .is_none_or(|row| row.n != 0); | |
| 79 | return Ok(RepoSettings { | |
| 80 | hold_low_confidence: hold, | |
| 81 | ..row.map_or_else(RepoSettings::default, RepoSettings::from) | |
| 82 | }); | |
| 83 | } | |
| Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step | 84 | let row = async { |
| 85 | self.db | |
| 86 | .prepare("SELECT * FROM repo_settings WHERE repo_id = ?") | |
| 87 | .bind(&[repo_id.into()])? | |
| 88 | .first::<SettingsRow>(None) | |
| 89 | .await | |
| 90 | }; | |
| 91 | let (row, hold) = futures_util::future::try_join(row, self.holds_low_confidence(repo_id)).await?; | |
| 92 | Ok(RepoSettings { | |
| 93 | hold_low_confidence: hold, | |
| 94 | ..row.map_or_else(RepoSettings::default, RepoSettings::from) | |
| 95 | }) | |
| Agents as a team: lifecycle, merge queue, billing and a new shell | 96 | } |
| 97 | ||
| Merge rulesets: branch and tag rules, agent-first, enforced on push and merge | 98 | /// The default branch's settings: the stored ones, with branch |
| 99 | /// protection as its rules stack. | |
| Agents as a team: lifecycle, merge queue, billing and a new shell | 100 | pub(crate) async fn get_settings(&self, a: ViewArgs) -> Result<Outcome<RepoSettings>> { |
| Merge rulesets: branch and tag rules, agent-first, enforced on push and merge | 101 | let repo = match self.repo(&a.repo, &a.viewer).await? { |
| 102 | Outcome::Ok(repo) => repo, | |
| 103 | Outcome::Fail(failure) => return Ok(Outcome::Fail(failure)), | |
| 104 | }; | |
| 105 | Ok(Outcome::Ok(self.timing.db(3, self.default_branch_settings(&repo)).await?)) | |
| Agents as a team: lifecycle, merge queue, billing and a new shell | 106 | } |
| 107 | ||
| Merge rulesets: branch and tag rules, agent-first, enforced on push and merge | 108 | /// Replaces a repository's settings: how g1t's agents work, kept here, |
| 109 | /// and the branch protection ones, written to its "Default branch | |
| 110 | /// protection" ruleset (made when it has none and they protect | |
| 111 | /// anything). Rules that only rulesets have stay as they are. | |
| Agents as a team: lifecycle, merge queue, billing and a new shell | 112 | pub(crate) async fn update_settings( |
| 113 | &self, | |
| 114 | a: UpdateSettingsArgs, | |
| 115 | ) -> Result<Outcome<RepoSettings>> { | |
| 116 | let repo = match self.repo(&a.repo, &Some(a.actor.clone())).await? { | |
| 117 | Outcome::Ok(repo) => repo, | |
| 118 | Outcome::Fail(failure) => return Ok(Outcome::Fail(failure)), | |
| 119 | }; | |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 120 | if let Outcome::Fail(failure) = crate::retired::writable(&repo) { |
| 121 | return Ok(Outcome::Fail(failure)); | |
| 122 | } | |
| 123 | if !a.actor.verified { | |
| 124 | return Ok(Outcome::fail(FailureCode::Forbidden, crate::UNVERIFIED)); | |
| 125 | } | |
| 126 | if let Outcome::Fail(failure) = | |
| 127 | crate::allowed(Some(&a.actor), &repo, g1t_contracts::access::Capability::ManageSettings) | |
| 128 | { | |
| 129 | return Ok(Outcome::Fail(failure)); | |
| Agents as a team: lifecycle, merge queue, billing and a new shell | 130 | } |
| Merge rulesets: branch and tag rules, agent-first, enforced on push and merge | 131 | let before = self.default_branch_settings(&repo).await?; |
| Agents as a team: lifecycle, merge queue, billing and a new shell | 132 | let settings = RepoSettings { |
| 133 | required_approvals: a.settings.required_approvals.min(MAX_REQUIRED_APPROVALS), | |
| 134 | max_revisions: a.settings.max_revisions.min(MAX_REVISIONS), | |
| Fast pages, required checks on the branch, self-hosted runners, honest incidents | 135 | required_checks: tidy_required(&a.settings.required_checks), |
| Merge rulesets: branch and tag rules, agent-first, enforced on push and merge | 136 | updated_by: Some(a.actor.username.clone()), |
| Agents as a team: lifecycle, merge queue, billing and a new shell | 137 | updated_at: Some(rfc3339(now_ms())), |
| 138 | ..a.settings | |
| 139 | }; | |
| Merge rulesets: branch and tag rules, agent-first, enforced on push and merge | 140 | // Branch protection changes need the role that changes it. |
| 141 | if protection_changed(&before, &settings) | |
| 142 | && let Outcome::Fail(failure) = | |
| 143 | crate::allowed(Some(&a.actor), &repo, g1t_contracts::access::Capability::ManageProtection) | |
| 144 | { | |
| 145 | return Ok(Outcome::Fail(failure)); | |
| 146 | } | |
| Agents as a team: lifecycle, merge queue, billing and a new shell | 147 | self.db |
| 148 | .prepare( | |
| 149 | "INSERT INTO repo_settings | |
| 150 | (repo_id, auto_merge, require_up_to_date, required_approvals, | |
| 151 | count_agent_approvals, allow_ignoring_checks, agent_review, max_revisions, | |
| Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar | 152 | merge_queue, required_checks, require_code_owner_review, updated_by, updated_at) |
| 153 | VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) | |
| Agents as a team: lifecycle, merge queue, billing and a new shell | 154 | ON CONFLICT (repo_id) DO UPDATE SET |
| 155 | auto_merge = excluded.auto_merge, | |
| 156 | require_up_to_date = excluded.require_up_to_date, | |
| 157 | required_approvals = excluded.required_approvals, | |
| 158 | count_agent_approvals = excluded.count_agent_approvals, | |
| 159 | allow_ignoring_checks = excluded.allow_ignoring_checks, | |
| 160 | agent_review = excluded.agent_review, | |
| 161 | max_revisions = excluded.max_revisions, | |
| 162 | merge_queue = excluded.merge_queue, | |
| Fast pages, required checks on the branch, self-hosted runners, honest incidents | 163 | required_checks = excluded.required_checks, |
| Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar | 164 | require_code_owner_review = excluded.require_code_owner_review, |
| Agents as a team: lifecycle, merge queue, billing and a new shell | 165 | updated_by = excluded.updated_by, |
| 166 | updated_at = excluded.updated_at", | |
| 167 | ) | |
| 168 | .bind(&[ | |
| 169 | repo.id.as_str().into(), | |
| 170 | u32::from(settings.auto_merge).into(), | |
| 171 | u32::from(settings.require_up_to_date).into(), | |
| 172 | settings.required_approvals.into(), | |
| 173 | u32::from(settings.count_agent_approvals).into(), | |
| 174 | u32::from(settings.allow_ignoring_checks).into(), | |
| 175 | u32::from(settings.agent_review).into(), | |
| 176 | settings.max_revisions.into(), | |
| 177 | u32::from(settings.merge_queue).into(), | |
| Fast pages, required checks on the branch, self-hosted runners, honest incidents | 178 | serde_json::to_string(&settings.required_checks)?.into(), |
| Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar | 179 | u32::from(settings.require_code_owner_review).into(), |
| Agents as a team: lifecycle, merge queue, billing and a new shell | 180 | settings.updated_by.as_deref().unwrap_or_default().into(), |
| 181 | settings.updated_at.as_deref().unwrap_or_default().into(), | |
| 182 | ])? | |
| 183 | .run() | |
| 184 | .await?; | |
| Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step | 185 | self.set_hold_low_confidence( |
| 186 | &repo.id, | |
| 187 | settings.hold_low_confidence, | |
| 188 | settings.updated_by.as_deref().unwrap_or_default(), | |
| 189 | settings.updated_at.as_deref().unwrap_or_default(), | |
| 190 | ) | |
| 191 | .await?; | |
| Merge rulesets: branch and tag rules, agent-first, enforced on push and merge | 192 | if protection_changed(&before, &settings) { |
| 193 | self.write_branch_protection(&repo, &settings, &a.actor).await?; | |
| 194 | } | |
| 195 | Ok(Outcome::Ok(self.default_branch_settings(&repo).await?)) | |
| Agents as a team: lifecycle, merge queue, billing and a new shell | 196 | } |
| 197 | } | |
| 198 | ||
| Merge rulesets: branch and tag rules, agent-first, enforced on push and merge | 199 | /// Whether the branch protection part of the settings differs. |
| 200 | fn protection_changed(before: &RepoSettings, after: &RepoSettings) -> bool { | |
| 201 | let names = |settings: &RepoSettings| -> Vec<String> { | |
| 202 | let mut names: Vec<String> = settings.required_checks.iter().map(|name| name.to_lowercase()).collect(); | |
| 203 | names.sort(); | |
| 204 | names | |
| 205 | }; | |
| 206 | names(before) != names(after) | |
| 207 | || before.require_up_to_date != after.require_up_to_date | |
| 208 | || before.required_approvals != after.required_approvals | |
| 209 | || before.count_agent_approvals != after.count_agent_approvals | |
| 210 | || before.allow_ignoring_checks != after.allow_ignoring_checks | |
| 211 | || before.merge_queue != after.merge_queue | |
| 212 | || before.require_code_owner_review != after.require_code_owner_review | |
| 213 | } | |
| 214 | ||
| Agents as a team: lifecycle, merge queue, billing and a new shell | 215 | #[cfg(test)] |
| 216 | mod tests { | |
| 217 | use super::*; | |
| 218 | ||
| 219 | #[test] | |
| Merge rulesets: branch and tag rules, agent-first, enforced on push and merge | 220 | fn only_protection_changes_count_as_protection_changes() { |
| 221 | let before = RepoSettings::default(); | |
| 222 | let agents = RepoSettings { auto_merge: true, agent_review: false, max_revisions: 4, ..RepoSettings::default() }; | |
| 223 | assert!(!protection_changed(&before, &agents)); | |
| 224 | let checks = RepoSettings { required_checks: vec!["CI".into()], ..RepoSettings::default() }; | |
| 225 | assert!(protection_changed(&before, &checks)); | |
| 226 | let same = RepoSettings { required_checks: vec!["ci".into()], ..RepoSettings::default() }; | |
| 227 | assert!(!protection_changed(&checks, &same), "names compare without case"); | |
| 228 | let approvals = RepoSettings { required_approvals: 1, ..RepoSettings::default() }; | |
| 229 | assert!(protection_changed(&before, &approvals)); | |
| g1t is the stored author of what it opens; the person who asked is requested_by and keeps the author's rights | 230 | } |
| Agents as a team: lifecycle, merge queue, billing and a new shell | 231 | } |
This file's history is long; its oldest lines are credited to the oldest commit read.