1use schemars::JsonSchema;
2use serde::{Deserialize, Serialize};
3
4use super::{
5 ContentSource, ExtraFileSpec, StringOrBool, TemplatedExtraFile, deserialize_string_or_bool_opt,
6};
7
8#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
13#[serde(default)]
14pub struct ReleaseConfig {
15 pub github: Option<ScmRepoConfig>,
17 pub gitlab: Option<ScmRepoConfig>,
19 pub gitea: Option<ScmRepoConfig>,
21 pub draft: Option<bool>,
23 #[schemars(schema_with = "prerelease_schema")]
24 pub prerelease: Option<PrereleaseConfig>,
26 #[schemars(schema_with = "make_latest_schema")]
27 pub make_latest: Option<MakeLatestConfig>,
29 pub name_template: Option<String>,
31 pub header: Option<ContentSource>,
33 pub footer: Option<ContentSource>,
35 pub extra_files: Option<Vec<ExtraFileSpec>>,
37 pub templated_extra_files: Option<Vec<TemplatedExtraFile>>,
41 #[serde(deserialize_with = "deserialize_string_or_bool_opt", default)]
44 pub skip_upload: Option<StringOrBool>,
45 pub replace_existing_draft: Option<bool>,
47 pub replace_existing_artifacts: Option<bool>,
49 #[serde(
56 default,
57 alias = "disable",
58 deserialize_with = "deserialize_string_or_bool_opt"
59 )]
60 pub skip: Option<StringOrBool>,
61 pub mode: Option<String>,
63 pub ids: Option<Vec<String>>,
65 pub target_commitish: Option<String>,
67 pub discussion_category_name: Option<String>,
69 pub include_meta: Option<bool>,
71 pub use_existing_draft: Option<bool>,
73 pub tag: Option<String>,
79}
80
81impl ReleaseConfig {
82 pub const DEFAULT_NAME_TEMPLATE: &'static str = "{{ Tag }}";
87
88 pub const DEFAULT_MODE: &'static str = "keep-existing";
92
93 pub const VALID_MODES: &[&'static str] = &["keep-existing", "append", "prepend", "replace"];
95
96 pub fn resolved_name_template(&self) -> &str {
99 self.name_template
100 .as_deref()
101 .unwrap_or(Self::DEFAULT_NAME_TEMPLATE)
102 }
103
104 pub fn resolved_mode(&self) -> anyhow::Result<&str> {
110 match self.mode.as_deref() {
111 None | Some("") => Ok(Self::DEFAULT_MODE),
112 Some(m) if Self::VALID_MODES.contains(&m) => Ok(m),
113 Some(other) => Err(anyhow::anyhow!(
114 "release: invalid mode '{}', must be one of: {}",
115 other,
116 Self::VALID_MODES.join(", ")
117 )),
118 }
119 }
120
121 pub fn resolved_draft(&self) -> bool {
123 self.draft.unwrap_or(false)
124 }
125
126 pub fn resolved_replace_existing_draft(&self) -> bool {
128 self.replace_existing_draft.unwrap_or(false)
129 }
130
131 pub fn resolved_replace_existing_artifacts(&self) -> bool {
133 self.replace_existing_artifacts.unwrap_or(false)
134 }
135
136 pub fn resolved_include_meta(&self) -> bool {
139 self.include_meta.unwrap_or(false)
140 }
141
142 pub fn resolved_use_existing_draft(&self) -> bool {
145 self.use_existing_draft.unwrap_or(false)
146 }
147}
148
149fn prerelease_schema(
151 _generator: &mut schemars::r#gen::SchemaGenerator,
152) -> schemars::schema::Schema {
153 use schemars::schema::{InstanceType, Schema, SchemaObject, SingleOrVec, SubschemaValidation};
154 Schema::Object(SchemaObject {
155 subschemas: Some(Box::new(SubschemaValidation {
156 one_of: Some(vec![
157 Schema::Object(SchemaObject {
158 instance_type: Some(SingleOrVec::Single(Box::new(InstanceType::String))),
159 enum_values: Some(vec![serde_json::json!("auto")]),
160 ..Default::default()
161 }),
162 Schema::Object(SchemaObject {
163 instance_type: Some(SingleOrVec::Single(Box::new(InstanceType::Boolean))),
164 ..Default::default()
165 }),
166 ]),
167 ..Default::default()
168 })),
169 ..Default::default()
170 })
171}
172
173fn make_latest_schema(
175 _generator: &mut schemars::r#gen::SchemaGenerator,
176) -> schemars::schema::Schema {
177 use schemars::schema::{InstanceType, Schema, SchemaObject, SingleOrVec, SubschemaValidation};
178 Schema::Object(SchemaObject {
179 subschemas: Some(Box::new(SubschemaValidation {
180 one_of: Some(vec![
181 Schema::Object(SchemaObject {
182 instance_type: Some(SingleOrVec::Single(Box::new(InstanceType::String))),
183 enum_values: Some(vec![serde_json::json!("auto")]),
184 ..Default::default()
185 }),
186 Schema::Object(SchemaObject {
187 instance_type: Some(SingleOrVec::Single(Box::new(InstanceType::Boolean))),
188 ..Default::default()
189 }),
190 ]),
191 ..Default::default()
192 })),
193 ..Default::default()
194 })
195}
196
197pub(super) fn skip_push_schema(
199 _generator: &mut schemars::r#gen::SchemaGenerator,
200) -> schemars::schema::Schema {
201 use schemars::schema::{InstanceType, Schema, SchemaObject, SingleOrVec, SubschemaValidation};
202 Schema::Object(SchemaObject {
203 subschemas: Some(Box::new(SubschemaValidation {
204 one_of: Some(vec![
205 Schema::Object(SchemaObject {
206 instance_type: Some(SingleOrVec::Single(Box::new(InstanceType::String))),
207 enum_values: Some(vec![serde_json::json!("auto")]),
208 ..Default::default()
209 }),
210 Schema::Object(SchemaObject {
211 instance_type: Some(SingleOrVec::Single(Box::new(InstanceType::Boolean))),
212 ..Default::default()
213 }),
214 ]),
215 ..Default::default()
216 })),
217 ..Default::default()
218 })
219}
220
221#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
222pub struct ScmRepoConfig {
223 pub owner: String,
225 pub name: String,
227}
228
229pub type GitHubConfig = ScmRepoConfig;
231
232#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
238#[serde(rename_all = "lowercase")]
239pub enum ForceTokenKind {
240 GitHub,
241 GitLab,
242 Gitea,
243}
244
245#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
252#[serde(default, deny_unknown_fields)]
253pub struct GitHubUrlsConfig {
254 pub api: Option<String>,
256 pub upload: Option<String>,
258 pub download: Option<String>,
260 pub skip_tls_verify: Option<bool>,
262}
263
264#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
267#[serde(default, deny_unknown_fields)]
268pub struct GitLabUrlsConfig {
269 pub api: Option<String>,
271 pub download: Option<String>,
273 pub skip_tls_verify: Option<bool>,
275 pub use_package_registry: Option<bool>,
277 pub use_job_token: Option<bool>,
279}
280
281#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
284#[serde(default, deny_unknown_fields)]
285pub struct GiteaUrlsConfig {
286 pub api: Option<String>,
288 pub download: Option<String>,
290 pub skip_tls_verify: Option<bool>,
292}
293
294macro_rules! impl_auto_or_bool_serde {
301 ($ty:ty, $auto:path, $bool_variant:path) => {
302 impl Serialize for $ty {
303 fn serialize<S: serde::Serializer>(
304 &self,
305 serializer: S,
306 ) -> std::result::Result<S::Ok, S::Error> {
307 match self {
308 $auto => serializer.serialize_str("auto"),
309 $bool_variant(b) => serializer.serialize_bool(*b),
310 }
311 }
312 }
313
314 impl<'de> Deserialize<'de> for $ty {
315 fn deserialize<D: serde::Deserializer<'de>>(
316 deserializer: D,
317 ) -> std::result::Result<Self, D::Error> {
318 struct Visitor;
319 impl serde::de::Visitor<'_> for Visitor {
320 type Value = $ty;
321 fn expecting(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
322 write!(f, "\"auto\" or a boolean")
323 }
324 fn visit_bool<E: serde::de::Error>(
325 self,
326 v: bool,
327 ) -> std::result::Result<$ty, E> {
328 Ok($bool_variant(v))
329 }
330 fn visit_str<E: serde::de::Error>(
331 self,
332 v: &str,
333 ) -> std::result::Result<$ty, E> {
334 if v == "auto" {
335 Ok($auto)
336 } else {
337 Err(E::custom(format!("expected \"auto\", got \"{}\"", v)))
338 }
339 }
340 }
341 deserializer.deserialize_any(Visitor)
342 }
343 }
344 };
345}
346
347#[derive(Debug, Clone, PartialEq, Eq)]
349pub enum PrereleaseConfig {
350 Auto,
351 Bool(bool),
352}
353
354impl_auto_or_bool_serde!(
355 PrereleaseConfig,
356 PrereleaseConfig::Auto,
357 PrereleaseConfig::Bool
358);
359
360#[derive(Debug, Clone, PartialEq, Eq)]
365pub enum MakeLatestConfig {
366 Auto,
367 Bool(bool),
368 String(String),
370}
371
372impl Serialize for MakeLatestConfig {
373 fn serialize<S: serde::Serializer>(
374 &self,
375 serializer: S,
376 ) -> std::result::Result<S::Ok, S::Error> {
377 match self {
378 MakeLatestConfig::Auto => serializer.serialize_str("auto"),
379 MakeLatestConfig::Bool(b) => serializer.serialize_bool(*b),
380 MakeLatestConfig::String(s) => serializer.serialize_str(s),
381 }
382 }
383}
384
385impl<'de> Deserialize<'de> for MakeLatestConfig {
386 fn deserialize<D: serde::Deserializer<'de>>(
387 deserializer: D,
388 ) -> std::result::Result<Self, D::Error> {
389 struct Visitor;
390 impl serde::de::Visitor<'_> for Visitor {
391 type Value = MakeLatestConfig;
392 fn expecting(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
393 write!(f, "\"auto\", a boolean, or a template string")
394 }
395 fn visit_bool<E: serde::de::Error>(
396 self,
397 v: bool,
398 ) -> std::result::Result<MakeLatestConfig, E> {
399 Ok(MakeLatestConfig::Bool(v))
400 }
401 fn visit_str<E: serde::de::Error>(
402 self,
403 v: &str,
404 ) -> std::result::Result<MakeLatestConfig, E> {
405 match v {
406 "auto" => Ok(MakeLatestConfig::Auto),
407 "true" => Ok(MakeLatestConfig::Bool(true)),
408 "false" => Ok(MakeLatestConfig::Bool(false)),
409 other => Ok(MakeLatestConfig::String(other.to_string())),
410 }
411 }
412 }
413 deserializer.deserialize_any(Visitor)
414 }
415}
416
417#[derive(Debug, Clone, PartialEq, Eq)]
420pub enum SkipPushConfig {
421 Auto,
422 Bool(bool),
423 Template(String),
425}
426
427impl Serialize for SkipPushConfig {
428 fn serialize<S: serde::Serializer>(
429 &self,
430 serializer: S,
431 ) -> std::result::Result<S::Ok, S::Error> {
432 match self {
433 SkipPushConfig::Auto => serializer.serialize_str("auto"),
434 SkipPushConfig::Bool(b) => serializer.serialize_bool(*b),
435 SkipPushConfig::Template(s) => serializer.serialize_str(s),
436 }
437 }
438}
439
440impl<'de> Deserialize<'de> for SkipPushConfig {
441 fn deserialize<D: serde::Deserializer<'de>>(
442 deserializer: D,
443 ) -> std::result::Result<Self, D::Error> {
444 struct Visitor;
445 impl serde::de::Visitor<'_> for Visitor {
446 type Value = SkipPushConfig;
447 fn expecting(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
448 write!(f, "\"auto\", a boolean, or a template string")
449 }
450 fn visit_bool<E: serde::de::Error>(
451 self,
452 v: bool,
453 ) -> std::result::Result<SkipPushConfig, E> {
454 Ok(SkipPushConfig::Bool(v))
455 }
456 fn visit_str<E: serde::de::Error>(
457 self,
458 v: &str,
459 ) -> std::result::Result<SkipPushConfig, E> {
460 match v {
461 "auto" => Ok(SkipPushConfig::Auto),
462 "true" => Ok(SkipPushConfig::Bool(true)),
463 "false" => Ok(SkipPushConfig::Bool(false)),
464 other => Ok(SkipPushConfig::Template(other.to_string())),
465 }
466 }
467 }
468 deserializer.deserialize_any(Visitor)
469 }
470}