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. | |
| Files pages say Files in the trail and keep the description in About only | 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; | |
| 20 | ||
| 21 | #[derive(Deserialize)] | |
| 22 | struct SettingsRow { | |
| 23 | auto_merge: u8, | |
| 24 | require_up_to_date: u8, | |
| 25 | required_approvals: u32, | |
| 26 | count_agent_approvals: u8, | |
| 27 | allow_ignoring_checks: u8, | |
| 28 | agent_review: u8, | |
| 29 | max_revisions: u32, | |
| 30 | #[serde(default)] | |
| 31 | merge_queue: u8, | |
| 32 | /// JSON array of names. | |
| 33 | #[serde(default)] | |
| 34 | required_checks: Option<String>, | |
| Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar | 35 | #[serde(default)] |
| 36 | require_code_owner_review: u8, | |
| Files pages say Files in the trail and keep the description in About only | 37 | updated_by: String, |
| 38 | updated_at: String, | |
| 39 | } | |
| 40 | ||
| 41 | impl From<SettingsRow> for RepoSettings { | |
| 42 | fn from(row: SettingsRow) -> Self { | |
| 43 | RepoSettings { | |
| 44 | auto_merge: row.auto_merge != 0, | |
| 45 | required_checks: row | |
| 46 | .required_checks | |
| 47 | .as_deref() | |
| 48 | .and_then(|names| serde_json::from_str(names).ok()) | |
| 49 | .unwrap_or_default(), | |
| 50 | require_up_to_date: row.require_up_to_date != 0, | |
| 51 | required_approvals: row.required_approvals, | |
| 52 | count_agent_approvals: row.count_agent_approvals != 0, | |
| 53 | allow_ignoring_checks: row.allow_ignoring_checks != 0, | |
| 54 | agent_review: row.agent_review != 0, | |
| 55 | max_revisions: row.max_revisions, | |
| 56 | merge_queue: row.merge_queue != 0, | |
| The work, events, webhooks, actions, repos and packages services no longer run g1t's own agents. Work drops the 35 calls that queued issues for g1t, delegated them, messaged and reviewed through the agent, planned, captured memories automatically, rated confidence and capped agent spend, and its pull request lifecycle no longer revises or reviews: a pull request only advances to a merge by the repository's auto-merge setting. Merging a pull request that is behind now catches it up itself, through the repository's branch update, and lands it when the push arrives; when it can't be caught up cleanly the merge says to merge the base in and try again. Recording a session on a pull request, memory recall and remember, overlaps, the merge queue and required checks stay as they were. | 57 | // Nothing is held for low confidence any more. |
| 58 | hold_low_confidence: false, | |
| Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar | 59 | require_code_owner_review: row.require_code_owner_review != 0, |
| Files pages say Files in the trail and keep the description in About only | 60 | updated_by: Some(row.updated_by), |
| 61 | updated_at: Some(row.updated_at), | |
| 62 | } | |
| 63 | } | |
| 64 | } | |
| 65 | ||
| 66 | impl Work { | |
| Merge rulesets: branch and tag rules, agent-first, enforced on push and merge | 67 | /// A repository's stored settings, by its id: how g1t's agents handle |
| 68 | /// its pull requests. Defaults if none were set. Branch protection is | |
| 69 | /// its rules': see `settings_on`. | |
| Files pages say Files in the trail and keep the description in About only | 70 | pub(crate) async fn settings(&self, repo_id: &str) -> Result<RepoSettings> { |
| 71 | if let Some(found) = self.prefetched_repo(repo_id) { | |
| 72 | let row = found.first::<SettingsRow>(crate::prefetch::Slot::Settings)?; | |
| The work, events, webhooks, actions, repos and packages services no longer run g1t's own agents. Work drops the 35 calls that queued issues for g1t, delegated them, messaged and reviewed through the agent, planned, captured memories automatically, rated confidence and capped agent spend, and its pull request lifecycle no longer revises or reviews: a pull request only advances to a merge by the repository's auto-merge setting. Merging a pull request that is behind now catches it up itself, through the repository's branch update, and lands it when the push arrives; when it can't be caught up cleanly the merge says to merge the base in and try again. Recording a session on a pull request, memory recall and remember, overlaps, the merge queue and required checks stay as they were. | 73 | return Ok(row.map_or_else(RepoSettings::default, RepoSettings::from)); |
| Files pages say Files in the trail and keep the description in About only | 74 | } |
| The work, events, webhooks, actions, repos and packages services no longer run g1t's own agents. Work drops the 35 calls that queued issues for g1t, delegated them, messaged and reviewed through the agent, planned, captured memories automatically, rated confidence and capped agent spend, and its pull request lifecycle no longer revises or reviews: a pull request only advances to a merge by the repository's auto-merge setting. Merging a pull request that is behind now catches it up itself, through the repository's branch update, and lands it when the push arrives; when it can't be caught up cleanly the merge says to merge the base in and try again. Recording a session on a pull request, memory recall and remember, overlaps, the merge queue and required checks stay as they were. | 75 | let row = self |
| 76 | .db | |
| 77 | .prepare("SELECT * FROM repo_settings WHERE repo_id = ?") | |
| 78 | .bind(&[repo_id.into()])? | |
| 79 | .first::<SettingsRow>(None) | |
| 80 | .await?; | |
| 81 | Ok(row.map_or_else(RepoSettings::default, RepoSettings::from)) | |
| Files pages say Files in the trail and keep the description in About only | 82 | } |
| 83 | ||
| Merge rulesets: branch and tag rules, agent-first, enforced on push and merge | 84 | /// The default branch's settings: the stored ones, with branch |
| 85 | /// protection as its rules stack. | |
| Files pages say Files in the trail and keep the description in About only | 86 | 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 | 87 | let repo = match self.repo(&a.repo, &a.viewer).await? { |
| 88 | Outcome::Ok(repo) => repo, | |
| 89 | Outcome::Fail(failure) => return Ok(Outcome::Fail(failure)), | |
| 90 | }; | |
| 91 | Ok(Outcome::Ok(self.timing.db(3, self.default_branch_settings(&repo)).await?)) | |
| Files pages say Files in the trail and keep the description in About only | 92 | } |
| 93 | ||
| Merge rulesets: branch and tag rules, agent-first, enforced on push and merge | 94 | /// Replaces a repository's settings: how g1t's agents work, kept here, |
| 95 | /// and the branch protection ones, written to its "Default branch | |
| 96 | /// protection" ruleset (made when it has none and they protect | |
| 97 | /// anything). Rules that only rulesets have stay as they are. | |
| Files pages say Files in the trail and keep the description in About only | 98 | pub(crate) async fn update_settings( |
| 99 | &self, | |
| 100 | a: UpdateSettingsArgs, | |
| 101 | ) -> Result<Outcome<RepoSettings>> { | |
| 102 | let repo = match self.repo(&a.repo, &Some(a.actor.clone())).await? { | |
| 103 | Outcome::Ok(repo) => repo, | |
| 104 | Outcome::Fail(failure) => return Ok(Outcome::Fail(failure)), | |
| 105 | }; | |
| Merge branch 'mirroring' into artifacts-mode | 106 | if let Outcome::Fail(failure) = crate::retired::not_archived(&repo) { |
| Files pages say Files in the trail and keep the description in About only | 107 | return Ok(Outcome::Fail(failure)); |
| 108 | } | |
| 109 | if !a.actor.verified { | |
| 110 | return Ok(Outcome::fail(FailureCode::Forbidden, crate::UNVERIFIED)); | |
| 111 | } | |
| 112 | if let Outcome::Fail(failure) = | |
| 113 | crate::allowed(Some(&a.actor), &repo, g1t_contracts::access::Capability::ManageSettings) | |
| 114 | { | |
| 115 | return Ok(Outcome::Fail(failure)); | |
| 116 | } | |
| Merge rulesets: branch and tag rules, agent-first, enforced on push and merge | 117 | let before = self.default_branch_settings(&repo).await?; |
| Files pages say Files in the trail and keep the description in About only | 118 | let settings = RepoSettings { |
| 119 | required_approvals: a.settings.required_approvals.min(MAX_REQUIRED_APPROVALS), | |
| 120 | max_revisions: a.settings.max_revisions.min(MAX_REVISIONS), | |
| 121 | required_checks: tidy_required(&a.settings.required_checks), | |
| Merge rulesets: branch and tag rules, agent-first, enforced on push and merge | 122 | updated_by: Some(a.actor.username.clone()), |
| Files pages say Files in the trail and keep the description in About only | 123 | updated_at: Some(rfc3339(now_ms())), |
| 124 | ..a.settings | |
| 125 | }; | |
| Merge rulesets: branch and tag rules, agent-first, enforced on push and merge | 126 | // Branch protection changes need the role that changes it. |
| 127 | if protection_changed(&before, &settings) | |
| 128 | && let Outcome::Fail(failure) = | |
| 129 | crate::allowed(Some(&a.actor), &repo, g1t_contracts::access::Capability::ManageProtection) | |
| 130 | { | |
| 131 | return Ok(Outcome::Fail(failure)); | |
| 132 | } | |
| Files pages say Files in the trail and keep the description in About only | 133 | self.db |
| 134 | .prepare( | |
| 135 | "INSERT INTO repo_settings | |
| 136 | (repo_id, auto_merge, require_up_to_date, required_approvals, | |
| 137 | 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 | 138 | merge_queue, required_checks, require_code_owner_review, updated_by, updated_at) |
| 139 | VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) | |
| Files pages say Files in the trail and keep the description in About only | 140 | ON CONFLICT (repo_id) DO UPDATE SET |
| 141 | auto_merge = excluded.auto_merge, | |
| 142 | require_up_to_date = excluded.require_up_to_date, | |
| 143 | required_approvals = excluded.required_approvals, | |
| 144 | count_agent_approvals = excluded.count_agent_approvals, | |
| 145 | allow_ignoring_checks = excluded.allow_ignoring_checks, | |
| 146 | agent_review = excluded.agent_review, | |
| 147 | max_revisions = excluded.max_revisions, | |
| 148 | merge_queue = excluded.merge_queue, | |
| 149 | required_checks = excluded.required_checks, | |
| Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar | 150 | require_code_owner_review = excluded.require_code_owner_review, |
| Files pages say Files in the trail and keep the description in About only | 151 | updated_by = excluded.updated_by, |
| 152 | updated_at = excluded.updated_at", | |
| 153 | ) | |
| 154 | .bind(&[ | |
| 155 | repo.id.as_str().into(), | |
| 156 | u32::from(settings.auto_merge).into(), | |
| 157 | u32::from(settings.require_up_to_date).into(), | |
| 158 | settings.required_approvals.into(), | |
| 159 | u32::from(settings.count_agent_approvals).into(), | |
| 160 | u32::from(settings.allow_ignoring_checks).into(), | |
| 161 | u32::from(settings.agent_review).into(), | |
| 162 | settings.max_revisions.into(), | |
| 163 | u32::from(settings.merge_queue).into(), | |
| 164 | serde_json::to_string(&settings.required_checks)?.into(), | |
| Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar | 165 | u32::from(settings.require_code_owner_review).into(), |
| Files pages say Files in the trail and keep the description in About only | 166 | settings.updated_by.as_deref().unwrap_or_default().into(), |
| 167 | settings.updated_at.as_deref().unwrap_or_default().into(), | |
| 168 | ])? | |
| 169 | .run() | |
| 170 | .await?; | |
| Merge rulesets: branch and tag rules, agent-first, enforced on push and merge | 171 | if protection_changed(&before, &settings) { |
| 172 | self.write_branch_protection(&repo, &settings, &a.actor).await?; | |
| 173 | } | |
| 174 | Ok(Outcome::Ok(self.default_branch_settings(&repo).await?)) | |
| Files pages say Files in the trail and keep the description in About only | 175 | } |
| 176 | } | |
| 177 | ||
| Merge rulesets: branch and tag rules, agent-first, enforced on push and merge | 178 | /// Whether the branch protection part of the settings differs. |
| 179 | fn protection_changed(before: &RepoSettings, after: &RepoSettings) -> bool { | |
| 180 | let names = |settings: &RepoSettings| -> Vec<String> { | |
| 181 | let mut names: Vec<String> = settings.required_checks.iter().map(|name| name.to_lowercase()).collect(); | |
| 182 | names.sort(); | |
| 183 | names | |
| 184 | }; | |
| 185 | names(before) != names(after) | |
| 186 | || before.require_up_to_date != after.require_up_to_date | |
| 187 | || before.required_approvals != after.required_approvals | |
| 188 | || before.count_agent_approvals != after.count_agent_approvals | |
| 189 | || before.allow_ignoring_checks != after.allow_ignoring_checks | |
| 190 | || before.merge_queue != after.merge_queue | |
| 191 | || before.require_code_owner_review != after.require_code_owner_review | |
| 192 | } | |
| 193 | ||
| Files pages say Files in the trail and keep the description in About only | 194 | #[cfg(test)] |
| 195 | mod tests { | |
| 196 | use super::*; | |
| 197 | ||
| 198 | #[test] | |
| Merge rulesets: branch and tag rules, agent-first, enforced on push and merge | 199 | fn only_protection_changes_count_as_protection_changes() { |
| 200 | let before = RepoSettings::default(); | |
| 201 | let agents = RepoSettings { auto_merge: true, agent_review: false, max_revisions: 4, ..RepoSettings::default() }; | |
| 202 | assert!(!protection_changed(&before, &agents)); | |
| 203 | let checks = RepoSettings { required_checks: vec!["CI".into()], ..RepoSettings::default() }; | |
| 204 | assert!(protection_changed(&before, &checks)); | |
| 205 | let same = RepoSettings { required_checks: vec!["ci".into()], ..RepoSettings::default() }; | |
| 206 | assert!(!protection_changed(&checks, &same), "names compare without case"); | |
| 207 | let approvals = RepoSettings { required_approvals: 1, ..RepoSettings::default() }; | |
| 208 | assert!(protection_changed(&before, &approvals)); | |
| Files pages say Files in the trail and keep the description in About only | 209 | } |
| 210 | } |
This file's history is long; its oldest lines are credited to the oldest commit read.