use std::future::Future;
use onetaskgraph_plugin_api::SecretResolver;
use secrecy::SecretString;
use serde_json::{Value, json};
pub struct LiveSecret(pub SecretString);
impl SecretResolver for LiveSecret {
fn get(&self, variable: &str) -> Option<SecretString> {
(variable == "GH_PROJECTS_TOKEN").then(|| self.0.clone())
}
}
pub fn live_write_config(
owner: &str,
project_number: u32,
repository: &str,
status_option: &str,
) -> Value {
json!({"owner":owner,"project_number":project_number,"repository":repository,
"status_mapping":{"todo":status_option,"backlog":null,"in-progress":null}})
}
pub const ARTIFACT_PREFIX: &str = "onetaskgraph live cleanup ";
pub fn artifact_title(process_id: u32, stamp_micros: i64) -> String {
format!("{ARTIFACT_PREFIX}{process_id}-{stamp_micros}")
}
pub fn is_artifact_title(title: &str) -> bool {
let Some(suffix) = title.strip_prefix(ARTIFACT_PREFIX) else {
return false;
};
let Some((process_id, stamp_micros)) = suffix.split_once('-') else {
return false;
};
[process_id, stamp_micros]
.iter()
.all(|part| !part.is_empty() && part.bytes().all(|byte| byte.is_ascii_digit()))
}
pub fn is_run_artifact_title(process_id: u32, title: &str) -> bool {
is_artifact_title(title) && title.starts_with(&format!("{ARTIFACT_PREFIX}{process_id}-"))
}
pub const LABEL_PREFIX: &str = "onetaskgraph-live-";
pub fn artifact_label(process_id: u32, stamp_micros: i64) -> String {
format!("{LABEL_PREFIX}{process_id}-{stamp_micros}")
}
pub fn is_artifact_label(name: &str) -> bool {
let Some(suffix) = name.strip_prefix(LABEL_PREFIX) else {
return false;
};
let Some((process_id, stamp_micros)) = suffix.split_once('-') else {
return false;
};
[process_id, stamp_micros]
.iter()
.all(|part| !part.is_empty() && part.bytes().all(|byte| byte.is_ascii_digit()))
}
pub async fn run_then_cleanup<J, JF, C, CF>(journey: J, cleanup: C) -> Result<(), String>
where
J: FnOnce() -> JF,
JF: Future<Output = Result<(), String>>,
C: FnOnce() -> CF,
CF: Future<Output = Result<(), String>>,
{
let journey_result = journey().await;
let cleanup_result = cleanup().await;
match (journey_result, cleanup_result) {
(Ok(()), Ok(())) => Ok(()),
(Err(journey), Ok(())) => Err(journey),
(Ok(()), Err(cleanup)) => Err(format!("live cleanup failed: {cleanup}")),
(Err(journey), Err(cleanup)) => Err(format!(
"{journey}; additionally, live cleanup failed: {cleanup}"
)),
}
}
#[derive(Debug, PartialEq, Eq)]
pub enum LiveLane {
Run {
token: String,
owner: String,
project_number: u32,
repository: String,
},
Skip(String),
}
pub fn live_lane(
token: Option<&str>,
owner: Option<&str>,
project_number: Option<&str>,
repository: Option<&str>,
required: Option<&str>,
) -> Result<LiveLane, String> {
let required = match required.map(str::trim) {
None | Some("") | Some("0") => false,
Some("1") => true,
Some(other) => {
return Err(format!(
"ONETASKGRAPH_LIVE_REQUIRED must be 1, 0 or unset, not {other:?}"
));
}
};
let skip = |reason: String| -> Result<LiveLane, String> {
if required {
return Err(format!(
"{reason}, and ONETASKGRAPH_LIVE_REQUIRED=1 requires the GitHub Projects live \
lane to run"
));
}
Ok(LiveLane::Skip(reason))
};
let Some(token) = token else {
return skip("GH_PROJECTS_TOKEN is not set".to_owned());
};
if token.trim().is_empty() {
return skip("GH_PROJECTS_TOKEN is empty".to_owned());
}
let owner = owner.map(str::trim).filter(|owner| !owner.is_empty());
let project_number = project_number
.map(str::trim)
.filter(|number| !number.is_empty());
let (owner, project_number) = match (owner, project_number) {
(Some(owner), Some(project_number)) => (owner, project_number),
(None, None) => {
return skip(
"GH_PROJECTS_OWNER and GH_PROJECTS_NUMBER are not set, and this lane writes only \
to the board those two name rather than discovering one"
.to_owned(),
);
}
(owner, _) => {
return Err(format!(
"GH_PROJECTS_OWNER and GH_PROJECTS_NUMBER name one board together: {} is missing",
if owner.is_some() {
"GH_PROJECTS_NUMBER"
} else {
"GH_PROJECTS_OWNER"
}
));
}
};
let number = project_number
.parse::<u32>()
.ok()
.filter(|number| *number > 0 && *number <= i32::MAX as u32)
.ok_or_else(|| {
format!("GH_PROJECTS_NUMBER must be a positive GraphQL Int, not {project_number:?}")
})?;
let Some(repository) = repository
.map(str::trim)
.filter(|repository| !repository.is_empty())
else {
return skip(
"GH_PROJECTS_REPOSITORY is not set, and this lane creates its artifact as an issue \
in the repository that name gives rather than discovering one"
.to_owned(),
);
};
if repository
.split_once('/')
.is_none_or(|(owner, name)| owner.is_empty() || name.is_empty() || name.contains('/'))
{
return Err(format!(
"GH_PROJECTS_REPOSITORY must be spelled owner/name, not {repository:?}"
));
}
Ok(LiveLane::Run {
token: token.to_owned(),
owner: owner.to_owned(),
project_number: number,
repository: repository.to_owned(),
})
}