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_under(
99 home,
100 crate::daemon::is_working_on(home, &state.id, jiff::Timestamp::now())
101 ) == Liveness::Dead,
102 "stop the current driver before dismissing this obsolete approval"
103 );
104 ensure!(
105 state.land_armed_head.is_none(),
106 "disable existing PR auto-merge before dismissing this approval"
107 );
108 let binding = state
109 .land_approval
110 .as_ref()
111 .filter(|b| b.question == q.id)
112 .ok_or_else(|| anyhow::anyhow!("this is no longer the run's approval question"))?;
113 let pr = state
114 .pr
115 .as_ref()
116 .ok_or_else(|| anyhow::anyhow!("approval has no recorded PR"))?;
117 let target = q.merge_target.clone().unwrap_or_else(|| Target {
118 pr: pr.url.clone(),
119 head: binding.head.clone(),
120 language: state.config.graph.language.clone(),
121 });
122 ensure!(
123 target.pr == pr.url && target.head == binding.head,
124 "approval target does not match the recorded run"
125 );
126 ensure!(
127 state
128 .merge_decision
129 .as_ref()
130 .is_none_or(|d| d.question == q.id && d.choice == DISMISS),
131 "another owner decision already controls this run"
132 );
133 if state.merge_decision.is_none() {
134 state.merge_decision = Some(Decision {
135 question: q.id.clone(),
136 target,
137 choice: DISMISS.into(),
138 closed: false,
139 replacement_run: None,
140 replacement_pr: None,
141 });
142 if !state.status.done() {
143 state.status = RunStatus::Superseded;
144 }
145 state.parked = false;
146 state.event(
147 "land",
148 "Owner dismissed obsolete approval; execution retired, PR unchanged",
149 );
150 state.save_under(home)?;
151 }
152 for owner in owners {
153 let mut task = queue.get(&owner.id)?;
154 if task.runs.last() == Some(&q.run) && task.status != crate::queue::TaskStatus::Done {
156 task.succeed();
157 task.mark_action_applied(&q.id);
158 queue.put(&mut task)?;
159 }
160 }
161 Ok(())
162}
163
164pub(crate) fn resumable_hold(state: &crate::run::RunState) -> bool {
167 state.status == crate::run::RunStatus::Blocked
168 && state.merge_decision.as_ref().is_some_and(|decision| {
169 !decision.closed && decision.choice.eq_ignore_ascii_case(crate::land::HOLD)
170 })
171}
172
173pub fn exact_owner_choice(choice: &str, message: &str, quote: &str) -> bool {
176 if choice.eq_ignore_ascii_case(crate::land::HOLD) {
177 message.trim().eq_ignore_ascii_case(choice) && quote.trim().eq_ignore_ascii_case(choice)
178 } else {
179 (stops(choice) || choice == crate::land::REPAIR)
180 && message.trim() == choice
181 && quote.trim() == choice
182 }
183}