Skip to content

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

Commit

Security keeps work items, not issue numbers

The work items g1t opened for upgrades before security updates, and the alert activity that named them, are rewritten from the old issue numbers to the work items' numbers in the workspace when work.migrated is heard. Old upgrade work items are read and closed through the work item RPCs.

G1T-Session:
ses_01m566357hejh9yeqjbetx23f3
syntaqxcommitted Parenteb66a39Browse files
14 files+224−830/14 viewed
+1−0
16811681 "g1t-kit",
16821682 "g1t-scan",
16831683 "getrandom 0.2.17",
1684+ "rusqlite",
16841685 "serde",
16851686 "serde_json",
16861687 "serde_yaml",
+4−0
2323 futures-util = { version = "0.3", default-features = false, features = ["alloc"] }
2424 getrandom = { version = "0.2", features = ["js"] }
2525
26+[dev-dependencies]
27+# The migrations and the SQL that rewrites rows, on SQLite as D1 runs it.
28+rusqlite = { version = "0.32", features = ["bundled"] }
29+
2630 # wasm-opt at -O1: about the same gzipped size as -O in a tenth of the
2731 # time.
2832 [package.metadata.wasm-pack.profile.release]
+14−0
1+-- Issues became work items of the workspace (g1t #57). What g1t opened
2+-- for an upgrade, and what it was put on to fix an alert, is now a work
3+-- item, by its number in the repository's workspace.
4+--
5+-- Until the work service has numbered the moved issues, these columns
6+-- still hold the old issue numbers of each row's repository: `work.migrated`
7+-- rewrites them (lib.rs `work_migrated`). Every new number in a workspace
8+-- is above every old number of its repositories, so a rewritten value is
9+-- never taken for an old one.
10+ALTER TABLE updates RENAME COLUMN issue TO item;
11+ALTER TABLE update_pulls RENAME COLUMN issue TO item;
12+ALTER TABLE code_alerts RENAME COLUMN issue TO item;
13+ALTER TABLE alert_fixes RENAME COLUMN issue TO item;
14+ALTER TABLE upgrades RENAME COLUMN number TO item;
+2−1
492492 "add_comment",
493493 &AddCommentArgs {
494494 actor: system.clone(),
495− repo: path.clone(),
495+ repo: Some(path.clone()),
496+ workspace: String::new(),
496497 number,
497498 body: comment_text(&result.tool, result, &page),
498499 path: result.path.clone(),
+18−9
99 use g1t_contracts::security::{AlertState, FindLockfilesArgs, Lockfiles, VersionUpdatesState, VulnStatus, Vulnerability};
1010 use g1t_contracts::updates::DEPENDABOT_PATHS;
1111 use g1t_contracts::time::rfc3339;
12−use g1t_contracts::work::{AddCommentArgs, Issue, IssueDetail, ViewArgs};
12+use g1t_contracts::work::{AddCommentArgs, WorkItem, WorkItemDetail, WorkItemViewArgs};
1313 use g1t_contracts::{Outcome, User};
1414 use g1t_kit::now_ms;
1515 use g1t_scan::lockfiles::{Lockfile, Package};
271271 Ok(config.map(|(config, file)| Loaded { config, file, default_branch: default_branch.unwrap_or_default() }))
272272 }
273273
274− pub(crate) async fn issue(&self, actor: &User, repo: &RepoPath, number: u32) -> Result<Option<Issue>> {
275− let found: Outcome<IssueDetail> = g1t_kit::call(
274+ /// A work item of `workspace`, as `actor` sees it.
275+ pub(crate) async fn item(&self, actor: &User, workspace: &str, number: u32) -> Result<Option<WorkItem>> {
276+ let found: Outcome<WorkItemDetail> = g1t_kit::call(
276277 &self.work,
277− "get_issue",
278− &ViewArgs { repo: repo.clone(), number, viewer: Some(actor.clone()), after_seq: 0 },
278+ "get_work_item",
279+ &WorkItemViewArgs { workspace: workspace.to_owned(), number, viewer: Some(actor.clone()) },
279280 )
280281 .await?;
281− Ok(found.into_result().ok().map(|detail| detail.issue))
282+ Ok(found.into_result().ok().map(|detail| detail.item))
282283 }
283284
285+ /// Comments on pull request `number` of `repo`.
284286 pub(crate) async fn comment(&self, actor: &User, repo: &RepoPath, number: u32, body: String) -> Result<()> {
287+ self.comment_on(actor, Some(repo), "", number, body).await
288+ }
289+
290+ /// Comments on a thread: with `repo`, its pull request `number`;
291+ /// without, the work item `number` of `workspace`.
292+ pub(crate) async fn comment_on(&self, actor: &User, repo: Option<&RepoPath>, workspace: &str, number: u32, body: String) -> Result<()> {
285293 let _: Outcome<Value> = g1t_kit::call(
286294 &self.work,
287295 "add_comment",
288296 &AddCommentArgs {
289297 actor: actor.clone(),
290− repo: repo.clone(),
298+ repo: repo.cloned(),
299+ workspace: workspace.to_owned(),
291300 number,
292301 body,
293302 path: None,
363372 severity: advisory.severity.as_str().to_owned(),
364373 fixed_version: advisory.fixed.clone(),
365374 status: VulnStatus::Open,
366− issue: None,
375+ item: None,
367376 found_at: String::new(),
368377 fixed_at: None,
369378 state: AlertState::Open,
443452 status: "open".into(),
444453 found_at: "2026-10-04T00:00:00Z".into(),
445454 fixed_at: None,
446− number: None,
455+ item: None,
447456 dismiss_reason: None,
448457 dismiss_comment: None,
449458 dismissed_by: None,
+7−0
643643 self.store.rename_namespace(&renamed.stale_slugs(&renamed.to), &renamed.to).await?;
644644 }
645645 }
646+ // Issues became work items: what was kept by their old numbers
647+ // names the work items now.
648+ "work.migrated" => {
649+ if let Ok(moved) = serde_json::from_value::<g1t_contracts::events::WorkMigrated>(event.data.clone()) {
650+ self.store.work_migrated(&moved.items).await?;
651+ }
652+ }
646653 _ => {}
647654 }
648655 Ok(())
+1−1
246246 body.push('\n');
247247 }
248248 body.push_str(&format!(
249− "\nThis pull request lands through this branch's required checks like any other. If an update breaks them, g1t closes it and puts g1t on an issue to make the code changes it needs.\n\n{COMMANDS}\n\n---\n_Opened by g1t's version updates, as `{file}` asks{}._",
249+ "\nThis pull request lands through this branch's required checks like any other. If an update breaks them, g1t closes it and puts g1t on a work item to make the code changes it needs.\n\n{COMMANDS}\n\n---\n_Opened by g1t's version updates, as `{file}` asks{}._",
250250 entry.name.as_deref().map(|name| format!(" for {name}")).unwrap_or_default()
251251 ));
252252 body
+4−4
282282 if still_needed(&fixes) {
283283 continue;
284284 }
285− self.store.set_update(&row, UpdateState::Superseded, row.pull(), row.issue(), None).await?;
285+ self.store.set_update(&row, UpdateState::Superseded, row.pull(), row.item(), None).await?;
286286 let Some(number) = row.pull() else { continue };
287287 let packages = [row.package.clone()];
288288 match self.retire(repo, number, |base| resolved_text(base, &packages, &fixes)).await? {
289− Retired::Merged => self.store.set_update(&row, UpdateState::Merged, Some(number), row.issue(), None).await?,
289+ Retired::Merged => self.store.set_update(&row, UpdateState::Merged, Some(number), row.item(), None).await?,
290290 Retired::Closed => {
291291 let activity: Vec<Activity> =
292292 fixes.iter().map(|alert| Activity { alert_id: &alert.id, action: "update_superseded", actor: Some(system), reason: None, comment: None, number: Some(number) }).collect();
343343 if let Some(update) = self.store.update(&repo.repo_id, osv, &dependency.name).await?
344344 && (update.state().in_progress() && (update.pull() == row.pull() || update.branch.as_deref() == Some(row.branch.as_str())))
345345 {
346− self.store.set_update(&update, state, update.pull(), update.issue(), None).await?;
346+ self.store.set_update(&update, state, update.pull(), update.item(), None).await?;
347347 }
348348 }
349349 }
426426 status: status.into(),
427427 found_at: "2026-10-04T00:00:00Z".into(),
428428 fixed_at: None,
429− number: None,
429+ item: None,
430430 dismiss_reason: None,
431431 dismiss_comment: None,
432432 dismissed_by: None,
+31−27
88 //! (`User::system`). It lands through the branch's required checks like
99 //! any other. An older one for the same package is closed as superseded.
1010 //! 3. When its checks fail because code has to change, or the sandbox
11−//! never pushed, the pull request is closed and an issue is opened for
11+//! never pushed, the pull request is closed and a work item is opened for
1212 //! g1t to work on, started by g1t. That is the only time an agent is
1313 //! used.
1414 //! 4. Once none of the alerts it fixes is open (fixed on the default
2222 use g1t_contracts::repos::RepoPath;
2323 use g1t_contracts::security::{BumpArgs, UpdateState, update_branch};
2424 use g1t_contracts::time::rfc3339;
25−use g1t_contracts::work::{IssueActionArgs, IssueReason, OpenPullArgs, Pull, PullActionArgs, PullDetail, PullStatus, Runtime, ViewArgs};
25+use g1t_contracts::work::{
26+ CloseReason, CloseWorkItemArgs, OpenPullArgs, Pull, PullActionArgs, PullDetail, PullStatus, Runtime, ViewArgs, WorkItemActionArgs,
27+};
2628 use g1t_contracts::{Outcome, User};
2729 use g1t_kit::now_ms;
2830 use g1t_scan::osv::{self, Severity};
105107 body.push_str(&format!(
106108 "\nLockfiles changed: {}.\n\n\
107109 Only the version changes. This pull request lands through this branch's required checks like any other. \
108− If they fail because code has to change, g1t closes it and puts g1t on an issue to make the change.\n\n\
110+ If they fail because code has to change, g1t closes it and puts g1t on a work item to make the change.\n\n\
109111 ---\n_Opened by g1t's security updates. Turn them off for this project on its Security page._",
110112 lockfiles.iter().map(|path| format!("`{path}`")).collect::<Vec<_>>().join(", ")
111113 ));
210212 body.push_str(&advisory_table(vulns));
211213 body.push_str(&format!(
212214 "\nOnly the versions change. This pull request lands through this branch's required checks like any other. \
213− If they fail because code has to change, g1t closes it and puts g1t on an issue to make the change.\n\n{}\n\n---\n\
215+ If they fail because code has to change, g1t closes it and puts g1t on a work item to make the change.\n\n{}\n\n---\n\
214216 _Opened by g1t's security updates, grouped as `{file}` asks. Turn them off for this project on its Security page._",
215217 pull_text::COMMANDS
216218 ));
245247 repo: Self::path_of(repo),
246248 number,
247249 summary: String::new(),
248− keep_issue_open: false,
250+ keep_item_open: false,
249251 ignore_checks: false,
250252 bypass_rules: false,
251253 merge_method: None,
263265 Ok(matches!(closed, Outcome::Ok(_)))
264266 }
265267
266− /// Closes an issue g1t opened for an update before it stopped opening
267− /// them, saying why. One already closed is left as it is.
268− pub(crate) async fn close_issue_with(&self, repo: &RepoRow, number: u32, why: String) -> Result<()> {
268+ /// The slug of the workspace a repository is in.
269+ pub(crate) fn workspace_of(repo: &RepoRow) -> String {
270+ repo.namespace.to_lowercase()
271+ }
272+
273+ /// Closes a work item g1t opened for an update before it stopped
274+ /// opening them, saying why. One already closed is left as it is.
275+ pub(crate) async fn close_item_with(&self, repo: &RepoRow, number: u32, why: String) -> Result<()> {
269276 let system = User::system(&repo.namespace);
270− let path = Self::path_of(repo);
271− let closed: Outcome<Value> = g1t_kit::call(
272− &self.work,
273− "close_issue",
274− &IssueActionArgs { actor: system.clone(), repo: path.clone(), number, reason: Some(IssueReason::NotPlanned) },
275− )
276− .await?;
277+ let workspace = Self::workspace_of(repo);
278+ let item = WorkItemActionArgs { actor: system.clone(), workspace: workspace.clone(), number, reason: Some(CloseReason::NotPlanned) };
279+ let closed: Outcome<Value> =
280+ g1t_kit::call(&self.work, "close_work_item", &CloseWorkItemArgs { item, repo: None, commit: None }).await?;
277281 if matches!(closed, Outcome::Ok(_)) {
278− self.comment(&system, &path, number, why).await?;
282+ self.comment_on(&system, None, &workspace, number, why).await?;
279283 }
280284 Ok(())
281285 }
350354 }
351355 }
352356 None => {
353− // An upgrade issue from before security updates, still
354− // open, is left to the agent on it.
357+ // An upgrade work item from before security updates,
358+ // still open, is left to the agent on it.
355359 if let Some(existing) = self.store.upgrade(&repo.repo_id, &ecosystem, &package).await?
356360 && self
357− .issue(&User::system(&repo.namespace), &Self::path_of(repo), existing.number as u32)
361+ .item(&User::system(&repo.namespace), &Self::workspace_of(repo), existing.item as u32)
358362 .await?
359− .is_some_and(|issue| issue.state == g1t_contracts::work::State::Open)
363+ .is_some_and(|item| item.state == g1t_contracts::work::State::Open)
360364 {
361365 continue;
362366 }
539543 &OpenPullArgs {
540544 actor: system.clone(),
541545 repo: Self::path_of(&repo),
542− issue: None,
546+ item: None,
543547 title: prefixed(&prefix, pull_title(&row.package, &row.target)),
544548 body: pull_text(&row.ecosystem, &row.package, &row.target, &vulns),
545549 branch: Some(branch.to_owned()),
656660 self.store.set_update(&row, UpdateState::Failed, row.pull(), None, Some(&why)).await?;
657661 self.note(&row, "update_failed", Some(g1t_contracts::system::USERNAME), None, Some(&why)).await?;
658662 }
659− // Left from when g1t opened an issue for an upgrade it could not
660− // make: the issue closes, and the update fails as one would now.
663+ // Left from when g1t opened a work item for an upgrade it could not
664+ // make: the work item closes, and the update fails as one would now.
661665 for row in self.store.needs_code_updates(10).await? {
662666 let why = cannot_raise(&row.package, &row.target);
663− if let (Some(repo), Some(number)) = (self.store.repo(&row.repo_id).await?, row.issue()) {
667+ if let (Some(repo), Some(number)) = (self.store.repo(&row.repo_id).await?, row.item()) {
664668 let path = Self::path_of(&repo);
665669 let alerts = format!("/{}/{}/security/vulnerabilities", path.namespace, path.name);
666− self.close_issue_with(&repo, number, format!(
667− "g1t no longer opens issues for upgrades it cannot make on its own. This one is on [its alert]({alerts}), \
670+ self.close_item_with(&repo, number, format!(
671+ "g1t no longer opens work items for upgrades it cannot make on its own. This one is on [its alert]({alerts}), \
668672 which says why and is tried again later."
669673 ))
670674 .await?;
742746 status: "open".into(),
743747 found_at: "2026-10-04T00:00:00Z".into(),
744748 fixed_at: None,
745− number: None,
749+ item: None,
746750 dismiss_reason: None,
747751 dismiss_comment: None,
748752 dismissed_by: None,
+107−16
113113 }
114114
115115 /// Secrets with how many places each was found.
116+/// Rewrites the work items kept by the old issue numbers of their
117+/// repositories to their numbers in the workspace, `?1` a page of
118+/// `work.migrated`'s items as JSON. Each new number is above every old one
119+/// of its workspace, so a page heard twice changes nothing more.
120+pub const WORK_MIGRATED: [&str; 6] = [
121+ "UPDATE updates SET item = m.number FROM moved_items m WHERE updates.repo_id = m.repo_id AND updates.item = m.legacy",
122+ "UPDATE update_pulls SET item = m.number FROM moved_items m WHERE update_pulls.repo_id = m.repo_id AND update_pulls.item = m.legacy",
123+ "UPDATE upgrades SET item = m.number FROM moved_items m WHERE upgrades.repo_id = m.repo_id AND upgrades.item = m.legacy",
124+ "UPDATE code_alerts SET item = m.number FROM moved_items m WHERE code_alerts.repo_id = m.repo_id AND code_alerts.item = m.legacy",
125+ "UPDATE alert_fixes SET item = m.number FROM moved_items m WHERE alert_fixes.repo_id = m.repo_id AND alert_fixes.item = m.legacy",
126+ // Only an update that needed code changes named a work item; the
127+ // other steps name pull requests, which kept their numbers.
128+ "UPDATE alert_activity SET number = m.number FROM moved_items m
129+ WHERE alert_activity.repo_id = m.repo_id AND alert_activity.number = m.legacy AND alert_activity.action = 'update_needs_code'",
130+];
131+
132+/// `work.migrated`'s items (`?1`), as the rows [`WORK_MIGRATED`] reads.
133+const MOVED_ITEMS: &str = "WITH moved_items AS (
134+ SELECT json_extract(value, '$.repoId') AS repo_id, json_extract(value, '$.legacyNumber') AS legacy,
135+ json_extract(value, '$.number') AS number
136+ FROM json_each(?1))";
137+
138+/// One of [`WORK_MIGRATED`], ready to run.
139+pub fn work_migrated_sql(statement: &str) -> String {
140+ format!("{MOVED_ITEMS}
141+{statement}")
142+}
143+
116144 const SECRET_SELECT: &str =
117145 "SELECT s.*, (SELECT count(*) FROM secret_locations l WHERE l.secret_id = s.id) AS location_count FROM secrets s";
118146
174202 pub status: String,
175203 pub found_at: String,
176204 pub fixed_at: Option<String>,
177− pub number: Option<i64>,
205+ pub item: Option<i64>,
178206 #[serde(default)]
179207 pub dismiss_reason: Option<String>,
180208 #[serde(default)]
202230 severity: row.severity,
203231 fixed_version: row.fixed_version,
204232 status,
205− issue: row.number.map(|number| number as u32),
233+ item: row.item.map(|number| number as u32),
206234 found_at: row.found_at,
207235 fixed_at: row.fixed_at,
208236 state: status.state(),
225253 pub state: String,
226254 pub branch: Option<String>,
227255 pub pull: Option<i64>,
228− pub issue: Option<i64>,
256+ pub item: Option<i64>,
229257 pub error: Option<String>,
230258 pub updated_at: String,
231259 }
239267 self.pull.map(|n| n.max(0) as u32)
240268 }
241269
242− pub fn issue(&self) -> Option<u32> {
243− self.issue.map(|n| n.max(0) as u32)
270+ pub fn item(&self) -> Option<u32> {
271+ self.item.map(|n| n.max(0) as u32)
244272 }
245273
246274 pub fn to_contract(&self) -> SecurityUpdate {
249277 target: self.target.clone(),
250278 branch: self.branch.clone(),
251279 pull: self.pull(),
252− issue: self.issue(),
280+ item: self.item(),
253281 error: self.error.clone(),
254282 updated_at: self.updated_at.clone(),
255283 }
308336
309337 #[derive(Deserialize)]
310338 pub struct UpgradeRow {
311− pub number: i64,
339+ pub item: i64,
312340 }
313341
314342 #[derive(Deserialize)]
348376
349377 const VULN_COLUMNS: &str = "v.id, v.repo_id, v.ecosystem, v.package, v.version, v.manifest, v.osv_id, v.advisory,
350378 v.summary, v.severity, v.fixed_version, v.status, v.found_at, v.fixed_at, v.dismiss_reason, v.dismiss_comment,
351− v.dismissed_by, v.dismissed_at, COALESCE(n.issue, u.number) AS number";
379+ v.dismissed_by, v.dismissed_at, COALESCE(n.item, u.item) AS item";
352380
353−/// The issue an upgrade has: the security update's, when it went to
381+/// The work item an upgrade has: the security update's, when it went to
354382 /// g1t, or one opened before security updates.
355383 const VULN_JOINS: &str = "LEFT JOIN upgrades u ON u.repo_id = v.repo_id AND u.ecosystem = v.ecosystem AND u.package = v.package
356384 LEFT JOIN updates n ON n.repo_id = v.repo_id AND n.ecosystem = v.ecosystem AND n.package = v.package";
420448 Ok(())
421449 }
422450
451+ /// `work.migrated`: what was kept by old issue numbers names the work
452+ /// items they became.
453+ pub async fn work_migrated(&self, items: &[g1t_contracts::work::MigratedItem]) -> Result<()> {
454+ if items.is_empty() {
455+ return Ok(());
456+ }
457+ let page = serde_json::to_string(items)?;
458+ let mut batch = Vec::with_capacity(WORK_MIGRATED.len());
459+ for sql in WORK_MIGRATED {
460+ batch.push(self.db.prepare(work_migrated_sql(sql)).bind(&[page.as_str().into()])?);
461+ }
462+ self.db.batch(batch).await?;
463+ Ok(())
464+ }
465+
423466 /// Records that a repository's daily dependency read was skipped, and
424467 /// why, so the sweep moves on to others until the next day.
425468 pub async fn skip_dependencies(&self, repo_id: &str, why: &str) -> Result<()> {
9841027 }
9851028
9861029 /// Updates asked of a sandbox before `before` that never pushed.
987− /// Updates left in `needs_code`, which g1t opened an issue for before
1030+ /// Updates left in `needs_code`, which g1t opened a work item for before
9881031 /// it stopped doing that.
9891032 pub async fn needs_code_updates(&self, limit: u32) -> Result<Vec<UpdateRow>> {
9901033 self.db
10141057 "INSERT INTO updates (repo_id, ecosystem, package, target, state, branch, requested_at, updated_at)
10151058 VALUES (?1, ?2, ?3, ?4, 'requested', ?5, ?6, ?6)
10161059 ON CONFLICT (repo_id, ecosystem, package) DO UPDATE SET
1017− target = ?4, state = 'requested', branch = ?5, error = NULL, issue = NULL,
1060+ target = ?4, state = 'requested', branch = ?5, error = NULL, item = NULL,
10181061 pull = CASE WHEN updates.state = 'open' THEN updates.pull ELSE NULL END,
10191062 requested_at = ?6, updated_at = ?6",
10201063 )
10291072 row: &UpdateRow,
10301073 state: UpdateState,
10311074 pull: Option<u32>,
1032− issue: Option<u32>,
1075+ item: Option<u32>,
10331076 error: Option<&str>,
10341077 ) -> Result<()> {
10351078 self.db
10361079 .prepare(
1037− "UPDATE updates SET state = ?, pull = ?, issue = ?, error = ?, updated_at = ?
1080+ "UPDATE updates SET state = ?, pull = ?, item = ?, error = ?, updated_at = ?
10381081 WHERE repo_id = ? AND ecosystem = ? AND package = ?",
10391082 )
10401083 .bind(&[
10411084 state.as_str().into(),
10421085 pull.map_or(JsValue::NULL, JsValue::from),
1043− issue.map_or(JsValue::NULL, JsValue::from),
1086+ item.map_or(JsValue::NULL, JsValue::from),
10441087 optional(error),
10451088 now().into(),
10461089 row.repo_id.as_str().into(),
11081151
11091152 // --- Upgrades -----------------------------------------------------------
11101153
1111− /// The issue opened for a package before security updates, if any.
1154+ /// The work item opened for a package before security updates, if any.
11121155 pub async fn upgrade(&self, repo_id: &str, ecosystem: &str, package: &str) -> Result<Option<UpgradeRow>> {
11131156 self.db
1114− .prepare("SELECT number FROM upgrades WHERE repo_id = ? AND ecosystem = ? AND package = ?")
1157+ .prepare("SELECT item FROM upgrades WHERE repo_id = ? AND ecosystem = ? AND package = ?")
11151158 .bind(&[repo_id.into(), ecosystem.into(), package.into()])?
11161159 .first::<UpgradeRow>(None)
11171160 .await
11981241 mod tests {
11991242 use super::*;
12001243
1244+ /// The migrations, in order, from `migrations/`.
1245+ fn migrations() -> Vec<(String, String)> {
1246+ let dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("migrations");
1247+ let mut names: Vec<String> =
1248+ std::fs::read_dir(&dir).unwrap().map(|entry| entry.unwrap().file_name().into_string().unwrap()).collect();
1249+ names.sort();
1250+ names.into_iter().map(|name| (name.clone(), std::fs::read_to_string(dir.join(&name)).unwrap())).collect()
1251+ }
1252+
1253+ #[test]
1254+ fn old_issue_numbers_become_their_work_items_numbers() {
1255+ let db = rusqlite::Connection::open_in_memory().unwrap();
1256+ let (before, after): (Vec<_>, Vec<_>) = migrations().into_iter().partition(|(name, _)| name.as_str() < "0007");
1257+ for (name, sql) in &before {
1258+ db.execute_batch(sql).unwrap_or_else(|error| panic!("{name}: {error}"));
1259+ }
1260+ db.execute_batch(
1261+ "INSERT INTO updates (repo_id, ecosystem, package, target, state, pull, issue, requested_at, updated_at)
1262+ VALUES ('rep_1', 'npm', 'a', '2', 'needs_code', NULL, 5, '', ''), ('rep_2', 'npm', 'a', '2', 'needs_code', NULL, 5, '', '');
1263+ INSERT INTO upgrades (repo_id, ecosystem, package, number, target, opened_at) VALUES ('rep_1', 'npm', 'b', 3, '2', '');
1264+ INSERT INTO alert_activity (id, repo_id, alert_id, action, number, at)
1265+ VALUES ('a1', 'rep_1', 'vul_1', 'update_needs_code', 5, ''), ('a2', 'rep_1', 'vul_1', 'update_opened', 5, '');",
1266+ )
1267+ .unwrap();
1268+ for (name, sql) in &after {
1269+ db.execute_batch(sql).unwrap_or_else(|error| panic!("{name}: {error}"));
1270+ }
1271+ let page = serde_json::json!([
1272+ { "repoId": "rep_1", "legacyNumber": 5, "workspace": "acme", "number": 41, "itemId": "iss_5" },
1273+ { "repoId": "rep_1", "legacyNumber": 3, "workspace": "acme", "number": 40, "itemId": "iss_3" },
1274+ ])
1275+ .to_string();
1276+ // Heard twice, as a queue may deliver it.
1277+ for _ in 0..2 {
1278+ for statement in WORK_MIGRATED {
1279+ db.execute(&work_migrated_sql(statement), [&page]).unwrap();
1280+ }
1281+ }
1282+ let item = |sql: &str| db.query_row(sql, [], |row| row.get::<_, Option<i64>>(0)).unwrap();
1283+ assert_eq!(item("SELECT item FROM updates WHERE repo_id = 'rep_1'"), Some(41));
1284+ // Another repository's number 5 was another issue.
1285+ assert_eq!(item("SELECT item FROM updates WHERE repo_id = 'rep_2'"), Some(5));
1286+ assert_eq!(item("SELECT item FROM upgrades WHERE repo_id = 'rep_1'"), Some(40));
1287+ assert_eq!(item("SELECT number FROM alert_activity WHERE id = 'a1'"), Some(41));
1288+ // A pull request kept its number.
1289+ assert_eq!(item("SELECT number FROM alert_activity WHERE id = 'a2'"), Some(5));
1290+ }
1291+
12011292 #[test]
12021293 fn only_real_secrets_in_history_are_open() {
12031294 let counts = secret_counts(
+2−2
204204 pub dismiss_comment: Option<String>,
205205 pub dismissed_by: Option<String>,
206206 pub dismissed_at: Option<String>,
207− pub issue: Option<i64>,
207+ pub item: Option<i64>,
208208 }
209209
210210 fn small(value: Option<i64>) -> Option<u32> {
247247 dismissed_reason: row.dismiss_reason.as_deref().and_then(DismissReason::parse).filter(|_| dismissed),
248248 dismissed_comment: row.dismiss_comment.filter(|_| dismissed),
249249 dismissed_at: row.dismissed_at.filter(|_| dismissed),
250− issue: small(row.issue),
250+ item: small(row.item),
251251 }
252252 }
253253 }
+13−3
371371 let done: Result<Outcome<Value>> = match comment_plan(our_comment(comments), &body, changes) {
372372 CommentPlan::Leave => return,
373373 CommentPlan::Edit(comment_id) => {
374− g1t_kit::call(&self.work, "edit_comment", &CommentActionArgs { actor, repo: path, comment_id, body }).await
374+ g1t_kit::call(&self.work, "edit_comment", &CommentActionArgs { actor, repo: Some(path), workspace: String::new(), comment_id, body }).await
375375 }
376376 CommentPlan::Add => {
377− g1t_kit::call(&self.work, "add_comment", &AddCommentArgs { actor, repo: path, number, body, path: None, line: None, verdict: None })
378− .await
377+ g1t_kit::call(&self.work, "add_comment", &AddCommentArgs {
378+ actor,
379+ repo: Some(path),
380+ workspace: String::new(),
381+ number,
382+ body,
383+ path: None,
384+ line: None,
385+ verdict: None,
386+ },
387+ )
388+ .await
379389 }
380390 };
381391 match done {
+8−8
4242 pub reviewers: String,
4343 pub state: String,
4444 pub pull: Option<i64>,
45− /// The issue g1t opened for it, from before it stopped doing that.
45+ /// The work item g1t opened for it, from before it stopped doing that.
4646 #[serde(default)]
47− pub issue: Option<i64>,
47+ pub item: Option<i64>,
4848 pub head: Option<String>,
4949 pub merge_by: Option<String>,
5050 pub error: Option<String>,
6060 self.pull.map(|n| n.max(0) as u32)
6161 }
6262
63− pub fn issue(&self) -> Option<u32> {
64− self.issue.map(|n| n.max(0) as u32)
63+ pub fn item(&self) -> Option<u32> {
64+ self.item.map(|n| n.max(0) as u32)
6565 }
6666
6767 pub fn dependencies(&self) -> Vec<UpdatedDependency> {
259259 }
260260
261261 /// Update pull requests asked of a sandbox before `before` that never pushed.
262− /// Version updates left in `needs_code`, which g1t opened an issue for
262+ /// Version updates left in `needs_code`, which g1t opened a work item for
263263 /// before it stopped doing that.
264264 pub async fn needs_code_update_pulls(&self, limit: u32) -> Result<Vec<PullRow>> {
265265 self.db
311311 Ok(id)
312312 }
313313
314− pub async fn set_update_pull(&self, id: &str, state: UpdateState, pull: Option<u32>, issue: Option<u32>, error: Option<&str>) -> Result<()> {
314+ pub async fn set_update_pull(&self, id: &str, state: UpdateState, pull: Option<u32>, item: Option<u32>, error: Option<&str>) -> Result<()> {
315315 self.db
316− .prepare("UPDATE update_pulls SET state = ?, pull = ?, issue = ?, error = ?, updated_at = ? WHERE id = ?")
316+ .prepare("UPDATE update_pulls SET state = ?, pull = ?, item = ?, error = ?, updated_at = ? WHERE id = ?")
317317 .bind(&[
318318 state.as_str().into(),
319319 pull.map_or(JsValue::NULL, JsValue::from),
320− issue.map_or(JsValue::NULL, JsValue::from),
320+ item.map_or(JsValue::NULL, JsValue::from),
321321 optional(error),
322322 now().into(),
323323 id.into(),
+12−12
1616 //! asked as the file says. An older one for the same dependency or group
1717 //! is closed as superseded.
1818 //! 4. The sweep watches open ones: a failing required check closes it and
19−//! puts g1t on an issue to make the code changes, as security updates
19+//! puts g1t on a work item to make the code changes, as security updates
2020 //! do; one asked to merge (`@g1t merge`) merges once its checks pass;
2121 //! one in conflict is made again from its base (`rebase-strategy`).
2222 //! 5. `@g1t` comments on these pull requests are commands
399399 let error = "The update could not be made in a sandbox: its packages may need code changes to move, or a registry could not be reached.";
400400 self.store.set_update_pull(&row.id, UpdateState::Failed, row.pull(), None, Some(error)).await?;
401401 }
402− // Left from when g1t opened an issue for an update whose checks
403− // failed: the issue closes, and the next run opens the update again
402+ // Left from when g1t opened a work item for an update whose checks
403+ // failed: the work item closes, and the next run opens the update again
404404 // as a pull request that stays open with its failing checks.
405405 for row in self.store.needs_code_update_pulls(10).await? {
406− if let (Some(repo), Some(number)) = (self.store.repo(&row.repo_id).await?, row.issue()) {
407− let why = "g1t no longer opens issues for updates whose checks fail. The entry's next run opens this update again \
406+ if let (Some(repo), Some(number)) = (self.store.repo(&row.repo_id).await?, row.item()) {
407+ let why = "g1t no longer opens work items for updates whose checks fail. The entry's next run opens this update again \
408408 as a pull request, which stays open with its checks for you to decide on.";
409− self.close_issue_with(&repo, number, why.to_owned()).await?;
409+ self.close_item_with(&repo, number, why.to_owned()).await?;
410410 }
411411 let error = "Raising the versions failed this branch's required checks.";
412412 self.store.set_update_pull(&row.id, UpdateState::Failed, row.pull(), None, Some(error)).await?;
931931 &OpenPullArgs {
932932 actor: system.clone(),
933933 repo: Self::repo_path(repo),
934− issue: None,
934+ item: None,
935935 title: row.title.clone(),
936936 body: row.body.clone(),
937937 branch: Some(row.branch.clone()),
10731073 repo: Self::repo_path(&repo),
10741074 number,
10751075 summary: String::new(),
1076− keep_issue_open: false,
1076+ keep_item_open: false,
10771077 ignore_checks: false,
10781078 bypass_rules: false,
10791079 merge_method: None,
13761376 let merged: Outcome<Pull> = g1t_kit::call(
13771377 &self.work,
13781378 "merge_pull",
1379− &PullActionArgs { actor: system.clone(), repo: path.clone(), number, summary: String::new(), keep_issue_open: false, ignore_checks: false, bypass_rules: false, merge_method: matches!(command, UpdateCommand::SquashAndMerge).then_some(g1t_contracts::rules::MergeMethod::Squash), commit_title: None, commit_message: None, expected_head: None, delete_branch: None, update_method: None },
1379+ &PullActionArgs { actor: system.clone(), repo: path.clone(), number, summary: String::new(), keep_item_open: false, ignore_checks: false, bypass_rules: false, merge_method: matches!(command, UpdateCommand::SquashAndMerge).then_some(g1t_contracts::rules::MergeMethod::Squash), commit_title: None, commit_message: None, expected_head: None, delete_branch: None, update_method: None },
13801380 )
13811381 .await?;
13821382 match merged {
15861586 reviewers: "[]".into(),
15871587 state: state.into(),
15881588 pull: Some(3),
1589− issue: None,
1589+ item: None,
15901590 head: None,
15911591 merge_by: None,
15921592 error: None,
16361636 let status = |state: &str| CommitStatus { context: "CI / push".into(), state: state.into(), description: None, target_url: None, updated_at: String::new(), source: None, check_run_id: None };
16371637 let detail = |required: Vec<RequiredCheck>, statuses: Vec<CommitStatus>| {
16381638 let mut detail: PullDetail = serde_json::from_value(json!({
1639− "pull": {"id": "pul_1", "repoId": "rep_1", "number": 1, "issue": null, "title": "t", "body": null, "agent": "", "runtime": "external",
1639+ "pull": {"id": "pul_1", "repoId": "rep_1", "number": 1, "item": null, "title": "t", "body": null, "agent": "", "runtime": "external",
16401640 "status": "open", "fork": null, "forkRepoId": null, "branch": "b", "headCommit": "c", "mergeBase": null, "mergedBy": null,
16411641 "mergedAt": null, "supersededBy": null, "checkStatus": null,
16421642 "author": {"id": "g1t", "username": "g1t", "kind": "system"}, "createdAt": "", "updatedAt": ""},
1643− "issue": null, "comments": [], "checks": null
1643+ "item": null, "comments": [], "checks": null
16441644 }))
16451645 .unwrap();
16461646 detail.required_checks = required;

Comments

Sign in to comment.