summaryrefslogtreecommitdiff
path: root/ofborg/tickborg/src/tasks
diff options
context:
space:
mode:
authorMehmet Samet Duman <yongdohyun@projecttick.org>2026-04-04 23:00:30 +0300
committerMehmet Samet Duman <yongdohyun@projecttick.org>2026-04-04 23:00:30 +0300
commit71ffb442e5f8072c6e0a974df9ae085bcf0e5d2a (patch)
treed336b1d64747aeebb1a80c2e7c4e9b5d24253751 /ofborg/tickborg/src/tasks
parentf96ea38d595162813a460f80f84e20f8d7f241bc (diff)
downloadProject-Tick-71ffb442e5f8072c6e0a974df9ae085bcf0e5d2a.tar.gz
Project-Tick-71ffb442e5f8072c6e0a974df9ae085bcf0e5d2a.zip
NOISSUE update bootstrap script paths in documentation for Linux and Windows
remove unnecessary badges from README add example environment configuration for Ofborg create production configuration for Ofborg correct RabbitMQ host in example configuration add push event handling in GitHub webhook receiver implement push filter task for handling push events extend build job structure to include push event information enhance build result structure to accommodate push event data add push event data handling in various message processing tasks update log message collector to prevent 404 errors on log links add push filter task to task module Signed-off-by: Mehmet Samet Duman <yongdohyun@projecttick.org>
Diffstat (limited to 'ofborg/tickborg/src/tasks')
-rw-r--r--ofborg/tickborg/src/tasks/build.rs73
-rw-r--r--ofborg/tickborg/src/tasks/githubcommentposter.rs95
-rw-r--r--ofborg/tickborg/src/tasks/log_message_collector.rs3
-rw-r--r--ofborg/tickborg/src/tasks/mod.rs1
-rw-r--r--ofborg/tickborg/src/tasks/pushfilter.rs165
5 files changed, 302 insertions, 35 deletions
diff --git a/ofborg/tickborg/src/tasks/build.rs b/ofborg/tickborg/src/tasks/build.rs
index 56583b28b4..2bac6d749b 100644
--- a/ofborg/tickborg/src/tasks/build.rs
+++ b/ofborg/tickborg/src/tasks/build.rs
@@ -121,6 +121,7 @@ impl JobActions {
attempted_attrs: None,
skipped_attrs: None,
status: BuildStatus::Failure,
+ push: self.job.push.clone(),
};
let result_exchange = self.result_exchange.clone();
@@ -209,6 +210,7 @@ impl JobActions {
skipped_attrs: Some(not_attempted_attrs),
attempted_attrs: None,
status: BuildStatus::Skipped,
+ push: self.job.push.clone(),
};
let result_exchange = self.result_exchange.clone();
@@ -249,6 +251,7 @@ impl JobActions {
status,
attempted_attrs: Some(attempted_attrs),
skipped_attrs: Some(not_attempted_attrs),
+ push: self.job.push.clone(),
};
let result_exchange = self.result_exchange.clone();
@@ -301,7 +304,12 @@ impl notifyworker::SimpleNotifyWorker for BuildWorker {
dyn notifyworker::NotificationReceiver + std::marker::Send + std::marker::Sync,
>,
) {
- let span = debug_span!("job", pr = ?job.pr.number);
+ let is_push = job.is_push();
+ let span = if is_push {
+ debug_span!("job", push_branch = ?job.push.as_ref().map(|p| &p.branch), sha = %job.pr.head_sha)
+ } else {
+ debug_span!("job", pr = ?job.pr.number)
+ };
let _enter = span.enter();
let actions = self.actions(job, notifier);
@@ -312,10 +320,19 @@ impl notifyworker::SimpleNotifyWorker for BuildWorker {
return;
}
- info!(
- "Working on https://github.com/{}/pull/{}",
- actions.job.repo.full_name, actions.job.pr.number
- );
+ if is_push {
+ let push = actions.job.push.as_ref().unwrap();
+ info!(
+ "Working on push to {}:{} ({})",
+ actions.job.repo.full_name, push.branch, push.head_sha
+ );
+ } else {
+ info!(
+ "Working on https://github.com/{}/pull/{}",
+ actions.job.repo.full_name, actions.job.pr.number
+ );
+ }
+
let project = self.cloner.project(
&actions.job.repo.full_name,
actions.job.repo.clone_url.clone(),
@@ -331,22 +348,38 @@ impl notifyworker::SimpleNotifyWorker for BuildWorker {
let refpath = co.checkout_origin_ref(target_branch.as_ref()).unwrap();
- if co.fetch_pr(actions.job.pr.number).is_err() {
- info!("Failed to fetch {}", actions.job.pr.number);
- actions.pr_head_missing().await;
- return;
- }
+ if is_push {
+ // For push builds: the commit is already on the branch, just verify it exists
+ if !co.commit_exists(actions.job.pr.head_sha.as_ref()) {
+ info!("Push commit {} doesn't exist after fetch", actions.job.pr.head_sha);
+ actions.commit_missing().await;
+ return;
+ }
+ // Checkout the exact pushed commit
+ if co.checkout_ref(actions.job.pr.head_sha.as_ref()).is_err() {
+ info!("Failed to checkout push commit {}", actions.job.pr.head_sha);
+ actions.merge_failed().await;
+ return;
+ }
+ } else {
+ // For PR builds: fetch PR ref, verify commit, merge
+ if co.fetch_pr(actions.job.pr.number).is_err() {
+ info!("Failed to fetch {}", actions.job.pr.number);
+ actions.pr_head_missing().await;
+ return;
+ }
- if !co.commit_exists(actions.job.pr.head_sha.as_ref()) {
- info!("Commit {} doesn't exist", actions.job.pr.head_sha);
- actions.commit_missing().await;
- return;
- }
+ if !co.commit_exists(actions.job.pr.head_sha.as_ref()) {
+ info!("Commit {} doesn't exist", actions.job.pr.head_sha);
+ actions.commit_missing().await;
+ return;
+ }
- if co.merge_commit(actions.job.pr.head_sha.as_ref()).is_err() {
- info!("Failed to merge {}", actions.job.pr.head_sha);
- actions.merge_failed().await;
- return;
+ if co.merge_commit(actions.job.pr.head_sha.as_ref()).is_err() {
+ info!("Failed to merge {}", actions.job.pr.head_sha);
+ actions.merge_failed().await;
+ return;
+ }
}
// Determine which projects to build from the requested attrs
@@ -528,6 +561,7 @@ mod tests {
logs: Some((Some(String::from("logs")), Some(String::from("build.log")))),
statusreport: Some((Some(String::from("build-results")), None)),
request_id: "bogus-request-id".to_owned(),
+ push: None,
};
let dummyreceiver = Arc::new(notifyworker::DummyNotificationReceiver::new());
@@ -574,6 +608,7 @@ mod tests {
logs: Some((Some(String::from("logs")), Some(String::from("build.log")))),
statusreport: Some((Some(String::from("build-results")), None)),
request_id: "bogus-request-id".to_owned(),
+ push: None,
};
let dummyreceiver = Arc::new(notifyworker::DummyNotificationReceiver::new());
diff --git a/ofborg/tickborg/src/tasks/githubcommentposter.rs b/ofborg/tickborg/src/tasks/githubcommentposter.rs
index 70c4a118e4..2f49d7401b 100644
--- a/ofborg/tickborg/src/tasks/githubcommentposter.rs
+++ b/ofborg/tickborg/src/tasks/githubcommentposter.rs
@@ -71,7 +71,11 @@ impl worker::SimpleWorker for GitHubCommentPoster {
}
};
- let span = debug_span!("job", pr = ?pr.number);
+ let span = if pr.number == 0 {
+ debug_span!("job", push_sha = %pr.head_sha)
+ } else {
+ debug_span!("job", pr = ?pr.number)
+ };
let _enter = span.enter();
for check in checks {
@@ -111,17 +115,40 @@ fn job_to_check(job: &BuildJob, architecture: &str, timestamp: DateTime<Utc>) ->
all_attrs = vec![String::from("(unknown attributes)")];
}
+ let details_key = if job.is_push() {
+ format!(
+ "push.{}",
+ job.push
+ .as_ref()
+ .map(|p| p.branch.replace('/', "-"))
+ .unwrap_or_default()
+ )
+ } else {
+ format!("{}", job.pr.number)
+ };
+
+ let name = if job.is_push() {
+ let branch = job
+ .push
+ .as_ref()
+ .map(|p| p.branch.as_str())
+ .unwrap_or("unknown");
+ format!("{} on {architecture} (push to {branch})", all_attrs.join(", "))
+ } else {
+ format!("{} on {architecture}", all_attrs.join(", "))
+ };
+
CheckRunOptions {
- name: format!("{} on {architecture}", all_attrs.join(", ")),
+ name,
actions: None,
completed_at: None,
started_at: Some(timestamp.to_rfc3339_opts(chrono::SecondsFormat::Secs, true)),
conclusion: None,
details_url: Some(format!(
- "https://logs.tickborg.project-tick.net/?key={}/{}.{}",
+ "https://logs.tickborg.projecttick.net/?key={}/{}.{}",
&job.repo.owner.to_lowercase(),
&job.repo.name.to_lowercase(),
- job.pr.number,
+ details_key,
)),
external_id: None,
head_sha: job.pr.head_sha.clone(),
@@ -180,17 +207,47 @@ fn result_to_check(result: &LegacyBuildResult, timestamp: DateTime<Utc>) -> Chec
String::from("No partial log is available.")
};
+ let is_push = result.push.is_some();
+
+ let details_key = if is_push {
+ format!(
+ "push.{}",
+ result
+ .push
+ .as_ref()
+ .map(|p| p.branch.replace('/', "-"))
+ .unwrap_or_default()
+ )
+ } else {
+ format!("{}", result.pr.number)
+ };
+
+ let name = if is_push {
+ let branch = result
+ .push
+ .as_ref()
+ .map(|p| p.branch.as_str())
+ .unwrap_or("unknown");
+ format!(
+ "{} on {} (push to {branch})",
+ all_attrs.join(", "),
+ result.system
+ )
+ } else {
+ format!("{} on {}", all_attrs.join(", "), result.system)
+ };
+
CheckRunOptions {
- name: format!("{} on {}", all_attrs.join(", "), result.system),
+ name,
actions: None,
completed_at: Some(timestamp.to_rfc3339_opts(chrono::SecondsFormat::Secs, true)),
started_at: None,
conclusion: Some(conclusion),
details_url: Some(format!(
- "https://logs.tickborg.project-tick.net/?key={}/{}.{}&attempt_id={}",
+ "https://logs.tickborg.projecttick.net/?key={}/{}.{}&attempt_id={}",
&result.repo.owner.to_lowercase(),
&result.repo.name.to_lowercase(),
- result.pr.number,
+ details_key,
result.attempt_id,
)),
external_id: Some(result.attempt_id.clone()),
@@ -244,6 +301,7 @@ mod tests {
request_id: "bogus-request-id".to_owned(),
attrs: vec!["foo".to_owned(), "bar".to_owned()],
+ push: None,
};
let timestamp = Utc.with_ymd_and_hms(2023, 4, 20, 13, 37, 42).unwrap();
@@ -256,7 +314,7 @@ mod tests {
completed_at: None,
status: Some(CheckRunState::Queued),
conclusion: None,
- details_url: Some("https://logs.tickborg.project-tick.net/?key=project-tick/Project-Tick.2345".to_string()),
+ details_url: Some("https://logs.tickborg.projecttick.net/?key=project-tick/Project-Tick.2345".to_string()),
external_id: None,
head_sha: "abc123".to_string(),
output: None,
@@ -296,6 +354,7 @@ mod tests {
attempted_attrs: Some(vec!["foo".to_owned()]),
skipped_attrs: Some(vec!["bar".to_owned()]),
status: BuildStatus::Success,
+ push: None,
};
let timestamp = Utc.with_ymd_and_hms(2023, 4, 20, 13, 37, 42).unwrap();
@@ -310,7 +369,7 @@ mod tests {
status: Some(CheckRunState::Completed),
conclusion: Some(Conclusion::Success),
details_url: Some(
- "https://logs.tickborg.project-tick.net/?key=project-tick/Project-Tick.2345&attempt_id=neatattemptid"
+ "https://logs.tickborg.projecttick.net/?key=project-tick/Project-Tick.2345&attempt_id=neatattemptid"
.to_string()
),
external_id: Some("neatattemptid".to_string()),
@@ -378,6 +437,7 @@ patching script interpreter paths in /nix/store/pcja75y9isdvgz5i00pkrpif9rxzxc29
attempted_attrs: Some(vec!["foo".to_owned()]),
skipped_attrs: None,
status: BuildStatus::Failure,
+ push: None,
};
let timestamp = Utc.with_ymd_and_hms(2023, 4, 20, 13, 37, 42).unwrap();
@@ -392,7 +452,7 @@ patching script interpreter paths in /nix/store/pcja75y9isdvgz5i00pkrpif9rxzxc29
status: Some(CheckRunState::Completed),
conclusion: Some(Conclusion::Neutral),
details_url: Some(
- "https://logs.tickborg.project-tick.net/?key=project-tick/Project-Tick.2345&attempt_id=neatattemptid"
+ "https://logs.tickborg.projecttick.net/?key=project-tick/Project-Tick.2345&attempt_id=neatattemptid"
.to_string()
),
external_id: Some("neatattemptid".to_string()),
@@ -457,6 +517,7 @@ patching script interpreter paths in /nix/store/pcja75y9isdvgz5i00pkrpif9rxzxc29
attempted_attrs: Some(vec!["foo".to_owned()]),
skipped_attrs: None,
status: BuildStatus::TimedOut,
+ push: None,
};
let timestamp = Utc.with_ymd_and_hms(2023, 4, 20, 13, 37, 42).unwrap();
@@ -471,7 +532,7 @@ patching script interpreter paths in /nix/store/pcja75y9isdvgz5i00pkrpif9rxzxc29
status: Some(CheckRunState::Completed),
conclusion: Some(Conclusion::Neutral),
details_url: Some(
- "https://logs.tickborg.project-tick.net/?key=project-tick/Project-Tick.2345&attempt_id=neatattemptid"
+ "https://logs.tickborg.projecttick.net/?key=project-tick/Project-Tick.2345&attempt_id=neatattemptid"
.to_string()
),
external_id: Some("neatattemptid".to_string()),
@@ -537,6 +598,7 @@ error: build of '/nix/store/l1limh50lx2cx45yb2gqpv7k8xl1mik2-gdb-8.1.drv' failed
attempted_attrs: None,
skipped_attrs: None,
status: BuildStatus::Success,
+ push: None,
};
let timestamp = Utc.with_ymd_and_hms(2023, 4, 20, 13, 37, 42).unwrap();
@@ -551,7 +613,7 @@ error: build of '/nix/store/l1limh50lx2cx45yb2gqpv7k8xl1mik2-gdb-8.1.drv' failed
status: Some(CheckRunState::Completed),
conclusion: Some(Conclusion::Success),
details_url: Some(
- "https://logs.tickborg.project-tick.net/?key=project-tick/Project-Tick.2345&attempt_id=neatattemptid"
+ "https://logs.tickborg.projecttick.net/?key=project-tick/Project-Tick.2345&attempt_id=neatattemptid"
.to_string()
),
external_id: Some("neatattemptid".to_string()),
@@ -615,6 +677,7 @@ patching script interpreter paths in /nix/store/pcja75y9isdvgz5i00pkrpif9rxzxc29
attempted_attrs: None,
skipped_attrs: None,
status: BuildStatus::Failure,
+ push: None,
};
let timestamp = Utc.with_ymd_and_hms(2023, 4, 20, 13, 37, 42).unwrap();
@@ -629,7 +692,7 @@ patching script interpreter paths in /nix/store/pcja75y9isdvgz5i00pkrpif9rxzxc29
status: Some(CheckRunState::Completed),
conclusion: Some(Conclusion::Neutral),
details_url: Some(
- "https://logs.tickborg.project-tick.net/?key=project-tick/Project-Tick.2345&attempt_id=neatattemptid"
+ "https://logs.tickborg.projecttick.net/?key=project-tick/Project-Tick.2345&attempt_id=neatattemptid"
.to_string()
),
external_id: Some("neatattemptid".to_string()),
@@ -682,6 +745,7 @@ patching script interpreter paths in /nix/store/pcja75y9isdvgz5i00pkrpif9rxzxc29
attempted_attrs: None,
skipped_attrs: Some(vec!["not-attempted".to_owned()]),
status: BuildStatus::Skipped,
+ push: None,
};
let timestamp = Utc.with_ymd_and_hms(2023, 4, 20, 13, 37, 42).unwrap();
@@ -695,7 +759,7 @@ patching script interpreter paths in /nix/store/pcja75y9isdvgz5i00pkrpif9rxzxc29
completed_at: Some("2023-04-20T13:37:42Z".to_string()),
status: Some(CheckRunState::Completed),
conclusion: Some(Conclusion::Skipped),
- details_url: Some("https://logs.tickborg.project-tick.net/?key=project-tick/Project-Tick.2345&attempt_id=neatattemptid".to_string()),
+ details_url: Some("https://logs.tickborg.projecttick.net/?key=project-tick/Project-Tick.2345&attempt_id=neatattemptid".to_string()),
external_id: Some("neatattemptid".to_string()),
head_sha: "abc123".to_string(),
output: Some(Output {
@@ -735,6 +799,7 @@ foo
attempted_attrs: None,
skipped_attrs: Some(vec!["not-attempted".to_owned()]),
status: BuildStatus::Skipped,
+ push: None,
};
let timestamp = Utc.with_ymd_and_hms(2023, 4, 20, 13, 37, 42).unwrap();
@@ -748,7 +813,7 @@ foo
completed_at: Some("2023-04-20T13:37:42Z".to_string()),
status: Some(CheckRunState::Completed),
conclusion: Some(Conclusion::Skipped),
- details_url: Some("https://logs.tickborg.project-tick.net/?key=project-tick/Project-Tick.2345&attempt_id=neatattemptid".to_string()),
+ details_url: Some("https://logs.tickborg.projecttick.net/?key=project-tick/Project-Tick.2345&attempt_id=neatattemptid".to_string()),
external_id: Some("neatattemptid".to_string()),
head_sha: "abc123".to_string(),
output: Some(Output {
diff --git a/ofborg/tickborg/src/tasks/log_message_collector.rs b/ofborg/tickborg/src/tasks/log_message_collector.rs
index 2d80f72f03..302445e2ff 100644
--- a/ofborg/tickborg/src/tasks/log_message_collector.rs
+++ b/ofborg/tickborg/src/tasks/log_message_collector.rs
@@ -215,7 +215,7 @@ impl worker::SimpleWorker for LogMessageCollector {
// Make sure the log content exists by opening its handle.
// This (hopefully) prevents builds that produce no output (for any reason) from
- // having their logs.tickborg.project-tick.net link complaining about a 404.
+ // having their logs.tickborg.projecttick.net link complaining about a 404.
let _ = self.handle_for(&job.from).unwrap();
}
MsgType::Msg(ref message) => {
@@ -448,6 +448,7 @@ mod tests {
status: BuildStatus::Success,
attempted_attrs: Some(vec!["foo".to_owned()]),
skipped_attrs: Some(vec!["bar".to_owned()]),
+ push: None,
}))
})
.await
diff --git a/ofborg/tickborg/src/tasks/mod.rs b/ofborg/tickborg/src/tasks/mod.rs
index 5aab0fa631..3bf701870d 100644
--- a/ofborg/tickborg/src/tasks/mod.rs
+++ b/ofborg/tickborg/src/tasks/mod.rs
@@ -5,4 +5,5 @@ pub mod evaluationfilter;
pub mod githubcommentfilter;
pub mod githubcommentposter;
pub mod log_message_collector;
+pub mod pushfilter;
pub mod statscollector;
diff --git a/ofborg/tickborg/src/tasks/pushfilter.rs b/ofborg/tickborg/src/tasks/pushfilter.rs
new file mode 100644
index 0000000000..8cf8d7a0ef
--- /dev/null
+++ b/ofborg/tickborg/src/tasks/pushfilter.rs
@@ -0,0 +1,165 @@
+use crate::acl;
+use crate::ghevent;
+use crate::message::buildjob;
+use crate::message::{PushTrigger, Repo};
+use crate::systems;
+use crate::worker;
+
+use tracing::{debug_span, info};
+use uuid::Uuid;
+
+pub struct PushFilterWorker {
+ acl: acl::Acl,
+ /// Default projects/attrs to build when push doesn't match any known project.
+ default_attrs: Vec<String>,
+}
+
+impl PushFilterWorker {
+ pub fn new(acl: acl::Acl, default_attrs: Vec<String>) -> PushFilterWorker {
+ PushFilterWorker {
+ acl,
+ default_attrs,
+ }
+ }
+}
+
+impl worker::SimpleWorker for PushFilterWorker {
+ type J = ghevent::PushEvent;
+
+ async fn msg_to_job(
+ &mut self,
+ _: &str,
+ _: &Option<String>,
+ body: &[u8],
+ ) -> Result<Self::J, String> {
+ match serde_json::from_slice(body) {
+ Ok(event) => Ok(event),
+ Err(err) => Err(format!(
+ "Failed to deserialize push event {err:?}: {:?}",
+ std::str::from_utf8(body).unwrap_or("<job not utf8>")
+ )),
+ }
+ }
+
+ async fn consumer(&mut self, job: &ghevent::PushEvent) -> worker::Actions {
+ let branch = job.branch().unwrap_or_default();
+ let span = debug_span!("push", branch = %branch, after = %job.after);
+ let _enter = span.enter();
+
+ if !self.acl.is_repo_eligible(&job.repository.full_name) {
+ info!("Repo not authorized ({})", job.repository.full_name);
+ return vec![worker::Action::Ack];
+ }
+
+ // Skip tag events
+ if job.is_tag() {
+ info!("Skipping tag push: {}", job.git_ref);
+ return vec![worker::Action::Ack];
+ }
+
+ // Skip branch deletion events
+ if job.is_delete() {
+ info!("Skipping branch delete: {}", job.git_ref);
+ return vec![worker::Action::Ack];
+ }
+
+ // Skip zero SHA (shouldn't happen for non-delete, but just in case)
+ if job.is_zero_sha() {
+ info!("Skipping zero SHA push");
+ return vec![worker::Action::Ack];
+ }
+
+ let branch_name = branch.to_string();
+ info!(
+ "Processing push to {}:{} ({})",
+ job.repository.full_name, branch_name, job.after
+ );
+
+ // Detect which projects changed from the push event's commit info
+ let changed_files: Vec<String> = if let Some(ref head) = job.head_commit {
+ let mut files = Vec::new();
+ if let Some(ref added) = head.added {
+ files.extend(added.iter().cloned());
+ }
+ if let Some(ref removed) = head.removed {
+ files.extend(removed.iter().cloned());
+ }
+ if let Some(ref modified) = head.modified {
+ files.extend(modified.iter().cloned());
+ }
+ files
+ } else {
+ Vec::new()
+ };
+
+ let attrs = if !changed_files.is_empty() {
+ let detected = crate::buildtool::detect_changed_projects(&changed_files);
+ if detected.is_empty() {
+ info!("No known projects changed in push, using defaults");
+ self.default_attrs.clone()
+ } else {
+ info!("Detected changed projects: {:?}", detected);
+ detected
+ }
+ } else {
+ info!("No file change info in push event, using defaults");
+ self.default_attrs.clone()
+ };
+
+ if attrs.is_empty() {
+ info!("No projects to build, skipping push");
+ return vec![worker::Action::Ack];
+ }
+
+ let repo_msg = Repo {
+ clone_url: job.repository.clone_url.clone(),
+ full_name: job.repository.full_name.clone(),
+ owner: job.repository.owner.login.clone(),
+ name: job.repository.name.clone(),
+ };
+
+ let push_trigger = PushTrigger {
+ head_sha: job.after.clone(),
+ branch: branch_name,
+ before_sha: Some(job.before.clone()),
+ };
+
+ let request_id = Uuid::new_v4().to_string();
+
+ let build_job = buildjob::BuildJob::new_push(
+ repo_msg.clone(),
+ push_trigger,
+ attrs.clone(),
+ request_id,
+ );
+
+ // Schedule the build on all known architectures
+ let build_archs = systems::System::primary_systems();
+ let mut response = vec![];
+
+ info!(
+ "Scheduling push build for {:?} on {:?}",
+ attrs, build_archs
+ );
+
+ for arch in &build_archs {
+ let (exchange, routingkey) = arch.as_build_destination();
+ response.push(worker::publish_serde_action(
+ exchange, routingkey, &build_job,
+ ));
+ }
+
+ // Also publish to build-results for the comment poster to pick up
+ response.push(worker::publish_serde_action(
+ Some("build-results".to_string()),
+ None,
+ &buildjob::QueuedBuildJobs {
+ job: build_job,
+ architectures: build_archs.iter().map(|a| a.to_string()).collect(),
+ },
+ ));
+
+ response.push(worker::Action::Ack);
+ response
+ }
+}