1use std::collections::HashMap;
2use std::path::PathBuf;
3
4use schemars::JsonSchema;
5use serde::{Deserialize, Deserializer, Serialize};
6
7#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
25#[serde(untagged)]
26pub enum IncludeSpec {
27 Path(String),
29 FromFile { from_file: IncludeFilePath },
31 FromUrl { from_url: IncludeUrlConfig },
33}
34
35#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
37pub struct IncludeFilePath {
38 pub path: String,
40}
41
42#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
44pub struct IncludeUrlConfig {
45 pub url: String,
48 pub headers: Option<HashMap<String, String>>,
50}
51
52#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
59#[serde(default, deny_unknown_fields)]
60pub struct Config {
61 pub version: Option<u32>,
63 pub project_name: String,
65 #[serde(default = "default_dist")]
67 pub dist: PathBuf,
68 pub includes: Option<Vec<IncludeSpec>>,
72 pub env_files: Option<EnvFilesConfig>,
76 pub defaults: Option<Defaults>,
78 pub before: Option<HooksConfig>,
80 pub after: Option<HooksConfig>,
82 pub crates: Vec<CrateConfig>,
84 pub changelog: Option<ChangelogConfig>,
86 #[serde(default, alias = "sign", deserialize_with = "deserialize_signs")]
88 #[schemars(schema_with = "signs_schema")]
89 pub signs: Vec<SignConfig>,
90 #[serde(default, alias = "binary_sign", deserialize_with = "deserialize_signs")]
92 #[schemars(schema_with = "signs_schema")]
93 pub binary_signs: Vec<SignConfig>,
94 pub docker_signs: Option<Vec<DockerSignConfig>>,
96 #[serde(default, deserialize_with = "deserialize_upx")]
100 #[schemars(schema_with = "upx_schema")]
101 pub upx: Vec<UpxConfig>,
102 pub snapshot: Option<SnapshotConfig>,
104 pub nightly: Option<NightlyConfig>,
106 pub announce: Option<AnnounceConfig>,
108 pub report_sizes: Option<bool>,
110 #[serde(default, deserialize_with = "deserialize_env_map")]
119 pub env: Option<HashMap<String, String>>,
120 pub variables: Option<HashMap<String, String>>,
123 pub publishers: Option<Vec<PublisherConfig>>,
125 pub dockerhub: Option<Vec<DockerHubConfig>>,
127 pub artifactories: Option<Vec<ArtifactoryConfig>>,
129 pub cloudsmiths: Option<Vec<CloudSmithConfig>>,
131 pub homebrew_casks: Option<Vec<TopLevelHomebrewCaskConfig>>,
136 pub tag: Option<TagConfig>,
138 pub git: Option<GitConfig>,
140 pub partial: Option<PartialConfig>,
142 pub workspaces: Option<Vec<WorkspaceConfig>>,
144 pub source: Option<SourceConfig>,
146 #[serde(default, alias = "sbom", deserialize_with = "deserialize_sboms")]
148 #[schemars(schema_with = "sboms_schema")]
149 pub sboms: Vec<SbomConfig>,
150 pub release: Option<ReleaseConfig>,
152 pub github_urls: Option<GitHubUrlsConfig>,
154 pub gitlab_urls: Option<GitLabUrlsConfig>,
156 pub gitea_urls: Option<GiteaUrlsConfig>,
158 pub force_token: Option<ForceTokenKind>,
161 pub notarize: Option<NotarizeConfig>,
163 pub metadata: Option<MetadataConfig>,
165 pub template_files: Option<Vec<TemplateFileConfig>>,
168 pub monorepo: Option<MonorepoConfig>,
172 #[serde(
174 default,
175 alias = "makeself",
176 deserialize_with = "deserialize_makeselfs"
177 )]
178 #[schemars(schema_with = "makeselfs_schema")]
179 pub makeselfs: Vec<MakeselfConfig>,
180 pub srpm: Option<SrpmConfig>,
182 pub milestones: Option<Vec<MilestoneConfig>>,
184 pub uploads: Option<Vec<UploadConfig>>,
186 pub aur_sources: Option<Vec<AurSourceConfig>>,
188}
189
190fn signs_schema(generator: &mut schemars::r#gen::SchemaGenerator) -> schemars::schema::Schema {
192 let mut schema = generator.subschema_for::<Vec<SignConfig>>();
193 if let schemars::schema::Schema::Object(ref mut obj) = schema {
194 obj.metadata().description = Some("Artifact signing configurations (cosign, GPG, etc.). Accepts a single object or array.".to_owned());
195 }
196 schema
197}
198
199fn upx_schema(generator: &mut schemars::r#gen::SchemaGenerator) -> schemars::schema::Schema {
201 let mut schema = generator.subschema_for::<Vec<UpxConfig>>();
202 if let schemars::schema::Schema::Object(ref mut obj) = schema {
203 obj.metadata().description = Some(
204 "UPX binary compression configurations. Accepts a single object or array.".to_owned(),
205 );
206 }
207 schema
208}
209
210fn sboms_schema(generator: &mut schemars::r#gen::SchemaGenerator) -> schemars::schema::Schema {
212 let mut schema = generator.subschema_for::<Vec<SbomConfig>>();
213 if let schemars::schema::Schema::Object(ref mut obj) = schema {
214 obj.metadata().description =
215 Some("SBOM generation configurations. Accepts a single object or array.".to_owned());
216 }
217 schema
218}
219
220fn default_dist() -> PathBuf {
221 PathBuf::from("./dist")
222}
223
224impl Default for Config {
225 fn default() -> Self {
226 Config {
227 version: None,
228 project_name: String::new(),
229 dist: default_dist(),
230 includes: None,
231 env_files: None,
232 defaults: None,
233 before: None,
234 after: None,
235 crates: Vec::new(),
236 changelog: None,
237 signs: Vec::new(),
238 binary_signs: Vec::new(),
239 docker_signs: None,
240 upx: Vec::new(),
241 snapshot: None,
242 nightly: None,
243 announce: None,
244 report_sizes: None,
245 env: None,
246 variables: None,
247 publishers: None,
248 dockerhub: None,
249 artifactories: None,
250 cloudsmiths: None,
251 homebrew_casks: None,
252 tag: None,
253 git: None,
254 partial: None,
255 workspaces: None,
256 source: None,
257 sboms: Vec::new(),
258 release: None,
259 github_urls: None,
260 gitlab_urls: None,
261 gitea_urls: None,
262 force_token: None,
263 notarize: None,
264 metadata: None,
265 template_files: None,
266 monorepo: None,
267 makeselfs: Vec::new(),
268 srpm: None,
269 milestones: None,
270 uploads: None,
271 aur_sources: None,
272 }
273 }
274}
275
276impl Config {
277 pub fn monorepo_tag_prefix(&self) -> Option<&str> {
281 self.monorepo.as_ref().and_then(|m| m.tag_prefix.as_deref())
282 }
283
284 pub fn monorepo_dir(&self) -> Option<&str> {
288 self.monorepo.as_ref().and_then(|m| m.dir.as_deref())
289 }
290
291 pub fn meta_homepage(&self) -> Option<&str> {
305 self.metadata.as_ref().and_then(|m| m.homepage.as_deref())
306 }
307
308 pub fn meta_license(&self) -> Option<&str> {
310 self.metadata.as_ref().and_then(|m| m.license.as_deref())
311 }
312
313 pub fn meta_description(&self) -> Option<&str> {
315 self.metadata
316 .as_ref()
317 .and_then(|m| m.description.as_deref())
318 }
319
320 pub fn meta_maintainers(&self) -> &[String] {
322 self.metadata
323 .as_ref()
324 .and_then(|m| m.maintainers.as_deref())
325 .unwrap_or(&[])
326 }
327
328 pub fn meta_first_maintainer(&self) -> Option<&str> {
331 self.meta_maintainers().first().map(|s| s.as_str())
332 }
333}
334
335pub fn validate_version(config: &Config) -> Result<(), String> {
338 match config.version {
339 None | Some(1) | Some(2) => Ok(()),
340 Some(v) => Err(format!(
341 "unsupported config version: {}. Supported versions are 1 and 2.",
342 v
343 )),
344 }
345}
346
347pub fn validate_tag_sort(config: &Config) -> Result<(), String> {
353 if let Some(ref git) = config.git
354 && let Some(ref sort) = git.tag_sort
355 {
356 match sort.as_str() {
357 "-version:refname" | "-version:creatordate" => {}
358 other => {
359 return Err(format!(
360 "unsupported git.tag_sort value: \"{}\". \
361 Accepted values: \"-version:refname\", \"-version:creatordate\".",
362 other
363 ));
364 }
365 }
366 }
367 Ok(())
368}
369
370const KNOWN_GOOS: &[&str] = &[
375 "aix",
376 "android",
377 "darwin",
378 "dragonfly",
379 "freebsd",
380 "illumos",
381 "ios",
382 "js",
383 "linux",
384 "netbsd",
385 "openbsd",
386 "plan9",
387 "solaris",
388 "wasip1",
389 "windows",
390];
391
392pub fn validate_release_backends(config: &Config) -> Result<(), String> {
397 let check = |crate_name: &str, release: &ReleaseConfig| -> Result<(), String> {
398 let mut set = Vec::new();
399 if release.github.is_some() {
400 set.push("github");
401 }
402 if release.gitlab.is_some() {
403 set.push("gitlab");
404 }
405 if release.gitea.is_some() {
406 set.push("gitea");
407 }
408 if set.len() > 1 {
409 return Err(format!(
410 "crate {}: release config sets multiple mutually-exclusive SCM \
411 backends ({}). Pick one.",
412 crate_name,
413 set.join(" + ")
414 ));
415 }
416 Ok(())
417 };
418 for krate in &config.crates {
419 if let Some(ref release) = krate.release {
420 check(&krate.name, release)?;
421 }
422 }
423 if let Some(ws_list) = config.workspaces.as_ref() {
424 for ws in ws_list {
425 for krate in &ws.crates {
426 if let Some(ref release) = krate.release {
427 check(&krate.name, release)?;
428 }
429 }
430 }
431 }
432 Ok(())
433}
434
435pub fn validate_format_overrides(config: &Config) -> Result<(), String> {
439 let check = |crate_name: &str, archives: &[ArchiveConfig]| -> Result<(), String> {
440 for (idx, archive) in archives.iter().enumerate() {
441 let Some(ref overrides) = archive.format_overrides else {
442 continue;
443 };
444 for over in overrides {
445 if !KNOWN_GOOS.contains(&over.os.as_str()) {
446 let archive_id = archive.id.as_deref().unwrap_or("default");
447 return Err(format!(
448 "crate {}: archives[{}] (id={}): format_overrides.goos=\"{}\" is not a recognised OS. \
449 Accepted values: {}.",
450 crate_name,
451 idx,
452 archive_id,
453 over.os,
454 KNOWN_GOOS.join(", ")
455 ));
456 }
457 }
458 }
459 Ok(())
460 };
461 for krate in &config.crates {
462 if let ArchivesConfig::Configs(ref list) = krate.archives {
463 check(&krate.name, list)?;
464 }
465 }
466 if let Some(ws_list) = config.workspaces.as_ref() {
467 for ws in ws_list {
468 for krate in &ws.crates {
469 if let ArchivesConfig::Configs(ref list) = krate.archives {
470 check(&krate.name, list)?;
471 }
472 }
473 }
474 }
475 Ok(())
476}
477
478#[derive(Debug, Clone, Serialize, JsonSchema)]
499#[serde(untagged)]
500pub enum EnvFilesConfig {
501 List(Vec<String>),
503 TokenFiles(EnvFilesTokenConfig),
505}
506
507impl<'de> Deserialize<'de> for EnvFilesConfig {
508 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
509 let value = serde_yaml_ng::Value::deserialize(deserializer)?;
510 match &value {
511 serde_yaml_ng::Value::Sequence(_) => {
512 let list: Vec<String> =
513 serde_yaml_ng::from_value(value).map_err(serde::de::Error::custom)?;
514 Ok(EnvFilesConfig::List(list))
515 }
516 serde_yaml_ng::Value::Mapping(_) => {
517 let tokens: EnvFilesTokenConfig =
518 serde_yaml_ng::from_value(value).map_err(serde::de::Error::custom)?;
519 Ok(EnvFilesConfig::TokenFiles(tokens))
520 }
521 _ => Err(serde::de::Error::custom(
522 "env_files must be an array of file paths or a mapping with token file paths",
523 )),
524 }
525 }
526}
527
528impl EnvFilesConfig {
529 pub fn as_list(&self) -> Option<&[String]> {
531 match self {
532 EnvFilesConfig::List(files) => Some(files),
533 EnvFilesConfig::TokenFiles(_) => None,
534 }
535 }
536
537 pub fn as_token_files(&self) -> Option<&EnvFilesTokenConfig> {
539 match self {
540 EnvFilesConfig::List(_) => None,
541 EnvFilesConfig::TokenFiles(tokens) => Some(tokens),
542 }
543 }
544}
545
546#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
554#[serde(default, deny_unknown_fields)]
555pub struct EnvFilesTokenConfig {
556 pub github_token: Option<String>,
558 pub gitlab_token: Option<String>,
560 pub gitea_token: Option<String>,
562}
563
564pub fn read_token_file(path: &str) -> Result<Option<String>, String> {
569 let expanded = if let Some(suffix) = path.strip_prefix("~/") {
571 if let Ok(home) = std::env::var("HOME") {
572 format!("{}/{}", home, suffix)
573 } else {
574 path.to_string()
575 }
576 } else {
577 path.to_string()
578 };
579
580 match std::fs::read_to_string(&expanded) {
581 Ok(content) => {
582 let token = content.lines().next().unwrap_or("").trim().to_string();
583 if token.is_empty() {
584 Ok(None)
585 } else {
586 Ok(Some(token))
587 }
588 }
589 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None),
590 Err(e) => Err(format!("failed to read token file '{}': {}", path, e)),
591 }
592}
593
594pub fn load_token_files(
604 config: &EnvFilesTokenConfig,
605 log: &crate::log::StageLogger,
606) -> Result<std::collections::HashMap<String, String>, String> {
607 let mut vars = std::collections::HashMap::new();
608
609 let github_candidates: Vec<&str> = match config.github_token.as_deref() {
613 Some(p) => vec![p],
614 None => vec![
615 "~/.config/anodizer/github_token",
616 "~/.config/goreleaser/github_token",
617 ],
618 };
619 let gitlab_candidates: Vec<&str> = match config.gitlab_token.as_deref() {
620 Some(p) => vec![p],
621 None => vec![
622 "~/.config/anodizer/gitlab_token",
623 "~/.config/goreleaser/gitlab_token",
624 ],
625 };
626 let gitea_candidates: Vec<&str> = match config.gitea_token.as_deref() {
627 Some(p) => vec![p],
628 None => vec![
629 "~/.config/anodizer/gitea_token",
630 "~/.config/goreleaser/gitea_token",
631 ],
632 };
633 let mappings: [(&str, &[&str]); 3] = [
634 ("GITHUB_TOKEN", &github_candidates),
635 ("GITLAB_TOKEN", &gitlab_candidates),
636 ("GITEA_TOKEN", &gitea_candidates),
637 ];
638
639 for (env_name, candidates) in &mappings {
640 if std::env::var(env_name)
642 .ok()
643 .filter(|v| !v.is_empty())
644 .is_some()
645 {
646 log.verbose(&format!("using {} from process environment", env_name));
647 continue;
648 }
649 for file_path in candidates.iter() {
650 match read_token_file(file_path) {
651 Ok(Some(token)) => {
652 log.verbose(&format!("loaded {} from {}", env_name, file_path));
653 vars.insert(env_name.to_string(), token);
654 break;
655 }
656 Ok(None) => {
657 }
659 Err(e) => {
660 return Err(e);
661 }
662 }
663 }
664 }
665
666 Ok(vars)
667}
668
669pub fn load_env_files(
675 files: &[String],
676 log: &crate::log::StageLogger,
677 strict: bool,
678) -> Result<std::collections::HashMap<String, String>, String> {
679 let mut vars = std::collections::HashMap::new();
680 for file_path in files {
681 let content = match std::fs::read_to_string(file_path) {
682 Ok(c) => c,
683 Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
684 if strict {
685 return Err(format!("env file '{}' not found (strict mode)", file_path));
686 }
687 log.warn(&format!("env file '{}' not found, skipping", file_path));
688 continue;
689 }
690 Err(e) => {
691 return Err(format!("failed to read env file '{}': {}", file_path, e));
692 }
693 };
694 for line in content.lines() {
695 let trimmed = line.trim();
696 if trimmed.is_empty() || trimmed.starts_with('#') {
697 continue;
698 }
699 let trimmed = trimmed.strip_prefix("export ").unwrap_or(trimmed);
701 if let Some((key, value)) = trimmed.split_once('=') {
702 let key = key.trim();
703 if key.is_empty() {
704 log.warn(&format!(
705 "skipping line with empty key in '{}': {}",
706 file_path,
707 line.trim()
708 ));
709 continue;
710 }
711 let value = value.trim();
712 let value = if value.len() >= 2
714 && ((value.starts_with('"') && value.ends_with('"'))
715 || (value.starts_with('\'') && value.ends_with('\'')))
716 {
717 &value[1..value.len() - 1]
718 } else {
719 value
720 };
721 vars.insert(key.to_string(), value.to_string());
722 } else {
723 log.warn(&format!(
724 "skipping line without '=' in '{}': {}",
725 file_path, trimmed
726 ));
727 }
728 }
729 }
730 Ok(vars)
731}
732
733fn deserialize_env_map<'de, D>(deserializer: D) -> Result<Option<HashMap<String, String>>, D::Error>
756where
757 D: Deserializer<'de>,
758{
759 use serde::de::{self, Visitor};
760
761 struct EnvMapVisitor;
762
763 impl<'de> Visitor<'de> for EnvMapVisitor {
764 type Value = Option<HashMap<String, String>>;
765
766 fn expecting(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
767 f.write_str("a mapping of env vars (KEY: VALUE) or a list of KEY=VALUE strings")
768 }
769
770 fn visit_none<E: de::Error>(self) -> Result<Self::Value, E> {
771 Ok(None)
772 }
773
774 fn visit_unit<E: de::Error>(self) -> Result<Self::Value, E> {
775 Ok(None)
776 }
777
778 fn visit_map<M: de::MapAccess<'de>>(self, mut map: M) -> Result<Self::Value, M::Error> {
779 let mut result = HashMap::new();
780 while let Some((key, value)) = map.next_entry::<String, String>()? {
781 result.insert(key, value);
782 }
783 Ok(Some(result))
784 }
785
786 fn visit_seq<A: de::SeqAccess<'de>>(self, mut seq: A) -> Result<Self::Value, A::Error> {
787 let mut result = HashMap::new();
788 while let Some(entry) = seq.next_element::<String>()? {
789 match entry.split_once('=') {
790 Some((key, value)) => {
791 let key = key.trim();
792 if key.is_empty() {
793 return Err(de::Error::custom(format!(
794 "env list entry has empty key: {:?}",
795 entry
796 )));
797 }
798 result.insert(key.to_string(), value.to_string());
799 }
800 None => {
801 return Err(de::Error::custom(format!(
802 "env list entry must be KEY=VALUE, got: {:?}",
803 entry
804 )));
805 }
806 }
807 }
808 Ok(Some(result))
809 }
810 }
811
812 deserializer.deserialize_any(EnvMapVisitor)
813}
814
815#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
820#[serde(default)]
821pub struct Defaults {
822 pub targets: Option<Vec<String>>,
824 pub cross: Option<CrossStrategy>,
826 pub flags: Option<String>,
828 pub archives: Option<DefaultArchiveConfig>,
830 pub checksum: Option<ChecksumConfig>,
832 pub ignore: Option<Vec<BuildIgnore>>,
834 pub overrides: Option<Vec<BuildOverride>>,
836}
837
838#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
839#[serde(default)]
840pub struct DefaultArchiveConfig {
841 pub format: Option<String>,
843 pub format_overrides: Option<Vec<FormatOverride>>,
845}
846
847#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
853pub struct BuildIgnore {
854 pub os: String,
856 pub arch: String,
858}
859
860#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
866#[serde(default)]
867pub struct BuildOverride {
868 pub targets: Vec<String>,
870 #[serde(default, deserialize_with = "deserialize_env_map")]
872 pub env: Option<HashMap<String, String>>,
873 pub flags: Option<String>,
875 pub features: Option<Vec<String>>,
877}
878
879#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
884#[serde(rename_all = "lowercase")]
885pub enum CrossStrategy {
886 Auto,
887 Zigbuild,
888 Cross,
889 Cargo,
890}
891
892#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
897#[serde(default)]
898pub struct CrateConfig {
899 pub name: String,
901 pub path: String,
903 pub tag_template: String,
905 pub version: Option<String>,
911 pub depends_on: Option<Vec<String>>,
913 pub builds: Option<Vec<BuildConfig>>,
915 pub cross: Option<CrossStrategy>,
917 #[serde(default, deserialize_with = "deserialize_archives_config")]
918 #[schemars(schema_with = "archives_schema")]
919 pub archives: ArchivesConfig,
920 pub checksum: Option<ChecksumConfig>,
922 pub release: Option<ReleaseConfig>,
924 pub publish: Option<PublishConfig>,
926 pub docker: Option<Vec<DockerConfig>>,
928 pub docker_v2: Option<Vec<DockerV2Config>>,
930 pub docker_digest: Option<DockerDigestConfig>,
932 pub docker_manifests: Option<Vec<DockerManifestConfig>>,
934 pub nfpm: Option<Vec<NfpmConfig>>,
936 pub snapcrafts: Option<Vec<SnapcraftConfig>>,
938 pub dmgs: Option<Vec<DmgConfig>>,
940 pub msis: Option<Vec<MsiConfig>>,
942 pub pkgs: Option<Vec<PkgConfig>>,
944 pub nsis: Option<Vec<NsisConfig>>,
946 pub app_bundles: Option<Vec<AppBundleConfig>>,
948 pub flatpaks: Option<Vec<FlatpakConfig>>,
950 pub blobs: Option<Vec<BlobConfig>>,
952 pub binstall: Option<BinstallConfig>,
954 pub version_sync: Option<VersionSyncConfig>,
956 pub universal_binaries: Option<Vec<UniversalBinaryConfig>>,
958 #[serde(default, deserialize_with = "deserialize_string_or_bool_opt")]
961 pub no_unique_dist_dir: Option<StringOrBool>,
962}
963
964fn archives_schema(generator: &mut schemars::r#gen::SchemaGenerator) -> schemars::schema::Schema {
966 let mut schema = generator.subschema_for::<Option<Vec<ArchiveConfig>>>();
967 if let schemars::schema::Schema::Object(ref mut obj) = schema {
968 obj.metadata().description = Some("Archive configurations for this crate. Set to false to disable archiving, or provide an array of archive configs.".to_owned());
969 }
970 schema
971}
972
973impl Default for CrateConfig {
974 fn default() -> Self {
975 CrateConfig {
976 name: String::new(),
977 path: String::new(),
978 tag_template: String::new(),
979 version: None,
980 depends_on: None,
981 builds: None,
982 cross: None,
983 archives: ArchivesConfig::Configs(vec![]),
984 checksum: None,
985 release: None,
986 publish: None,
987 docker: None,
988 docker_v2: None,
989 docker_digest: None,
990 docker_manifests: None,
991 nfpm: None,
992 snapcrafts: None,
993 dmgs: None,
994 msis: None,
995 pkgs: None,
996 nsis: None,
997 app_bundles: None,
998 flatpaks: None,
999 blobs: None,
1000 binstall: None,
1001 version_sync: None,
1002 universal_binaries: None,
1003 no_unique_dist_dir: None,
1004 }
1005 }
1006}
1007
1008#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
1013#[serde(default)]
1014pub struct UniversalBinaryConfig {
1015 #[serde(default)]
1018 pub id: Option<String>,
1019 pub name_template: Option<String>,
1021 pub replace: Option<bool>,
1023 pub ids: Option<Vec<String>>,
1025 pub hooks: Option<BuildHooksConfig>,
1027 pub mod_timestamp: Option<String>,
1029}
1030
1031#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
1036#[serde(default)]
1037pub struct BuildConfig {
1038 pub id: Option<String>,
1040 pub binary: String,
1042 #[serde(default, deserialize_with = "deserialize_string_or_bool_opt")]
1044 pub skip: Option<StringOrBool>,
1045 pub targets: Option<Vec<String>>,
1047 pub features: Option<Vec<String>>,
1049 pub no_default_features: Option<bool>,
1051 pub env: Option<HashMap<String, HashMap<String, String>>>,
1053 pub copy_from: Option<String>,
1055 pub flags: Option<String>,
1057 pub reproducible: Option<bool>,
1059 pub hooks: Option<BuildHooksConfig>,
1061 pub ignore: Option<Vec<BuildIgnore>>,
1064 pub overrides: Option<Vec<BuildOverride>>,
1067 pub cross_tool: Option<String>,
1070 pub mod_timestamp: Option<String>,
1073 pub command: Option<String>,
1076 #[serde(default, deserialize_with = "deserialize_string_or_bool_opt")]
1079 pub no_unique_dist_dir: Option<StringOrBool>,
1080}
1081
1082#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
1093#[serde(default)]
1094pub struct BuildHooksConfig {
1095 #[serde(alias = "before")]
1097 pub pre: Option<Vec<HookEntry>>,
1098 #[serde(alias = "after")]
1100 pub post: Option<Vec<HookEntry>>,
1101}
1102
1103#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
1110#[serde(default)]
1111pub struct ArchiveHooksConfig {
1112 #[serde(alias = "pre")]
1114 pub before: Option<Vec<HookEntry>>,
1115 #[serde(alias = "post")]
1117 pub after: Option<Vec<HookEntry>>,
1118}
1119
1120#[derive(Debug, Clone, JsonSchema)]
1125pub enum ArchivesConfig {
1126 Disabled,
1127 Configs(Vec<ArchiveConfig>),
1128}
1129
1130impl Serialize for ArchivesConfig {
1131 fn serialize<S: serde::Serializer>(
1132 &self,
1133 serializer: S,
1134 ) -> std::result::Result<S::Ok, S::Error> {
1135 match self {
1136 ArchivesConfig::Disabled => serializer.serialize_bool(false),
1137 ArchivesConfig::Configs(configs) => configs.serialize(serializer),
1138 }
1139 }
1140}
1141
1142impl Default for ArchivesConfig {
1143 fn default() -> Self {
1144 ArchivesConfig::Configs(vec![])
1145 }
1146}
1147
1148fn deserialize_archives_config<'de, D>(deserializer: D) -> Result<ArchivesConfig, D::Error>
1154where
1155 D: Deserializer<'de>,
1156{
1157 use serde::de::{self, Visitor};
1158
1159 struct ArchivesVisitor;
1160
1161 impl<'de> Visitor<'de> for ArchivesVisitor {
1162 type Value = ArchivesConfig;
1163
1164 fn expecting(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1165 f.write_str("false or a list of archive configs")
1166 }
1167
1168 fn visit_bool<E: de::Error>(self, v: bool) -> Result<Self::Value, E> {
1169 if !v {
1170 Ok(ArchivesConfig::Disabled)
1171 } else {
1172 Err(E::custom(
1173 "archives: true is not valid; use false or a list",
1174 ))
1175 }
1176 }
1177
1178 fn visit_seq<A: de::SeqAccess<'de>>(self, mut seq: A) -> Result<Self::Value, A::Error> {
1179 let mut configs = Vec::new();
1180 while let Some(item) = seq.next_element::<ArchiveConfig>()? {
1181 configs.push(item);
1182 }
1183 Ok(ArchivesConfig::Configs(configs))
1184 }
1185
1186 fn visit_unit<E: de::Error>(self) -> Result<Self::Value, E> {
1188 Ok(ArchivesConfig::Configs(vec![]))
1189 }
1190
1191 fn visit_none<E: de::Error>(self) -> Result<Self::Value, E> {
1192 Ok(ArchivesConfig::Configs(vec![]))
1193 }
1194 }
1195
1196 deserializer.deserialize_any(ArchivesVisitor)
1197}
1198
1199fn deserialize_signs<'de, D>(deserializer: D) -> Result<Vec<SignConfig>, D::Error>
1205where
1206 D: Deserializer<'de>,
1207{
1208 use serde::de::{self, Visitor};
1209
1210 struct SignsVisitor;
1211
1212 impl<'de> Visitor<'de> for SignsVisitor {
1213 type Value = Vec<SignConfig>;
1214
1215 fn expecting(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1216 f.write_str("a sign config object or an array of sign config objects")
1217 }
1218
1219 fn visit_seq<A: de::SeqAccess<'de>>(self, mut seq: A) -> Result<Self::Value, A::Error> {
1220 let mut configs = Vec::new();
1221 while let Some(item) = seq.next_element::<SignConfig>()? {
1222 configs.push(item);
1223 }
1224 Ok(configs)
1225 }
1226
1227 fn visit_map<M: de::MapAccess<'de>>(self, map: M) -> Result<Self::Value, M::Error> {
1228 let config = SignConfig::deserialize(de::value::MapAccessDeserializer::new(map))?;
1229 Ok(vec![config])
1230 }
1231
1232 fn visit_unit<E: de::Error>(self) -> Result<Self::Value, E> {
1233 Ok(Vec::new())
1234 }
1235
1236 fn visit_none<E: de::Error>(self) -> Result<Self::Value, E> {
1237 Ok(Vec::new())
1238 }
1239 }
1240
1241 deserializer.deserialize_any(SignsVisitor)
1242}
1243
1244#[derive(Debug, Clone, PartialEq, Serialize, JsonSchema)]
1249#[serde(untagged)]
1250pub enum WrapInDirectory {
1251 Bool(bool),
1252 Name(String),
1253}
1254
1255impl<'de> serde::Deserialize<'de> for WrapInDirectory {
1256 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
1257 let value = serde_yaml_ng::Value::deserialize(deserializer)?;
1258 match value {
1259 serde_yaml_ng::Value::Bool(b) => Ok(WrapInDirectory::Bool(b)),
1260 serde_yaml_ng::Value::String(s) => Ok(WrapInDirectory::Name(s)),
1261 _ => Err(serde::de::Error::custom("expected bool or string")),
1262 }
1263 }
1264}
1265
1266impl WrapInDirectory {
1267 pub fn directory_name(&self, default_name: &str) -> Option<String> {
1273 match self {
1274 WrapInDirectory::Bool(true) => Some(default_name.to_string()),
1275 WrapInDirectory::Bool(false) => None,
1276 WrapInDirectory::Name(s) if s.is_empty() => None,
1277 WrapInDirectory::Name(s) => Some(s.clone()),
1278 }
1279 }
1280}
1281
1282#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
1287#[serde(default)]
1288pub struct ArchiveConfig {
1289 pub id: Option<String>,
1291 pub name_template: Option<String>,
1293 pub format: Option<String>,
1295 pub formats: Option<Vec<String>>,
1297 pub format_overrides: Option<Vec<FormatOverride>>,
1299 pub files: Option<Vec<ArchiveFileSpec>>,
1301 pub binaries: Option<Vec<String>>,
1303 pub wrap_in_directory: Option<WrapInDirectory>,
1307 #[serde(alias = "builds")]
1312 pub ids: Option<Vec<String>>,
1313 pub meta: Option<bool>,
1315 pub builds_info: Option<ArchiveFileInfo>,
1317 pub strip_binary_directory: Option<bool>,
1319 pub allow_different_binary_count: Option<bool>,
1321 pub hooks: Option<ArchiveHooksConfig>,
1323}
1324
1325#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
1326pub struct FormatOverride {
1327 #[serde(alias = "goos")]
1330 pub os: String,
1331 pub format: Option<String>,
1333 pub formats: Option<Vec<String>>,
1335}
1336
1337#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
1348#[serde(untagged)]
1349pub enum ArchiveFileSpec {
1350 Glob(String),
1351 Detailed {
1352 src: String,
1353 dst: Option<String>,
1354 info: Option<ArchiveFileInfo>,
1355 strip_parent: Option<bool>,
1357 },
1358}
1359
1360impl PartialEq<&str> for ArchiveFileSpec {
1361 fn eq(&self, other: &&str) -> bool {
1362 match self {
1363 ArchiveFileSpec::Glob(s) => s.as_str() == *other,
1364 _ => false,
1365 }
1366 }
1367}
1368
1369#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default, JsonSchema)]
1373#[serde(default)]
1374pub struct FileInfo {
1375 pub owner: Option<String>,
1377 pub group: Option<String>,
1379 pub mode: Option<String>,
1381 pub mtime: Option<String>,
1383}
1384
1385pub type ArchiveFileInfo = FileInfo;
1387
1388pub fn parse_octal_mode(s: &str) -> Option<u32> {
1391 let cleaned = s
1392 .strip_prefix("0o")
1393 .or_else(|| s.strip_prefix("0O"))
1394 .unwrap_or(s);
1395 let cleaned = if cleaned.is_empty() { "0" } else { cleaned };
1396 u32::from_str_radix(cleaned, 8).ok()
1397}
1398
1399pub const VALID_ARCHIVE_FORMATS: &[&str] = &[
1403 "tar.gz", "tgz", "tar.xz", "txz", "tar.zst", "tzst", "tar", "zip", "gz", "binary", "none",
1404];
1405
1406#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
1415#[serde(untagged)]
1416pub enum ExtraFileSpec {
1417 Glob(String),
1418 Detailed {
1419 glob: String,
1420 #[serde(alias = "name", default)]
1423 name_template: Option<String>,
1424 },
1425}
1426
1427impl ExtraFileSpec {
1428 pub fn glob(&self) -> &str {
1430 match self {
1431 ExtraFileSpec::Glob(s) => s,
1432 ExtraFileSpec::Detailed { glob, .. } => glob,
1433 }
1434 }
1435
1436 pub fn name_template(&self) -> Option<&str> {
1438 match self {
1439 ExtraFileSpec::Glob(_) => None,
1440 ExtraFileSpec::Detailed { name_template, .. } => name_template.as_deref(),
1441 }
1442 }
1443}
1444
1445#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema, PartialEq)]
1448#[serde(default)]
1449pub struct TemplatedExtraFile {
1450 pub src: String,
1452 pub dst: Option<String>,
1455 pub mode: Option<String>,
1458}
1459
1460#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
1461#[serde(default)]
1462pub struct ChecksumConfig {
1463 pub name_template: Option<String>,
1465 pub algorithm: Option<String>,
1467 #[serde(deserialize_with = "deserialize_string_or_bool_opt", default)]
1469 pub disable: Option<StringOrBool>,
1470 pub extra_files: Option<Vec<ExtraFileSpec>>,
1472 pub templated_extra_files: Option<Vec<TemplatedExtraFile>>,
1476 pub ids: Option<Vec<String>>,
1478 pub split: Option<bool>,
1480}
1481
1482#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
1504#[serde(untagged)]
1505pub enum ContentSource {
1506 Inline(String),
1507 FromFile {
1508 from_file: String,
1509 },
1510 FromUrl {
1511 from_url: String,
1512 #[serde(default, skip_serializing_if = "Option::is_none")]
1515 headers: Option<HashMap<String, String>>,
1516 },
1517}
1518
1519impl PartialEq for ContentSource {
1520 fn eq(&self, other: &Self) -> bool {
1521 match (self, other) {
1522 (Self::Inline(a), Self::Inline(b)) => a == b,
1523 (Self::FromFile { from_file: a }, Self::FromFile { from_file: b }) => a == b,
1524 (
1525 Self::FromUrl {
1526 from_url: a,
1527 headers: ha,
1528 },
1529 Self::FromUrl {
1530 from_url: b,
1531 headers: hb,
1532 },
1533 ) => a == b && ha == hb,
1534 _ => false,
1535 }
1536 }
1537}
1538
1539#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
1544#[serde(default)]
1545pub struct ReleaseConfig {
1546 pub github: Option<ScmRepoConfig>,
1548 pub gitlab: Option<ScmRepoConfig>,
1550 pub gitea: Option<ScmRepoConfig>,
1552 pub draft: Option<bool>,
1554 #[schemars(schema_with = "prerelease_schema")]
1555 pub prerelease: Option<PrereleaseConfig>,
1557 #[schemars(schema_with = "make_latest_schema")]
1558 pub make_latest: Option<MakeLatestConfig>,
1560 pub name_template: Option<String>,
1562 pub header: Option<ContentSource>,
1564 pub footer: Option<ContentSource>,
1566 pub extra_files: Option<Vec<ExtraFileSpec>>,
1568 pub templated_extra_files: Option<Vec<TemplatedExtraFile>>,
1572 #[serde(deserialize_with = "deserialize_string_or_bool_opt", default)]
1575 pub skip_upload: Option<StringOrBool>,
1576 pub replace_existing_draft: Option<bool>,
1578 pub replace_existing_artifacts: Option<bool>,
1580 #[serde(deserialize_with = "deserialize_string_or_bool_opt", default)]
1584 pub disable: Option<StringOrBool>,
1585 pub mode: Option<String>,
1587 pub ids: Option<Vec<String>>,
1589 pub target_commitish: Option<String>,
1591 pub discussion_category_name: Option<String>,
1593 pub include_meta: Option<bool>,
1595 pub use_existing_draft: Option<bool>,
1597 pub tag: Option<String>,
1603}
1604
1605fn prerelease_schema(
1607 _generator: &mut schemars::r#gen::SchemaGenerator,
1608) -> schemars::schema::Schema {
1609 use schemars::schema::{InstanceType, Schema, SchemaObject, SingleOrVec, SubschemaValidation};
1610 Schema::Object(SchemaObject {
1611 subschemas: Some(Box::new(SubschemaValidation {
1612 one_of: Some(vec![
1613 Schema::Object(SchemaObject {
1614 instance_type: Some(SingleOrVec::Single(Box::new(InstanceType::String))),
1615 enum_values: Some(vec![serde_json::json!("auto")]),
1616 ..Default::default()
1617 }),
1618 Schema::Object(SchemaObject {
1619 instance_type: Some(SingleOrVec::Single(Box::new(InstanceType::Boolean))),
1620 ..Default::default()
1621 }),
1622 ]),
1623 ..Default::default()
1624 })),
1625 ..Default::default()
1626 })
1627}
1628
1629fn make_latest_schema(
1631 _generator: &mut schemars::r#gen::SchemaGenerator,
1632) -> schemars::schema::Schema {
1633 use schemars::schema::{InstanceType, Schema, SchemaObject, SingleOrVec, SubschemaValidation};
1634 Schema::Object(SchemaObject {
1635 subschemas: Some(Box::new(SubschemaValidation {
1636 one_of: Some(vec![
1637 Schema::Object(SchemaObject {
1638 instance_type: Some(SingleOrVec::Single(Box::new(InstanceType::String))),
1639 enum_values: Some(vec![serde_json::json!("auto")]),
1640 ..Default::default()
1641 }),
1642 Schema::Object(SchemaObject {
1643 instance_type: Some(SingleOrVec::Single(Box::new(InstanceType::Boolean))),
1644 ..Default::default()
1645 }),
1646 ]),
1647 ..Default::default()
1648 })),
1649 ..Default::default()
1650 })
1651}
1652
1653fn skip_push_schema(_generator: &mut schemars::r#gen::SchemaGenerator) -> schemars::schema::Schema {
1655 use schemars::schema::{InstanceType, Schema, SchemaObject, SingleOrVec, SubschemaValidation};
1656 Schema::Object(SchemaObject {
1657 subschemas: Some(Box::new(SubschemaValidation {
1658 one_of: Some(vec![
1659 Schema::Object(SchemaObject {
1660 instance_type: Some(SingleOrVec::Single(Box::new(InstanceType::String))),
1661 enum_values: Some(vec![serde_json::json!("auto")]),
1662 ..Default::default()
1663 }),
1664 Schema::Object(SchemaObject {
1665 instance_type: Some(SingleOrVec::Single(Box::new(InstanceType::Boolean))),
1666 ..Default::default()
1667 }),
1668 ]),
1669 ..Default::default()
1670 })),
1671 ..Default::default()
1672 })
1673}
1674
1675#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
1676pub struct ScmRepoConfig {
1677 pub owner: String,
1679 pub name: String,
1681}
1682
1683pub type GitHubConfig = ScmRepoConfig;
1685
1686#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
1692#[serde(rename_all = "lowercase")]
1693pub enum ForceTokenKind {
1694 GitHub,
1695 GitLab,
1696 Gitea,
1697}
1698
1699#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
1706#[serde(default, deny_unknown_fields)]
1707pub struct GitHubUrlsConfig {
1708 pub api: Option<String>,
1710 pub upload: Option<String>,
1712 pub download: Option<String>,
1714 pub skip_tls_verify: Option<bool>,
1716}
1717
1718#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
1721#[serde(default, deny_unknown_fields)]
1722pub struct GitLabUrlsConfig {
1723 pub api: Option<String>,
1725 pub download: Option<String>,
1727 pub skip_tls_verify: Option<bool>,
1729 pub use_package_registry: Option<bool>,
1731 pub use_job_token: Option<bool>,
1733}
1734
1735#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
1738#[serde(default, deny_unknown_fields)]
1739pub struct GiteaUrlsConfig {
1740 pub api: Option<String>,
1742 pub download: Option<String>,
1744 pub skip_tls_verify: Option<bool>,
1746}
1747
1748macro_rules! impl_auto_or_bool_serde {
1755 ($ty:ty, $auto:path, $bool_variant:path) => {
1756 impl Serialize for $ty {
1757 fn serialize<S: serde::Serializer>(
1758 &self,
1759 serializer: S,
1760 ) -> std::result::Result<S::Ok, S::Error> {
1761 match self {
1762 $auto => serializer.serialize_str("auto"),
1763 $bool_variant(b) => serializer.serialize_bool(*b),
1764 }
1765 }
1766 }
1767
1768 impl<'de> Deserialize<'de> for $ty {
1769 fn deserialize<D: serde::Deserializer<'de>>(
1770 deserializer: D,
1771 ) -> std::result::Result<Self, D::Error> {
1772 struct Visitor;
1773 impl serde::de::Visitor<'_> for Visitor {
1774 type Value = $ty;
1775 fn expecting(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1776 write!(f, "\"auto\" or a boolean")
1777 }
1778 fn visit_bool<E: serde::de::Error>(
1779 self,
1780 v: bool,
1781 ) -> std::result::Result<$ty, E> {
1782 Ok($bool_variant(v))
1783 }
1784 fn visit_str<E: serde::de::Error>(
1785 self,
1786 v: &str,
1787 ) -> std::result::Result<$ty, E> {
1788 if v == "auto" {
1789 Ok($auto)
1790 } else {
1791 Err(E::custom(format!("expected \"auto\", got \"{}\"", v)))
1792 }
1793 }
1794 }
1795 deserializer.deserialize_any(Visitor)
1796 }
1797 }
1798 };
1799}
1800
1801#[derive(Debug, Clone, PartialEq, Eq)]
1803pub enum PrereleaseConfig {
1804 Auto,
1805 Bool(bool),
1806}
1807
1808impl_auto_or_bool_serde!(
1809 PrereleaseConfig,
1810 PrereleaseConfig::Auto,
1811 PrereleaseConfig::Bool
1812);
1813
1814#[derive(Debug, Clone, PartialEq, Eq)]
1819pub enum MakeLatestConfig {
1820 Auto,
1821 Bool(bool),
1822 String(String),
1824}
1825
1826impl Serialize for MakeLatestConfig {
1827 fn serialize<S: serde::Serializer>(
1828 &self,
1829 serializer: S,
1830 ) -> std::result::Result<S::Ok, S::Error> {
1831 match self {
1832 MakeLatestConfig::Auto => serializer.serialize_str("auto"),
1833 MakeLatestConfig::Bool(b) => serializer.serialize_bool(*b),
1834 MakeLatestConfig::String(s) => serializer.serialize_str(s),
1835 }
1836 }
1837}
1838
1839impl<'de> Deserialize<'de> for MakeLatestConfig {
1840 fn deserialize<D: serde::Deserializer<'de>>(
1841 deserializer: D,
1842 ) -> std::result::Result<Self, D::Error> {
1843 struct Visitor;
1844 impl serde::de::Visitor<'_> for Visitor {
1845 type Value = MakeLatestConfig;
1846 fn expecting(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1847 write!(f, "\"auto\", a boolean, or a template string")
1848 }
1849 fn visit_bool<E: serde::de::Error>(
1850 self,
1851 v: bool,
1852 ) -> std::result::Result<MakeLatestConfig, E> {
1853 Ok(MakeLatestConfig::Bool(v))
1854 }
1855 fn visit_str<E: serde::de::Error>(
1856 self,
1857 v: &str,
1858 ) -> std::result::Result<MakeLatestConfig, E> {
1859 match v {
1860 "auto" => Ok(MakeLatestConfig::Auto),
1861 "true" => Ok(MakeLatestConfig::Bool(true)),
1862 "false" => Ok(MakeLatestConfig::Bool(false)),
1863 other => Ok(MakeLatestConfig::String(other.to_string())),
1864 }
1865 }
1866 }
1867 deserializer.deserialize_any(Visitor)
1868 }
1869}
1870
1871#[derive(Debug, Clone, PartialEq, Eq)]
1874pub enum SkipPushConfig {
1875 Auto,
1876 Bool(bool),
1877 Template(String),
1879}
1880
1881impl Serialize for SkipPushConfig {
1882 fn serialize<S: serde::Serializer>(
1883 &self,
1884 serializer: S,
1885 ) -> std::result::Result<S::Ok, S::Error> {
1886 match self {
1887 SkipPushConfig::Auto => serializer.serialize_str("auto"),
1888 SkipPushConfig::Bool(b) => serializer.serialize_bool(*b),
1889 SkipPushConfig::Template(s) => serializer.serialize_str(s),
1890 }
1891 }
1892}
1893
1894impl<'de> Deserialize<'de> for SkipPushConfig {
1895 fn deserialize<D: serde::Deserializer<'de>>(
1896 deserializer: D,
1897 ) -> std::result::Result<Self, D::Error> {
1898 struct Visitor;
1899 impl serde::de::Visitor<'_> for Visitor {
1900 type Value = SkipPushConfig;
1901 fn expecting(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1902 write!(f, "\"auto\", a boolean, or a template string")
1903 }
1904 fn visit_bool<E: serde::de::Error>(
1905 self,
1906 v: bool,
1907 ) -> std::result::Result<SkipPushConfig, E> {
1908 Ok(SkipPushConfig::Bool(v))
1909 }
1910 fn visit_str<E: serde::de::Error>(
1911 self,
1912 v: &str,
1913 ) -> std::result::Result<SkipPushConfig, E> {
1914 match v {
1915 "auto" => Ok(SkipPushConfig::Auto),
1916 "true" => Ok(SkipPushConfig::Bool(true)),
1917 "false" => Ok(SkipPushConfig::Bool(false)),
1918 other => Ok(SkipPushConfig::Template(other.to_string())),
1919 }
1920 }
1921 }
1922 deserializer.deserialize_any(Visitor)
1923 }
1924}
1925
1926#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
1933#[serde(default)]
1934pub struct RepositoryConfig {
1935 pub owner: Option<String>,
1937 pub name: Option<String>,
1939 pub token: Option<String>,
1941 pub token_type: Option<String>,
1943 pub branch: Option<String>,
1945 pub git: Option<GitRepoConfig>,
1947 pub pull_request: Option<PullRequestConfig>,
1949}
1950
1951#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
1953#[serde(default)]
1954pub struct GitRepoConfig {
1955 pub url: Option<String>,
1957 pub ssh_command: Option<String>,
1959 pub private_key: Option<String>,
1961}
1962
1963#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
1965#[serde(default)]
1966pub struct PullRequestConfig {
1967 pub enabled: Option<bool>,
1969 pub draft: Option<bool>,
1971 pub body: Option<String>,
1973 pub base: Option<PullRequestBaseConfig>,
1975}
1976
1977#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
1979#[serde(default)]
1980pub struct PullRequestBaseConfig {
1981 pub owner: Option<String>,
1983 pub name: Option<String>,
1985 pub branch: Option<String>,
1987}
1988
1989#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
1992#[serde(default)]
1993pub struct CommitAuthorConfig {
1994 pub name: Option<String>,
1996 pub email: Option<String>,
1998 pub signing: Option<CommitSigningConfig>,
2000}
2001
2002impl CommitAuthorConfig {
2003 pub fn normalize_defaults(&mut self) {
2008 if self.name.as_deref().is_none_or(str::is_empty) {
2009 self.name = Some("anodizer".to_string());
2010 }
2011 if self.email.as_deref().is_none_or(str::is_empty) {
2012 self.email = Some("bot@anodizer.dev".to_string());
2013 }
2014 }
2015}
2016
2017#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
2019#[serde(default)]
2020pub struct CommitSigningConfig {
2021 pub enabled: Option<bool>,
2023 pub key: Option<String>,
2025 pub program: Option<String>,
2027 pub format: Option<String>,
2029}
2030
2031#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
2036#[serde(default)]
2037pub struct PublishConfig {
2038 #[schemars(schema_with = "crates_publish_schema")]
2039 pub crates: Option<CratesPublishConfig>,
2041 pub homebrew: Option<HomebrewConfig>,
2043 pub scoop: Option<ScoopConfig>,
2045 pub chocolatey: Option<ChocolateyConfig>,
2047 pub winget: Option<WingetConfig>,
2049 pub aur: Option<AurConfig>,
2051 pub aur_source: Option<AurSourceConfig>,
2053 pub krew: Option<KrewConfig>,
2055 pub nix: Option<NixConfig>,
2057}
2058
2059fn crates_publish_schema(
2061 _generator: &mut schemars::r#gen::SchemaGenerator,
2062) -> schemars::schema::Schema {
2063 schemars::schema::Schema::Bool(true)
2064}
2065
2066impl PublishConfig {
2067 pub fn crates_config(&self) -> CratesPublishSettings {
2068 match &self.crates {
2069 None => CratesPublishSettings::default(),
2070 Some(CratesPublishConfig::Bool(enabled)) => CratesPublishSettings {
2071 enabled: *enabled,
2072 index_timeout: 300,
2073 },
2074 Some(CratesPublishConfig::Object {
2075 enabled,
2076 index_timeout,
2077 }) => CratesPublishSettings {
2078 enabled: *enabled,
2079 index_timeout: *index_timeout,
2080 },
2081 }
2082 }
2083}
2084
2085#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
2087#[serde(untagged)]
2088pub enum CratesPublishConfig {
2089 Bool(bool),
2090 Object {
2091 enabled: bool,
2092 #[serde(default = "default_index_timeout")]
2093 index_timeout: u64,
2094 },
2095}
2096
2097fn default_index_timeout() -> u64 {
2098 300
2099}
2100
2101#[derive(Debug, Clone)]
2103pub struct CratesPublishSettings {
2104 pub enabled: bool,
2105 pub index_timeout: u64,
2106}
2107
2108impl Default for CratesPublishSettings {
2109 fn default() -> Self {
2110 CratesPublishSettings {
2111 enabled: false,
2112 index_timeout: 300,
2113 }
2114 }
2115}
2116
2117#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
2122#[serde(default)]
2123pub struct HomebrewConfig {
2124 pub tap: Option<TapConfig>,
2126 pub repository: Option<RepositoryConfig>,
2128 pub commit_author: Option<CommitAuthorConfig>,
2130 pub directory: Option<String>,
2132 pub name: Option<String>,
2134 pub description: Option<String>,
2136 pub license: Option<String>,
2138 pub install: Option<String>,
2140 pub extra_install: Option<String>,
2142 pub post_install: Option<String>,
2144 pub test: Option<String>,
2146 pub homepage: Option<String>,
2148 pub dependencies: Option<Vec<HomebrewDependency>>,
2150 pub conflicts: Option<Vec<HomebrewConflict>>,
2152 pub caveats: Option<String>,
2154 #[serde(deserialize_with = "deserialize_string_or_bool_opt", default)]
2157 pub skip_upload: Option<StringOrBool>,
2158 pub commit_msg_template: Option<String>,
2161 pub commit_author_name: Option<String>,
2163 pub commit_author_email: Option<String>,
2165 pub ids: Option<Vec<String>>,
2167 pub url_template: Option<String>,
2169 pub url_headers: Option<Vec<String>>,
2171 pub download_strategy: Option<String>,
2173 pub custom_require: Option<String>,
2175 pub custom_block: Option<String>,
2177 pub plist: Option<String>,
2179 pub service: Option<String>,
2181 pub cask: Option<HomebrewCaskConfig>,
2183 #[serde(alias = "goamd64")]
2186 pub amd64_variant: Option<String>,
2187 #[serde(alias = "goarm")]
2190 pub arm_variant: Option<String>,
2191}
2192
2193#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, JsonSchema)]
2194#[serde(default)]
2195pub struct HomebrewDependency {
2196 pub name: String,
2198 #[serde(skip_serializing_if = "Option::is_none")]
2200 pub os: Option<String>,
2201 #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
2203 pub dep_type: Option<String>,
2204 #[serde(skip_serializing_if = "Option::is_none")]
2206 pub version: Option<String>,
2207}
2208
2209#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, JsonSchema)]
2212#[serde(untagged)]
2213pub enum HomebrewConflict {
2214 Name(String),
2216 WithReason {
2218 name: String,
2219 #[serde(skip_serializing_if = "Option::is_none")]
2220 because: Option<String>,
2221 },
2222}
2223
2224impl HomebrewConflict {
2225 pub fn name(&self) -> &str {
2226 match self {
2227 Self::Name(n) => n,
2228 Self::WithReason { name, .. } => name,
2229 }
2230 }
2231 pub fn because(&self) -> Option<&str> {
2232 match self {
2233 Self::Name(_) => None,
2234 Self::WithReason { because, .. } => because.as_deref(),
2235 }
2236 }
2237}
2238
2239#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
2241#[serde(default)]
2242pub struct HomebrewCaskConfig {
2243 pub name: Option<String>,
2245 pub alternative_names: Option<Vec<String>>,
2247 pub app: Option<String>,
2249 pub binaries: Option<Vec<String>>,
2251 pub description: Option<String>,
2253 pub homepage: Option<String>,
2255 pub url_template: Option<String>,
2257 pub caveats: Option<String>,
2259 pub zap: Option<Vec<String>>,
2261 pub uninstall: Option<Vec<String>>,
2263 pub custom_block: Option<String>,
2265 pub service: Option<String>,
2267 pub license: Option<String>,
2269 pub manpages: Option<Vec<String>>,
2271 pub completions: Option<HomebrewCaskCompletions>,
2273 pub dependencies: Option<Vec<HomebrewCaskDependencyEntry>>,
2275 pub conflicts: Option<Vec<HomebrewCaskConflictEntry>>,
2277 pub hooks: Option<HomebrewCaskHooks>,
2279}
2280
2281#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
2289#[serde(default)]
2290pub struct TopLevelHomebrewCaskConfig {
2291 pub name: Option<String>,
2293 pub repository: Option<RepositoryConfig>,
2295 pub commit_author: Option<CommitAuthorConfig>,
2297 pub commit_msg_template: Option<String>,
2300 pub directory: Option<String>,
2302 pub description: Option<String>,
2304 pub homepage: Option<String>,
2306 #[serde(deserialize_with = "deserialize_string_or_bool_opt", default)]
2309 pub skip_upload: Option<StringOrBool>,
2310 pub custom_block: Option<String>,
2312 pub ids: Option<Vec<String>>,
2314 pub service: Option<String>,
2316 pub binaries: Option<Vec<String>>,
2318 pub manpages: Option<Vec<String>>,
2320 pub caveats: Option<String>,
2322 pub license: Option<String>,
2324 pub url: Option<HomebrewCaskURL>,
2326 pub completions: Option<HomebrewCaskCompletions>,
2328 pub dependencies: Option<Vec<HomebrewCaskDependencyEntry>>,
2330 pub conflicts: Option<Vec<HomebrewCaskConflictEntry>>,
2332 pub hooks: Option<HomebrewCaskHooks>,
2334 pub uninstall: Option<HomebrewCaskUninstall>,
2336 pub zap: Option<HomebrewCaskUninstall>,
2338 pub generate_completions_from_executable: Option<HomebrewCaskGeneratedCompletions>,
2340 pub app: Option<String>,
2342 pub alternative_names: Option<Vec<String>>,
2344}
2345
2346#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
2348#[serde(default)]
2349pub struct HomebrewCaskURL {
2350 pub template: Option<String>,
2352 pub verified: Option<String>,
2354 pub using: Option<String>,
2356 pub cookies: Option<HashMap<String, String>>,
2358 pub referer: Option<String>,
2360 pub headers: Option<Vec<String>>,
2362 pub user_agent: Option<String>,
2364 pub data: Option<HashMap<String, String>>,
2366}
2367
2368#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
2371#[serde(default)]
2372pub struct HomebrewCaskUninstall {
2373 pub launchctl: Option<Vec<String>>,
2375 pub quit: Option<Vec<String>>,
2377 pub login_item: Option<Vec<String>>,
2379 pub delete: Option<Vec<String>>,
2381 pub trash: Option<Vec<String>>,
2383}
2384
2385#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
2387#[serde(default)]
2388pub struct HomebrewCaskHooks {
2389 pub pre: Option<HomebrewCaskHook>,
2391 pub post: Option<HomebrewCaskHook>,
2393}
2394
2395#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
2397#[serde(default)]
2398pub struct HomebrewCaskHook {
2399 pub install: Option<String>,
2401 pub uninstall: Option<String>,
2403}
2404
2405#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
2407#[serde(default)]
2408pub struct HomebrewCaskCompletions {
2409 pub bash: Option<String>,
2411 pub zsh: Option<String>,
2413 pub fish: Option<String>,
2415}
2416
2417#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
2419#[serde(default)]
2420pub struct HomebrewCaskDependencyEntry {
2421 pub cask: Option<String>,
2423 pub formula: Option<String>,
2425}
2426
2427#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
2429#[serde(default)]
2430pub struct HomebrewCaskConflictEntry {
2431 pub cask: Option<String>,
2433 pub formula: Option<String>,
2435}
2436
2437#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
2439#[serde(default)]
2440pub struct HomebrewCaskGeneratedCompletions {
2441 pub executable: Option<String>,
2443 pub args: Option<Vec<String>>,
2445 pub base_name: Option<String>,
2447 pub shell_parameter_format: Option<String>,
2449 pub shells: Option<Vec<String>>,
2451}
2452
2453#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
2454#[serde(default)]
2455pub struct ScoopConfig {
2456 pub bucket: Option<BucketConfig>,
2458 pub repository: Option<RepositoryConfig>,
2460 pub commit_author: Option<CommitAuthorConfig>,
2462 pub name: Option<String>,
2464 pub directory: Option<String>,
2466 pub description: Option<String>,
2468 pub license: Option<String>,
2470 pub homepage: Option<String>,
2472 pub persist: Option<Vec<String>>,
2474 pub depends: Option<Vec<String>>,
2476 pub pre_install: Option<Vec<String>>,
2478 pub post_install: Option<Vec<String>>,
2480 pub shortcuts: Option<Vec<Vec<String>>>,
2482 #[serde(deserialize_with = "deserialize_string_or_bool_opt", default)]
2485 pub skip_upload: Option<StringOrBool>,
2486 pub commit_msg_template: Option<String>,
2488 pub commit_author_name: Option<String>,
2490 pub commit_author_email: Option<String>,
2492 pub ids: Option<Vec<String>>,
2494 pub url_template: Option<String>,
2496 #[serde(rename = "use")]
2498 pub use_artifact: Option<String>,
2499 #[serde(alias = "goamd64")]
2502 pub amd64_variant: Option<String>,
2503}
2504
2505#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
2506pub struct TapConfig {
2507 pub owner: String,
2509 pub name: String,
2511}
2512
2513#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
2514pub struct BucketConfig {
2515 pub owner: String,
2517 pub name: String,
2519}
2520
2521#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
2526#[serde(default)]
2527pub struct ChocolateyConfig {
2528 pub name: Option<String>,
2530 pub ids: Option<Vec<String>>,
2532 pub project_repo: Option<ChocolateyRepoConfig>,
2534 pub package_source_url: Option<String>,
2536 pub owners: Option<String>,
2538 pub title: Option<String>,
2540 pub authors: Option<String>,
2542 pub project_url: Option<String>,
2544 pub url_template: Option<String>,
2546 pub icon_url: Option<String>,
2548 pub copyright: Option<String>,
2550 pub description: Option<String>,
2552 pub license: Option<String>,
2554 pub license_url: Option<String>,
2557 pub require_license_acceptance: Option<bool>,
2559 pub project_source_url: Option<String>,
2561 pub docs_url: Option<String>,
2563 pub bug_tracker_url: Option<String>,
2565 #[serde(
2568 deserialize_with = "deserialize_space_separated_string_or_vec_opt",
2569 default
2570 )]
2571 pub tags: Option<Vec<String>>,
2572 pub summary: Option<String>,
2574 pub release_notes: Option<String>,
2576 pub dependencies: Option<Vec<ChocolateyDependency>>,
2578 pub api_key: Option<String>,
2580 pub source_repo: Option<String>,
2582 pub skip_publish: Option<bool>,
2584 #[serde(deserialize_with = "deserialize_string_or_bool_opt", default)]
2586 pub disable: Option<StringOrBool>,
2587 #[serde(rename = "use")]
2589 pub use_artifact: Option<String>,
2590 #[serde(alias = "goamd64")]
2593 pub amd64_variant: Option<String>,
2594}
2595
2596#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
2598#[serde(default)]
2599pub struct ChocolateyDependency {
2600 pub id: String,
2602 pub version: Option<String>,
2604}
2605
2606#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
2607pub struct ChocolateyRepoConfig {
2608 pub owner: String,
2610 pub name: String,
2612}
2613
2614#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
2619#[serde(default)]
2620pub struct WingetConfig {
2621 pub name: Option<String>,
2623 pub package_name: Option<String>,
2625 pub package_identifier: Option<String>,
2627 pub publisher: Option<String>,
2629 pub publisher_url: Option<String>,
2631 pub publisher_support_url: Option<String>,
2633 pub privacy_url: Option<String>,
2635 pub author: Option<String>,
2637 pub copyright: Option<String>,
2639 pub copyright_url: Option<String>,
2641 pub license: Option<String>,
2643 pub license_url: Option<String>,
2645 pub short_description: Option<String>,
2647 pub description: Option<String>,
2649 pub homepage: Option<String>,
2651 pub url_template: Option<String>,
2653 pub ids: Option<Vec<String>>,
2655 #[serde(deserialize_with = "deserialize_string_or_bool_opt", default)]
2658 pub skip_upload: Option<StringOrBool>,
2659 pub commit_msg_template: Option<String>,
2661 pub path: Option<String>,
2663 pub release_notes: Option<String>,
2665 pub release_notes_url: Option<String>,
2667 pub installation_notes: Option<String>,
2669 pub tags: Option<Vec<String>>,
2671 pub dependencies: Option<Vec<WingetDependency>>,
2673 pub manifests_repo: Option<WingetManifestsRepoConfig>,
2675 pub repository: Option<RepositoryConfig>,
2677 pub commit_author: Option<CommitAuthorConfig>,
2679 pub product_code: Option<String>,
2681 #[serde(rename = "use")]
2683 pub use_artifact: Option<String>,
2684 #[serde(alias = "goamd64")]
2687 pub amd64_variant: Option<String>,
2688}
2689
2690#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
2692#[serde(default)]
2693pub struct WingetDependency {
2694 pub package_identifier: String,
2696 pub minimum_version: Option<String>,
2698}
2699
2700#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
2701pub struct WingetManifestsRepoConfig {
2702 pub owner: String,
2704 pub name: String,
2706}
2707
2708#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
2713#[serde(default)]
2714pub struct AurConfig {
2715 #[serde(alias = "package_name")]
2717 pub name: Option<String>,
2718 pub ids: Option<Vec<String>>,
2720 pub commit_author: Option<CommitAuthorConfig>,
2722 pub commit_msg_template: Option<String>,
2724 pub description: Option<String>,
2726 pub homepage: Option<String>,
2728 pub license: Option<String>,
2730 #[serde(deserialize_with = "deserialize_string_or_bool_opt", default)]
2733 pub skip_upload: Option<StringOrBool>,
2734 pub url_template: Option<String>,
2736 pub maintainers: Option<Vec<String>>,
2738 pub contributors: Option<Vec<String>>,
2740 pub provides: Option<Vec<String>>,
2742 pub conflicts: Option<Vec<String>>,
2744 pub depends: Option<Vec<String>>,
2746 pub optdepends: Option<Vec<String>>,
2748 pub backup: Option<Vec<String>>,
2750 pub rel: Option<String>,
2752 #[serde(alias = "install_template")]
2754 pub package: Option<String>,
2755 pub git_url: Option<String>,
2757 pub git_ssh_command: Option<String>,
2759 pub private_key: Option<String>,
2761 pub directory: Option<String>,
2763 #[serde(deserialize_with = "deserialize_string_or_bool_opt", default)]
2766 pub disable: Option<StringOrBool>,
2767 pub install: Option<String>,
2769 pub url: Option<String>,
2771 pub replaces: Option<Vec<String>>,
2773 #[serde(alias = "goamd64")]
2776 pub amd64_variant: Option<String>,
2777}
2778
2779#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
2784#[serde(default)]
2785pub struct KrewConfig {
2786 pub name: Option<String>,
2788 pub ids: Option<Vec<String>>,
2790 pub manifests_repo: Option<KrewManifestsRepoConfig>,
2792 pub repository: Option<RepositoryConfig>,
2794 pub commit_author: Option<CommitAuthorConfig>,
2796 pub commit_msg_template: Option<String>,
2798 pub description: Option<String>,
2800 pub short_description: Option<String>,
2802 pub homepage: Option<String>,
2804 pub url_template: Option<String>,
2806 pub caveats: Option<String>,
2808 #[serde(deserialize_with = "deserialize_string_or_bool_opt", default)]
2811 pub skip_upload: Option<StringOrBool>,
2812 #[serde(deserialize_with = "deserialize_string_or_bool_opt", default)]
2816 pub disable: Option<StringOrBool>,
2817 pub upstream_repo: Option<KrewManifestsRepoConfig>,
2819 #[serde(alias = "goamd64")]
2822 pub amd64_variant: Option<String>,
2823 #[serde(alias = "goarm")]
2826 pub arm_variant: Option<String>,
2827}
2828
2829#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
2830pub struct KrewManifestsRepoConfig {
2831 pub owner: String,
2833 pub name: String,
2835}
2836
2837#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
2842#[serde(default)]
2843pub struct NixConfig {
2844 pub name: Option<String>,
2846 pub path: Option<String>,
2848 pub repository: Option<RepositoryConfig>,
2850 pub commit_author: Option<CommitAuthorConfig>,
2852 pub commit_msg_template: Option<String>,
2854 pub ids: Option<Vec<String>>,
2856 pub url_template: Option<String>,
2858 #[serde(deserialize_with = "deserialize_string_or_bool_opt", default)]
2861 pub skip_upload: Option<StringOrBool>,
2862 pub install: Option<String>,
2864 pub extra_install: Option<String>,
2866 pub post_install: Option<String>,
2868 pub description: Option<String>,
2870 pub homepage: Option<String>,
2872 pub license: Option<String>,
2874 pub dependencies: Option<Vec<NixDependency>>,
2876 pub formatter: Option<String>,
2878 #[serde(alias = "goamd64")]
2881 pub amd64_variant: Option<String>,
2882}
2883
2884#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
2886#[serde(default)]
2887pub struct NixDependency {
2888 pub name: String,
2890 pub os: Option<String>,
2892}
2893
2894#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
2899#[serde(default)]
2900pub struct DockerConfig {
2901 pub id: Option<String>,
2903 pub image_templates: Vec<String>,
2905 pub dockerfile: String,
2907 pub platforms: Option<Vec<String>>,
2909 pub binaries: Option<Vec<String>>,
2911 pub build_flag_templates: Option<Vec<String>>,
2913 #[schemars(schema_with = "skip_push_schema")]
2915 pub skip_push: Option<SkipPushConfig>,
2916 pub extra_files: Option<Vec<String>>,
2918 pub templated_extra_files: Option<Vec<TemplatedExtraFile>>,
2922 pub push_flags: Option<Vec<String>>,
2924 pub ids: Option<Vec<String>>,
2926 pub labels: Option<HashMap<String, String>>,
2928 pub retry: Option<DockerRetryConfig>,
2930 #[serde(rename = "use")]
2932 pub use_backend: Option<String>,
2933 #[serde(deserialize_with = "deserialize_string_or_bool_opt", default)]
2935 pub disable: Option<StringOrBool>,
2936}
2937
2938#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
2939#[serde(default)]
2940pub struct DockerRetryConfig {
2941 pub attempts: Option<u32>,
2943 pub delay: Option<String>,
2945 pub max_delay: Option<String>,
2947}
2948
2949#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
2964#[serde(default)]
2965pub struct DockerV2Config {
2966 pub id: Option<String>,
2968 pub ids: Option<Vec<String>>,
2970 pub dockerfile: String,
2972 pub images: Vec<String>,
2974 pub tags: Vec<String>,
2976 pub labels: Option<HashMap<String, String>>,
2978 pub annotations: Option<HashMap<String, String>>,
2980 pub extra_files: Option<Vec<String>>,
2982 pub platforms: Option<Vec<String>>,
2984 pub build_args: Option<HashMap<String, String>>,
2986 pub retry: Option<DockerRetryConfig>,
2988 pub flags: Option<Vec<String>>,
2990 #[serde(deserialize_with = "deserialize_string_or_bool_opt", default)]
2992 pub disable: Option<StringOrBool>,
2993 #[serde(deserialize_with = "deserialize_string_or_bool_opt", default)]
2995 pub sbom: Option<StringOrBool>,
2996 #[serde(deserialize_with = "deserialize_string_or_bool_opt", default)]
2998 pub skip_push: Option<StringOrBool>,
2999}
3000
3001#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
3011#[serde(default)]
3012pub struct DockerDigestConfig {
3013 #[serde(deserialize_with = "deserialize_string_or_bool_opt", default)]
3015 pub disable: Option<StringOrBool>,
3016 pub name_template: Option<String>,
3019}
3020
3021#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
3026#[serde(default)]
3027pub struct DockerManifestConfig {
3028 pub name_template: String,
3030 pub image_templates: Vec<String>,
3032 pub create_flags: Option<Vec<String>>,
3034 pub push_flags: Option<Vec<String>>,
3036 #[schemars(schema_with = "skip_push_schema")]
3038 pub skip_push: Option<SkipPushConfig>,
3039 pub id: Option<String>,
3041 #[serde(rename = "use")]
3043 pub use_backend: Option<String>,
3044 pub retry: Option<DockerRetryConfig>,
3046 #[serde(deserialize_with = "deserialize_string_or_bool_opt", default)]
3048 pub disable: Option<StringOrBool>,
3049}
3050
3051#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
3056#[serde(default)]
3057pub struct NfpmConfig {
3058 pub id: Option<String>,
3060 pub package_name: Option<String>,
3062 pub formats: Vec<String>,
3064 pub vendor: Option<String>,
3066 pub homepage: Option<String>,
3068 pub maintainer: Option<String>,
3070 pub description: Option<String>,
3072 pub license: Option<String>,
3074 pub bindir: Option<String>,
3076 pub contents: Option<Vec<NfpmContent>>,
3078 pub dependencies: Option<HashMap<String, Vec<String>>>,
3080 pub overrides: Option<HashMap<String, serde_json::Value>>,
3082 pub file_name_template: Option<String>,
3084 pub scripts: Option<NfpmScripts>,
3086 pub recommends: Option<Vec<String>>,
3088 pub suggests: Option<Vec<String>>,
3090 pub conflicts: Option<Vec<String>>,
3092 pub replaces: Option<Vec<String>>,
3094 pub provides: Option<Vec<String>>,
3096 #[serde(alias = "builds")]
3098 pub ids: Option<Vec<String>>,
3099 pub epoch: Option<String>,
3101 pub release: Option<String>,
3103 pub prerelease: Option<String>,
3105 pub version_metadata: Option<String>,
3107 pub section: Option<String>,
3109 pub priority: Option<String>,
3111 pub meta: Option<bool>,
3113 pub umask: Option<String>,
3115 pub mtime: Option<String>,
3117 pub rpm: Option<NfpmRpmConfig>,
3119 pub deb: Option<NfpmDebConfig>,
3121 pub apk: Option<NfpmApkConfig>,
3123 pub archlinux: Option<NfpmArchlinuxConfig>,
3125 pub ipk: Option<NfpmIpkConfig>,
3127 pub libdirs: Option<NfpmLibdirs>,
3129 pub changelog: Option<String>,
3131 #[serde(rename = "if")]
3134 pub if_condition: Option<String>,
3135 pub templated_contents: Option<Vec<NfpmContent>>,
3141 pub templated_scripts: Option<NfpmScripts>,
3146}
3147
3148#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
3153#[serde(default)]
3154pub struct NfpmLibdirs {
3155 pub header: Option<String>,
3157 pub carchive: Option<String>,
3159 pub cshared: Option<String>,
3161}
3162
3163#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
3164#[serde(default)]
3165pub struct NfpmScripts {
3166 pub preinstall: Option<String>,
3168 pub postinstall: Option<String>,
3170 pub preremove: Option<String>,
3172 pub postremove: Option<String>,
3174}
3175
3176pub type NfpmFileInfo = FileInfo;
3178
3179#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
3185pub struct NfpmContent {
3186 pub src: String,
3188 pub dst: String,
3190 #[serde(rename = "type")]
3192 pub content_type: Option<String>,
3193 pub file_info: Option<NfpmFileInfo>,
3195 pub packager: Option<String>,
3198 pub expand: Option<bool>,
3200}
3201
3202#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
3207#[serde(default)]
3208pub struct NfpmRpmConfig {
3209 pub summary: Option<String>,
3211 pub compression: Option<String>,
3213 pub group: Option<String>,
3215 pub packager: Option<String>,
3217 pub prefixes: Option<Vec<String>>,
3219 pub signature: Option<NfpmSignatureConfig>,
3221 pub scripts: Option<NfpmRpmScripts>,
3223 pub build_host: Option<String>,
3225}
3226
3227#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
3229#[serde(default)]
3230pub struct NfpmRpmScripts {
3231 pub pretrans: Option<String>,
3233 pub posttrans: Option<String>,
3235}
3236
3237impl NfpmRpmConfig {
3238 pub fn is_empty(&self) -> bool {
3241 self.summary.is_none()
3242 && self.compression.is_none()
3243 && self.group.is_none()
3244 && self.packager.is_none()
3245 && self.prefixes.is_none()
3246 && self.signature.is_none()
3247 && self.scripts.is_none()
3248 && self.build_host.is_none()
3249 }
3250}
3251
3252#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
3253#[serde(default)]
3254pub struct NfpmDebConfig {
3255 pub compression: Option<String>,
3257 pub predepends: Option<Vec<String>>,
3259 pub triggers: Option<NfpmDebTriggers>,
3261 pub breaks: Option<Vec<String>>,
3263 pub lintian_overrides: Option<Vec<String>>,
3265 pub signature: Option<NfpmSignatureConfig>,
3267 pub fields: Option<HashMap<String, String>>,
3269 pub scripts: Option<NfpmDebScripts>,
3271 pub arch_variant: Option<String>,
3275}
3276
3277#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
3279#[serde(default)]
3280pub struct NfpmDebScripts {
3281 pub rules: Option<String>,
3283 pub templates: Option<String>,
3285 pub config: Option<String>,
3287}
3288
3289impl NfpmDebConfig {
3290 pub fn is_empty(&self) -> bool {
3293 self.compression.is_none()
3294 && self.predepends.is_none()
3295 && self.triggers.is_none()
3296 && self.breaks.is_none()
3297 && self.lintian_overrides.is_none()
3298 && self.signature.is_none()
3299 && self.fields.is_none()
3300 && self.scripts.is_none()
3301 }
3302}
3303
3304#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
3305#[serde(default)]
3306pub struct NfpmDebTriggers {
3307 pub interest: Option<Vec<String>>,
3309 pub interest_await: Option<Vec<String>>,
3311 pub interest_noawait: Option<Vec<String>>,
3313 pub activate: Option<Vec<String>>,
3315 pub activate_await: Option<Vec<String>>,
3317 pub activate_noawait: Option<Vec<String>>,
3319}
3320
3321#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
3322#[serde(default)]
3323pub struct NfpmApkConfig {
3324 pub signature: Option<NfpmSignatureConfig>,
3326 pub scripts: Option<NfpmApkScripts>,
3328}
3329
3330#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
3332#[serde(default)]
3333pub struct NfpmApkScripts {
3334 pub preupgrade: Option<String>,
3336 pub postupgrade: Option<String>,
3338}
3339
3340impl NfpmApkConfig {
3341 pub fn is_empty(&self) -> bool {
3344 self.signature.is_none() && self.scripts.is_none()
3345 }
3346}
3347
3348#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
3349#[serde(default)]
3350pub struct NfpmArchlinuxConfig {
3351 pub pkgbase: Option<String>,
3353 pub packager: Option<String>,
3355 pub scripts: Option<NfpmArchlinuxScripts>,
3357}
3358
3359impl NfpmArchlinuxConfig {
3360 pub fn is_empty(&self) -> bool {
3363 self.pkgbase.is_none() && self.packager.is_none() && self.scripts.is_none()
3364 }
3365}
3366
3367#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
3368#[serde(default)]
3369pub struct NfpmArchlinuxScripts {
3370 pub preupgrade: Option<String>,
3372 pub postupgrade: Option<String>,
3374}
3375
3376#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
3378#[serde(default)]
3379pub struct NfpmIpkConfig {
3380 pub abi_version: Option<String>,
3382 pub alternatives: Option<Vec<NfpmIpkAlternative>>,
3384 pub auto_installed: Option<bool>,
3386 pub essential: Option<bool>,
3388 pub predepends: Option<Vec<String>>,
3390 pub tags: Option<Vec<String>>,
3392 pub fields: Option<HashMap<String, String>>,
3394}
3395
3396impl NfpmIpkConfig {
3397 pub fn is_empty(&self) -> bool {
3400 self.abi_version.is_none()
3401 && self.alternatives.is_none()
3402 && self.auto_installed.is_none()
3403 && self.essential.is_none()
3404 && self.predepends.is_none()
3405 && self.tags.is_none()
3406 && self.fields.is_none()
3407 }
3408}
3409
3410#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
3412#[serde(default)]
3413pub struct NfpmIpkAlternative {
3414 pub priority: Option<i32>,
3416 pub target: Option<String>,
3418 pub link_name: Option<String>,
3420}
3421
3422#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
3423#[serde(default)]
3424pub struct NfpmSignatureConfig {
3425 pub key_file: Option<String>,
3427 pub key_id: Option<String>,
3429 pub key_passphrase: Option<String>,
3431 pub key_name: Option<String>,
3433 #[serde(rename = "type")]
3435 pub type_: Option<String>,
3436}
3437
3438#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
3443#[serde(default)]
3444pub struct SnapcraftConfig {
3445 pub id: Option<String>,
3447 #[serde(alias = "builds")]
3449 pub ids: Option<Vec<String>>,
3450 pub name: Option<String>,
3452 pub title: Option<String>,
3454 pub summary: Option<String>,
3456 pub description: Option<String>,
3458 pub icon: Option<String>,
3460 pub base: Option<String>,
3462 pub grade: Option<String>,
3464 pub license: Option<String>,
3466 pub publish: Option<bool>,
3468 pub channel_templates: Option<Vec<String>>,
3470 pub confinement: Option<String>,
3472 pub plugs: Option<HashMap<String, serde_json::Value>>,
3477 pub slots: Option<Vec<String>>,
3479 pub assumes: Option<Vec<String>>,
3481 pub apps: Option<HashMap<String, SnapcraftApp>>,
3483 pub layouts: Option<HashMap<String, SnapcraftLayout>>,
3485 pub extra_files: Option<Vec<SnapcraftExtraFileSpec>>,
3487 pub templated_extra_files: Option<Vec<TemplatedExtraFile>>,
3491 pub name_template: Option<String>,
3493 #[serde(deserialize_with = "deserialize_string_or_bool_opt", default)]
3496 pub disable: Option<StringOrBool>,
3497 pub replace: Option<bool>,
3499 pub mod_timestamp: Option<String>,
3501 pub hooks: Option<HashMap<String, serde_json::Value>>,
3503}
3504
3505#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
3506#[serde(default)]
3507pub struct SnapcraftApp {
3508 pub command: Option<String>,
3510 pub daemon: Option<String>,
3512 #[serde(alias = "stop-mode")]
3514 pub stop_mode: Option<String>,
3515 pub plugs: Option<Vec<String>>,
3517 pub environment: Option<HashMap<String, serde_json::Value>>,
3519 pub args: Option<String>,
3521 #[serde(alias = "restart-condition")]
3523 pub restart_condition: Option<String>,
3524 pub adapter: Option<String>,
3526 pub after: Option<Vec<String>>,
3528 pub aliases: Option<Vec<String>>,
3530 pub autostart: Option<String>,
3532 pub before: Option<Vec<String>>,
3534 #[serde(alias = "bus-name")]
3536 pub bus_name: Option<String>,
3537 #[serde(alias = "command-chain")]
3539 pub command_chain: Option<Vec<String>>,
3540 #[serde(alias = "common-id")]
3542 pub common_id: Option<String>,
3543 pub completer: Option<String>,
3545 pub desktop: Option<String>,
3547 pub extensions: Option<Vec<String>>,
3549 #[serde(alias = "install-mode")]
3551 pub install_mode: Option<String>,
3552 pub passthrough: Option<HashMap<String, serde_json::Value>>,
3554 #[serde(alias = "post-stop-command")]
3556 pub post_stop_command: Option<String>,
3557 #[serde(alias = "refresh-mode")]
3559 pub refresh_mode: Option<String>,
3560 #[serde(alias = "reload-command")]
3562 pub reload_command: Option<String>,
3563 #[serde(alias = "restart-delay")]
3565 pub restart_delay: Option<String>,
3566 pub slots: Option<Vec<String>>,
3568 pub sockets: Option<HashMap<String, serde_json::Value>>,
3570 #[serde(alias = "start-timeout")]
3572 pub start_timeout: Option<String>,
3573 #[serde(alias = "stop-command")]
3575 pub stop_command: Option<String>,
3576 #[serde(alias = "stop-timeout")]
3578 pub stop_timeout: Option<String>,
3579 pub timer: Option<String>,
3581 #[serde(alias = "watchdog-timeout")]
3583 pub watchdog_timeout: Option<String>,
3584}
3585
3586#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
3587#[serde(default)]
3588pub struct SnapcraftLayout {
3589 pub bind: Option<String>,
3591 pub bind_file: Option<String>,
3593 pub symlink: Option<String>,
3595 #[serde(rename = "type")]
3597 pub type_: Option<String>,
3598}
3599
3600#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
3604#[serde(untagged)]
3605pub enum SnapcraftExtraFileSpec {
3606 Source(String),
3608 Detailed {
3610 source: String,
3611 #[serde(skip_serializing_if = "Option::is_none")]
3612 destination: Option<String>,
3613 #[serde(skip_serializing_if = "Option::is_none")]
3614 mode: Option<u32>,
3615 },
3616}
3617
3618impl SnapcraftExtraFileSpec {
3619 pub fn source(&self) -> &str {
3621 match self {
3622 SnapcraftExtraFileSpec::Source(s) => s,
3623 SnapcraftExtraFileSpec::Detailed { source, .. } => source,
3624 }
3625 }
3626
3627 pub fn destination(&self) -> Option<&str> {
3629 match self {
3630 SnapcraftExtraFileSpec::Source(_) => None,
3631 SnapcraftExtraFileSpec::Detailed { destination, .. } => destination.as_deref(),
3632 }
3633 }
3634
3635 pub fn mode(&self) -> Option<u32> {
3637 match self {
3638 SnapcraftExtraFileSpec::Source(_) => None,
3639 SnapcraftExtraFileSpec::Detailed { mode, .. } => *mode,
3640 }
3641 }
3642}
3643
3644#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
3649#[serde(default)]
3650pub struct DmgConfig {
3651 pub id: Option<String>,
3653 pub ids: Option<Vec<String>>,
3655 pub name: Option<String>,
3657 pub extra_files: Option<Vec<ExtraFileSpec>>,
3659 pub templated_extra_files: Option<Vec<TemplatedExtraFile>>,
3663 pub replace: Option<bool>,
3665 pub mod_timestamp: Option<String>,
3667 #[serde(deserialize_with = "deserialize_string_or_bool_opt", default)]
3669 pub disable: Option<StringOrBool>,
3670 #[serde(rename = "use")]
3672 pub use_: Option<String>,
3673 #[serde(rename = "if")]
3676 pub if_condition: Option<String>,
3677}
3678
3679#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
3684#[serde(default)]
3685pub struct MsiConfig {
3686 pub id: Option<String>,
3688 pub ids: Option<Vec<String>>,
3690 pub wxs: Option<String>,
3692 pub name: Option<String>,
3694 pub version: Option<String>,
3696 pub replace: Option<bool>,
3698 pub mod_timestamp: Option<String>,
3700 #[serde(deserialize_with = "deserialize_string_or_bool_opt", default)]
3702 pub disable: Option<StringOrBool>,
3703 pub extra_files: Option<Vec<String>>,
3705 pub extensions: Option<Vec<String>>,
3707 #[serde(rename = "if")]
3710 pub if_condition: Option<String>,
3711 pub hooks: Option<BuildHooksConfig>,
3715}
3716
3717#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
3722#[serde(default)]
3723pub struct PkgConfig {
3724 pub id: Option<String>,
3726 pub ids: Option<Vec<String>>,
3728 pub identifier: Option<String>,
3730 pub name: Option<String>,
3732 pub install_location: Option<String>,
3734 pub scripts: Option<String>,
3736 pub extra_files: Option<Vec<ExtraFileSpec>>,
3738 pub replace: Option<bool>,
3740 pub mod_timestamp: Option<String>,
3742 #[serde(rename = "use")]
3744 pub use_: Option<String>,
3745 pub min_os_version: Option<String>,
3747 #[serde(deserialize_with = "deserialize_string_or_bool_opt", default)]
3749 pub disable: Option<StringOrBool>,
3750 #[serde(rename = "if")]
3753 pub if_condition: Option<String>,
3754}
3755
3756#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
3761#[serde(default)]
3762pub struct NsisConfig {
3763 pub id: Option<String>,
3765 pub ids: Option<Vec<String>>,
3767 pub name: Option<String>,
3769 pub script: Option<String>,
3771 pub extra_files: Option<Vec<ExtraFileSpec>>,
3773 pub templated_extra_files: Option<Vec<TemplatedExtraFile>>,
3777 #[serde(deserialize_with = "deserialize_string_or_bool_opt", default)]
3779 pub disable: Option<StringOrBool>,
3780 pub replace: Option<bool>,
3782 pub mod_timestamp: Option<String>,
3784 #[serde(rename = "if")]
3787 pub if_condition: Option<String>,
3788}
3789
3790#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
3795#[serde(default)]
3796pub struct AppBundleConfig {
3797 pub id: Option<String>,
3799 pub ids: Option<Vec<String>>,
3801 pub name: Option<String>,
3803 pub icon: Option<String>,
3805 pub bundle: Option<String>,
3807 pub extra_files: Option<Vec<ArchiveFileSpec>>,
3809 pub templated_extra_files: Option<Vec<TemplatedExtraFile>>,
3813 pub mod_timestamp: Option<String>,
3815 pub replace: Option<bool>,
3817 #[serde(deserialize_with = "deserialize_string_or_bool_opt", default)]
3819 pub disable: Option<StringOrBool>,
3820 #[serde(rename = "if")]
3823 pub if_condition: Option<String>,
3824}
3825
3826#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
3831#[serde(default)]
3832pub struct FlatpakConfig {
3833 pub id: Option<String>,
3835 pub ids: Option<Vec<String>>,
3837 pub name_template: Option<String>,
3839 pub app_id: Option<String>,
3841 pub runtime: Option<String>,
3843 pub runtime_version: Option<String>,
3845 pub sdk: Option<String>,
3847 pub command: Option<String>,
3849 pub finish_args: Option<Vec<String>>,
3851 pub extra_files: Option<Vec<ExtraFileSpec>>,
3853 pub replace: Option<bool>,
3855 pub mod_timestamp: Option<String>,
3857 #[serde(deserialize_with = "deserialize_string_or_bool_opt", default)]
3859 pub disable: Option<StringOrBool>,
3860}
3861
3862#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
3867#[serde(default)]
3868pub struct BlobConfig {
3869 pub id: Option<String>,
3871 pub provider: String,
3873 pub bucket: String,
3875 pub directory: Option<String>,
3878 pub region: Option<String>,
3880 pub endpoint: Option<String>,
3882 pub disable_ssl: Option<bool>,
3884 pub s3_force_path_style: Option<bool>,
3888 pub acl: Option<String>,
3890 #[serde(deserialize_with = "deserialize_string_or_vec_opt", default)]
3893 pub cache_control: Option<Vec<String>>,
3894 pub content_disposition: Option<String>,
3897 pub kms_key: Option<String>,
3899 pub ids: Option<Vec<String>>,
3901 #[serde(deserialize_with = "deserialize_string_or_bool_opt", default)]
3904 pub disable: Option<StringOrBool>,
3905 pub include_meta: Option<bool>,
3907 pub extra_files: Option<Vec<ExtraFileSpec>>,
3909 pub templated_extra_files: Option<Vec<TemplatedExtraFile>>,
3913 pub extra_files_only: Option<bool>,
3915 pub parallelism: Option<usize>,
3918}
3919
3920#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
3925#[serde(default)]
3926pub struct PartialConfig {
3927 pub by: Option<String>,
3931}
3932
3933#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
3938#[serde(default)]
3939pub struct BinstallConfig {
3940 pub enabled: Option<bool>,
3942 pub pkg_url: Option<String>,
3944 pub bin_dir: Option<String>,
3946 pub pkg_fmt: Option<String>,
3948}
3949
3950#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
3957#[serde(default)]
3958pub struct NotarizeConfig {
3959 #[serde(deserialize_with = "deserialize_string_or_bool_opt", default)]
3961 pub disable: Option<StringOrBool>,
3962 pub macos: Option<Vec<MacOSSignNotarizeConfig>>,
3964 pub macos_native: Option<Vec<MacOSNativeSignNotarizeConfig>>,
3966}
3967
3968#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
3970#[serde(default)]
3971pub struct MacOSSignNotarizeConfig {
3972 pub ids: Option<Vec<String>>,
3974 #[serde(deserialize_with = "deserialize_string_or_bool_opt", default)]
3976 pub enabled: Option<StringOrBool>,
3977 pub sign: Option<MacOSSignConfig>,
3979 pub notarize: Option<MacOSNotarizeApiConfig>,
3981}
3982
3983#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
3985#[serde(default)]
3986pub struct MacOSSignConfig {
3987 pub certificate: Option<String>,
3989 pub password: Option<String>,
3991 pub entitlements: Option<String>,
3993}
3994
3995#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
3997#[serde(default)]
3998pub struct MacOSNotarizeApiConfig {
3999 pub issuer_id: Option<String>,
4001 pub key: Option<String>,
4003 pub key_id: Option<String>,
4005 pub timeout: Option<String>,
4007 pub wait: Option<bool>,
4009}
4010
4011#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
4013#[serde(default)]
4014pub struct MacOSNativeSignNotarizeConfig {
4015 pub ids: Option<Vec<String>>,
4017 #[serde(deserialize_with = "deserialize_string_or_bool_opt", default)]
4019 pub enabled: Option<StringOrBool>,
4020 #[serde(rename = "use")]
4022 pub use_: Option<String>,
4023 pub sign: Option<MacOSNativeSignConfig>,
4025 pub notarize: Option<MacOSNativeNotarizeConfig>,
4027}
4028
4029#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
4031#[serde(default)]
4032pub struct MacOSNativeSignConfig {
4033 pub identity: Option<String>,
4035 pub keychain: Option<String>,
4037 pub options: Option<Vec<String>>,
4039 pub entitlements: Option<String>,
4041}
4042
4043#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
4045#[serde(default)]
4046pub struct MacOSNativeNotarizeConfig {
4047 pub profile_name: Option<String>,
4049 pub wait: Option<bool>,
4051 pub timeout: Option<String>,
4053}
4054
4055#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
4062#[serde(default)]
4063pub struct SourceFileEntry {
4064 pub src: String,
4066 pub dst: Option<String>,
4068 pub strip_parent: Option<bool>,
4070 pub info: Option<SourceFileInfo>,
4072}
4073
4074#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
4076#[serde(default)]
4077pub struct SourceFileInfo {
4078 pub owner: Option<String>,
4080 pub group: Option<String>,
4082 pub mode: Option<u32>,
4084 pub mtime: Option<String>,
4086}
4087
4088#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
4089#[serde(default)]
4090pub struct SourceConfig {
4091 pub enabled: Option<bool>,
4093 pub format: Option<String>,
4095 pub name_template: Option<String>,
4097 pub prefix_template: Option<String>,
4100 #[serde(default, deserialize_with = "deserialize_source_files")]
4102 #[schemars(schema_with = "source_files_schema")]
4103 pub files: Vec<SourceFileEntry>,
4104}
4105
4106impl SourceConfig {
4107 pub fn is_enabled(&self) -> bool {
4109 self.enabled.unwrap_or(false)
4110 }
4111
4112 pub fn archive_format(&self) -> &str {
4114 self.format.as_deref().unwrap_or("tar.gz")
4115 }
4116}
4117
4118fn source_files_schema(
4120 generator: &mut schemars::r#gen::SchemaGenerator,
4121) -> schemars::schema::Schema {
4122 let mut schema = generator.subschema_for::<Vec<SourceFileEntry>>();
4123 if let schemars::schema::Schema::Object(ref mut obj) = schema {
4124 obj.metadata().description = Some(
4125 "Extra files for the source archive. Accepts strings (glob patterns), objects with src/dst/info, or a mixed array.".to_owned(),
4126 );
4127 }
4128 schema
4129}
4130
4131fn deserialize_source_files<'de, D>(deserializer: D) -> Result<Vec<SourceFileEntry>, D::Error>
4138where
4139 D: Deserializer<'de>,
4140{
4141 use serde::de::{self, SeqAccess, Visitor};
4142
4143 struct SourceFilesVisitor;
4144
4145 impl<'de> Visitor<'de> for SourceFilesVisitor {
4146 type Value = Vec<SourceFileEntry>;
4147
4148 fn expecting(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
4149 f.write_str("a string, a source file entry object, or an array of strings/objects")
4150 }
4151
4152 fn visit_str<E: de::Error>(self, v: &str) -> Result<Self::Value, E> {
4153 Ok(vec![SourceFileEntry {
4154 src: v.to_string(),
4155 ..Default::default()
4156 }])
4157 }
4158
4159 fn visit_seq<A: SeqAccess<'de>>(self, mut seq: A) -> Result<Self::Value, A::Error> {
4160 let mut entries = Vec::new();
4161 while let Some(value) = seq.next_element::<serde_yaml_ng::Value>()? {
4162 match value {
4163 serde_yaml_ng::Value::String(s) => {
4164 entries.push(SourceFileEntry {
4165 src: s,
4166 ..Default::default()
4167 });
4168 }
4169 other => {
4170 let entry =
4171 SourceFileEntry::deserialize(other).map_err(de::Error::custom)?;
4172 entries.push(entry);
4173 }
4174 }
4175 }
4176 Ok(entries)
4177 }
4178
4179 fn visit_map<M: de::MapAccess<'de>>(self, map: M) -> Result<Self::Value, M::Error> {
4180 let entry = SourceFileEntry::deserialize(de::value::MapAccessDeserializer::new(map))?;
4181 Ok(vec![entry])
4182 }
4183
4184 fn visit_unit<E: de::Error>(self) -> Result<Self::Value, E> {
4185 Ok(Vec::new())
4186 }
4187
4188 fn visit_none<E: de::Error>(self) -> Result<Self::Value, E> {
4189 Ok(Vec::new())
4190 }
4191 }
4192
4193 deserializer.deserialize_any(SourceFilesVisitor)
4194}
4195
4196#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
4201#[serde(default)]
4202pub struct SbomConfig {
4203 pub id: Option<String>,
4205 pub cmd: Option<String>,
4207 #[serde(default, deserialize_with = "deserialize_env_map")]
4212 pub env: Option<HashMap<String, String>>,
4213 pub args: Option<Vec<String>>,
4215 pub documents: Option<Vec<String>>,
4217 pub artifacts: Option<String>,
4219 pub ids: Option<Vec<String>>,
4221 #[serde(deserialize_with = "deserialize_string_or_bool_opt", default)]
4223 pub disable: Option<StringOrBool>,
4224}
4225
4226fn deserialize_sboms<'de, D>(deserializer: D) -> Result<Vec<SbomConfig>, D::Error>
4232where
4233 D: Deserializer<'de>,
4234{
4235 use serde::de::{self, Visitor};
4236
4237 struct SbomsVisitor;
4238
4239 impl<'de> Visitor<'de> for SbomsVisitor {
4240 type Value = Vec<SbomConfig>;
4241
4242 fn expecting(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
4243 f.write_str("an SBOM config object or an array of SBOM config objects")
4244 }
4245
4246 fn visit_seq<A: de::SeqAccess<'de>>(self, mut seq: A) -> Result<Self::Value, A::Error> {
4247 let mut configs = Vec::new();
4248 while let Some(item) = seq.next_element::<SbomConfig>()? {
4249 configs.push(item);
4250 }
4251 Ok(configs)
4252 }
4253
4254 fn visit_map<M: de::MapAccess<'de>>(self, map: M) -> Result<Self::Value, M::Error> {
4255 let config = SbomConfig::deserialize(de::value::MapAccessDeserializer::new(map))?;
4256 Ok(vec![config])
4257 }
4258
4259 fn visit_unit<E: de::Error>(self) -> Result<Self::Value, E> {
4260 Ok(Vec::new())
4261 }
4262
4263 fn visit_none<E: de::Error>(self) -> Result<Self::Value, E> {
4264 Ok(Vec::new())
4265 }
4266 }
4267
4268 deserializer.deserialize_any(SbomsVisitor)
4269}
4270
4271#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
4276#[serde(default)]
4277pub struct VersionSyncConfig {
4278 pub enabled: Option<bool>,
4280 pub mode: Option<String>,
4282}
4283
4284#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
4289#[serde(default)]
4290pub struct ChangelogConfig {
4291 pub sort: Option<String>,
4293 pub filters: Option<ChangelogFilters>,
4295 pub groups: Option<Vec<ChangelogGroup>>,
4297 pub header: Option<String>,
4299 pub footer: Option<String>,
4301 #[serde(deserialize_with = "deserialize_string_or_bool_opt", default)]
4304 pub disable: Option<StringOrBool>,
4305 #[serde(rename = "use")]
4310 pub use_source: Option<String>,
4311 pub abbrev: Option<i32>,
4313 pub format: Option<String>,
4319 pub paths: Option<Vec<String>>,
4324 pub title: Option<String>,
4326 pub divider: Option<String>,
4328 pub ai: Option<ChangelogAiConfig>,
4330}
4331
4332#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
4334#[serde(default)]
4335pub struct ChangelogAiConfig {
4336 #[serde(rename = "use")]
4339 pub provider: Option<String>,
4340 pub model: Option<String>,
4342 pub prompt: Option<ChangelogAiPrompt>,
4345}
4346
4347#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
4349#[serde(untagged)]
4350pub enum ChangelogAiPrompt {
4351 Inline(String),
4353 Source(ChangelogAiPromptSource),
4355}
4356
4357#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
4359#[serde(default)]
4360pub struct ChangelogAiPromptSource {
4361 pub from_url: Option<ContentFromUrl>,
4363 pub from_file: Option<ContentFromFile>,
4365}
4366
4367#[derive(Debug, Clone, PartialEq, Eq)]
4369pub enum ResolvedPromptSource {
4370 File(String),
4372 Url {
4374 url: String,
4375 headers: Option<std::collections::HashMap<String, String>>,
4376 },
4377 None,
4379}
4380
4381impl ChangelogAiPromptSource {
4382 pub fn resolve(&self) -> ResolvedPromptSource {
4384 if let Some(ref file) = self.from_file
4385 && let Some(ref path) = file.path
4386 {
4387 return ResolvedPromptSource::File(path.clone());
4388 }
4389 if let Some(ref url_cfg) = self.from_url
4390 && let Some(ref url) = url_cfg.url
4391 {
4392 return ResolvedPromptSource::Url {
4393 url: url.clone(),
4394 headers: url_cfg.headers.clone(),
4395 };
4396 }
4397 ResolvedPromptSource::None
4398 }
4399}
4400
4401#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
4403#[serde(default)]
4404pub struct ContentFromUrl {
4405 pub url: Option<String>,
4407 pub headers: Option<std::collections::HashMap<String, String>>,
4409}
4410
4411#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
4413#[serde(default)]
4414pub struct ContentFromFile {
4415 pub path: Option<String>,
4417}
4418
4419#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
4420#[serde(default)]
4421pub struct ChangelogFilters {
4422 pub exclude: Option<Vec<String>>,
4424 pub include: Option<Vec<String>>,
4426}
4427
4428#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
4429#[serde(default)]
4430pub struct ChangelogGroup {
4431 pub title: String,
4433 pub regexp: Option<String>,
4435 pub order: Option<i32>,
4437 pub groups: Option<Vec<ChangelogGroup>>,
4439}
4440
4441#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
4446#[serde(default)]
4447pub struct SignConfig {
4448 pub id: Option<String>,
4450 pub artifacts: Option<String>,
4452 pub cmd: Option<String>,
4454 pub args: Option<Vec<String>>,
4456 pub signature: Option<String>,
4458 pub stdin: Option<String>,
4460 pub stdin_file: Option<String>,
4462 pub ids: Option<Vec<String>>,
4464 #[serde(default, deserialize_with = "deserialize_env_map")]
4466 pub env: Option<HashMap<String, String>>,
4467 pub certificate: Option<String>,
4469 #[serde(deserialize_with = "deserialize_string_or_bool_opt", default)]
4472 pub output: Option<StringOrBool>,
4473 #[serde(rename = "if")]
4475 pub if_condition: Option<String>,
4476}
4477
4478#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
4479#[serde(default)]
4480pub struct DockerSignConfig {
4481 pub id: Option<String>,
4483 pub artifacts: Option<String>,
4485 pub cmd: Option<String>,
4487 pub args: Option<Vec<String>>,
4489 pub signature: Option<String>,
4491 pub certificate: Option<String>,
4493 pub ids: Option<Vec<String>>,
4495 pub stdin: Option<String>,
4497 pub stdin_file: Option<String>,
4499 #[serde(default, deserialize_with = "deserialize_env_map")]
4501 pub env: Option<HashMap<String, String>>,
4502 #[serde(deserialize_with = "deserialize_string_or_bool_opt", default)]
4504 pub output: Option<StringOrBool>,
4505 #[serde(rename = "if")]
4507 pub if_condition: Option<String>,
4508}
4509
4510#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
4515#[serde(default)]
4516pub struct UpxConfig {
4517 pub id: Option<String>,
4519 pub ids: Option<Vec<String>>,
4521 #[serde(deserialize_with = "deserialize_string_or_bool_opt", default)]
4524 pub enabled: Option<StringOrBool>,
4525 pub binary: String,
4527 pub args: Vec<String>,
4529 pub required: bool,
4531 pub targets: Option<Vec<String>>,
4533 pub compress: Option<String>,
4535 pub lzma: Option<bool>,
4537 pub brute: Option<bool>,
4539}
4540
4541impl Default for UpxConfig {
4542 fn default() -> Self {
4543 UpxConfig {
4544 id: None,
4545 ids: None,
4546 enabled: None,
4547 binary: "upx".to_string(),
4548 args: Vec::new(),
4549 required: false,
4550 targets: None,
4551 compress: None,
4552 lzma: None,
4553 brute: None,
4554 }
4555 }
4556}
4557
4558fn deserialize_upx<'de, D>(deserializer: D) -> Result<Vec<UpxConfig>, D::Error>
4564where
4565 D: Deserializer<'de>,
4566{
4567 use serde::de::{self, Visitor};
4568
4569 struct UpxVisitor;
4570
4571 impl<'de> Visitor<'de> for UpxVisitor {
4572 type Value = Vec<UpxConfig>;
4573
4574 fn expecting(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
4575 f.write_str("a UPX config object or an array of UPX config objects")
4576 }
4577
4578 fn visit_seq<A: de::SeqAccess<'de>>(self, mut seq: A) -> Result<Self::Value, A::Error> {
4579 let mut configs = Vec::new();
4580 while let Some(item) = seq.next_element::<UpxConfig>()? {
4581 configs.push(item);
4582 }
4583 Ok(configs)
4584 }
4585
4586 fn visit_map<M: de::MapAccess<'de>>(self, map: M) -> Result<Self::Value, M::Error> {
4587 let config = UpxConfig::deserialize(de::value::MapAccessDeserializer::new(map))?;
4588 Ok(vec![config])
4589 }
4590
4591 fn visit_unit<E: de::Error>(self) -> Result<Self::Value, E> {
4592 Ok(Vec::new())
4593 }
4594
4595 fn visit_none<E: de::Error>(self) -> Result<Self::Value, E> {
4596 Ok(Vec::new())
4597 }
4598 }
4599
4600 deserializer.deserialize_any(UpxVisitor)
4601}
4602
4603#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
4608pub struct SnapshotConfig {
4609 #[serde(alias = "name_template", rename = "version_template")]
4613 pub name_template: String,
4614}
4615
4616#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
4621#[serde(default)]
4622pub struct NightlyConfig {
4623 pub name_template: Option<String>,
4625 pub tag_name: Option<String>,
4627}
4628
4629#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
4634#[serde(default)]
4635pub struct MetadataConfig {
4636 pub description: Option<String>,
4638 pub homepage: Option<String>,
4640 pub license: Option<String>,
4642 pub maintainers: Option<Vec<String>>,
4644 pub mod_timestamp: Option<String>,
4649 pub full_description: Option<ContentSource>,
4654 pub commit_author: Option<CommitAuthorConfig>,
4658}
4659
4660#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
4670#[serde(default)]
4671pub struct TemplateFileConfig {
4672 pub id: Option<String>,
4674 pub src: String,
4677 pub dst: String,
4680 pub mode: Option<String>,
4683}
4684
4685#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
4690#[serde(default)]
4691pub struct AnnounceConfig {
4692 #[serde(deserialize_with = "deserialize_string_or_bool_opt", default)]
4694 pub skip: Option<StringOrBool>,
4695 pub discord: Option<DiscordAnnounce>,
4697 pub discourse: Option<DiscourseAnnounce>,
4699 pub slack: Option<SlackAnnounce>,
4701 pub webhook: Option<WebhookConfig>,
4703 pub telegram: Option<TelegramAnnounce>,
4705 pub teams: Option<TeamsAnnounce>,
4707 pub mattermost: Option<MattermostAnnounce>,
4709 pub email: Option<EmailAnnounce>,
4711 pub reddit: Option<RedditAnnounce>,
4713 pub twitter: Option<TwitterAnnounce>,
4715 pub mastodon: Option<MastodonAnnounce>,
4717 pub bluesky: Option<BlueskyAnnounce>,
4719 pub linkedin: Option<LinkedInAnnounce>,
4721 pub opencollective: Option<OpenCollectiveAnnounce>,
4723}
4724
4725#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
4726#[serde(default)]
4727pub struct BlueskyAnnounce {
4728 #[serde(deserialize_with = "deserialize_string_or_bool_opt", default)]
4730 pub enabled: Option<StringOrBool>,
4731 pub username: Option<String>,
4733 pub message_template: Option<String>,
4735 pub pds_url: Option<String>,
4739}
4740
4741#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
4742#[serde(default)]
4743pub struct DiscourseAnnounce {
4744 #[serde(deserialize_with = "deserialize_string_or_bool_opt", default)]
4746 pub enabled: Option<StringOrBool>,
4747 pub server: Option<String>,
4749 pub category_id: Option<u64>,
4751 pub username: Option<String>,
4753 pub title_template: Option<String>,
4755 pub message_template: Option<String>,
4757}
4758
4759#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
4760#[serde(default)]
4761pub struct LinkedInAnnounce {
4762 #[serde(deserialize_with = "deserialize_string_or_bool_opt", default)]
4764 pub enabled: Option<StringOrBool>,
4765 pub message_template: Option<String>,
4767}
4768
4769#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
4770#[serde(default)]
4771pub struct OpenCollectiveAnnounce {
4772 #[serde(deserialize_with = "deserialize_string_or_bool_opt", default)]
4774 pub enabled: Option<StringOrBool>,
4775 pub slug: Option<String>,
4777 pub title_template: Option<String>,
4779 pub message_template: Option<String>,
4781}
4782
4783#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
4784#[serde(default)]
4785pub struct TwitterAnnounce {
4786 #[serde(deserialize_with = "deserialize_string_or_bool_opt", default)]
4788 pub enabled: Option<StringOrBool>,
4789 pub message_template: Option<String>,
4791}
4792
4793#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
4794#[serde(default)]
4795pub struct MastodonAnnounce {
4796 #[serde(deserialize_with = "deserialize_string_or_bool_opt", default)]
4798 pub enabled: Option<StringOrBool>,
4799 pub server: Option<String>,
4801 pub message_template: Option<String>,
4803}
4804
4805#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
4806#[serde(default)]
4807pub struct DiscordAnnounce {
4808 #[serde(deserialize_with = "deserialize_string_or_bool_opt", default)]
4810 pub enabled: Option<StringOrBool>,
4811 pub webhook_url: Option<String>,
4813 pub message_template: Option<String>,
4815 pub author: Option<String>,
4817 pub color: Option<String>,
4820 pub icon_url: Option<String>,
4822}
4823
4824#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
4825#[serde(default)]
4826pub struct WebhookConfig {
4827 #[serde(deserialize_with = "deserialize_string_or_bool_opt", default)]
4829 pub enabled: Option<StringOrBool>,
4830 pub endpoint_url: Option<String>,
4832 pub headers: Option<HashMap<String, String>>,
4846 pub content_type: Option<String>,
4848 pub message_template: Option<String>,
4850 pub skip_tls_verify: Option<bool>,
4852 #[serde(default)]
4854 pub expected_status_codes: Vec<u16>,
4855}
4856
4857#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
4858#[serde(default)]
4859pub struct TelegramAnnounce {
4860 #[serde(deserialize_with = "deserialize_string_or_bool_opt", default)]
4862 pub enabled: Option<StringOrBool>,
4863 pub bot_token: Option<String>,
4865 pub chat_id: Option<String>,
4867 pub message_template: Option<String>,
4869 pub parse_mode: Option<String>,
4871 pub message_thread_id: Option<String>,
4874}
4875
4876#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
4877#[serde(default)]
4878pub struct TeamsAnnounce {
4879 #[serde(deserialize_with = "deserialize_string_or_bool_opt", default)]
4881 pub enabled: Option<StringOrBool>,
4882 pub webhook_url: Option<String>,
4884 pub message_template: Option<String>,
4886 pub title_template: Option<String>,
4888 pub color: Option<String>,
4890 pub icon_url: Option<String>,
4892}
4893
4894#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
4895#[serde(default)]
4896pub struct MattermostAnnounce {
4897 #[serde(deserialize_with = "deserialize_string_or_bool_opt", default)]
4899 pub enabled: Option<StringOrBool>,
4900 pub webhook_url: Option<String>,
4902 pub channel: Option<String>,
4904 pub username: Option<String>,
4906 pub icon_url: Option<String>,
4908 pub icon_emoji: Option<String>,
4910 pub color: Option<String>,
4912 pub message_template: Option<String>,
4914 pub title_template: Option<String>,
4916}
4917
4918#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
4919#[serde(default)]
4920pub struct EmailAnnounce {
4921 #[serde(deserialize_with = "deserialize_string_or_bool_opt", default)]
4923 pub enabled: Option<StringOrBool>,
4924 pub host: Option<String>,
4927 pub port: Option<u16>,
4929 pub username: Option<String>,
4931 pub from: Option<String>,
4933 #[serde(default)]
4935 pub to: Vec<String>,
4936 pub subject_template: Option<String>,
4938 #[serde(alias = "body_template")]
4940 pub message_template: Option<String>,
4941 pub insecure_skip_verify: Option<bool>,
4943}
4944
4945#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
4946#[serde(default)]
4947pub struct RedditAnnounce {
4948 #[serde(deserialize_with = "deserialize_string_or_bool_opt", default)]
4950 pub enabled: Option<StringOrBool>,
4951 pub application_id: Option<String>,
4953 pub username: Option<String>,
4955 pub sub: Option<String>,
4957 pub title_template: Option<String>,
4959 pub url_template: Option<String>,
4961}
4962
4963#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
4964#[serde(default)]
4965pub struct SlackAnnounce {
4966 #[serde(deserialize_with = "deserialize_string_or_bool_opt", default)]
4968 pub enabled: Option<StringOrBool>,
4969 pub webhook_url: Option<String>,
4971 pub message_template: Option<String>,
4973 pub channel: Option<String>,
4975 pub username: Option<String>,
4977 pub icon_emoji: Option<String>,
4979 pub icon_url: Option<String>,
4981 pub blocks: Option<Vec<SlackBlock>>,
4983 pub attachments: Option<Vec<SlackAttachment>>,
4985}
4986
4987#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
4990pub struct SlackBlock {
4991 #[serde(rename = "type")]
4993 pub block_type: String,
4994 #[serde(default, skip_serializing_if = "Option::is_none")]
4996 pub text: Option<SlackTextObject>,
4997 #[serde(default, skip_serializing_if = "Option::is_none")]
4999 pub block_id: Option<String>,
5000 #[serde(flatten)]
5002 pub extra: HashMap<String, serde_json::Value>,
5003}
5004
5005#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
5007pub struct SlackTextObject {
5008 #[serde(rename = "type")]
5010 pub text_type: String,
5011 pub text: String,
5013 #[serde(default, skip_serializing_if = "Option::is_none")]
5015 pub emoji: Option<bool>,
5016 #[serde(default, skip_serializing_if = "Option::is_none")]
5018 pub verbatim: Option<bool>,
5019}
5020
5021#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
5024pub struct SlackAttachment {
5025 #[serde(default, skip_serializing_if = "Option::is_none")]
5027 pub color: Option<String>,
5028 #[serde(default, skip_serializing_if = "Option::is_none")]
5030 pub text: Option<String>,
5031 #[serde(default, skip_serializing_if = "Option::is_none")]
5033 pub title: Option<String>,
5034 #[serde(default, skip_serializing_if = "Option::is_none")]
5036 pub fallback: Option<String>,
5037 #[serde(default, skip_serializing_if = "Option::is_none")]
5039 pub pretext: Option<String>,
5040 #[serde(default, skip_serializing_if = "Option::is_none")]
5042 pub footer: Option<String>,
5043 #[serde(flatten)]
5045 pub extra: HashMap<String, serde_json::Value>,
5046}
5047
5048#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
5055#[serde(default)]
5056pub struct DockerHubConfig {
5057 pub username: Option<String>,
5059 pub secret_name: Option<String>,
5061 pub images: Option<Vec<String>>,
5063 pub description: Option<String>,
5065 pub full_description: Option<DockerHubFullDescription>,
5067 #[serde(deserialize_with = "deserialize_string_or_bool_opt", default)]
5069 pub disable: Option<StringOrBool>,
5070}
5071
5072#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
5074#[serde(default)]
5075pub struct DockerHubFullDescription {
5076 pub from_url: Option<DockerHubFromUrl>,
5078 pub from_file: Option<DockerHubFromFile>,
5080}
5081
5082#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
5084#[serde(default)]
5085pub struct DockerHubFromUrl {
5086 pub url: String,
5088 pub headers: Option<HashMap<String, String>>,
5090}
5091
5092#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
5094#[serde(default)]
5095pub struct DockerHubFromFile {
5096 pub path: String,
5098}
5099
5100#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
5107#[serde(default)]
5108pub struct ArtifactoryConfig {
5109 pub name: Option<String>,
5111 pub target: Option<String>,
5113 pub mode: Option<String>,
5115 pub username: Option<String>,
5117 pub password: Option<String>,
5119 pub ids: Option<Vec<String>>,
5121 pub exts: Option<Vec<String>>,
5123 pub client_x509_cert: Option<String>,
5125 pub client_x509_key: Option<String>,
5127 pub custom_headers: Option<HashMap<String, String>>,
5129 pub checksum_header: Option<String>,
5131 pub extra_files: Option<Vec<ExtraFileSpec>>,
5133 pub checksum: Option<bool>,
5135 pub signature: Option<bool>,
5137 pub meta: Option<bool>,
5139 pub custom_artifact_name: Option<bool>,
5141 pub extra_files_only: Option<bool>,
5143 pub method: Option<String>,
5145 pub trusted_certificates: Option<String>,
5148 #[serde(deserialize_with = "deserialize_string_or_bool_opt", default)]
5150 pub skip: Option<StringOrBool>,
5151 #[serde(deserialize_with = "deserialize_string_or_bool_opt", default)]
5155 pub disable: Option<StringOrBool>,
5156}
5157
5158#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
5165#[serde(default)]
5166pub struct CloudSmithConfig {
5167 pub organization: Option<String>,
5169 pub repository: Option<String>,
5171 pub ids: Option<Vec<String>>,
5173 pub formats: Option<Vec<String>>,
5175 pub distributions: Option<HashMap<String, serde_json::Value>>,
5177 pub component: Option<String>,
5179 pub secret_name: Option<String>,
5181 #[serde(deserialize_with = "deserialize_string_or_bool_opt", default)]
5183 pub skip: Option<StringOrBool>,
5184 #[serde(deserialize_with = "deserialize_string_or_bool_opt", default)]
5186 pub republish: Option<StringOrBool>,
5187}
5188
5189#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
5194#[serde(default)]
5195pub struct PublisherConfig {
5196 pub name: Option<String>,
5198 pub cmd: String,
5200 pub args: Option<Vec<String>>,
5202 pub ids: Option<Vec<String>>,
5204 pub artifact_types: Option<Vec<String>>,
5206 #[serde(default, deserialize_with = "deserialize_env_map")]
5208 pub env: Option<HashMap<String, String>>,
5209 pub dir: Option<String>,
5211 #[serde(deserialize_with = "deserialize_string_or_bool_opt", default)]
5214 pub disable: Option<StringOrBool>,
5215 pub checksum: Option<bool>,
5217 pub signature: Option<bool>,
5219 pub meta: Option<bool>,
5221 pub extra_files: Option<Vec<ExtraFileSpec>>,
5223 pub templated_extra_files: Option<Vec<TemplatedExtraFile>>,
5227}
5228
5229#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default, JsonSchema)]
5237#[serde(default)]
5238pub struct HooksConfig {
5239 pub hooks: Option<Vec<HookEntry>>,
5242 pub post: Option<Vec<HookEntry>>,
5245}
5246
5247#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default, JsonSchema)]
5248#[serde(default)]
5249pub struct StructuredHook {
5250 pub cmd: String,
5252 pub dir: Option<String>,
5254 #[serde(default, deserialize_with = "deserialize_env_map")]
5256 pub env: Option<HashMap<String, String>>,
5257 pub output: Option<bool>,
5259}
5260
5261#[derive(Debug, Clone, PartialEq, Serialize, JsonSchema)]
5262#[serde(untagged)]
5263pub enum HookEntry {
5264 Simple(String),
5265 Structured(StructuredHook),
5266}
5267
5268impl PartialEq<&str> for HookEntry {
5269 fn eq(&self, other: &&str) -> bool {
5270 match self {
5271 HookEntry::Simple(s) => s.as_str() == *other,
5272 HookEntry::Structured(h) => h.cmd.as_str() == *other,
5273 }
5274 }
5275}
5276
5277impl<'de> Deserialize<'de> for HookEntry {
5278 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
5279 where
5280 D: Deserializer<'de>,
5281 {
5282 let value = serde_json::Value::deserialize(deserializer)?;
5283 match &value {
5284 serde_json::Value::String(s) => Ok(HookEntry::Simple(s.clone())),
5285 serde_json::Value::Object(_) => {
5286 let hook: StructuredHook =
5287 serde_json::from_value(value).map_err(serde::de::Error::custom)?;
5288 Ok(HookEntry::Structured(hook))
5289 }
5290 _ => Err(serde::de::Error::custom(
5291 "hook entry must be a string or an object with cmd/dir/env/output",
5292 )),
5293 }
5294 }
5295}
5296
5297#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
5307#[serde(default)]
5308pub struct GitConfig {
5309 pub tag_sort: Option<String>,
5315 pub ignore_tags: Option<Vec<String>>,
5319 pub ignore_tag_prefixes: Option<Vec<String>>,
5323 pub prerelease_suffix: Option<String>,
5327}
5328
5329#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
5348#[serde(default, deny_unknown_fields)]
5349pub struct MonorepoConfig {
5350 pub tag_prefix: Option<String>,
5356 pub dir: Option<String>,
5361}
5362
5363#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
5368#[serde(default)]
5369pub struct TagConfig {
5370 pub default_bump: Option<String>,
5372 pub tag_prefix: Option<String>,
5374 pub release_branches: Option<Vec<String>>,
5376 pub custom_tag: Option<String>,
5378 pub tag_context: Option<String>,
5380 pub branch_history: Option<String>,
5382 pub initial_version: Option<String>,
5384 pub prerelease: Option<bool>,
5386 pub prerelease_suffix: Option<String>,
5388 pub force_without_changes: Option<bool>,
5390 pub force_without_changes_pre: Option<bool>,
5392 pub major_string_token: Option<String>,
5394 pub minor_string_token: Option<String>,
5396 pub patch_string_token: Option<String>,
5398 pub none_string_token: Option<String>,
5400 pub git_api_tagging: Option<bool>,
5402 pub verbose: Option<bool>,
5404 pub tag_pre_hooks: Option<Vec<HookEntry>>,
5410 pub tag_post_hooks: Option<Vec<HookEntry>>,
5413}
5414
5415#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
5423#[serde(default, deny_unknown_fields)]
5424pub struct WorkspaceConfig {
5425 pub name: String,
5427 pub crates: Vec<CrateConfig>,
5429 pub changelog: Option<ChangelogConfig>,
5431 #[serde(default, alias = "sign", deserialize_with = "deserialize_signs")]
5433 #[schemars(schema_with = "signs_schema")]
5434 pub signs: Vec<SignConfig>,
5435 #[serde(default, alias = "binary_sign", deserialize_with = "deserialize_signs")]
5437 #[schemars(schema_with = "signs_schema")]
5438 pub binary_signs: Vec<SignConfig>,
5439 pub before: Option<HooksConfig>,
5441 pub after: Option<HooksConfig>,
5443 #[serde(default, deserialize_with = "deserialize_env_map")]
5448 pub env: Option<HashMap<String, String>>,
5449 #[serde(default)]
5452 pub skip: Vec<String>,
5453}
5454
5455#[derive(Debug, Clone, PartialEq, Serialize, JsonSchema)]
5464#[serde(untagged)]
5465pub enum StringOrBool {
5466 Bool(bool),
5467 String(String),
5468}
5469
5470impl StringOrBool {
5471 pub fn as_bool(&self) -> bool {
5474 match self {
5475 StringOrBool::Bool(b) => *b,
5476 StringOrBool::String(s) => matches!(s.trim(), "true" | "1"),
5477 }
5478 }
5479
5480 pub fn as_str(&self) -> &str {
5482 match self {
5483 StringOrBool::Bool(true) => "true",
5484 StringOrBool::Bool(false) => "false",
5485 StringOrBool::String(s) => s,
5486 }
5487 }
5488
5489 pub fn is_template(&self) -> bool {
5491 matches!(self, StringOrBool::String(s) if s.contains('{'))
5492 }
5493
5494 pub fn evaluates_to_true(&self, render: impl Fn(&str) -> anyhow::Result<String>) -> bool {
5500 if self.is_template() {
5501 render(self.as_str())
5502 .map(|r| r.trim() == "true")
5503 .unwrap_or(false)
5504 } else {
5505 self.as_bool()
5506 }
5507 }
5508
5509 pub fn is_disabled(&self, render: impl Fn(&str) -> anyhow::Result<String>) -> bool {
5514 self.evaluates_to_true(render)
5515 }
5516}
5517
5518impl Default for StringOrBool {
5519 fn default() -> Self {
5520 StringOrBool::Bool(false)
5521 }
5522}
5523
5524fn deserialize_string_or_bool_opt<'de, D>(deserializer: D) -> Result<Option<StringOrBool>, D::Error>
5526where
5527 D: Deserializer<'de>,
5528{
5529 use serde::de::{self, Visitor};
5530
5531 struct StringOrBoolVisitor;
5532
5533 impl<'de> Visitor<'de> for StringOrBoolVisitor {
5534 type Value = Option<StringOrBool>;
5535
5536 fn expecting(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
5537 f.write_str("a bool, a string, or null")
5538 }
5539
5540 fn visit_bool<E: de::Error>(self, v: bool) -> Result<Self::Value, E> {
5541 Ok(Some(StringOrBool::Bool(v)))
5542 }
5543
5544 fn visit_str<E: de::Error>(self, v: &str) -> Result<Self::Value, E> {
5545 Ok(Some(StringOrBool::String(v.to_owned())))
5546 }
5547
5548 fn visit_string<E: de::Error>(self, v: String) -> Result<Self::Value, E> {
5549 Ok(Some(StringOrBool::String(v)))
5550 }
5551
5552 fn visit_none<E: de::Error>(self) -> Result<Self::Value, E> {
5553 Ok(None)
5554 }
5555
5556 fn visit_unit<E: de::Error>(self) -> Result<Self::Value, E> {
5557 Ok(None)
5558 }
5559 }
5560
5561 deserializer.deserialize_any(StringOrBoolVisitor)
5562}
5563
5564fn deserialize_string_or_vec_opt<'de, D>(deserializer: D) -> Result<Option<Vec<String>>, D::Error>
5567where
5568 D: Deserializer<'de>,
5569{
5570 use serde::de::{self, Visitor};
5571
5572 struct StringOrVecVisitor;
5573
5574 impl<'de> Visitor<'de> for StringOrVecVisitor {
5575 type Value = Option<Vec<String>>;
5576
5577 fn expecting(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
5578 f.write_str("a string, a list of strings, or null")
5579 }
5580
5581 fn visit_str<E: de::Error>(self, v: &str) -> Result<Self::Value, E> {
5582 Ok(Some(vec![v.to_owned()]))
5583 }
5584
5585 fn visit_string<E: de::Error>(self, v: String) -> Result<Self::Value, E> {
5586 Ok(Some(vec![v]))
5587 }
5588
5589 fn visit_seq<A: de::SeqAccess<'de>>(self, mut seq: A) -> Result<Self::Value, A::Error> {
5590 let mut items = Vec::new();
5591 while let Some(item) = seq.next_element::<String>()? {
5592 items.push(item);
5593 }
5594 Ok(Some(items))
5595 }
5596
5597 fn visit_none<E: de::Error>(self) -> Result<Self::Value, E> {
5598 Ok(None)
5599 }
5600
5601 fn visit_unit<E: de::Error>(self) -> Result<Self::Value, E> {
5602 Ok(None)
5603 }
5604 }
5605
5606 deserializer.deserialize_any(StringOrVecVisitor)
5607}
5608
5609fn deserialize_space_separated_string_or_vec_opt<'de, D>(
5614 deserializer: D,
5615) -> Result<Option<Vec<String>>, D::Error>
5616where
5617 D: Deserializer<'de>,
5618{
5619 use serde::de::{self, Visitor};
5620
5621 struct SpaceSepOrVecVisitor;
5622
5623 impl<'de> Visitor<'de> for SpaceSepOrVecVisitor {
5624 type Value = Option<Vec<String>>;
5625
5626 fn expecting(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
5627 f.write_str("a space-separated string, a list of strings, or null")
5628 }
5629
5630 fn visit_str<E: de::Error>(self, v: &str) -> Result<Self::Value, E> {
5631 let tags: Vec<String> = v.split_whitespace().map(|s| s.to_owned()).collect();
5632 if tags.is_empty() {
5633 Ok(None)
5634 } else {
5635 Ok(Some(tags))
5636 }
5637 }
5638
5639 fn visit_string<E: de::Error>(self, v: String) -> Result<Self::Value, E> {
5640 self.visit_str(&v)
5641 }
5642
5643 fn visit_seq<A: de::SeqAccess<'de>>(self, mut seq: A) -> Result<Self::Value, A::Error> {
5644 let mut items = Vec::new();
5645 while let Some(item) = seq.next_element::<String>()? {
5646 items.push(item);
5647 }
5648 if items.is_empty() {
5649 Ok(None)
5650 } else {
5651 Ok(Some(items))
5652 }
5653 }
5654
5655 fn visit_none<E: de::Error>(self) -> Result<Self::Value, E> {
5656 Ok(None)
5657 }
5658
5659 fn visit_unit<E: de::Error>(self) -> Result<Self::Value, E> {
5660 Ok(None)
5661 }
5662 }
5663
5664 deserializer.deserialize_any(SpaceSepOrVecVisitor)
5665}
5666
5667#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
5672#[serde(default)]
5673pub struct MakeselfConfig {
5674 pub id: Option<String>,
5676 pub ids: Option<Vec<String>>,
5678 pub name_template: Option<String>,
5680 pub name: Option<String>,
5682 pub script: Option<String>,
5685 pub description: Option<String>,
5687 pub maintainer: Option<String>,
5689 pub keywords: Option<Vec<String>>,
5691 pub homepage: Option<String>,
5693 pub license: Option<String>,
5695 pub compression: Option<String>,
5697 pub extra_args: Option<Vec<String>>,
5699 pub files: Option<Vec<MakeselfFile>>,
5701 pub goos: Option<Vec<String>>,
5703 pub goarch: Option<Vec<String>>,
5705 #[serde(deserialize_with = "deserialize_string_or_bool_opt", default)]
5707 pub disable: Option<StringOrBool>,
5708}
5709
5710#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
5711#[serde(default)]
5712pub struct MakeselfFile {
5713 #[serde(alias = "src")]
5715 pub source: String,
5716 #[serde(alias = "dst")]
5718 pub destination: Option<String>,
5719 pub strip_parent: Option<bool>,
5721}
5722
5723fn deserialize_makeselfs<'de, D>(deserializer: D) -> Result<Vec<MakeselfConfig>, D::Error>
5725where
5726 D: Deserializer<'de>,
5727{
5728 use serde::de::{self, Visitor};
5729
5730 struct MakeselfVisitor;
5731
5732 impl<'de> Visitor<'de> for MakeselfVisitor {
5733 type Value = Vec<MakeselfConfig>;
5734
5735 fn expecting(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
5736 f.write_str("a makeself config object or an array of makeself config objects")
5737 }
5738
5739 fn visit_seq<A: de::SeqAccess<'de>>(self, mut seq: A) -> Result<Self::Value, A::Error> {
5740 let mut configs = Vec::new();
5741 while let Some(item) = seq.next_element::<MakeselfConfig>()? {
5742 configs.push(item);
5743 }
5744 Ok(configs)
5745 }
5746
5747 fn visit_map<M: de::MapAccess<'de>>(self, map: M) -> Result<Self::Value, M::Error> {
5748 let config = MakeselfConfig::deserialize(de::value::MapAccessDeserializer::new(map))?;
5749 Ok(vec![config])
5750 }
5751
5752 fn visit_unit<E: de::Error>(self) -> Result<Self::Value, E> {
5753 Ok(Vec::new())
5754 }
5755
5756 fn visit_none<E: de::Error>(self) -> Result<Self::Value, E> {
5757 Ok(Vec::new())
5758 }
5759 }
5760
5761 deserializer.deserialize_any(MakeselfVisitor)
5762}
5763
5764fn makeselfs_schema(generator: &mut schemars::r#gen::SchemaGenerator) -> schemars::schema::Schema {
5765 let mut schema = generator.subschema_for::<Vec<MakeselfConfig>>();
5766 if let schemars::schema::Schema::Object(ref mut obj) = schema {
5767 obj.metadata().description = Some(
5768 "Makeself self-extracting archive configurations. Accepts a single object or array."
5769 .to_owned(),
5770 );
5771 }
5772 schema
5773}
5774
5775#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
5780#[serde(default)]
5781pub struct SrpmConfig {
5782 pub enabled: Option<bool>,
5784 pub package_name: Option<String>,
5786 pub file_name_template: Option<String>,
5788 pub spec_file: Option<String>,
5790 pub epoch: Option<String>,
5792 pub section: Option<String>,
5794 pub maintainer: Option<String>,
5796 pub vendor: Option<String>,
5798 pub summary: Option<String>,
5800 pub group: Option<String>,
5802 pub description: Option<String>,
5804 pub license: Option<String>,
5806 pub license_file_name: Option<String>,
5808 pub url: Option<String>,
5810 pub packager: Option<String>,
5812 pub compression: Option<String>,
5814 pub docs: Option<Vec<String>>,
5816 pub contents: Option<Vec<NfpmContentConfig>>,
5818 pub signature: Option<SrpmSignatureConfig>,
5820 #[serde(deserialize_with = "deserialize_string_or_bool_opt", default)]
5822 pub disable: Option<StringOrBool>,
5823}
5824
5825#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
5826#[serde(default)]
5827pub struct SrpmSignatureConfig {
5828 pub key_file: Option<String>,
5830 pub passphrase: Option<String>,
5832}
5833
5834#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
5836#[serde(default)]
5837pub struct NfpmContentConfig {
5838 #[serde(alias = "src")]
5840 pub source: Option<String>,
5841 #[serde(alias = "dst")]
5843 pub destination: String,
5844 #[serde(rename = "type")]
5846 pub type_: Option<String>,
5847 pub packager: Option<String>,
5849}
5850
5851#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
5856#[serde(default)]
5857pub struct MilestoneConfig {
5858 pub repo: Option<ScmRepoConfig>,
5860 pub close: Option<bool>,
5862 pub fail_on_error: Option<bool>,
5864 pub name_template: Option<String>,
5866}
5867
5868#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
5873#[serde(default)]
5874pub struct UploadConfig {
5875 pub name: Option<String>,
5877 pub ids: Option<Vec<String>>,
5879 pub exts: Option<Vec<String>>,
5881 pub target: String,
5883 pub username: Option<String>,
5885 pub password: Option<String>,
5887 pub method: Option<String>,
5889 pub mode: Option<String>,
5891 pub checksum_header: Option<String>,
5893 pub trusted_certificates: Option<String>,
5895 pub client_x509_cert: Option<String>,
5897 pub client_x509_key: Option<String>,
5899 pub checksum: Option<bool>,
5901 pub signature: Option<bool>,
5903 pub meta: Option<bool>,
5905 pub custom_headers: Option<HashMap<String, String>>,
5907 pub custom_artifact_name: Option<bool>,
5909 pub extra_files: Option<Vec<ExtraFileSpec>>,
5911 pub extra_files_only: Option<bool>,
5913 #[serde(deserialize_with = "deserialize_string_or_bool_opt", default)]
5915 pub disable: Option<StringOrBool>,
5916}
5917
5918#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
5923#[serde(default)]
5924pub struct AurSourceConfig {
5925 #[serde(alias = "package_name")]
5927 pub name: Option<String>,
5928 pub ids: Option<Vec<String>>,
5930 pub commit_author: Option<CommitAuthorConfig>,
5932 pub commit_msg_template: Option<String>,
5934 pub description: Option<String>,
5936 pub homepage: Option<String>,
5938 pub license: Option<String>,
5940 #[serde(deserialize_with = "deserialize_string_or_bool_opt", default)]
5942 pub skip_upload: Option<StringOrBool>,
5943 pub url_template: Option<String>,
5945 pub maintainers: Option<Vec<String>>,
5947 pub contributors: Option<Vec<String>>,
5949 pub provides: Option<Vec<String>>,
5951 pub conflicts: Option<Vec<String>>,
5953 pub depends: Option<Vec<String>>,
5955 pub optdepends: Option<Vec<String>>,
5957 pub makedepends: Option<Vec<String>>,
5959 pub backup: Option<Vec<String>>,
5961 pub rel: Option<String>,
5963 pub prepare: Option<String>,
5965 pub build: Option<String>,
5967 pub package: Option<String>,
5969 pub git_url: Option<String>,
5971 pub git_ssh_command: Option<String>,
5973 pub private_key: Option<String>,
5975 pub directory: Option<String>,
5977 #[serde(deserialize_with = "deserialize_string_or_bool_opt", default)]
5979 pub disable: Option<StringOrBool>,
5980 pub arches: Option<Vec<String>>,
5982}
5983
5984#[cfg(test)]
5989#[allow(clippy::field_reassign_with_default)]
5990mod tests {
5991 use super::*;
5992
5993 #[test]
5994 fn test_minimal_yaml_config() {
5995 let yaml = r#"
5996project_name: myproject
5997crates:
5998 - name: myproject
5999 path: "."
6000 tag_template: "v{{ .Version }}"
6001"#;
6002 let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
6003 assert_eq!(config.project_name, "myproject");
6004 assert_eq!(config.crates.len(), 1);
6005 assert_eq!(config.dist, std::path::PathBuf::from("./dist"));
6006 }
6007
6008 #[test]
6009 fn test_minimal_toml_config() {
6010 let toml_str = r#"
6011project_name = "myproject"
6012
6013[[crates]]
6014name = "myproject"
6015path = "."
6016tag_template = "v{{ .Version }}"
6017"#;
6018 let config: Config = toml::from_str(toml_str).unwrap();
6019 assert_eq!(config.project_name, "myproject");
6020 }
6021
6022 #[test]
6023 fn test_full_config_with_defaults() {
6024 let yaml = r#"
6025project_name: cfgd
6026dist: ./dist
6027defaults:
6028 targets:
6029 - x86_64-unknown-linux-gnu
6030 - aarch64-apple-darwin
6031 cross: auto
6032 flags: --release
6033 archives:
6034 format: tar.gz
6035 format_overrides:
6036 - os: windows
6037 format: zip
6038 checksum:
6039 algorithm: sha256
6040crates:
6041 - name: cfgd
6042 path: crates/cfgd
6043 tag_template: "v{{ .Version }}"
6044 builds:
6045 - binary: cfgd
6046 features: []
6047 no_default_features: false
6048 archives:
6049 - name_template: "{{ .ProjectName }}-{{ .Version }}-{{ .Os }}-{{ .Arch }}"
6050 files:
6051 - LICENSE
6052 release:
6053 github:
6054 owner: tj-smith47
6055 name: cfgd
6056 draft: false
6057 prerelease: auto
6058 name_template: "{{ .Tag }}"
6059"#;
6060 let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
6061 let defaults = config.defaults.unwrap();
6062 assert_eq!(defaults.targets.unwrap().len(), 2);
6063 assert_eq!(defaults.cross, Some(CrossStrategy::Auto));
6064 let release = config.crates[0].release.as_ref().unwrap();
6065 assert_eq!(release.name_template, Some("{{ .Tag }}".to_string()));
6066 }
6067
6068 #[test]
6069 fn test_snapshot_config() {
6070 let yaml = r#"
6071project_name: test
6072snapshot:
6073 name_template: "{{ .Version }}-SNAPSHOT-{{ .ShortCommit }}"
6074crates:
6075 - name: test
6076 path: "."
6077 tag_template: "v{{ .Version }}"
6078"#;
6079 let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
6080 assert_eq!(
6081 config.snapshot.unwrap().name_template,
6082 "{{ .Version }}-SNAPSHOT-{{ .ShortCommit }}"
6083 );
6084 }
6085
6086 #[test]
6087 fn test_archives_false() {
6088 let yaml = r#"
6089project_name: test
6090crates:
6091 - name: operator
6092 path: crates/operator
6093 tag_template: "v{{ .Version }}"
6094 archives: false
6095"#;
6096 let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
6097 assert!(matches!(
6098 config.crates[0].archives,
6099 ArchivesConfig::Disabled
6100 ));
6101 }
6102
6103 #[test]
6104 fn test_publish_crates_bool_and_object() {
6105 let yaml_bool = r#"
6106project_name: test
6107crates:
6108 - name: a
6109 path: "."
6110 tag_template: "v{{ .Version }}"
6111 publish:
6112 crates: true
6113"#;
6114 let config: Config = serde_yaml_ng::from_str(yaml_bool).unwrap();
6115 assert!(
6116 config.crates[0]
6117 .publish
6118 .as_ref()
6119 .unwrap()
6120 .crates_config()
6121 .enabled
6122 );
6123
6124 let yaml_obj = r#"
6125project_name: test
6126crates:
6127 - name: a
6128 path: "."
6129 tag_template: "v{{ .Version }}"
6130 publish:
6131 crates:
6132 enabled: true
6133 index_timeout: 120
6134"#;
6135 let config: Config = serde_yaml_ng::from_str(yaml_obj).unwrap();
6136 let crates_cfg = config.crates[0].publish.as_ref().unwrap().crates_config();
6137 assert!(crates_cfg.enabled);
6138 assert_eq!(crates_cfg.index_timeout, 120);
6139 }
6140
6141 #[test]
6144 fn test_make_latest_auto() {
6145 let yaml = r#"
6146project_name: test
6147crates:
6148 - name: a
6149 path: "."
6150 tag_template: "v{{ .Version }}"
6151 release:
6152 make_latest: auto
6153"#;
6154 let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
6155 let release = config.crates[0].release.as_ref().unwrap();
6156 assert_eq!(release.make_latest, Some(MakeLatestConfig::Auto));
6157 }
6158
6159 #[test]
6160 fn test_make_latest_true() {
6161 let yaml = r#"
6162project_name: test
6163crates:
6164 - name: a
6165 path: "."
6166 tag_template: "v{{ .Version }}"
6167 release:
6168 make_latest: true
6169"#;
6170 let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
6171 let release = config.crates[0].release.as_ref().unwrap();
6172 assert_eq!(release.make_latest, Some(MakeLatestConfig::Bool(true)));
6173 }
6174
6175 #[test]
6176 fn test_make_latest_false() {
6177 let yaml = r#"
6178project_name: test
6179crates:
6180 - name: a
6181 path: "."
6182 tag_template: "v{{ .Version }}"
6183 release:
6184 make_latest: false
6185"#;
6186 let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
6187 let release = config.crates[0].release.as_ref().unwrap();
6188 assert_eq!(release.make_latest, Some(MakeLatestConfig::Bool(false)));
6189 }
6190
6191 #[test]
6192 fn test_make_latest_omitted() {
6193 let yaml = r#"
6194project_name: test
6195crates:
6196 - name: a
6197 path: "."
6198 tag_template: "v{{ .Version }}"
6199 release:
6200 draft: false
6201"#;
6202 let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
6203 let release = config.crates[0].release.as_ref().unwrap();
6204 assert_eq!(release.make_latest, None);
6205 }
6206
6207 #[test]
6208 fn test_make_latest_template_string() {
6209 let yaml = r#"
6210project_name: test
6211crates:
6212 - name: a
6213 path: "."
6214 tag_template: "v{{ .Version }}"
6215 release:
6216 make_latest: "{{ if .IsSnapshot }}false{{ else }}true{{ end }}"
6217"#;
6218 let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
6219 let release = config.crates[0].release.as_ref().unwrap();
6220 assert_eq!(
6221 release.make_latest,
6222 Some(MakeLatestConfig::String(
6223 "{{ if .IsSnapshot }}false{{ else }}true{{ end }}".to_string()
6224 ))
6225 );
6226 }
6227
6228 #[test]
6229 fn test_make_latest_string_true() {
6230 let yaml = r#"
6232project_name: test
6233crates:
6234 - name: a
6235 path: "."
6236 tag_template: "v{{ .Version }}"
6237 release:
6238 make_latest: "true"
6239"#;
6240 let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
6241 let release = config.crates[0].release.as_ref().unwrap();
6242 assert_eq!(release.make_latest, Some(MakeLatestConfig::Bool(true)));
6243 }
6244
6245 #[test]
6246 fn test_make_latest_string_false() {
6247 let yaml = r#"
6249project_name: test
6250crates:
6251 - name: a
6252 path: "."
6253 tag_template: "v{{ .Version }}"
6254 release:
6255 make_latest: "false"
6256"#;
6257 let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
6258 let release = config.crates[0].release.as_ref().unwrap();
6259 assert_eq!(release.make_latest, Some(MakeLatestConfig::Bool(false)));
6260 }
6261
6262 #[test]
6265 fn test_changelog_header_footer() {
6266 let yaml = r##"
6267project_name: test
6268changelog:
6269 header: "# My Release Notes"
6270 footer: "---\nGenerated by anodizer"
6271crates:
6272 - name: a
6273 path: "."
6274 tag_template: "v{{ .Version }}"
6275"##;
6276 let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
6277 let cl = config.changelog.as_ref().unwrap();
6278 assert_eq!(cl.header, Some("# My Release Notes".to_string()));
6279 assert_eq!(cl.footer, Some("---\nGenerated by anodizer".to_string()));
6280 }
6281
6282 #[test]
6283 fn test_changelog_disable() {
6284 let yaml = r#"
6285project_name: test
6286changelog:
6287 disable: true
6288crates:
6289 - name: a
6290 path: "."
6291 tag_template: "v{{ .Version }}"
6292"#;
6293 let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
6294 let cl = config.changelog.as_ref().unwrap();
6295 assert_eq!(cl.disable, Some(StringOrBool::Bool(true)));
6296 }
6297
6298 #[test]
6299 fn test_changelog_disable_false() {
6300 let yaml = r#"
6301project_name: test
6302changelog:
6303 disable: false
6304 sort: desc
6305crates:
6306 - name: a
6307 path: "."
6308 tag_template: "v{{ .Version }}"
6309"#;
6310 let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
6311 let cl = config.changelog.as_ref().unwrap();
6312 assert_eq!(cl.disable, Some(StringOrBool::Bool(false)));
6313 assert_eq!(cl.sort, Some("desc".to_string()));
6314 }
6315
6316 #[test]
6319 fn test_checksum_disable() {
6320 let yaml = r#"
6321project_name: test
6322defaults:
6323 checksum:
6324 disable: true
6325crates:
6326 - name: a
6327 path: "."
6328 tag_template: "v{{ .Version }}"
6329"#;
6330 let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
6331 let checksum = config.defaults.as_ref().unwrap().checksum.as_ref().unwrap();
6332 assert_eq!(checksum.disable, Some(StringOrBool::Bool(true)));
6333 }
6334
6335 #[test]
6336 fn test_checksum_disable_per_crate() {
6337 let yaml = r#"
6338project_name: test
6339crates:
6340 - name: a
6341 path: "."
6342 tag_template: "v{{ .Version }}"
6343 checksum:
6344 disable: true
6345 algorithm: sha512
6346"#;
6347 let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
6348 let checksum = config.crates[0].checksum.as_ref().unwrap();
6349 assert_eq!(checksum.disable, Some(StringOrBool::Bool(true)));
6350 assert_eq!(checksum.algorithm, Some("sha512".to_string()));
6351 }
6352
6353 #[test]
6354 fn test_checksum_disable_template_string() {
6355 let yaml = r#"
6356project_name: test
6357defaults:
6358 checksum:
6359 disable: "{{ if .IsSnapshot }}true{{ end }}"
6360crates:
6361 - name: a
6362 path: "."
6363 tag_template: "v{{ .Version }}"
6364"#;
6365 let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
6366 let checksum = config.defaults.as_ref().unwrap().checksum.as_ref().unwrap();
6367 match &checksum.disable {
6368 Some(StringOrBool::String(s)) => {
6369 assert!(s.contains("IsSnapshot"));
6370 }
6371 other => panic!("expected StringOrBool::String, got {:?}", other),
6372 }
6373 }
6374
6375 #[test]
6376 fn test_checksum_extra_files_object_form() {
6377 let yaml = r#"
6378project_name: test
6379crates:
6380 - name: a
6381 path: "."
6382 tag_template: "v{{ .Version }}"
6383 checksum:
6384 extra_files:
6385 - "dist/*.bin"
6386 - glob: "release/*.deb"
6387 name_template: "{{ .ArtifactName }}.checksum"
6388"#;
6389 let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
6390 let checksum = config.crates[0].checksum.as_ref().unwrap();
6391 let extra = checksum.extra_files.as_ref().unwrap();
6392 assert_eq!(extra.len(), 2);
6393 assert_eq!(extra[0], ExtraFileSpec::Glob("dist/*.bin".to_string()));
6394 match &extra[1] {
6395 ExtraFileSpec::Detailed {
6396 glob,
6397 name_template,
6398 } => {
6399 assert_eq!(glob, "release/*.deb");
6400 assert_eq!(
6401 name_template.as_deref(),
6402 Some("{{ .ArtifactName }}.checksum")
6403 );
6404 }
6405 other => panic!("expected ExtraFileSpec::Detailed, got {:?}", other),
6406 }
6407 }
6408
6409 #[test]
6412 fn test_make_latest_serialize_roundtrip() {
6413 let auto = MakeLatestConfig::Auto;
6414 let json = serde_json::to_string(&auto).unwrap();
6415 assert_eq!(json, "\"auto\"");
6416
6417 let bool_true = MakeLatestConfig::Bool(true);
6418 let json = serde_json::to_string(&bool_true).unwrap();
6419 assert_eq!(json, "true");
6420
6421 let bool_false = MakeLatestConfig::Bool(false);
6422 let json = serde_json::to_string(&bool_false).unwrap();
6423 assert_eq!(json, "false");
6424
6425 let tmpl = MakeLatestConfig::String(
6426 "{{ if .IsSnapshot }}false{{ else }}true{{ end }}".to_string(),
6427 );
6428 let json = serde_json::to_string(&tmpl).unwrap();
6429 assert_eq!(json, "\"{{ if .IsSnapshot }}false{{ else }}true{{ end }}\"");
6430 }
6431
6432 #[test]
6435 fn test_release_header_footer_inline() {
6436 let yaml = r###"
6437project_name: test
6438crates:
6439 - name: a
6440 path: "."
6441 tag_template: "v{{ .Version }}"
6442 release:
6443 header: "## Custom Header"
6444 footer: "---\nPowered by anodizer"
6445"###;
6446 let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
6447 let release = config.crates[0].release.as_ref().unwrap();
6448 assert_eq!(
6449 release.header,
6450 Some(ContentSource::Inline("## Custom Header".to_string()))
6451 );
6452 assert_eq!(
6453 release.footer,
6454 Some(ContentSource::Inline(
6455 "---\nPowered by anodizer".to_string()
6456 ))
6457 );
6458 }
6459
6460 #[test]
6461 fn test_release_header_footer_from_file() {
6462 let yaml = r#"
6463project_name: test
6464crates:
6465 - name: a
6466 path: "."
6467 tag_template: "v{{ .Version }}"
6468 release:
6469 header:
6470 from_file: ./RELEASE_HEADER.md
6471 footer:
6472 from_file: ./RELEASE_FOOTER.md
6473"#;
6474 let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
6475 let release = config.crates[0].release.as_ref().unwrap();
6476 assert_eq!(
6477 release.header,
6478 Some(ContentSource::FromFile {
6479 from_file: "./RELEASE_HEADER.md".to_string()
6480 })
6481 );
6482 assert_eq!(
6483 release.footer,
6484 Some(ContentSource::FromFile {
6485 from_file: "./RELEASE_FOOTER.md".to_string()
6486 })
6487 );
6488 }
6489
6490 #[test]
6491 fn test_release_header_footer_from_url() {
6492 let yaml = r#"
6493project_name: test
6494crates:
6495 - name: a
6496 path: "."
6497 tag_template: "v{{ .Version }}"
6498 release:
6499 header:
6500 from_url: https://example.com/header.md
6501 footer:
6502 from_url: https://example.com/footer.md
6503"#;
6504 let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
6505 let release = config.crates[0].release.as_ref().unwrap();
6506 assert_eq!(
6507 release.header,
6508 Some(ContentSource::FromUrl {
6509 from_url: "https://example.com/header.md".to_string(),
6510 headers: None,
6511 })
6512 );
6513 assert_eq!(
6514 release.footer,
6515 Some(ContentSource::FromUrl {
6516 from_url: "https://example.com/footer.md".to_string(),
6517 headers: None,
6518 })
6519 );
6520 }
6521
6522 #[test]
6523 fn test_release_header_footer_omitted() {
6524 let yaml = r#"
6525project_name: test
6526crates:
6527 - name: a
6528 path: "."
6529 tag_template: "v{{ .Version }}"
6530 release:
6531 draft: false
6532"#;
6533 let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
6534 let release = config.crates[0].release.as_ref().unwrap();
6535 assert_eq!(release.header, None);
6536 assert_eq!(release.footer, None);
6537 }
6538
6539 #[test]
6542 fn test_release_extra_files_glob_strings() {
6543 let yaml = r#"
6544project_name: test
6545crates:
6546 - name: a
6547 path: "."
6548 tag_template: "v{{ .Version }}"
6549 release:
6550 extra_files:
6551 - "dist/*.sig"
6552 - "CHANGELOG.md"
6553"#;
6554 let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
6555 let release = config.crates[0].release.as_ref().unwrap();
6556 let files = release.extra_files.as_ref().unwrap();
6557 assert_eq!(files.len(), 2);
6558 assert_eq!(files[0], ExtraFileSpec::Glob("dist/*.sig".to_string()));
6559 assert_eq!(files[1], ExtraFileSpec::Glob("CHANGELOG.md".to_string()));
6560 }
6561
6562 #[test]
6563 fn test_release_extra_files_detailed_objects() {
6564 let yaml = r#"
6565project_name: test
6566crates:
6567 - name: a
6568 path: "."
6569 tag_template: "v{{ .Version }}"
6570 release:
6571 extra_files:
6572 - glob: "dist/*.sig"
6573 name_template: "{{ .ArtifactName }}.sig"
6574 - glob: "docs/*.pdf"
6575"#;
6576 let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
6577 let release = config.crates[0].release.as_ref().unwrap();
6578 let files = release.extra_files.as_ref().unwrap();
6579 assert_eq!(files.len(), 2);
6580 assert_eq!(files[0].glob(), "dist/*.sig");
6581 assert_eq!(files[0].name_template(), Some("{{ .ArtifactName }}.sig"));
6582 assert_eq!(files[1].glob(), "docs/*.pdf");
6583 assert_eq!(files[1].name_template(), None);
6584 }
6585
6586 #[test]
6587 fn test_release_extra_files_mixed() {
6588 let yaml = r#"
6589project_name: test
6590crates:
6591 - name: a
6592 path: "."
6593 tag_template: "v{{ .Version }}"
6594 release:
6595 extra_files:
6596 - "dist/*.sig"
6597 - glob: "docs/*.pdf"
6598 name_template: "{{ .ArtifactName }}"
6599"#;
6600 let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
6601 let release = config.crates[0].release.as_ref().unwrap();
6602 let files = release.extra_files.as_ref().unwrap();
6603 assert_eq!(files.len(), 2);
6604 assert_eq!(files[0], ExtraFileSpec::Glob("dist/*.sig".to_string()));
6605 assert_eq!(files[1].glob(), "docs/*.pdf");
6606 }
6607
6608 #[test]
6609 fn test_release_extra_files_omitted() {
6610 let yaml = r#"
6611project_name: test
6612crates:
6613 - name: a
6614 path: "."
6615 tag_template: "v{{ .Version }}"
6616 release:
6617 draft: true
6618"#;
6619 let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
6620 let release = config.crates[0].release.as_ref().unwrap();
6621 assert_eq!(release.extra_files, None);
6622 }
6623
6624 #[test]
6627 fn test_release_templated_extra_files_parsed() {
6628 let yaml = r#"
6629project_name: test
6630crates:
6631 - name: a
6632 path: "."
6633 tag_template: "v{{ .Version }}"
6634 release:
6635 templated_extra_files:
6636 - src: LICENSE.tpl
6637 dst: LICENSE.txt
6638 - src: README.md.tpl
6639"#;
6640 let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
6641 let release = config.crates[0].release.as_ref().unwrap();
6642 let tpl = release.templated_extra_files.as_ref().unwrap();
6643 assert_eq!(tpl.len(), 2);
6644 assert_eq!(tpl[0].src, "LICENSE.tpl");
6645 assert_eq!(tpl[0].dst.as_deref(), Some("LICENSE.txt"));
6646 assert_eq!(tpl[1].src, "README.md.tpl");
6647 assert_eq!(tpl[1].dst, None);
6648 }
6649
6650 #[test]
6651 fn test_release_templated_extra_files_defaults_to_none() {
6652 let yaml = r#"
6653project_name: test
6654crates:
6655 - name: a
6656 path: "."
6657 tag_template: "v{{ .Version }}"
6658 release:
6659 draft: true
6660"#;
6661 let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
6662 let release = config.crates[0].release.as_ref().unwrap();
6663 assert_eq!(release.templated_extra_files, None);
6664 }
6665
6666 #[test]
6667 fn test_checksum_templated_extra_files_parsed() {
6668 let yaml = r#"
6669name_template: "checksums.txt"
6670templated_extra_files:
6671 - src: "notes.tpl"
6672 dst: "RELEASE_NOTES.txt"
6673"#;
6674 let cfg: ChecksumConfig = serde_yaml_ng::from_str(yaml).unwrap();
6675 let tpl = cfg.templated_extra_files.as_ref().unwrap();
6676 assert_eq!(tpl.len(), 1);
6677 assert_eq!(tpl[0].src, "notes.tpl");
6678 assert_eq!(tpl[0].dst.as_deref(), Some("RELEASE_NOTES.txt"));
6679 }
6680
6681 #[test]
6684 fn test_release_skip_upload() {
6685 let yaml = r#"
6686project_name: test
6687crates:
6688 - name: a
6689 path: "."
6690 tag_template: "v{{ .Version }}"
6691 release:
6692 skip_upload: true
6693"#;
6694 let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
6695 let release = config.crates[0].release.as_ref().unwrap();
6696 assert_eq!(release.skip_upload, Some(StringOrBool::Bool(true)));
6697 }
6698
6699 #[test]
6700 fn test_release_skip_upload_false() {
6701 let yaml = r#"
6702project_name: test
6703crates:
6704 - name: a
6705 path: "."
6706 tag_template: "v{{ .Version }}"
6707 release:
6708 skip_upload: false
6709"#;
6710 let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
6711 let release = config.crates[0].release.as_ref().unwrap();
6712 assert_eq!(release.skip_upload, Some(StringOrBool::Bool(false)));
6713 }
6714
6715 #[test]
6716 fn test_release_skip_upload_auto() {
6717 let yaml = r#"
6718project_name: test
6719crates:
6720 - name: a
6721 path: "."
6722 tag_template: "v{{ .Version }}"
6723 release:
6724 skip_upload: "auto"
6725"#;
6726 let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
6727 let release = config.crates[0].release.as_ref().unwrap();
6728 assert_eq!(
6729 release.skip_upload,
6730 Some(StringOrBool::String("auto".to_string()))
6731 );
6732 }
6733
6734 #[test]
6737 fn test_release_replace_existing_draft() {
6738 let yaml = r#"
6739project_name: test
6740crates:
6741 - name: a
6742 path: "."
6743 tag_template: "v{{ .Version }}"
6744 release:
6745 replace_existing_draft: true
6746"#;
6747 let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
6748 let release = config.crates[0].release.as_ref().unwrap();
6749 assert_eq!(release.replace_existing_draft, Some(true));
6750 }
6751
6752 #[test]
6753 fn test_release_replace_existing_artifacts() {
6754 let yaml = r#"
6755project_name: test
6756crates:
6757 - name: a
6758 path: "."
6759 tag_template: "v{{ .Version }}"
6760 release:
6761 replace_existing_artifacts: true
6762"#;
6763 let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
6764 let release = config.crates[0].release.as_ref().unwrap();
6765 assert_eq!(release.replace_existing_artifacts, Some(true));
6766 }
6767
6768 #[test]
6771 fn test_release_tag_override_parsed() {
6772 let yaml = r#"
6773project_name: test
6774crates:
6775 - name: a
6776 path: "."
6777 tag_template: "myapp/v{{ .Version }}"
6778 release:
6779 tag: "v{{ .Version }}"
6780"#;
6781 let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
6782 let release = config.crates[0].release.as_ref().unwrap();
6783 assert_eq!(release.tag, Some("v{{ .Version }}".to_string()));
6784 }
6785
6786 #[test]
6787 fn test_release_tag_override_omitted() {
6788 let yaml = r#"
6789project_name: test
6790crates:
6791 - name: a
6792 path: "."
6793 tag_template: "v{{ .Version }}"
6794 release:
6795 draft: false
6796"#;
6797 let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
6798 let release = config.crates[0].release.as_ref().unwrap();
6799 assert_eq!(release.tag, None);
6800 }
6801
6802 #[test]
6803 fn test_release_all_new_fields() {
6804 let yaml = r##"
6805project_name: test
6806crates:
6807 - name: a
6808 path: "."
6809 tag_template: "v{{ .Version }}"
6810 release:
6811 github:
6812 owner: myorg
6813 name: myrepo
6814 draft: true
6815 make_latest: auto
6816 header: "# Release Notes"
6817 footer: "Thank you!"
6818 extra_files:
6819 - "dist/extra.zip"
6820 skip_upload: false
6821 replace_existing_draft: true
6822 replace_existing_artifacts: false
6823 target_commitish: main
6824 discussion_category_name: Announcements
6825 include_meta: true
6826 use_existing_draft: false
6827 tag: "v{{ .Version }}"
6828"##;
6829 let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
6830 let release = config.crates[0].release.as_ref().unwrap();
6831 assert_eq!(
6832 release.header,
6833 Some(ContentSource::Inline("# Release Notes".to_string()))
6834 );
6835 assert_eq!(
6836 release.footer,
6837 Some(ContentSource::Inline("Thank you!".to_string()))
6838 );
6839 assert_eq!(
6840 release.extra_files.as_ref().unwrap(),
6841 &[ExtraFileSpec::Glob("dist/extra.zip".to_string())]
6842 );
6843 assert_eq!(release.skip_upload, Some(StringOrBool::Bool(false)));
6844 assert_eq!(release.replace_existing_draft, Some(true));
6845 assert_eq!(release.replace_existing_artifacts, Some(false));
6846 assert_eq!(release.make_latest, Some(MakeLatestConfig::Auto));
6847 assert_eq!(release.target_commitish, Some("main".to_string()));
6848 assert_eq!(
6849 release.discussion_category_name,
6850 Some("Announcements".to_string())
6851 );
6852 assert_eq!(release.include_meta, Some(true));
6853 assert_eq!(release.use_existing_draft, Some(false));
6854 assert_eq!(release.tag, Some("v{{ .Version }}".to_string()));
6855 }
6856
6857 #[test]
6860 fn test_signs_single_object_backward_compat() {
6861 let yaml = r#"
6862project_name: test
6863sign:
6864 artifacts: all
6865 cmd: gpg
6866 args:
6867 - "--detach-sig"
6868crates:
6869 - name: a
6870 path: "."
6871 tag_template: "v{{ .Version }}"
6872"#;
6873 let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
6874 assert_eq!(config.signs.len(), 1);
6875 assert_eq!(config.signs[0].artifacts, Some("all".to_string()));
6876 assert_eq!(config.signs[0].cmd, Some("gpg".to_string()));
6877 assert_eq!(config.signs[0].args.as_ref().unwrap().len(), 1);
6878 }
6879
6880 #[test]
6881 fn test_signs_array_format() {
6882 let yaml = r#"
6883project_name: test
6884signs:
6885 - id: gpg-sign
6886 artifacts: checksum
6887 cmd: gpg
6888 args:
6889 - "--detach-sig"
6890 - id: cosign-sign
6891 artifacts: binary
6892 cmd: cosign
6893 args:
6894 - "sign"
6895crates:
6896 - name: a
6897 path: "."
6898 tag_template: "v{{ .Version }}"
6899"#;
6900 let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
6901 assert_eq!(config.signs.len(), 2);
6902 assert_eq!(config.signs[0].id, Some("gpg-sign".to_string()));
6903 assert_eq!(config.signs[0].artifacts, Some("checksum".to_string()));
6904 assert_eq!(config.signs[1].id, Some("cosign-sign".to_string()));
6905 assert_eq!(config.signs[1].artifacts, Some("binary".to_string()));
6906 }
6907
6908 #[test]
6909 fn test_signs_omitted_is_empty() {
6910 let yaml = r#"
6911project_name: test
6912crates:
6913 - name: a
6914 path: "."
6915 tag_template: "v{{ .Version }}"
6916"#;
6917 let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
6918 assert!(config.signs.is_empty());
6919 }
6920
6921 #[test]
6922 fn test_signs_new_fields() {
6923 let yaml = r#"
6924project_name: test
6925signs:
6926 - id: my-signer
6927 artifacts: archive
6928 cmd: gpg
6929 args:
6930 - "--detach-sig"
6931 signature: "{{ .Artifact }}.asc"
6932 stdin: "my-passphrase"
6933 ids:
6934 - my-archive
6935 - my-binary
6936crates:
6937 - name: a
6938 path: "."
6939 tag_template: "v{{ .Version }}"
6940"#;
6941 let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
6942 assert_eq!(config.signs.len(), 1);
6943 let sign = &config.signs[0];
6944 assert_eq!(sign.id, Some("my-signer".to_string()));
6945 assert_eq!(sign.artifacts, Some("archive".to_string()));
6946 assert_eq!(sign.signature, Some("{{ .Artifact }}.asc".to_string()));
6947 assert_eq!(sign.stdin, Some("my-passphrase".to_string()));
6948 assert_eq!(sign.ids.as_ref().unwrap().len(), 2);
6949 assert_eq!(sign.ids.as_ref().unwrap()[0], "my-archive");
6950 }
6951
6952 #[test]
6953 fn test_signs_stdin_file_field() {
6954 let yaml = r#"
6955project_name: test
6956signs:
6957 - artifacts: all
6958 cmd: gpg
6959 stdin_file: "/path/to/passphrase.txt"
6960crates:
6961 - name: a
6962 path: "."
6963 tag_template: "v{{ .Version }}"
6964"#;
6965 let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
6966 assert_eq!(config.signs.len(), 1);
6967 assert_eq!(
6968 config.signs[0].stdin_file,
6969 Some("/path/to/passphrase.txt".to_string())
6970 );
6971 }
6972
6973 #[test]
6974 fn test_signs_single_object_with_new_fields() {
6975 let yaml = r#"
6976project_name: test
6977sign:
6978 id: default
6979 artifacts: package
6980 cmd: gpg
6981 signature: "{{ .Artifact }}.sig"
6982 stdin: "pass"
6983 ids:
6984 - pkg-id
6985crates:
6986 - name: a
6987 path: "."
6988 tag_template: "v{{ .Version }}"
6989"#;
6990 let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
6991 assert_eq!(config.signs.len(), 1);
6992 let sign = &config.signs[0];
6993 assert_eq!(sign.id, Some("default".to_string()));
6994 assert_eq!(sign.artifacts, Some("package".to_string()));
6995 assert_eq!(sign.signature, Some("{{ .Artifact }}.sig".to_string()));
6996 assert_eq!(sign.stdin, Some("pass".to_string()));
6997 assert_eq!(sign.ids.as_ref().unwrap(), &["pkg-id"]);
6998 }
6999
7000 #[test]
7001 fn test_signs_toml_single_object() {
7002 let toml_str = r#"
7003project_name = "test"
7004
7005[sign]
7006artifacts = "checksum"
7007cmd = "gpg"
7008
7009[[crates]]
7010name = "a"
7011path = "."
7012tag_template = "v{{ .Version }}"
7013"#;
7014 let config: Config = toml::from_str(toml_str).unwrap();
7015 assert_eq!(config.signs.len(), 1);
7016 assert_eq!(config.signs[0].artifacts, Some("checksum".to_string()));
7017 }
7018
7019 #[test]
7020 fn test_signs_toml_array() {
7021 let toml_str = r#"
7022project_name = "test"
7023
7024[[signs]]
7025id = "first"
7026artifacts = "all"
7027cmd = "gpg"
7028
7029[[signs]]
7030id = "second"
7031artifacts = "binary"
7032cmd = "cosign"
7033
7034[[crates]]
7035name = "a"
7036path = "."
7037tag_template = "v{{ .Version }}"
7038"#;
7039 let config: Config = toml::from_str(toml_str).unwrap();
7040 assert_eq!(config.signs.len(), 2);
7041 assert_eq!(config.signs[0].id, Some("first".to_string()));
7042 assert_eq!(config.signs[1].id, Some("second".to_string()));
7043 }
7044
7045 #[test]
7046 fn test_signs_default_config_has_empty_signs() {
7047 let config = Config::default();
7048 assert!(config.signs.is_empty());
7049 }
7050
7051 #[test]
7054 fn test_report_sizes_true() {
7055 let yaml = r#"
7056project_name: test
7057report_sizes: true
7058crates:
7059 - name: a
7060 path: "."
7061 tag_template: "v{{ .Version }}"
7062"#;
7063 let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
7064 assert_eq!(config.report_sizes, Some(true));
7065 }
7066
7067 #[test]
7068 fn test_report_sizes_false() {
7069 let yaml = r#"
7070project_name: test
7071report_sizes: false
7072crates:
7073 - name: a
7074 path: "."
7075 tag_template: "v{{ .Version }}"
7076"#;
7077 let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
7078 assert_eq!(config.report_sizes, Some(false));
7079 }
7080
7081 #[test]
7082 fn test_report_sizes_omitted() {
7083 let yaml = r#"
7084project_name: test
7085crates:
7086 - name: a
7087 path: "."
7088 tag_template: "v{{ .Version }}"
7089"#;
7090 let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
7091 assert_eq!(config.report_sizes, None);
7092 }
7093
7094 #[test]
7097 fn test_env_field_parsed() {
7098 let yaml = r#"
7099project_name: test
7100env:
7101 MY_VAR: hello
7102 DEPLOY_ENV: staging
7103crates:
7104 - name: a
7105 path: "."
7106 tag_template: "v{{ .Version }}"
7107"#;
7108 let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
7109 let env = config.env.as_ref().unwrap();
7110 assert_eq!(env.get("MY_VAR").unwrap(), "hello");
7111 assert_eq!(env.get("DEPLOY_ENV").unwrap(), "staging");
7112 }
7113
7114 #[test]
7115 fn test_env_field_omitted() {
7116 let yaml = r#"
7117project_name: test
7118crates:
7119 - name: a
7120 path: "."
7121 tag_template: "v{{ .Version }}"
7122"#;
7123 let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
7124 assert_eq!(config.env, None);
7125 }
7126
7127 #[test]
7128 fn test_env_field_toml() {
7129 let toml_str = r#"
7130project_name = "test"
7131
7132[env]
7133API_KEY = "secret123"
7134STAGE = "prod"
7135
7136[[crates]]
7137name = "a"
7138path = "."
7139tag_template = "v{{ .Version }}"
7140"#;
7141 let config: Config = toml::from_str(toml_str).unwrap();
7142 let env = config.env.as_ref().unwrap();
7143 assert_eq!(env.get("API_KEY").unwrap(), "secret123");
7144 assert_eq!(env.get("STAGE").unwrap(), "prod");
7145 }
7146
7147 #[test]
7148 fn test_env_list_form_toml() {
7149 let toml_str = r#"
7150project_name = "test"
7151env = ["MY_VAR=hello", "STAGE=prod"]
7152crates = []
7153"#;
7154 let config: Config = toml::from_str(toml_str).unwrap();
7155 let env = config.env.as_ref().unwrap();
7156 assert_eq!(env.get("MY_VAR").unwrap(), "hello");
7157 assert_eq!(env.get("STAGE").unwrap(), "prod");
7158 }
7159
7160 #[test]
7163 fn test_env_list_form_parsed() {
7164 let yaml = r#"
7165project_name: test
7166env:
7167 - MY_VAR=hello
7168 - DEPLOY_ENV=staging
7169crates: []
7170"#;
7171 let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
7172 let env = config.env.as_ref().unwrap();
7173 assert_eq!(env.get("MY_VAR").unwrap(), "hello");
7174 assert_eq!(env.get("DEPLOY_ENV").unwrap(), "staging");
7175 }
7176
7177 #[test]
7178 fn test_env_list_form_with_template_expressions() {
7179 let yaml = r#"
7180project_name: test
7181env:
7182 - "MY_VERSION={{ .Tag }}"
7183 - "BUILD_DATE={{ .Date }}"
7184crates: []
7185"#;
7186 let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
7187 let env = config.env.as_ref().unwrap();
7188 assert_eq!(env.get("MY_VERSION").unwrap(), "{{ .Tag }}");
7190 assert_eq!(env.get("BUILD_DATE").unwrap(), "{{ .Date }}");
7191 }
7192
7193 #[test]
7194 fn test_env_list_form_value_with_equals() {
7195 let yaml = r#"
7197project_name: test
7198env:
7199 - "LDFLAGS=-X main.version=1.0.0"
7200crates: []
7201"#;
7202 let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
7203 let env = config.env.as_ref().unwrap();
7204 assert_eq!(
7205 env.get("LDFLAGS").unwrap(),
7206 "-X main.version=1.0.0",
7207 "only first = should split key from value"
7208 );
7209 }
7210
7211 #[test]
7212 fn test_env_list_form_empty_value() {
7213 let yaml = r#"
7214project_name: test
7215env:
7216 - "EMPTY_VAR="
7217crates: []
7218"#;
7219 let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
7220 let env = config.env.as_ref().unwrap();
7221 assert_eq!(env.get("EMPTY_VAR").unwrap(), "");
7222 }
7223
7224 #[test]
7225 fn test_env_list_form_no_equals_is_error() {
7226 let yaml = r#"
7227project_name: test
7228env:
7229 - "NO_EQUALS"
7230crates: []
7231"#;
7232 let result: Result<Config, _> = serde_yaml_ng::from_str(yaml);
7233 assert!(result.is_err(), "list entries without = should be rejected");
7234 let err = result.unwrap_err().to_string();
7235 assert!(
7236 err.contains("KEY=VALUE"),
7237 "error should mention KEY=VALUE format, got: {}",
7238 err
7239 );
7240 }
7241
7242 #[test]
7243 fn test_env_list_form_empty_key_is_error() {
7244 let yaml = r#"
7245project_name: test
7246env:
7247 - "=orphan_value"
7248crates: []
7249"#;
7250 let result: Result<Config, _> = serde_yaml_ng::from_str(yaml);
7251 assert!(
7252 result.is_err(),
7253 "list entries with empty key should be rejected"
7254 );
7255 let err = result.unwrap_err().to_string();
7256 assert!(
7257 err.contains("empty key"),
7258 "error should mention empty key, got: {}",
7259 err
7260 );
7261 }
7262
7263 #[test]
7264 fn test_env_list_form_last_wins_on_duplicates() {
7265 let yaml = r#"
7266project_name: test
7267env:
7268 - "DUPED=first"
7269 - "DUPED=second"
7270crates: []
7271"#;
7272 let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
7273 let env = config.env.as_ref().unwrap();
7274 assert_eq!(
7275 env.get("DUPED").unwrap(),
7276 "second",
7277 "later entries should override earlier ones"
7278 );
7279 }
7280
7281 #[test]
7282 fn test_workspace_env_list_form() {
7283 let yaml = r#"
7284project_name: test
7285crates: []
7286workspaces:
7287 - name: ws1
7288 crates: []
7289 env:
7290 - "WS_VAR=from-workspace"
7291 - "WS_BUILD={{ .Tag }}"
7292"#;
7293 let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
7294 let ws = &config.workspaces.as_ref().unwrap()[0];
7295 let env = ws.env.as_ref().unwrap();
7296 assert_eq!(env.get("WS_VAR").unwrap(), "from-workspace");
7297 assert_eq!(env.get("WS_BUILD").unwrap(), "{{ .Tag }}");
7298 }
7299
7300 #[test]
7303 fn test_malformed_yaml_syntax_error() {
7304 let yaml = r#"
7305project_name: test
7306crates:
7307 - name: a
7308 path: "."
7309 tag_template: "v{{ .Version }}"
7310 invalid_indentation
7311 this_is_broken: [
7312"#;
7313 let result: Result<Config, _> = serde_yaml_ng::from_str(yaml);
7314 assert!(result.is_err(), "malformed YAML should fail to parse");
7315 let err = result.unwrap_err().to_string();
7316 assert!(!err.is_empty(), "error message should not be empty");
7318 }
7319
7320 #[test]
7321 fn test_type_mismatch_string_where_array_expected() {
7322 let yaml = r#"
7323project_name: test
7324crates: "this should be an array"
7325"#;
7326 let result: Result<Config, _> = serde_yaml_ng::from_str(yaml);
7327 assert!(result.is_err(), "string where array expected should fail");
7328 let err = result.unwrap_err().to_string();
7329 assert!(
7330 err.contains("invalid type") || err.contains("expected a sequence"),
7331 "error should mention type mismatch, got: {err}"
7332 );
7333 }
7334
7335 #[test]
7336 fn test_type_mismatch_object_where_string_expected() {
7337 let yaml = r#"
7340project_name:
7341 nested: object
7342 another: field
7343crates:
7344 - name: a
7345 path: "."
7346 tag_template: "v{{ .Version }}"
7347"#;
7348 let result: Result<Config, _> = serde_yaml_ng::from_str(yaml);
7349 assert!(
7350 result.is_err(),
7351 "mapping where string expected should fail to parse"
7352 );
7353 let err = result.unwrap_err().to_string();
7354 assert!(
7355 err.contains("invalid type") || err.contains("expected a string"),
7356 "error should mention type mismatch, got: {err}"
7357 );
7358 }
7359
7360 #[test]
7361 fn test_type_mismatch_bool_where_array_expected_for_targets() {
7362 let yaml = r#"
7363project_name: test
7364defaults:
7365 targets: true
7366crates:
7367 - name: a
7368 path: "."
7369 tag_template: "v{{ .Version }}"
7370"#;
7371 let result: Result<Config, _> = serde_yaml_ng::from_str(yaml);
7372 assert!(
7373 result.is_err(),
7374 "bool where array expected for targets should fail"
7375 );
7376 let err = result.unwrap_err().to_string();
7377 assert!(
7378 err.contains("invalid type")
7379 || err.contains("expected a sequence")
7380 || err.contains("targets"),
7381 "error should mention type mismatch for targets, got: {err}"
7382 );
7383 }
7384
7385 #[test]
7386 fn test_invalid_cross_strategy_value() {
7387 let yaml = r#"
7388project_name: test
7389defaults:
7390 cross: invalid_strategy
7391crates:
7392 - name: a
7393 path: "."
7394 tag_template: "v{{ .Version }}"
7395"#;
7396 let result: Result<Config, _> = serde_yaml_ng::from_str(yaml);
7397 assert!(
7398 result.is_err(),
7399 "invalid cross strategy should fail to parse"
7400 );
7401 let err = result.unwrap_err().to_string();
7402 assert!(
7403 err.contains("unknown variant") || err.contains("invalid_strategy"),
7404 "error should mention the invalid variant, got: {err}"
7405 );
7406 }
7407
7408 #[test]
7409 fn test_prerelease_invalid_string_value() {
7410 let yaml = r#"
7411project_name: test
7412crates:
7413 - name: a
7414 path: "."
7415 tag_template: "v{{ .Version }}"
7416 release:
7417 prerelease: "always"
7418"#;
7419 let result: Result<Config, _> = serde_yaml_ng::from_str(yaml);
7420 assert!(
7421 result.is_err(),
7422 "prerelease: 'always' should fail (only 'auto' or bool accepted)"
7423 );
7424 let err = result.unwrap_err().to_string();
7425 assert!(
7426 err.contains("auto") || err.contains("always"),
7427 "error should mention expected values, got: {err}"
7428 );
7429 }
7430
7431 #[test]
7432 fn test_archives_true_is_invalid() {
7433 let yaml = r#"
7434project_name: test
7435crates:
7436 - name: a
7437 path: "."
7438 tag_template: "v{{ .Version }}"
7439 archives: true
7440"#;
7441 let result: Result<Config, _> = serde_yaml_ng::from_str(yaml);
7442 assert!(
7443 result.is_err(),
7444 "archives: true should be rejected (only false or array accepted)"
7445 );
7446 let err = result.unwrap_err().to_string();
7447 assert!(
7448 err.contains("true is not valid") || err.contains("false or a list"),
7449 "error should explain valid archives values, got: {err}"
7450 );
7451 }
7452
7453 #[test]
7454 fn test_completely_empty_yaml() {
7455 let yaml = "";
7458 let result: Result<Config, _> = serde_yaml_ng::from_str(yaml);
7459 let config =
7460 result.unwrap_or_else(|e| panic!("empty YAML should parse to Config defaults: {e}"));
7461 assert!(
7462 config.project_name.is_empty(),
7463 "default project_name should be empty"
7464 );
7465 assert!(config.crates.is_empty(), "default crates should be empty");
7466 assert_eq!(
7467 config.dist,
7468 std::path::PathBuf::from("./dist"),
7469 "default dist should be ./dist"
7470 );
7471 }
7472
7473 #[test]
7478 fn test_binstall_config_parsed() {
7479 let yaml = r#"
7480project_name: test
7481crates:
7482 - name: myapp
7483 path: "."
7484 tag_template: "v{{ .Version }}"
7485 binstall:
7486 enabled: true
7487 pkg_url: "https://example.com/{{ .Version }}/{ target }"
7488 bin_dir: "{ bin }{ binary-ext }"
7489 pkg_fmt: tgz
7490"#;
7491 let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
7492 let bs = config.crates[0].binstall.as_ref().unwrap();
7493 assert_eq!(bs.enabled, Some(true));
7494 assert_eq!(
7495 bs.pkg_url,
7496 Some("https://example.com/{{ .Version }}/{ target }".to_string())
7497 );
7498 assert_eq!(bs.bin_dir, Some("{ bin }{ binary-ext }".to_string()));
7499 assert_eq!(bs.pkg_fmt, Some("tgz".to_string()));
7500 }
7501
7502 #[test]
7503 fn test_binstall_config_omitted() {
7504 let yaml = r#"
7505project_name: test
7506crates:
7507 - name: myapp
7508 path: "."
7509 tag_template: "v{{ .Version }}"
7510"#;
7511 let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
7512 assert!(config.crates[0].binstall.is_none());
7513 }
7514
7515 #[test]
7516 fn test_binstall_config_partial() {
7517 let yaml = r#"
7518project_name: test
7519crates:
7520 - name: myapp
7521 path: "."
7522 tag_template: "v{{ .Version }}"
7523 binstall:
7524 enabled: true
7525"#;
7526 let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
7527 let bs = config.crates[0].binstall.as_ref().unwrap();
7528 assert_eq!(bs.enabled, Some(true));
7529 assert_eq!(bs.pkg_url, None);
7530 assert_eq!(bs.bin_dir, None);
7531 assert_eq!(bs.pkg_fmt, None);
7532 }
7533
7534 #[test]
7535 fn test_version_sync_config_parsed() {
7536 let yaml = r#"
7537project_name: test
7538crates:
7539 - name: myapp
7540 path: "."
7541 tag_template: "v{{ .Version }}"
7542 version_sync:
7543 enabled: true
7544 mode: tag
7545"#;
7546 let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
7547 let vs = config.crates[0].version_sync.as_ref().unwrap();
7548 assert_eq!(vs.enabled, Some(true));
7549 assert_eq!(vs.mode, Some("tag".to_string()));
7550 }
7551
7552 #[test]
7553 fn test_version_sync_config_explicit_mode() {
7554 let yaml = r#"
7555project_name: test
7556crates:
7557 - name: myapp
7558 path: "."
7559 tag_template: "v{{ .Version }}"
7560 version_sync:
7561 enabled: true
7562 mode: explicit
7563"#;
7564 let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
7565 let vs = config.crates[0].version_sync.as_ref().unwrap();
7566 assert_eq!(vs.mode, Some("explicit".to_string()));
7567 }
7568
7569 #[test]
7570 fn test_version_sync_config_omitted() {
7571 let yaml = r#"
7572project_name: test
7573crates:
7574 - name: myapp
7575 path: "."
7576 tag_template: "v{{ .Version }}"
7577"#;
7578 let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
7579 assert!(config.crates[0].version_sync.is_none());
7580 }
7581
7582 #[test]
7583 fn test_binstall_and_version_sync_together() {
7584 let yaml = r#"
7585project_name: test
7586crates:
7587 - name: myapp
7588 path: "."
7589 tag_template: "v{{ .Version }}"
7590 binstall:
7591 enabled: true
7592 pkg_fmt: zip
7593 version_sync:
7594 enabled: true
7595 mode: tag
7596"#;
7597 let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
7598 assert!(config.crates[0].binstall.is_some());
7599 assert!(config.crates[0].version_sync.is_some());
7600 }
7601
7602 #[test]
7603 fn test_binstall_config_toml() {
7604 let toml_str = r#"
7605project_name = "test"
7606
7607[[crates]]
7608name = "myapp"
7609path = "."
7610tag_template = "v{{ .Version }}"
7611
7612[crates.binstall]
7613enabled = true
7614pkg_url = "https://example.com"
7615pkg_fmt = "tgz"
7616"#;
7617 let config: Config = toml::from_str(toml_str).unwrap();
7618 let bs = config.crates[0].binstall.as_ref().unwrap();
7619 assert_eq!(bs.enabled, Some(true));
7620 assert_eq!(bs.pkg_url, Some("https://example.com".to_string()));
7621 }
7622
7623 #[test]
7624 fn test_version_sync_config_toml() {
7625 let toml_str = r#"
7626project_name = "test"
7627
7628[[crates]]
7629name = "myapp"
7630path = "."
7631tag_template = "v{{ .Version }}"
7632
7633[crates.version_sync]
7634enabled = true
7635mode = "tag"
7636"#;
7637 let config: Config = toml::from_str(toml_str).unwrap();
7638 let vs = config.crates[0].version_sync.as_ref().unwrap();
7639 assert_eq!(vs.enabled, Some(true));
7640 assert_eq!(vs.mode, Some("tag".to_string()));
7641 }
7642
7643 #[test]
7644 fn test_crate_config_default_has_none_binstall_version_sync() {
7645 let config = CrateConfig::default();
7646 assert!(config.binstall.is_none());
7647 assert!(config.version_sync.is_none());
7648 }
7649
7650 #[test]
7653 fn test_unknown_top_level_fields_rejected() {
7654 let yaml = r#"
7656project_name: test
7657unknown_top_level_field: "this should be rejected"
7658crates:
7659 - name: a
7660 path: "."
7661 tag_template: "v{{ .Version }}"
7662"#;
7663 let result: Result<Config, _> = serde_yaml_ng::from_str(yaml);
7664 assert!(
7665 result.is_err(),
7666 "unknown top-level fields should be rejected"
7667 );
7668 assert!(
7669 result.unwrap_err().to_string().contains("unknown field"),
7670 "error should mention unknown field"
7671 );
7672 }
7673
7674 #[test]
7675 fn test_unknown_crate_level_fields_ignored() {
7676 let yaml = r#"
7677project_name: test
7678crates:
7679 - name: a
7680 path: "."
7681 tag_template: "v{{ .Version }}"
7682 nonexistent_field: true
7683 something_else: "hello"
7684"#;
7685 let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
7686 assert_eq!(config.crates[0].name, "a");
7687 }
7688
7689 #[test]
7690 fn test_unknown_nested_fields_ignored() {
7691 let yaml = r#"
7692project_name: test
7693defaults:
7694 targets:
7695 - x86_64-unknown-linux-gnu
7696 unknown_default_field: "ignored"
7697changelog:
7698 sort: asc
7699 mystery_option: true
7700crates:
7701 - name: a
7702 path: "."
7703 tag_template: "v{{ .Version }}"
7704 checksum:
7705 algorithm: sha256
7706 future_field: "ignored"
7707"#;
7708 let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
7709 assert_eq!(
7710 config
7711 .defaults
7712 .as_ref()
7713 .unwrap()
7714 .targets
7715 .as_ref()
7716 .unwrap()
7717 .len(),
7718 1
7719 );
7720 assert_eq!(
7721 config.changelog.as_ref().unwrap().sort,
7722 Some("asc".to_string())
7723 );
7724 assert_eq!(
7725 config.crates[0].checksum.as_ref().unwrap().algorithm,
7726 Some("sha256".to_string())
7727 );
7728 }
7729
7730 #[test]
7733 fn test_build_config_reproducible_true() {
7734 let yaml = r#"
7735project_name: test
7736crates:
7737 - name: myapp
7738 path: "."
7739 tag_template: "v{{ .Version }}"
7740 builds:
7741 - binary: myapp
7742 reproducible: true
7743"#;
7744 let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
7745 let build = &config.crates[0].builds.as_ref().unwrap()[0];
7746 assert_eq!(build.reproducible, Some(true));
7747 }
7748
7749 #[test]
7750 fn test_build_config_reproducible_false() {
7751 let yaml = r#"
7752project_name: test
7753crates:
7754 - name: myapp
7755 path: "."
7756 tag_template: "v{{ .Version }}"
7757 builds:
7758 - binary: myapp
7759 reproducible: false
7760"#;
7761 let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
7762 let build = &config.crates[0].builds.as_ref().unwrap()[0];
7763 assert_eq!(build.reproducible, Some(false));
7764 }
7765
7766 #[test]
7767 fn test_build_config_reproducible_omitted() {
7768 let yaml = r#"
7769project_name: test
7770crates:
7771 - name: myapp
7772 path: "."
7773 tag_template: "v{{ .Version }}"
7774 builds:
7775 - binary: myapp
7776"#;
7777 let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
7778 let build = &config.crates[0].builds.as_ref().unwrap()[0];
7779 assert_eq!(build.reproducible, None);
7780 }
7781
7782 #[test]
7785 fn test_workspace_config_parses() {
7786 let yaml = r#"
7787project_name: monorepo
7788crates: []
7789workspaces:
7790 - name: frontend
7791 crates:
7792 - name: frontend-app
7793 path: "apps/frontend"
7794 tag_template: "frontend-v{{ .Version }}"
7795 changelog:
7796 sort: asc
7797 - name: backend
7798 crates:
7799 - name: backend-api
7800 path: "apps/backend"
7801 tag_template: "backend-v{{ .Version }}"
7802 - name: backend-worker
7803 path: "apps/worker"
7804 tag_template: "worker-v{{ .Version }}"
7805"#;
7806 let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
7807 let workspaces = config.workspaces.as_ref().unwrap();
7808 assert_eq!(workspaces.len(), 2);
7809 assert_eq!(workspaces[0].name, "frontend");
7810 assert_eq!(workspaces[0].crates.len(), 1);
7811 assert_eq!(workspaces[0].crates[0].name, "frontend-app");
7812 assert!(workspaces[0].changelog.is_some());
7813 assert_eq!(workspaces[1].name, "backend");
7814 assert_eq!(workspaces[1].crates.len(), 2);
7815 }
7816
7817 #[test]
7818 fn test_workspace_config_with_signs_and_hooks() {
7819 let yaml = r#"
7820project_name: monorepo
7821crates: []
7822workspaces:
7823 - name: myws
7824 crates:
7825 - name: mylib
7826 path: "."
7827 tag_template: "v{{ .Version }}"
7828 signs:
7829 - artifacts: all
7830 cmd: gpg
7831 before:
7832 hooks:
7833 - echo before
7834 after:
7835 hooks:
7836 - echo after
7837 env:
7838 MY_VAR: hello
7839"#;
7840 let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
7841 let ws = &config.workspaces.as_ref().unwrap()[0];
7842 assert_eq!(ws.name, "myws");
7843 assert_eq!(ws.signs.len(), 1);
7844 assert!(ws.before.is_some());
7845 assert!(ws.after.is_some());
7846 assert_eq!(ws.env.as_ref().unwrap().get("MY_VAR").unwrap(), "hello");
7847 }
7848
7849 #[test]
7850 fn test_workspace_config_omitted() {
7851 let yaml = r#"
7852project_name: simple
7853crates:
7854 - name: myapp
7855 path: "."
7856 tag_template: "v{{ .Version }}"
7857"#;
7858 let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
7859 assert!(config.workspaces.is_none());
7860 }
7861
7862 #[test]
7863 fn test_workspace_config_empty_array() {
7864 let yaml = r#"
7865project_name: test
7866crates: []
7867workspaces: []
7868"#;
7869 let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
7870 let workspaces = config.workspaces.as_ref().unwrap();
7871 assert!(workspaces.is_empty());
7872 }
7873
7874 #[test]
7877 fn test_chocolatey_config_yaml() {
7878 let yaml = r#"
7879project_name: test
7880crates:
7881 - name: mytool
7882 path: "."
7883 tag_template: "v{{ .Version }}"
7884 publish:
7885 chocolatey:
7886 project_repo:
7887 owner: myorg
7888 name: mytool
7889 description: "A great tool"
7890 license: MIT
7891 tags:
7892 - cli
7893 - tool
7894 authors: "Test Author"
7895 project_url: "https://github.com/myorg/mytool"
7896 icon_url: "https://example.com/icon.png"
7897"#;
7898 let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
7899 let choco = config.crates[0]
7900 .publish
7901 .as_ref()
7902 .unwrap()
7903 .chocolatey
7904 .as_ref()
7905 .unwrap();
7906
7907 let repo = choco.project_repo.as_ref().unwrap();
7908 assert_eq!(repo.owner, "myorg");
7909 assert_eq!(repo.name, "mytool");
7910 assert_eq!(choco.description, Some("A great tool".to_string()));
7911 assert_eq!(choco.license, Some("MIT".to_string()));
7912 assert_eq!(
7913 choco.tags,
7914 Some(vec!["cli".to_string(), "tool".to_string()])
7915 );
7916 assert_eq!(choco.authors, Some("Test Author".to_string()));
7917 assert_eq!(
7918 choco.project_url,
7919 Some("https://github.com/myorg/mytool".to_string())
7920 );
7921 assert_eq!(
7922 choco.icon_url,
7923 Some("https://example.com/icon.png".to_string())
7924 );
7925 }
7926
7927 #[test]
7928 fn test_chocolatey_config_minimal() {
7929 let yaml = r#"
7930project_name: test
7931crates:
7932 - name: mytool
7933 path: "."
7934 tag_template: "v{{ .Version }}"
7935 publish:
7936 chocolatey:
7937 project_repo:
7938 owner: myorg
7939 name: mytool
7940"#;
7941 let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
7942 let choco = config.crates[0]
7943 .publish
7944 .as_ref()
7945 .unwrap()
7946 .chocolatey
7947 .as_ref()
7948 .unwrap();
7949
7950 let repo = choco.project_repo.as_ref().unwrap();
7951 assert_eq!(repo.owner, "myorg");
7952 assert_eq!(repo.name, "mytool");
7953 assert!(choco.description.is_none());
7954 assert!(choco.license.is_none());
7955 assert!(choco.tags.is_none());
7956 assert!(choco.authors.is_none());
7957 assert!(choco.project_url.is_none());
7958 assert!(choco.icon_url.is_none());
7959 }
7960
7961 #[test]
7962 fn test_chocolatey_config_toml() {
7963 let toml_str = r#"
7964project_name = "test"
7965
7966[[crates]]
7967name = "mytool"
7968path = "."
7969tag_template = "v{{ .Version }}"
7970
7971[crates.publish.chocolatey]
7972description = "A tool"
7973license = "MIT"
7974authors = "Author"
7975tags = ["cli"]
7976
7977[crates.publish.chocolatey.project_repo]
7978owner = "org"
7979name = "tool"
7980"#;
7981 let config: Config = toml::from_str(toml_str).unwrap();
7982 let choco = config.crates[0]
7983 .publish
7984 .as_ref()
7985 .unwrap()
7986 .chocolatey
7987 .as_ref()
7988 .unwrap();
7989
7990 assert_eq!(choco.description, Some("A tool".to_string()));
7991 let repo = choco.project_repo.as_ref().unwrap();
7992 assert_eq!(repo.owner, "org");
7993 }
7994
7995 #[test]
7996 fn test_chocolatey_tags_space_separated_string() {
7997 let yaml = r#"
7999project_name: test
8000crates:
8001 - name: mytool
8002 path: "."
8003 tag_template: "v{{ .Version }}"
8004 publish:
8005 chocolatey:
8006 project_repo:
8007 owner: myorg
8008 name: mytool
8009 tags: "cli tool automation"
8010"#;
8011 let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
8012 let choco = config.crates[0]
8013 .publish
8014 .as_ref()
8015 .unwrap()
8016 .chocolatey
8017 .as_ref()
8018 .unwrap();
8019
8020 assert_eq!(
8021 choco.tags,
8022 Some(vec![
8023 "cli".to_string(),
8024 "tool".to_string(),
8025 "automation".to_string()
8026 ])
8027 );
8028 }
8029
8030 #[test]
8031 fn test_chocolatey_tags_empty_string_is_none() {
8032 let yaml = r#"
8033project_name: test
8034crates:
8035 - name: mytool
8036 path: "."
8037 tag_template: "v{{ .Version }}"
8038 publish:
8039 chocolatey:
8040 project_repo:
8041 owner: myorg
8042 name: mytool
8043 tags: ""
8044"#;
8045 let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
8046 let choco = config.crates[0]
8047 .publish
8048 .as_ref()
8049 .unwrap()
8050 .chocolatey
8051 .as_ref()
8052 .unwrap();
8053
8054 assert!(choco.tags.is_none());
8055 }
8056
8057 #[test]
8060 fn test_winget_config_yaml() {
8061 let yaml = r#"
8062project_name: test
8063crates:
8064 - name: mytool
8065 path: "."
8066 tag_template: "v{{ .Version }}"
8067 publish:
8068 winget:
8069 manifests_repo:
8070 owner: myorg
8071 name: winget-pkgs
8072 description: "A great tool"
8073 license: MIT
8074 package_identifier: "MyOrg.MyTool"
8075 publisher: "My Org"
8076 publisher_url: "https://github.com/myorg"
8077"#;
8078 let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
8079 let winget = config.crates[0]
8080 .publish
8081 .as_ref()
8082 .unwrap()
8083 .winget
8084 .as_ref()
8085 .unwrap();
8086
8087 let repo = winget.manifests_repo.as_ref().unwrap();
8088 assert_eq!(repo.owner, "myorg");
8089 assert_eq!(repo.name, "winget-pkgs");
8090 assert_eq!(winget.description, Some("A great tool".to_string()));
8091 assert_eq!(winget.license, Some("MIT".to_string()));
8092 assert_eq!(winget.package_identifier, Some("MyOrg.MyTool".to_string()));
8093 assert_eq!(winget.publisher, Some("My Org".to_string()));
8094 assert_eq!(
8095 winget.publisher_url,
8096 Some("https://github.com/myorg".to_string())
8097 );
8098 }
8099
8100 #[test]
8101 fn test_winget_config_minimal() {
8102 let yaml = r#"
8103project_name: test
8104crates:
8105 - name: mytool
8106 path: "."
8107 tag_template: "v{{ .Version }}"
8108 publish:
8109 winget:
8110 manifests_repo:
8111 owner: myorg
8112 name: winget-pkgs
8113 package_identifier: "MyOrg.MyTool"
8114"#;
8115 let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
8116 let winget = config.crates[0]
8117 .publish
8118 .as_ref()
8119 .unwrap()
8120 .winget
8121 .as_ref()
8122 .unwrap();
8123
8124 let repo = winget.manifests_repo.as_ref().unwrap();
8125 assert_eq!(repo.owner, "myorg");
8126 assert_eq!(repo.name, "winget-pkgs");
8127 assert_eq!(winget.package_identifier, Some("MyOrg.MyTool".to_string()));
8128 assert!(winget.description.is_none());
8129 assert!(winget.license.is_none());
8130 assert!(winget.publisher.is_none());
8131 assert!(winget.publisher_url.is_none());
8132 }
8133
8134 #[test]
8135 fn test_winget_config_toml() {
8136 let toml_str = r#"
8137project_name = "test"
8138
8139[[crates]]
8140name = "mytool"
8141path = "."
8142tag_template = "v{{ .Version }}"
8143
8144[crates.publish.winget]
8145description = "A tool"
8146license = "MIT"
8147package_identifier = "Org.Tool"
8148publisher = "Org"
8149
8150[crates.publish.winget.manifests_repo]
8151owner = "org"
8152name = "winget-pkgs"
8153"#;
8154 let config: Config = toml::from_str(toml_str).unwrap();
8155 let winget = config.crates[0]
8156 .publish
8157 .as_ref()
8158 .unwrap()
8159 .winget
8160 .as_ref()
8161 .unwrap();
8162
8163 assert_eq!(winget.description, Some("A tool".to_string()));
8164 assert_eq!(winget.package_identifier, Some("Org.Tool".to_string()));
8165 let repo = winget.manifests_repo.as_ref().unwrap();
8166 assert_eq!(repo.owner, "org");
8167 }
8168
8169 #[test]
8172 fn test_aur_config_yaml() {
8173 let yaml = r#"
8174project_name: test
8175crates:
8176 - name: mytool
8177 path: "."
8178 tag_template: "v{{ .Version }}"
8179 publish:
8180 aur:
8181 git_url: "ssh://aur@aur.archlinux.org/mytool.git"
8182 package_name: mytool-bin
8183 description: "A great tool"
8184 license: MIT
8185 maintainers:
8186 - "Jane Doe <jane@example.com>"
8187 depends:
8188 - glibc
8189 - openssl
8190 optdepends:
8191 - "git: for VCS support"
8192 conflicts:
8193 - mytool-git
8194 provides:
8195 - mytool
8196 replaces:
8197 - old-mytool
8198 backup:
8199 - etc/mytool/config.toml
8200 url: "https://github.com/org/mytool"
8201"#;
8202 let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
8203 let aur = config.crates[0]
8204 .publish
8205 .as_ref()
8206 .unwrap()
8207 .aur
8208 .as_ref()
8209 .unwrap();
8210
8211 assert_eq!(
8212 aur.git_url,
8213 Some("ssh://aur@aur.archlinux.org/mytool.git".to_string())
8214 );
8215 assert_eq!(aur.name, Some("mytool-bin".to_string()));
8216 assert_eq!(aur.description, Some("A great tool".to_string()));
8217 assert_eq!(aur.license, Some("MIT".to_string()));
8218 assert_eq!(
8219 aur.maintainers,
8220 Some(vec!["Jane Doe <jane@example.com>".to_string()])
8221 );
8222 assert_eq!(
8223 aur.depends,
8224 Some(vec!["glibc".to_string(), "openssl".to_string()])
8225 );
8226 assert_eq!(
8227 aur.optdepends,
8228 Some(vec!["git: for VCS support".to_string()])
8229 );
8230 assert_eq!(aur.conflicts, Some(vec!["mytool-git".to_string()]));
8231 assert_eq!(aur.provides, Some(vec!["mytool".to_string()]));
8232 assert_eq!(aur.replaces, Some(vec!["old-mytool".to_string()]));
8233 assert_eq!(aur.backup, Some(vec!["etc/mytool/config.toml".to_string()]));
8234 assert_eq!(aur.url, Some("https://github.com/org/mytool".to_string()));
8235 }
8236
8237 #[test]
8238 fn test_aur_config_minimal() {
8239 let yaml = r#"
8240project_name: test
8241crates:
8242 - name: mytool
8243 path: "."
8244 tag_template: "v{{ .Version }}"
8245 publish:
8246 aur:
8247 git_url: "ssh://aur@aur.archlinux.org/mytool.git"
8248"#;
8249 let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
8250 let aur = config.crates[0]
8251 .publish
8252 .as_ref()
8253 .unwrap()
8254 .aur
8255 .as_ref()
8256 .unwrap();
8257
8258 assert_eq!(
8259 aur.git_url,
8260 Some("ssh://aur@aur.archlinux.org/mytool.git".to_string())
8261 );
8262 assert!(aur.name.is_none());
8263 assert!(aur.description.is_none());
8264 assert!(aur.license.is_none());
8265 assert!(aur.maintainers.is_none());
8266 assert!(aur.depends.is_none());
8267 assert!(aur.optdepends.is_none());
8268 assert!(aur.conflicts.is_none());
8269 assert!(aur.provides.is_none());
8270 assert!(aur.replaces.is_none());
8271 assert!(aur.backup.is_none());
8272 }
8273
8274 #[test]
8275 fn test_aur_config_toml() {
8276 let toml_str = r#"
8277project_name = "test"
8278
8279[[crates]]
8280name = "mytool"
8281path = "."
8282tag_template = "v{{ .Version }}"
8283
8284[crates.publish.aur]
8285git_url = "ssh://aur@aur.archlinux.org/mytool.git"
8286description = "A tool"
8287license = "MIT"
8288depends = ["glibc"]
8289"#;
8290 let config: Config = toml::from_str(toml_str).unwrap();
8291 let aur = config.crates[0]
8292 .publish
8293 .as_ref()
8294 .unwrap()
8295 .aur
8296 .as_ref()
8297 .unwrap();
8298
8299 assert_eq!(
8300 aur.git_url,
8301 Some("ssh://aur@aur.archlinux.org/mytool.git".to_string())
8302 );
8303 assert_eq!(aur.description, Some("A tool".to_string()));
8304 assert_eq!(aur.depends, Some(vec!["glibc".to_string()]));
8305 }
8306
8307 #[test]
8310 fn test_krew_config_yaml() {
8311 let yaml = r#"
8312project_name: test
8313crates:
8314 - name: kubectl-mytool
8315 path: "."
8316 tag_template: "v{{ .Version }}"
8317 publish:
8318 krew:
8319 manifests_repo:
8320 owner: myorg
8321 name: krew-index
8322 description: "A comprehensive kubectl plugin"
8323 short_description: "A kubectl plugin"
8324 homepage: "https://github.com/myorg/kubectl-mytool"
8325 caveats: "Run 'kubectl mytool init' after installation."
8326"#;
8327 let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
8328 let krew = config.crates[0]
8329 .publish
8330 .as_ref()
8331 .unwrap()
8332 .krew
8333 .as_ref()
8334 .unwrap();
8335
8336 let repo = krew.manifests_repo.as_ref().unwrap();
8337 assert_eq!(repo.owner, "myorg");
8338 assert_eq!(repo.name, "krew-index");
8339 assert_eq!(
8340 krew.description,
8341 Some("A comprehensive kubectl plugin".to_string())
8342 );
8343 assert_eq!(krew.short_description, Some("A kubectl plugin".to_string()));
8344 assert_eq!(
8345 krew.homepage,
8346 Some("https://github.com/myorg/kubectl-mytool".to_string())
8347 );
8348 assert_eq!(
8349 krew.caveats,
8350 Some("Run 'kubectl mytool init' after installation.".to_string())
8351 );
8352 }
8353
8354 #[test]
8355 fn test_krew_config_minimal() {
8356 let yaml = r#"
8357project_name: test
8358crates:
8359 - name: kubectl-mytool
8360 path: "."
8361 tag_template: "v{{ .Version }}"
8362 publish:
8363 krew:
8364 manifests_repo:
8365 owner: myorg
8366 name: krew-index
8367"#;
8368 let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
8369 let krew = config.crates[0]
8370 .publish
8371 .as_ref()
8372 .unwrap()
8373 .krew
8374 .as_ref()
8375 .unwrap();
8376
8377 let repo = krew.manifests_repo.as_ref().unwrap();
8378 assert_eq!(repo.owner, "myorg");
8379 assert_eq!(repo.name, "krew-index");
8380 assert!(krew.description.is_none());
8381 assert!(krew.short_description.is_none());
8382 assert!(krew.homepage.is_none());
8383 assert!(krew.caveats.is_none());
8384 }
8385
8386 #[test]
8387 fn test_krew_config_toml() {
8388 let toml_str = r#"
8389project_name = "test"
8390
8391[[crates]]
8392name = "kubectl-mytool"
8393path = "."
8394tag_template = "v{{ .Version }}"
8395
8396[crates.publish.krew]
8397short_description = "A kubectl plugin"
8398homepage = "https://example.com"
8399
8400[crates.publish.krew.manifests_repo]
8401owner = "org"
8402name = "krew-index"
8403"#;
8404 let config: Config = toml::from_str(toml_str).unwrap();
8405 let krew = config.crates[0]
8406 .publish
8407 .as_ref()
8408 .unwrap()
8409 .krew
8410 .as_ref()
8411 .unwrap();
8412
8413 assert_eq!(krew.short_description, Some("A kubectl plugin".to_string()));
8414 let repo = krew.manifests_repo.as_ref().unwrap();
8415 assert_eq!(repo.owner, "org");
8416 }
8417
8418 #[test]
8421 fn test_all_seven_publishers_config() {
8422 let yaml = r#"
8423project_name: test
8424crates:
8425 - name: mytool
8426 path: "."
8427 tag_template: "v{{ .Version }}"
8428 publish:
8429 crates: true
8430 homebrew:
8431 tap:
8432 owner: org
8433 name: homebrew-tap
8434 scoop:
8435 bucket:
8436 owner: org
8437 name: scoop-bucket
8438 chocolatey:
8439 project_repo:
8440 owner: org
8441 name: mytool
8442 winget:
8443 manifests_repo:
8444 owner: org
8445 name: winget-pkgs
8446 package_identifier: "Org.MyTool"
8447 aur:
8448 git_url: "ssh://aur@aur.archlinux.org/mytool.git"
8449 krew:
8450 manifests_repo:
8451 owner: org
8452 name: krew-index
8453"#;
8454 let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
8455 let publish = config.crates[0].publish.as_ref().unwrap();
8456
8457 assert!(publish.crates.is_some());
8458 assert!(publish.homebrew.is_some());
8459 assert!(publish.scoop.is_some());
8460 assert!(publish.chocolatey.is_some());
8461 assert!(publish.winget.is_some());
8462 assert!(publish.aur.is_some());
8463 assert!(publish.krew.is_some());
8464 }
8465
8466 #[test]
8469 fn test_version_field_none_is_valid() {
8470 let config = Config::default();
8471 assert!(validate_version(&config).is_ok());
8472 }
8473
8474 #[test]
8475 fn test_version_field_1_is_valid() {
8476 let yaml = r#"
8477project_name: test
8478version: 1
8479crates: []
8480"#;
8481 let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
8482 assert_eq!(config.version, Some(1));
8483 assert!(validate_version(&config).is_ok());
8484 }
8485
8486 #[test]
8487 fn test_version_field_2_is_valid() {
8488 let yaml = r#"
8489project_name: test
8490version: 2
8491crates: []
8492"#;
8493 let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
8494 assert_eq!(config.version, Some(2));
8495 assert!(validate_version(&config).is_ok());
8496 }
8497
8498 #[test]
8499 fn test_version_field_99_is_rejected() {
8500 let config = Config {
8501 version: Some(99),
8502 ..Default::default()
8503 };
8504 let result = validate_version(&config);
8505 assert!(result.is_err());
8506 assert!(
8507 result
8508 .unwrap_err()
8509 .contains("unsupported config version: 99")
8510 );
8511 }
8512
8513 #[test]
8516 fn test_env_files_list_form_parses() {
8517 let yaml = r#"
8518project_name: test
8519env_files:
8520 - ".env"
8521 - ".release.env"
8522crates: []
8523"#;
8524 let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
8525 let env_files = config.env_files.unwrap();
8526 let files = env_files
8527 .as_list()
8528 .unwrap_or_else(|| panic!("expected List variant"));
8529 assert_eq!(files.len(), 2);
8530 assert_eq!(files[0], ".env");
8531 assert_eq!(files[1], ".release.env");
8532 }
8533
8534 #[test]
8535 fn test_env_files_struct_form_parses() {
8536 let yaml = r#"
8537project_name: test
8538env_files:
8539 github_token: "~/.config/goreleaser/github_token"
8540 gitlab_token: "/etc/tokens/gitlab"
8541crates: []
8542"#;
8543 let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
8544 let env_files = config.env_files.unwrap();
8545 let tokens = env_files
8546 .as_token_files()
8547 .unwrap_or_else(|| panic!("expected TokenFiles variant"));
8548 assert_eq!(
8549 tokens.github_token.as_deref(),
8550 Some("~/.config/goreleaser/github_token")
8551 );
8552 assert_eq!(tokens.gitlab_token.as_deref(), Some("/etc/tokens/gitlab"));
8553 assert!(tokens.gitea_token.is_none());
8554 }
8555
8556 #[test]
8557 fn test_env_files_struct_form_empty_mapping() {
8558 let yaml = r#"
8559project_name: test
8560env_files:
8561 gitea_token: "/tmp/gitea"
8562crates: []
8563"#;
8564 let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
8565 let env_files = config.env_files.unwrap();
8566 let tokens = env_files
8567 .as_token_files()
8568 .unwrap_or_else(|| panic!("expected TokenFiles variant"));
8569 assert!(tokens.github_token.is_none());
8570 assert!(tokens.gitlab_token.is_none());
8571 assert_eq!(tokens.gitea_token.as_deref(), Some("/tmp/gitea"));
8572 }
8573
8574 #[test]
8575 fn test_env_files_field_omitted() {
8576 let yaml = r#"
8577project_name: test
8578crates: []
8579"#;
8580 let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
8581 assert!(config.env_files.is_none());
8582 }
8583
8584 #[test]
8585 fn test_read_token_file_reads_first_line() {
8586 use std::io::Write;
8587 let dir = tempfile::TempDir::new().unwrap();
8588 let token_path = dir.path().join("github_token");
8589 let mut f = std::fs::File::create(&token_path).unwrap();
8590 writeln!(f, "ghp_abc123xyz").unwrap();
8591 writeln!(f, "this line should be ignored").unwrap();
8592 drop(f);
8593
8594 let result = read_token_file(&token_path.to_string_lossy()).unwrap();
8595 assert_eq!(result, Some("ghp_abc123xyz".to_string()));
8596 }
8597
8598 #[test]
8599 fn test_read_token_file_trims_whitespace() {
8600 use std::io::Write;
8601 let dir = tempfile::TempDir::new().unwrap();
8602 let token_path = dir.path().join("token");
8603 let mut f = std::fs::File::create(&token_path).unwrap();
8604 writeln!(f, " spaced_token ").unwrap();
8605 drop(f);
8606
8607 let result = read_token_file(&token_path.to_string_lossy()).unwrap();
8608 assert_eq!(result, Some("spaced_token".to_string()));
8609 }
8610
8611 #[test]
8612 fn test_read_token_file_nonexistent_returns_none() {
8613 let result = read_token_file("/tmp/nonexistent_token_file_99999").unwrap();
8614 assert!(result.is_none());
8615 }
8616
8617 #[test]
8618 fn test_read_token_file_empty_returns_none() {
8619 let dir = tempfile::TempDir::new().unwrap();
8620 let token_path = dir.path().join("empty_token");
8621 std::fs::write(&token_path, "").unwrap();
8622
8623 let result = read_token_file(&token_path.to_string_lossy()).unwrap();
8624 assert!(result.is_none());
8625 }
8626
8627 #[test]
8628 #[serial_test::serial]
8629 fn test_load_token_files_reads_tokens() {
8630 use std::io::Write;
8631 let dir = tempfile::TempDir::new().unwrap();
8632
8633 let gh_path = dir.path().join("github_token");
8634 let mut f = std::fs::File::create(&gh_path).unwrap();
8635 writeln!(f, "ghp_test123").unwrap();
8636 drop(f);
8637
8638 let gl_path = dir.path().join("gitlab_token");
8639 let mut f = std::fs::File::create(&gl_path).unwrap();
8640 writeln!(f, "glpat-test456").unwrap();
8641 drop(f);
8642
8643 let config = EnvFilesTokenConfig {
8644 github_token: Some(gh_path.to_string_lossy().to_string()),
8645 gitlab_token: Some(gl_path.to_string_lossy().to_string()),
8646 gitea_token: None, };
8648
8649 let orig_gh = std::env::var("GITHUB_TOKEN").ok();
8651 let orig_gl = std::env::var("GITLAB_TOKEN").ok();
8652 let orig_gt = std::env::var("GITEA_TOKEN").ok();
8653 unsafe {
8655 std::env::remove_var("GITHUB_TOKEN");
8656 std::env::remove_var("GITLAB_TOKEN");
8657 std::env::remove_var("GITEA_TOKEN");
8658 }
8659
8660 let log = crate::log::StageLogger::new("test", crate::log::Verbosity::Normal);
8661 let vars = load_token_files(&config, &log).unwrap();
8662
8663 unsafe {
8665 if let Some(v) = orig_gh {
8666 std::env::set_var("GITHUB_TOKEN", v);
8667 }
8668 if let Some(v) = orig_gl {
8669 std::env::set_var("GITLAB_TOKEN", v);
8670 }
8671 if let Some(v) = orig_gt {
8672 std::env::set_var("GITEA_TOKEN", v);
8673 }
8674 }
8675
8676 assert_eq!(vars.get("GITHUB_TOKEN").unwrap(), "ghp_test123");
8677 assert_eq!(vars.get("GITLAB_TOKEN").unwrap(), "glpat-test456");
8678 assert!(!vars.contains_key("GITEA_TOKEN"));
8680 }
8681
8682 #[test]
8683 #[serial_test::serial]
8684 fn test_load_token_files_env_var_takes_precedence() {
8685 use std::io::Write;
8686 let dir = tempfile::TempDir::new().unwrap();
8687
8688 let gh_path = dir.path().join("github_token");
8689 let mut f = std::fs::File::create(&gh_path).unwrap();
8690 writeln!(f, "file_token").unwrap();
8691 drop(f);
8692
8693 let config = EnvFilesTokenConfig {
8694 github_token: Some(gh_path.to_string_lossy().to_string()),
8695 gitlab_token: None,
8696 gitea_token: None,
8697 };
8698
8699 let orig = std::env::var("GITHUB_TOKEN").ok();
8701 unsafe {
8703 std::env::set_var("GITHUB_TOKEN", "env_token");
8704 }
8705
8706 let log = crate::log::StageLogger::new("test", crate::log::Verbosity::Normal);
8707 let vars = load_token_files(&config, &log).unwrap();
8708
8709 unsafe {
8711 match orig {
8712 Some(v) => std::env::set_var("GITHUB_TOKEN", v),
8713 None => std::env::remove_var("GITHUB_TOKEN"),
8714 }
8715 }
8716
8717 assert!(
8719 !vars.contains_key("GITHUB_TOKEN"),
8720 "env var should take precedence; file should not be loaded"
8721 );
8722 }
8723
8724 #[test]
8725 fn test_read_token_file_tilde_expansion() {
8726 let dir = tempfile::TempDir::new().unwrap();
8728 let token_path = dir.path().join(".config/goreleaser/github_token");
8729 std::fs::create_dir_all(token_path.parent().unwrap()).unwrap();
8730 std::fs::write(&token_path, "tilde_token\n").unwrap();
8731
8732 let orig_home = std::env::var("HOME").ok();
8733 unsafe {
8735 std::env::set_var("HOME", dir.path());
8736 }
8737
8738 let result = read_token_file("~/.config/goreleaser/github_token").unwrap();
8739
8740 unsafe {
8741 match orig_home {
8742 Some(v) => std::env::set_var("HOME", v),
8743 None => std::env::remove_var("HOME"),
8744 }
8745 }
8746
8747 assert_eq!(result, Some("tilde_token".to_string()));
8748 }
8749
8750 #[test]
8751 fn test_load_env_files_sets_vars() {
8752 use std::io::Write;
8753 let dir = tempfile::TempDir::new().unwrap();
8754 let env_path = dir.path().join(".env");
8755 let mut f = std::fs::File::create(&env_path).unwrap();
8756 writeln!(f, "# comment line").unwrap();
8757 writeln!(f).unwrap();
8758 writeln!(f, "TEST_ANODIZER_KEY=hello_world").unwrap();
8759 writeln!(f, "TEST_ANODIZER_QUOTED=\"with quotes\"").unwrap();
8760 writeln!(f, "TEST_ANODIZER_SINGLE='single_quoted'").unwrap();
8761 writeln!(f, "export TEST_ANODIZER_EXPORT=exported_val").unwrap();
8762 drop(f);
8763
8764 let log = crate::log::StageLogger::new("test", crate::log::Verbosity::Normal);
8765 let vars = load_env_files(&[env_path.to_string_lossy().to_string()], &log, false).unwrap();
8766 assert_eq!(vars.get("TEST_ANODIZER_KEY").unwrap(), "hello_world");
8767 assert_eq!(vars.get("TEST_ANODIZER_QUOTED").unwrap(), "with quotes");
8768 assert_eq!(
8769 vars.get("TEST_ANODIZER_SINGLE").unwrap(),
8770 "single_quoted",
8771 "single-quoted values should have quotes stripped"
8772 );
8773 assert_eq!(
8774 vars.get("TEST_ANODIZER_EXPORT").unwrap(),
8775 "exported_val",
8776 "export prefix should be stripped"
8777 );
8778 }
8779
8780 #[test]
8781 fn test_load_env_files_edge_cases() {
8782 use std::io::Write;
8783 let dir = tempfile::TempDir::new().unwrap();
8784 let env_path = dir.path().join(".env-edge");
8785 let mut f = std::fs::File::create(&env_path).unwrap();
8786 writeln!(f, "TEST_ANODIZER_SINGLEQ=\"").unwrap();
8788 writeln!(f, "=orphan_value").unwrap();
8790 writeln!(f, "NO_EQUALS_HERE").unwrap();
8792 drop(f);
8793
8794 let log = crate::log::StageLogger::new("test", crate::log::Verbosity::Normal);
8795 let vars = load_env_files(&[env_path.to_string_lossy().to_string()], &log, false).unwrap();
8796 assert_eq!(vars.get("TEST_ANODIZER_SINGLEQ").unwrap(), "\"");
8799 assert!(!vars.contains_key(""), "empty key should be skipped");
8801 }
8802
8803 #[test]
8804 fn test_load_env_files_nonexistent_skips_with_warning() {
8805 let log = crate::log::StageLogger::new("test", crate::log::Verbosity::Normal);
8806 let result = load_env_files(
8807 &["/tmp/nonexistent_anodizer_env_file_12345".to_string()],
8808 &log,
8809 false,
8810 );
8811 assert!(result.is_ok());
8813 assert!(result.unwrap().is_empty());
8814 }
8815
8816 #[test]
8817 fn test_load_env_files_nonexistent_strict_mode_errors() {
8818 let log = crate::log::StageLogger::new("test", crate::log::Verbosity::Normal);
8819 let result = load_env_files(
8820 &["/tmp/nonexistent_anodizer_env_file_12345".to_string()],
8821 &log,
8822 true,
8823 );
8824 assert!(result.is_err());
8825 assert!(result.unwrap_err().contains("strict mode"));
8826 }
8827
8828 #[test]
8837 fn test_env_files_list_form_toml() {
8838 #[derive(Deserialize)]
8841 struct Wrapper {
8842 env_files: EnvFilesConfig,
8843 }
8844 let toml_str = r#"env_files = [".env", ".env.local"]"#;
8845 let wrapper: Wrapper = toml::from_str(toml_str).unwrap();
8846 let files = wrapper
8847 .env_files
8848 .as_list()
8849 .unwrap_or_else(|| panic!("expected List variant"));
8850 assert_eq!(files.len(), 2);
8851 assert_eq!(files[0], ".env");
8852 assert_eq!(files[1], ".env.local");
8853 }
8854
8855 #[test]
8856 fn test_env_files_struct_form_toml() {
8857 #[derive(Deserialize)]
8860 struct Wrapper {
8861 env_files: EnvFilesConfig,
8862 }
8863 let toml_str = r#"
8864[env_files]
8865github_token = "~/.config/goreleaser/github_token"
8866gitlab_token = "/etc/tokens/gitlab"
8867"#;
8868 let wrapper: Wrapper = toml::from_str(toml_str).unwrap();
8869 let tokens = wrapper
8870 .env_files
8871 .as_token_files()
8872 .unwrap_or_else(|| panic!("expected TokenFiles variant"));
8873 assert_eq!(
8874 tokens.github_token.as_deref(),
8875 Some("~/.config/goreleaser/github_token")
8876 );
8877 assert_eq!(tokens.gitlab_token.as_deref(), Some("/etc/tokens/gitlab"));
8878 assert!(tokens.gitea_token.is_none());
8879 }
8880
8881 #[test]
8882 fn test_env_files_token_config_toml_rejects_unknown_fields() {
8883 let toml_str = r#"github_tokne = "~/.config/goreleaser/github_token""#;
8885 let result = toml::from_str::<EnvFilesTokenConfig>(toml_str);
8886 assert!(
8887 result.is_err(),
8888 "EnvFilesTokenConfig should reject unknown fields like 'github_tokne'"
8889 );
8890 }
8891
8892 #[test]
8895 fn test_build_ignore_parses() {
8896 let yaml = r#"
8897project_name: test
8898defaults:
8899 targets:
8900 - x86_64-unknown-linux-gnu
8901 - aarch64-unknown-linux-gnu
8902 ignore:
8903 - os: windows
8904 arch: arm64
8905 - os: linux
8906 arch: "386"
8907crates: []
8908"#;
8909 let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
8910 let defaults = config.defaults.unwrap();
8911 let ignores = defaults.ignore.unwrap();
8912 assert_eq!(ignores.len(), 2);
8913 assert_eq!(ignores[0].os, "windows");
8914 assert_eq!(ignores[0].arch, "arm64");
8915 assert_eq!(ignores[1].os, "linux");
8916 assert_eq!(ignores[1].arch, "386");
8917 }
8918
8919 #[test]
8920 fn test_build_ignore_omitted() {
8921 let yaml = r#"
8922project_name: test
8923defaults:
8924 targets:
8925 - x86_64-unknown-linux-gnu
8926crates: []
8927"#;
8928 let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
8929 let defaults = config.defaults.unwrap();
8930 assert!(defaults.ignore.is_none());
8931 }
8932
8933 #[test]
8936 fn test_build_override_parses() {
8937 let yaml = r#"
8938project_name: test
8939defaults:
8940 overrides:
8941 - targets:
8942 - "x86_64-*"
8943 features:
8944 - simd
8945 flags: "--release"
8946 env:
8947 CC: gcc
8948 - targets:
8949 - "*-apple-darwin"
8950 features:
8951 - metal
8952crates: []
8953"#;
8954 let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
8955 let defaults = config.defaults.unwrap();
8956 let overrides = defaults.overrides.unwrap();
8957 assert_eq!(overrides.len(), 2);
8958 assert_eq!(overrides[0].targets, vec!["x86_64-*"]);
8959 assert_eq!(overrides[0].features, Some(vec!["simd".to_string()]));
8960 assert_eq!(overrides[0].flags, Some("--release".to_string()));
8961 assert_eq!(overrides[0].env.as_ref().unwrap().get("CC").unwrap(), "gcc");
8962 assert_eq!(overrides[1].targets, vec!["*-apple-darwin"]);
8963 assert_eq!(overrides[1].features, Some(vec!["metal".to_string()]));
8964 assert!(overrides[1].env.is_none());
8965 }
8966
8967 #[test]
8968 fn test_build_override_omitted() {
8969 let yaml = r#"
8970project_name: test
8971defaults:
8972 targets:
8973 - x86_64-unknown-linux-gnu
8974crates: []
8975"#;
8976 let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
8977 let defaults = config.defaults.unwrap();
8978 assert!(defaults.overrides.is_none());
8979 }
8980
8981 #[test]
8984 fn test_json_schema_generation() {
8985 let schema = schemars::schema_for!(Config);
8986 let json = serde_json::to_string_pretty(&schema).unwrap();
8987 assert!(json.contains("project_name"));
8988 assert!(json.contains("env_files"));
8989 assert!(json.contains("version"));
8990 assert!(json.contains("BuildIgnore"));
8991 assert!(json.contains("BuildOverride"));
8992 }
8993
8994 #[test]
8997 fn test_homebrew_config_new_fields() {
8998 let yaml = r#"
8999project_name: test
9000crates:
9001 - name: a
9002 path: "."
9003 tag_template: "v{{ .Version }}"
9004 publish:
9005 homebrew:
9006 tap:
9007 owner: myorg
9008 name: homebrew-tap
9009 homepage: "https://example.com"
9010 dependencies:
9011 - name: openssl
9012 - name: libgit2
9013 os: mac
9014 - name: zlib
9015 type: optional
9016 conflicts:
9017 - other-tool
9018 - old-tool
9019 caveats: "Run `tool init` after installing."
9020 skip_upload: "auto"
9021"#;
9022 let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
9023 let hb = config.crates[0]
9024 .publish
9025 .as_ref()
9026 .unwrap()
9027 .homebrew
9028 .as_ref()
9029 .unwrap();
9030 assert_eq!(hb.homepage.as_deref(), Some("https://example.com"));
9031 assert_eq!(
9032 hb.skip_upload,
9033 Some(StringOrBool::String("auto".to_string()))
9034 );
9035 assert_eq!(
9036 hb.caveats.as_deref(),
9037 Some("Run `tool init` after installing.")
9038 );
9039
9040 let conflicts = hb.conflicts.as_ref().unwrap();
9041 assert_eq!(
9042 conflicts,
9043 &[
9044 HomebrewConflict::Name("other-tool".to_string()),
9045 HomebrewConflict::Name("old-tool".to_string()),
9046 ]
9047 );
9048
9049 let deps = hb.dependencies.as_ref().unwrap();
9050 assert_eq!(deps.len(), 3);
9051 assert_eq!(deps[0].name, "openssl");
9052 assert_eq!(deps[0].os, None);
9053 assert_eq!(deps[0].dep_type, None);
9054 assert_eq!(deps[1].name, "libgit2");
9055 assert_eq!(deps[1].os.as_deref(), Some("mac"));
9056 assert_eq!(deps[2].name, "zlib");
9057 assert_eq!(deps[2].dep_type.as_deref(), Some("optional"));
9058 }
9059
9060 #[test]
9061 fn test_homebrew_config_defaults_when_new_fields_omitted() {
9062 let yaml = r#"
9063project_name: test
9064crates:
9065 - name: a
9066 path: "."
9067 tag_template: "v{{ .Version }}"
9068 publish:
9069 homebrew:
9070 tap:
9071 owner: myorg
9072 name: homebrew-tap
9073"#;
9074 let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
9075 let hb = config.crates[0]
9076 .publish
9077 .as_ref()
9078 .unwrap()
9079 .homebrew
9080 .as_ref()
9081 .unwrap();
9082 assert!(hb.homepage.is_none());
9083 assert!(hb.dependencies.is_none());
9084 assert!(hb.conflicts.is_none());
9085 assert!(hb.caveats.is_none());
9086 assert!(hb.skip_upload.is_none());
9087 }
9088
9089 #[test]
9092 fn test_scoop_config_new_fields() {
9093 let yaml = r#"
9094project_name: test
9095crates:
9096 - name: a
9097 path: "."
9098 tag_template: "v{{ .Version }}"
9099 publish:
9100 scoop:
9101 bucket:
9102 owner: myorg
9103 name: scoop-bucket
9104 homepage: "https://example.com"
9105 persist:
9106 - data
9107 - config.ini
9108 depends:
9109 - git
9110 - 7zip
9111 pre_install:
9112 - "Write-Host 'Installing...'"
9113 post_install:
9114 - "Write-Host 'Done!'"
9115 shortcuts:
9116 - ["myapp.exe", "My App"]
9117 - ["myapp.exe", "My App CLI", "--cli"]
9118 skip_upload: "true"
9119"#;
9120 let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
9121 let sc = config.crates[0]
9122 .publish
9123 .as_ref()
9124 .unwrap()
9125 .scoop
9126 .as_ref()
9127 .unwrap();
9128 assert_eq!(sc.homepage.as_deref(), Some("https://example.com"));
9129 assert_eq!(
9130 sc.skip_upload,
9131 Some(StringOrBool::String("true".to_string()))
9132 );
9133
9134 let persist = sc.persist.as_ref().unwrap();
9135 assert_eq!(persist, &["data", "config.ini"]);
9136
9137 let depends = sc.depends.as_ref().unwrap();
9138 assert_eq!(depends, &["git", "7zip"]);
9139
9140 let pre = sc.pre_install.as_ref().unwrap();
9141 assert_eq!(pre, &["Write-Host 'Installing...'"]);
9142
9143 let post = sc.post_install.as_ref().unwrap();
9144 assert_eq!(post, &["Write-Host 'Done!'"]);
9145
9146 let shortcuts = sc.shortcuts.as_ref().unwrap();
9147 assert_eq!(shortcuts.len(), 2);
9148 assert_eq!(shortcuts[0], vec!["myapp.exe", "My App"]);
9149 assert_eq!(shortcuts[1], vec!["myapp.exe", "My App CLI", "--cli"]);
9150 }
9151
9152 #[test]
9153 fn test_scoop_config_defaults_when_new_fields_omitted() {
9154 let yaml = r#"
9155project_name: test
9156crates:
9157 - name: a
9158 path: "."
9159 tag_template: "v{{ .Version }}"
9160 publish:
9161 scoop:
9162 bucket:
9163 owner: myorg
9164 name: scoop-bucket
9165"#;
9166 let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
9167 let sc = config.crates[0]
9168 .publish
9169 .as_ref()
9170 .unwrap()
9171 .scoop
9172 .as_ref()
9173 .unwrap();
9174 assert!(sc.homepage.is_none());
9175 assert!(sc.persist.is_none());
9176 assert!(sc.depends.is_none());
9177 assert!(sc.pre_install.is_none());
9178 assert!(sc.post_install.is_none());
9179 assert!(sc.shortcuts.is_none());
9180 assert!(sc.skip_upload.is_none());
9181 }
9182
9183 #[test]
9188 fn test_git_config_all_fields() {
9189 let yaml = r#"
9190project_name: test
9191crates:
9192 - name: a
9193 path: "."
9194 tag_template: "v{{ .Version }}"
9195git:
9196 tag_sort: "-version:creatordate"
9197 ignore_tags:
9198 - "nightly*"
9199 - "legacy-*"
9200 ignore_tag_prefixes:
9201 - "internal/"
9202 - "test-"
9203 prerelease_suffix: "-rc"
9204"#;
9205 let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
9206 let git = config
9207 .git
9208 .unwrap_or_else(|| panic!("git section should be present"));
9209 assert_eq!(git.tag_sort.as_deref(), Some("-version:creatordate"));
9210 assert_eq!(
9211 git.ignore_tags.as_deref(),
9212 Some(&["nightly*".to_string(), "legacy-*".to_string()][..])
9213 );
9214 assert_eq!(
9215 git.ignore_tag_prefixes.as_deref(),
9216 Some(&["internal/".to_string(), "test-".to_string()][..])
9217 );
9218 assert_eq!(git.prerelease_suffix.as_deref(), Some("-rc"));
9219 }
9220
9221 #[test]
9222 fn test_git_config_omitted_is_none() {
9223 let yaml = r#"
9224project_name: test
9225crates:
9226 - name: a
9227 path: "."
9228 tag_template: "v{{ .Version }}"
9229"#;
9230 let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
9231 assert!(config.git.is_none());
9232 }
9233
9234 #[test]
9235 fn test_git_config_partial_only_tag_sort() {
9236 let yaml = r#"
9237project_name: test
9238crates:
9239 - name: a
9240 path: "."
9241 tag_template: "v{{ .Version }}"
9242git:
9243 tag_sort: "-version:refname"
9244"#;
9245 let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
9246 let git = config
9247 .git
9248 .unwrap_or_else(|| panic!("git section should be present"));
9249 assert_eq!(git.tag_sort.as_deref(), Some("-version:refname"));
9250 assert!(git.ignore_tags.is_none());
9251 assert!(git.ignore_tag_prefixes.is_none());
9252 assert!(git.prerelease_suffix.is_none());
9253 }
9254
9255 #[test]
9256 fn test_git_config_ignore_tags_accepts_array() {
9257 let yaml = r#"
9258project_name: test
9259crates:
9260 - name: a
9261 path: "."
9262 tag_template: "v{{ .Version }}"
9263git:
9264 ignore_tags:
9265 - "alpha*"
9266 - "beta*"
9267 - "rc-*"
9268"#;
9269 let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
9270 let tags = config.git.unwrap().ignore_tags.unwrap();
9271 assert_eq!(tags.len(), 3);
9272 assert_eq!(tags[0], "alpha*");
9273 assert_eq!(tags[1], "beta*");
9274 assert_eq!(tags[2], "rc-*");
9275 }
9276
9277 #[test]
9278 fn test_validate_tag_sort_valid_refname() {
9279 let config = Config {
9280 git: Some(GitConfig {
9281 tag_sort: Some("-version:refname".to_string()),
9282 ..Default::default()
9283 }),
9284 ..Default::default()
9285 };
9286 assert!(validate_tag_sort(&config).is_ok());
9287 }
9288
9289 #[test]
9290 fn test_validate_tag_sort_valid_creatordate() {
9291 let config = Config {
9292 git: Some(GitConfig {
9293 tag_sort: Some("-version:creatordate".to_string()),
9294 ..Default::default()
9295 }),
9296 ..Default::default()
9297 };
9298 assert!(validate_tag_sort(&config).is_ok());
9299 }
9300
9301 #[test]
9302 fn test_validate_tag_sort_none_is_valid() {
9303 let config = Config {
9304 git: Some(GitConfig::default()),
9305 ..Default::default()
9306 };
9307 assert!(validate_tag_sort(&config).is_ok());
9308 }
9309
9310 #[test]
9311 fn test_validate_tag_sort_no_git_config_is_valid() {
9312 let config = Config::default();
9313 assert!(validate_tag_sort(&config).is_ok());
9314 }
9315
9316 #[test]
9317 fn test_validate_tag_sort_invalid_rejected() {
9318 let config = Config {
9319 git: Some(GitConfig {
9320 tag_sort: Some("alphabetical".to_string()),
9321 ..Default::default()
9322 }),
9323 ..Default::default()
9324 };
9325 let result = validate_tag_sort(&config);
9326 assert!(result.is_err());
9327 let err = result.unwrap_err();
9328 assert!(
9329 err.contains("alphabetical"),
9330 "error should contain the bad value: {}",
9331 err
9332 );
9333 assert!(
9334 err.contains("-version:refname"),
9335 "error should list accepted values: {}",
9336 err
9337 );
9338 }
9339
9340 #[test]
9341 fn test_git_config_ignore_tag_prefixes_accepts_array() {
9342 let yaml = r#"
9343project_name: test
9344crates:
9345 - name: a
9346 path: "."
9347 tag_template: "v{{ .Version }}"
9348git:
9349 ignore_tag_prefixes:
9350 - "wip/"
9351 - "experiment/"
9352"#;
9353 let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
9354 let prefixes = config.git.unwrap().ignore_tag_prefixes.unwrap();
9355 assert_eq!(prefixes.len(), 2);
9356 assert_eq!(prefixes[0], "wip/");
9357 assert_eq!(prefixes[1], "experiment/");
9358 }
9359
9360 #[test]
9361 fn test_metadata_config_with_mod_timestamp() {
9362 let yaml = r#"
9363project_name: test
9364crates:
9365 - name: a
9366 path: "."
9367 tag_template: "v{{ .Version }}"
9368metadata:
9369 mod_timestamp: "{{ .CommitTimestamp }}"
9370"#;
9371 let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
9372 let meta = config.metadata.unwrap();
9373 assert_eq!(meta.mod_timestamp.unwrap(), "{{ .CommitTimestamp }}");
9374 }
9375
9376 #[test]
9377 fn test_metadata_config_omitted_is_none() {
9378 let yaml = r#"
9379project_name: test
9380crates:
9381 - name: a
9382 path: "."
9383 tag_template: "v{{ .Version }}"
9384"#;
9385 let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
9386 assert!(config.metadata.is_none());
9387 }
9388
9389 #[test]
9390 fn test_metadata_config_empty_section() {
9391 let yaml = r#"
9392project_name: test
9393crates:
9394 - name: a
9395 path: "."
9396 tag_template: "v{{ .Version }}"
9397metadata: {}
9398"#;
9399 let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
9400 let meta = config.metadata.unwrap();
9401 assert!(meta.mod_timestamp.is_none());
9402 }
9403
9404 #[test]
9405 fn test_variables_config_parsed() {
9406 let yaml = r#"
9407project_name: test
9408variables:
9409 description: "my project description"
9410 somethingElse: "yada yada yada"
9411 empty: ""
9412crates:
9413 - name: test
9414 path: "."
9415 tag_template: "v{{ .Version }}"
9416"#;
9417 let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
9418 let vars = config.variables.as_ref().unwrap();
9419 assert_eq!(vars.get("description").unwrap(), "my project description");
9420 assert_eq!(vars.get("somethingElse").unwrap(), "yada yada yada");
9421 assert_eq!(vars.get("empty").unwrap(), "");
9422 assert_eq!(vars.len(), 3);
9423 }
9424
9425 #[test]
9426 fn test_variables_config_omitted_is_none() {
9427 let yaml = r#"
9428project_name: test
9429crates:
9430 - name: test
9431 path: "."
9432 tag_template: "v{{ .Version }}"
9433"#;
9434 let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
9435 assert!(config.variables.is_none());
9436 }
9437
9438 #[test]
9441 fn test_snapcraft_disable_bool_true() {
9442 let yaml = r#"
9443project_name: test
9444crates:
9445 - name: a
9446 path: "."
9447 tag_template: "v{{ .Version }}"
9448 snapcrafts:
9449 - disable: true
9450"#;
9451 let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
9452 let snap = &config.crates[0].snapcrafts.as_ref().unwrap()[0];
9453 assert_eq!(snap.disable, Some(StringOrBool::Bool(true)));
9454 }
9455
9456 #[test]
9457 fn test_snapcraft_disable_bool_false() {
9458 let yaml = r#"
9459project_name: test
9460crates:
9461 - name: a
9462 path: "."
9463 tag_template: "v{{ .Version }}"
9464 snapcrafts:
9465 - disable: false
9466"#;
9467 let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
9468 let snap = &config.crates[0].snapcrafts.as_ref().unwrap()[0];
9469 assert_eq!(snap.disable, Some(StringOrBool::Bool(false)));
9470 }
9471
9472 #[test]
9473 fn test_snapcraft_disable_template_string() {
9474 let yaml = r#"
9475project_name: test
9476crates:
9477 - name: a
9478 path: "."
9479 tag_template: "v{{ .Version }}"
9480 snapcrafts:
9481 - disable: "{{ if .IsSnapshot }}true{{ end }}"
9482"#;
9483 let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
9484 let snap = &config.crates[0].snapcrafts.as_ref().unwrap()[0];
9485 match &snap.disable {
9486 Some(StringOrBool::String(s)) => {
9487 assert!(s.contains("IsSnapshot"));
9488 }
9489 other => panic!("expected StringOrBool::String, got {:?}", other),
9490 }
9491 }
9492
9493 #[test]
9494 fn test_snapcraft_disable_omitted() {
9495 let yaml = r#"
9496project_name: test
9497crates:
9498 - name: a
9499 path: "."
9500 tag_template: "v{{ .Version }}"
9501 snapcrafts:
9502 - name: mysnap
9503"#;
9504 let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
9505 let snap = &config.crates[0].snapcrafts.as_ref().unwrap()[0];
9506 assert!(snap.disable.is_none());
9507 }
9508
9509 #[test]
9512 fn test_aur_disable_bool_true() {
9513 let yaml = r#"
9514project_name: test
9515crates:
9516 - name: a
9517 path: "."
9518 tag_template: "v{{ .Version }}"
9519 publish:
9520 aur:
9521 disable: true
9522 git_url: "ssh://aur@aur.archlinux.org/a.git"
9523"#;
9524 let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
9525 let aur = config.crates[0]
9526 .publish
9527 .as_ref()
9528 .unwrap()
9529 .aur
9530 .as_ref()
9531 .unwrap();
9532 assert_eq!(aur.disable, Some(StringOrBool::Bool(true)));
9533 }
9534
9535 #[test]
9536 fn test_aur_disable_template_string() {
9537 let yaml = r#"
9538project_name: test
9539crates:
9540 - name: a
9541 path: "."
9542 tag_template: "v{{ .Version }}"
9543 publish:
9544 aur:
9545 disable: "{{ if .IsSnapshot }}true{{ end }}"
9546 git_url: "ssh://aur@aur.archlinux.org/a.git"
9547"#;
9548 let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
9549 let aur = config.crates[0]
9550 .publish
9551 .as_ref()
9552 .unwrap()
9553 .aur
9554 .as_ref()
9555 .unwrap();
9556 match &aur.disable {
9557 Some(StringOrBool::String(s)) => {
9558 assert!(s.contains("IsSnapshot"));
9559 }
9560 other => panic!("expected StringOrBool::String, got {:?}", other),
9561 }
9562 }
9563
9564 #[test]
9565 fn test_aur_disable_omitted() {
9566 let yaml = r#"
9567project_name: test
9568crates:
9569 - name: a
9570 path: "."
9571 tag_template: "v{{ .Version }}"
9572 publish:
9573 aur:
9574 git_url: "ssh://aur@aur.archlinux.org/a.git"
9575"#;
9576 let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
9577 let aur = config.crates[0]
9578 .publish
9579 .as_ref()
9580 .unwrap()
9581 .aur
9582 .as_ref()
9583 .unwrap();
9584 assert!(aur.disable.is_none());
9585 }
9586
9587 #[test]
9590 fn test_publisher_disable_bool_true() {
9591 let yaml = r#"
9592project_name: test
9593publishers:
9594 - cmd: "echo hello"
9595 disable: true
9596crates:
9597 - name: a
9598 path: "."
9599 tag_template: "v{{ .Version }}"
9600"#;
9601 let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
9602 let pub_cfg = &config.publishers.as_ref().unwrap()[0];
9603 assert_eq!(pub_cfg.disable, Some(StringOrBool::Bool(true)));
9604 }
9605
9606 #[test]
9607 fn test_publisher_disable_template_string() {
9608 let yaml = r#"
9609project_name: test
9610publishers:
9611 - cmd: "echo hello"
9612 disable: "{{ if .IsSnapshot }}true{{ end }}"
9613crates:
9614 - name: a
9615 path: "."
9616 tag_template: "v{{ .Version }}"
9617"#;
9618 let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
9619 let pub_cfg = &config.publishers.as_ref().unwrap()[0];
9620 match &pub_cfg.disable {
9621 Some(StringOrBool::String(s)) => {
9622 assert!(s.contains("IsSnapshot"));
9623 }
9624 other => panic!("expected StringOrBool::String, got {:?}", other),
9625 }
9626 }
9627
9628 #[test]
9629 fn test_publisher_disable_omitted() {
9630 let yaml = r#"
9631project_name: test
9632publishers:
9633 - cmd: "echo hello"
9634crates:
9635 - name: a
9636 path: "."
9637 tag_template: "v{{ .Version }}"
9638"#;
9639 let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
9640 let pub_cfg = &config.publishers.as_ref().unwrap()[0];
9641 assert!(pub_cfg.disable.is_none());
9642 }
9643
9644 #[test]
9647 fn test_homebrew_skip_upload_bool_true() {
9648 let yaml = r#"
9649project_name: test
9650crates:
9651 - name: a
9652 path: "."
9653 tag_template: "v{{ .Version }}"
9654 publish:
9655 homebrew:
9656 skip_upload: true
9657 tap:
9658 owner: org
9659 name: homebrew-tap
9660"#;
9661 let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
9662 let hb = config.crates[0]
9663 .publish
9664 .as_ref()
9665 .unwrap()
9666 .homebrew
9667 .as_ref()
9668 .unwrap();
9669 assert_eq!(hb.skip_upload, Some(StringOrBool::Bool(true)));
9670 }
9671
9672 #[test]
9673 fn test_scoop_skip_upload_bool_true() {
9674 let yaml = r#"
9675project_name: test
9676crates:
9677 - name: a
9678 path: "."
9679 tag_template: "v{{ .Version }}"
9680 publish:
9681 scoop:
9682 skip_upload: true
9683 bucket:
9684 owner: org
9685 name: scoop-bucket
9686"#;
9687 let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
9688 let sc = config.crates[0]
9689 .publish
9690 .as_ref()
9691 .unwrap()
9692 .scoop
9693 .as_ref()
9694 .unwrap();
9695 assert_eq!(sc.skip_upload, Some(StringOrBool::Bool(true)));
9696 }
9697
9698 #[test]
9699 fn test_aur_skip_upload_bool_true() {
9700 let yaml = r#"
9701project_name: test
9702crates:
9703 - name: a
9704 path: "."
9705 tag_template: "v{{ .Version }}"
9706 publish:
9707 aur:
9708 skip_upload: true
9709 git_url: "ssh://aur@aur.archlinux.org/a.git"
9710"#;
9711 let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
9712 let aur = config.crates[0]
9713 .publish
9714 .as_ref()
9715 .unwrap()
9716 .aur
9717 .as_ref()
9718 .unwrap();
9719 assert_eq!(aur.skip_upload, Some(StringOrBool::Bool(true)));
9720 }
9721
9722 #[test]
9723 fn test_winget_skip_upload_bool_true() {
9724 let yaml = r#"
9725project_name: test
9726crates:
9727 - name: a
9728 path: "."
9729 tag_template: "v{{ .Version }}"
9730 publish:
9731 winget:
9732 skip_upload: true
9733 manifests_repo:
9734 owner: org
9735 name: winget-pkgs
9736 package_identifier: "Org.App"
9737"#;
9738 let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
9739 let wg = config.crates[0]
9740 .publish
9741 .as_ref()
9742 .unwrap()
9743 .winget
9744 .as_ref()
9745 .unwrap();
9746 assert_eq!(wg.skip_upload, Some(StringOrBool::Bool(true)));
9747 }
9748
9749 #[test]
9750 fn test_krew_skip_upload_auto_string() {
9751 let yaml = r#"
9752project_name: test
9753crates:
9754 - name: a
9755 path: "."
9756 tag_template: "v{{ .Version }}"
9757 publish:
9758 krew:
9759 skip_upload: "auto"
9760 manifests_repo:
9761 owner: org
9762 name: krew-index
9763"#;
9764 let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
9765 let krew = config.crates[0]
9766 .publish
9767 .as_ref()
9768 .unwrap()
9769 .krew
9770 .as_ref()
9771 .unwrap();
9772 assert_eq!(
9773 krew.skip_upload,
9774 Some(StringOrBool::String("auto".to_string()))
9775 );
9776 }
9777
9778 #[test]
9779 fn test_nix_skip_upload_template() {
9780 let yaml = r#"
9781project_name: test
9782crates:
9783 - name: a
9784 path: "."
9785 tag_template: "v{{ .Version }}"
9786 publish:
9787 nix:
9788 skip_upload: "{{ .Env.SKIP }}"
9789 repository:
9790 owner: org
9791 name: nixpkgs
9792"#;
9793 let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
9794 let nix = config.crates[0]
9795 .publish
9796 .as_ref()
9797 .unwrap()
9798 .nix
9799 .as_ref()
9800 .unwrap();
9801 match &nix.skip_upload {
9802 Some(StringOrBool::String(s)) => {
9803 assert!(s.contains(".Env.SKIP"));
9804 }
9805 other => panic!("expected StringOrBool::String, got {:?}", other),
9806 }
9807 }
9808
9809 #[test]
9810 fn test_skip_upload_string_or_bool() {
9811 let yaml = r#"
9812project_name: test
9813crates:
9814 - name: test
9815 path: "."
9816 tag_template: "v{{ .Version }}"
9817 publish:
9818 homebrew:
9819 name: test
9820 skip_upload: "{{ if .IsSnapshot }}true{{ endif }}"
9821 tap:
9822 owner: org
9823 name: homebrew-tap
9824"#;
9825 let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
9826 let hb = config.crates[0]
9827 .publish
9828 .as_ref()
9829 .unwrap()
9830 .homebrew
9831 .as_ref()
9832 .unwrap();
9833 match &hb.skip_upload {
9834 Some(StringOrBool::String(s)) => {
9835 assert!(
9836 s.contains(".IsSnapshot"),
9837 "expected template with .IsSnapshot, got: {}",
9838 s
9839 );
9840 }
9841 other => panic!(
9842 "expected StringOrBool::String with template, got {:?}",
9843 other
9844 ),
9845 }
9846 }
9847
9848 #[test]
9853 fn test_template_files_parses_from_yaml() {
9854 let yaml = r#"
9855project_name: myproject
9856crates: []
9857template_files:
9858 - id: install-script
9859 src: install.sh.tpl
9860 dst: install.sh
9861 mode: "0755"
9862 - src: README.md.tpl
9863 dst: README.md
9864"#;
9865 let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
9866 let tfs = config.template_files.unwrap();
9867 assert_eq!(tfs.len(), 2);
9868
9869 assert_eq!(tfs[0].id.as_deref(), Some("install-script"));
9870 assert_eq!(tfs[0].src, "install.sh.tpl");
9871 assert_eq!(tfs[0].dst, "install.sh");
9872 assert_eq!(tfs[0].mode, Some("0755".to_string()));
9873
9874 assert_eq!(tfs[1].id, None);
9875 assert_eq!(tfs[1].src, "README.md.tpl");
9876 assert_eq!(tfs[1].dst, "README.md");
9877 assert_eq!(tfs[1].mode, None);
9878 }
9879
9880 #[test]
9881 fn test_template_files_defaults_to_none() {
9882 let yaml = r#"
9883project_name: myproject
9884crates: []
9885"#;
9886 let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
9887 assert!(config.template_files.is_none());
9888 }
9889
9890 #[test]
9895 fn test_include_spec_plain_string() {
9896 let yaml = r#"
9897project_name: test
9898includes:
9899 - ./defaults.yaml
9900 - extra.yaml
9901crates: []
9902"#;
9903 let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
9904 let includes = config.includes.unwrap();
9905 assert_eq!(includes.len(), 2);
9906 assert_eq!(
9907 includes[0],
9908 IncludeSpec::Path("./defaults.yaml".to_string())
9909 );
9910 assert_eq!(includes[1], IncludeSpec::Path("extra.yaml".to_string()));
9911 }
9912
9913 #[test]
9914 fn test_include_spec_from_file() {
9915 let yaml = r#"
9916project_name: test
9917includes:
9918 - from_file:
9919 path: ./config/goreleaser.yaml
9920crates: []
9921"#;
9922 let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
9923 let includes = config.includes.unwrap();
9924 assert_eq!(includes.len(), 1);
9925 assert_eq!(
9926 includes[0],
9927 IncludeSpec::FromFile {
9928 from_file: IncludeFilePath {
9929 path: "./config/goreleaser.yaml".to_string(),
9930 },
9931 }
9932 );
9933 }
9934
9935 #[test]
9936 fn test_include_spec_from_url_without_headers() {
9937 let yaml = r#"
9938project_name: test
9939includes:
9940 - from_url:
9941 url: https://example.com/config.yaml
9942crates: []
9943"#;
9944 let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
9945 let includes = config.includes.unwrap();
9946 assert_eq!(includes.len(), 1);
9947 assert_eq!(
9948 includes[0],
9949 IncludeSpec::FromUrl {
9950 from_url: IncludeUrlConfig {
9951 url: "https://example.com/config.yaml".to_string(),
9952 headers: None,
9953 },
9954 }
9955 );
9956 }
9957
9958 #[test]
9959 fn test_include_spec_from_url_with_headers() {
9960 let yaml = r#"
9961project_name: test
9962includes:
9963 - from_url:
9964 url: https://api.mycompany.com/configs/release.yaml
9965 headers:
9966 x-api-token: "${MYCOMPANY_TOKEN}"
9967 Authorization: "Bearer ${GITHUB_TOKEN}"
9968crates: []
9969"#;
9970 let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
9971 let includes = config.includes.unwrap();
9972 assert_eq!(includes.len(), 1);
9973 match &includes[0] {
9974 IncludeSpec::FromUrl { from_url } => {
9975 assert_eq!(
9976 from_url.url,
9977 "https://api.mycompany.com/configs/release.yaml"
9978 );
9979 let headers = from_url.headers.as_ref().unwrap();
9980 assert_eq!(headers.len(), 2);
9981 assert_eq!(headers["x-api-token"], "${MYCOMPANY_TOKEN}");
9982 assert_eq!(headers["Authorization"], "Bearer ${GITHUB_TOKEN}");
9983 }
9984 other => panic!("expected FromUrl, got: {:?}", other),
9985 }
9986 }
9987
9988 #[test]
9989 fn test_include_spec_mixed_forms() {
9990 let yaml = r#"
9991project_name: test
9992includes:
9993 - ./defaults.yaml
9994 - from_file:
9995 path: ./config/shared.yaml
9996 - from_url:
9997 url: https://example.com/config.yaml
9998 headers:
9999 x-token: secret
10000crates: []
10001"#;
10002 let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
10003 let includes = config.includes.unwrap();
10004 assert_eq!(includes.len(), 3);
10005 assert!(matches!(&includes[0], IncludeSpec::Path(s) if s == "./defaults.yaml"));
10006 assert!(
10007 matches!(&includes[1], IncludeSpec::FromFile { from_file } if from_file.path == "./config/shared.yaml")
10008 );
10009 assert!(
10010 matches!(&includes[2], IncludeSpec::FromUrl { from_url } if from_url.url == "https://example.com/config.yaml")
10011 );
10012 }
10013
10014 #[test]
10015 fn test_include_spec_no_includes_field() {
10016 let yaml = r#"
10017project_name: test
10018crates: []
10019"#;
10020 let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
10021 assert!(config.includes.is_none());
10022 }
10023
10024 #[test]
10025 fn test_include_spec_empty_includes() {
10026 let yaml = r#"
10027project_name: test
10028includes: []
10029crates: []
10030"#;
10031 let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
10032 assert_eq!(config.includes, Some(vec![]));
10033 }
10034
10035 #[test]
10036 fn test_include_spec_github_shorthand_url() {
10037 let yaml = r#"
10040project_name: test
10041includes:
10042 - from_url:
10043 url: caarlos0/goreleaserfiles/main/packages.yml
10044crates: []
10045"#;
10046 let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
10047 let includes = config.includes.unwrap();
10048 assert_eq!(includes.len(), 1);
10049 match &includes[0] {
10050 IncludeSpec::FromUrl { from_url } => {
10051 assert_eq!(from_url.url, "caarlos0/goreleaserfiles/main/packages.yml");
10052 }
10053 other => panic!("expected FromUrl, got: {:?}", other),
10054 }
10055 }
10056
10057 #[test]
10060 fn test_github_urls_config_all_fields() {
10061 let yaml = r#"
10062api: https://github.example.com/api/v3/
10063upload: https://github.example.com/api/uploads/
10064download: https://github.example.com/
10065skip_tls_verify: true
10066"#;
10067 let cfg: GitHubUrlsConfig = serde_yaml_ng::from_str(yaml).unwrap();
10068 assert_eq!(
10069 cfg.api.as_deref(),
10070 Some("https://github.example.com/api/v3/")
10071 );
10072 assert_eq!(
10073 cfg.upload.as_deref(),
10074 Some("https://github.example.com/api/uploads/")
10075 );
10076 assert_eq!(cfg.download.as_deref(), Some("https://github.example.com/"));
10077 assert_eq!(cfg.skip_tls_verify, Some(true));
10078 }
10079
10080 #[test]
10081 fn test_github_urls_config_defaults() {
10082 let yaml = "{}";
10083 let cfg: GitHubUrlsConfig = serde_yaml_ng::from_str(yaml).unwrap();
10084 assert_eq!(cfg.api, None);
10085 assert_eq!(cfg.upload, None);
10086 assert_eq!(cfg.download, None);
10087 assert_eq!(cfg.skip_tls_verify, None);
10088 }
10089
10090 #[test]
10091 fn test_gitlab_urls_config_all_fields() {
10092 let yaml = r#"
10093api: https://gitlab.example.com/api/v4/
10094download: https://gitlab.example.com/
10095skip_tls_verify: false
10096use_package_registry: true
10097use_job_token: true
10098"#;
10099 let cfg: GitLabUrlsConfig = serde_yaml_ng::from_str(yaml).unwrap();
10100 assert_eq!(
10101 cfg.api.as_deref(),
10102 Some("https://gitlab.example.com/api/v4/")
10103 );
10104 assert_eq!(cfg.download.as_deref(), Some("https://gitlab.example.com/"));
10105 assert_eq!(cfg.skip_tls_verify, Some(false));
10106 assert_eq!(cfg.use_package_registry, Some(true));
10107 assert_eq!(cfg.use_job_token, Some(true));
10108 }
10109
10110 #[test]
10111 fn test_gitlab_urls_config_defaults() {
10112 let yaml = "{}";
10113 let cfg: GitLabUrlsConfig = serde_yaml_ng::from_str(yaml).unwrap();
10114 assert_eq!(cfg.api, None);
10115 assert_eq!(cfg.download, None);
10116 assert_eq!(cfg.skip_tls_verify, None);
10117 assert_eq!(cfg.use_package_registry, None);
10118 assert_eq!(cfg.use_job_token, None);
10119 }
10120
10121 #[test]
10122 fn test_gitea_urls_config_all_fields() {
10123 let yaml = r#"
10124api: https://gitea.example.com/api/v1/
10125download: https://gitea.example.com/
10126skip_tls_verify: true
10127"#;
10128 let cfg: GiteaUrlsConfig = serde_yaml_ng::from_str(yaml).unwrap();
10129 assert_eq!(
10130 cfg.api.as_deref(),
10131 Some("https://gitea.example.com/api/v1/")
10132 );
10133 assert_eq!(cfg.download.as_deref(), Some("https://gitea.example.com/"));
10134 assert_eq!(cfg.skip_tls_verify, Some(true));
10135 }
10136
10137 #[test]
10138 fn test_gitea_urls_config_defaults() {
10139 let yaml = "{}";
10140 let cfg: GiteaUrlsConfig = serde_yaml_ng::from_str(yaml).unwrap();
10141 assert_eq!(cfg.api, None);
10142 assert_eq!(cfg.download, None);
10143 assert_eq!(cfg.skip_tls_verify, None);
10144 }
10145
10146 #[test]
10147 fn test_release_config_gitlab_gitea_fields() {
10148 let yaml = r#"
10149project_name: test
10150crates:
10151 - name: a
10152 path: "."
10153 tag_template: "v{{ .Version }}"
10154 release:
10155 github:
10156 owner: gh-owner
10157 name: gh-repo
10158 gitlab:
10159 owner: gitlab-owner
10160 name: gitlab-repo
10161 gitea:
10162 owner: gitea-owner
10163 name: gitea-repo
10164"#;
10165 let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
10166 let release = config.crates[0].release.as_ref().unwrap();
10167 let github = release.github.as_ref().unwrap();
10168 assert_eq!(github.owner, "gh-owner");
10169 assert_eq!(github.name, "gh-repo");
10170 let gitlab = release.gitlab.as_ref().unwrap();
10171 assert_eq!(gitlab.owner, "gitlab-owner");
10172 assert_eq!(gitlab.name, "gitlab-repo");
10173 let gitea = release.gitea.as_ref().unwrap();
10174 assert_eq!(gitea.owner, "gitea-owner");
10175 assert_eq!(gitea.name, "gitea-repo");
10176 }
10177
10178 #[test]
10179 fn test_config_github_urls_field() {
10180 let yaml = r#"
10181project_name: test
10182github_urls:
10183 api: https://ghe.corp.com/api/v3/
10184 upload: https://ghe.corp.com/api/uploads/
10185 download: https://ghe.corp.com/
10186 skip_tls_verify: true
10187crates:
10188 - name: a
10189 path: "."
10190 tag_template: "v{{ .Version }}"
10191"#;
10192 let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
10193 let urls = config.github_urls.as_ref().unwrap();
10194 assert_eq!(urls.api.as_deref(), Some("https://ghe.corp.com/api/v3/"));
10195 assert_eq!(
10196 urls.upload.as_deref(),
10197 Some("https://ghe.corp.com/api/uploads/")
10198 );
10199 assert_eq!(urls.download.as_deref(), Some("https://ghe.corp.com/"));
10200 assert_eq!(urls.skip_tls_verify, Some(true));
10201 }
10202
10203 #[test]
10204 fn test_config_gitlab_urls_field() {
10205 let yaml = r#"
10206project_name: test
10207gitlab_urls:
10208 api: https://gitlab.corp.com/api/v4/
10209 download: https://gitlab.corp.com/
10210 skip_tls_verify: false
10211 use_package_registry: true
10212 use_job_token: false
10213crates:
10214 - name: a
10215 path: "."
10216 tag_template: "v{{ .Version }}"
10217"#;
10218 let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
10219 let urls = config.gitlab_urls.as_ref().unwrap();
10220 assert_eq!(urls.api.as_deref(), Some("https://gitlab.corp.com/api/v4/"));
10221 assert_eq!(urls.download.as_deref(), Some("https://gitlab.corp.com/"));
10222 assert_eq!(urls.skip_tls_verify, Some(false));
10223 assert_eq!(urls.use_package_registry, Some(true));
10224 assert_eq!(urls.use_job_token, Some(false));
10225 }
10226
10227 #[test]
10228 fn test_config_gitea_urls_field() {
10229 let yaml = r#"
10230project_name: test
10231gitea_urls:
10232 api: https://gitea.corp.com/api/v1/
10233 download: https://gitea.corp.com/
10234 skip_tls_verify: true
10235crates:
10236 - name: a
10237 path: "."
10238 tag_template: "v{{ .Version }}"
10239"#;
10240 let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
10241 let urls = config.gitea_urls.as_ref().unwrap();
10242 assert_eq!(urls.api.as_deref(), Some("https://gitea.corp.com/api/v1/"));
10243 assert_eq!(urls.download.as_deref(), Some("https://gitea.corp.com/"));
10244 assert_eq!(urls.skip_tls_verify, Some(true));
10245 }
10246
10247 #[test]
10248 fn test_config_force_token_field() {
10249 let yaml = r#"
10250project_name: test
10251force_token: gitlab
10252crates:
10253 - name: a
10254 path: "."
10255 tag_template: "v{{ .Version }}"
10256"#;
10257 let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
10258 assert_eq!(config.force_token, Some(ForceTokenKind::GitLab));
10259 }
10260
10261 #[test]
10262 fn test_config_force_token_omitted() {
10263 let yaml = r#"
10264project_name: test
10265crates:
10266 - name: a
10267 path: "."
10268 tag_template: "v{{ .Version }}"
10269"#;
10270 let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
10271 assert_eq!(config.force_token, None::<ForceTokenKind>);
10272 }
10273
10274 #[test]
10275 fn test_config_all_platform_urls_and_force_token() {
10276 let yaml = r#"
10277project_name: test
10278github_urls:
10279 api: https://ghe.corp.com/api/v3/
10280gitlab_urls:
10281 api: https://gitlab.corp.com/api/v4/
10282 use_job_token: true
10283gitea_urls:
10284 api: https://gitea.corp.com/api/v1/
10285force_token: github
10286crates:
10287 - name: a
10288 path: "."
10289 tag_template: "v{{ .Version }}"
10290"#;
10291 let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
10292 assert_eq!(
10293 config.github_urls.as_ref().unwrap().api.as_deref(),
10294 Some("https://ghe.corp.com/api/v3/")
10295 );
10296 assert_eq!(
10297 config.gitlab_urls.as_ref().unwrap().api.as_deref(),
10298 Some("https://gitlab.corp.com/api/v4/")
10299 );
10300 assert_eq!(
10301 config.gitlab_urls.as_ref().unwrap().use_job_token,
10302 Some(true)
10303 );
10304 assert_eq!(
10305 config.gitea_urls.as_ref().unwrap().api.as_deref(),
10306 Some("https://gitea.corp.com/api/v1/")
10307 );
10308 assert_eq!(config.force_token, Some(ForceTokenKind::GitHub));
10309 }
10310
10311 #[test]
10312 fn test_dockerhub_config_parse() {
10313 let yaml = r#"
10314project_name: test
10315dockerhub:
10316 - username: myuser
10317 secret_name: DOCKER_TOKEN
10318 images:
10319 - myorg/myapp
10320 description: "My app"
10321 disable: true
10322 full_description:
10323 from_file:
10324 path: ./README.md
10325"#;
10326 let cfg: Config = serde_yaml_ng::from_str(yaml).unwrap();
10327 let dh = &cfg.dockerhub.unwrap()[0];
10328 assert_eq!(dh.username.as_deref(), Some("myuser"));
10329 assert_eq!(dh.secret_name.as_deref(), Some("DOCKER_TOKEN"));
10330 assert_eq!(dh.images.as_ref().unwrap(), &["myorg/myapp"]);
10331 assert_eq!(dh.description.as_deref(), Some("My app"));
10332 assert_eq!(dh.disable, Some(StringOrBool::Bool(true)));
10333 let fd = dh.full_description.as_ref().unwrap();
10334 assert!(fd.from_url.is_none());
10335 let ff = fd.from_file.as_ref().unwrap();
10336 assert_eq!(ff.path, "./README.md");
10337 }
10338
10339 #[test]
10340 fn test_dockerhub_from_url_parse() {
10341 let yaml = r#"
10342project_name: test
10343dockerhub:
10344 - username: myuser
10345 full_description:
10346 from_url:
10347 url: "https://raw.githubusercontent.com/org/repo/main/README.md"
10348 headers:
10349 Authorization: "Bearer {{ .Env.GH_TOKEN }}"
10350"#;
10351 let cfg: Config = serde_yaml_ng::from_str(yaml).unwrap();
10352 let dh = &cfg.dockerhub.unwrap()[0];
10353 let fu = dh
10354 .full_description
10355 .as_ref()
10356 .unwrap()
10357 .from_url
10358 .as_ref()
10359 .unwrap();
10360 assert_eq!(
10361 fu.url,
10362 "https://raw.githubusercontent.com/org/repo/main/README.md"
10363 );
10364 let headers = fu.headers.as_ref().unwrap();
10365 assert_eq!(
10366 headers.get("Authorization").unwrap(),
10367 "Bearer {{ .Env.GH_TOKEN }}"
10368 );
10369 }
10370
10371 #[test]
10372 fn test_artifactory_config_parse() {
10373 let yaml = r#"
10374project_name: test
10375artifactories:
10376 - name: production
10377 target: "https://artifactory.example.com/repo/{{ .ProjectName }}/{{ .Version }}/"
10378 username: deployer
10379 mode: archive
10380 skip: "{{ .Env.SKIP }}"
10381 ids:
10382 - default
10383"#;
10384 let cfg: Config = serde_yaml_ng::from_str(yaml).unwrap();
10385 let art = &cfg.artifactories.unwrap()[0];
10386 assert_eq!(art.name.as_deref(), Some("production"));
10387 assert_eq!(
10388 art.target.as_deref(),
10389 Some("https://artifactory.example.com/repo/{{ .ProjectName }}/{{ .Version }}/")
10390 );
10391 assert_eq!(art.username.as_deref(), Some("deployer"));
10392 assert_eq!(art.mode.as_deref(), Some("archive"));
10393 assert_eq!(
10394 art.skip,
10395 Some(StringOrBool::String("{{ .Env.SKIP }}".to_string()))
10396 );
10397 assert_eq!(art.ids.as_ref().unwrap(), &["default"]);
10398 }
10399
10400 #[test]
10401 fn test_cloudsmith_config_parse() {
10402 let yaml = r#"
10403project_name: test
10404cloudsmiths:
10405 - organization: myorg
10406 repository: myrepo
10407 formats:
10408 - deb
10409 distributions:
10410 deb: "ubuntu/focal"
10411"#;
10412 let cfg: Config = serde_yaml_ng::from_str(yaml).unwrap();
10413 let cs = &cfg.cloudsmiths.unwrap()[0];
10414 assert_eq!(cs.organization.as_deref(), Some("myorg"));
10415 assert_eq!(cs.repository.as_deref(), Some("myrepo"));
10416 assert_eq!(cs.formats.as_ref().unwrap(), &["deb"]);
10417 let dists = cs.distributions.as_ref().unwrap();
10418 assert_eq!(dists.get("deb").unwrap(), "ubuntu/focal");
10419 }
10420
10421 #[test]
10426 fn test_docker_sign_env_map_format() {
10427 let yaml = r#"
10428project_name: test
10429docker_signs:
10430 - cmd: cosign
10431 env:
10432 COSIGN_PASSWORD: hunter2
10433 COSIGN_KEY: /path/to/key
10434"#;
10435 let cfg: Config = serde_yaml_ng::from_str(yaml).unwrap();
10436 let ds = &cfg.docker_signs.as_ref().unwrap()[0];
10437 let env = ds
10438 .env
10439 .as_ref()
10440 .unwrap_or_else(|| panic!("env should be Some"));
10441 assert_eq!(env.get("COSIGN_PASSWORD").unwrap(), "hunter2");
10442 assert_eq!(env.get("COSIGN_KEY").unwrap(), "/path/to/key");
10443 }
10444
10445 #[test]
10446 fn test_docker_sign_env_list_format() {
10447 let yaml = r#"
10448project_name: test
10449docker_signs:
10450 - cmd: cosign
10451 env:
10452 - COSIGN_PASSWORD=hunter2
10453 - COSIGN_KEY=/path/to/key
10454"#;
10455 let cfg: Config = serde_yaml_ng::from_str(yaml).unwrap();
10456 let ds = &cfg.docker_signs.as_ref().unwrap()[0];
10457 let env = ds
10458 .env
10459 .as_ref()
10460 .unwrap_or_else(|| panic!("env should be Some"));
10461 assert_eq!(env.get("COSIGN_PASSWORD").unwrap(), "hunter2");
10462 assert_eq!(env.get("COSIGN_KEY").unwrap(), "/path/to/key");
10463 }
10464
10465 #[test]
10466 fn test_docker_sign_env_list_split_on_first_equals() {
10467 let yaml = r#"
10468project_name: test
10469docker_signs:
10470 - cmd: cosign
10471 env:
10472 - FLAGS=--key=val --other=stuff
10473"#;
10474 let cfg: Config = serde_yaml_ng::from_str(yaml).unwrap();
10475 let ds = &cfg.docker_signs.as_ref().unwrap()[0];
10476 let env = ds
10477 .env
10478 .as_ref()
10479 .unwrap_or_else(|| panic!("env should be Some"));
10480 assert_eq!(env.get("FLAGS").unwrap(), "--key=val --other=stuff");
10481 }
10482
10483 #[test]
10484 fn test_docker_sign_env_null() {
10485 let yaml = r#"
10486project_name: test
10487docker_signs:
10488 - cmd: cosign
10489 env: ~
10490"#;
10491 let cfg: Config = serde_yaml_ng::from_str(yaml).unwrap();
10492 let ds = &cfg.docker_signs.as_ref().unwrap()[0];
10493 assert!(ds.env.is_none());
10494 }
10495
10496 #[test]
10497 fn test_docker_sign_env_missing() {
10498 let yaml = r#"
10499project_name: test
10500docker_signs:
10501 - cmd: cosign
10502"#;
10503 let cfg: Config = serde_yaml_ng::from_str(yaml).unwrap();
10504 let ds = &cfg.docker_signs.as_ref().unwrap()[0];
10505 assert!(ds.env.is_none());
10506 }
10507
10508 #[test]
10509 fn test_docker_sign_env_list_invalid_no_equals() {
10510 let yaml = r#"
10511project_name: test
10512docker_signs:
10513 - cmd: cosign
10514 env:
10515 - COSIGN_PASSWORD
10516"#;
10517 let result = serde_yaml_ng::from_str::<Config>(yaml);
10518 assert!(result.is_err(), "entry without '=' should fail");
10519 }
10520
10521 #[test]
10522 fn test_sign_config_env_list_format() {
10523 let yaml = r#"
10524project_name: test
10525signs:
10526 - cmd: gpg
10527 env:
10528 - GPG_KEY=ABCDEF
10529 - GPG_TTY=/dev/pts/0
10530"#;
10531 let cfg: Config = serde_yaml_ng::from_str(yaml).unwrap();
10532 let s = &cfg.signs[0];
10533 let env = s
10534 .env
10535 .as_ref()
10536 .unwrap_or_else(|| panic!("env should be Some"));
10537 assert_eq!(env.get("GPG_KEY").unwrap(), "ABCDEF");
10538 assert_eq!(env.get("GPG_TTY").unwrap(), "/dev/pts/0");
10539 }
10540
10541 #[test]
10542 fn test_publisher_env_list_format() {
10543 let yaml = r#"
10544project_name: test
10545publishers:
10546 - name: mypub
10547 cmd: publish.sh
10548 env:
10549 - API_TOKEN=secret123
10550"#;
10551 let cfg: Config = serde_yaml_ng::from_str(yaml).unwrap();
10552 let p = &cfg.publishers.as_ref().unwrap()[0];
10553 let env = p
10554 .env
10555 .as_ref()
10556 .unwrap_or_else(|| panic!("env should be Some"));
10557 assert_eq!(env.get("API_TOKEN").unwrap(), "secret123");
10558 }
10559
10560 #[test]
10565 fn test_build_override_env_list_format() {
10566 let yaml = r#"
10567project_name: test
10568defaults:
10569 targets:
10570 - x86_64-unknown-linux-gnu
10571 overrides:
10572 - targets:
10573 - "x86_64-*"
10574 env:
10575 - CC=gcc-12
10576 - CFLAGS=-O2 -Wall
10577crates: []
10578"#;
10579 let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
10580 let overrides = config.defaults.unwrap().overrides.unwrap();
10581 let env = overrides[0]
10582 .env
10583 .as_ref()
10584 .unwrap_or_else(|| panic!("env should be Some"));
10585 assert_eq!(env.get("CC").unwrap(), "gcc-12");
10586 assert_eq!(env.get("CFLAGS").unwrap(), "-O2 -Wall");
10587 }
10588
10589 #[test]
10590 fn test_build_override_env_map_format() {
10591 let yaml = r#"
10592project_name: test
10593defaults:
10594 targets:
10595 - x86_64-unknown-linux-gnu
10596 overrides:
10597 - targets:
10598 - "x86_64-*"
10599 env:
10600 CC: gcc-12
10601 CFLAGS: "-O2 -Wall"
10602crates: []
10603"#;
10604 let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
10605 let overrides = config.defaults.unwrap().overrides.unwrap();
10606 let env = overrides[0]
10607 .env
10608 .as_ref()
10609 .unwrap_or_else(|| panic!("env should be Some"));
10610 assert_eq!(env.get("CC").unwrap(), "gcc-12");
10611 assert_eq!(env.get("CFLAGS").unwrap(), "-O2 -Wall");
10612 }
10613
10614 #[test]
10619 fn test_structured_hook_env_list_format() {
10620 let yaml = r#"
10621project_name: test
10622before:
10623 hooks:
10624 - cmd: echo hello
10625 env:
10626 - MY_VAR=foo
10627 - OTHER=bar=baz
10628"#;
10629 let cfg: Config = serde_yaml_ng::from_str(yaml).unwrap();
10630 let hooks = cfg.before.as_ref().unwrap().hooks.as_ref().unwrap();
10631 match &hooks[0] {
10632 HookEntry::Structured(h) => {
10633 let env = h
10634 .env
10635 .as_ref()
10636 .unwrap_or_else(|| panic!("env should be Some"));
10637 assert_eq!(env.get("MY_VAR").unwrap(), "foo");
10638 assert_eq!(env.get("OTHER").unwrap(), "bar=baz");
10639 }
10640 HookEntry::Simple(_) => panic!("expected Structured hook"),
10641 }
10642 }
10643
10644 #[test]
10645 fn test_structured_hook_env_map_format() {
10646 let yaml = r#"
10647project_name: test
10648before:
10649 hooks:
10650 - cmd: echo hello
10651 env:
10652 MY_VAR: foo
10653 OTHER: "bar=baz"
10654"#;
10655 let cfg: Config = serde_yaml_ng::from_str(yaml).unwrap();
10656 let hooks = cfg.before.as_ref().unwrap().hooks.as_ref().unwrap();
10657 match &hooks[0] {
10658 HookEntry::Structured(h) => {
10659 let env = h
10660 .env
10661 .as_ref()
10662 .unwrap_or_else(|| panic!("env should be Some"));
10663 assert_eq!(env.get("MY_VAR").unwrap(), "foo");
10664 assert_eq!(env.get("OTHER").unwrap(), "bar=baz");
10665 }
10666 HookEntry::Simple(_) => panic!("expected Structured hook"),
10667 }
10668 }
10669
10670 #[test]
10675 fn test_sign_config_env_map_format() {
10676 let yaml = r#"
10677project_name: test
10678signs:
10679 - cmd: gpg
10680 env:
10681 GPG_KEY: ABCDEF
10682 GPG_TTY: /dev/pts/0
10683"#;
10684 let cfg: Config = serde_yaml_ng::from_str(yaml).unwrap();
10685 let s = &cfg.signs[0];
10686 let env = s
10687 .env
10688 .as_ref()
10689 .unwrap_or_else(|| panic!("env should be Some"));
10690 assert_eq!(env.get("GPG_KEY").unwrap(), "ABCDEF");
10691 assert_eq!(env.get("GPG_TTY").unwrap(), "/dev/pts/0");
10692 }
10693
10694 #[test]
10699 fn test_publisher_env_map_format() {
10700 let yaml = r#"
10701project_name: test
10702publishers:
10703 - name: mypub
10704 cmd: publish.sh
10705 env:
10706 API_TOKEN: secret123
10707 DEPLOY_ENV: staging
10708"#;
10709 let cfg: Config = serde_yaml_ng::from_str(yaml).unwrap();
10710 let p = &cfg.publishers.as_ref().unwrap()[0];
10711 let env = p
10712 .env
10713 .as_ref()
10714 .unwrap_or_else(|| panic!("env should be Some"));
10715 assert_eq!(env.get("API_TOKEN").unwrap(), "secret123");
10716 assert_eq!(env.get("DEPLOY_ENV").unwrap(), "staging");
10717 }
10718
10719 #[test]
10724 fn test_sbom_config_env_map_format() {
10725 let yaml = r#"
10726project_name: test
10727sboms:
10728 - cmd: syft
10729 env:
10730 SYFT_FILE_METADATA_CATALOGER_ENABLED: "true"
10731 SYFT_SCOPE: all-layers
10732"#;
10733 let cfg: Config = serde_yaml_ng::from_str(yaml).unwrap();
10734 let s = &cfg.sboms[0];
10735 let env = s
10736 .env
10737 .as_ref()
10738 .unwrap_or_else(|| panic!("env should be Some"));
10739 assert_eq!(
10740 env.get("SYFT_FILE_METADATA_CATALOGER_ENABLED").unwrap(),
10741 "true"
10742 );
10743 assert_eq!(env.get("SYFT_SCOPE").unwrap(), "all-layers");
10744 }
10745
10746 #[test]
10747 fn test_sbom_config_env_list_format() {
10748 let yaml = r#"
10749project_name: test
10750sboms:
10751 - cmd: syft
10752 env:
10753 - SYFT_FILE_METADATA_CATALOGER_ENABLED=true
10754 - SYFT_SCOPE=all-layers
10755"#;
10756 let cfg: Config = serde_yaml_ng::from_str(yaml).unwrap();
10757 let s = &cfg.sboms[0];
10758 let env = s
10759 .env
10760 .as_ref()
10761 .unwrap_or_else(|| panic!("env should be Some"));
10762 assert_eq!(
10763 env.get("SYFT_FILE_METADATA_CATALOGER_ENABLED").unwrap(),
10764 "true"
10765 );
10766 assert_eq!(env.get("SYFT_SCOPE").unwrap(), "all-layers");
10767 }
10768
10769 #[test]
10770 fn test_sbom_config_env_missing() {
10771 let yaml = r#"
10772project_name: test
10773sboms:
10774 - cmd: syft
10775"#;
10776 let cfg: Config = serde_yaml_ng::from_str(yaml).unwrap();
10777 let s = &cfg.sboms[0];
10778 assert!(s.env.is_none());
10779 }
10780}