clpack integration

master
Ondřej Hruška 3 months ago
parent 753cbebd48
commit 9810ecc886
  1. 2
      changelog/entries/SW-4712-clpack-youtrack-integration.md
  2. 0
      changelog/entries/SW-4716-add-cl-status.md
  3. 40
      clpack.toml
  4. 2
      src/action_log.rs
  5. 31
      src/action_pack.rs
  6. 39
      src/assets/config_file_template.toml
  7. 38
      src/config.rs
  8. 31
      src/git.rs
  9. 5
      src/integrations/mod.rs
  10. 303
      src/integrations/youtrack.rs

@ -0,0 +1,2 @@
# New features
- Add integration to JetBrains YouTrack (#SW-4712)

@ -76,3 +76,43 @@ branch_version_pattern = '/^rel\/([\d.]+)$/'
# To specify a regex pattern (wildcard name), enclose it in slashes, e.g. '/^release\//'
[channels]
default = '/^(?:main|master)$/'
[integrations.youtrack]
# When creating a release, clpack can mark the included issues
# as "Released" and record the versions into YouTrack.
#
# clpack will ask for confirmation before doing this.
#
# Requirements:
#
# This integration only works if your changelog entry file names (by default taken from branch names)
# contain the issue numbers - e.g. SW-1234-added-stuff.md. If the issue number can't be recognized,
# the entry will be skipped.
#
# You must not combine multiple tickets into one file if you want the numbers to be found
# - the file content is not parsed by this integration.
#
# Each developer who wants to use this integration when packing changelog must set their YouTrack
# API token in an env variable CLPACK_YOUTRACK_TOKEN (in their environment or in an .env file).
# It is also possible to change the server URL by setting CLPACK_YOUTRACK_URL, if needed.
# Enable the YouTrack integration
enabled = true
# YouTrack server URL. Can be changed locally by setting env var CLPACK_YOUTRACK_URL
url = "SET IN YOUR ENV"
# Channels filter - release on those channels will trigger the YouTrack integration
# (i.e. don't mark as Released if it's only in beta)
channels = [
"default"
]
# Change the "State" field of the released issues.
# Uncomment to enable, change to fit your project
released_state = "Released"
# Change a custom version field of the released issues.
# Uncomment to enable, change to fit your project
version_field = "Available in version"

@ -12,7 +12,7 @@ pub(crate) fn cl_log(ctx: AppContext) -> anyhow::Result<()> {
let branch = get_branch_name(&ctx);
let issue = branch
.as_ref()
.map(|b| b.parse_issue(&ctx))
.map(|b| b.parse_issue(&ctx.config))
.transpose()?
.flatten();

@ -1,8 +1,12 @@
use crate::AppContext;
use crate::config::ChannelName;
use crate::config::{ChannelName, ENV_YOUTRACK_TOKEN, ENV_YOUTRACK_URL};
use crate::git::{BranchName, get_branch_name};
use crate::integrations::youtrack::{
YouTrackClient, youtrack_integration_enabled, youtrack_integration_on_release,
};
use crate::store::{Release, Store};
use anyhow::bail;
use crate::utils::empty_to_none::EmptyToNone;
use anyhow::{Context, bail};
use colored::Colorize;
pub fn pack_resolve_and_show_preview(
@ -50,7 +54,7 @@ fn resolve_channel(
None => (
branch
.as_ref()
.map(|b| b.parse_channel(ctx))
.map(|b| b.parse_channel(&ctx.config))
.transpose()?
.flatten(),
false,
@ -107,7 +111,7 @@ pub(crate) fn cl_pack(
// TODO try to get something better from git!
let version_base = branch
.as_ref()
.map(|b| b.parse_version(&ctx))
.map(|b| b.parse_version(&ctx.config))
.transpose()?
.flatten();
@ -130,7 +134,7 @@ pub(crate) fn cl_pack(
}
}
release.version = version;
release.version = version.clone();
if !inquire::Confirm::new("Continue - write to changelog file?")
.with_default(true)
@ -140,8 +144,23 @@ pub(crate) fn cl_pack(
return Ok(());
}
store.create_release(channel, release)?;
store.create_release(channel.clone(), release.clone())?;
println!("{}", "Changelog written.".green());
// YouTrack
if youtrack_integration_enabled(&ctx.config, &channel) {
if inquire::Confirm::new("Update released issues in YouTrack?")
.with_default(true)
.prompt()?
{
youtrack_integration_on_release(&ctx.config, release)?;
println!("{}", "YouTrack updated.".green());
} else {
eprintln!("{}", "YouTrack changes skipped.".yellow());
return Ok(());
}
}
Ok(())
}

@ -76,3 +76,42 @@ branch_version_pattern = '/^rel\/([\d.]+)$/'
# To specify a regex pattern (wildcard name), enclose it in slashes, e.g. '/^release\//'
[channels]
default = '/^(?:main|master)$/'
[integrations.youtrack]
# When creating a release, clpack can mark the included issues
# as "Released" and record the versions into YouTrack.
#
# clpack will ask for confirmation before doing this.
#
# Requirements:
#
# This integration only works if your changelog entry file names (by default taken from branch names)
# contain the issue numbers - e.g. SW-1234-added-stuff.md. If the issue number can't be recognized,
# the entry will be skipped.
#
# You must not combine multiple tickets into one file if you want the numbers to be found
# - the file content is not parsed by this integration.
#
# Each developer who wants to use this integration when packing changelog must set their YouTrack
# API token in an env variable CLPACK_YOUTRACK_TOKEN (in their environment or in an .env file).
# It is also possible to change the server URL by setting CLPACK_YOUTRACK_URL, if needed.
# Enable the YouTrack integration
enabled = false
# YouTrack server URL. Can be changed locally by setting env var CLPACK_YOUTRACK_URL
url = "https://example.youtrack.cloud"
# Channels filter - release on those channels will trigger the YouTrack integration
# (i.e. don't mark as Released if it's only in beta)
channels = [
"default"
]
# Change the "State" field of the released issues.
# Uncomment to enable, change to fit your project
#released_state = "Released"
# Change a custom version field of the released issues.
# Uncomment to enable, change to fit your project
#version_field = "Available in version"

@ -11,10 +11,14 @@ pub type VersionName = String;
/// e.g. SW-1234-stuff-is-broken (without .md)
pub type EntryName = String;
/// Config file with nice comments
pub const CONFIG_FILE_TEMPLATE: &str = include_str!("assets/config_file_template.toml");
/// ENV / dotenv key for the youtrack integration server URL
/// This is only for unit tests
pub const ENV_YOUTRACK_URL: &str = "CLPACK_YOUTRACK_URL";
/// ENV / dotenv key for the youtrack integration API token
pub const ENV_YOUTRACK_TOKEN: &str = "CLPACK_YOUTRACK_TOKEN";
#[cfg(test)]
@ -107,4 +111,38 @@ pub struct Config {
/// TODO attempt to parse version from package.json, composer.json, Cargo.toml and others
#[default(Some(r"/^rel\/([\d.]+)$/".to_string()))]
pub branch_version_pattern: Option<String>,
/// Integrations config
pub integrations: IntegrationsConfig,
}
/// Integrations config
#[derive(Debug, Serialize, Deserialize, SmartDefault, PartialEq, Clone)]
#[serde(deny_unknown_fields, default)]
pub struct IntegrationsConfig {
/// YouTrack integration
pub youtrack: YouTrackIntegrationConfig,
}
#[derive(Debug, Serialize, Deserialize, SmartDefault, PartialEq, Clone)]
#[serde(deny_unknown_fields, default)]
pub struct YouTrackIntegrationConfig {
/// Enable the integration
pub enabled: bool,
/// URL of the youtrack server (just https://domain)
#[default = "https://example.youtrack.cloud"]
pub url: String,
/// Channels filter
#[default(vec![
"default".to_string(),
])]
pub channels: Vec<ChannelName>,
/// Name of the State option to switch to when generating changelog (e.g. Released)
pub released_state: Option<String>,
/// Name of the version field (Available in version)
pub version_field: Option<String>,
}

@ -1,4 +1,5 @@
use crate::AppContext;
use crate::config::Config;
use crate::utils::empty_to_none::EmptyToNone;
use anyhow::bail;
use std::fmt::Display;
@ -79,8 +80,8 @@ impl BranchName {
/// Parse version from this branch name.
///
/// Aborts if the configured regex pattern is invalid.
pub fn parse_version(&self, ctx: &AppContext) -> anyhow::Result<Option<String>> {
let Some(pat) = ctx.config.branch_version_pattern.as_ref().empty_to_none() else {
pub fn parse_version(&self, config: &Config) -> anyhow::Result<Option<String>> {
let Some(pat) = config.branch_version_pattern.as_ref().empty_to_none() else {
return Ok(None);
};
self.parse_using_regex(pat, "branch_version_pattern")
@ -89,16 +90,16 @@ impl BranchName {
/// Parse issue number from this branch name.
///
/// Aborts if the configured regex pattern is invalid.
pub fn parse_issue(&self, ctx: &AppContext) -> anyhow::Result<Option<String>> {
let Some(pat) = ctx.config.branch_issue_pattern.as_ref().empty_to_none() else {
pub fn parse_issue(&self, config: &Config) -> anyhow::Result<Option<String>> {
let Some(pat) = config.branch_issue_pattern.as_ref().empty_to_none() else {
return Ok(None);
};
self.parse_using_regex(pat, "branch_issue_pattern")
}
/// Try to detect a release channel from this branch name (e.g. stable, EAP)
pub fn parse_channel(&self, ctx: &AppContext) -> anyhow::Result<Option<String>> {
for (channel_id, template) in &ctx.config.channels {
pub fn parse_channel(&self, config: &Config) -> anyhow::Result<Option<String>> {
for (channel_id, template) in &config.channels {
if template.is_empty() {
// Channel only for manual choosing
continue;
@ -165,14 +166,14 @@ mod test {
assert_eq!(
BranchName("rel/3.14".to_string())
.parse_version(&ctx)
.parse_version(&ctx.config)
.unwrap(),
Some("3.14".to_string())
);
assert_eq!(
BranchName("rel/foo".to_string())
.parse_version(&ctx)
.parse_version(&ctx.config)
.unwrap(),
None
);
@ -188,21 +189,21 @@ mod test {
assert_eq!(
BranchName("1234-bober-kurwa".to_string())
.parse_issue(&ctx)
.parse_issue(&ctx.config)
.unwrap(),
Some("1234".to_string())
);
assert_eq!(
BranchName("SW-778-jakie-bydłe-jebane".to_string())
.parse_issue(&ctx)
.parse_issue(&ctx.config)
.unwrap(),
Some("SW-778".to_string())
);
assert_eq!(
BranchName("nie-spierdalaj-mordo".to_string())
.parse_issue(&ctx)
.parse_issue(&ctx.config)
.unwrap(),
None
);
@ -217,20 +218,22 @@ mod test {
};
assert_eq!(
BranchName("main".to_string()).parse_channel(&ctx).unwrap(),
BranchName("main".to_string())
.parse_channel(&ctx.config)
.unwrap(),
Some("default".to_string())
);
assert_eq!(
BranchName("master".to_string())
.parse_channel(&ctx)
.parse_channel(&ctx.config)
.unwrap(),
Some("default".to_string())
);
assert_eq!(
BranchName("my-cool-feature".to_string())
.parse_version(&ctx)
.parse_version(&ctx.config)
.unwrap(),
None
);

@ -1,3 +1,2 @@
mod youtrack;
pub use youtrack::YouTrackClient;
/// Third party service (e.g. issue trackers) integrations
pub mod youtrack;

@ -1,48 +1,152 @@
//! Youtrack integration (mark issues as Released when packing to changelog)
//! Youtrack integration (mark issues as Released when packing to changelog, change Available in version)
use crate::config::VersionName;
use anyhow::bail;
use crate::config::{ChannelName, ENV_YOUTRACK_TOKEN, ENV_YOUTRACK_URL, VersionName};
use crate::git::BranchName;
use crate::store::Release;
use anyhow::{Context, 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;
/// ID of a youtrack project
type ProjectId = String;
type BundleID = String;
type FieldID = String;
pub fn youtrack_integration_enabled(config: &crate::Config, channel: &ChannelName) -> bool {
let ytconf = &config.integrations.youtrack;
ytconf.enabled
// Channel filter
&& ytconf.channels.contains(&channel)
// URL is required
&& (!ytconf.url.is_empty() || dotenv::var(ENV_YOUTRACK_URL).is_ok_and(|v| !v.is_empty()))
// Token is required
&& dotenv::var(ENV_YOUTRACK_TOKEN).is_ok_and(|v| !v.is_empty())
// Check if we have something to do
&& (ytconf.version_field.as_ref().is_some_and(|v| !v.is_empty())
|| ytconf
.released_state
.as_ref()
.is_some_and(|v| !v.is_empty()))
}
pub fn youtrack_integration_on_release(
config: &crate::Config,
release: Release,
) -> anyhow::Result<()> {
let ytconf = &config.integrations.youtrack;
let url = dotenv::var(ENV_YOUTRACK_URL)
.ok()
.unwrap_or_else(|| ytconf.url.clone());
if url.is_empty() {
bail!("YouTrack URL is empty!");
}
let token = dotenv::var(ENV_YOUTRACK_TOKEN).context("Error getting YouTrack token")?;
if token.is_empty() {
bail!("YouTrack token is empty!");
}
let client = YouTrackClient::new(url, &token)?;
let mut project_id_opt = None;
let date = chrono::Utc::now();
for entry in release.entries {
let branch_name = BranchName(entry);
let Ok(Some(issue_num)) = branch_name.parse_issue(config) else {
eprintln!("No issue number recognized in {}", branch_name.0);
continue;
};
// Assume all tickets belong to the same project
if project_id_opt.is_none() {
match client.find_project_id(&issue_num) {
Ok(project_id) => {
project_id_opt = Some(project_id);
}
Err(e) => {
eprintln!("Failed to find project number from {issue_num}: {e}");
continue;
}
}
}
let project_id = project_id_opt.as_ref().unwrap(); // We know it is set now
let mut set_version_opt = None;
if let Some(field) = &ytconf.version_field {
let set_version = SetVersion {
field_name: field,
version: &release.version,
};
client.ensure_version_exists_in_project(&project_id, &set_version, Some(date))?;
set_version_opt = Some(set_version);
}
println!("Update issue {issue_num} ({}) in YouTrack", branch_name.0);
client.set_issue_version_and_state_by_name(
&issue_num,
set_version_opt.as_ref(),
ytconf.released_state.as_deref(),
)?;
}
Ok(())
}
/// YouTrack API client (with only the bare minimum of the API implemented to satisfy clpack's needs)
pub struct YouTrackClient {
/// HTTPS client with default presets to access the API
client: reqwest::blocking::Client,
pub url: String,
/// Base URL of the API server
url: String,
}
/// Error received from the API instead of the normal response
#[derive(Deserialize)]
struct YoutrackErrorResponse {
/// Error ID
error: String,
/// Error message
error_description: String,
}
impl YouTrackClient {
/// Create a YouTrack client
///
/// url - API server base URL (e.g. https://mycompany.youtrack.cloud)
/// token - JWT-like token, starts with "perm-". Obtained from YouTrack profile settings
pub fn new(url: impl ToString, token: &str) -> anyhow::Result<Self> {
let token_bearer = format!("Bearer {token}");
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")?);
let mut headers = HeaderMap::new();
headers.insert("Authorization", HeaderValue::from_str(&token_bearer)?);
headers.insert("Content-Type", HeaderValue::from_str("application/json")?);
headers.insert("Accept", HeaderValue::from_str("application/json")?);
Ok(YouTrackClient {
url: url.to_string(),
client: reqwest::blocking::Client::builder()
.default_headers(hm)
.default_headers(headers)
.build()?,
})
}
fn parse_youtrack_error_response(payload: &str) -> anyhow::Error {
if let Ok(e) = serde_json::from_str::<YoutrackErrorResponse>(&payload) {
anyhow::format_err!("Error from YouTrack: {} - {}", e.error, e.error_description)
} else {
anyhow::format_err!("Error from YouTrack (unknown response format): {payload}")
}
}
/// Send a GET request with query parameters. Deserialize response.
fn get_json<T: Serialize + ?Sized, O: DeserializeOwned>(
&self,
api_path: String,
@ -57,18 +161,19 @@ impl YouTrackClient {
debug!("GET {}", url);
let response = self.client.get(&url).query(query).send()?;
let is_ok = response.status().is_success();
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);
if !is_ok {
return Err(Self::parse_youtrack_error_response(&response_text));
}
Ok(serde_json::from_str(&response_text)?)
}
/// Send a POST request with query parameters and serializable (JSON) body. Deserialize response.
fn post_json<T: Serialize + ?Sized, B: Serialize + ?Sized, O: DeserializeOwned>(
&self,
api_path: String,
@ -91,18 +196,20 @@ impl YouTrackClient {
.body(body_serialized.into_bytes())
.send()?;
let is_ok = response.status().is_success();
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);
if !is_ok {
return Err(Self::parse_youtrack_error_response(&response_text));
}
Ok(serde_json::from_str(&response_text)?)
}
pub fn find_project_id(&self, issue: &str) -> anyhow::Result<ProjectId> {
/// Find YouTrack project ID from an issue name
pub fn find_project_id(&self, issue_name: &str) -> anyhow::Result<ProjectId> {
#[derive(Deserialize)]
struct Issue {
project: Project,
@ -114,7 +221,7 @@ impl YouTrackClient {
}
let issue: Issue =
self.get_json(format!("issues/{issue}"), &[("fields", "project(id)")])?;
self.get_json(format!("issues/{issue_name}"), &[("fields", "project(id)")])?;
// example:
// {"project":{"id":"0-172","$type":"Project"},"$type":"Issue"}
@ -125,27 +232,30 @@ impl YouTrackClient {
/// 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.
/// - project_id - obtained by `find_project_id()`
/// - version_info - version name and field name
/// - release_date - if creating, a date YYYY-MM-DD can be passed here. It will be stored into the
/// newly created version & it will be marked as released.
pub fn ensure_version_exists_in_project(
&self,
project_id: &str,
field_name: &str,
version_to_create: &str,
version_info: &SetVersion,
release_date: Option<DateTime<Utc>>,
) -> anyhow::Result<()> {
type BundleID = String;
type FieldID = String;
#[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
@ -159,9 +269,10 @@ impl YouTrackClient {
&[("fields", "field(name,id),bundle(id)"), ("top", "200")],
)?;
// Find the field we want in the list (XXX this can probably be done with some API query?)
let mut field_bundle = None;
for entry in fields {
if &entry.field.name == field_name
if &entry.field.name == version_info.field_name
&& let Some(bundle) = entry.bundle
{
field_bundle = Some((entry.field.id, bundle.id));
@ -169,8 +280,12 @@ impl YouTrackClient {
}
}
// Got something?
let Some((field_id, bundle_id)) = field_bundle else {
bail!("YouTrack version field {field_name} not found in the project {project_id}");
bail!(
"YouTrack version field {field_name} not found in the project {project_id}",
field_name = version_info.field_name
);
};
println!("Found YouTrack version field, checking defined versions");
@ -181,60 +296,46 @@ impl YouTrackClient {
id: String,
}
// Look at options already defined on the field
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");
// Is our version defined?
if versions.iter().any(|v| v.name == version_info.version) {
eprintln!(
"Version {v} already exists in YouTrack",
v = version_info.version
);
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'],
println!(
"Creating version in YouTrack: {v}",
v = version_info.version
);
*/
#[derive(Serialize)]
#[allow(non_snake_case)]
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(),
let request_body = CreateVersionBody {
name: version_info.version.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)]
struct CreateVersionResponse {
releaseDate: Option<i64>,
released: bool,
archived: bool,
@ -244,7 +345,7 @@ impl YouTrackClient {
let resp: CreateVersionResponse = self.post_json(
format!("admin/customFieldSettings/bundles/version/{bundle_id}/values"),
&body,
&request_body,
&[("fields", "id,name,released,releaseDate,archived")],
)?;
@ -253,21 +354,28 @@ impl YouTrackClient {
// {"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.");
println!("Version {v} created in YouTrack.", v = version_info.version);
Ok(())
}
pub fn set_issue_version_and_state(
/// Modify a YouTrack issue by changing its State and setting "Available in version".
///
/// Before calling this, make sure the version exists, e.g. using `ensure_version_exists_in_project`
///
/// - issue_id - e.g. SW-1234
/// - version_field_name - name of the YT custom field to modify
/// - version_name - name of the version, e.g. 1.0.0
/// - target_state_name name of the State to switch the issue to (None for no-op)
pub fn set_issue_version_and_state_by_name(
&self,
issue_id: &str,
version_field_name: &str,
version_name: &str,
target_state_name: &str,
version: Option<&SetVersion>,
state: Option<&str>,
) -> anyhow::Result<()> {
#[derive(Serialize)]
struct PatchIssueBody {
#[allow(non_snake_case)]
struct PatchIssueBody {
customFields: Vec<CustomFieldValue>,
}
@ -284,23 +392,35 @@ impl YouTrackClient {
value: EnumValue,
}
let body = PatchIssueBody {
customFields: vec![
CustomFieldValue {
name: version_field_name.to_string(),
let mut custom_fields = Vec::new();
if let Some(version) = version {
custom_fields.push(CustomFieldValue {
name: version.field_name.to_string(),
field_type: "SingleVersionIssueCustomField".to_string(),
value: EnumValue {
name: version_name.to_string(),
name: version.version.to_string(),
},
},
CustomFieldValue {
});
}
if let Some(target_state_name) = state {
custom_fields.push(CustomFieldValue {
name: "State".to_string(),
field_type: "StateIssueCustomField".to_string(),
value: EnumValue {
name: target_state_name.to_string(),
},
},
],
});
}
if custom_fields.is_empty() {
eprintln!("Nothing to do in YouTrack - no version field, no target state.");
return Ok(());
}
let body = PatchIssueBody {
customFields: custom_fields,
};
let resp: Value = self.post_json(
@ -309,7 +429,7 @@ impl YouTrackClient {
&[("fields", "id,customFields(name,value(name))")],
)?;
// TODO? Do something with the fields
// Do something with the requested 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"}
@ -321,12 +441,20 @@ impl YouTrackClient {
}
}
/// Params for YT to change version field
#[derive(Clone)]
pub struct SetVersion<'a> {
/// Field name, e.g. Available in version
pub field_name: &'a str,
/// Version name, e.g. 1.0.0
pub version: &'a str,
}
#[cfg(test)]
mod tests {
use super::YouTrackClient;
use super::{SetVersion, YouTrackClient};
use crate::config::{ENV_YOUTRACK_TOKEN, ENV_YOUTRACK_URL};
use log::{LevelFilter, debug};
use serde_json::Value;
// #[test] // Disabled
fn test_youtrack_communication() {
@ -341,7 +469,12 @@ mod tests {
let target_state_name = "Released";
let version_name = "TEST2";
let mut client = YouTrackClient::new(url, &token).unwrap();
let set_version = SetVersion {
field_name: version_field_name,
version: version_name,
};
let client = YouTrackClient::new(url, &token).unwrap();
let project_id = client.find_project_id(issue_id).unwrap();
@ -350,20 +483,14 @@ mod tests {
let date = chrono::Utc::now();
client
.ensure_version_exists_in_project(
&project_id,
version_field_name,
version_name,
Some(date),
)
.ensure_version_exists_in_project(&project_id, &set_version, Some(date))
.unwrap();
client
.set_issue_version_and_state(
.set_issue_version_and_state_by_name(
issue_id,
version_field_name,
version_name,
target_state_name,
Some(&set_version),
Some(target_state_name),
)
.unwrap();
}

Loading…
Cancel
Save