implement youtrack client
This commit is contained in:
@@ -1,2 +1,3 @@
|
||||
/target
|
||||
.idea/
|
||||
.env
|
||||
|
||||
Generated
+136
-32
@@ -218,13 +218,17 @@ dependencies = [
|
||||
"chrono",
|
||||
"clap",
|
||||
"colored",
|
||||
"dotenv",
|
||||
"faccess",
|
||||
"indexmap",
|
||||
"inquire",
|
||||
"json_dotpath",
|
||||
"log",
|
||||
"regex",
|
||||
"reqwest",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"simple-logging",
|
||||
"smart-default",
|
||||
"toml",
|
||||
]
|
||||
@@ -244,6 +248,15 @@ dependencies = [
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "convert_case"
|
||||
version = "0.7.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bb402b8d4c85569410425650ce3eddc7d698ed96d39a73f941b08fb63082f1e7"
|
||||
dependencies = [
|
||||
"unicode-segmentation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "core-foundation"
|
||||
version = "0.9.4"
|
||||
@@ -262,15 +275,17 @@ checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
|
||||
|
||||
[[package]]
|
||||
name = "crossterm"
|
||||
version = "0.28.1"
|
||||
version = "0.29.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6"
|
||||
checksum = "d8b9f2e4c67f833b660cdb0a3523065869fb35570177239812ed4c905aeff87b"
|
||||
dependencies = [
|
||||
"bitflags 2.9.4",
|
||||
"crossterm_winapi",
|
||||
"derive_more",
|
||||
"document-features",
|
||||
"mio",
|
||||
"parking_lot",
|
||||
"rustix 0.38.44",
|
||||
"rustix",
|
||||
"signal-hook",
|
||||
"signal-hook-mio",
|
||||
"winapi",
|
||||
@@ -285,6 +300,27 @@ dependencies = [
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "derive_more"
|
||||
version = "2.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "093242cf7570c207c83073cf82f79706fe7b8317e98620a47d5be7c3d8497678"
|
||||
dependencies = [
|
||||
"derive_more-impl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "derive_more-impl"
|
||||
version = "2.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3"
|
||||
dependencies = [
|
||||
"convert_case",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "displaydoc"
|
||||
version = "0.2.5"
|
||||
@@ -296,6 +332,21 @@ dependencies = [
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "document-features"
|
||||
version = "0.2.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "95249b50c6c185bee49034bcb378a49dc2b5dff0be90ff6616d31d64febab05d"
|
||||
dependencies = [
|
||||
"litrs",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dotenv"
|
||||
version = "0.15.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "77c90badedccf4105eca100756a0b1289e191f6fcbdadd3cee1d2f614f97da8f"
|
||||
|
||||
[[package]]
|
||||
name = "dyn-clone"
|
||||
version = "1.0.20"
|
||||
@@ -762,9 +813,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "inquire"
|
||||
version = "0.8.0"
|
||||
version = "0.9.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "95b8b5b4fd6d0ef1235f11c2e8ce9734be5736c21230ff585c3bae2e940abced"
|
||||
checksum = "2628910d0114e9139056161d8644a2026be7b117f8498943f9437748b04c9e0a"
|
||||
dependencies = [
|
||||
"bitflags 2.9.4",
|
||||
"crossterm",
|
||||
@@ -824,18 +875,30 @@ dependencies = [
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "json_dotpath"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dbdcfef3cf5591f0cef62da413ae795e3d1f5a00936ccec0b2071499a32efd1a"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
"thiserror 1.0.69",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lazy_static"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.175"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543"
|
||||
|
||||
[[package]]
|
||||
name = "linux-raw-sys"
|
||||
version = "0.4.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab"
|
||||
|
||||
[[package]]
|
||||
name = "linux-raw-sys"
|
||||
version = "0.9.4"
|
||||
@@ -848,6 +911,12 @@ version = "0.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956"
|
||||
|
||||
[[package]]
|
||||
name = "litrs"
|
||||
version = "0.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f5e54036fe321fd421e10d732f155734c4e4afd610dd556d9a82833ab3ee0bed"
|
||||
|
||||
[[package]]
|
||||
name = "lock_api"
|
||||
version = "0.4.13"
|
||||
@@ -1012,7 +1081,7 @@ checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"redox_syscall",
|
||||
"redox_syscall 0.5.17",
|
||||
"smallvec",
|
||||
"windows-targets 0.52.6",
|
||||
]
|
||||
@@ -1082,7 +1151,7 @@ dependencies = [
|
||||
"rustc-hash",
|
||||
"rustls",
|
||||
"socket2",
|
||||
"thiserror",
|
||||
"thiserror 2.0.16",
|
||||
"tokio",
|
||||
"tracing",
|
||||
"web-time",
|
||||
@@ -1103,7 +1172,7 @@ dependencies = [
|
||||
"rustls",
|
||||
"rustls-pki-types",
|
||||
"slab",
|
||||
"thiserror",
|
||||
"thiserror 2.0.16",
|
||||
"tinyvec",
|
||||
"tracing",
|
||||
"web-time",
|
||||
@@ -1167,6 +1236,12 @@ dependencies = [
|
||||
"getrandom 0.3.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "redox_syscall"
|
||||
version = "0.1.57"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "41cc0f7e4d5d4544e8861606a285bb08d3e70712ccc7d2b84d7c0ccfaf4b05ce"
|
||||
|
||||
[[package]]
|
||||
name = "redox_syscall"
|
||||
version = "0.5.17"
|
||||
@@ -1277,19 +1352,6 @@ version = "2.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d"
|
||||
|
||||
[[package]]
|
||||
name = "rustix"
|
||||
version = "0.38.44"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154"
|
||||
dependencies = [
|
||||
"bitflags 2.9.4",
|
||||
"errno",
|
||||
"libc",
|
||||
"linux-raw-sys 0.4.15",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustix"
|
||||
version = "1.0.8"
|
||||
@@ -1299,7 +1361,7 @@ dependencies = [
|
||||
"bitflags 2.9.4",
|
||||
"errno",
|
||||
"libc",
|
||||
"linux-raw-sys 0.9.4",
|
||||
"linux-raw-sys",
|
||||
"windows-sys 0.60.2",
|
||||
]
|
||||
|
||||
@@ -1477,6 +1539,17 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "simple-logging"
|
||||
version = "2.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b00d48e85675326bb182a2286ea7c1a0b264333ae10f27a937a72be08628b542"
|
||||
dependencies = [
|
||||
"lazy_static",
|
||||
"log",
|
||||
"thread-id",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "slab"
|
||||
version = "0.4.11"
|
||||
@@ -1589,17 +1662,37 @@ dependencies = [
|
||||
"fastrand",
|
||||
"getrandom 0.3.3",
|
||||
"once_cell",
|
||||
"rustix 1.0.8",
|
||||
"rustix",
|
||||
"windows-sys 0.60.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "1.0.69"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
|
||||
dependencies = [
|
||||
"thiserror-impl 1.0.69",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "2.0.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3467d614147380f2e4e374161426ff399c91084acd2363eaf549172b3d5e60c0"
|
||||
dependencies = [
|
||||
"thiserror-impl",
|
||||
"thiserror-impl 2.0.16",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror-impl"
|
||||
version = "1.0.69"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1613,6 +1706,17 @@ dependencies = [
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thread-id"
|
||||
version = "3.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c7fbf4c9d56b320106cd64fd024dadfa0be7cb4706725fc44a7d7ce952d820c1"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"redox_syscall 0.1.57",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thread_local"
|
||||
version = "1.1.9"
|
||||
@@ -1820,9 +1924,9 @@ checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-width"
|
||||
version = "0.1.14"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af"
|
||||
checksum = "4a1a07cc7db3810833284e8d372ccdc6da29741639ecc70c9ec107df0fa6154c"
|
||||
|
||||
[[package]]
|
||||
name = "untrusted"
|
||||
|
||||
+8
-4
@@ -6,6 +6,7 @@ authors = ["Ondřej Hruška <ondra@ondrovo.com>"]
|
||||
description = "Manage changelog across multiple release channels"
|
||||
|
||||
[dependencies]
|
||||
log = "0.4"
|
||||
clap = { version = "4.5", features = ["string"] }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
@@ -17,9 +18,12 @@ colored = "3"
|
||||
faccess = "0.2"
|
||||
chrono = "0.4"
|
||||
indexmap = { version = "2.11", features = ["serde"] }
|
||||
inquire = { version = "0.8.0", features = ["editor"] }
|
||||
inquire = { version = "0.9", features = ["editor"] }
|
||||
|
||||
reqwest = { version = "0.12", features = ["rustls-tls", "blocking"], optional = true }
|
||||
# For integrations
|
||||
reqwest = { version = "0.12", features = ["rustls-tls", "blocking"] } #, optional = true
|
||||
json_dotpath = "1.1.0"
|
||||
dotenv = "0.15.0"
|
||||
|
||||
[features]
|
||||
youtrack = ["reqwest"]
|
||||
[dev-dependencies]
|
||||
simple-logging = "2"
|
||||
|
||||
@@ -13,6 +13,10 @@ pub type EntryName = String;
|
||||
|
||||
pub const CONFIG_FILE_TEMPLATE: &str = include_str!("assets/config_file_template.toml");
|
||||
|
||||
pub const ENV_YOUTRACK_URL : &str = "CLPACK_YOUTRACK_URL";
|
||||
|
||||
pub const ENV_YOUTRACK_TOKEN : &str = "CLPACK_YOUTRACK_TOKEN";
|
||||
|
||||
#[cfg(test)]
|
||||
#[test]
|
||||
fn test_template_file() {
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
mod youtrack;
|
||||
|
||||
pub use youtrack::YouTrackClient;
|
||||
@@ -0,0 +1,370 @@
|
||||
//! Youtrack integration (mark issues as Released when packing to changelog)
|
||||
|
||||
use crate::config::VersionName;
|
||||
use anyhow::bail;
|
||||
use chrono::{DateTime, Utc};
|
||||
use json_dotpath::DotPaths;
|
||||
use log::debug;
|
||||
use reqwest::header::{HeaderMap, HeaderValue};
|
||||
use serde::de::DeserializeOwned;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
use std::borrow::Cow;
|
||||
|
||||
type ProjectId = String;
|
||||
type BundleID = String;
|
||||
type FieldID = String;
|
||||
|
||||
pub struct YouTrackClient {
|
||||
client: reqwest::blocking::Client,
|
||||
pub url: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct YoutrackErrorResponse {
|
||||
error: String,
|
||||
error_description: String,
|
||||
}
|
||||
|
||||
impl YouTrackClient {
|
||||
pub fn new(url: impl ToString, token: &str) -> anyhow::Result<Self> {
|
||||
let token_bearer = format!("Bearer {token}");
|
||||
|
||||
let mut hm = HeaderMap::new();
|
||||
hm.insert("Authorization", HeaderValue::from_str(&token_bearer)?);
|
||||
hm.insert("Content-Type", HeaderValue::from_str("application/json")?);
|
||||
hm.insert("Accept", HeaderValue::from_str("application/json")?);
|
||||
|
||||
Ok(YouTrackClient {
|
||||
url: url.to_string(),
|
||||
client: reqwest::blocking::Client::builder()
|
||||
.default_headers(hm)
|
||||
.build()?,
|
||||
})
|
||||
}
|
||||
|
||||
fn get_json<T: Serialize + ?Sized, O: DeserializeOwned>(
|
||||
&self,
|
||||
api_path: String,
|
||||
query: &T,
|
||||
) -> anyhow::Result<O> {
|
||||
let url = format!(
|
||||
"{base}/api/{path}",
|
||||
base = self.url.trim_end_matches('/'),
|
||||
path = api_path.trim_start_matches('/')
|
||||
);
|
||||
|
||||
debug!("GET {}", url);
|
||||
|
||||
let response = self.client.get(&url).query(query).send()?;
|
||||
|
||||
let response_text = response.text()?;
|
||||
|
||||
debug!("Resp = {}", response_text);
|
||||
|
||||
if let Ok(e) = serde_json::from_str::<YoutrackErrorResponse>(&response_text) {
|
||||
bail!("Error from YouTrack: {} - {}", e.error, e.error_description);
|
||||
}
|
||||
|
||||
Ok(serde_json::from_str(&response_text)?)
|
||||
}
|
||||
|
||||
fn post_json<T: Serialize + ?Sized, B: Serialize + ?Sized, O: DeserializeOwned>(
|
||||
&self,
|
||||
api_path: String,
|
||||
body: &B,
|
||||
query: &T,
|
||||
) -> anyhow::Result<O> {
|
||||
let url = format!(
|
||||
"{base}/api/{path}",
|
||||
base = self.url.trim_end_matches('/'),
|
||||
path = api_path.trim_start_matches('/')
|
||||
);
|
||||
|
||||
debug!("POST {}", url);
|
||||
|
||||
let body_serialized = serde_json::to_string(body)?;
|
||||
let response = self
|
||||
.client
|
||||
.post(&url)
|
||||
.query(query)
|
||||
.body(body_serialized.into_bytes())
|
||||
.send()?;
|
||||
|
||||
let response_text = response.text()?;
|
||||
|
||||
debug!("Resp = {}", response_text);
|
||||
|
||||
if let Ok(e) = serde_json::from_str::<YoutrackErrorResponse>(&response_text) {
|
||||
bail!("Error from YouTrack: {} - {}", e.error, e.error_description);
|
||||
}
|
||||
|
||||
Ok(serde_json::from_str(&response_text)?)
|
||||
}
|
||||
|
||||
pub fn find_project_id(&self, issue: &str) -> anyhow::Result<ProjectId> {
|
||||
#[derive(Deserialize)]
|
||||
struct Issue {
|
||||
project: Project,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct Project {
|
||||
id: ProjectId,
|
||||
}
|
||||
|
||||
let issue: Issue =
|
||||
self.get_json(format!("issues/{issue}"), &[("fields", "project(id)")])?;
|
||||
|
||||
// example:
|
||||
// {"project":{"id":"0-172","$type":"Project"},"$type":"Issue"}
|
||||
|
||||
Ok(issue.project.id)
|
||||
}
|
||||
|
||||
/// Try to find a version by name in a YouTrack project.
|
||||
/// If it is not found but we find the field where to add it, the version will be created.
|
||||
///
|
||||
/// project_id - obtained by `find_project_id()`
|
||||
/// field_name - name of the version field, e.g. "Available in version"
|
||||
/// version_to_create - name of the version to find or create
|
||||
/// release_date - if creating, a date YYYY-MM-DD can be passed here. It will be stored into the
|
||||
/// newly created version & this marked as released.
|
||||
pub fn ensure_version_exists_in_project(
|
||||
&self,
|
||||
project_id: &str,
|
||||
field_name: &str,
|
||||
version_to_create: &str,
|
||||
release_date: Option<DateTime<Utc>>,
|
||||
) -> anyhow::Result<()> {
|
||||
#[derive(Deserialize)]
|
||||
struct BudleDescription {
|
||||
id: BundleID,
|
||||
}
|
||||
#[derive(Deserialize)]
|
||||
struct FieldDescription {
|
||||
name: String,
|
||||
id: FieldID,
|
||||
}
|
||||
#[derive(Deserialize)]
|
||||
struct YTCustomField {
|
||||
// Bundle is sometimes missing - we skip these entries
|
||||
bundle: Option<BudleDescription>,
|
||||
field: FieldDescription,
|
||||
}
|
||||
|
||||
// Find field description
|
||||
let fields: Vec<YTCustomField> = self.get_json(
|
||||
format!("admin/projects/{project_id}/customFields"),
|
||||
&[("fields", "field(name,id),bundle(id)"), ("top", "200")],
|
||||
)?;
|
||||
|
||||
let mut field_bundle = None;
|
||||
for entry in fields {
|
||||
if &entry.field.name == field_name
|
||||
&& let Some(bundle) = entry.bundle
|
||||
{
|
||||
field_bundle = Some((entry.field.id, bundle.id));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let Some((field_id, bundle_id)) = field_bundle else {
|
||||
bail!("YouTrack version field {field_name} not found in the project {project_id}");
|
||||
};
|
||||
|
||||
println!("Found YouTrack version field, checking defined versions");
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct YTVersion {
|
||||
name: VersionName,
|
||||
id: String,
|
||||
}
|
||||
|
||||
let versions: Vec<YTVersion> = self.get_json(
|
||||
format!("admin/customFieldSettings/bundles/version/{bundle_id}/values"),
|
||||
&[("fields", "id,name"), ("top", "500")],
|
||||
)?;
|
||||
|
||||
// Find the version we want
|
||||
for version in versions {
|
||||
if &version.name == version_to_create {
|
||||
eprintln!("Version {version_to_create} already exists in YouTrack");
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
println!("Creating version in YouTrack: {version_to_create}");
|
||||
|
||||
/*
|
||||
$body = ['name' => $name];
|
||||
if ($released !== null) {
|
||||
$body['released'] = $released;
|
||||
}
|
||||
if ($releaseDate !== null) {
|
||||
$body['releaseDate'] = $releaseDate;
|
||||
}
|
||||
if ($archived !== null) {
|
||||
$body['archived'] = $archived;
|
||||
}
|
||||
|
||||
return $this->postJson(
|
||||
"admin/customFieldSettings/bundles/version/$bundleId/values",
|
||||
$body,
|
||||
['fields' => 'id,name,released,releaseDate,archived'],
|
||||
);
|
||||
*/
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct CreateVersionBody {
|
||||
name: String,
|
||||
archived: bool,
|
||||
released: bool,
|
||||
#[allow(non_snake_case)]
|
||||
releaseDate: Option<i64>,
|
||||
// archived
|
||||
}
|
||||
|
||||
let body = CreateVersionBody {
|
||||
name: version_to_create.to_string(),
|
||||
archived: false,
|
||||
released: release_date.is_some(),
|
||||
releaseDate: release_date.map(|d| d.timestamp()),
|
||||
};
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
struct CreateVersionResponse {
|
||||
#[allow(non_snake_case)]
|
||||
releaseDate: Option<i64>,
|
||||
released: bool,
|
||||
archived: bool,
|
||||
name: String,
|
||||
id: String,
|
||||
}
|
||||
|
||||
let resp: CreateVersionResponse = self.post_json(
|
||||
format!("admin/customFieldSettings/bundles/version/{bundle_id}/values"),
|
||||
&body,
|
||||
&[("fields", "id,name,released,releaseDate,archived")],
|
||||
)?;
|
||||
|
||||
// Example response:
|
||||
// {"releaseDate":null,"released":false,"archived":false,"name":"TEST1","id":"232-356","$type":"VersionBundleElement"}
|
||||
// {"releaseDate":1758619201,"released":true,"archived":false,"name":"TEST2","id":"232-358","$type":"VersionBundleElement"}
|
||||
|
||||
debug!("Created version entry = {:#?}", resp);
|
||||
println!("Version {version_to_create} created in YouTrack.");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn set_issue_version_and_state(
|
||||
&self,
|
||||
issue_id: &str,
|
||||
version_field_name: &str,
|
||||
version_name: &str,
|
||||
target_state_name: &str,
|
||||
) -> anyhow::Result<()> {
|
||||
#[derive(Serialize)]
|
||||
struct PatchIssueBody {
|
||||
#[allow(non_snake_case)]
|
||||
customFields: Vec<CustomFieldValue>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct EnumValue {
|
||||
name: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct CustomFieldValue {
|
||||
name: String,
|
||||
#[serde(rename = "$type")]
|
||||
field_type: String,
|
||||
value: EnumValue,
|
||||
}
|
||||
|
||||
let body = PatchIssueBody {
|
||||
customFields: vec![
|
||||
CustomFieldValue {
|
||||
name: version_field_name.to_string(),
|
||||
field_type: "SingleVersionIssueCustomField".to_string(),
|
||||
value: EnumValue {
|
||||
name: version_name.to_string(),
|
||||
},
|
||||
},
|
||||
CustomFieldValue {
|
||||
name: "State".to_string(),
|
||||
field_type: "StateIssueCustomField".to_string(),
|
||||
value: EnumValue {
|
||||
name: target_state_name.to_string(),
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
let resp: Value = self.post_json(
|
||||
format!("issues/{issue_id}"),
|
||||
&body,
|
||||
&[("fields", "id,customFields(name,value(name))")],
|
||||
)?;
|
||||
|
||||
// TODO? Do something with the fields
|
||||
|
||||
// Example success:
|
||||
// {"customFields":[{"value":null,"name":"Type","$type":"SingleEnumIssueCustomField"},{"value":{"name":"Released","$type":"StateBundleElement"},"name":"State","$type":"StateIssueCustomField"},{"value":null,"name":"Assignee","$type":"SingleUserIssueCustomField"},{"value":null,"name":"Priority","$type":"SingleEnumIssueCustomField"},{"value":{"name":"Internal tooling","$type":"EnumBundleElement"},"name":"Category","$type":"SingleEnumIssueCustomField"},{"value":[],"name":"Customer","$type":"MultiEnumIssueCustomField"},{"value":null,"name":"Customer Funding","$type":"SingleEnumIssueCustomField"},{"value":null,"name":"Product Stream","$type":"SingleEnumIssueCustomField"},{"value":null,"name":"Estimation","$type":"PeriodIssueCustomField"},{"value":{"$type":"PeriodValue"},"name":"Spent time","$type":"PeriodIssueCustomField"},{"value":null,"name":"Due Date","$type":"DateIssueCustomField"},{"value":[],"name":"Affected version","$type":"MultiVersionIssueCustomField"},{"value":{"name":"TEST2","$type":"VersionBundleElement"},"name":"Available in version","$type":"SingleVersionIssueCustomField"},{"value":null,"name":"SlackAlertSent","$type":"SimpleIssueCustomField"},{"value":13.0,"name":"Dev costs","$type":"SimpleIssueCustomField"}],"id":"2-25820","$type":"Issue"}
|
||||
|
||||
println!("YouTrack issue {issue_id} updated.");
|
||||
|
||||
debug!("Response to request to edit issue: {resp:?}");
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::YouTrackClient;
|
||||
use crate::config::{ENV_YOUTRACK_TOKEN, ENV_YOUTRACK_URL};
|
||||
use log::{LevelFilter, debug};
|
||||
use serde_json::Value;
|
||||
|
||||
// #[test] // Disabled
|
||||
fn test_youtrack_communication() {
|
||||
simple_logging::log_to_stderr(LevelFilter::Debug);
|
||||
|
||||
let url = dotenv::var(ENV_YOUTRACK_URL).expect("Missing youtrack URL from env");
|
||||
let token = dotenv::var(ENV_YOUTRACK_TOKEN).expect("Missing youtrack token from env");
|
||||
|
||||
// this must match the config in the connected youtrack
|
||||
let issue_id = "SW-4739";
|
||||
let version_field_name = "Available in version";
|
||||
let target_state_name = "Released";
|
||||
let version_name = "TEST2";
|
||||
|
||||
let mut client = YouTrackClient::new(url, &token).unwrap();
|
||||
|
||||
let project_id = client.find_project_id(issue_id).unwrap();
|
||||
|
||||
debug!("Found YouTrack project ID = {project_id}");
|
||||
|
||||
let date = chrono::Utc::now();
|
||||
|
||||
client
|
||||
.ensure_version_exists_in_project(
|
||||
&project_id,
|
||||
version_field_name,
|
||||
version_name,
|
||||
Some(date),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
client
|
||||
.set_issue_version_and_state(
|
||||
issue_id,
|
||||
version_field_name,
|
||||
version_name,
|
||||
target_state_name,
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
+1
-2
@@ -24,8 +24,7 @@ mod store;
|
||||
|
||||
mod utils;
|
||||
|
||||
#[cfg(feature = "youtrack")]
|
||||
mod youtrack;
|
||||
mod integrations;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct AppContext {
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
//! Youtrack integration (mark issues as Released when packing to changelog)
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[test]
|
||||
fn test_youtrack_communication() {
|
||||
let token = "";
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user