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.
| Files pages say Files in the trail and keep the description in About only | 1 | //! Repositories that are archived, deleted or purged, and branches |
| 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. | 2 | //! renamed: what issues, pull requests and runs do about each. |
| Files pages say Files in the trail and keep the description in About only | 3 | //! |
| 4 | //! An archived repository is read-only: its issues and pull requests are | |
| Merge branch 'mirroring' into artifacts-mode | 5 | //! locked and nothing new starts on it. So is a mirror standing by (see |
| 6 | //! `g1t_contracts::mirrors`): its work happens where it is mirrored from, | |
| 7 | //! until someone takes over on g1t. A deleted one looks missing (repos | |
| Files pages say Files in the trail and keep the description in About only | 8 | //! hides it) and keeps its rows for a restore. A purged one is gone, and |
| 9 | //! every row kept for it goes with it. | |
| 10 | ||
| 11 | use g1t_contracts::events::{BranchRenamed, Event, RepoArchived, RepoDeleted}; | |
| 12 | use g1t_contracts::repos::{Repo, RepoStatus, StatusByIdArgs, archived_message}; | |
| 13 | use g1t_contracts::{FailureCode, Outcome}; | |
| 14 | use g1t_contracts::time::rfc3339; | |
| 15 | use g1t_kit::now_ms; | |
| 16 | use serde::{Deserialize, Serialize}; | |
| 17 | use worker::Result; | |
| 18 | ||
| 19 | use crate::Work; | |
| 20 | ||
| 21 | /// `Ok` when `repo` may be changed; refused, saying why, when it is | |
| Merge branch 'mirroring' into artifacts-mode | 22 | /// archived or a mirror that is not taken over. |
| Files pages say Files in the trail and keep the description in About only | 23 | pub(crate) fn writable(repo: &Repo) -> Outcome<()> { |
| Merge branch 'mirroring' into artifacts-mode | 24 | match repo.read_only_reason() { |
| 25 | Some(reason) => Outcome::fail(FailureCode::Forbidden, reason), | |
| 26 | None => Outcome::Ok(()), | |
| 27 | } | |
| 28 | } | |
| 29 | ||
| 30 | /// `Ok` unless `repo` is archived. For what stays g1t's own on a mirror: | |
| 31 | /// its settings and rules, and the statuses and checks reported on its | |
| 32 | /// commits (CI failover reports them). | |
| 33 | pub(crate) fn not_archived(repo: &Repo) -> Outcome<()> { | |
| Files pages say Files in the trail and keep the description in About only | 34 | if repo.archived() { |
| 35 | Outcome::fail(FailureCode::Forbidden, archived_message(&repo.namespace, &repo.name)) | |
| 36 | } else { | |
| 37 | Outcome::Ok(()) | |
| 38 | } | |
| 39 | } | |
| 40 | ||
| 41 | /// `repo` as found, unless it is archived: then refused, as [`writable`] | |
| 42 | /// says. For the steps g1t starts by itself, which stop on a refusal. | |
| 43 | pub(crate) fn unless_archived(repo: Outcome<Repo>) -> Outcome<Repo> { | |
| 44 | match repo { | |
| 45 | Outcome::Ok(repo) => match writable(&repo) { | |
| 46 | Outcome::Ok(()) => Outcome::Ok(repo), | |
| 47 | Outcome::Fail(failure) => Outcome::Fail(failure), | |
| 48 | }, | |
| 49 | failed => failed, | |
| 50 | } | |
| 51 | } | |
| 52 | ||
| 53 | /// `repo.purged`: every row kept for the repository, `?1` its id. Rows | |
| 54 | /// kept by pull request go first, while the pull requests still name them. | |
| 55 | pub(crate) const PURGED: &[&str] = &[ | |
| 56 | "DELETE FROM session_entries WHERE pull_id IN (SELECT id FROM pulls WHERE repo_id = ?1)", | |
| 57 | "DELETE FROM check_runs WHERE pull_id IN (SELECT id FROM pulls WHERE repo_id = ?1)", | |
| 58 | "DELETE FROM review_runs WHERE pull_id IN (SELECT id FROM pulls WHERE repo_id = ?1)", | |
| 59 | "DELETE FROM agent_messages WHERE repo_id = ?1 OR pull_id IN (SELECT id FROM pulls WHERE repo_id = ?1)", | |
| 60 | "DELETE FROM queue_entries WHERE repo_id = ?1", | |
| 61 | "DELETE FROM agent_mentions WHERE repo_id = ?1", | |
| 62 | "DELETE FROM agent_rules WHERE repo_id = ?1", | |
| 63 | "DELETE FROM pull_confidence WHERE repo_id = ?1", | |
| 64 | "DELETE FROM run_confidence WHERE repo_id = ?1", | |
| 65 | "DELETE FROM confidence_rules WHERE repo_id = ?1", | |
| 66 | "DELETE FROM agent_runs WHERE repo_id = ?1", | |
| 67 | "DELETE FROM commit_statuses WHERE repo_id = ?1", | |
| Merge checks: statuses and check runs on every commit | 68 | "DELETE FROM commit_check_annotations WHERE run_id IN (SELECT id FROM commit_check_runs WHERE repo_id = ?1)", |
| 69 | "DELETE FROM commit_check_runs WHERE repo_id = ?1", | |
| 70 | "DELETE FROM commit_check_suites WHERE repo_id = ?1", | |
| Files pages say Files in the trail and keep the description in About only | 71 | "DELETE FROM plans WHERE repo_id = ?1", |
| 72 | "DELETE FROM comments WHERE repo_id = ?1", | |
| 73 | "DELETE FROM pulls WHERE repo_id = ?1", | |
| 74 | "DELETE FROM issues WHERE repo_id = ?1", | |
| Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar | 75 | "DELETE FROM labels WHERE repo_id = ?1", |
| 76 | "DELETE FROM milestones WHERE repo_id = ?1", | |
| Files pages say Files in the trail and keep the description in About only | 77 | "DELETE FROM counters WHERE repo_id = ?1", |
| 78 | "DELETE FROM repo_settings WHERE repo_id = ?1", | |
| 79 | "DELETE FROM memories WHERE scope = 'project' AND scope_key = ?1", | |
| 80 | "DELETE FROM guardrails WHERE scope <> 'workspace' AND scope_key = ?1", | |
| 81 | ]; | |
| 82 | ||
| 83 | /// What a run stopped because its repository was archived or deleted says | |
| 84 | /// as its last step; [`Work::runs_in_repo`] finds such runs by it. | |
| 85 | const STOPPED_STEP: &str = "Stopped: the repository was archived or deleted."; | |
| 86 | ||
| 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. | 87 | /// `repo.deleted` and `repo.archived`: the repository's runs that |
| Files pages say Files in the trail and keep the description in About only | 88 | /// are queued or running stop, `?1` the step they end on, `?2` the time, |
| 89 | /// `?3` its id. | |
| 90 | const STOP_RUNS: &str = "UPDATE agent_runs SET status = 'stopped', step = ?1, finished_at = ?2, updated_at = ?2 | |
| 91 | WHERE repo_id = ?3 AND status IN ('queued', 'running')"; | |
| 92 | ||
| 93 | /// `runs_in_repo` (internal, for the runner): the agent runs of a | |
| 94 | /// repository whose sandboxes should be stopped: those queued or running, | |
| 95 | /// and those work stopped in the last hour because the repository was | |
| 96 | /// archived or deleted. Takes `{ "repoId" }`; returns `[RepoRun]`. | |
| 97 | #[derive(Deserialize)] | |
| 98 | #[serde(rename_all = "camelCase")] | |
| 99 | pub(crate) struct RunsInRepoArgs { | |
| 100 | pub repo_id: String, | |
| 101 | } | |
| 102 | ||
| 103 | #[derive(Debug, Deserialize, Serialize)] | |
| 104 | #[serde(rename_all = "camelCase")] | |
| 105 | pub(crate) struct RepoRun { | |
| 106 | pub run_id: String, | |
| 107 | pub sandbox: String, | |
| 108 | pub kind: String, | |
| 109 | pub status: String, | |
| 110 | pub pull_id: Option<String>, | |
| 111 | } | |
| 112 | ||
| 113 | /// The repository, by id and path, that `event` says was deleted or | |
| 114 | /// archived (not unarchived): its runs stop. | |
| 115 | fn stopping(event: &Event) -> Option<(String, String, String)> { | |
| 116 | let data = event.data.clone(); | |
| 117 | match event.kind.as_str() { | |
| 118 | "repo.deleted" => serde_json::from_value::<RepoDeleted>(data).ok().map(|e| (e.repo_id, e.namespace, e.name)), | |
| 119 | "repo.archived" => serde_json::from_value::<RepoArchived>(data) | |
| 120 | .ok() | |
| 121 | .filter(|e| e.archived) | |
| 122 | .map(|e| (e.repo_id, e.namespace, e.name)), | |
| 123 | _ => None, | |
| 124 | } | |
| 125 | } | |
| 126 | ||
| 127 | /// `branch.renamed`: open pull requests from `?2` in repository `?1` | |
| 128 | /// come from `?3` now. A fork carries its change on its own default | |
| 129 | /// branch, so only branches of the repository itself are named. | |
| 130 | const BRANCH_RENAMED: &str = "UPDATE pulls SET source_branch = ?3 | |
| 131 | WHERE repo_id = ?1 AND source_branch = ?2 AND fork_repo_id IS NULL | |
| 132 | AND status IN ('draft', 'open')"; | |
| 133 | ||
| Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar | 134 | /// `branch.renamed`: open pull requests into `?2` merge into `?3` now. |
| 135 | /// Those into the default branch name none, and follow it as they are. | |
| 136 | const BASE_RENAMED: &str = "UPDATE pulls SET base_branch = ?3 | |
| 137 | WHERE repo_id = ?1 AND base_branch = ?2 AND status IN ('draft', 'open')"; | |
| 138 | ||
| Files pages say Files in the trail and keep the description in About only | 139 | impl Work { |
| 140 | /// Whether work may start on the repository: neither archived nor | |
| 141 | /// deleted. When repos cannot say, it is taken as active, and the | |
| 142 | /// repos calls that follow (which hide deleted repositories) decide. | |
| 143 | pub(crate) async fn repo_active(&self, repo_id: &str) -> Result<bool> { | |
| 144 | let status: Result<RepoStatus> = g1t_kit::call( | |
| 145 | &self.repos, | |
| 146 | "status_by_id", | |
| 147 | &StatusByIdArgs { | |
| 148 | id: repo_id.to_owned(), | |
| 149 | }, | |
| 150 | ) | |
| 151 | .await; | |
| 152 | Ok(match status { | |
| 153 | Ok(status) => status.active(), | |
| 154 | Err(error) => { | |
| 155 | worker::console_error!("status_by_id {repo_id}: {error}"); | |
| 156 | true | |
| 157 | } | |
| 158 | }) | |
| 159 | } | |
| 160 | ||
| 161 | /// See [`RunsInRepoArgs`]. | |
| 162 | pub(crate) async fn runs_in_repo(&self, a: RunsInRepoArgs) -> Result<Vec<RepoRun>> { | |
| 163 | let hour_ago = rfc3339(now_ms().saturating_sub(60 * 60 * 1000)); | |
| 164 | self.db | |
| 165 | .prepare( | |
| 166 | "SELECT id AS run_id, sandbox, kind, status, pull_id FROM agent_runs | |
| 167 | WHERE repo_id = ?1 AND (status IN ('queued', 'running') | |
| 168 | OR (status = 'stopped' AND step = ?2 AND finished_at >= ?3)) | |
| 169 | ORDER BY created_at LIMIT 200", | |
| 170 | ) | |
| 171 | .bind(&[a.repo_id.as_str().into(), STOPPED_STEP.into(), hour_ago.as_str().into()])? | |
| 172 | .all() | |
| 173 | .await? | |
| 174 | .results::<RepoRun>() | |
| 175 | } | |
| 176 | ||
| 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. | 177 | /// `repo.deleted` and `repo.archived`: its runs stop (the runner stops |
| 178 | /// their sandboxes, see [`RunsInRepoArgs`]). Rows are kept, for a restore. | |
| 179 | async fn stop_runs_in(&self, repo_id: &str) -> Result<()> { | |
| Files pages say Files in the trail and keep the description in About only | 180 | let now = rfc3339(now_ms()); |
| 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. | 181 | self.db |
| Files pages say Files in the trail and keep the description in About only | 182 | .prepare(STOP_RUNS) |
| 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. | 183 | .bind(&[STOPPED_STEP.into(), now.as_str().into(), repo_id.into()])? |
| 184 | .run() | |
| 185 | .await?; | |
| Files pages say Files in the trail and keep the description in About only | 186 | Ok(()) |
| 187 | } | |
| 188 | ||
| 189 | /// `repo.purged`, `repo.deleted`, `repo.archived` and `branch.renamed`. | |
| 190 | /// Says whether `event` was one this handles completely. | |
| 191 | pub(crate) async fn on_retired(&self, event: &Event) -> Result<bool> { | |
| 192 | if g1t_kit::lifecycle::on_purged(&self.db, event, PURGED).await? { | |
| 193 | return Ok(true); | |
| 194 | } | |
| 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. | 195 | if let Some((repo_id, _, _)) = stopping(event) { |
| 196 | self.stop_runs_in(&repo_id).await?; | |
| Files pages say Files in the trail and keep the description in About only | 197 | return Ok(true); |
| 198 | } | |
| 199 | if event.kind != "branch.renamed" { | |
| 200 | return Ok(false); | |
| 201 | } | |
| 202 | let Ok(renamed) = serde_json::from_value::<BranchRenamed>(event.data.clone()) else { | |
| 203 | worker::console_error!("branch.renamed {} could not be read", event.id); | |
| 204 | return Ok(true); | |
| 205 | }; | |
| 206 | if renamed.from == renamed.to { | |
| 207 | return Ok(true); | |
| 208 | } | |
| Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar | 209 | let names = [ |
| 210 | renamed.repo_id.as_str().into(), | |
| 211 | renamed.from.as_str().into(), | |
| 212 | renamed.to.as_str().into(), | |
| 213 | ]; | |
| Files pages say Files in the trail and keep the description in About only | 214 | self.db |
| Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar | 215 | .batch(vec![ |
| 216 | self.db.prepare(BRANCH_RENAMED).bind(&names)?, | |
| 217 | self.db.prepare(BASE_RENAMED).bind(&names)?, | |
| 218 | ]) | |
| Files pages say Files in the trail and keep the description in About only | 219 | .await?; |
| 220 | Ok(true) | |
| 221 | } | |
| 222 | } | |
| 223 | ||
| 224 | #[cfg(test)] | |
| 225 | mod tests { | |
| 226 | use super::*; | |
| 227 | ||
| 228 | fn repo(archived_at: Option<&str>) -> Repo { | |
| 229 | Repo { | |
| 230 | id: "rep_1".into(), | |
| 231 | namespace: "acme".into(), | |
| 232 | name: "web".into(), | |
| 233 | description: None, | |
| 234 | is_private: false, | |
| 235 | owner_id: "usr_1".into(), | |
| 236 | default_branch: "main".into(), | |
| 237 | fork_of: None, | |
| 238 | protected: false, | |
| 239 | created_at: "2026-10-05T00:00:00Z".into(), | |
| 240 | topics: Vec::new(), | |
| 241 | website: None, | |
| 242 | archived_at: archived_at.map(str::to_owned), | |
| Merge branch 'mirroring' into artifacts-mode | 243 | mirror: None, |
| Files pages say Files in the trail and keep the description in About only | 244 | } |
| 245 | } | |
| 246 | ||
| 247 | #[test] | |
| 248 | fn an_archived_repository_refuses_changes() { | |
| 249 | assert!(matches!(writable(&repo(None)), Outcome::Ok(()))); | |
| 250 | match writable(&repo(Some("2026-10-05T00:00:00Z"))) { | |
| 251 | Outcome::Fail(failure) => { | |
| 252 | assert_eq!(failure.code, FailureCode::Forbidden); | |
| 253 | assert!(failure.message.contains("acme/web is archived")); | |
| 254 | } | |
| 255 | Outcome::Ok(()) => panic!("an archived repository was writable"), | |
| 256 | } | |
| 257 | } | |
| 258 | ||
| 259 | #[test] | |
| 260 | fn purging_takes_only_the_repository_id() { | |
| 261 | for sql in PURGED { | |
| 262 | assert_eq!(g1t_kit::transfer::parameters(sql), 1, "{sql}"); | |
| 263 | } | |
| 264 | // Rows kept by pull request go before the pull requests do. | |
| 265 | let pulls = PURGED.iter().position(|sql| sql.starts_with("DELETE FROM pulls")).unwrap(); | |
| 266 | for table in ["session_entries", "check_runs", "review_runs", "agent_messages"] { | |
| 267 | let at = PURGED.iter().position(|sql| sql.contains(&format!("FROM {table} "))).unwrap(); | |
| 268 | assert!(at < pulls, "{table}"); | |
| 269 | } | |
| 270 | } | |
| 271 | ||
| 272 | fn event(kind: &str, data: serde_json::Value) -> Event { | |
| 273 | Event { | |
| 274 | id: "evt_1".into(), | |
| 275 | kind: kind.into(), | |
| 276 | source: "repos".into(), | |
| 277 | time: "2026-10-05T00:00:00Z".into(), | |
| 278 | repo_id: Some("rep_1".into()), | |
| 279 | actor: None, | |
| 280 | data, | |
| 281 | } | |
| 282 | } | |
| 283 | ||
| 284 | #[test] | |
| 285 | fn deleting_or_archiving_stops_runs_and_unarchiving_does_not() { | |
| 286 | let named = Some(("rep_1".to_owned(), "acme".to_owned(), "web".to_owned())); | |
| 287 | let data = serde_json::json!({ "repoId": "rep_1", "namespace": "acme", "name": "web", "archived": true }); | |
| 288 | assert_eq!(stopping(&event("repo.deleted", data.clone())), named); | |
| 289 | assert_eq!(stopping(&event("repo.archived", data)), named); | |
| 290 | let unarchived = serde_json::json!({ "repoId": "rep_1", "namespace": "acme", "name": "web", "archived": false }); | |
| 291 | assert_eq!(stopping(&event("repo.unarchived", unarchived.clone())), None); | |
| 292 | assert_eq!(stopping(&event("repo.archived", unarchived)), None); | |
| 293 | assert_eq!(STOP_RUNS.matches('?').count(), 4); | |
| 294 | } | |
| 295 | ||
| 296 | #[test] | |
| 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. | 297 | fn renamed_branches_bind_three_names() { |
| Files pages say Files in the trail and keep the description in About only | 298 | assert_eq!(g1t_kit::transfer::parameters(BRANCH_RENAMED), 3); |
| 299 | } | |
| 300 | } |
This file's history is long; its oldest lines are credited to the oldest commit read.