fix: require observed merge target completion

This commit is contained in:
2026-08-18 08:05:57 +09:00
parent 796fa2ee85
commit 9a548d2b5e
4 changed files with 148 additions and 116 deletions
+29 -1
View File
@@ -68,6 +68,15 @@ pub struct MergeRequestAuth {
pub assignment_id: String,
}
impl MergeRequestAuth {
fn actor(&self) -> WorkerIdentity {
WorkerIdentity {
runtime_id: self.runtime_id.clone(),
worker_id: self.worker_id.clone(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ReviewRequestedEvent {
pub event_id: String,
@@ -641,7 +650,7 @@ impl MergeRequestStore {
}
pub fn validate_completion(&self, i: &CompleteMergeRequest) -> Result<(), MergeRequestError> {
let mr = self.get(&i.auth.workspace_id, &i.ticket_id)?;
self.completion_auth(&i.auth, &i.ticket_id, &mr.repository_id)?;
self.repo(&i.auth, &mr.repository_id)?;
if let Some(existing) = mr.thread.iter().find_map(|event| match event {
MergeRequestThreadEvent::Merge(value) if value.operation_id == i.operation_id => {
Some(value)
@@ -649,8 +658,12 @@ impl MergeRequestStore {
_ => None,
}) {
if existing.approval_event_id == i.approval_event_id
&& existing.approved_source_ref == i.current_subject_ref
&& existing.target_ref_before == i.target_ref_before
&& existing.target_ref_after == i.target_ref_after
&& existing.strategy == i.strategy
&& existing.resolution == i.resolution
&& existing.merged_by == i.auth.actor()
{
return Ok(());
}
@@ -658,6 +671,7 @@ impl MergeRequestStore {
"operation fingerprint mismatch".into(),
));
}
self.completion_auth(&i.auth, &i.ticket_id, &mr.repository_id)?;
if mr.state != MergeRequestState::Open {
return Err(MergeRequestError::Conflict(
"Merge Request is not open".into(),
@@ -711,8 +725,12 @@ impl MergeRequestStore {
_ => None,
}) {
if existing.approval_event_id == i.approval_event_id
&& existing.approved_source_ref == i.current_subject_ref
&& existing.target_ref_before == i.target_ref_before
&& existing.target_ref_after == i.target_ref_after
&& existing.strategy == i.strategy
&& existing.resolution == i.resolution
&& existing.merged_by == i.auth.actor()
{
return Ok(existing.clone());
}
@@ -767,6 +785,16 @@ impl MergeRequestStore {
WHERE workspace_id=?1 AND ticket_id=?2 AND workflow_state='inprogress'",
params![mr.workspace_id, i.ticket_id, i.now.to_rfc3339()],
)?;
let released_assignment = transaction.execute(
"DELETE FROM ticket_current_worker_assignments
WHERE workspace_id=?1 AND ticket_id=?2 AND assignment_id=?3",
params![mr.workspace_id, i.ticket_id, i.auth.assignment_id],
)?;
if released_assignment != 1 {
return Err(MergeRequestError::Unauthorized(
"completion assignment changed while closing Ticket".into(),
));
}
let issued_grants = {
let mut statement = transaction.prepare(
"SELECT request_event_id,subject_ref,capability_token
+28 -1
View File
@@ -93,7 +93,7 @@ fn approve(s: &MergeRequestStore, subject: &str, token: &str) -> ReviewEvent {
}
#[test]
fn selectors_thread_and_completion_have_no_revision_or_commit_api() {
let (_d, s) = fixture();
let (d, s) = fixture();
open(&s);
let review = approve(&s, "opaque-source-ref", "token");
let ready = s
@@ -122,6 +122,33 @@ fn selectors_thread_and_completion_have_no_revision_or_commit_api() {
let mr = s.get("W", "T").unwrap();
assert_eq!(mr.selector_from.as_deref(), Some("work/t"));
assert_eq!(mr.state, MergeRequestState::Merged);
let current_assignment: bool = Connection::open(d.path().join("db"))
.unwrap()
.query_row(
"SELECT EXISTS(
SELECT 1 FROM ticket_current_worker_assignments
WHERE workspace_id='W' AND ticket_id='T'
)",
[],
|row| row.get(0),
)
.unwrap();
assert!(!current_assignment);
let replayed = s
.complete(CompleteMergeRequest {
ticket_id: "T".into(),
operation_id: "op".into(),
approval_event_id: merged.approval_event_id.clone(),
current_subject_ref: merged.approved_source_ref.clone(),
target_ref_before: merged.target_ref_before.clone(),
target_ref_after: merged.target_ref_after.clone(),
strategy: merged.strategy,
resolution: merged.resolution,
auth: auth(),
now: at(6),
})
.unwrap();
assert_eq!(replayed, merged);
let json = serde_json::to_string(&mr).unwrap();
for banned in [
"revision_id",