use std::collections::HashMap;
use std::path::PathBuf;
use schemars::JsonSchema;
use serde::{Deserialize, Deserializer, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
#[serde(untagged)]
pub enum IncludeSpec {
Path(String),
FromFile { from_file: IncludeFilePath },
FromUrl { from_url: IncludeUrlConfig },
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
pub struct IncludeFilePath {
pub path: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
pub struct IncludeUrlConfig {
pub url: String,
pub headers: Option<HashMap<String, String>>,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(default, deny_unknown_fields)]
pub struct Config {
pub version: Option<u32>,
pub project_name: String,
#[serde(default = "default_dist")]
pub dist: PathBuf,
pub includes: Option<Vec<IncludeSpec>>,
pub env_files: Option<EnvFilesConfig>,
pub defaults: Option<Defaults>,
pub before: Option<HooksConfig>,
pub after: Option<HooksConfig>,
pub crates: Vec<CrateConfig>,
pub changelog: Option<ChangelogConfig>,
#[serde(default, alias = "sign", deserialize_with = "deserialize_signs")]
#[schemars(schema_with = "signs_schema")]
pub signs: Vec<SignConfig>,
#[serde(default, alias = "binary_sign", deserialize_with = "deserialize_signs")]
#[schemars(schema_with = "signs_schema")]
pub binary_signs: Vec<SignConfig>,
pub docker_signs: Option<Vec<DockerSignConfig>>,
#[serde(default, deserialize_with = "deserialize_upx")]
#[schemars(schema_with = "upx_schema")]
pub upx: Vec<UpxConfig>,
pub snapshot: Option<SnapshotConfig>,
pub nightly: Option<NightlyConfig>,
pub announce: Option<AnnounceConfig>,
pub report_sizes: Option<bool>,
#[serde(default, deserialize_with = "deserialize_env_map")]
pub env: Option<HashMap<String, String>>,
pub variables: Option<HashMap<String, String>>,
pub publishers: Option<Vec<PublisherConfig>>,
pub dockerhub: Option<Vec<DockerHubConfig>>,
pub artifactories: Option<Vec<ArtifactoryConfig>>,
pub cloudsmiths: Option<Vec<CloudSmithConfig>>,
pub homebrew_casks: Option<Vec<TopLevelHomebrewCaskConfig>>,
pub tag: Option<TagConfig>,
pub git: Option<GitConfig>,
pub partial: Option<PartialConfig>,
pub workspaces: Option<Vec<WorkspaceConfig>>,
pub source: Option<SourceConfig>,
#[serde(default, alias = "sbom", deserialize_with = "deserialize_sboms")]
#[schemars(schema_with = "sboms_schema")]
pub sboms: Vec<SbomConfig>,
pub release: Option<ReleaseConfig>,
pub github_urls: Option<GitHubUrlsConfig>,
pub gitlab_urls: Option<GitLabUrlsConfig>,
pub gitea_urls: Option<GiteaUrlsConfig>,
pub force_token: Option<ForceTokenKind>,
pub notarize: Option<NotarizeConfig>,
pub metadata: Option<MetadataConfig>,
pub template_files: Option<Vec<TemplateFileConfig>>,
pub monorepo: Option<MonorepoConfig>,
#[serde(
default,
alias = "makeself",
deserialize_with = "deserialize_makeselfs"
)]
#[schemars(schema_with = "makeselfs_schema")]
pub makeselfs: Vec<MakeselfConfig>,
pub srpm: Option<SrpmConfig>,
pub milestones: Option<Vec<MilestoneConfig>>,
pub uploads: Option<Vec<UploadConfig>>,
pub aur_sources: Option<Vec<AurSourceConfig>>,
}
fn signs_schema(generator: &mut schemars::r#gen::SchemaGenerator) -> schemars::schema::Schema {
let mut schema = generator.subschema_for::<Vec<SignConfig>>();
if let schemars::schema::Schema::Object(ref mut obj) = schema {
obj.metadata().description = Some("Artifact signing configurations (cosign, GPG, etc.). Accepts a single object or array.".to_owned());
}
schema
}
fn upx_schema(generator: &mut schemars::r#gen::SchemaGenerator) -> schemars::schema::Schema {
let mut schema = generator.subschema_for::<Vec<UpxConfig>>();
if let schemars::schema::Schema::Object(ref mut obj) = schema {
obj.metadata().description = Some(
"UPX binary compression configurations. Accepts a single object or array.".to_owned(),
);
}
schema
}
fn sboms_schema(generator: &mut schemars::r#gen::SchemaGenerator) -> schemars::schema::Schema {
let mut schema = generator.subschema_for::<Vec<SbomConfig>>();
if let schemars::schema::Schema::Object(ref mut obj) = schema {
obj.metadata().description =
Some("SBOM generation configurations. Accepts a single object or array.".to_owned());
}
schema
}
fn default_dist() -> PathBuf {
PathBuf::from("./dist")
}
impl Default for Config {
fn default() -> Self {
Config {
version: None,
project_name: String::new(),
dist: default_dist(),
includes: None,
env_files: None,
defaults: None,
before: None,
after: None,
crates: Vec::new(),
changelog: None,
signs: Vec::new(),
binary_signs: Vec::new(),
docker_signs: None,
upx: Vec::new(),
snapshot: None,
nightly: None,
announce: None,
report_sizes: None,
env: None,
variables: None,
publishers: None,
dockerhub: None,
artifactories: None,
cloudsmiths: None,
homebrew_casks: None,
tag: None,
git: None,
partial: None,
workspaces: None,
source: None,
sboms: Vec::new(),
release: None,
github_urls: None,
gitlab_urls: None,
gitea_urls: None,
force_token: None,
notarize: None,
metadata: None,
template_files: None,
monorepo: None,
makeselfs: Vec::new(),
srpm: None,
milestones: None,
uploads: None,
aur_sources: None,
}
}
}
impl Config {
pub fn monorepo_tag_prefix(&self) -> Option<&str> {
self.monorepo.as_ref().and_then(|m| m.tag_prefix.as_deref())
}
pub fn monorepo_dir(&self) -> Option<&str> {
self.monorepo.as_ref().and_then(|m| m.dir.as_deref())
}
pub fn meta_homepage(&self) -> Option<&str> {
self.metadata.as_ref().and_then(|m| m.homepage.as_deref())
}
pub fn meta_license(&self) -> Option<&str> {
self.metadata.as_ref().and_then(|m| m.license.as_deref())
}
pub fn meta_description(&self) -> Option<&str> {
self.metadata
.as_ref()
.and_then(|m| m.description.as_deref())
}
pub fn meta_maintainers(&self) -> &[String] {
self.metadata
.as_ref()
.and_then(|m| m.maintainers.as_deref())
.unwrap_or(&[])
}
pub fn meta_first_maintainer(&self) -> Option<&str> {
self.meta_maintainers().first().map(|s| s.as_str())
}
}
pub fn validate_version(config: &Config) -> Result<(), String> {
match config.version {
None | Some(1) | Some(2) => Ok(()),
Some(v) => Err(format!(
"unsupported config version: {}. Supported versions are 1 and 2.",
v
)),
}
}
pub fn validate_tag_sort(config: &Config) -> Result<(), String> {
if let Some(ref git) = config.git
&& let Some(ref sort) = git.tag_sort
{
match sort.as_str() {
"-version:refname" | "-version:creatordate" => {}
other => {
return Err(format!(
"unsupported git.tag_sort value: \"{}\". \
Accepted values: \"-version:refname\", \"-version:creatordate\".",
other
));
}
}
}
Ok(())
}
const KNOWN_GOOS: &[&str] = &[
"aix",
"android",
"darwin",
"dragonfly",
"freebsd",
"illumos",
"ios",
"js",
"linux",
"netbsd",
"openbsd",
"plan9",
"solaris",
"wasip1",
"windows",
];
pub fn validate_release_backends(config: &Config) -> Result<(), String> {
let check = |crate_name: &str, release: &ReleaseConfig| -> Result<(), String> {
let mut set = Vec::new();
if release.github.is_some() {
set.push("github");
}
if release.gitlab.is_some() {
set.push("gitlab");
}
if release.gitea.is_some() {
set.push("gitea");
}
if set.len() > 1 {
return Err(format!(
"crate {}: release config sets multiple mutually-exclusive SCM \
backends ({}). Pick one.",
crate_name,
set.join(" + ")
));
}
Ok(())
};
for krate in &config.crates {
if let Some(ref release) = krate.release {
check(&krate.name, release)?;
}
}
if let Some(ws_list) = config.workspaces.as_ref() {
for ws in ws_list {
for krate in &ws.crates {
if let Some(ref release) = krate.release {
check(&krate.name, release)?;
}
}
}
}
Ok(())
}
pub fn validate_format_overrides(config: &Config) -> Result<(), String> {
let check = |crate_name: &str, archives: &[ArchiveConfig]| -> Result<(), String> {
for (idx, archive) in archives.iter().enumerate() {
let Some(ref overrides) = archive.format_overrides else {
continue;
};
for over in overrides {
if !KNOWN_GOOS.contains(&over.os.as_str()) {
let archive_id = archive.id.as_deref().unwrap_or("default");
return Err(format!(
"crate {}: archives[{}] (id={}): format_overrides.goos=\"{}\" is not a recognised OS. \
Accepted values: {}.",
crate_name,
idx,
archive_id,
over.os,
KNOWN_GOOS.join(", ")
));
}
}
}
Ok(())
};
for krate in &config.crates {
if let ArchivesConfig::Configs(ref list) = krate.archives {
check(&krate.name, list)?;
}
}
if let Some(ws_list) = config.workspaces.as_ref() {
for ws in ws_list {
for krate in &ws.crates {
if let ArchivesConfig::Configs(ref list) = krate.archives {
check(&krate.name, list)?;
}
}
}
}
Ok(())
}
#[derive(Debug, Clone, Serialize, JsonSchema)]
#[serde(untagged)]
pub enum EnvFilesConfig {
List(Vec<String>),
TokenFiles(EnvFilesTokenConfig),
}
impl<'de> Deserialize<'de> for EnvFilesConfig {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let value = serde_yaml_ng::Value::deserialize(deserializer)?;
match &value {
serde_yaml_ng::Value::Sequence(_) => {
let list: Vec<String> =
serde_yaml_ng::from_value(value).map_err(serde::de::Error::custom)?;
Ok(EnvFilesConfig::List(list))
}
serde_yaml_ng::Value::Mapping(_) => {
let tokens: EnvFilesTokenConfig =
serde_yaml_ng::from_value(value).map_err(serde::de::Error::custom)?;
Ok(EnvFilesConfig::TokenFiles(tokens))
}
_ => Err(serde::de::Error::custom(
"env_files must be an array of file paths or a mapping with token file paths",
)),
}
}
}
impl EnvFilesConfig {
pub fn as_list(&self) -> Option<&[String]> {
match self {
EnvFilesConfig::List(files) => Some(files),
EnvFilesConfig::TokenFiles(_) => None,
}
}
pub fn as_token_files(&self) -> Option<&EnvFilesTokenConfig> {
match self {
EnvFilesConfig::List(_) => None,
EnvFilesConfig::TokenFiles(tokens) => Some(tokens),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
#[serde(default, deny_unknown_fields)]
pub struct EnvFilesTokenConfig {
pub github_token: Option<String>,
pub gitlab_token: Option<String>,
pub gitea_token: Option<String>,
}
pub fn read_token_file(path: &str) -> Result<Option<String>, String> {
let expanded = if let Some(suffix) = path.strip_prefix("~/") {
if let Ok(home) = std::env::var("HOME") {
format!("{}/{}", home, suffix)
} else {
path.to_string()
}
} else {
path.to_string()
};
match std::fs::read_to_string(&expanded) {
Ok(content) => {
let token = content.lines().next().unwrap_or("").trim().to_string();
if token.is_empty() {
Ok(None)
} else {
Ok(Some(token))
}
}
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None),
Err(e) => Err(format!("failed to read token file '{}': {}", path, e)),
}
}
pub fn load_token_files(
config: &EnvFilesTokenConfig,
log: &crate::log::StageLogger,
) -> Result<std::collections::HashMap<String, String>, String> {
let mut vars = std::collections::HashMap::new();
let github_candidates: Vec<&str> = match config.github_token.as_deref() {
Some(p) => vec![p],
None => vec![
"~/.config/anodizer/github_token",
"~/.config/goreleaser/github_token",
],
};
let gitlab_candidates: Vec<&str> = match config.gitlab_token.as_deref() {
Some(p) => vec![p],
None => vec![
"~/.config/anodizer/gitlab_token",
"~/.config/goreleaser/gitlab_token",
],
};
let gitea_candidates: Vec<&str> = match config.gitea_token.as_deref() {
Some(p) => vec![p],
None => vec![
"~/.config/anodizer/gitea_token",
"~/.config/goreleaser/gitea_token",
],
};
let mappings: [(&str, &[&str]); 3] = [
("GITHUB_TOKEN", &github_candidates),
("GITLAB_TOKEN", &gitlab_candidates),
("GITEA_TOKEN", &gitea_candidates),
];
for (env_name, candidates) in &mappings {
if std::env::var(env_name)
.ok()
.filter(|v| !v.is_empty())
.is_some()
{
log.verbose(&format!("using {} from process environment", env_name));
continue;
}
for file_path in candidates.iter() {
match read_token_file(file_path) {
Ok(Some(token)) => {
log.verbose(&format!("loaded {} from {}", env_name, file_path));
vars.insert(env_name.to_string(), token);
break;
}
Ok(None) => {
}
Err(e) => {
return Err(e);
}
}
}
}
Ok(vars)
}
pub fn load_env_files(
files: &[String],
log: &crate::log::StageLogger,
strict: bool,
) -> Result<std::collections::HashMap<String, String>, String> {
let mut vars = std::collections::HashMap::new();
for file_path in files {
let content = match std::fs::read_to_string(file_path) {
Ok(c) => c,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
if strict {
return Err(format!("env file '{}' not found (strict mode)", file_path));
}
log.warn(&format!("env file '{}' not found, skipping", file_path));
continue;
}
Err(e) => {
return Err(format!("failed to read env file '{}': {}", file_path, e));
}
};
for line in content.lines() {
let trimmed = line.trim();
if trimmed.is_empty() || trimmed.starts_with('#') {
continue;
}
let trimmed = trimmed.strip_prefix("export ").unwrap_or(trimmed);
if let Some((key, value)) = trimmed.split_once('=') {
let key = key.trim();
if key.is_empty() {
log.warn(&format!(
"skipping line with empty key in '{}': {}",
file_path,
line.trim()
));
continue;
}
let value = value.trim();
let value = if value.len() >= 2
&& ((value.starts_with('"') && value.ends_with('"'))
|| (value.starts_with('\'') && value.ends_with('\'')))
{
&value[1..value.len() - 1]
} else {
value
};
vars.insert(key.to_string(), value.to_string());
} else {
log.warn(&format!(
"skipping line without '=' in '{}': {}",
file_path, trimmed
));
}
}
}
Ok(vars)
}
fn deserialize_env_map<'de, D>(deserializer: D) -> Result<Option<HashMap<String, String>>, D::Error>
where
D: Deserializer<'de>,
{
use serde::de::{self, Visitor};
struct EnvMapVisitor;
impl<'de> Visitor<'de> for EnvMapVisitor {
type Value = Option<HashMap<String, String>>;
fn expecting(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("a mapping of env vars (KEY: VALUE) or a list of KEY=VALUE strings")
}
fn visit_none<E: de::Error>(self) -> Result<Self::Value, E> {
Ok(None)
}
fn visit_unit<E: de::Error>(self) -> Result<Self::Value, E> {
Ok(None)
}
fn visit_map<M: de::MapAccess<'de>>(self, mut map: M) -> Result<Self::Value, M::Error> {
let mut result = HashMap::new();
while let Some((key, value)) = map.next_entry::<String, String>()? {
result.insert(key, value);
}
Ok(Some(result))
}
fn visit_seq<A: de::SeqAccess<'de>>(self, mut seq: A) -> Result<Self::Value, A::Error> {
let mut result = HashMap::new();
while let Some(entry) = seq.next_element::<String>()? {
match entry.split_once('=') {
Some((key, value)) => {
let key = key.trim();
if key.is_empty() {
return Err(de::Error::custom(format!(
"env list entry has empty key: {:?}",
entry
)));
}
result.insert(key.to_string(), value.to_string());
}
None => {
return Err(de::Error::custom(format!(
"env list entry must be KEY=VALUE, got: {:?}",
entry
)));
}
}
}
Ok(Some(result))
}
}
deserializer.deserialize_any(EnvMapVisitor)
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
#[serde(default)]
pub struct Defaults {
pub targets: Option<Vec<String>>,
pub cross: Option<CrossStrategy>,
pub flags: Option<String>,
pub archives: Option<DefaultArchiveConfig>,
pub checksum: Option<ChecksumConfig>,
pub ignore: Option<Vec<BuildIgnore>>,
pub overrides: Option<Vec<BuildOverride>>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
#[serde(default)]
pub struct DefaultArchiveConfig {
pub format: Option<String>,
pub format_overrides: Option<Vec<FormatOverride>>,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct BuildIgnore {
pub os: String,
pub arch: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
#[serde(default)]
pub struct BuildOverride {
pub targets: Vec<String>,
#[serde(default, deserialize_with = "deserialize_env_map")]
pub env: Option<HashMap<String, String>>,
pub flags: Option<String>,
pub features: Option<Vec<String>>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
#[serde(rename_all = "lowercase")]
pub enum CrossStrategy {
Auto,
Zigbuild,
Cross,
Cargo,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(default)]
pub struct CrateConfig {
pub name: String,
pub path: String,
pub tag_template: String,
pub version: Option<String>,
pub depends_on: Option<Vec<String>>,
pub builds: Option<Vec<BuildConfig>>,
pub cross: Option<CrossStrategy>,
#[serde(default, deserialize_with = "deserialize_archives_config")]
#[schemars(schema_with = "archives_schema")]
pub archives: ArchivesConfig,
pub checksum: Option<ChecksumConfig>,
pub release: Option<ReleaseConfig>,
pub publish: Option<PublishConfig>,
pub docker: Option<Vec<DockerConfig>>,
pub docker_v2: Option<Vec<DockerV2Config>>,
pub docker_digest: Option<DockerDigestConfig>,
pub docker_manifests: Option<Vec<DockerManifestConfig>>,
pub nfpm: Option<Vec<NfpmConfig>>,
pub snapcrafts: Option<Vec<SnapcraftConfig>>,
pub dmgs: Option<Vec<DmgConfig>>,
pub msis: Option<Vec<MsiConfig>>,
pub pkgs: Option<Vec<PkgConfig>>,
pub nsis: Option<Vec<NsisConfig>>,
pub app_bundles: Option<Vec<AppBundleConfig>>,
pub flatpaks: Option<Vec<FlatpakConfig>>,
pub blobs: Option<Vec<BlobConfig>>,
pub binstall: Option<BinstallConfig>,
pub version_sync: Option<VersionSyncConfig>,
pub universal_binaries: Option<Vec<UniversalBinaryConfig>>,
#[serde(default, deserialize_with = "deserialize_string_or_bool_opt")]
pub no_unique_dist_dir: Option<StringOrBool>,
}
fn archives_schema(generator: &mut schemars::r#gen::SchemaGenerator) -> schemars::schema::Schema {
let mut schema = generator.subschema_for::<Option<Vec<ArchiveConfig>>>();
if let schemars::schema::Schema::Object(ref mut obj) = schema {
obj.metadata().description = Some("Archive configurations for this crate. Set to false to disable archiving, or provide an array of archive configs.".to_owned());
}
schema
}
impl Default for CrateConfig {
fn default() -> Self {
CrateConfig {
name: String::new(),
path: String::new(),
tag_template: String::new(),
version: None,
depends_on: None,
builds: None,
cross: None,
archives: ArchivesConfig::Configs(vec![]),
checksum: None,
release: None,
publish: None,
docker: None,
docker_v2: None,
docker_digest: None,
docker_manifests: None,
nfpm: None,
snapcrafts: None,
dmgs: None,
msis: None,
pkgs: None,
nsis: None,
app_bundles: None,
flatpaks: None,
blobs: None,
binstall: None,
version_sync: None,
universal_binaries: None,
no_unique_dist_dir: None,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
#[serde(default)]
pub struct UniversalBinaryConfig {
#[serde(default)]
pub id: Option<String>,
pub name_template: Option<String>,
pub replace: Option<bool>,
pub ids: Option<Vec<String>>,
pub hooks: Option<BuildHooksConfig>,
pub mod_timestamp: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
#[serde(default)]
pub struct BuildConfig {
pub id: Option<String>,
pub binary: String,
#[serde(default, deserialize_with = "deserialize_string_or_bool_opt")]
pub skip: Option<StringOrBool>,
pub targets: Option<Vec<String>>,
pub features: Option<Vec<String>>,
pub no_default_features: Option<bool>,
pub env: Option<HashMap<String, HashMap<String, String>>>,
pub copy_from: Option<String>,
pub flags: Option<String>,
pub reproducible: Option<bool>,
pub hooks: Option<BuildHooksConfig>,
pub ignore: Option<Vec<BuildIgnore>>,
pub overrides: Option<Vec<BuildOverride>>,
pub cross_tool: Option<String>,
pub mod_timestamp: Option<String>,
pub command: Option<String>,
#[serde(default, deserialize_with = "deserialize_string_or_bool_opt")]
pub no_unique_dist_dir: Option<StringOrBool>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
#[serde(default)]
pub struct BuildHooksConfig {
#[serde(alias = "before")]
pub pre: Option<Vec<HookEntry>>,
#[serde(alias = "after")]
pub post: Option<Vec<HookEntry>>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
#[serde(default)]
pub struct ArchiveHooksConfig {
#[serde(alias = "pre")]
pub before: Option<Vec<HookEntry>>,
#[serde(alias = "post")]
pub after: Option<Vec<HookEntry>>,
}
#[derive(Debug, Clone, JsonSchema)]
pub enum ArchivesConfig {
Disabled,
Configs(Vec<ArchiveConfig>),
}
impl Serialize for ArchivesConfig {
fn serialize<S: serde::Serializer>(
&self,
serializer: S,
) -> std::result::Result<S::Ok, S::Error> {
match self {
ArchivesConfig::Disabled => serializer.serialize_bool(false),
ArchivesConfig::Configs(configs) => configs.serialize(serializer),
}
}
}
impl Default for ArchivesConfig {
fn default() -> Self {
ArchivesConfig::Configs(vec![])
}
}
fn deserialize_archives_config<'de, D>(deserializer: D) -> Result<ArchivesConfig, D::Error>
where
D: Deserializer<'de>,
{
use serde::de::{self, Visitor};
struct ArchivesVisitor;
impl<'de> Visitor<'de> for ArchivesVisitor {
type Value = ArchivesConfig;
fn expecting(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("false or a list of archive configs")
}
fn visit_bool<E: de::Error>(self, v: bool) -> Result<Self::Value, E> {
if !v {
Ok(ArchivesConfig::Disabled)
} else {
Err(E::custom(
"archives: true is not valid; use false or a list",
))
}
}
fn visit_seq<A: de::SeqAccess<'de>>(self, mut seq: A) -> Result<Self::Value, A::Error> {
let mut configs = Vec::new();
while let Some(item) = seq.next_element::<ArchiveConfig>()? {
configs.push(item);
}
Ok(ArchivesConfig::Configs(configs))
}
fn visit_unit<E: de::Error>(self) -> Result<Self::Value, E> {
Ok(ArchivesConfig::Configs(vec![]))
}
fn visit_none<E: de::Error>(self) -> Result<Self::Value, E> {
Ok(ArchivesConfig::Configs(vec![]))
}
}
deserializer.deserialize_any(ArchivesVisitor)
}
fn deserialize_signs<'de, D>(deserializer: D) -> Result<Vec<SignConfig>, D::Error>
where
D: Deserializer<'de>,
{
use serde::de::{self, Visitor};
struct SignsVisitor;
impl<'de> Visitor<'de> for SignsVisitor {
type Value = Vec<SignConfig>;
fn expecting(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("a sign config object or an array of sign config objects")
}
fn visit_seq<A: de::SeqAccess<'de>>(self, mut seq: A) -> Result<Self::Value, A::Error> {
let mut configs = Vec::new();
while let Some(item) = seq.next_element::<SignConfig>()? {
configs.push(item);
}
Ok(configs)
}
fn visit_map<M: de::MapAccess<'de>>(self, map: M) -> Result<Self::Value, M::Error> {
let config = SignConfig::deserialize(de::value::MapAccessDeserializer::new(map))?;
Ok(vec![config])
}
fn visit_unit<E: de::Error>(self) -> Result<Self::Value, E> {
Ok(Vec::new())
}
fn visit_none<E: de::Error>(self) -> Result<Self::Value, E> {
Ok(Vec::new())
}
}
deserializer.deserialize_any(SignsVisitor)
}
#[derive(Debug, Clone, PartialEq, Serialize, JsonSchema)]
#[serde(untagged)]
pub enum WrapInDirectory {
Bool(bool),
Name(String),
}
impl<'de> serde::Deserialize<'de> for WrapInDirectory {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let value = serde_yaml_ng::Value::deserialize(deserializer)?;
match value {
serde_yaml_ng::Value::Bool(b) => Ok(WrapInDirectory::Bool(b)),
serde_yaml_ng::Value::String(s) => Ok(WrapInDirectory::Name(s)),
_ => Err(serde::de::Error::custom("expected bool or string")),
}
}
}
impl WrapInDirectory {
pub fn directory_name(&self, default_name: &str) -> Option<String> {
match self {
WrapInDirectory::Bool(true) => Some(default_name.to_string()),
WrapInDirectory::Bool(false) => None,
WrapInDirectory::Name(s) if s.is_empty() => None,
WrapInDirectory::Name(s) => Some(s.clone()),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
#[serde(default)]
pub struct ArchiveConfig {
pub id: Option<String>,
pub name_template: Option<String>,
pub format: Option<String>,
pub formats: Option<Vec<String>>,
pub format_overrides: Option<Vec<FormatOverride>>,
pub files: Option<Vec<ArchiveFileSpec>>,
pub binaries: Option<Vec<String>>,
pub wrap_in_directory: Option<WrapInDirectory>,
#[serde(alias = "builds")]
pub ids: Option<Vec<String>>,
pub meta: Option<bool>,
pub builds_info: Option<ArchiveFileInfo>,
pub strip_binary_directory: Option<bool>,
pub allow_different_binary_count: Option<bool>,
pub hooks: Option<ArchiveHooksConfig>,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct FormatOverride {
#[serde(alias = "goos")]
pub os: String,
pub format: Option<String>,
pub formats: Option<Vec<String>>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(untagged)]
pub enum ArchiveFileSpec {
Glob(String),
Detailed {
src: String,
dst: Option<String>,
info: Option<ArchiveFileInfo>,
strip_parent: Option<bool>,
},
}
impl PartialEq<&str> for ArchiveFileSpec {
fn eq(&self, other: &&str) -> bool {
match self {
ArchiveFileSpec::Glob(s) => s.as_str() == *other,
_ => false,
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default, JsonSchema)]
#[serde(default)]
pub struct FileInfo {
pub owner: Option<String>,
pub group: Option<String>,
pub mode: Option<String>,
pub mtime: Option<String>,
}
pub type ArchiveFileInfo = FileInfo;
pub fn parse_octal_mode(s: &str) -> Option<u32> {
let cleaned = s
.strip_prefix("0o")
.or_else(|| s.strip_prefix("0O"))
.unwrap_or(s);
let cleaned = if cleaned.is_empty() { "0" } else { cleaned };
u32::from_str_radix(cleaned, 8).ok()
}
pub const VALID_ARCHIVE_FORMATS: &[&str] = &[
"tar.gz", "tgz", "tar.xz", "txz", "tar.zst", "tzst", "tar", "zip", "gz", "binary", "none",
];
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(untagged)]
pub enum ExtraFileSpec {
Glob(String),
Detailed {
glob: String,
#[serde(alias = "name", default)]
name_template: Option<String>,
},
}
impl ExtraFileSpec {
pub fn glob(&self) -> &str {
match self {
ExtraFileSpec::Glob(s) => s,
ExtraFileSpec::Detailed { glob, .. } => glob,
}
}
pub fn name_template(&self) -> Option<&str> {
match self {
ExtraFileSpec::Glob(_) => None,
ExtraFileSpec::Detailed { name_template, .. } => name_template.as_deref(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema, PartialEq)]
#[serde(default)]
pub struct TemplatedExtraFile {
pub src: String,
pub dst: Option<String>,
pub mode: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
#[serde(default)]
pub struct ChecksumConfig {
pub name_template: Option<String>,
pub algorithm: Option<String>,
#[serde(deserialize_with = "deserialize_string_or_bool_opt", default)]
pub disable: Option<StringOrBool>,
pub extra_files: Option<Vec<ExtraFileSpec>>,
pub templated_extra_files: Option<Vec<TemplatedExtraFile>>,
pub ids: Option<Vec<String>>,
pub split: Option<bool>,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(untagged)]
pub enum ContentSource {
Inline(String),
FromFile {
from_file: String,
},
FromUrl {
from_url: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
headers: Option<HashMap<String, String>>,
},
}
impl PartialEq for ContentSource {
fn eq(&self, other: &Self) -> bool {
match (self, other) {
(Self::Inline(a), Self::Inline(b)) => a == b,
(Self::FromFile { from_file: a }, Self::FromFile { from_file: b }) => a == b,
(
Self::FromUrl {
from_url: a,
headers: ha,
},
Self::FromUrl {
from_url: b,
headers: hb,
},
) => a == b && ha == hb,
_ => false,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
#[serde(default)]
pub struct ReleaseConfig {
pub github: Option<ScmRepoConfig>,
pub gitlab: Option<ScmRepoConfig>,
pub gitea: Option<ScmRepoConfig>,
pub draft: Option<bool>,
#[schemars(schema_with = "prerelease_schema")]
pub prerelease: Option<PrereleaseConfig>,
#[schemars(schema_with = "make_latest_schema")]
pub make_latest: Option<MakeLatestConfig>,
pub name_template: Option<String>,
pub header: Option<ContentSource>,
pub footer: Option<ContentSource>,
pub extra_files: Option<Vec<ExtraFileSpec>>,
pub templated_extra_files: Option<Vec<TemplatedExtraFile>>,
#[serde(deserialize_with = "deserialize_string_or_bool_opt", default)]
pub skip_upload: Option<StringOrBool>,
pub replace_existing_draft: Option<bool>,
pub replace_existing_artifacts: Option<bool>,
#[serde(deserialize_with = "deserialize_string_or_bool_opt", default)]
pub disable: Option<StringOrBool>,
pub mode: Option<String>,
pub ids: Option<Vec<String>>,
pub target_commitish: Option<String>,
pub discussion_category_name: Option<String>,
pub include_meta: Option<bool>,
pub use_existing_draft: Option<bool>,
pub tag: Option<String>,
}
fn prerelease_schema(
_generator: &mut schemars::r#gen::SchemaGenerator,
) -> schemars::schema::Schema {
use schemars::schema::{InstanceType, Schema, SchemaObject, SingleOrVec, SubschemaValidation};
Schema::Object(SchemaObject {
subschemas: Some(Box::new(SubschemaValidation {
one_of: Some(vec![
Schema::Object(SchemaObject {
instance_type: Some(SingleOrVec::Single(Box::new(InstanceType::String))),
enum_values: Some(vec![serde_json::json!("auto")]),
..Default::default()
}),
Schema::Object(SchemaObject {
instance_type: Some(SingleOrVec::Single(Box::new(InstanceType::Boolean))),
..Default::default()
}),
]),
..Default::default()
})),
..Default::default()
})
}
fn make_latest_schema(
_generator: &mut schemars::r#gen::SchemaGenerator,
) -> schemars::schema::Schema {
use schemars::schema::{InstanceType, Schema, SchemaObject, SingleOrVec, SubschemaValidation};
Schema::Object(SchemaObject {
subschemas: Some(Box::new(SubschemaValidation {
one_of: Some(vec![
Schema::Object(SchemaObject {
instance_type: Some(SingleOrVec::Single(Box::new(InstanceType::String))),
enum_values: Some(vec![serde_json::json!("auto")]),
..Default::default()
}),
Schema::Object(SchemaObject {
instance_type: Some(SingleOrVec::Single(Box::new(InstanceType::Boolean))),
..Default::default()
}),
]),
..Default::default()
})),
..Default::default()
})
}
fn skip_push_schema(_generator: &mut schemars::r#gen::SchemaGenerator) -> schemars::schema::Schema {
use schemars::schema::{InstanceType, Schema, SchemaObject, SingleOrVec, SubschemaValidation};
Schema::Object(SchemaObject {
subschemas: Some(Box::new(SubschemaValidation {
one_of: Some(vec![
Schema::Object(SchemaObject {
instance_type: Some(SingleOrVec::Single(Box::new(InstanceType::String))),
enum_values: Some(vec![serde_json::json!("auto")]),
..Default::default()
}),
Schema::Object(SchemaObject {
instance_type: Some(SingleOrVec::Single(Box::new(InstanceType::Boolean))),
..Default::default()
}),
]),
..Default::default()
})),
..Default::default()
})
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct ScmRepoConfig {
pub owner: String,
pub name: String,
}
pub type GitHubConfig = ScmRepoConfig;
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
#[serde(rename_all = "lowercase")]
pub enum ForceTokenKind {
GitHub,
GitLab,
Gitea,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
#[serde(default, deny_unknown_fields)]
pub struct GitHubUrlsConfig {
pub api: Option<String>,
pub upload: Option<String>,
pub download: Option<String>,
pub skip_tls_verify: Option<bool>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
#[serde(default, deny_unknown_fields)]
pub struct GitLabUrlsConfig {
pub api: Option<String>,
pub download: Option<String>,
pub skip_tls_verify: Option<bool>,
pub use_package_registry: Option<bool>,
pub use_job_token: Option<bool>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
#[serde(default, deny_unknown_fields)]
pub struct GiteaUrlsConfig {
pub api: Option<String>,
pub download: Option<String>,
pub skip_tls_verify: Option<bool>,
}
macro_rules! impl_auto_or_bool_serde {
($ty:ty, $auto:path, $bool_variant:path) => {
impl Serialize for $ty {
fn serialize<S: serde::Serializer>(
&self,
serializer: S,
) -> std::result::Result<S::Ok, S::Error> {
match self {
$auto => serializer.serialize_str("auto"),
$bool_variant(b) => serializer.serialize_bool(*b),
}
}
}
impl<'de> Deserialize<'de> for $ty {
fn deserialize<D: serde::Deserializer<'de>>(
deserializer: D,
) -> std::result::Result<Self, D::Error> {
struct Visitor;
impl serde::de::Visitor<'_> for Visitor {
type Value = $ty;
fn expecting(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "\"auto\" or a boolean")
}
fn visit_bool<E: serde::de::Error>(
self,
v: bool,
) -> std::result::Result<$ty, E> {
Ok($bool_variant(v))
}
fn visit_str<E: serde::de::Error>(
self,
v: &str,
) -> std::result::Result<$ty, E> {
if v == "auto" {
Ok($auto)
} else {
Err(E::custom(format!("expected \"auto\", got \"{}\"", v)))
}
}
}
deserializer.deserialize_any(Visitor)
}
}
};
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PrereleaseConfig {
Auto,
Bool(bool),
}
impl_auto_or_bool_serde!(
PrereleaseConfig,
PrereleaseConfig::Auto,
PrereleaseConfig::Bool
);
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum MakeLatestConfig {
Auto,
Bool(bool),
String(String),
}
impl Serialize for MakeLatestConfig {
fn serialize<S: serde::Serializer>(
&self,
serializer: S,
) -> std::result::Result<S::Ok, S::Error> {
match self {
MakeLatestConfig::Auto => serializer.serialize_str("auto"),
MakeLatestConfig::Bool(b) => serializer.serialize_bool(*b),
MakeLatestConfig::String(s) => serializer.serialize_str(s),
}
}
}
impl<'de> Deserialize<'de> for MakeLatestConfig {
fn deserialize<D: serde::Deserializer<'de>>(
deserializer: D,
) -> std::result::Result<Self, D::Error> {
struct Visitor;
impl serde::de::Visitor<'_> for Visitor {
type Value = MakeLatestConfig;
fn expecting(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "\"auto\", a boolean, or a template string")
}
fn visit_bool<E: serde::de::Error>(
self,
v: bool,
) -> std::result::Result<MakeLatestConfig, E> {
Ok(MakeLatestConfig::Bool(v))
}
fn visit_str<E: serde::de::Error>(
self,
v: &str,
) -> std::result::Result<MakeLatestConfig, E> {
match v {
"auto" => Ok(MakeLatestConfig::Auto),
"true" => Ok(MakeLatestConfig::Bool(true)),
"false" => Ok(MakeLatestConfig::Bool(false)),
other => Ok(MakeLatestConfig::String(other.to_string())),
}
}
}
deserializer.deserialize_any(Visitor)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SkipPushConfig {
Auto,
Bool(bool),
Template(String),
}
impl Serialize for SkipPushConfig {
fn serialize<S: serde::Serializer>(
&self,
serializer: S,
) -> std::result::Result<S::Ok, S::Error> {
match self {
SkipPushConfig::Auto => serializer.serialize_str("auto"),
SkipPushConfig::Bool(b) => serializer.serialize_bool(*b),
SkipPushConfig::Template(s) => serializer.serialize_str(s),
}
}
}
impl<'de> Deserialize<'de> for SkipPushConfig {
fn deserialize<D: serde::Deserializer<'de>>(
deserializer: D,
) -> std::result::Result<Self, D::Error> {
struct Visitor;
impl serde::de::Visitor<'_> for Visitor {
type Value = SkipPushConfig;
fn expecting(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "\"auto\", a boolean, or a template string")
}
fn visit_bool<E: serde::de::Error>(
self,
v: bool,
) -> std::result::Result<SkipPushConfig, E> {
Ok(SkipPushConfig::Bool(v))
}
fn visit_str<E: serde::de::Error>(
self,
v: &str,
) -> std::result::Result<SkipPushConfig, E> {
match v {
"auto" => Ok(SkipPushConfig::Auto),
"true" => Ok(SkipPushConfig::Bool(true)),
"false" => Ok(SkipPushConfig::Bool(false)),
other => Ok(SkipPushConfig::Template(other.to_string())),
}
}
}
deserializer.deserialize_any(Visitor)
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
#[serde(default)]
pub struct RepositoryConfig {
pub owner: Option<String>,
pub name: Option<String>,
pub token: Option<String>,
pub token_type: Option<String>,
pub branch: Option<String>,
pub git: Option<GitRepoConfig>,
pub pull_request: Option<PullRequestConfig>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
#[serde(default)]
pub struct GitRepoConfig {
pub url: Option<String>,
pub ssh_command: Option<String>,
pub private_key: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
#[serde(default)]
pub struct PullRequestConfig {
pub enabled: Option<bool>,
pub draft: Option<bool>,
pub body: Option<String>,
pub base: Option<PullRequestBaseConfig>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
#[serde(default)]
pub struct PullRequestBaseConfig {
pub owner: Option<String>,
pub name: Option<String>,
pub branch: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
#[serde(default)]
pub struct CommitAuthorConfig {
pub name: Option<String>,
pub email: Option<String>,
pub signing: Option<CommitSigningConfig>,
}
impl CommitAuthorConfig {
pub fn normalize_defaults(&mut self) {
if self.name.as_deref().is_none_or(str::is_empty) {
self.name = Some("anodizer".to_string());
}
if self.email.as_deref().is_none_or(str::is_empty) {
self.email = Some("bot@anodizer.dev".to_string());
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
#[serde(default)]
pub struct CommitSigningConfig {
pub enabled: Option<bool>,
pub key: Option<String>,
pub program: Option<String>,
pub format: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
#[serde(default)]
pub struct PublishConfig {
#[schemars(schema_with = "crates_publish_schema")]
pub crates: Option<CratesPublishConfig>,
pub homebrew: Option<HomebrewConfig>,
pub scoop: Option<ScoopConfig>,
pub chocolatey: Option<ChocolateyConfig>,
pub winget: Option<WingetConfig>,
pub aur: Option<AurConfig>,
pub aur_source: Option<AurSourceConfig>,
pub krew: Option<KrewConfig>,
pub nix: Option<NixConfig>,
}
fn crates_publish_schema(
_generator: &mut schemars::r#gen::SchemaGenerator,
) -> schemars::schema::Schema {
schemars::schema::Schema::Bool(true)
}
impl PublishConfig {
pub fn crates_config(&self) -> CratesPublishSettings {
match &self.crates {
None => CratesPublishSettings::default(),
Some(CratesPublishConfig::Bool(enabled)) => CratesPublishSettings {
enabled: *enabled,
index_timeout: 300,
},
Some(CratesPublishConfig::Object {
enabled,
index_timeout,
}) => CratesPublishSettings {
enabled: *enabled,
index_timeout: *index_timeout,
},
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(untagged)]
pub enum CratesPublishConfig {
Bool(bool),
Object {
enabled: bool,
#[serde(default = "default_index_timeout")]
index_timeout: u64,
},
}
fn default_index_timeout() -> u64 {
300
}
#[derive(Debug, Clone)]
pub struct CratesPublishSettings {
pub enabled: bool,
pub index_timeout: u64,
}
impl Default for CratesPublishSettings {
fn default() -> Self {
CratesPublishSettings {
enabled: false,
index_timeout: 300,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
#[serde(default)]
pub struct HomebrewConfig {
pub tap: Option<TapConfig>,
pub repository: Option<RepositoryConfig>,
pub commit_author: Option<CommitAuthorConfig>,
pub directory: Option<String>,
pub name: Option<String>,
pub description: Option<String>,
pub license: Option<String>,
pub install: Option<String>,
pub extra_install: Option<String>,
pub post_install: Option<String>,
pub test: Option<String>,
pub homepage: Option<String>,
pub dependencies: Option<Vec<HomebrewDependency>>,
pub conflicts: Option<Vec<HomebrewConflict>>,
pub caveats: Option<String>,
#[serde(deserialize_with = "deserialize_string_or_bool_opt", default)]
pub skip_upload: Option<StringOrBool>,
pub commit_msg_template: Option<String>,
pub commit_author_name: Option<String>,
pub commit_author_email: Option<String>,
pub ids: Option<Vec<String>>,
pub url_template: Option<String>,
pub url_headers: Option<Vec<String>>,
pub download_strategy: Option<String>,
pub custom_require: Option<String>,
pub custom_block: Option<String>,
pub plist: Option<String>,
pub service: Option<String>,
pub cask: Option<HomebrewCaskConfig>,
#[serde(alias = "goamd64")]
pub amd64_variant: Option<String>,
#[serde(alias = "goarm")]
pub arm_variant: Option<String>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, JsonSchema)]
#[serde(default)]
pub struct HomebrewDependency {
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub os: Option<String>,
#[serde(rename = "type", skip_serializing_if = "Option::is_none")]
pub dep_type: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub version: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, JsonSchema)]
#[serde(untagged)]
pub enum HomebrewConflict {
Name(String),
WithReason {
name: String,
#[serde(skip_serializing_if = "Option::is_none")]
because: Option<String>,
},
}
impl HomebrewConflict {
pub fn name(&self) -> &str {
match self {
Self::Name(n) => n,
Self::WithReason { name, .. } => name,
}
}
pub fn because(&self) -> Option<&str> {
match self {
Self::Name(_) => None,
Self::WithReason { because, .. } => because.as_deref(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
#[serde(default)]
pub struct HomebrewCaskConfig {
pub name: Option<String>,
pub alternative_names: Option<Vec<String>>,
pub app: Option<String>,
pub binaries: Option<Vec<String>>,
pub description: Option<String>,
pub homepage: Option<String>,
pub url_template: Option<String>,
pub caveats: Option<String>,
pub zap: Option<Vec<String>>,
pub uninstall: Option<Vec<String>>,
pub custom_block: Option<String>,
pub service: Option<String>,
pub license: Option<String>,
pub manpages: Option<Vec<String>>,
pub completions: Option<HomebrewCaskCompletions>,
pub dependencies: Option<Vec<HomebrewCaskDependencyEntry>>,
pub conflicts: Option<Vec<HomebrewCaskConflictEntry>>,
pub hooks: Option<HomebrewCaskHooks>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
#[serde(default)]
pub struct TopLevelHomebrewCaskConfig {
pub name: Option<String>,
pub repository: Option<RepositoryConfig>,
pub commit_author: Option<CommitAuthorConfig>,
pub commit_msg_template: Option<String>,
pub directory: Option<String>,
pub description: Option<String>,
pub homepage: Option<String>,
#[serde(deserialize_with = "deserialize_string_or_bool_opt", default)]
pub skip_upload: Option<StringOrBool>,
pub custom_block: Option<String>,
pub ids: Option<Vec<String>>,
pub service: Option<String>,
pub binaries: Option<Vec<String>>,
pub manpages: Option<Vec<String>>,
pub caveats: Option<String>,
pub license: Option<String>,
pub url: Option<HomebrewCaskURL>,
pub completions: Option<HomebrewCaskCompletions>,
pub dependencies: Option<Vec<HomebrewCaskDependencyEntry>>,
pub conflicts: Option<Vec<HomebrewCaskConflictEntry>>,
pub hooks: Option<HomebrewCaskHooks>,
pub uninstall: Option<HomebrewCaskUninstall>,
pub zap: Option<HomebrewCaskUninstall>,
pub generate_completions_from_executable: Option<HomebrewCaskGeneratedCompletions>,
pub app: Option<String>,
pub alternative_names: Option<Vec<String>>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
#[serde(default)]
pub struct HomebrewCaskURL {
pub template: Option<String>,
pub verified: Option<String>,
pub using: Option<String>,
pub cookies: Option<HashMap<String, String>>,
pub referer: Option<String>,
pub headers: Option<Vec<String>>,
pub user_agent: Option<String>,
pub data: Option<HashMap<String, String>>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
#[serde(default)]
pub struct HomebrewCaskUninstall {
pub launchctl: Option<Vec<String>>,
pub quit: Option<Vec<String>>,
pub login_item: Option<Vec<String>>,
pub delete: Option<Vec<String>>,
pub trash: Option<Vec<String>>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
#[serde(default)]
pub struct HomebrewCaskHooks {
pub pre: Option<HomebrewCaskHook>,
pub post: Option<HomebrewCaskHook>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
#[serde(default)]
pub struct HomebrewCaskHook {
pub install: Option<String>,
pub uninstall: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
#[serde(default)]
pub struct HomebrewCaskCompletions {
pub bash: Option<String>,
pub zsh: Option<String>,
pub fish: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
#[serde(default)]
pub struct HomebrewCaskDependencyEntry {
pub cask: Option<String>,
pub formula: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
#[serde(default)]
pub struct HomebrewCaskConflictEntry {
pub cask: Option<String>,
pub formula: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
#[serde(default)]
pub struct HomebrewCaskGeneratedCompletions {
pub executable: Option<String>,
pub args: Option<Vec<String>>,
pub base_name: Option<String>,
pub shell_parameter_format: Option<String>,
pub shells: Option<Vec<String>>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
#[serde(default)]
pub struct ScoopConfig {
pub bucket: Option<BucketConfig>,
pub repository: Option<RepositoryConfig>,
pub commit_author: Option<CommitAuthorConfig>,
pub name: Option<String>,
pub directory: Option<String>,
pub description: Option<String>,
pub license: Option<String>,
pub homepage: Option<String>,
pub persist: Option<Vec<String>>,
pub depends: Option<Vec<String>>,
pub pre_install: Option<Vec<String>>,
pub post_install: Option<Vec<String>>,
pub shortcuts: Option<Vec<Vec<String>>>,
#[serde(deserialize_with = "deserialize_string_or_bool_opt", default)]
pub skip_upload: Option<StringOrBool>,
pub commit_msg_template: Option<String>,
pub commit_author_name: Option<String>,
pub commit_author_email: Option<String>,
pub ids: Option<Vec<String>>,
pub url_template: Option<String>,
#[serde(rename = "use")]
pub use_artifact: Option<String>,
#[serde(alias = "goamd64")]
pub amd64_variant: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct TapConfig {
pub owner: String,
pub name: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct BucketConfig {
pub owner: String,
pub name: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
#[serde(default)]
pub struct ChocolateyConfig {
pub name: Option<String>,
pub ids: Option<Vec<String>>,
pub project_repo: Option<ChocolateyRepoConfig>,
pub package_source_url: Option<String>,
pub owners: Option<String>,
pub title: Option<String>,
pub authors: Option<String>,
pub project_url: Option<String>,
pub url_template: Option<String>,
pub icon_url: Option<String>,
pub copyright: Option<String>,
pub description: Option<String>,
pub license: Option<String>,
pub license_url: Option<String>,
pub require_license_acceptance: Option<bool>,
pub project_source_url: Option<String>,
pub docs_url: Option<String>,
pub bug_tracker_url: Option<String>,
#[serde(
deserialize_with = "deserialize_space_separated_string_or_vec_opt",
default
)]
pub tags: Option<Vec<String>>,
pub summary: Option<String>,
pub release_notes: Option<String>,
pub dependencies: Option<Vec<ChocolateyDependency>>,
pub api_key: Option<String>,
pub source_repo: Option<String>,
pub skip_publish: Option<bool>,
#[serde(deserialize_with = "deserialize_string_or_bool_opt", default)]
pub disable: Option<StringOrBool>,
#[serde(rename = "use")]
pub use_artifact: Option<String>,
#[serde(alias = "goamd64")]
pub amd64_variant: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
#[serde(default)]
pub struct ChocolateyDependency {
pub id: String,
pub version: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct ChocolateyRepoConfig {
pub owner: String,
pub name: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
#[serde(default)]
pub struct WingetConfig {
pub name: Option<String>,
pub package_name: Option<String>,
pub package_identifier: Option<String>,
pub publisher: Option<String>,
pub publisher_url: Option<String>,
pub publisher_support_url: Option<String>,
pub privacy_url: Option<String>,
pub author: Option<String>,
pub copyright: Option<String>,
pub copyright_url: Option<String>,
pub license: Option<String>,
pub license_url: Option<String>,
pub short_description: Option<String>,
pub description: Option<String>,
pub homepage: Option<String>,
pub url_template: Option<String>,
pub ids: Option<Vec<String>>,
#[serde(deserialize_with = "deserialize_string_or_bool_opt", default)]
pub skip_upload: Option<StringOrBool>,
pub commit_msg_template: Option<String>,
pub path: Option<String>,
pub release_notes: Option<String>,
pub release_notes_url: Option<String>,
pub installation_notes: Option<String>,
pub tags: Option<Vec<String>>,
pub dependencies: Option<Vec<WingetDependency>>,
pub manifests_repo: Option<WingetManifestsRepoConfig>,
pub repository: Option<RepositoryConfig>,
pub commit_author: Option<CommitAuthorConfig>,
pub product_code: Option<String>,
#[serde(rename = "use")]
pub use_artifact: Option<String>,
#[serde(alias = "goamd64")]
pub amd64_variant: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
#[serde(default)]
pub struct WingetDependency {
pub package_identifier: String,
pub minimum_version: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct WingetManifestsRepoConfig {
pub owner: String,
pub name: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
#[serde(default)]
pub struct AurConfig {
#[serde(alias = "package_name")]
pub name: Option<String>,
pub ids: Option<Vec<String>>,
pub commit_author: Option<CommitAuthorConfig>,
pub commit_msg_template: Option<String>,
pub description: Option<String>,
pub homepage: Option<String>,
pub license: Option<String>,
#[serde(deserialize_with = "deserialize_string_or_bool_opt", default)]
pub skip_upload: Option<StringOrBool>,
pub url_template: Option<String>,
pub maintainers: Option<Vec<String>>,
pub contributors: Option<Vec<String>>,
pub provides: Option<Vec<String>>,
pub conflicts: Option<Vec<String>>,
pub depends: Option<Vec<String>>,
pub optdepends: Option<Vec<String>>,
pub backup: Option<Vec<String>>,
pub rel: Option<String>,
#[serde(alias = "install_template")]
pub package: Option<String>,
pub git_url: Option<String>,
pub git_ssh_command: Option<String>,
pub private_key: Option<String>,
pub directory: Option<String>,
#[serde(deserialize_with = "deserialize_string_or_bool_opt", default)]
pub disable: Option<StringOrBool>,
pub install: Option<String>,
pub url: Option<String>,
pub replaces: Option<Vec<String>>,
#[serde(alias = "goamd64")]
pub amd64_variant: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
#[serde(default)]
pub struct KrewConfig {
pub name: Option<String>,
pub ids: Option<Vec<String>>,
pub manifests_repo: Option<KrewManifestsRepoConfig>,
pub repository: Option<RepositoryConfig>,
pub commit_author: Option<CommitAuthorConfig>,
pub commit_msg_template: Option<String>,
pub description: Option<String>,
pub short_description: Option<String>,
pub homepage: Option<String>,
pub url_template: Option<String>,
pub caveats: Option<String>,
#[serde(deserialize_with = "deserialize_string_or_bool_opt", default)]
pub skip_upload: Option<StringOrBool>,
#[serde(deserialize_with = "deserialize_string_or_bool_opt", default)]
pub disable: Option<StringOrBool>,
pub upstream_repo: Option<KrewManifestsRepoConfig>,
#[serde(alias = "goamd64")]
pub amd64_variant: Option<String>,
#[serde(alias = "goarm")]
pub arm_variant: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct KrewManifestsRepoConfig {
pub owner: String,
pub name: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
#[serde(default)]
pub struct NixConfig {
pub name: Option<String>,
pub path: Option<String>,
pub repository: Option<RepositoryConfig>,
pub commit_author: Option<CommitAuthorConfig>,
pub commit_msg_template: Option<String>,
pub ids: Option<Vec<String>>,
pub url_template: Option<String>,
#[serde(deserialize_with = "deserialize_string_or_bool_opt", default)]
pub skip_upload: Option<StringOrBool>,
pub install: Option<String>,
pub extra_install: Option<String>,
pub post_install: Option<String>,
pub description: Option<String>,
pub homepage: Option<String>,
pub license: Option<String>,
pub dependencies: Option<Vec<NixDependency>>,
pub formatter: Option<String>,
#[serde(alias = "goamd64")]
pub amd64_variant: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
#[serde(default)]
pub struct NixDependency {
pub name: String,
pub os: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
#[serde(default)]
pub struct DockerConfig {
pub id: Option<String>,
pub image_templates: Vec<String>,
pub dockerfile: String,
pub platforms: Option<Vec<String>>,
pub binaries: Option<Vec<String>>,
pub build_flag_templates: Option<Vec<String>>,
#[schemars(schema_with = "skip_push_schema")]
pub skip_push: Option<SkipPushConfig>,
pub extra_files: Option<Vec<String>>,
pub templated_extra_files: Option<Vec<TemplatedExtraFile>>,
pub push_flags: Option<Vec<String>>,
pub ids: Option<Vec<String>>,
pub labels: Option<HashMap<String, String>>,
pub retry: Option<DockerRetryConfig>,
#[serde(rename = "use")]
pub use_backend: Option<String>,
#[serde(deserialize_with = "deserialize_string_or_bool_opt", default)]
pub disable: Option<StringOrBool>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
#[serde(default)]
pub struct DockerRetryConfig {
pub attempts: Option<u32>,
pub delay: Option<String>,
pub max_delay: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
#[serde(default)]
pub struct DockerV2Config {
pub id: Option<String>,
pub ids: Option<Vec<String>>,
pub dockerfile: String,
pub images: Vec<String>,
pub tags: Vec<String>,
pub labels: Option<HashMap<String, String>>,
pub annotations: Option<HashMap<String, String>>,
pub extra_files: Option<Vec<String>>,
pub platforms: Option<Vec<String>>,
pub build_args: Option<HashMap<String, String>>,
pub retry: Option<DockerRetryConfig>,
pub flags: Option<Vec<String>>,
#[serde(deserialize_with = "deserialize_string_or_bool_opt", default)]
pub disable: Option<StringOrBool>,
#[serde(deserialize_with = "deserialize_string_or_bool_opt", default)]
pub sbom: Option<StringOrBool>,
#[serde(deserialize_with = "deserialize_string_or_bool_opt", default)]
pub skip_push: Option<StringOrBool>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
#[serde(default)]
pub struct DockerDigestConfig {
#[serde(deserialize_with = "deserialize_string_or_bool_opt", default)]
pub disable: Option<StringOrBool>,
pub name_template: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
#[serde(default)]
pub struct DockerManifestConfig {
pub name_template: String,
pub image_templates: Vec<String>,
pub create_flags: Option<Vec<String>>,
pub push_flags: Option<Vec<String>>,
#[schemars(schema_with = "skip_push_schema")]
pub skip_push: Option<SkipPushConfig>,
pub id: Option<String>,
#[serde(rename = "use")]
pub use_backend: Option<String>,
pub retry: Option<DockerRetryConfig>,
#[serde(deserialize_with = "deserialize_string_or_bool_opt", default)]
pub disable: Option<StringOrBool>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
#[serde(default)]
pub struct NfpmConfig {
pub id: Option<String>,
pub package_name: Option<String>,
pub formats: Vec<String>,
pub vendor: Option<String>,
pub homepage: Option<String>,
pub maintainer: Option<String>,
pub description: Option<String>,
pub license: Option<String>,
pub bindir: Option<String>,
pub contents: Option<Vec<NfpmContent>>,
pub dependencies: Option<HashMap<String, Vec<String>>>,
pub overrides: Option<HashMap<String, serde_json::Value>>,
pub file_name_template: Option<String>,
pub scripts: Option<NfpmScripts>,
pub recommends: Option<Vec<String>>,
pub suggests: Option<Vec<String>>,
pub conflicts: Option<Vec<String>>,
pub replaces: Option<Vec<String>>,
pub provides: Option<Vec<String>>,
#[serde(alias = "builds")]
pub ids: Option<Vec<String>>,
pub epoch: Option<String>,
pub release: Option<String>,
pub prerelease: Option<String>,
pub version_metadata: Option<String>,
pub section: Option<String>,
pub priority: Option<String>,
pub meta: Option<bool>,
pub umask: Option<String>,
pub mtime: Option<String>,
pub rpm: Option<NfpmRpmConfig>,
pub deb: Option<NfpmDebConfig>,
pub apk: Option<NfpmApkConfig>,
pub archlinux: Option<NfpmArchlinuxConfig>,
pub ipk: Option<NfpmIpkConfig>,
pub libdirs: Option<NfpmLibdirs>,
pub changelog: Option<String>,
#[serde(rename = "if")]
pub if_condition: Option<String>,
pub templated_contents: Option<Vec<NfpmContent>>,
pub templated_scripts: Option<NfpmScripts>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
#[serde(default)]
pub struct NfpmLibdirs {
pub header: Option<String>,
pub carchive: Option<String>,
pub cshared: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
#[serde(default)]
pub struct NfpmScripts {
pub preinstall: Option<String>,
pub postinstall: Option<String>,
pub preremove: Option<String>,
pub postremove: Option<String>,
}
pub type NfpmFileInfo = FileInfo;
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct NfpmContent {
pub src: String,
pub dst: String,
#[serde(rename = "type")]
pub content_type: Option<String>,
pub file_info: Option<NfpmFileInfo>,
pub packager: Option<String>,
pub expand: Option<bool>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
#[serde(default)]
pub struct NfpmRpmConfig {
pub summary: Option<String>,
pub compression: Option<String>,
pub group: Option<String>,
pub packager: Option<String>,
pub prefixes: Option<Vec<String>>,
pub signature: Option<NfpmSignatureConfig>,
pub scripts: Option<NfpmRpmScripts>,
pub build_host: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
#[serde(default)]
pub struct NfpmRpmScripts {
pub pretrans: Option<String>,
pub posttrans: Option<String>,
}
impl NfpmRpmConfig {
pub fn is_empty(&self) -> bool {
self.summary.is_none()
&& self.compression.is_none()
&& self.group.is_none()
&& self.packager.is_none()
&& self.prefixes.is_none()
&& self.signature.is_none()
&& self.scripts.is_none()
&& self.build_host.is_none()
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
#[serde(default)]
pub struct NfpmDebConfig {
pub compression: Option<String>,
pub predepends: Option<Vec<String>>,
pub triggers: Option<NfpmDebTriggers>,
pub breaks: Option<Vec<String>>,
pub lintian_overrides: Option<Vec<String>>,
pub signature: Option<NfpmSignatureConfig>,
pub fields: Option<HashMap<String, String>>,
pub scripts: Option<NfpmDebScripts>,
pub arch_variant: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
#[serde(default)]
pub struct NfpmDebScripts {
pub rules: Option<String>,
pub templates: Option<String>,
pub config: Option<String>,
}
impl NfpmDebConfig {
pub fn is_empty(&self) -> bool {
self.compression.is_none()
&& self.predepends.is_none()
&& self.triggers.is_none()
&& self.breaks.is_none()
&& self.lintian_overrides.is_none()
&& self.signature.is_none()
&& self.fields.is_none()
&& self.scripts.is_none()
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
#[serde(default)]
pub struct NfpmDebTriggers {
pub interest: Option<Vec<String>>,
pub interest_await: Option<Vec<String>>,
pub interest_noawait: Option<Vec<String>>,
pub activate: Option<Vec<String>>,
pub activate_await: Option<Vec<String>>,
pub activate_noawait: Option<Vec<String>>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
#[serde(default)]
pub struct NfpmApkConfig {
pub signature: Option<NfpmSignatureConfig>,
pub scripts: Option<NfpmApkScripts>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
#[serde(default)]
pub struct NfpmApkScripts {
pub preupgrade: Option<String>,
pub postupgrade: Option<String>,
}
impl NfpmApkConfig {
pub fn is_empty(&self) -> bool {
self.signature.is_none() && self.scripts.is_none()
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
#[serde(default)]
pub struct NfpmArchlinuxConfig {
pub pkgbase: Option<String>,
pub packager: Option<String>,
pub scripts: Option<NfpmArchlinuxScripts>,
}
impl NfpmArchlinuxConfig {
pub fn is_empty(&self) -> bool {
self.pkgbase.is_none() && self.packager.is_none() && self.scripts.is_none()
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
#[serde(default)]
pub struct NfpmArchlinuxScripts {
pub preupgrade: Option<String>,
pub postupgrade: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
#[serde(default)]
pub struct NfpmIpkConfig {
pub abi_version: Option<String>,
pub alternatives: Option<Vec<NfpmIpkAlternative>>,
pub auto_installed: Option<bool>,
pub essential: Option<bool>,
pub predepends: Option<Vec<String>>,
pub tags: Option<Vec<String>>,
pub fields: Option<HashMap<String, String>>,
}
impl NfpmIpkConfig {
pub fn is_empty(&self) -> bool {
self.abi_version.is_none()
&& self.alternatives.is_none()
&& self.auto_installed.is_none()
&& self.essential.is_none()
&& self.predepends.is_none()
&& self.tags.is_none()
&& self.fields.is_none()
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
#[serde(default)]
pub struct NfpmIpkAlternative {
pub priority: Option<i32>,
pub target: Option<String>,
pub link_name: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
#[serde(default)]
pub struct NfpmSignatureConfig {
pub key_file: Option<String>,
pub key_id: Option<String>,
pub key_passphrase: Option<String>,
pub key_name: Option<String>,
#[serde(rename = "type")]
pub type_: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
#[serde(default)]
pub struct SnapcraftConfig {
pub id: Option<String>,
#[serde(alias = "builds")]
pub ids: Option<Vec<String>>,
pub name: Option<String>,
pub title: Option<String>,
pub summary: Option<String>,
pub description: Option<String>,
pub icon: Option<String>,
pub base: Option<String>,
pub grade: Option<String>,
pub license: Option<String>,
pub publish: Option<bool>,
pub channel_templates: Option<Vec<String>>,
pub confinement: Option<String>,
pub plugs: Option<HashMap<String, serde_json::Value>>,
pub slots: Option<Vec<String>>,
pub assumes: Option<Vec<String>>,
pub apps: Option<HashMap<String, SnapcraftApp>>,
pub layouts: Option<HashMap<String, SnapcraftLayout>>,
pub extra_files: Option<Vec<SnapcraftExtraFileSpec>>,
pub templated_extra_files: Option<Vec<TemplatedExtraFile>>,
pub name_template: Option<String>,
#[serde(deserialize_with = "deserialize_string_or_bool_opt", default)]
pub disable: Option<StringOrBool>,
pub replace: Option<bool>,
pub mod_timestamp: Option<String>,
pub hooks: Option<HashMap<String, serde_json::Value>>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
#[serde(default)]
pub struct SnapcraftApp {
pub command: Option<String>,
pub daemon: Option<String>,
#[serde(alias = "stop-mode")]
pub stop_mode: Option<String>,
pub plugs: Option<Vec<String>>,
pub environment: Option<HashMap<String, serde_json::Value>>,
pub args: Option<String>,
#[serde(alias = "restart-condition")]
pub restart_condition: Option<String>,
pub adapter: Option<String>,
pub after: Option<Vec<String>>,
pub aliases: Option<Vec<String>>,
pub autostart: Option<String>,
pub before: Option<Vec<String>>,
#[serde(alias = "bus-name")]
pub bus_name: Option<String>,
#[serde(alias = "command-chain")]
pub command_chain: Option<Vec<String>>,
#[serde(alias = "common-id")]
pub common_id: Option<String>,
pub completer: Option<String>,
pub desktop: Option<String>,
pub extensions: Option<Vec<String>>,
#[serde(alias = "install-mode")]
pub install_mode: Option<String>,
pub passthrough: Option<HashMap<String, serde_json::Value>>,
#[serde(alias = "post-stop-command")]
pub post_stop_command: Option<String>,
#[serde(alias = "refresh-mode")]
pub refresh_mode: Option<String>,
#[serde(alias = "reload-command")]
pub reload_command: Option<String>,
#[serde(alias = "restart-delay")]
pub restart_delay: Option<String>,
pub slots: Option<Vec<String>>,
pub sockets: Option<HashMap<String, serde_json::Value>>,
#[serde(alias = "start-timeout")]
pub start_timeout: Option<String>,
#[serde(alias = "stop-command")]
pub stop_command: Option<String>,
#[serde(alias = "stop-timeout")]
pub stop_timeout: Option<String>,
pub timer: Option<String>,
#[serde(alias = "watchdog-timeout")]
pub watchdog_timeout: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
#[serde(default)]
pub struct SnapcraftLayout {
pub bind: Option<String>,
pub bind_file: Option<String>,
pub symlink: Option<String>,
#[serde(rename = "type")]
pub type_: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(untagged)]
pub enum SnapcraftExtraFileSpec {
Source(String),
Detailed {
source: String,
#[serde(skip_serializing_if = "Option::is_none")]
destination: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
mode: Option<u32>,
},
}
impl SnapcraftExtraFileSpec {
pub fn source(&self) -> &str {
match self {
SnapcraftExtraFileSpec::Source(s) => s,
SnapcraftExtraFileSpec::Detailed { source, .. } => source,
}
}
pub fn destination(&self) -> Option<&str> {
match self {
SnapcraftExtraFileSpec::Source(_) => None,
SnapcraftExtraFileSpec::Detailed { destination, .. } => destination.as_deref(),
}
}
pub fn mode(&self) -> Option<u32> {
match self {
SnapcraftExtraFileSpec::Source(_) => None,
SnapcraftExtraFileSpec::Detailed { mode, .. } => *mode,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
#[serde(default)]
pub struct DmgConfig {
pub id: Option<String>,
pub ids: Option<Vec<String>>,
pub name: Option<String>,
pub extra_files: Option<Vec<ExtraFileSpec>>,
pub templated_extra_files: Option<Vec<TemplatedExtraFile>>,
pub replace: Option<bool>,
pub mod_timestamp: Option<String>,
#[serde(deserialize_with = "deserialize_string_or_bool_opt", default)]
pub disable: Option<StringOrBool>,
#[serde(rename = "use")]
pub use_: Option<String>,
#[serde(rename = "if")]
pub if_condition: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
#[serde(default)]
pub struct MsiConfig {
pub id: Option<String>,
pub ids: Option<Vec<String>>,
pub wxs: Option<String>,
pub name: Option<String>,
pub version: Option<String>,
pub replace: Option<bool>,
pub mod_timestamp: Option<String>,
#[serde(deserialize_with = "deserialize_string_or_bool_opt", default)]
pub disable: Option<StringOrBool>,
pub extra_files: Option<Vec<String>>,
pub extensions: Option<Vec<String>>,
#[serde(rename = "if")]
pub if_condition: Option<String>,
pub hooks: Option<BuildHooksConfig>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
#[serde(default)]
pub struct PkgConfig {
pub id: Option<String>,
pub ids: Option<Vec<String>>,
pub identifier: Option<String>,
pub name: Option<String>,
pub install_location: Option<String>,
pub scripts: Option<String>,
pub extra_files: Option<Vec<ExtraFileSpec>>,
pub replace: Option<bool>,
pub mod_timestamp: Option<String>,
#[serde(rename = "use")]
pub use_: Option<String>,
pub min_os_version: Option<String>,
#[serde(deserialize_with = "deserialize_string_or_bool_opt", default)]
pub disable: Option<StringOrBool>,
#[serde(rename = "if")]
pub if_condition: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
#[serde(default)]
pub struct NsisConfig {
pub id: Option<String>,
pub ids: Option<Vec<String>>,
pub name: Option<String>,
pub script: Option<String>,
pub extra_files: Option<Vec<ExtraFileSpec>>,
pub templated_extra_files: Option<Vec<TemplatedExtraFile>>,
#[serde(deserialize_with = "deserialize_string_or_bool_opt", default)]
pub disable: Option<StringOrBool>,
pub replace: Option<bool>,
pub mod_timestamp: Option<String>,
#[serde(rename = "if")]
pub if_condition: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
#[serde(default)]
pub struct AppBundleConfig {
pub id: Option<String>,
pub ids: Option<Vec<String>>,
pub name: Option<String>,
pub icon: Option<String>,
pub bundle: Option<String>,
pub extra_files: Option<Vec<ArchiveFileSpec>>,
pub templated_extra_files: Option<Vec<TemplatedExtraFile>>,
pub mod_timestamp: Option<String>,
pub replace: Option<bool>,
#[serde(deserialize_with = "deserialize_string_or_bool_opt", default)]
pub disable: Option<StringOrBool>,
#[serde(rename = "if")]
pub if_condition: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
#[serde(default)]
pub struct FlatpakConfig {
pub id: Option<String>,
pub ids: Option<Vec<String>>,
pub name_template: Option<String>,
pub app_id: Option<String>,
pub runtime: Option<String>,
pub runtime_version: Option<String>,
pub sdk: Option<String>,
pub command: Option<String>,
pub finish_args: Option<Vec<String>>,
pub extra_files: Option<Vec<ExtraFileSpec>>,
pub replace: Option<bool>,
pub mod_timestamp: Option<String>,
#[serde(deserialize_with = "deserialize_string_or_bool_opt", default)]
pub disable: Option<StringOrBool>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
#[serde(default)]
pub struct BlobConfig {
pub id: Option<String>,
pub provider: String,
pub bucket: String,
pub directory: Option<String>,
pub region: Option<String>,
pub endpoint: Option<String>,
pub disable_ssl: Option<bool>,
pub s3_force_path_style: Option<bool>,
pub acl: Option<String>,
#[serde(deserialize_with = "deserialize_string_or_vec_opt", default)]
pub cache_control: Option<Vec<String>>,
pub content_disposition: Option<String>,
pub kms_key: Option<String>,
pub ids: Option<Vec<String>>,
#[serde(deserialize_with = "deserialize_string_or_bool_opt", default)]
pub disable: Option<StringOrBool>,
pub include_meta: Option<bool>,
pub extra_files: Option<Vec<ExtraFileSpec>>,
pub templated_extra_files: Option<Vec<TemplatedExtraFile>>,
pub extra_files_only: Option<bool>,
pub parallelism: Option<usize>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
#[serde(default)]
pub struct PartialConfig {
pub by: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
#[serde(default)]
pub struct BinstallConfig {
pub enabled: Option<bool>,
pub pkg_url: Option<String>,
pub bin_dir: Option<String>,
pub pkg_fmt: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
#[serde(default)]
pub struct NotarizeConfig {
#[serde(deserialize_with = "deserialize_string_or_bool_opt", default)]
pub disable: Option<StringOrBool>,
pub macos: Option<Vec<MacOSSignNotarizeConfig>>,
pub macos_native: Option<Vec<MacOSNativeSignNotarizeConfig>>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
#[serde(default)]
pub struct MacOSSignNotarizeConfig {
pub ids: Option<Vec<String>>,
#[serde(deserialize_with = "deserialize_string_or_bool_opt", default)]
pub enabled: Option<StringOrBool>,
pub sign: Option<MacOSSignConfig>,
pub notarize: Option<MacOSNotarizeApiConfig>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
#[serde(default)]
pub struct MacOSSignConfig {
pub certificate: Option<String>,
pub password: Option<String>,
pub entitlements: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
#[serde(default)]
pub struct MacOSNotarizeApiConfig {
pub issuer_id: Option<String>,
pub key: Option<String>,
pub key_id: Option<String>,
pub timeout: Option<String>,
pub wait: Option<bool>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
#[serde(default)]
pub struct MacOSNativeSignNotarizeConfig {
pub ids: Option<Vec<String>>,
#[serde(deserialize_with = "deserialize_string_or_bool_opt", default)]
pub enabled: Option<StringOrBool>,
#[serde(rename = "use")]
pub use_: Option<String>,
pub sign: Option<MacOSNativeSignConfig>,
pub notarize: Option<MacOSNativeNotarizeConfig>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
#[serde(default)]
pub struct MacOSNativeSignConfig {
pub identity: Option<String>,
pub keychain: Option<String>,
pub options: Option<Vec<String>>,
pub entitlements: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
#[serde(default)]
pub struct MacOSNativeNotarizeConfig {
pub profile_name: Option<String>,
pub wait: Option<bool>,
pub timeout: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
#[serde(default)]
pub struct SourceFileEntry {
pub src: String,
pub dst: Option<String>,
pub strip_parent: Option<bool>,
pub info: Option<SourceFileInfo>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
#[serde(default)]
pub struct SourceFileInfo {
pub owner: Option<String>,
pub group: Option<String>,
pub mode: Option<u32>,
pub mtime: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
#[serde(default)]
pub struct SourceConfig {
pub enabled: Option<bool>,
pub format: Option<String>,
pub name_template: Option<String>,
pub prefix_template: Option<String>,
#[serde(default, deserialize_with = "deserialize_source_files")]
#[schemars(schema_with = "source_files_schema")]
pub files: Vec<SourceFileEntry>,
}
impl SourceConfig {
pub fn is_enabled(&self) -> bool {
self.enabled.unwrap_or(false)
}
pub fn archive_format(&self) -> &str {
self.format.as_deref().unwrap_or("tar.gz")
}
}
fn source_files_schema(
generator: &mut schemars::r#gen::SchemaGenerator,
) -> schemars::schema::Schema {
let mut schema = generator.subschema_for::<Vec<SourceFileEntry>>();
if let schemars::schema::Schema::Object(ref mut obj) = schema {
obj.metadata().description = Some(
"Extra files for the source archive. Accepts strings (glob patterns), objects with src/dst/info, or a mixed array.".to_owned(),
);
}
schema
}
fn deserialize_source_files<'de, D>(deserializer: D) -> Result<Vec<SourceFileEntry>, D::Error>
where
D: Deserializer<'de>,
{
use serde::de::{self, SeqAccess, Visitor};
struct SourceFilesVisitor;
impl<'de> Visitor<'de> for SourceFilesVisitor {
type Value = Vec<SourceFileEntry>;
fn expecting(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("a string, a source file entry object, or an array of strings/objects")
}
fn visit_str<E: de::Error>(self, v: &str) -> Result<Self::Value, E> {
Ok(vec![SourceFileEntry {
src: v.to_string(),
..Default::default()
}])
}
fn visit_seq<A: SeqAccess<'de>>(self, mut seq: A) -> Result<Self::Value, A::Error> {
let mut entries = Vec::new();
while let Some(value) = seq.next_element::<serde_yaml_ng::Value>()? {
match value {
serde_yaml_ng::Value::String(s) => {
entries.push(SourceFileEntry {
src: s,
..Default::default()
});
}
other => {
let entry =
SourceFileEntry::deserialize(other).map_err(de::Error::custom)?;
entries.push(entry);
}
}
}
Ok(entries)
}
fn visit_map<M: de::MapAccess<'de>>(self, map: M) -> Result<Self::Value, M::Error> {
let entry = SourceFileEntry::deserialize(de::value::MapAccessDeserializer::new(map))?;
Ok(vec![entry])
}
fn visit_unit<E: de::Error>(self) -> Result<Self::Value, E> {
Ok(Vec::new())
}
fn visit_none<E: de::Error>(self) -> Result<Self::Value, E> {
Ok(Vec::new())
}
}
deserializer.deserialize_any(SourceFilesVisitor)
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
#[serde(default)]
pub struct SbomConfig {
pub id: Option<String>,
pub cmd: Option<String>,
#[serde(default, deserialize_with = "deserialize_env_map")]
pub env: Option<HashMap<String, String>>,
pub args: Option<Vec<String>>,
pub documents: Option<Vec<String>>,
pub artifacts: Option<String>,
pub ids: Option<Vec<String>>,
#[serde(deserialize_with = "deserialize_string_or_bool_opt", default)]
pub disable: Option<StringOrBool>,
}
fn deserialize_sboms<'de, D>(deserializer: D) -> Result<Vec<SbomConfig>, D::Error>
where
D: Deserializer<'de>,
{
use serde::de::{self, Visitor};
struct SbomsVisitor;
impl<'de> Visitor<'de> for SbomsVisitor {
type Value = Vec<SbomConfig>;
fn expecting(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("an SBOM config object or an array of SBOM config objects")
}
fn visit_seq<A: de::SeqAccess<'de>>(self, mut seq: A) -> Result<Self::Value, A::Error> {
let mut configs = Vec::new();
while let Some(item) = seq.next_element::<SbomConfig>()? {
configs.push(item);
}
Ok(configs)
}
fn visit_map<M: de::MapAccess<'de>>(self, map: M) -> Result<Self::Value, M::Error> {
let config = SbomConfig::deserialize(de::value::MapAccessDeserializer::new(map))?;
Ok(vec![config])
}
fn visit_unit<E: de::Error>(self) -> Result<Self::Value, E> {
Ok(Vec::new())
}
fn visit_none<E: de::Error>(self) -> Result<Self::Value, E> {
Ok(Vec::new())
}
}
deserializer.deserialize_any(SbomsVisitor)
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
#[serde(default)]
pub struct VersionSyncConfig {
pub enabled: Option<bool>,
pub mode: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
#[serde(default)]
pub struct ChangelogConfig {
pub sort: Option<String>,
pub filters: Option<ChangelogFilters>,
pub groups: Option<Vec<ChangelogGroup>>,
pub header: Option<String>,
pub footer: Option<String>,
#[serde(deserialize_with = "deserialize_string_or_bool_opt", default)]
pub disable: Option<StringOrBool>,
#[serde(rename = "use")]
pub use_source: Option<String>,
pub abbrev: Option<i32>,
pub format: Option<String>,
pub paths: Option<Vec<String>>,
pub title: Option<String>,
pub divider: Option<String>,
pub ai: Option<ChangelogAiConfig>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
#[serde(default)]
pub struct ChangelogAiConfig {
#[serde(rename = "use")]
pub provider: Option<String>,
pub model: Option<String>,
pub prompt: Option<ChangelogAiPrompt>,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(untagged)]
pub enum ChangelogAiPrompt {
Inline(String),
Source(ChangelogAiPromptSource),
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
#[serde(default)]
pub struct ChangelogAiPromptSource {
pub from_url: Option<ContentFromUrl>,
pub from_file: Option<ContentFromFile>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ResolvedPromptSource {
File(String),
Url {
url: String,
headers: Option<std::collections::HashMap<String, String>>,
},
None,
}
impl ChangelogAiPromptSource {
pub fn resolve(&self) -> ResolvedPromptSource {
if let Some(ref file) = self.from_file
&& let Some(ref path) = file.path
{
return ResolvedPromptSource::File(path.clone());
}
if let Some(ref url_cfg) = self.from_url
&& let Some(ref url) = url_cfg.url
{
return ResolvedPromptSource::Url {
url: url.clone(),
headers: url_cfg.headers.clone(),
};
}
ResolvedPromptSource::None
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
#[serde(default)]
pub struct ContentFromUrl {
pub url: Option<String>,
pub headers: Option<std::collections::HashMap<String, String>>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
#[serde(default)]
pub struct ContentFromFile {
pub path: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
#[serde(default)]
pub struct ChangelogFilters {
pub exclude: Option<Vec<String>>,
pub include: Option<Vec<String>>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
#[serde(default)]
pub struct ChangelogGroup {
pub title: String,
pub regexp: Option<String>,
pub order: Option<i32>,
pub groups: Option<Vec<ChangelogGroup>>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
#[serde(default)]
pub struct SignConfig {
pub id: Option<String>,
pub artifacts: Option<String>,
pub cmd: Option<String>,
pub args: Option<Vec<String>>,
pub signature: Option<String>,
pub stdin: Option<String>,
pub stdin_file: Option<String>,
pub ids: Option<Vec<String>>,
#[serde(default, deserialize_with = "deserialize_env_map")]
pub env: Option<HashMap<String, String>>,
pub certificate: Option<String>,
#[serde(deserialize_with = "deserialize_string_or_bool_opt", default)]
pub output: Option<StringOrBool>,
#[serde(rename = "if")]
pub if_condition: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
#[serde(default)]
pub struct DockerSignConfig {
pub id: Option<String>,
pub artifacts: Option<String>,
pub cmd: Option<String>,
pub args: Option<Vec<String>>,
pub signature: Option<String>,
pub certificate: Option<String>,
pub ids: Option<Vec<String>>,
pub stdin: Option<String>,
pub stdin_file: Option<String>,
#[serde(default, deserialize_with = "deserialize_env_map")]
pub env: Option<HashMap<String, String>>,
#[serde(deserialize_with = "deserialize_string_or_bool_opt", default)]
pub output: Option<StringOrBool>,
#[serde(rename = "if")]
pub if_condition: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(default)]
pub struct UpxConfig {
pub id: Option<String>,
pub ids: Option<Vec<String>>,
#[serde(deserialize_with = "deserialize_string_or_bool_opt", default)]
pub enabled: Option<StringOrBool>,
pub binary: String,
pub args: Vec<String>,
pub required: bool,
pub targets: Option<Vec<String>>,
pub compress: Option<String>,
pub lzma: Option<bool>,
pub brute: Option<bool>,
}
impl Default for UpxConfig {
fn default() -> Self {
UpxConfig {
id: None,
ids: None,
enabled: None,
binary: "upx".to_string(),
args: Vec::new(),
required: false,
targets: None,
compress: None,
lzma: None,
brute: None,
}
}
}
fn deserialize_upx<'de, D>(deserializer: D) -> Result<Vec<UpxConfig>, D::Error>
where
D: Deserializer<'de>,
{
use serde::de::{self, Visitor};
struct UpxVisitor;
impl<'de> Visitor<'de> for UpxVisitor {
type Value = Vec<UpxConfig>;
fn expecting(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("a UPX config object or an array of UPX config objects")
}
fn visit_seq<A: de::SeqAccess<'de>>(self, mut seq: A) -> Result<Self::Value, A::Error> {
let mut configs = Vec::new();
while let Some(item) = seq.next_element::<UpxConfig>()? {
configs.push(item);
}
Ok(configs)
}
fn visit_map<M: de::MapAccess<'de>>(self, map: M) -> Result<Self::Value, M::Error> {
let config = UpxConfig::deserialize(de::value::MapAccessDeserializer::new(map))?;
Ok(vec![config])
}
fn visit_unit<E: de::Error>(self) -> Result<Self::Value, E> {
Ok(Vec::new())
}
fn visit_none<E: de::Error>(self) -> Result<Self::Value, E> {
Ok(Vec::new())
}
}
deserializer.deserialize_any(UpxVisitor)
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct SnapshotConfig {
#[serde(alias = "name_template", rename = "version_template")]
pub name_template: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
#[serde(default)]
pub struct NightlyConfig {
pub name_template: Option<String>,
pub tag_name: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
#[serde(default)]
pub struct MetadataConfig {
pub description: Option<String>,
pub homepage: Option<String>,
pub license: Option<String>,
pub maintainers: Option<Vec<String>>,
pub mod_timestamp: Option<String>,
pub full_description: Option<ContentSource>,
pub commit_author: Option<CommitAuthorConfig>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
#[serde(default)]
pub struct TemplateFileConfig {
pub id: Option<String>,
pub src: String,
pub dst: String,
pub mode: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
#[serde(default)]
pub struct AnnounceConfig {
#[serde(deserialize_with = "deserialize_string_or_bool_opt", default)]
pub skip: Option<StringOrBool>,
pub discord: Option<DiscordAnnounce>,
pub discourse: Option<DiscourseAnnounce>,
pub slack: Option<SlackAnnounce>,
pub webhook: Option<WebhookConfig>,
pub telegram: Option<TelegramAnnounce>,
pub teams: Option<TeamsAnnounce>,
pub mattermost: Option<MattermostAnnounce>,
pub email: Option<EmailAnnounce>,
pub reddit: Option<RedditAnnounce>,
pub twitter: Option<TwitterAnnounce>,
pub mastodon: Option<MastodonAnnounce>,
pub bluesky: Option<BlueskyAnnounce>,
pub linkedin: Option<LinkedInAnnounce>,
pub opencollective: Option<OpenCollectiveAnnounce>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
#[serde(default)]
pub struct BlueskyAnnounce {
#[serde(deserialize_with = "deserialize_string_or_bool_opt", default)]
pub enabled: Option<StringOrBool>,
pub username: Option<String>,
pub message_template: Option<String>,
pub pds_url: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
#[serde(default)]
pub struct DiscourseAnnounce {
#[serde(deserialize_with = "deserialize_string_or_bool_opt", default)]
pub enabled: Option<StringOrBool>,
pub server: Option<String>,
pub category_id: Option<u64>,
pub username: Option<String>,
pub title_template: Option<String>,
pub message_template: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
#[serde(default)]
pub struct LinkedInAnnounce {
#[serde(deserialize_with = "deserialize_string_or_bool_opt", default)]
pub enabled: Option<StringOrBool>,
pub message_template: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
#[serde(default)]
pub struct OpenCollectiveAnnounce {
#[serde(deserialize_with = "deserialize_string_or_bool_opt", default)]
pub enabled: Option<StringOrBool>,
pub slug: Option<String>,
pub title_template: Option<String>,
pub message_template: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
#[serde(default)]
pub struct TwitterAnnounce {
#[serde(deserialize_with = "deserialize_string_or_bool_opt", default)]
pub enabled: Option<StringOrBool>,
pub message_template: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
#[serde(default)]
pub struct MastodonAnnounce {
#[serde(deserialize_with = "deserialize_string_or_bool_opt", default)]
pub enabled: Option<StringOrBool>,
pub server: Option<String>,
pub message_template: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
#[serde(default)]
pub struct DiscordAnnounce {
#[serde(deserialize_with = "deserialize_string_or_bool_opt", default)]
pub enabled: Option<StringOrBool>,
pub webhook_url: Option<String>,
pub message_template: Option<String>,
pub author: Option<String>,
pub color: Option<String>,
pub icon_url: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
#[serde(default)]
pub struct WebhookConfig {
#[serde(deserialize_with = "deserialize_string_or_bool_opt", default)]
pub enabled: Option<StringOrBool>,
pub endpoint_url: Option<String>,
pub headers: Option<HashMap<String, String>>,
pub content_type: Option<String>,
pub message_template: Option<String>,
pub skip_tls_verify: Option<bool>,
#[serde(default)]
pub expected_status_codes: Vec<u16>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
#[serde(default)]
pub struct TelegramAnnounce {
#[serde(deserialize_with = "deserialize_string_or_bool_opt", default)]
pub enabled: Option<StringOrBool>,
pub bot_token: Option<String>,
pub chat_id: Option<String>,
pub message_template: Option<String>,
pub parse_mode: Option<String>,
pub message_thread_id: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
#[serde(default)]
pub struct TeamsAnnounce {
#[serde(deserialize_with = "deserialize_string_or_bool_opt", default)]
pub enabled: Option<StringOrBool>,
pub webhook_url: Option<String>,
pub message_template: Option<String>,
pub title_template: Option<String>,
pub color: Option<String>,
pub icon_url: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
#[serde(default)]
pub struct MattermostAnnounce {
#[serde(deserialize_with = "deserialize_string_or_bool_opt", default)]
pub enabled: Option<StringOrBool>,
pub webhook_url: Option<String>,
pub channel: Option<String>,
pub username: Option<String>,
pub icon_url: Option<String>,
pub icon_emoji: Option<String>,
pub color: Option<String>,
pub message_template: Option<String>,
pub title_template: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
#[serde(default)]
pub struct EmailAnnounce {
#[serde(deserialize_with = "deserialize_string_or_bool_opt", default)]
pub enabled: Option<StringOrBool>,
pub host: Option<String>,
pub port: Option<u16>,
pub username: Option<String>,
pub from: Option<String>,
#[serde(default)]
pub to: Vec<String>,
pub subject_template: Option<String>,
#[serde(alias = "body_template")]
pub message_template: Option<String>,
pub insecure_skip_verify: Option<bool>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
#[serde(default)]
pub struct RedditAnnounce {
#[serde(deserialize_with = "deserialize_string_or_bool_opt", default)]
pub enabled: Option<StringOrBool>,
pub application_id: Option<String>,
pub username: Option<String>,
pub sub: Option<String>,
pub title_template: Option<String>,
pub url_template: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
#[serde(default)]
pub struct SlackAnnounce {
#[serde(deserialize_with = "deserialize_string_or_bool_opt", default)]
pub enabled: Option<StringOrBool>,
pub webhook_url: Option<String>,
pub message_template: Option<String>,
pub channel: Option<String>,
pub username: Option<String>,
pub icon_emoji: Option<String>,
pub icon_url: Option<String>,
pub blocks: Option<Vec<SlackBlock>>,
pub attachments: Option<Vec<SlackAttachment>>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
pub struct SlackBlock {
#[serde(rename = "type")]
pub block_type: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub text: Option<SlackTextObject>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub block_id: Option<String>,
#[serde(flatten)]
pub extra: HashMap<String, serde_json::Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
pub struct SlackTextObject {
#[serde(rename = "type")]
pub text_type: String,
pub text: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub emoji: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub verbatim: Option<bool>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
pub struct SlackAttachment {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub color: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub text: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub title: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub fallback: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub pretext: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub footer: Option<String>,
#[serde(flatten)]
pub extra: HashMap<String, serde_json::Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
#[serde(default)]
pub struct DockerHubConfig {
pub username: Option<String>,
pub secret_name: Option<String>,
pub images: Option<Vec<String>>,
pub description: Option<String>,
pub full_description: Option<DockerHubFullDescription>,
#[serde(deserialize_with = "deserialize_string_or_bool_opt", default)]
pub disable: Option<StringOrBool>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
#[serde(default)]
pub struct DockerHubFullDescription {
pub from_url: Option<DockerHubFromUrl>,
pub from_file: Option<DockerHubFromFile>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
#[serde(default)]
pub struct DockerHubFromUrl {
pub url: String,
pub headers: Option<HashMap<String, String>>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
#[serde(default)]
pub struct DockerHubFromFile {
pub path: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
#[serde(default)]
pub struct ArtifactoryConfig {
pub name: Option<String>,
pub target: Option<String>,
pub mode: Option<String>,
pub username: Option<String>,
pub password: Option<String>,
pub ids: Option<Vec<String>>,
pub exts: Option<Vec<String>>,
pub client_x509_cert: Option<String>,
pub client_x509_key: Option<String>,
pub custom_headers: Option<HashMap<String, String>>,
pub checksum_header: Option<String>,
pub extra_files: Option<Vec<ExtraFileSpec>>,
pub checksum: Option<bool>,
pub signature: Option<bool>,
pub meta: Option<bool>,
pub custom_artifact_name: Option<bool>,
pub extra_files_only: Option<bool>,
pub method: Option<String>,
pub trusted_certificates: Option<String>,
#[serde(deserialize_with = "deserialize_string_or_bool_opt", default)]
pub skip: Option<StringOrBool>,
#[serde(deserialize_with = "deserialize_string_or_bool_opt", default)]
pub disable: Option<StringOrBool>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
#[serde(default)]
pub struct CloudSmithConfig {
pub organization: Option<String>,
pub repository: Option<String>,
pub ids: Option<Vec<String>>,
pub formats: Option<Vec<String>>,
pub distributions: Option<HashMap<String, serde_json::Value>>,
pub component: Option<String>,
pub secret_name: Option<String>,
#[serde(deserialize_with = "deserialize_string_or_bool_opt", default)]
pub skip: Option<StringOrBool>,
#[serde(deserialize_with = "deserialize_string_or_bool_opt", default)]
pub republish: Option<StringOrBool>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
#[serde(default)]
pub struct PublisherConfig {
pub name: Option<String>,
pub cmd: String,
pub args: Option<Vec<String>>,
pub ids: Option<Vec<String>>,
pub artifact_types: Option<Vec<String>>,
#[serde(default, deserialize_with = "deserialize_env_map")]
pub env: Option<HashMap<String, String>>,
pub dir: Option<String>,
#[serde(deserialize_with = "deserialize_string_or_bool_opt", default)]
pub disable: Option<StringOrBool>,
pub checksum: Option<bool>,
pub signature: Option<bool>,
pub meta: Option<bool>,
pub extra_files: Option<Vec<ExtraFileSpec>>,
pub templated_extra_files: Option<Vec<TemplatedExtraFile>>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default, JsonSchema)]
#[serde(default)]
pub struct HooksConfig {
pub hooks: Option<Vec<HookEntry>>,
pub post: Option<Vec<HookEntry>>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default, JsonSchema)]
#[serde(default)]
pub struct StructuredHook {
pub cmd: String,
pub dir: Option<String>,
#[serde(default, deserialize_with = "deserialize_env_map")]
pub env: Option<HashMap<String, String>>,
pub output: Option<bool>,
}
#[derive(Debug, Clone, PartialEq, Serialize, JsonSchema)]
#[serde(untagged)]
pub enum HookEntry {
Simple(String),
Structured(StructuredHook),
}
impl PartialEq<&str> for HookEntry {
fn eq(&self, other: &&str) -> bool {
match self {
HookEntry::Simple(s) => s.as_str() == *other,
HookEntry::Structured(h) => h.cmd.as_str() == *other,
}
}
}
impl<'de> Deserialize<'de> for HookEntry {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let value = serde_json::Value::deserialize(deserializer)?;
match &value {
serde_json::Value::String(s) => Ok(HookEntry::Simple(s.clone())),
serde_json::Value::Object(_) => {
let hook: StructuredHook =
serde_json::from_value(value).map_err(serde::de::Error::custom)?;
Ok(HookEntry::Structured(hook))
}
_ => Err(serde::de::Error::custom(
"hook entry must be a string or an object with cmd/dir/env/output",
)),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
#[serde(default)]
pub struct GitConfig {
pub tag_sort: Option<String>,
pub ignore_tags: Option<Vec<String>>,
pub ignore_tag_prefixes: Option<Vec<String>>,
pub prerelease_suffix: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
#[serde(default, deny_unknown_fields)]
pub struct MonorepoConfig {
pub tag_prefix: Option<String>,
pub dir: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
#[serde(default)]
pub struct TagConfig {
pub default_bump: Option<String>,
pub tag_prefix: Option<String>,
pub release_branches: Option<Vec<String>>,
pub custom_tag: Option<String>,
pub tag_context: Option<String>,
pub branch_history: Option<String>,
pub initial_version: Option<String>,
pub prerelease: Option<bool>,
pub prerelease_suffix: Option<String>,
pub force_without_changes: Option<bool>,
pub force_without_changes_pre: Option<bool>,
pub major_string_token: Option<String>,
pub minor_string_token: Option<String>,
pub patch_string_token: Option<String>,
pub none_string_token: Option<String>,
pub git_api_tagging: Option<bool>,
pub verbose: Option<bool>,
pub tag_pre_hooks: Option<Vec<HookEntry>>,
pub tag_post_hooks: Option<Vec<HookEntry>>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
#[serde(default, deny_unknown_fields)]
pub struct WorkspaceConfig {
pub name: String,
pub crates: Vec<CrateConfig>,
pub changelog: Option<ChangelogConfig>,
#[serde(default, alias = "sign", deserialize_with = "deserialize_signs")]
#[schemars(schema_with = "signs_schema")]
pub signs: Vec<SignConfig>,
#[serde(default, alias = "binary_sign", deserialize_with = "deserialize_signs")]
#[schemars(schema_with = "signs_schema")]
pub binary_signs: Vec<SignConfig>,
pub before: Option<HooksConfig>,
pub after: Option<HooksConfig>,
#[serde(default, deserialize_with = "deserialize_env_map")]
pub env: Option<HashMap<String, String>>,
#[serde(default)]
pub skip: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Serialize, JsonSchema)]
#[serde(untagged)]
pub enum StringOrBool {
Bool(bool),
String(String),
}
impl StringOrBool {
pub fn as_bool(&self) -> bool {
match self {
StringOrBool::Bool(b) => *b,
StringOrBool::String(s) => matches!(s.trim(), "true" | "1"),
}
}
pub fn as_str(&self) -> &str {
match self {
StringOrBool::Bool(true) => "true",
StringOrBool::Bool(false) => "false",
StringOrBool::String(s) => s,
}
}
pub fn is_template(&self) -> bool {
matches!(self, StringOrBool::String(s) if s.contains('{'))
}
pub fn evaluates_to_true(&self, render: impl Fn(&str) -> anyhow::Result<String>) -> bool {
if self.is_template() {
render(self.as_str())
.map(|r| r.trim() == "true")
.unwrap_or(false)
} else {
self.as_bool()
}
}
pub fn is_disabled(&self, render: impl Fn(&str) -> anyhow::Result<String>) -> bool {
self.evaluates_to_true(render)
}
}
impl Default for StringOrBool {
fn default() -> Self {
StringOrBool::Bool(false)
}
}
fn deserialize_string_or_bool_opt<'de, D>(deserializer: D) -> Result<Option<StringOrBool>, D::Error>
where
D: Deserializer<'de>,
{
use serde::de::{self, Visitor};
struct StringOrBoolVisitor;
impl<'de> Visitor<'de> for StringOrBoolVisitor {
type Value = Option<StringOrBool>;
fn expecting(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("a bool, a string, or null")
}
fn visit_bool<E: de::Error>(self, v: bool) -> Result<Self::Value, E> {
Ok(Some(StringOrBool::Bool(v)))
}
fn visit_str<E: de::Error>(self, v: &str) -> Result<Self::Value, E> {
Ok(Some(StringOrBool::String(v.to_owned())))
}
fn visit_string<E: de::Error>(self, v: String) -> Result<Self::Value, E> {
Ok(Some(StringOrBool::String(v)))
}
fn visit_none<E: de::Error>(self) -> Result<Self::Value, E> {
Ok(None)
}
fn visit_unit<E: de::Error>(self) -> Result<Self::Value, E> {
Ok(None)
}
}
deserializer.deserialize_any(StringOrBoolVisitor)
}
fn deserialize_string_or_vec_opt<'de, D>(deserializer: D) -> Result<Option<Vec<String>>, D::Error>
where
D: Deserializer<'de>,
{
use serde::de::{self, Visitor};
struct StringOrVecVisitor;
impl<'de> Visitor<'de> for StringOrVecVisitor {
type Value = Option<Vec<String>>;
fn expecting(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("a string, a list of strings, or null")
}
fn visit_str<E: de::Error>(self, v: &str) -> Result<Self::Value, E> {
Ok(Some(vec![v.to_owned()]))
}
fn visit_string<E: de::Error>(self, v: String) -> Result<Self::Value, E> {
Ok(Some(vec![v]))
}
fn visit_seq<A: de::SeqAccess<'de>>(self, mut seq: A) -> Result<Self::Value, A::Error> {
let mut items = Vec::new();
while let Some(item) = seq.next_element::<String>()? {
items.push(item);
}
Ok(Some(items))
}
fn visit_none<E: de::Error>(self) -> Result<Self::Value, E> {
Ok(None)
}
fn visit_unit<E: de::Error>(self) -> Result<Self::Value, E> {
Ok(None)
}
}
deserializer.deserialize_any(StringOrVecVisitor)
}
fn deserialize_space_separated_string_or_vec_opt<'de, D>(
deserializer: D,
) -> Result<Option<Vec<String>>, D::Error>
where
D: Deserializer<'de>,
{
use serde::de::{self, Visitor};
struct SpaceSepOrVecVisitor;
impl<'de> Visitor<'de> for SpaceSepOrVecVisitor {
type Value = Option<Vec<String>>;
fn expecting(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("a space-separated string, a list of strings, or null")
}
fn visit_str<E: de::Error>(self, v: &str) -> Result<Self::Value, E> {
let tags: Vec<String> = v.split_whitespace().map(|s| s.to_owned()).collect();
if tags.is_empty() {
Ok(None)
} else {
Ok(Some(tags))
}
}
fn visit_string<E: de::Error>(self, v: String) -> Result<Self::Value, E> {
self.visit_str(&v)
}
fn visit_seq<A: de::SeqAccess<'de>>(self, mut seq: A) -> Result<Self::Value, A::Error> {
let mut items = Vec::new();
while let Some(item) = seq.next_element::<String>()? {
items.push(item);
}
if items.is_empty() {
Ok(None)
} else {
Ok(Some(items))
}
}
fn visit_none<E: de::Error>(self) -> Result<Self::Value, E> {
Ok(None)
}
fn visit_unit<E: de::Error>(self) -> Result<Self::Value, E> {
Ok(None)
}
}
deserializer.deserialize_any(SpaceSepOrVecVisitor)
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
#[serde(default)]
pub struct MakeselfConfig {
pub id: Option<String>,
pub ids: Option<Vec<String>>,
pub name_template: Option<String>,
pub name: Option<String>,
pub script: Option<String>,
pub description: Option<String>,
pub maintainer: Option<String>,
pub keywords: Option<Vec<String>>,
pub homepage: Option<String>,
pub license: Option<String>,
pub compression: Option<String>,
pub extra_args: Option<Vec<String>>,
pub files: Option<Vec<MakeselfFile>>,
pub goos: Option<Vec<String>>,
pub goarch: Option<Vec<String>>,
#[serde(deserialize_with = "deserialize_string_or_bool_opt", default)]
pub disable: Option<StringOrBool>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
#[serde(default)]
pub struct MakeselfFile {
#[serde(alias = "src")]
pub source: String,
#[serde(alias = "dst")]
pub destination: Option<String>,
pub strip_parent: Option<bool>,
}
fn deserialize_makeselfs<'de, D>(deserializer: D) -> Result<Vec<MakeselfConfig>, D::Error>
where
D: Deserializer<'de>,
{
use serde::de::{self, Visitor};
struct MakeselfVisitor;
impl<'de> Visitor<'de> for MakeselfVisitor {
type Value = Vec<MakeselfConfig>;
fn expecting(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("a makeself config object or an array of makeself config objects")
}
fn visit_seq<A: de::SeqAccess<'de>>(self, mut seq: A) -> Result<Self::Value, A::Error> {
let mut configs = Vec::new();
while let Some(item) = seq.next_element::<MakeselfConfig>()? {
configs.push(item);
}
Ok(configs)
}
fn visit_map<M: de::MapAccess<'de>>(self, map: M) -> Result<Self::Value, M::Error> {
let config = MakeselfConfig::deserialize(de::value::MapAccessDeserializer::new(map))?;
Ok(vec![config])
}
fn visit_unit<E: de::Error>(self) -> Result<Self::Value, E> {
Ok(Vec::new())
}
fn visit_none<E: de::Error>(self) -> Result<Self::Value, E> {
Ok(Vec::new())
}
}
deserializer.deserialize_any(MakeselfVisitor)
}
fn makeselfs_schema(generator: &mut schemars::r#gen::SchemaGenerator) -> schemars::schema::Schema {
let mut schema = generator.subschema_for::<Vec<MakeselfConfig>>();
if let schemars::schema::Schema::Object(ref mut obj) = schema {
obj.metadata().description = Some(
"Makeself self-extracting archive configurations. Accepts a single object or array."
.to_owned(),
);
}
schema
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
#[serde(default)]
pub struct SrpmConfig {
pub enabled: Option<bool>,
pub package_name: Option<String>,
pub file_name_template: Option<String>,
pub spec_file: Option<String>,
pub epoch: Option<String>,
pub section: Option<String>,
pub maintainer: Option<String>,
pub vendor: Option<String>,
pub summary: Option<String>,
pub group: Option<String>,
pub description: Option<String>,
pub license: Option<String>,
pub license_file_name: Option<String>,
pub url: Option<String>,
pub packager: Option<String>,
pub compression: Option<String>,
pub docs: Option<Vec<String>>,
pub contents: Option<Vec<NfpmContentConfig>>,
pub signature: Option<SrpmSignatureConfig>,
#[serde(deserialize_with = "deserialize_string_or_bool_opt", default)]
pub disable: Option<StringOrBool>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
#[serde(default)]
pub struct SrpmSignatureConfig {
pub key_file: Option<String>,
pub passphrase: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
#[serde(default)]
pub struct NfpmContentConfig {
#[serde(alias = "src")]
pub source: Option<String>,
#[serde(alias = "dst")]
pub destination: String,
#[serde(rename = "type")]
pub type_: Option<String>,
pub packager: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
#[serde(default)]
pub struct MilestoneConfig {
pub repo: Option<ScmRepoConfig>,
pub close: Option<bool>,
pub fail_on_error: Option<bool>,
pub name_template: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
#[serde(default)]
pub struct UploadConfig {
pub name: Option<String>,
pub ids: Option<Vec<String>>,
pub exts: Option<Vec<String>>,
pub target: String,
pub username: Option<String>,
pub password: Option<String>,
pub method: Option<String>,
pub mode: Option<String>,
pub checksum_header: Option<String>,
pub trusted_certificates: Option<String>,
pub client_x509_cert: Option<String>,
pub client_x509_key: Option<String>,
pub checksum: Option<bool>,
pub signature: Option<bool>,
pub meta: Option<bool>,
pub custom_headers: Option<HashMap<String, String>>,
pub custom_artifact_name: Option<bool>,
pub extra_files: Option<Vec<ExtraFileSpec>>,
pub extra_files_only: Option<bool>,
#[serde(deserialize_with = "deserialize_string_or_bool_opt", default)]
pub disable: Option<StringOrBool>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
#[serde(default)]
pub struct AurSourceConfig {
#[serde(alias = "package_name")]
pub name: Option<String>,
pub ids: Option<Vec<String>>,
pub commit_author: Option<CommitAuthorConfig>,
pub commit_msg_template: Option<String>,
pub description: Option<String>,
pub homepage: Option<String>,
pub license: Option<String>,
#[serde(deserialize_with = "deserialize_string_or_bool_opt", default)]
pub skip_upload: Option<StringOrBool>,
pub url_template: Option<String>,
pub maintainers: Option<Vec<String>>,
pub contributors: Option<Vec<String>>,
pub provides: Option<Vec<String>>,
pub conflicts: Option<Vec<String>>,
pub depends: Option<Vec<String>>,
pub optdepends: Option<Vec<String>>,
pub makedepends: Option<Vec<String>>,
pub backup: Option<Vec<String>>,
pub rel: Option<String>,
pub prepare: Option<String>,
pub build: Option<String>,
pub package: Option<String>,
pub git_url: Option<String>,
pub git_ssh_command: Option<String>,
pub private_key: Option<String>,
pub directory: Option<String>,
#[serde(deserialize_with = "deserialize_string_or_bool_opt", default)]
pub disable: Option<StringOrBool>,
pub arches: Option<Vec<String>>,
}
#[cfg(test)]
#[allow(clippy::field_reassign_with_default)]
mod tests {
use super::*;
#[test]
fn test_minimal_yaml_config() {
let yaml = r#"
project_name: myproject
crates:
- name: myproject
path: "."
tag_template: "v{{ .Version }}"
"#;
let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
assert_eq!(config.project_name, "myproject");
assert_eq!(config.crates.len(), 1);
assert_eq!(config.dist, std::path::PathBuf::from("./dist"));
}
#[test]
fn test_minimal_toml_config() {
let toml_str = r#"
project_name = "myproject"
[[crates]]
name = "myproject"
path = "."
tag_template = "v{{ .Version }}"
"#;
let config: Config = toml::from_str(toml_str).unwrap();
assert_eq!(config.project_name, "myproject");
}
#[test]
fn test_full_config_with_defaults() {
let yaml = r#"
project_name: cfgd
dist: ./dist
defaults:
targets:
- x86_64-unknown-linux-gnu
- aarch64-apple-darwin
cross: auto
flags: --release
archives:
format: tar.gz
format_overrides:
- os: windows
format: zip
checksum:
algorithm: sha256
crates:
- name: cfgd
path: crates/cfgd
tag_template: "v{{ .Version }}"
builds:
- binary: cfgd
features: []
no_default_features: false
archives:
- name_template: "{{ .ProjectName }}-{{ .Version }}-{{ .Os }}-{{ .Arch }}"
files:
- LICENSE
release:
github:
owner: tj-smith47
name: cfgd
draft: false
prerelease: auto
name_template: "{{ .Tag }}"
"#;
let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
let defaults = config.defaults.unwrap();
assert_eq!(defaults.targets.unwrap().len(), 2);
assert_eq!(defaults.cross, Some(CrossStrategy::Auto));
let release = config.crates[0].release.as_ref().unwrap();
assert_eq!(release.name_template, Some("{{ .Tag }}".to_string()));
}
#[test]
fn test_snapshot_config() {
let yaml = r#"
project_name: test
snapshot:
name_template: "{{ .Version }}-SNAPSHOT-{{ .ShortCommit }}"
crates:
- name: test
path: "."
tag_template: "v{{ .Version }}"
"#;
let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
assert_eq!(
config.snapshot.unwrap().name_template,
"{{ .Version }}-SNAPSHOT-{{ .ShortCommit }}"
);
}
#[test]
fn test_archives_false() {
let yaml = r#"
project_name: test
crates:
- name: operator
path: crates/operator
tag_template: "v{{ .Version }}"
archives: false
"#;
let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
assert!(matches!(
config.crates[0].archives,
ArchivesConfig::Disabled
));
}
#[test]
fn test_publish_crates_bool_and_object() {
let yaml_bool = r#"
project_name: test
crates:
- name: a
path: "."
tag_template: "v{{ .Version }}"
publish:
crates: true
"#;
let config: Config = serde_yaml_ng::from_str(yaml_bool).unwrap();
assert!(
config.crates[0]
.publish
.as_ref()
.unwrap()
.crates_config()
.enabled
);
let yaml_obj = r#"
project_name: test
crates:
- name: a
path: "."
tag_template: "v{{ .Version }}"
publish:
crates:
enabled: true
index_timeout: 120
"#;
let config: Config = serde_yaml_ng::from_str(yaml_obj).unwrap();
let crates_cfg = config.crates[0].publish.as_ref().unwrap().crates_config();
assert!(crates_cfg.enabled);
assert_eq!(crates_cfg.index_timeout, 120);
}
#[test]
fn test_make_latest_auto() {
let yaml = r#"
project_name: test
crates:
- name: a
path: "."
tag_template: "v{{ .Version }}"
release:
make_latest: auto
"#;
let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
let release = config.crates[0].release.as_ref().unwrap();
assert_eq!(release.make_latest, Some(MakeLatestConfig::Auto));
}
#[test]
fn test_make_latest_true() {
let yaml = r#"
project_name: test
crates:
- name: a
path: "."
tag_template: "v{{ .Version }}"
release:
make_latest: true
"#;
let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
let release = config.crates[0].release.as_ref().unwrap();
assert_eq!(release.make_latest, Some(MakeLatestConfig::Bool(true)));
}
#[test]
fn test_make_latest_false() {
let yaml = r#"
project_name: test
crates:
- name: a
path: "."
tag_template: "v{{ .Version }}"
release:
make_latest: false
"#;
let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
let release = config.crates[0].release.as_ref().unwrap();
assert_eq!(release.make_latest, Some(MakeLatestConfig::Bool(false)));
}
#[test]
fn test_make_latest_omitted() {
let yaml = r#"
project_name: test
crates:
- name: a
path: "."
tag_template: "v{{ .Version }}"
release:
draft: false
"#;
let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
let release = config.crates[0].release.as_ref().unwrap();
assert_eq!(release.make_latest, None);
}
#[test]
fn test_make_latest_template_string() {
let yaml = r#"
project_name: test
crates:
- name: a
path: "."
tag_template: "v{{ .Version }}"
release:
make_latest: "{{ if .IsSnapshot }}false{{ else }}true{{ end }}"
"#;
let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
let release = config.crates[0].release.as_ref().unwrap();
assert_eq!(
release.make_latest,
Some(MakeLatestConfig::String(
"{{ if .IsSnapshot }}false{{ else }}true{{ end }}".to_string()
))
);
}
#[test]
fn test_make_latest_string_true() {
let yaml = r#"
project_name: test
crates:
- name: a
path: "."
tag_template: "v{{ .Version }}"
release:
make_latest: "true"
"#;
let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
let release = config.crates[0].release.as_ref().unwrap();
assert_eq!(release.make_latest, Some(MakeLatestConfig::Bool(true)));
}
#[test]
fn test_make_latest_string_false() {
let yaml = r#"
project_name: test
crates:
- name: a
path: "."
tag_template: "v{{ .Version }}"
release:
make_latest: "false"
"#;
let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
let release = config.crates[0].release.as_ref().unwrap();
assert_eq!(release.make_latest, Some(MakeLatestConfig::Bool(false)));
}
#[test]
fn test_changelog_header_footer() {
let yaml = r##"
project_name: test
changelog:
header: "# My Release Notes"
footer: "---\nGenerated by anodizer"
crates:
- name: a
path: "."
tag_template: "v{{ .Version }}"
"##;
let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
let cl = config.changelog.as_ref().unwrap();
assert_eq!(cl.header, Some("# My Release Notes".to_string()));
assert_eq!(cl.footer, Some("---\nGenerated by anodizer".to_string()));
}
#[test]
fn test_changelog_disable() {
let yaml = r#"
project_name: test
changelog:
disable: true
crates:
- name: a
path: "."
tag_template: "v{{ .Version }}"
"#;
let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
let cl = config.changelog.as_ref().unwrap();
assert_eq!(cl.disable, Some(StringOrBool::Bool(true)));
}
#[test]
fn test_changelog_disable_false() {
let yaml = r#"
project_name: test
changelog:
disable: false
sort: desc
crates:
- name: a
path: "."
tag_template: "v{{ .Version }}"
"#;
let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
let cl = config.changelog.as_ref().unwrap();
assert_eq!(cl.disable, Some(StringOrBool::Bool(false)));
assert_eq!(cl.sort, Some("desc".to_string()));
}
#[test]
fn test_checksum_disable() {
let yaml = r#"
project_name: test
defaults:
checksum:
disable: true
crates:
- name: a
path: "."
tag_template: "v{{ .Version }}"
"#;
let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
let checksum = config.defaults.as_ref().unwrap().checksum.as_ref().unwrap();
assert_eq!(checksum.disable, Some(StringOrBool::Bool(true)));
}
#[test]
fn test_checksum_disable_per_crate() {
let yaml = r#"
project_name: test
crates:
- name: a
path: "."
tag_template: "v{{ .Version }}"
checksum:
disable: true
algorithm: sha512
"#;
let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
let checksum = config.crates[0].checksum.as_ref().unwrap();
assert_eq!(checksum.disable, Some(StringOrBool::Bool(true)));
assert_eq!(checksum.algorithm, Some("sha512".to_string()));
}
#[test]
fn test_checksum_disable_template_string() {
let yaml = r#"
project_name: test
defaults:
checksum:
disable: "{{ if .IsSnapshot }}true{{ end }}"
crates:
- name: a
path: "."
tag_template: "v{{ .Version }}"
"#;
let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
let checksum = config.defaults.as_ref().unwrap().checksum.as_ref().unwrap();
match &checksum.disable {
Some(StringOrBool::String(s)) => {
assert!(s.contains("IsSnapshot"));
}
other => panic!("expected StringOrBool::String, got {:?}", other),
}
}
#[test]
fn test_checksum_extra_files_object_form() {
let yaml = r#"
project_name: test
crates:
- name: a
path: "."
tag_template: "v{{ .Version }}"
checksum:
extra_files:
- "dist/*.bin"
- glob: "release/*.deb"
name_template: "{{ .ArtifactName }}.checksum"
"#;
let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
let checksum = config.crates[0].checksum.as_ref().unwrap();
let extra = checksum.extra_files.as_ref().unwrap();
assert_eq!(extra.len(), 2);
assert_eq!(extra[0], ExtraFileSpec::Glob("dist/*.bin".to_string()));
match &extra[1] {
ExtraFileSpec::Detailed {
glob,
name_template,
} => {
assert_eq!(glob, "release/*.deb");
assert_eq!(
name_template.as_deref(),
Some("{{ .ArtifactName }}.checksum")
);
}
other => panic!("expected ExtraFileSpec::Detailed, got {:?}", other),
}
}
#[test]
fn test_make_latest_serialize_roundtrip() {
let auto = MakeLatestConfig::Auto;
let json = serde_json::to_string(&auto).unwrap();
assert_eq!(json, "\"auto\"");
let bool_true = MakeLatestConfig::Bool(true);
let json = serde_json::to_string(&bool_true).unwrap();
assert_eq!(json, "true");
let bool_false = MakeLatestConfig::Bool(false);
let json = serde_json::to_string(&bool_false).unwrap();
assert_eq!(json, "false");
let tmpl = MakeLatestConfig::String(
"{{ if .IsSnapshot }}false{{ else }}true{{ end }}".to_string(),
);
let json = serde_json::to_string(&tmpl).unwrap();
assert_eq!(json, "\"{{ if .IsSnapshot }}false{{ else }}true{{ end }}\"");
}
#[test]
fn test_release_header_footer_inline() {
let yaml = r###"
project_name: test
crates:
- name: a
path: "."
tag_template: "v{{ .Version }}"
release:
header: "## Custom Header"
footer: "---\nPowered by anodizer"
"###;
let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
let release = config.crates[0].release.as_ref().unwrap();
assert_eq!(
release.header,
Some(ContentSource::Inline("## Custom Header".to_string()))
);
assert_eq!(
release.footer,
Some(ContentSource::Inline(
"---\nPowered by anodizer".to_string()
))
);
}
#[test]
fn test_release_header_footer_from_file() {
let yaml = r#"
project_name: test
crates:
- name: a
path: "."
tag_template: "v{{ .Version }}"
release:
header:
from_file: ./RELEASE_HEADER.md
footer:
from_file: ./RELEASE_FOOTER.md
"#;
let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
let release = config.crates[0].release.as_ref().unwrap();
assert_eq!(
release.header,
Some(ContentSource::FromFile {
from_file: "./RELEASE_HEADER.md".to_string()
})
);
assert_eq!(
release.footer,
Some(ContentSource::FromFile {
from_file: "./RELEASE_FOOTER.md".to_string()
})
);
}
#[test]
fn test_release_header_footer_from_url() {
let yaml = r#"
project_name: test
crates:
- name: a
path: "."
tag_template: "v{{ .Version }}"
release:
header:
from_url: https://example.com/header.md
footer:
from_url: https://example.com/footer.md
"#;
let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
let release = config.crates[0].release.as_ref().unwrap();
assert_eq!(
release.header,
Some(ContentSource::FromUrl {
from_url: "https://example.com/header.md".to_string(),
headers: None,
})
);
assert_eq!(
release.footer,
Some(ContentSource::FromUrl {
from_url: "https://example.com/footer.md".to_string(),
headers: None,
})
);
}
#[test]
fn test_release_header_footer_omitted() {
let yaml = r#"
project_name: test
crates:
- name: a
path: "."
tag_template: "v{{ .Version }}"
release:
draft: false
"#;
let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
let release = config.crates[0].release.as_ref().unwrap();
assert_eq!(release.header, None);
assert_eq!(release.footer, None);
}
#[test]
fn test_release_extra_files_glob_strings() {
let yaml = r#"
project_name: test
crates:
- name: a
path: "."
tag_template: "v{{ .Version }}"
release:
extra_files:
- "dist/*.sig"
- "CHANGELOG.md"
"#;
let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
let release = config.crates[0].release.as_ref().unwrap();
let files = release.extra_files.as_ref().unwrap();
assert_eq!(files.len(), 2);
assert_eq!(files[0], ExtraFileSpec::Glob("dist/*.sig".to_string()));
assert_eq!(files[1], ExtraFileSpec::Glob("CHANGELOG.md".to_string()));
}
#[test]
fn test_release_extra_files_detailed_objects() {
let yaml = r#"
project_name: test
crates:
- name: a
path: "."
tag_template: "v{{ .Version }}"
release:
extra_files:
- glob: "dist/*.sig"
name_template: "{{ .ArtifactName }}.sig"
- glob: "docs/*.pdf"
"#;
let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
let release = config.crates[0].release.as_ref().unwrap();
let files = release.extra_files.as_ref().unwrap();
assert_eq!(files.len(), 2);
assert_eq!(files[0].glob(), "dist/*.sig");
assert_eq!(files[0].name_template(), Some("{{ .ArtifactName }}.sig"));
assert_eq!(files[1].glob(), "docs/*.pdf");
assert_eq!(files[1].name_template(), None);
}
#[test]
fn test_release_extra_files_mixed() {
let yaml = r#"
project_name: test
crates:
- name: a
path: "."
tag_template: "v{{ .Version }}"
release:
extra_files:
- "dist/*.sig"
- glob: "docs/*.pdf"
name_template: "{{ .ArtifactName }}"
"#;
let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
let release = config.crates[0].release.as_ref().unwrap();
let files = release.extra_files.as_ref().unwrap();
assert_eq!(files.len(), 2);
assert_eq!(files[0], ExtraFileSpec::Glob("dist/*.sig".to_string()));
assert_eq!(files[1].glob(), "docs/*.pdf");
}
#[test]
fn test_release_extra_files_omitted() {
let yaml = r#"
project_name: test
crates:
- name: a
path: "."
tag_template: "v{{ .Version }}"
release:
draft: true
"#;
let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
let release = config.crates[0].release.as_ref().unwrap();
assert_eq!(release.extra_files, None);
}
#[test]
fn test_release_templated_extra_files_parsed() {
let yaml = r#"
project_name: test
crates:
- name: a
path: "."
tag_template: "v{{ .Version }}"
release:
templated_extra_files:
- src: LICENSE.tpl
dst: LICENSE.txt
- src: README.md.tpl
"#;
let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
let release = config.crates[0].release.as_ref().unwrap();
let tpl = release.templated_extra_files.as_ref().unwrap();
assert_eq!(tpl.len(), 2);
assert_eq!(tpl[0].src, "LICENSE.tpl");
assert_eq!(tpl[0].dst.as_deref(), Some("LICENSE.txt"));
assert_eq!(tpl[1].src, "README.md.tpl");
assert_eq!(tpl[1].dst, None);
}
#[test]
fn test_release_templated_extra_files_defaults_to_none() {
let yaml = r#"
project_name: test
crates:
- name: a
path: "."
tag_template: "v{{ .Version }}"
release:
draft: true
"#;
let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
let release = config.crates[0].release.as_ref().unwrap();
assert_eq!(release.templated_extra_files, None);
}
#[test]
fn test_checksum_templated_extra_files_parsed() {
let yaml = r#"
name_template: "checksums.txt"
templated_extra_files:
- src: "notes.tpl"
dst: "RELEASE_NOTES.txt"
"#;
let cfg: ChecksumConfig = serde_yaml_ng::from_str(yaml).unwrap();
let tpl = cfg.templated_extra_files.as_ref().unwrap();
assert_eq!(tpl.len(), 1);
assert_eq!(tpl[0].src, "notes.tpl");
assert_eq!(tpl[0].dst.as_deref(), Some("RELEASE_NOTES.txt"));
}
#[test]
fn test_release_skip_upload() {
let yaml = r#"
project_name: test
crates:
- name: a
path: "."
tag_template: "v{{ .Version }}"
release:
skip_upload: true
"#;
let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
let release = config.crates[0].release.as_ref().unwrap();
assert_eq!(release.skip_upload, Some(StringOrBool::Bool(true)));
}
#[test]
fn test_release_skip_upload_false() {
let yaml = r#"
project_name: test
crates:
- name: a
path: "."
tag_template: "v{{ .Version }}"
release:
skip_upload: false
"#;
let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
let release = config.crates[0].release.as_ref().unwrap();
assert_eq!(release.skip_upload, Some(StringOrBool::Bool(false)));
}
#[test]
fn test_release_skip_upload_auto() {
let yaml = r#"
project_name: test
crates:
- name: a
path: "."
tag_template: "v{{ .Version }}"
release:
skip_upload: "auto"
"#;
let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
let release = config.crates[0].release.as_ref().unwrap();
assert_eq!(
release.skip_upload,
Some(StringOrBool::String("auto".to_string()))
);
}
#[test]
fn test_release_replace_existing_draft() {
let yaml = r#"
project_name: test
crates:
- name: a
path: "."
tag_template: "v{{ .Version }}"
release:
replace_existing_draft: true
"#;
let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
let release = config.crates[0].release.as_ref().unwrap();
assert_eq!(release.replace_existing_draft, Some(true));
}
#[test]
fn test_release_replace_existing_artifacts() {
let yaml = r#"
project_name: test
crates:
- name: a
path: "."
tag_template: "v{{ .Version }}"
release:
replace_existing_artifacts: true
"#;
let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
let release = config.crates[0].release.as_ref().unwrap();
assert_eq!(release.replace_existing_artifacts, Some(true));
}
#[test]
fn test_release_tag_override_parsed() {
let yaml = r#"
project_name: test
crates:
- name: a
path: "."
tag_template: "myapp/v{{ .Version }}"
release:
tag: "v{{ .Version }}"
"#;
let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
let release = config.crates[0].release.as_ref().unwrap();
assert_eq!(release.tag, Some("v{{ .Version }}".to_string()));
}
#[test]
fn test_release_tag_override_omitted() {
let yaml = r#"
project_name: test
crates:
- name: a
path: "."
tag_template: "v{{ .Version }}"
release:
draft: false
"#;
let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
let release = config.crates[0].release.as_ref().unwrap();
assert_eq!(release.tag, None);
}
#[test]
fn test_release_all_new_fields() {
let yaml = r##"
project_name: test
crates:
- name: a
path: "."
tag_template: "v{{ .Version }}"
release:
github:
owner: myorg
name: myrepo
draft: true
make_latest: auto
header: "# Release Notes"
footer: "Thank you!"
extra_files:
- "dist/extra.zip"
skip_upload: false
replace_existing_draft: true
replace_existing_artifacts: false
target_commitish: main
discussion_category_name: Announcements
include_meta: true
use_existing_draft: false
tag: "v{{ .Version }}"
"##;
let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
let release = config.crates[0].release.as_ref().unwrap();
assert_eq!(
release.header,
Some(ContentSource::Inline("# Release Notes".to_string()))
);
assert_eq!(
release.footer,
Some(ContentSource::Inline("Thank you!".to_string()))
);
assert_eq!(
release.extra_files.as_ref().unwrap(),
&[ExtraFileSpec::Glob("dist/extra.zip".to_string())]
);
assert_eq!(release.skip_upload, Some(StringOrBool::Bool(false)));
assert_eq!(release.replace_existing_draft, Some(true));
assert_eq!(release.replace_existing_artifacts, Some(false));
assert_eq!(release.make_latest, Some(MakeLatestConfig::Auto));
assert_eq!(release.target_commitish, Some("main".to_string()));
assert_eq!(
release.discussion_category_name,
Some("Announcements".to_string())
);
assert_eq!(release.include_meta, Some(true));
assert_eq!(release.use_existing_draft, Some(false));
assert_eq!(release.tag, Some("v{{ .Version }}".to_string()));
}
#[test]
fn test_signs_single_object_backward_compat() {
let yaml = r#"
project_name: test
sign:
artifacts: all
cmd: gpg
args:
- "--detach-sig"
crates:
- name: a
path: "."
tag_template: "v{{ .Version }}"
"#;
let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
assert_eq!(config.signs.len(), 1);
assert_eq!(config.signs[0].artifacts, Some("all".to_string()));
assert_eq!(config.signs[0].cmd, Some("gpg".to_string()));
assert_eq!(config.signs[0].args.as_ref().unwrap().len(), 1);
}
#[test]
fn test_signs_array_format() {
let yaml = r#"
project_name: test
signs:
- id: gpg-sign
artifacts: checksum
cmd: gpg
args:
- "--detach-sig"
- id: cosign-sign
artifacts: binary
cmd: cosign
args:
- "sign"
crates:
- name: a
path: "."
tag_template: "v{{ .Version }}"
"#;
let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
assert_eq!(config.signs.len(), 2);
assert_eq!(config.signs[0].id, Some("gpg-sign".to_string()));
assert_eq!(config.signs[0].artifacts, Some("checksum".to_string()));
assert_eq!(config.signs[1].id, Some("cosign-sign".to_string()));
assert_eq!(config.signs[1].artifacts, Some("binary".to_string()));
}
#[test]
fn test_signs_omitted_is_empty() {
let yaml = r#"
project_name: test
crates:
- name: a
path: "."
tag_template: "v{{ .Version }}"
"#;
let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
assert!(config.signs.is_empty());
}
#[test]
fn test_signs_new_fields() {
let yaml = r#"
project_name: test
signs:
- id: my-signer
artifacts: archive
cmd: gpg
args:
- "--detach-sig"
signature: "{{ .Artifact }}.asc"
stdin: "my-passphrase"
ids:
- my-archive
- my-binary
crates:
- name: a
path: "."
tag_template: "v{{ .Version }}"
"#;
let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
assert_eq!(config.signs.len(), 1);
let sign = &config.signs[0];
assert_eq!(sign.id, Some("my-signer".to_string()));
assert_eq!(sign.artifacts, Some("archive".to_string()));
assert_eq!(sign.signature, Some("{{ .Artifact }}.asc".to_string()));
assert_eq!(sign.stdin, Some("my-passphrase".to_string()));
assert_eq!(sign.ids.as_ref().unwrap().len(), 2);
assert_eq!(sign.ids.as_ref().unwrap()[0], "my-archive");
}
#[test]
fn test_signs_stdin_file_field() {
let yaml = r#"
project_name: test
signs:
- artifacts: all
cmd: gpg
stdin_file: "/path/to/passphrase.txt"
crates:
- name: a
path: "."
tag_template: "v{{ .Version }}"
"#;
let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
assert_eq!(config.signs.len(), 1);
assert_eq!(
config.signs[0].stdin_file,
Some("/path/to/passphrase.txt".to_string())
);
}
#[test]
fn test_signs_single_object_with_new_fields() {
let yaml = r#"
project_name: test
sign:
id: default
artifacts: package
cmd: gpg
signature: "{{ .Artifact }}.sig"
stdin: "pass"
ids:
- pkg-id
crates:
- name: a
path: "."
tag_template: "v{{ .Version }}"
"#;
let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
assert_eq!(config.signs.len(), 1);
let sign = &config.signs[0];
assert_eq!(sign.id, Some("default".to_string()));
assert_eq!(sign.artifacts, Some("package".to_string()));
assert_eq!(sign.signature, Some("{{ .Artifact }}.sig".to_string()));
assert_eq!(sign.stdin, Some("pass".to_string()));
assert_eq!(sign.ids.as_ref().unwrap(), &["pkg-id"]);
}
#[test]
fn test_signs_toml_single_object() {
let toml_str = r#"
project_name = "test"
[sign]
artifacts = "checksum"
cmd = "gpg"
[[crates]]
name = "a"
path = "."
tag_template = "v{{ .Version }}"
"#;
let config: Config = toml::from_str(toml_str).unwrap();
assert_eq!(config.signs.len(), 1);
assert_eq!(config.signs[0].artifacts, Some("checksum".to_string()));
}
#[test]
fn test_signs_toml_array() {
let toml_str = r#"
project_name = "test"
[[signs]]
id = "first"
artifacts = "all"
cmd = "gpg"
[[signs]]
id = "second"
artifacts = "binary"
cmd = "cosign"
[[crates]]
name = "a"
path = "."
tag_template = "v{{ .Version }}"
"#;
let config: Config = toml::from_str(toml_str).unwrap();
assert_eq!(config.signs.len(), 2);
assert_eq!(config.signs[0].id, Some("first".to_string()));
assert_eq!(config.signs[1].id, Some("second".to_string()));
}
#[test]
fn test_signs_default_config_has_empty_signs() {
let config = Config::default();
assert!(config.signs.is_empty());
}
#[test]
fn test_report_sizes_true() {
let yaml = r#"
project_name: test
report_sizes: true
crates:
- name: a
path: "."
tag_template: "v{{ .Version }}"
"#;
let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
assert_eq!(config.report_sizes, Some(true));
}
#[test]
fn test_report_sizes_false() {
let yaml = r#"
project_name: test
report_sizes: false
crates:
- name: a
path: "."
tag_template: "v{{ .Version }}"
"#;
let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
assert_eq!(config.report_sizes, Some(false));
}
#[test]
fn test_report_sizes_omitted() {
let yaml = r#"
project_name: test
crates:
- name: a
path: "."
tag_template: "v{{ .Version }}"
"#;
let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
assert_eq!(config.report_sizes, None);
}
#[test]
fn test_env_field_parsed() {
let yaml = r#"
project_name: test
env:
MY_VAR: hello
DEPLOY_ENV: staging
crates:
- name: a
path: "."
tag_template: "v{{ .Version }}"
"#;
let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
let env = config.env.as_ref().unwrap();
assert_eq!(env.get("MY_VAR").unwrap(), "hello");
assert_eq!(env.get("DEPLOY_ENV").unwrap(), "staging");
}
#[test]
fn test_env_field_omitted() {
let yaml = r#"
project_name: test
crates:
- name: a
path: "."
tag_template: "v{{ .Version }}"
"#;
let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
assert_eq!(config.env, None);
}
#[test]
fn test_env_field_toml() {
let toml_str = r#"
project_name = "test"
[env]
API_KEY = "secret123"
STAGE = "prod"
[[crates]]
name = "a"
path = "."
tag_template = "v{{ .Version }}"
"#;
let config: Config = toml::from_str(toml_str).unwrap();
let env = config.env.as_ref().unwrap();
assert_eq!(env.get("API_KEY").unwrap(), "secret123");
assert_eq!(env.get("STAGE").unwrap(), "prod");
}
#[test]
fn test_env_list_form_toml() {
let toml_str = r#"
project_name = "test"
env = ["MY_VAR=hello", "STAGE=prod"]
crates = []
"#;
let config: Config = toml::from_str(toml_str).unwrap();
let env = config.env.as_ref().unwrap();
assert_eq!(env.get("MY_VAR").unwrap(), "hello");
assert_eq!(env.get("STAGE").unwrap(), "prod");
}
#[test]
fn test_env_list_form_parsed() {
let yaml = r#"
project_name: test
env:
- MY_VAR=hello
- DEPLOY_ENV=staging
crates: []
"#;
let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
let env = config.env.as_ref().unwrap();
assert_eq!(env.get("MY_VAR").unwrap(), "hello");
assert_eq!(env.get("DEPLOY_ENV").unwrap(), "staging");
}
#[test]
fn test_env_list_form_with_template_expressions() {
let yaml = r#"
project_name: test
env:
- "MY_VERSION={{ .Tag }}"
- "BUILD_DATE={{ .Date }}"
crates: []
"#;
let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
let env = config.env.as_ref().unwrap();
assert_eq!(env.get("MY_VERSION").unwrap(), "{{ .Tag }}");
assert_eq!(env.get("BUILD_DATE").unwrap(), "{{ .Date }}");
}
#[test]
fn test_env_list_form_value_with_equals() {
let yaml = r#"
project_name: test
env:
- "LDFLAGS=-X main.version=1.0.0"
crates: []
"#;
let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
let env = config.env.as_ref().unwrap();
assert_eq!(
env.get("LDFLAGS").unwrap(),
"-X main.version=1.0.0",
"only first = should split key from value"
);
}
#[test]
fn test_env_list_form_empty_value() {
let yaml = r#"
project_name: test
env:
- "EMPTY_VAR="
crates: []
"#;
let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
let env = config.env.as_ref().unwrap();
assert_eq!(env.get("EMPTY_VAR").unwrap(), "");
}
#[test]
fn test_env_list_form_no_equals_is_error() {
let yaml = r#"
project_name: test
env:
- "NO_EQUALS"
crates: []
"#;
let result: Result<Config, _> = serde_yaml_ng::from_str(yaml);
assert!(result.is_err(), "list entries without = should be rejected");
let err = result.unwrap_err().to_string();
assert!(
err.contains("KEY=VALUE"),
"error should mention KEY=VALUE format, got: {}",
err
);
}
#[test]
fn test_env_list_form_empty_key_is_error() {
let yaml = r#"
project_name: test
env:
- "=orphan_value"
crates: []
"#;
let result: Result<Config, _> = serde_yaml_ng::from_str(yaml);
assert!(
result.is_err(),
"list entries with empty key should be rejected"
);
let err = result.unwrap_err().to_string();
assert!(
err.contains("empty key"),
"error should mention empty key, got: {}",
err
);
}
#[test]
fn test_env_list_form_last_wins_on_duplicates() {
let yaml = r#"
project_name: test
env:
- "DUPED=first"
- "DUPED=second"
crates: []
"#;
let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
let env = config.env.as_ref().unwrap();
assert_eq!(
env.get("DUPED").unwrap(),
"second",
"later entries should override earlier ones"
);
}
#[test]
fn test_workspace_env_list_form() {
let yaml = r#"
project_name: test
crates: []
workspaces:
- name: ws1
crates: []
env:
- "WS_VAR=from-workspace"
- "WS_BUILD={{ .Tag }}"
"#;
let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
let ws = &config.workspaces.as_ref().unwrap()[0];
let env = ws.env.as_ref().unwrap();
assert_eq!(env.get("WS_VAR").unwrap(), "from-workspace");
assert_eq!(env.get("WS_BUILD").unwrap(), "{{ .Tag }}");
}
#[test]
fn test_malformed_yaml_syntax_error() {
let yaml = r#"
project_name: test
crates:
- name: a
path: "."
tag_template: "v{{ .Version }}"
invalid_indentation
this_is_broken: [
"#;
let result: Result<Config, _> = serde_yaml_ng::from_str(yaml);
assert!(result.is_err(), "malformed YAML should fail to parse");
let err = result.unwrap_err().to_string();
assert!(!err.is_empty(), "error message should not be empty");
}
#[test]
fn test_type_mismatch_string_where_array_expected() {
let yaml = r#"
project_name: test
crates: "this should be an array"
"#;
let result: Result<Config, _> = serde_yaml_ng::from_str(yaml);
assert!(result.is_err(), "string where array expected should fail");
let err = result.unwrap_err().to_string();
assert!(
err.contains("invalid type") || err.contains("expected a sequence"),
"error should mention type mismatch, got: {err}"
);
}
#[test]
fn test_type_mismatch_object_where_string_expected() {
let yaml = r#"
project_name:
nested: object
another: field
crates:
- name: a
path: "."
tag_template: "v{{ .Version }}"
"#;
let result: Result<Config, _> = serde_yaml_ng::from_str(yaml);
assert!(
result.is_err(),
"mapping where string expected should fail to parse"
);
let err = result.unwrap_err().to_string();
assert!(
err.contains("invalid type") || err.contains("expected a string"),
"error should mention type mismatch, got: {err}"
);
}
#[test]
fn test_type_mismatch_bool_where_array_expected_for_targets() {
let yaml = r#"
project_name: test
defaults:
targets: true
crates:
- name: a
path: "."
tag_template: "v{{ .Version }}"
"#;
let result: Result<Config, _> = serde_yaml_ng::from_str(yaml);
assert!(
result.is_err(),
"bool where array expected for targets should fail"
);
let err = result.unwrap_err().to_string();
assert!(
err.contains("invalid type")
|| err.contains("expected a sequence")
|| err.contains("targets"),
"error should mention type mismatch for targets, got: {err}"
);
}
#[test]
fn test_invalid_cross_strategy_value() {
let yaml = r#"
project_name: test
defaults:
cross: invalid_strategy
crates:
- name: a
path: "."
tag_template: "v{{ .Version }}"
"#;
let result: Result<Config, _> = serde_yaml_ng::from_str(yaml);
assert!(
result.is_err(),
"invalid cross strategy should fail to parse"
);
let err = result.unwrap_err().to_string();
assert!(
err.contains("unknown variant") || err.contains("invalid_strategy"),
"error should mention the invalid variant, got: {err}"
);
}
#[test]
fn test_prerelease_invalid_string_value() {
let yaml = r#"
project_name: test
crates:
- name: a
path: "."
tag_template: "v{{ .Version }}"
release:
prerelease: "always"
"#;
let result: Result<Config, _> = serde_yaml_ng::from_str(yaml);
assert!(
result.is_err(),
"prerelease: 'always' should fail (only 'auto' or bool accepted)"
);
let err = result.unwrap_err().to_string();
assert!(
err.contains("auto") || err.contains("always"),
"error should mention expected values, got: {err}"
);
}
#[test]
fn test_archives_true_is_invalid() {
let yaml = r#"
project_name: test
crates:
- name: a
path: "."
tag_template: "v{{ .Version }}"
archives: true
"#;
let result: Result<Config, _> = serde_yaml_ng::from_str(yaml);
assert!(
result.is_err(),
"archives: true should be rejected (only false or array accepted)"
);
let err = result.unwrap_err().to_string();
assert!(
err.contains("true is not valid") || err.contains("false or a list"),
"error should explain valid archives values, got: {err}"
);
}
#[test]
fn test_completely_empty_yaml() {
let yaml = "";
let result: Result<Config, _> = serde_yaml_ng::from_str(yaml);
let config =
result.unwrap_or_else(|e| panic!("empty YAML should parse to Config defaults: {e}"));
assert!(
config.project_name.is_empty(),
"default project_name should be empty"
);
assert!(config.crates.is_empty(), "default crates should be empty");
assert_eq!(
config.dist,
std::path::PathBuf::from("./dist"),
"default dist should be ./dist"
);
}
#[test]
fn test_binstall_config_parsed() {
let yaml = r#"
project_name: test
crates:
- name: myapp
path: "."
tag_template: "v{{ .Version }}"
binstall:
enabled: true
pkg_url: "https://example.com/{{ .Version }}/{ target }"
bin_dir: "{ bin }{ binary-ext }"
pkg_fmt: tgz
"#;
let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
let bs = config.crates[0].binstall.as_ref().unwrap();
assert_eq!(bs.enabled, Some(true));
assert_eq!(
bs.pkg_url,
Some("https://example.com/{{ .Version }}/{ target }".to_string())
);
assert_eq!(bs.bin_dir, Some("{ bin }{ binary-ext }".to_string()));
assert_eq!(bs.pkg_fmt, Some("tgz".to_string()));
}
#[test]
fn test_binstall_config_omitted() {
let yaml = r#"
project_name: test
crates:
- name: myapp
path: "."
tag_template: "v{{ .Version }}"
"#;
let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
assert!(config.crates[0].binstall.is_none());
}
#[test]
fn test_binstall_config_partial() {
let yaml = r#"
project_name: test
crates:
- name: myapp
path: "."
tag_template: "v{{ .Version }}"
binstall:
enabled: true
"#;
let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
let bs = config.crates[0].binstall.as_ref().unwrap();
assert_eq!(bs.enabled, Some(true));
assert_eq!(bs.pkg_url, None);
assert_eq!(bs.bin_dir, None);
assert_eq!(bs.pkg_fmt, None);
}
#[test]
fn test_version_sync_config_parsed() {
let yaml = r#"
project_name: test
crates:
- name: myapp
path: "."
tag_template: "v{{ .Version }}"
version_sync:
enabled: true
mode: tag
"#;
let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
let vs = config.crates[0].version_sync.as_ref().unwrap();
assert_eq!(vs.enabled, Some(true));
assert_eq!(vs.mode, Some("tag".to_string()));
}
#[test]
fn test_version_sync_config_explicit_mode() {
let yaml = r#"
project_name: test
crates:
- name: myapp
path: "."
tag_template: "v{{ .Version }}"
version_sync:
enabled: true
mode: explicit
"#;
let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
let vs = config.crates[0].version_sync.as_ref().unwrap();
assert_eq!(vs.mode, Some("explicit".to_string()));
}
#[test]
fn test_version_sync_config_omitted() {
let yaml = r#"
project_name: test
crates:
- name: myapp
path: "."
tag_template: "v{{ .Version }}"
"#;
let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
assert!(config.crates[0].version_sync.is_none());
}
#[test]
fn test_binstall_and_version_sync_together() {
let yaml = r#"
project_name: test
crates:
- name: myapp
path: "."
tag_template: "v{{ .Version }}"
binstall:
enabled: true
pkg_fmt: zip
version_sync:
enabled: true
mode: tag
"#;
let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
assert!(config.crates[0].binstall.is_some());
assert!(config.crates[0].version_sync.is_some());
}
#[test]
fn test_binstall_config_toml() {
let toml_str = r#"
project_name = "test"
[[crates]]
name = "myapp"
path = "."
tag_template = "v{{ .Version }}"
[crates.binstall]
enabled = true
pkg_url = "https://example.com"
pkg_fmt = "tgz"
"#;
let config: Config = toml::from_str(toml_str).unwrap();
let bs = config.crates[0].binstall.as_ref().unwrap();
assert_eq!(bs.enabled, Some(true));
assert_eq!(bs.pkg_url, Some("https://example.com".to_string()));
}
#[test]
fn test_version_sync_config_toml() {
let toml_str = r#"
project_name = "test"
[[crates]]
name = "myapp"
path = "."
tag_template = "v{{ .Version }}"
[crates.version_sync]
enabled = true
mode = "tag"
"#;
let config: Config = toml::from_str(toml_str).unwrap();
let vs = config.crates[0].version_sync.as_ref().unwrap();
assert_eq!(vs.enabled, Some(true));
assert_eq!(vs.mode, Some("tag".to_string()));
}
#[test]
fn test_crate_config_default_has_none_binstall_version_sync() {
let config = CrateConfig::default();
assert!(config.binstall.is_none());
assert!(config.version_sync.is_none());
}
#[test]
fn test_unknown_top_level_fields_rejected() {
let yaml = r#"
project_name: test
unknown_top_level_field: "this should be rejected"
crates:
- name: a
path: "."
tag_template: "v{{ .Version }}"
"#;
let result: Result<Config, _> = serde_yaml_ng::from_str(yaml);
assert!(
result.is_err(),
"unknown top-level fields should be rejected"
);
assert!(
result.unwrap_err().to_string().contains("unknown field"),
"error should mention unknown field"
);
}
#[test]
fn test_unknown_crate_level_fields_ignored() {
let yaml = r#"
project_name: test
crates:
- name: a
path: "."
tag_template: "v{{ .Version }}"
nonexistent_field: true
something_else: "hello"
"#;
let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
assert_eq!(config.crates[0].name, "a");
}
#[test]
fn test_unknown_nested_fields_ignored() {
let yaml = r#"
project_name: test
defaults:
targets:
- x86_64-unknown-linux-gnu
unknown_default_field: "ignored"
changelog:
sort: asc
mystery_option: true
crates:
- name: a
path: "."
tag_template: "v{{ .Version }}"
checksum:
algorithm: sha256
future_field: "ignored"
"#;
let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
assert_eq!(
config
.defaults
.as_ref()
.unwrap()
.targets
.as_ref()
.unwrap()
.len(),
1
);
assert_eq!(
config.changelog.as_ref().unwrap().sort,
Some("asc".to_string())
);
assert_eq!(
config.crates[0].checksum.as_ref().unwrap().algorithm,
Some("sha256".to_string())
);
}
#[test]
fn test_build_config_reproducible_true() {
let yaml = r#"
project_name: test
crates:
- name: myapp
path: "."
tag_template: "v{{ .Version }}"
builds:
- binary: myapp
reproducible: true
"#;
let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
let build = &config.crates[0].builds.as_ref().unwrap()[0];
assert_eq!(build.reproducible, Some(true));
}
#[test]
fn test_build_config_reproducible_false() {
let yaml = r#"
project_name: test
crates:
- name: myapp
path: "."
tag_template: "v{{ .Version }}"
builds:
- binary: myapp
reproducible: false
"#;
let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
let build = &config.crates[0].builds.as_ref().unwrap()[0];
assert_eq!(build.reproducible, Some(false));
}
#[test]
fn test_build_config_reproducible_omitted() {
let yaml = r#"
project_name: test
crates:
- name: myapp
path: "."
tag_template: "v{{ .Version }}"
builds:
- binary: myapp
"#;
let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
let build = &config.crates[0].builds.as_ref().unwrap()[0];
assert_eq!(build.reproducible, None);
}
#[test]
fn test_workspace_config_parses() {
let yaml = r#"
project_name: monorepo
crates: []
workspaces:
- name: frontend
crates:
- name: frontend-app
path: "apps/frontend"
tag_template: "frontend-v{{ .Version }}"
changelog:
sort: asc
- name: backend
crates:
- name: backend-api
path: "apps/backend"
tag_template: "backend-v{{ .Version }}"
- name: backend-worker
path: "apps/worker"
tag_template: "worker-v{{ .Version }}"
"#;
let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
let workspaces = config.workspaces.as_ref().unwrap();
assert_eq!(workspaces.len(), 2);
assert_eq!(workspaces[0].name, "frontend");
assert_eq!(workspaces[0].crates.len(), 1);
assert_eq!(workspaces[0].crates[0].name, "frontend-app");
assert!(workspaces[0].changelog.is_some());
assert_eq!(workspaces[1].name, "backend");
assert_eq!(workspaces[1].crates.len(), 2);
}
#[test]
fn test_workspace_config_with_signs_and_hooks() {
let yaml = r#"
project_name: monorepo
crates: []
workspaces:
- name: myws
crates:
- name: mylib
path: "."
tag_template: "v{{ .Version }}"
signs:
- artifacts: all
cmd: gpg
before:
hooks:
- echo before
after:
hooks:
- echo after
env:
MY_VAR: hello
"#;
let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
let ws = &config.workspaces.as_ref().unwrap()[0];
assert_eq!(ws.name, "myws");
assert_eq!(ws.signs.len(), 1);
assert!(ws.before.is_some());
assert!(ws.after.is_some());
assert_eq!(ws.env.as_ref().unwrap().get("MY_VAR").unwrap(), "hello");
}
#[test]
fn test_workspace_config_omitted() {
let yaml = r#"
project_name: simple
crates:
- name: myapp
path: "."
tag_template: "v{{ .Version }}"
"#;
let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
assert!(config.workspaces.is_none());
}
#[test]
fn test_workspace_config_empty_array() {
let yaml = r#"
project_name: test
crates: []
workspaces: []
"#;
let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
let workspaces = config.workspaces.as_ref().unwrap();
assert!(workspaces.is_empty());
}
#[test]
fn test_chocolatey_config_yaml() {
let yaml = r#"
project_name: test
crates:
- name: mytool
path: "."
tag_template: "v{{ .Version }}"
publish:
chocolatey:
project_repo:
owner: myorg
name: mytool
description: "A great tool"
license: MIT
tags:
- cli
- tool
authors: "Test Author"
project_url: "https://github.com/myorg/mytool"
icon_url: "https://example.com/icon.png"
"#;
let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
let choco = config.crates[0]
.publish
.as_ref()
.unwrap()
.chocolatey
.as_ref()
.unwrap();
let repo = choco.project_repo.as_ref().unwrap();
assert_eq!(repo.owner, "myorg");
assert_eq!(repo.name, "mytool");
assert_eq!(choco.description, Some("A great tool".to_string()));
assert_eq!(choco.license, Some("MIT".to_string()));
assert_eq!(
choco.tags,
Some(vec!["cli".to_string(), "tool".to_string()])
);
assert_eq!(choco.authors, Some("Test Author".to_string()));
assert_eq!(
choco.project_url,
Some("https://github.com/myorg/mytool".to_string())
);
assert_eq!(
choco.icon_url,
Some("https://example.com/icon.png".to_string())
);
}
#[test]
fn test_chocolatey_config_minimal() {
let yaml = r#"
project_name: test
crates:
- name: mytool
path: "."
tag_template: "v{{ .Version }}"
publish:
chocolatey:
project_repo:
owner: myorg
name: mytool
"#;
let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
let choco = config.crates[0]
.publish
.as_ref()
.unwrap()
.chocolatey
.as_ref()
.unwrap();
let repo = choco.project_repo.as_ref().unwrap();
assert_eq!(repo.owner, "myorg");
assert_eq!(repo.name, "mytool");
assert!(choco.description.is_none());
assert!(choco.license.is_none());
assert!(choco.tags.is_none());
assert!(choco.authors.is_none());
assert!(choco.project_url.is_none());
assert!(choco.icon_url.is_none());
}
#[test]
fn test_chocolatey_config_toml() {
let toml_str = r#"
project_name = "test"
[[crates]]
name = "mytool"
path = "."
tag_template = "v{{ .Version }}"
[crates.publish.chocolatey]
description = "A tool"
license = "MIT"
authors = "Author"
tags = ["cli"]
[crates.publish.chocolatey.project_repo]
owner = "org"
name = "tool"
"#;
let config: Config = toml::from_str(toml_str).unwrap();
let choco = config.crates[0]
.publish
.as_ref()
.unwrap()
.chocolatey
.as_ref()
.unwrap();
assert_eq!(choco.description, Some("A tool".to_string()));
let repo = choco.project_repo.as_ref().unwrap();
assert_eq!(repo.owner, "org");
}
#[test]
fn test_chocolatey_tags_space_separated_string() {
let yaml = r#"
project_name: test
crates:
- name: mytool
path: "."
tag_template: "v{{ .Version }}"
publish:
chocolatey:
project_repo:
owner: myorg
name: mytool
tags: "cli tool automation"
"#;
let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
let choco = config.crates[0]
.publish
.as_ref()
.unwrap()
.chocolatey
.as_ref()
.unwrap();
assert_eq!(
choco.tags,
Some(vec![
"cli".to_string(),
"tool".to_string(),
"automation".to_string()
])
);
}
#[test]
fn test_chocolatey_tags_empty_string_is_none() {
let yaml = r#"
project_name: test
crates:
- name: mytool
path: "."
tag_template: "v{{ .Version }}"
publish:
chocolatey:
project_repo:
owner: myorg
name: mytool
tags: ""
"#;
let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
let choco = config.crates[0]
.publish
.as_ref()
.unwrap()
.chocolatey
.as_ref()
.unwrap();
assert!(choco.tags.is_none());
}
#[test]
fn test_winget_config_yaml() {
let yaml = r#"
project_name: test
crates:
- name: mytool
path: "."
tag_template: "v{{ .Version }}"
publish:
winget:
manifests_repo:
owner: myorg
name: winget-pkgs
description: "A great tool"
license: MIT
package_identifier: "MyOrg.MyTool"
publisher: "My Org"
publisher_url: "https://github.com/myorg"
"#;
let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
let winget = config.crates[0]
.publish
.as_ref()
.unwrap()
.winget
.as_ref()
.unwrap();
let repo = winget.manifests_repo.as_ref().unwrap();
assert_eq!(repo.owner, "myorg");
assert_eq!(repo.name, "winget-pkgs");
assert_eq!(winget.description, Some("A great tool".to_string()));
assert_eq!(winget.license, Some("MIT".to_string()));
assert_eq!(winget.package_identifier, Some("MyOrg.MyTool".to_string()));
assert_eq!(winget.publisher, Some("My Org".to_string()));
assert_eq!(
winget.publisher_url,
Some("https://github.com/myorg".to_string())
);
}
#[test]
fn test_winget_config_minimal() {
let yaml = r#"
project_name: test
crates:
- name: mytool
path: "."
tag_template: "v{{ .Version }}"
publish:
winget:
manifests_repo:
owner: myorg
name: winget-pkgs
package_identifier: "MyOrg.MyTool"
"#;
let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
let winget = config.crates[0]
.publish
.as_ref()
.unwrap()
.winget
.as_ref()
.unwrap();
let repo = winget.manifests_repo.as_ref().unwrap();
assert_eq!(repo.owner, "myorg");
assert_eq!(repo.name, "winget-pkgs");
assert_eq!(winget.package_identifier, Some("MyOrg.MyTool".to_string()));
assert!(winget.description.is_none());
assert!(winget.license.is_none());
assert!(winget.publisher.is_none());
assert!(winget.publisher_url.is_none());
}
#[test]
fn test_winget_config_toml() {
let toml_str = r#"
project_name = "test"
[[crates]]
name = "mytool"
path = "."
tag_template = "v{{ .Version }}"
[crates.publish.winget]
description = "A tool"
license = "MIT"
package_identifier = "Org.Tool"
publisher = "Org"
[crates.publish.winget.manifests_repo]
owner = "org"
name = "winget-pkgs"
"#;
let config: Config = toml::from_str(toml_str).unwrap();
let winget = config.crates[0]
.publish
.as_ref()
.unwrap()
.winget
.as_ref()
.unwrap();
assert_eq!(winget.description, Some("A tool".to_string()));
assert_eq!(winget.package_identifier, Some("Org.Tool".to_string()));
let repo = winget.manifests_repo.as_ref().unwrap();
assert_eq!(repo.owner, "org");
}
#[test]
fn test_aur_config_yaml() {
let yaml = r#"
project_name: test
crates:
- name: mytool
path: "."
tag_template: "v{{ .Version }}"
publish:
aur:
git_url: "ssh://aur@aur.archlinux.org/mytool.git"
package_name: mytool-bin
description: "A great tool"
license: MIT
maintainers:
- "Jane Doe <jane@example.com>"
depends:
- glibc
- openssl
optdepends:
- "git: for VCS support"
conflicts:
- mytool-git
provides:
- mytool
replaces:
- old-mytool
backup:
- etc/mytool/config.toml
url: "https://github.com/org/mytool"
"#;
let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
let aur = config.crates[0]
.publish
.as_ref()
.unwrap()
.aur
.as_ref()
.unwrap();
assert_eq!(
aur.git_url,
Some("ssh://aur@aur.archlinux.org/mytool.git".to_string())
);
assert_eq!(aur.name, Some("mytool-bin".to_string()));
assert_eq!(aur.description, Some("A great tool".to_string()));
assert_eq!(aur.license, Some("MIT".to_string()));
assert_eq!(
aur.maintainers,
Some(vec!["Jane Doe <jane@example.com>".to_string()])
);
assert_eq!(
aur.depends,
Some(vec!["glibc".to_string(), "openssl".to_string()])
);
assert_eq!(
aur.optdepends,
Some(vec!["git: for VCS support".to_string()])
);
assert_eq!(aur.conflicts, Some(vec!["mytool-git".to_string()]));
assert_eq!(aur.provides, Some(vec!["mytool".to_string()]));
assert_eq!(aur.replaces, Some(vec!["old-mytool".to_string()]));
assert_eq!(aur.backup, Some(vec!["etc/mytool/config.toml".to_string()]));
assert_eq!(aur.url, Some("https://github.com/org/mytool".to_string()));
}
#[test]
fn test_aur_config_minimal() {
let yaml = r#"
project_name: test
crates:
- name: mytool
path: "."
tag_template: "v{{ .Version }}"
publish:
aur:
git_url: "ssh://aur@aur.archlinux.org/mytool.git"
"#;
let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
let aur = config.crates[0]
.publish
.as_ref()
.unwrap()
.aur
.as_ref()
.unwrap();
assert_eq!(
aur.git_url,
Some("ssh://aur@aur.archlinux.org/mytool.git".to_string())
);
assert!(aur.name.is_none());
assert!(aur.description.is_none());
assert!(aur.license.is_none());
assert!(aur.maintainers.is_none());
assert!(aur.depends.is_none());
assert!(aur.optdepends.is_none());
assert!(aur.conflicts.is_none());
assert!(aur.provides.is_none());
assert!(aur.replaces.is_none());
assert!(aur.backup.is_none());
}
#[test]
fn test_aur_config_toml() {
let toml_str = r#"
project_name = "test"
[[crates]]
name = "mytool"
path = "."
tag_template = "v{{ .Version }}"
[crates.publish.aur]
git_url = "ssh://aur@aur.archlinux.org/mytool.git"
description = "A tool"
license = "MIT"
depends = ["glibc"]
"#;
let config: Config = toml::from_str(toml_str).unwrap();
let aur = config.crates[0]
.publish
.as_ref()
.unwrap()
.aur
.as_ref()
.unwrap();
assert_eq!(
aur.git_url,
Some("ssh://aur@aur.archlinux.org/mytool.git".to_string())
);
assert_eq!(aur.description, Some("A tool".to_string()));
assert_eq!(aur.depends, Some(vec!["glibc".to_string()]));
}
#[test]
fn test_krew_config_yaml() {
let yaml = r#"
project_name: test
crates:
- name: kubectl-mytool
path: "."
tag_template: "v{{ .Version }}"
publish:
krew:
manifests_repo:
owner: myorg
name: krew-index
description: "A comprehensive kubectl plugin"
short_description: "A kubectl plugin"
homepage: "https://github.com/myorg/kubectl-mytool"
caveats: "Run 'kubectl mytool init' after installation."
"#;
let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
let krew = config.crates[0]
.publish
.as_ref()
.unwrap()
.krew
.as_ref()
.unwrap();
let repo = krew.manifests_repo.as_ref().unwrap();
assert_eq!(repo.owner, "myorg");
assert_eq!(repo.name, "krew-index");
assert_eq!(
krew.description,
Some("A comprehensive kubectl plugin".to_string())
);
assert_eq!(krew.short_description, Some("A kubectl plugin".to_string()));
assert_eq!(
krew.homepage,
Some("https://github.com/myorg/kubectl-mytool".to_string())
);
assert_eq!(
krew.caveats,
Some("Run 'kubectl mytool init' after installation.".to_string())
);
}
#[test]
fn test_krew_config_minimal() {
let yaml = r#"
project_name: test
crates:
- name: kubectl-mytool
path: "."
tag_template: "v{{ .Version }}"
publish:
krew:
manifests_repo:
owner: myorg
name: krew-index
"#;
let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
let krew = config.crates[0]
.publish
.as_ref()
.unwrap()
.krew
.as_ref()
.unwrap();
let repo = krew.manifests_repo.as_ref().unwrap();
assert_eq!(repo.owner, "myorg");
assert_eq!(repo.name, "krew-index");
assert!(krew.description.is_none());
assert!(krew.short_description.is_none());
assert!(krew.homepage.is_none());
assert!(krew.caveats.is_none());
}
#[test]
fn test_krew_config_toml() {
let toml_str = r#"
project_name = "test"
[[crates]]
name = "kubectl-mytool"
path = "."
tag_template = "v{{ .Version }}"
[crates.publish.krew]
short_description = "A kubectl plugin"
homepage = "https://example.com"
[crates.publish.krew.manifests_repo]
owner = "org"
name = "krew-index"
"#;
let config: Config = toml::from_str(toml_str).unwrap();
let krew = config.crates[0]
.publish
.as_ref()
.unwrap()
.krew
.as_ref()
.unwrap();
assert_eq!(krew.short_description, Some("A kubectl plugin".to_string()));
let repo = krew.manifests_repo.as_ref().unwrap();
assert_eq!(repo.owner, "org");
}
#[test]
fn test_all_seven_publishers_config() {
let yaml = r#"
project_name: test
crates:
- name: mytool
path: "."
tag_template: "v{{ .Version }}"
publish:
crates: true
homebrew:
tap:
owner: org
name: homebrew-tap
scoop:
bucket:
owner: org
name: scoop-bucket
chocolatey:
project_repo:
owner: org
name: mytool
winget:
manifests_repo:
owner: org
name: winget-pkgs
package_identifier: "Org.MyTool"
aur:
git_url: "ssh://aur@aur.archlinux.org/mytool.git"
krew:
manifests_repo:
owner: org
name: krew-index
"#;
let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
let publish = config.crates[0].publish.as_ref().unwrap();
assert!(publish.crates.is_some());
assert!(publish.homebrew.is_some());
assert!(publish.scoop.is_some());
assert!(publish.chocolatey.is_some());
assert!(publish.winget.is_some());
assert!(publish.aur.is_some());
assert!(publish.krew.is_some());
}
#[test]
fn test_version_field_none_is_valid() {
let config = Config::default();
assert!(validate_version(&config).is_ok());
}
#[test]
fn test_version_field_1_is_valid() {
let yaml = r#"
project_name: test
version: 1
crates: []
"#;
let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
assert_eq!(config.version, Some(1));
assert!(validate_version(&config).is_ok());
}
#[test]
fn test_version_field_2_is_valid() {
let yaml = r#"
project_name: test
version: 2
crates: []
"#;
let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
assert_eq!(config.version, Some(2));
assert!(validate_version(&config).is_ok());
}
#[test]
fn test_version_field_99_is_rejected() {
let config = Config {
version: Some(99),
..Default::default()
};
let result = validate_version(&config);
assert!(result.is_err());
assert!(
result
.unwrap_err()
.contains("unsupported config version: 99")
);
}
#[test]
fn test_env_files_list_form_parses() {
let yaml = r#"
project_name: test
env_files:
- ".env"
- ".release.env"
crates: []
"#;
let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
let env_files = config.env_files.unwrap();
let files = env_files
.as_list()
.unwrap_or_else(|| panic!("expected List variant"));
assert_eq!(files.len(), 2);
assert_eq!(files[0], ".env");
assert_eq!(files[1], ".release.env");
}
#[test]
fn test_env_files_struct_form_parses() {
let yaml = r#"
project_name: test
env_files:
github_token: "~/.config/goreleaser/github_token"
gitlab_token: "/etc/tokens/gitlab"
crates: []
"#;
let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
let env_files = config.env_files.unwrap();
let tokens = env_files
.as_token_files()
.unwrap_or_else(|| panic!("expected TokenFiles variant"));
assert_eq!(
tokens.github_token.as_deref(),
Some("~/.config/goreleaser/github_token")
);
assert_eq!(tokens.gitlab_token.as_deref(), Some("/etc/tokens/gitlab"));
assert!(tokens.gitea_token.is_none());
}
#[test]
fn test_env_files_struct_form_empty_mapping() {
let yaml = r#"
project_name: test
env_files:
gitea_token: "/tmp/gitea"
crates: []
"#;
let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
let env_files = config.env_files.unwrap();
let tokens = env_files
.as_token_files()
.unwrap_or_else(|| panic!("expected TokenFiles variant"));
assert!(tokens.github_token.is_none());
assert!(tokens.gitlab_token.is_none());
assert_eq!(tokens.gitea_token.as_deref(), Some("/tmp/gitea"));
}
#[test]
fn test_env_files_field_omitted() {
let yaml = r#"
project_name: test
crates: []
"#;
let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
assert!(config.env_files.is_none());
}
#[test]
fn test_read_token_file_reads_first_line() {
use std::io::Write;
let dir = tempfile::TempDir::new().unwrap();
let token_path = dir.path().join("github_token");
let mut f = std::fs::File::create(&token_path).unwrap();
writeln!(f, "ghp_abc123xyz").unwrap();
writeln!(f, "this line should be ignored").unwrap();
drop(f);
let result = read_token_file(&token_path.to_string_lossy()).unwrap();
assert_eq!(result, Some("ghp_abc123xyz".to_string()));
}
#[test]
fn test_read_token_file_trims_whitespace() {
use std::io::Write;
let dir = tempfile::TempDir::new().unwrap();
let token_path = dir.path().join("token");
let mut f = std::fs::File::create(&token_path).unwrap();
writeln!(f, " spaced_token ").unwrap();
drop(f);
let result = read_token_file(&token_path.to_string_lossy()).unwrap();
assert_eq!(result, Some("spaced_token".to_string()));
}
#[test]
fn test_read_token_file_nonexistent_returns_none() {
let result = read_token_file("/tmp/nonexistent_token_file_99999").unwrap();
assert!(result.is_none());
}
#[test]
fn test_read_token_file_empty_returns_none() {
let dir = tempfile::TempDir::new().unwrap();
let token_path = dir.path().join("empty_token");
std::fs::write(&token_path, "").unwrap();
let result = read_token_file(&token_path.to_string_lossy()).unwrap();
assert!(result.is_none());
}
#[test]
#[serial_test::serial]
fn test_load_token_files_reads_tokens() {
use std::io::Write;
let dir = tempfile::TempDir::new().unwrap();
let gh_path = dir.path().join("github_token");
let mut f = std::fs::File::create(&gh_path).unwrap();
writeln!(f, "ghp_test123").unwrap();
drop(f);
let gl_path = dir.path().join("gitlab_token");
let mut f = std::fs::File::create(&gl_path).unwrap();
writeln!(f, "glpat-test456").unwrap();
drop(f);
let config = EnvFilesTokenConfig {
github_token: Some(gh_path.to_string_lossy().to_string()),
gitlab_token: Some(gl_path.to_string_lossy().to_string()),
gitea_token: None, };
let orig_gh = std::env::var("GITHUB_TOKEN").ok();
let orig_gl = std::env::var("GITLAB_TOKEN").ok();
let orig_gt = std::env::var("GITEA_TOKEN").ok();
unsafe {
std::env::remove_var("GITHUB_TOKEN");
std::env::remove_var("GITLAB_TOKEN");
std::env::remove_var("GITEA_TOKEN");
}
let log = crate::log::StageLogger::new("test", crate::log::Verbosity::Normal);
let vars = load_token_files(&config, &log).unwrap();
unsafe {
if let Some(v) = orig_gh {
std::env::set_var("GITHUB_TOKEN", v);
}
if let Some(v) = orig_gl {
std::env::set_var("GITLAB_TOKEN", v);
}
if let Some(v) = orig_gt {
std::env::set_var("GITEA_TOKEN", v);
}
}
assert_eq!(vars.get("GITHUB_TOKEN").unwrap(), "ghp_test123");
assert_eq!(vars.get("GITLAB_TOKEN").unwrap(), "glpat-test456");
assert!(!vars.contains_key("GITEA_TOKEN"));
}
#[test]
#[serial_test::serial]
fn test_load_token_files_env_var_takes_precedence() {
use std::io::Write;
let dir = tempfile::TempDir::new().unwrap();
let gh_path = dir.path().join("github_token");
let mut f = std::fs::File::create(&gh_path).unwrap();
writeln!(f, "file_token").unwrap();
drop(f);
let config = EnvFilesTokenConfig {
github_token: Some(gh_path.to_string_lossy().to_string()),
gitlab_token: None,
gitea_token: None,
};
let orig = std::env::var("GITHUB_TOKEN").ok();
unsafe {
std::env::set_var("GITHUB_TOKEN", "env_token");
}
let log = crate::log::StageLogger::new("test", crate::log::Verbosity::Normal);
let vars = load_token_files(&config, &log).unwrap();
unsafe {
match orig {
Some(v) => std::env::set_var("GITHUB_TOKEN", v),
None => std::env::remove_var("GITHUB_TOKEN"),
}
}
assert!(
!vars.contains_key("GITHUB_TOKEN"),
"env var should take precedence; file should not be loaded"
);
}
#[test]
fn test_read_token_file_tilde_expansion() {
let dir = tempfile::TempDir::new().unwrap();
let token_path = dir.path().join(".config/goreleaser/github_token");
std::fs::create_dir_all(token_path.parent().unwrap()).unwrap();
std::fs::write(&token_path, "tilde_token\n").unwrap();
let orig_home = std::env::var("HOME").ok();
unsafe {
std::env::set_var("HOME", dir.path());
}
let result = read_token_file("~/.config/goreleaser/github_token").unwrap();
unsafe {
match orig_home {
Some(v) => std::env::set_var("HOME", v),
None => std::env::remove_var("HOME"),
}
}
assert_eq!(result, Some("tilde_token".to_string()));
}
#[test]
fn test_load_env_files_sets_vars() {
use std::io::Write;
let dir = tempfile::TempDir::new().unwrap();
let env_path = dir.path().join(".env");
let mut f = std::fs::File::create(&env_path).unwrap();
writeln!(f, "# comment line").unwrap();
writeln!(f).unwrap();
writeln!(f, "TEST_ANODIZER_KEY=hello_world").unwrap();
writeln!(f, "TEST_ANODIZER_QUOTED=\"with quotes\"").unwrap();
writeln!(f, "TEST_ANODIZER_SINGLE='single_quoted'").unwrap();
writeln!(f, "export TEST_ANODIZER_EXPORT=exported_val").unwrap();
drop(f);
let log = crate::log::StageLogger::new("test", crate::log::Verbosity::Normal);
let vars = load_env_files(&[env_path.to_string_lossy().to_string()], &log, false).unwrap();
assert_eq!(vars.get("TEST_ANODIZER_KEY").unwrap(), "hello_world");
assert_eq!(vars.get("TEST_ANODIZER_QUOTED").unwrap(), "with quotes");
assert_eq!(
vars.get("TEST_ANODIZER_SINGLE").unwrap(),
"single_quoted",
"single-quoted values should have quotes stripped"
);
assert_eq!(
vars.get("TEST_ANODIZER_EXPORT").unwrap(),
"exported_val",
"export prefix should be stripped"
);
}
#[test]
fn test_load_env_files_edge_cases() {
use std::io::Write;
let dir = tempfile::TempDir::new().unwrap();
let env_path = dir.path().join(".env-edge");
let mut f = std::fs::File::create(&env_path).unwrap();
writeln!(f, "TEST_ANODIZER_SINGLEQ=\"").unwrap();
writeln!(f, "=orphan_value").unwrap();
writeln!(f, "NO_EQUALS_HERE").unwrap();
drop(f);
let log = crate::log::StageLogger::new("test", crate::log::Verbosity::Normal);
let vars = load_env_files(&[env_path.to_string_lossy().to_string()], &log, false).unwrap();
assert_eq!(vars.get("TEST_ANODIZER_SINGLEQ").unwrap(), "\"");
assert!(!vars.contains_key(""), "empty key should be skipped");
}
#[test]
fn test_load_env_files_nonexistent_skips_with_warning() {
let log = crate::log::StageLogger::new("test", crate::log::Verbosity::Normal);
let result = load_env_files(
&["/tmp/nonexistent_anodizer_env_file_12345".to_string()],
&log,
false,
);
assert!(result.is_ok());
assert!(result.unwrap().is_empty());
}
#[test]
fn test_load_env_files_nonexistent_strict_mode_errors() {
let log = crate::log::StageLogger::new("test", crate::log::Verbosity::Normal);
let result = load_env_files(
&["/tmp/nonexistent_anodizer_env_file_12345".to_string()],
&log,
true,
);
assert!(result.is_err());
assert!(result.unwrap_err().contains("strict mode"));
}
#[test]
fn test_env_files_list_form_toml() {
#[derive(Deserialize)]
struct Wrapper {
env_files: EnvFilesConfig,
}
let toml_str = r#"env_files = [".env", ".env.local"]"#;
let wrapper: Wrapper = toml::from_str(toml_str).unwrap();
let files = wrapper
.env_files
.as_list()
.unwrap_or_else(|| panic!("expected List variant"));
assert_eq!(files.len(), 2);
assert_eq!(files[0], ".env");
assert_eq!(files[1], ".env.local");
}
#[test]
fn test_env_files_struct_form_toml() {
#[derive(Deserialize)]
struct Wrapper {
env_files: EnvFilesConfig,
}
let toml_str = r#"
[env_files]
github_token = "~/.config/goreleaser/github_token"
gitlab_token = "/etc/tokens/gitlab"
"#;
let wrapper: Wrapper = toml::from_str(toml_str).unwrap();
let tokens = wrapper
.env_files
.as_token_files()
.unwrap_or_else(|| panic!("expected TokenFiles variant"));
assert_eq!(
tokens.github_token.as_deref(),
Some("~/.config/goreleaser/github_token")
);
assert_eq!(tokens.gitlab_token.as_deref(), Some("/etc/tokens/gitlab"));
assert!(tokens.gitea_token.is_none());
}
#[test]
fn test_env_files_token_config_toml_rejects_unknown_fields() {
let toml_str = r#"github_tokne = "~/.config/goreleaser/github_token""#;
let result = toml::from_str::<EnvFilesTokenConfig>(toml_str);
assert!(
result.is_err(),
"EnvFilesTokenConfig should reject unknown fields like 'github_tokne'"
);
}
#[test]
fn test_build_ignore_parses() {
let yaml = r#"
project_name: test
defaults:
targets:
- x86_64-unknown-linux-gnu
- aarch64-unknown-linux-gnu
ignore:
- os: windows
arch: arm64
- os: linux
arch: "386"
crates: []
"#;
let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
let defaults = config.defaults.unwrap();
let ignores = defaults.ignore.unwrap();
assert_eq!(ignores.len(), 2);
assert_eq!(ignores[0].os, "windows");
assert_eq!(ignores[0].arch, "arm64");
assert_eq!(ignores[1].os, "linux");
assert_eq!(ignores[1].arch, "386");
}
#[test]
fn test_build_ignore_omitted() {
let yaml = r#"
project_name: test
defaults:
targets:
- x86_64-unknown-linux-gnu
crates: []
"#;
let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
let defaults = config.defaults.unwrap();
assert!(defaults.ignore.is_none());
}
#[test]
fn test_build_override_parses() {
let yaml = r#"
project_name: test
defaults:
overrides:
- targets:
- "x86_64-*"
features:
- simd
flags: "--release"
env:
CC: gcc
- targets:
- "*-apple-darwin"
features:
- metal
crates: []
"#;
let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
let defaults = config.defaults.unwrap();
let overrides = defaults.overrides.unwrap();
assert_eq!(overrides.len(), 2);
assert_eq!(overrides[0].targets, vec!["x86_64-*"]);
assert_eq!(overrides[0].features, Some(vec!["simd".to_string()]));
assert_eq!(overrides[0].flags, Some("--release".to_string()));
assert_eq!(overrides[0].env.as_ref().unwrap().get("CC").unwrap(), "gcc");
assert_eq!(overrides[1].targets, vec!["*-apple-darwin"]);
assert_eq!(overrides[1].features, Some(vec!["metal".to_string()]));
assert!(overrides[1].env.is_none());
}
#[test]
fn test_build_override_omitted() {
let yaml = r#"
project_name: test
defaults:
targets:
- x86_64-unknown-linux-gnu
crates: []
"#;
let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
let defaults = config.defaults.unwrap();
assert!(defaults.overrides.is_none());
}
#[test]
fn test_json_schema_generation() {
let schema = schemars::schema_for!(Config);
let json = serde_json::to_string_pretty(&schema).unwrap();
assert!(json.contains("project_name"));
assert!(json.contains("env_files"));
assert!(json.contains("version"));
assert!(json.contains("BuildIgnore"));
assert!(json.contains("BuildOverride"));
}
#[test]
fn test_homebrew_config_new_fields() {
let yaml = r#"
project_name: test
crates:
- name: a
path: "."
tag_template: "v{{ .Version }}"
publish:
homebrew:
tap:
owner: myorg
name: homebrew-tap
homepage: "https://example.com"
dependencies:
- name: openssl
- name: libgit2
os: mac
- name: zlib
type: optional
conflicts:
- other-tool
- old-tool
caveats: "Run `tool init` after installing."
skip_upload: "auto"
"#;
let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
let hb = config.crates[0]
.publish
.as_ref()
.unwrap()
.homebrew
.as_ref()
.unwrap();
assert_eq!(hb.homepage.as_deref(), Some("https://example.com"));
assert_eq!(
hb.skip_upload,
Some(StringOrBool::String("auto".to_string()))
);
assert_eq!(
hb.caveats.as_deref(),
Some("Run `tool init` after installing.")
);
let conflicts = hb.conflicts.as_ref().unwrap();
assert_eq!(
conflicts,
&[
HomebrewConflict::Name("other-tool".to_string()),
HomebrewConflict::Name("old-tool".to_string()),
]
);
let deps = hb.dependencies.as_ref().unwrap();
assert_eq!(deps.len(), 3);
assert_eq!(deps[0].name, "openssl");
assert_eq!(deps[0].os, None);
assert_eq!(deps[0].dep_type, None);
assert_eq!(deps[1].name, "libgit2");
assert_eq!(deps[1].os.as_deref(), Some("mac"));
assert_eq!(deps[2].name, "zlib");
assert_eq!(deps[2].dep_type.as_deref(), Some("optional"));
}
#[test]
fn test_homebrew_config_defaults_when_new_fields_omitted() {
let yaml = r#"
project_name: test
crates:
- name: a
path: "."
tag_template: "v{{ .Version }}"
publish:
homebrew:
tap:
owner: myorg
name: homebrew-tap
"#;
let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
let hb = config.crates[0]
.publish
.as_ref()
.unwrap()
.homebrew
.as_ref()
.unwrap();
assert!(hb.homepage.is_none());
assert!(hb.dependencies.is_none());
assert!(hb.conflicts.is_none());
assert!(hb.caveats.is_none());
assert!(hb.skip_upload.is_none());
}
#[test]
fn test_scoop_config_new_fields() {
let yaml = r#"
project_name: test
crates:
- name: a
path: "."
tag_template: "v{{ .Version }}"
publish:
scoop:
bucket:
owner: myorg
name: scoop-bucket
homepage: "https://example.com"
persist:
- data
- config.ini
depends:
- git
- 7zip
pre_install:
- "Write-Host 'Installing...'"
post_install:
- "Write-Host 'Done!'"
shortcuts:
- ["myapp.exe", "My App"]
- ["myapp.exe", "My App CLI", "--cli"]
skip_upload: "true"
"#;
let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
let sc = config.crates[0]
.publish
.as_ref()
.unwrap()
.scoop
.as_ref()
.unwrap();
assert_eq!(sc.homepage.as_deref(), Some("https://example.com"));
assert_eq!(
sc.skip_upload,
Some(StringOrBool::String("true".to_string()))
);
let persist = sc.persist.as_ref().unwrap();
assert_eq!(persist, &["data", "config.ini"]);
let depends = sc.depends.as_ref().unwrap();
assert_eq!(depends, &["git", "7zip"]);
let pre = sc.pre_install.as_ref().unwrap();
assert_eq!(pre, &["Write-Host 'Installing...'"]);
let post = sc.post_install.as_ref().unwrap();
assert_eq!(post, &["Write-Host 'Done!'"]);
let shortcuts = sc.shortcuts.as_ref().unwrap();
assert_eq!(shortcuts.len(), 2);
assert_eq!(shortcuts[0], vec!["myapp.exe", "My App"]);
assert_eq!(shortcuts[1], vec!["myapp.exe", "My App CLI", "--cli"]);
}
#[test]
fn test_scoop_config_defaults_when_new_fields_omitted() {
let yaml = r#"
project_name: test
crates:
- name: a
path: "."
tag_template: "v{{ .Version }}"
publish:
scoop:
bucket:
owner: myorg
name: scoop-bucket
"#;
let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
let sc = config.crates[0]
.publish
.as_ref()
.unwrap()
.scoop
.as_ref()
.unwrap();
assert!(sc.homepage.is_none());
assert!(sc.persist.is_none());
assert!(sc.depends.is_none());
assert!(sc.pre_install.is_none());
assert!(sc.post_install.is_none());
assert!(sc.shortcuts.is_none());
assert!(sc.skip_upload.is_none());
}
#[test]
fn test_git_config_all_fields() {
let yaml = r#"
project_name: test
crates:
- name: a
path: "."
tag_template: "v{{ .Version }}"
git:
tag_sort: "-version:creatordate"
ignore_tags:
- "nightly*"
- "legacy-*"
ignore_tag_prefixes:
- "internal/"
- "test-"
prerelease_suffix: "-rc"
"#;
let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
let git = config
.git
.unwrap_or_else(|| panic!("git section should be present"));
assert_eq!(git.tag_sort.as_deref(), Some("-version:creatordate"));
assert_eq!(
git.ignore_tags.as_deref(),
Some(&["nightly*".to_string(), "legacy-*".to_string()][..])
);
assert_eq!(
git.ignore_tag_prefixes.as_deref(),
Some(&["internal/".to_string(), "test-".to_string()][..])
);
assert_eq!(git.prerelease_suffix.as_deref(), Some("-rc"));
}
#[test]
fn test_git_config_omitted_is_none() {
let yaml = r#"
project_name: test
crates:
- name: a
path: "."
tag_template: "v{{ .Version }}"
"#;
let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
assert!(config.git.is_none());
}
#[test]
fn test_git_config_partial_only_tag_sort() {
let yaml = r#"
project_name: test
crates:
- name: a
path: "."
tag_template: "v{{ .Version }}"
git:
tag_sort: "-version:refname"
"#;
let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
let git = config
.git
.unwrap_or_else(|| panic!("git section should be present"));
assert_eq!(git.tag_sort.as_deref(), Some("-version:refname"));
assert!(git.ignore_tags.is_none());
assert!(git.ignore_tag_prefixes.is_none());
assert!(git.prerelease_suffix.is_none());
}
#[test]
fn test_git_config_ignore_tags_accepts_array() {
let yaml = r#"
project_name: test
crates:
- name: a
path: "."
tag_template: "v{{ .Version }}"
git:
ignore_tags:
- "alpha*"
- "beta*"
- "rc-*"
"#;
let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
let tags = config.git.unwrap().ignore_tags.unwrap();
assert_eq!(tags.len(), 3);
assert_eq!(tags[0], "alpha*");
assert_eq!(tags[1], "beta*");
assert_eq!(tags[2], "rc-*");
}
#[test]
fn test_validate_tag_sort_valid_refname() {
let config = Config {
git: Some(GitConfig {
tag_sort: Some("-version:refname".to_string()),
..Default::default()
}),
..Default::default()
};
assert!(validate_tag_sort(&config).is_ok());
}
#[test]
fn test_validate_tag_sort_valid_creatordate() {
let config = Config {
git: Some(GitConfig {
tag_sort: Some("-version:creatordate".to_string()),
..Default::default()
}),
..Default::default()
};
assert!(validate_tag_sort(&config).is_ok());
}
#[test]
fn test_validate_tag_sort_none_is_valid() {
let config = Config {
git: Some(GitConfig::default()),
..Default::default()
};
assert!(validate_tag_sort(&config).is_ok());
}
#[test]
fn test_validate_tag_sort_no_git_config_is_valid() {
let config = Config::default();
assert!(validate_tag_sort(&config).is_ok());
}
#[test]
fn test_validate_tag_sort_invalid_rejected() {
let config = Config {
git: Some(GitConfig {
tag_sort: Some("alphabetical".to_string()),
..Default::default()
}),
..Default::default()
};
let result = validate_tag_sort(&config);
assert!(result.is_err());
let err = result.unwrap_err();
assert!(
err.contains("alphabetical"),
"error should contain the bad value: {}",
err
);
assert!(
err.contains("-version:refname"),
"error should list accepted values: {}",
err
);
}
#[test]
fn test_git_config_ignore_tag_prefixes_accepts_array() {
let yaml = r#"
project_name: test
crates:
- name: a
path: "."
tag_template: "v{{ .Version }}"
git:
ignore_tag_prefixes:
- "wip/"
- "experiment/"
"#;
let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
let prefixes = config.git.unwrap().ignore_tag_prefixes.unwrap();
assert_eq!(prefixes.len(), 2);
assert_eq!(prefixes[0], "wip/");
assert_eq!(prefixes[1], "experiment/");
}
#[test]
fn test_metadata_config_with_mod_timestamp() {
let yaml = r#"
project_name: test
crates:
- name: a
path: "."
tag_template: "v{{ .Version }}"
metadata:
mod_timestamp: "{{ .CommitTimestamp }}"
"#;
let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
let meta = config.metadata.unwrap();
assert_eq!(meta.mod_timestamp.unwrap(), "{{ .CommitTimestamp }}");
}
#[test]
fn test_metadata_config_omitted_is_none() {
let yaml = r#"
project_name: test
crates:
- name: a
path: "."
tag_template: "v{{ .Version }}"
"#;
let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
assert!(config.metadata.is_none());
}
#[test]
fn test_metadata_config_empty_section() {
let yaml = r#"
project_name: test
crates:
- name: a
path: "."
tag_template: "v{{ .Version }}"
metadata: {}
"#;
let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
let meta = config.metadata.unwrap();
assert!(meta.mod_timestamp.is_none());
}
#[test]
fn test_variables_config_parsed() {
let yaml = r#"
project_name: test
variables:
description: "my project description"
somethingElse: "yada yada yada"
empty: ""
crates:
- name: test
path: "."
tag_template: "v{{ .Version }}"
"#;
let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
let vars = config.variables.as_ref().unwrap();
assert_eq!(vars.get("description").unwrap(), "my project description");
assert_eq!(vars.get("somethingElse").unwrap(), "yada yada yada");
assert_eq!(vars.get("empty").unwrap(), "");
assert_eq!(vars.len(), 3);
}
#[test]
fn test_variables_config_omitted_is_none() {
let yaml = r#"
project_name: test
crates:
- name: test
path: "."
tag_template: "v{{ .Version }}"
"#;
let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
assert!(config.variables.is_none());
}
#[test]
fn test_snapcraft_disable_bool_true() {
let yaml = r#"
project_name: test
crates:
- name: a
path: "."
tag_template: "v{{ .Version }}"
snapcrafts:
- disable: true
"#;
let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
let snap = &config.crates[0].snapcrafts.as_ref().unwrap()[0];
assert_eq!(snap.disable, Some(StringOrBool::Bool(true)));
}
#[test]
fn test_snapcraft_disable_bool_false() {
let yaml = r#"
project_name: test
crates:
- name: a
path: "."
tag_template: "v{{ .Version }}"
snapcrafts:
- disable: false
"#;
let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
let snap = &config.crates[0].snapcrafts.as_ref().unwrap()[0];
assert_eq!(snap.disable, Some(StringOrBool::Bool(false)));
}
#[test]
fn test_snapcraft_disable_template_string() {
let yaml = r#"
project_name: test
crates:
- name: a
path: "."
tag_template: "v{{ .Version }}"
snapcrafts:
- disable: "{{ if .IsSnapshot }}true{{ end }}"
"#;
let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
let snap = &config.crates[0].snapcrafts.as_ref().unwrap()[0];
match &snap.disable {
Some(StringOrBool::String(s)) => {
assert!(s.contains("IsSnapshot"));
}
other => panic!("expected StringOrBool::String, got {:?}", other),
}
}
#[test]
fn test_snapcraft_disable_omitted() {
let yaml = r#"
project_name: test
crates:
- name: a
path: "."
tag_template: "v{{ .Version }}"
snapcrafts:
- name: mysnap
"#;
let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
let snap = &config.crates[0].snapcrafts.as_ref().unwrap()[0];
assert!(snap.disable.is_none());
}
#[test]
fn test_aur_disable_bool_true() {
let yaml = r#"
project_name: test
crates:
- name: a
path: "."
tag_template: "v{{ .Version }}"
publish:
aur:
disable: true
git_url: "ssh://aur@aur.archlinux.org/a.git"
"#;
let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
let aur = config.crates[0]
.publish
.as_ref()
.unwrap()
.aur
.as_ref()
.unwrap();
assert_eq!(aur.disable, Some(StringOrBool::Bool(true)));
}
#[test]
fn test_aur_disable_template_string() {
let yaml = r#"
project_name: test
crates:
- name: a
path: "."
tag_template: "v{{ .Version }}"
publish:
aur:
disable: "{{ if .IsSnapshot }}true{{ end }}"
git_url: "ssh://aur@aur.archlinux.org/a.git"
"#;
let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
let aur = config.crates[0]
.publish
.as_ref()
.unwrap()
.aur
.as_ref()
.unwrap();
match &aur.disable {
Some(StringOrBool::String(s)) => {
assert!(s.contains("IsSnapshot"));
}
other => panic!("expected StringOrBool::String, got {:?}", other),
}
}
#[test]
fn test_aur_disable_omitted() {
let yaml = r#"
project_name: test
crates:
- name: a
path: "."
tag_template: "v{{ .Version }}"
publish:
aur:
git_url: "ssh://aur@aur.archlinux.org/a.git"
"#;
let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
let aur = config.crates[0]
.publish
.as_ref()
.unwrap()
.aur
.as_ref()
.unwrap();
assert!(aur.disable.is_none());
}
#[test]
fn test_publisher_disable_bool_true() {
let yaml = r#"
project_name: test
publishers:
- cmd: "echo hello"
disable: true
crates:
- name: a
path: "."
tag_template: "v{{ .Version }}"
"#;
let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
let pub_cfg = &config.publishers.as_ref().unwrap()[0];
assert_eq!(pub_cfg.disable, Some(StringOrBool::Bool(true)));
}
#[test]
fn test_publisher_disable_template_string() {
let yaml = r#"
project_name: test
publishers:
- cmd: "echo hello"
disable: "{{ if .IsSnapshot }}true{{ end }}"
crates:
- name: a
path: "."
tag_template: "v{{ .Version }}"
"#;
let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
let pub_cfg = &config.publishers.as_ref().unwrap()[0];
match &pub_cfg.disable {
Some(StringOrBool::String(s)) => {
assert!(s.contains("IsSnapshot"));
}
other => panic!("expected StringOrBool::String, got {:?}", other),
}
}
#[test]
fn test_publisher_disable_omitted() {
let yaml = r#"
project_name: test
publishers:
- cmd: "echo hello"
crates:
- name: a
path: "."
tag_template: "v{{ .Version }}"
"#;
let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
let pub_cfg = &config.publishers.as_ref().unwrap()[0];
assert!(pub_cfg.disable.is_none());
}
#[test]
fn test_homebrew_skip_upload_bool_true() {
let yaml = r#"
project_name: test
crates:
- name: a
path: "."
tag_template: "v{{ .Version }}"
publish:
homebrew:
skip_upload: true
tap:
owner: org
name: homebrew-tap
"#;
let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
let hb = config.crates[0]
.publish
.as_ref()
.unwrap()
.homebrew
.as_ref()
.unwrap();
assert_eq!(hb.skip_upload, Some(StringOrBool::Bool(true)));
}
#[test]
fn test_scoop_skip_upload_bool_true() {
let yaml = r#"
project_name: test
crates:
- name: a
path: "."
tag_template: "v{{ .Version }}"
publish:
scoop:
skip_upload: true
bucket:
owner: org
name: scoop-bucket
"#;
let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
let sc = config.crates[0]
.publish
.as_ref()
.unwrap()
.scoop
.as_ref()
.unwrap();
assert_eq!(sc.skip_upload, Some(StringOrBool::Bool(true)));
}
#[test]
fn test_aur_skip_upload_bool_true() {
let yaml = r#"
project_name: test
crates:
- name: a
path: "."
tag_template: "v{{ .Version }}"
publish:
aur:
skip_upload: true
git_url: "ssh://aur@aur.archlinux.org/a.git"
"#;
let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
let aur = config.crates[0]
.publish
.as_ref()
.unwrap()
.aur
.as_ref()
.unwrap();
assert_eq!(aur.skip_upload, Some(StringOrBool::Bool(true)));
}
#[test]
fn test_winget_skip_upload_bool_true() {
let yaml = r#"
project_name: test
crates:
- name: a
path: "."
tag_template: "v{{ .Version }}"
publish:
winget:
skip_upload: true
manifests_repo:
owner: org
name: winget-pkgs
package_identifier: "Org.App"
"#;
let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
let wg = config.crates[0]
.publish
.as_ref()
.unwrap()
.winget
.as_ref()
.unwrap();
assert_eq!(wg.skip_upload, Some(StringOrBool::Bool(true)));
}
#[test]
fn test_krew_skip_upload_auto_string() {
let yaml = r#"
project_name: test
crates:
- name: a
path: "."
tag_template: "v{{ .Version }}"
publish:
krew:
skip_upload: "auto"
manifests_repo:
owner: org
name: krew-index
"#;
let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
let krew = config.crates[0]
.publish
.as_ref()
.unwrap()
.krew
.as_ref()
.unwrap();
assert_eq!(
krew.skip_upload,
Some(StringOrBool::String("auto".to_string()))
);
}
#[test]
fn test_nix_skip_upload_template() {
let yaml = r#"
project_name: test
crates:
- name: a
path: "."
tag_template: "v{{ .Version }}"
publish:
nix:
skip_upload: "{{ .Env.SKIP }}"
repository:
owner: org
name: nixpkgs
"#;
let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
let nix = config.crates[0]
.publish
.as_ref()
.unwrap()
.nix
.as_ref()
.unwrap();
match &nix.skip_upload {
Some(StringOrBool::String(s)) => {
assert!(s.contains(".Env.SKIP"));
}
other => panic!("expected StringOrBool::String, got {:?}", other),
}
}
#[test]
fn test_skip_upload_string_or_bool() {
let yaml = r#"
project_name: test
crates:
- name: test
path: "."
tag_template: "v{{ .Version }}"
publish:
homebrew:
name: test
skip_upload: "{{ if .IsSnapshot }}true{{ endif }}"
tap:
owner: org
name: homebrew-tap
"#;
let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
let hb = config.crates[0]
.publish
.as_ref()
.unwrap()
.homebrew
.as_ref()
.unwrap();
match &hb.skip_upload {
Some(StringOrBool::String(s)) => {
assert!(
s.contains(".IsSnapshot"),
"expected template with .IsSnapshot, got: {}",
s
);
}
other => panic!(
"expected StringOrBool::String with template, got {:?}",
other
),
}
}
#[test]
fn test_template_files_parses_from_yaml() {
let yaml = r#"
project_name: myproject
crates: []
template_files:
- id: install-script
src: install.sh.tpl
dst: install.sh
mode: "0755"
- src: README.md.tpl
dst: README.md
"#;
let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
let tfs = config.template_files.unwrap();
assert_eq!(tfs.len(), 2);
assert_eq!(tfs[0].id.as_deref(), Some("install-script"));
assert_eq!(tfs[0].src, "install.sh.tpl");
assert_eq!(tfs[0].dst, "install.sh");
assert_eq!(tfs[0].mode, Some("0755".to_string()));
assert_eq!(tfs[1].id, None);
assert_eq!(tfs[1].src, "README.md.tpl");
assert_eq!(tfs[1].dst, "README.md");
assert_eq!(tfs[1].mode, None);
}
#[test]
fn test_template_files_defaults_to_none() {
let yaml = r#"
project_name: myproject
crates: []
"#;
let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
assert!(config.template_files.is_none());
}
#[test]
fn test_include_spec_plain_string() {
let yaml = r#"
project_name: test
includes:
- ./defaults.yaml
- extra.yaml
crates: []
"#;
let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
let includes = config.includes.unwrap();
assert_eq!(includes.len(), 2);
assert_eq!(
includes[0],
IncludeSpec::Path("./defaults.yaml".to_string())
);
assert_eq!(includes[1], IncludeSpec::Path("extra.yaml".to_string()));
}
#[test]
fn test_include_spec_from_file() {
let yaml = r#"
project_name: test
includes:
- from_file:
path: ./config/goreleaser.yaml
crates: []
"#;
let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
let includes = config.includes.unwrap();
assert_eq!(includes.len(), 1);
assert_eq!(
includes[0],
IncludeSpec::FromFile {
from_file: IncludeFilePath {
path: "./config/goreleaser.yaml".to_string(),
},
}
);
}
#[test]
fn test_include_spec_from_url_without_headers() {
let yaml = r#"
project_name: test
includes:
- from_url:
url: https://example.com/config.yaml
crates: []
"#;
let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
let includes = config.includes.unwrap();
assert_eq!(includes.len(), 1);
assert_eq!(
includes[0],
IncludeSpec::FromUrl {
from_url: IncludeUrlConfig {
url: "https://example.com/config.yaml".to_string(),
headers: None,
},
}
);
}
#[test]
fn test_include_spec_from_url_with_headers() {
let yaml = r#"
project_name: test
includes:
- from_url:
url: https://api.mycompany.com/configs/release.yaml
headers:
x-api-token: "${MYCOMPANY_TOKEN}"
Authorization: "Bearer ${GITHUB_TOKEN}"
crates: []
"#;
let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
let includes = config.includes.unwrap();
assert_eq!(includes.len(), 1);
match &includes[0] {
IncludeSpec::FromUrl { from_url } => {
assert_eq!(
from_url.url,
"https://api.mycompany.com/configs/release.yaml"
);
let headers = from_url.headers.as_ref().unwrap();
assert_eq!(headers.len(), 2);
assert_eq!(headers["x-api-token"], "${MYCOMPANY_TOKEN}");
assert_eq!(headers["Authorization"], "Bearer ${GITHUB_TOKEN}");
}
other => panic!("expected FromUrl, got: {:?}", other),
}
}
#[test]
fn test_include_spec_mixed_forms() {
let yaml = r#"
project_name: test
includes:
- ./defaults.yaml
- from_file:
path: ./config/shared.yaml
- from_url:
url: https://example.com/config.yaml
headers:
x-token: secret
crates: []
"#;
let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
let includes = config.includes.unwrap();
assert_eq!(includes.len(), 3);
assert!(matches!(&includes[0], IncludeSpec::Path(s) if s == "./defaults.yaml"));
assert!(
matches!(&includes[1], IncludeSpec::FromFile { from_file } if from_file.path == "./config/shared.yaml")
);
assert!(
matches!(&includes[2], IncludeSpec::FromUrl { from_url } if from_url.url == "https://example.com/config.yaml")
);
}
#[test]
fn test_include_spec_no_includes_field() {
let yaml = r#"
project_name: test
crates: []
"#;
let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
assert!(config.includes.is_none());
}
#[test]
fn test_include_spec_empty_includes() {
let yaml = r#"
project_name: test
includes: []
crates: []
"#;
let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
assert_eq!(config.includes, Some(vec![]));
}
#[test]
fn test_include_spec_github_shorthand_url() {
let yaml = r#"
project_name: test
includes:
- from_url:
url: caarlos0/goreleaserfiles/main/packages.yml
crates: []
"#;
let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
let includes = config.includes.unwrap();
assert_eq!(includes.len(), 1);
match &includes[0] {
IncludeSpec::FromUrl { from_url } => {
assert_eq!(from_url.url, "caarlos0/goreleaserfiles/main/packages.yml");
}
other => panic!("expected FromUrl, got: {:?}", other),
}
}
#[test]
fn test_github_urls_config_all_fields() {
let yaml = r#"
api: https://github.example.com/api/v3/
upload: https://github.example.com/api/uploads/
download: https://github.example.com/
skip_tls_verify: true
"#;
let cfg: GitHubUrlsConfig = serde_yaml_ng::from_str(yaml).unwrap();
assert_eq!(
cfg.api.as_deref(),
Some("https://github.example.com/api/v3/")
);
assert_eq!(
cfg.upload.as_deref(),
Some("https://github.example.com/api/uploads/")
);
assert_eq!(cfg.download.as_deref(), Some("https://github.example.com/"));
assert_eq!(cfg.skip_tls_verify, Some(true));
}
#[test]
fn test_github_urls_config_defaults() {
let yaml = "{}";
let cfg: GitHubUrlsConfig = serde_yaml_ng::from_str(yaml).unwrap();
assert_eq!(cfg.api, None);
assert_eq!(cfg.upload, None);
assert_eq!(cfg.download, None);
assert_eq!(cfg.skip_tls_verify, None);
}
#[test]
fn test_gitlab_urls_config_all_fields() {
let yaml = r#"
api: https://gitlab.example.com/api/v4/
download: https://gitlab.example.com/
skip_tls_verify: false
use_package_registry: true
use_job_token: true
"#;
let cfg: GitLabUrlsConfig = serde_yaml_ng::from_str(yaml).unwrap();
assert_eq!(
cfg.api.as_deref(),
Some("https://gitlab.example.com/api/v4/")
);
assert_eq!(cfg.download.as_deref(), Some("https://gitlab.example.com/"));
assert_eq!(cfg.skip_tls_verify, Some(false));
assert_eq!(cfg.use_package_registry, Some(true));
assert_eq!(cfg.use_job_token, Some(true));
}
#[test]
fn test_gitlab_urls_config_defaults() {
let yaml = "{}";
let cfg: GitLabUrlsConfig = serde_yaml_ng::from_str(yaml).unwrap();
assert_eq!(cfg.api, None);
assert_eq!(cfg.download, None);
assert_eq!(cfg.skip_tls_verify, None);
assert_eq!(cfg.use_package_registry, None);
assert_eq!(cfg.use_job_token, None);
}
#[test]
fn test_gitea_urls_config_all_fields() {
let yaml = r#"
api: https://gitea.example.com/api/v1/
download: https://gitea.example.com/
skip_tls_verify: true
"#;
let cfg: GiteaUrlsConfig = serde_yaml_ng::from_str(yaml).unwrap();
assert_eq!(
cfg.api.as_deref(),
Some("https://gitea.example.com/api/v1/")
);
assert_eq!(cfg.download.as_deref(), Some("https://gitea.example.com/"));
assert_eq!(cfg.skip_tls_verify, Some(true));
}
#[test]
fn test_gitea_urls_config_defaults() {
let yaml = "{}";
let cfg: GiteaUrlsConfig = serde_yaml_ng::from_str(yaml).unwrap();
assert_eq!(cfg.api, None);
assert_eq!(cfg.download, None);
assert_eq!(cfg.skip_tls_verify, None);
}
#[test]
fn test_release_config_gitlab_gitea_fields() {
let yaml = r#"
project_name: test
crates:
- name: a
path: "."
tag_template: "v{{ .Version }}"
release:
github:
owner: gh-owner
name: gh-repo
gitlab:
owner: gitlab-owner
name: gitlab-repo
gitea:
owner: gitea-owner
name: gitea-repo
"#;
let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
let release = config.crates[0].release.as_ref().unwrap();
let github = release.github.as_ref().unwrap();
assert_eq!(github.owner, "gh-owner");
assert_eq!(github.name, "gh-repo");
let gitlab = release.gitlab.as_ref().unwrap();
assert_eq!(gitlab.owner, "gitlab-owner");
assert_eq!(gitlab.name, "gitlab-repo");
let gitea = release.gitea.as_ref().unwrap();
assert_eq!(gitea.owner, "gitea-owner");
assert_eq!(gitea.name, "gitea-repo");
}
#[test]
fn test_config_github_urls_field() {
let yaml = r#"
project_name: test
github_urls:
api: https://ghe.corp.com/api/v3/
upload: https://ghe.corp.com/api/uploads/
download: https://ghe.corp.com/
skip_tls_verify: true
crates:
- name: a
path: "."
tag_template: "v{{ .Version }}"
"#;
let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
let urls = config.github_urls.as_ref().unwrap();
assert_eq!(urls.api.as_deref(), Some("https://ghe.corp.com/api/v3/"));
assert_eq!(
urls.upload.as_deref(),
Some("https://ghe.corp.com/api/uploads/")
);
assert_eq!(urls.download.as_deref(), Some("https://ghe.corp.com/"));
assert_eq!(urls.skip_tls_verify, Some(true));
}
#[test]
fn test_config_gitlab_urls_field() {
let yaml = r#"
project_name: test
gitlab_urls:
api: https://gitlab.corp.com/api/v4/
download: https://gitlab.corp.com/
skip_tls_verify: false
use_package_registry: true
use_job_token: false
crates:
- name: a
path: "."
tag_template: "v{{ .Version }}"
"#;
let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
let urls = config.gitlab_urls.as_ref().unwrap();
assert_eq!(urls.api.as_deref(), Some("https://gitlab.corp.com/api/v4/"));
assert_eq!(urls.download.as_deref(), Some("https://gitlab.corp.com/"));
assert_eq!(urls.skip_tls_verify, Some(false));
assert_eq!(urls.use_package_registry, Some(true));
assert_eq!(urls.use_job_token, Some(false));
}
#[test]
fn test_config_gitea_urls_field() {
let yaml = r#"
project_name: test
gitea_urls:
api: https://gitea.corp.com/api/v1/
download: https://gitea.corp.com/
skip_tls_verify: true
crates:
- name: a
path: "."
tag_template: "v{{ .Version }}"
"#;
let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
let urls = config.gitea_urls.as_ref().unwrap();
assert_eq!(urls.api.as_deref(), Some("https://gitea.corp.com/api/v1/"));
assert_eq!(urls.download.as_deref(), Some("https://gitea.corp.com/"));
assert_eq!(urls.skip_tls_verify, Some(true));
}
#[test]
fn test_config_force_token_field() {
let yaml = r#"
project_name: test
force_token: gitlab
crates:
- name: a
path: "."
tag_template: "v{{ .Version }}"
"#;
let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
assert_eq!(config.force_token, Some(ForceTokenKind::GitLab));
}
#[test]
fn test_config_force_token_omitted() {
let yaml = r#"
project_name: test
crates:
- name: a
path: "."
tag_template: "v{{ .Version }}"
"#;
let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
assert_eq!(config.force_token, None::<ForceTokenKind>);
}
#[test]
fn test_config_all_platform_urls_and_force_token() {
let yaml = r#"
project_name: test
github_urls:
api: https://ghe.corp.com/api/v3/
gitlab_urls:
api: https://gitlab.corp.com/api/v4/
use_job_token: true
gitea_urls:
api: https://gitea.corp.com/api/v1/
force_token: github
crates:
- name: a
path: "."
tag_template: "v{{ .Version }}"
"#;
let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
assert_eq!(
config.github_urls.as_ref().unwrap().api.as_deref(),
Some("https://ghe.corp.com/api/v3/")
);
assert_eq!(
config.gitlab_urls.as_ref().unwrap().api.as_deref(),
Some("https://gitlab.corp.com/api/v4/")
);
assert_eq!(
config.gitlab_urls.as_ref().unwrap().use_job_token,
Some(true)
);
assert_eq!(
config.gitea_urls.as_ref().unwrap().api.as_deref(),
Some("https://gitea.corp.com/api/v1/")
);
assert_eq!(config.force_token, Some(ForceTokenKind::GitHub));
}
#[test]
fn test_dockerhub_config_parse() {
let yaml = r#"
project_name: test
dockerhub:
- username: myuser
secret_name: DOCKER_TOKEN
images:
- myorg/myapp
description: "My app"
disable: true
full_description:
from_file:
path: ./README.md
"#;
let cfg: Config = serde_yaml_ng::from_str(yaml).unwrap();
let dh = &cfg.dockerhub.unwrap()[0];
assert_eq!(dh.username.as_deref(), Some("myuser"));
assert_eq!(dh.secret_name.as_deref(), Some("DOCKER_TOKEN"));
assert_eq!(dh.images.as_ref().unwrap(), &["myorg/myapp"]);
assert_eq!(dh.description.as_deref(), Some("My app"));
assert_eq!(dh.disable, Some(StringOrBool::Bool(true)));
let fd = dh.full_description.as_ref().unwrap();
assert!(fd.from_url.is_none());
let ff = fd.from_file.as_ref().unwrap();
assert_eq!(ff.path, "./README.md");
}
#[test]
fn test_dockerhub_from_url_parse() {
let yaml = r#"
project_name: test
dockerhub:
- username: myuser
full_description:
from_url:
url: "https://raw.githubusercontent.com/org/repo/main/README.md"
headers:
Authorization: "Bearer {{ .Env.GH_TOKEN }}"
"#;
let cfg: Config = serde_yaml_ng::from_str(yaml).unwrap();
let dh = &cfg.dockerhub.unwrap()[0];
let fu = dh
.full_description
.as_ref()
.unwrap()
.from_url
.as_ref()
.unwrap();
assert_eq!(
fu.url,
"https://raw.githubusercontent.com/org/repo/main/README.md"
);
let headers = fu.headers.as_ref().unwrap();
assert_eq!(
headers.get("Authorization").unwrap(),
"Bearer {{ .Env.GH_TOKEN }}"
);
}
#[test]
fn test_artifactory_config_parse() {
let yaml = r#"
project_name: test
artifactories:
- name: production
target: "https://artifactory.example.com/repo/{{ .ProjectName }}/{{ .Version }}/"
username: deployer
mode: archive
skip: "{{ .Env.SKIP }}"
ids:
- default
"#;
let cfg: Config = serde_yaml_ng::from_str(yaml).unwrap();
let art = &cfg.artifactories.unwrap()[0];
assert_eq!(art.name.as_deref(), Some("production"));
assert_eq!(
art.target.as_deref(),
Some("https://artifactory.example.com/repo/{{ .ProjectName }}/{{ .Version }}/")
);
assert_eq!(art.username.as_deref(), Some("deployer"));
assert_eq!(art.mode.as_deref(), Some("archive"));
assert_eq!(
art.skip,
Some(StringOrBool::String("{{ .Env.SKIP }}".to_string()))
);
assert_eq!(art.ids.as_ref().unwrap(), &["default"]);
}
#[test]
fn test_cloudsmith_config_parse() {
let yaml = r#"
project_name: test
cloudsmiths:
- organization: myorg
repository: myrepo
formats:
- deb
distributions:
deb: "ubuntu/focal"
"#;
let cfg: Config = serde_yaml_ng::from_str(yaml).unwrap();
let cs = &cfg.cloudsmiths.unwrap()[0];
assert_eq!(cs.organization.as_deref(), Some("myorg"));
assert_eq!(cs.repository.as_deref(), Some("myrepo"));
assert_eq!(cs.formats.as_ref().unwrap(), &["deb"]);
let dists = cs.distributions.as_ref().unwrap();
assert_eq!(dists.get("deb").unwrap(), "ubuntu/focal");
}
#[test]
fn test_docker_sign_env_map_format() {
let yaml = r#"
project_name: test
docker_signs:
- cmd: cosign
env:
COSIGN_PASSWORD: hunter2
COSIGN_KEY: /path/to/key
"#;
let cfg: Config = serde_yaml_ng::from_str(yaml).unwrap();
let ds = &cfg.docker_signs.as_ref().unwrap()[0];
let env = ds
.env
.as_ref()
.unwrap_or_else(|| panic!("env should be Some"));
assert_eq!(env.get("COSIGN_PASSWORD").unwrap(), "hunter2");
assert_eq!(env.get("COSIGN_KEY").unwrap(), "/path/to/key");
}
#[test]
fn test_docker_sign_env_list_format() {
let yaml = r#"
project_name: test
docker_signs:
- cmd: cosign
env:
- COSIGN_PASSWORD=hunter2
- COSIGN_KEY=/path/to/key
"#;
let cfg: Config = serde_yaml_ng::from_str(yaml).unwrap();
let ds = &cfg.docker_signs.as_ref().unwrap()[0];
let env = ds
.env
.as_ref()
.unwrap_or_else(|| panic!("env should be Some"));
assert_eq!(env.get("COSIGN_PASSWORD").unwrap(), "hunter2");
assert_eq!(env.get("COSIGN_KEY").unwrap(), "/path/to/key");
}
#[test]
fn test_docker_sign_env_list_split_on_first_equals() {
let yaml = r#"
project_name: test
docker_signs:
- cmd: cosign
env:
- FLAGS=--key=val --other=stuff
"#;
let cfg: Config = serde_yaml_ng::from_str(yaml).unwrap();
let ds = &cfg.docker_signs.as_ref().unwrap()[0];
let env = ds
.env
.as_ref()
.unwrap_or_else(|| panic!("env should be Some"));
assert_eq!(env.get("FLAGS").unwrap(), "--key=val --other=stuff");
}
#[test]
fn test_docker_sign_env_null() {
let yaml = r#"
project_name: test
docker_signs:
- cmd: cosign
env: ~
"#;
let cfg: Config = serde_yaml_ng::from_str(yaml).unwrap();
let ds = &cfg.docker_signs.as_ref().unwrap()[0];
assert!(ds.env.is_none());
}
#[test]
fn test_docker_sign_env_missing() {
let yaml = r#"
project_name: test
docker_signs:
- cmd: cosign
"#;
let cfg: Config = serde_yaml_ng::from_str(yaml).unwrap();
let ds = &cfg.docker_signs.as_ref().unwrap()[0];
assert!(ds.env.is_none());
}
#[test]
fn test_docker_sign_env_list_invalid_no_equals() {
let yaml = r#"
project_name: test
docker_signs:
- cmd: cosign
env:
- COSIGN_PASSWORD
"#;
let result = serde_yaml_ng::from_str::<Config>(yaml);
assert!(result.is_err(), "entry without '=' should fail");
}
#[test]
fn test_sign_config_env_list_format() {
let yaml = r#"
project_name: test
signs:
- cmd: gpg
env:
- GPG_KEY=ABCDEF
- GPG_TTY=/dev/pts/0
"#;
let cfg: Config = serde_yaml_ng::from_str(yaml).unwrap();
let s = &cfg.signs[0];
let env = s
.env
.as_ref()
.unwrap_or_else(|| panic!("env should be Some"));
assert_eq!(env.get("GPG_KEY").unwrap(), "ABCDEF");
assert_eq!(env.get("GPG_TTY").unwrap(), "/dev/pts/0");
}
#[test]
fn test_publisher_env_list_format() {
let yaml = r#"
project_name: test
publishers:
- name: mypub
cmd: publish.sh
env:
- API_TOKEN=secret123
"#;
let cfg: Config = serde_yaml_ng::from_str(yaml).unwrap();
let p = &cfg.publishers.as_ref().unwrap()[0];
let env = p
.env
.as_ref()
.unwrap_or_else(|| panic!("env should be Some"));
assert_eq!(env.get("API_TOKEN").unwrap(), "secret123");
}
#[test]
fn test_build_override_env_list_format() {
let yaml = r#"
project_name: test
defaults:
targets:
- x86_64-unknown-linux-gnu
overrides:
- targets:
- "x86_64-*"
env:
- CC=gcc-12
- CFLAGS=-O2 -Wall
crates: []
"#;
let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
let overrides = config.defaults.unwrap().overrides.unwrap();
let env = overrides[0]
.env
.as_ref()
.unwrap_or_else(|| panic!("env should be Some"));
assert_eq!(env.get("CC").unwrap(), "gcc-12");
assert_eq!(env.get("CFLAGS").unwrap(), "-O2 -Wall");
}
#[test]
fn test_build_override_env_map_format() {
let yaml = r#"
project_name: test
defaults:
targets:
- x86_64-unknown-linux-gnu
overrides:
- targets:
- "x86_64-*"
env:
CC: gcc-12
CFLAGS: "-O2 -Wall"
crates: []
"#;
let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
let overrides = config.defaults.unwrap().overrides.unwrap();
let env = overrides[0]
.env
.as_ref()
.unwrap_or_else(|| panic!("env should be Some"));
assert_eq!(env.get("CC").unwrap(), "gcc-12");
assert_eq!(env.get("CFLAGS").unwrap(), "-O2 -Wall");
}
#[test]
fn test_structured_hook_env_list_format() {
let yaml = r#"
project_name: test
before:
hooks:
- cmd: echo hello
env:
- MY_VAR=foo
- OTHER=bar=baz
"#;
let cfg: Config = serde_yaml_ng::from_str(yaml).unwrap();
let hooks = cfg.before.as_ref().unwrap().hooks.as_ref().unwrap();
match &hooks[0] {
HookEntry::Structured(h) => {
let env = h
.env
.as_ref()
.unwrap_or_else(|| panic!("env should be Some"));
assert_eq!(env.get("MY_VAR").unwrap(), "foo");
assert_eq!(env.get("OTHER").unwrap(), "bar=baz");
}
HookEntry::Simple(_) => panic!("expected Structured hook"),
}
}
#[test]
fn test_structured_hook_env_map_format() {
let yaml = r#"
project_name: test
before:
hooks:
- cmd: echo hello
env:
MY_VAR: foo
OTHER: "bar=baz"
"#;
let cfg: Config = serde_yaml_ng::from_str(yaml).unwrap();
let hooks = cfg.before.as_ref().unwrap().hooks.as_ref().unwrap();
match &hooks[0] {
HookEntry::Structured(h) => {
let env = h
.env
.as_ref()
.unwrap_or_else(|| panic!("env should be Some"));
assert_eq!(env.get("MY_VAR").unwrap(), "foo");
assert_eq!(env.get("OTHER").unwrap(), "bar=baz");
}
HookEntry::Simple(_) => panic!("expected Structured hook"),
}
}
#[test]
fn test_sign_config_env_map_format() {
let yaml = r#"
project_name: test
signs:
- cmd: gpg
env:
GPG_KEY: ABCDEF
GPG_TTY: /dev/pts/0
"#;
let cfg: Config = serde_yaml_ng::from_str(yaml).unwrap();
let s = &cfg.signs[0];
let env = s
.env
.as_ref()
.unwrap_or_else(|| panic!("env should be Some"));
assert_eq!(env.get("GPG_KEY").unwrap(), "ABCDEF");
assert_eq!(env.get("GPG_TTY").unwrap(), "/dev/pts/0");
}
#[test]
fn test_publisher_env_map_format() {
let yaml = r#"
project_name: test
publishers:
- name: mypub
cmd: publish.sh
env:
API_TOKEN: secret123
DEPLOY_ENV: staging
"#;
let cfg: Config = serde_yaml_ng::from_str(yaml).unwrap();
let p = &cfg.publishers.as_ref().unwrap()[0];
let env = p
.env
.as_ref()
.unwrap_or_else(|| panic!("env should be Some"));
assert_eq!(env.get("API_TOKEN").unwrap(), "secret123");
assert_eq!(env.get("DEPLOY_ENV").unwrap(), "staging");
}
#[test]
fn test_sbom_config_env_map_format() {
let yaml = r#"
project_name: test
sboms:
- cmd: syft
env:
SYFT_FILE_METADATA_CATALOGER_ENABLED: "true"
SYFT_SCOPE: all-layers
"#;
let cfg: Config = serde_yaml_ng::from_str(yaml).unwrap();
let s = &cfg.sboms[0];
let env = s
.env
.as_ref()
.unwrap_or_else(|| panic!("env should be Some"));
assert_eq!(
env.get("SYFT_FILE_METADATA_CATALOGER_ENABLED").unwrap(),
"true"
);
assert_eq!(env.get("SYFT_SCOPE").unwrap(), "all-layers");
}
#[test]
fn test_sbom_config_env_list_format() {
let yaml = r#"
project_name: test
sboms:
- cmd: syft
env:
- SYFT_FILE_METADATA_CATALOGER_ENABLED=true
- SYFT_SCOPE=all-layers
"#;
let cfg: Config = serde_yaml_ng::from_str(yaml).unwrap();
let s = &cfg.sboms[0];
let env = s
.env
.as_ref()
.unwrap_or_else(|| panic!("env should be Some"));
assert_eq!(
env.get("SYFT_FILE_METADATA_CATALOGER_ENABLED").unwrap(),
"true"
);
assert_eq!(env.get("SYFT_SCOPE").unwrap(), "all-layers");
}
#[test]
fn test_sbom_config_env_missing() {
let yaml = r#"
project_name: test
sboms:
- cmd: syft
"#;
let cfg: Config = serde_yaml_ng::from_str(yaml).unwrap();
let s = &cfg.sboms[0];
assert!(s.env.is_none());
}
}