1use serde::{Deserialize, Serialize};
4
5pub const WITHDRAW: &str = "withdraw: close PR and end task";
7pub const DISMISS: &str = "dismiss: end obsolete approval without changing PR";
9pub const REIMPLEMENT: &str = "reimplement:";
11
12#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
14pub struct Target {
15 pub pr: String,
17 pub head: String,
19 pub language: String,
21}
22
23#[derive(Debug, Clone, Serialize, Deserialize)]
26pub struct Decision {
27 pub question: String,
29 pub target: Target,
31 pub choice: String,
33 #[serde(default)]
34 pub closed: bool,
36 #[serde(default)]
37 pub replacement_run: Option<String>,
39 #[serde(default)]
40 pub replacement_pr: Option<String>,
42}
43
44#[derive(Debug, Clone, Serialize, Deserialize)]
46pub struct Replacement {
47 pub question: String,
49 pub run: String,
51 pub pr: String,
53 pub agent: String,
55 pub context: String,
57}
58
59pub fn selected_agent(choice: &str) -> Option<&str> {
61 choice.strip_prefix(REIMPLEMENT).filter(|id| !id.is_empty())
62}
63
64pub fn stops(choice: &str) -> bool {
66 choice.eq_ignore_ascii_case(crate::land::HOLD)
67 || choice == WITHDRAW
68 || choice == DISMISS
69 || selected_agent(choice).is_some()
70}
71
72pub(crate) fn dismiss(home: &std::path::Path, q: &crate::ask::Question) -> anyhow::Result<()> {
78 use crate::run::{Liveness, RunState, RunStatus};
79 use anyhow::ensure;
80
81 ensure!(
82 q.node == crate::land::APPROVAL_NODE,
83 "only merge approvals can be dismissed"
84 );
85 let queue = crate::queue::Queue::at(home.join("queue"));
86 let owners: Vec<_> = queue
87 .list()
88 .into_iter()
89 .filter(|t| t.runs.contains(&q.run))
90 .collect();
91 let mut claims = Vec::new();
92 for task in &owners {
93 claims.push(queue.claim(&task.id)?);
94 }
95 let _lock = crate::pr_head::owner_lock(home, &q.run)?;
96 let mut state = RunState::load_under(&q.run, home)?;
97 ensure!(
98 state.liveness(false) == Liveness::Dead,
99 "stop the current driver before dismissing this obsolete approval"
100 );
101 ensure!(
102 state.land_armed_head.is_none(),
103 "disable existing PR auto-merge before dismissing this approval"
104 );
105 let binding = state
106 .land_approval
107 .as_ref()
108 .filter(|b| b.question == q.id)
109 .ok_or_else(|| anyhow::anyhow!("this is no longer the run's approval question"))?;
110 let pr = state
111 .pr
112 .as_ref()
113 .ok_or_else(|| anyhow::anyhow!("approval has no recorded PR"))?;
114 let target = q.merge_target.clone().unwrap_or_else(|| Target {
115 pr: pr.url.clone(),
116 head: binding.head.clone(),
117 language: state.config.graph.language.clone(),
118 });
119 ensure!(
120 target.pr == pr.url && target.head == binding.head,
121 "approval target does not match the recorded run"
122 );
123 ensure!(
124 state
125 .merge_decision
126 .as_ref()
127 .is_none_or(|d| d.question == q.id && d.choice == DISMISS),
128 "another owner decision already controls this run"
129 );
130 if state.merge_decision.is_none() {
131 state.merge_decision = Some(Decision {
132 question: q.id.clone(),
133 target,
134 choice: DISMISS.into(),
135 closed: false,
136 replacement_run: None,
137 replacement_pr: None,
138 });
139 if !state.status.done() {
140 state.status = RunStatus::Superseded;
141 }
142 state.parked = false;
143 state.event(
144 "land",
145 "Owner dismissed obsolete approval; execution retired, PR unchanged",
146 );
147 state.save_under(home)?;
148 }
149 for owner in owners {
150 let mut task = queue.get(&owner.id)?;
151 if task.runs.last() == Some(&q.run) && task.status != crate::queue::TaskStatus::Done {
153 task.succeed();
154 task.mark_action_applied(&q.id);
155 queue.put(&mut task)?;
156 }
157 }
158 Ok(())
159}
160
161pub(crate) fn resumable_hold(state: &crate::run::RunState) -> bool {
164 state.status == crate::run::RunStatus::Blocked
165 && state.merge_decision.as_ref().is_some_and(|decision| {
166 !decision.closed && decision.choice.eq_ignore_ascii_case(crate::land::HOLD)
167 })
168}
169
170pub fn exact_owner_choice(choice: &str, message: &str, quote: &str) -> bool {
173 if choice.eq_ignore_ascii_case(crate::land::HOLD) {
174 message.trim().eq_ignore_ascii_case(choice) && quote.trim().eq_ignore_ascii_case(choice)
175 } else {
176 (stops(choice) || choice == crate::land::REPAIR)
177 && message.trim() == choice
178 && quote.trim() == choice
179 }
180}