use std::{
future::IntoFuture,
sync::{Arc, Mutex},
};
use anyhow::Result;
use colored::Colorize;
use futures::StreamExt;
use graphql_ws_client::{Subscription, graphql::StreamingOperation};
use serde_json::Value;
use tokio::task::JoinHandle;
use crate::{
gql::subscriptions, subscription::connect_graphql, util::logs::strip_terminal_controls,
};
#[derive(Default)]
pub(super) struct DeploymentErrors(Mutex<Vec<(String, String)>>);
impl DeploymentErrors {
fn message(&self) -> Option<String> {
let errors = self.0.lock().unwrap();
let (_, error) = errors.last()?;
let error = strip_terminal_controls(error);
let message = strip_build_log_hint(&error);
(!message.is_empty()).then(|| message.to_owned())
}
pub(super) fn with_error(&self, mut result: Value) -> Value {
if let Some(error) = self.message() {
result["error"] = Value::String(error);
}
result
}
pub(super) fn print_failure(&self) {
let message = match self.message() {
Some(error) => format!("Deploy failed: {error}"),
None => "Deploy failed".to_owned(),
};
println!("{}", message.red().bold());
}
fn record(&self, event: subscriptions::deployment_events::DeploymentEventsDeploymentEvents) {
let mut errors = self.0.lock().unwrap();
errors.retain(|(id, _)| id != &event.id);
if let Some(payload) = event.payload
&& payload.skipped != Some(true)
&& let Some(error) = payload.error.filter(|error| !error.trim().is_empty())
{
errors.push((event.id, error));
}
}
}
fn strip_build_log_hint(message: &str) -> &str {
let mut letters = message
.char_indices()
.rev()
.filter(|(_, c)| c.is_alphanumeric());
let mut start = message.len();
for expected in "pleasecheckthebuildlogsformoredetails".chars().rev() {
match letters.next() {
Some((offset, actual)) if actual.eq_ignore_ascii_case(&expected) => start = offset,
_ => return message.trim(),
}
}
if message[..start].ends_with(char::is_alphanumeric) {
return message.trim();
}
message[..start].trim()
}
pub(super) struct DeploymentStream {
pub(super) status: Subscription<StreamingOperation<subscriptions::Deployment>>,
_tasks: SubscriptionTasks,
}
struct SubscriptionTasks {
connection: JoinHandle<()>,
events: Option<JoinHandle<()>>,
}
impl Drop for SubscriptionTasks {
fn drop(&mut self) {
if let Some(events) = &self.events {
events.abort();
}
self.connection.abort();
}
}
impl DeploymentStream {
pub(super) async fn connect(id: &str, errors: Arc<DeploymentErrors>) -> Result<Self> {
let (client, actor) = connect_graphql().await?;
let mut tasks = SubscriptionTasks {
connection: tokio::spawn(actor.into_future()),
events: None,
};
let mut events = client
.subscribe(StreamingOperation::<subscriptions::DeploymentEvents>::new(
subscriptions::deployment_events::Variables { id: id.to_owned() },
))
.await?;
tasks.events = Some(tokio::spawn(async move {
while let Some(Ok(response)) = events.next().await {
if let Some(data) = response.data {
errors.record(data.deployment_events);
}
}
}));
let status = client
.subscribe(StreamingOperation::<subscriptions::Deployment>::new(
subscriptions::deployment::Variables { id: id.to_owned() },
))
.await?;
Ok(Self {
status,
_tasks: tasks,
})
}
}