use anodizer_core::artifact::{Artifact, ArtifactKind};
use anodizer_core::config::{Config, ForceTokenKind, GitHubConfig, WorkspaceConfig};
use anodizer_core::context::Context;
use anodizer_core::git;
use anodizer_core::log::StageLogger;
use anodizer_core::scm::{self, ScmTokenType};
use anyhow::{Context as _, Result};
use std::collections::HashMap;
use std::path::Path;
fn set_env_var_single_threaded(key: &str, value: &str) {
unsafe { std::env::set_var(key, value) };
}
fn resolve_force_token(config: &Config) -> Option<ForceTokenKind> {
config.force_token.as_ref().cloned().or_else(|| {
let env_val = std::env::var("ANODIZER_FORCE_TOKEN")
.ok()
.or_else(|| std::env::var("GORELEASER_FORCE_TOKEN").ok())?;
match env_val.to_lowercase().as_str() {
"github" => Some(ForceTokenKind::GitHub),
"gitlab" => Some(ForceTokenKind::GitLab),
"gitea" => Some(ForceTokenKind::Gitea),
_ => None,
}
})
}
pub fn collect_build_targets(config: &Config, selected_crates: &[String]) -> Vec<String> {
let mut targets: Vec<String> = Vec::new();
let all_crates = config.crates.iter().chain(
config
.workspaces
.as_deref()
.unwrap_or_default()
.iter()
.flat_map(|w| w.crates.iter()),
);
for krate in all_crates {
if !selected_crates.is_empty() && !selected_crates.contains(&krate.name) {
continue;
}
if let Some(ref builds) = krate.builds {
for build in builds {
if let Some(ref build_targets) = build.targets {
for t in build_targets {
if !targets.contains(t) {
targets.push(t.clone());
}
}
}
}
}
if let Some(ref defaults) = config.defaults
&& let Some(ref default_targets) = defaults.targets
{
for t in default_targets {
if !targets.contains(t) {
targets.push(t.clone());
}
}
}
}
if let Some(ref defaults) = config.defaults
&& let Some(ref ignores) = defaults.ignore
{
targets.retain(|t| {
let (os, arch) = anodizer_core::target::map_target(t);
!ignores.iter().any(|ig| ig.os == os && ig.arch == arch)
});
}
targets
}
pub fn apply_workspace_overlay(config: &mut Config, ws: &WorkspaceConfig) {
config.crates = ws.crates.clone();
if ws.changelog.is_some() {
config.changelog = ws.changelog.clone();
}
if !ws.signs.is_empty() {
config.signs = ws.signs.clone();
}
if !ws.binary_signs.is_empty() {
config.binary_signs = ws.binary_signs.clone();
}
if ws.before.is_some() {
config.before = ws.before.clone();
}
if ws.after.is_some() {
config.after = ws.after.clone();
}
if let Some(ref env_map) = ws.env {
let merged = config.env.get_or_insert_with(HashMap::new);
for (k, v) in env_map {
merged.insert(k.clone(), v.clone());
}
}
}
pub fn resolve_git_context(
ctx: &mut Context,
config: &Config,
log: &StageLogger,
) -> anyhow::Result<()> {
if git::is_shallow_clone() {
eprintln!(
"WARNING: shallow clone detected; tag discovery may be incomplete. Use `git fetch --unshallow` in CI."
);
}
let tag_override = std::env::var("ANODIZER_CURRENT_TAG")
.ok()
.filter(|s| !s.is_empty())
.or_else(|| {
std::env::var("GORELEASER_CURRENT_TAG")
.ok()
.filter(|s| !s.is_empty())
});
let first_crate = ctx
.options
.selected_crates
.first()
.and_then(|name| {
config.crates.iter().find(|c| &c.name == name).or_else(|| {
config.workspaces.as_ref().and_then(|ws_list| {
ws_list
.iter()
.flat_map(|w| w.crates.iter())
.find(|c| &c.name == name)
})
})
})
.or_else(|| config.crates.first())
.or_else(|| {
config
.workspaces
.as_ref()
.and_then(|ws_list| ws_list.iter().flat_map(|w| w.crates.iter()).next())
});
if let Some(crate_cfg) = first_crate {
let tag = if let Some(ref override_tag) = tag_override {
log.verbose(&format!(
"using ANODIZER_CURRENT_TAG override: {}",
override_tag
));
override_tag.clone()
} else {
let monorepo_prefix = config.monorepo_tag_prefix();
let latest_tag = match git::find_latest_tag_matching_with_prefix(
&crate_cfg.tag_template,
config.git.as_ref(),
Some(ctx.template_vars()),
monorepo_prefix,
) {
Ok(found) => found,
Err(e) => {
log.warn(&format!("error finding tags matching template: {e}"));
None
}
};
match latest_tag {
Some(t) => t,
None => {
if ctx.options.snapshot {
log.warn("no git tags found, defaulting to v0.0.0 (snapshot mode).");
"v0.0.0".to_string()
} else if ctx.options.dry_run {
log.warn("no git tags found, defaulting to v0.0.0 (dry-run mode).");
"v0.0.0".to_string()
} else {
anyhow::bail!("no git tag found; create a tag or use --snapshot");
}
}
}
};
let is_synthetic_tag = tag == "v0.0.0" && tag_override.is_none();
if !is_synthetic_tag
&& let Ok(false) = git::tag_points_at_head(&tag)
&& !ctx.options.snapshot
{
let head = git::get_short_commit().unwrap_or_else(|_| "unknown".to_string());
anyhow::bail!(
"tag {} does not point at HEAD ({}). Check out the tag or use --snapshot to skip this check.",
tag,
head
);
}
match git::detect_git_info(&tag, ctx.skip_validate()) {
Ok(mut git_info) => {
if git_info.dirty && !ctx.options.snapshot {
if ctx.options.dry_run {
log.warn("git is in a dirty state; run `git status` to see what changed.");
} else {
anyhow::bail!(
"git is in a dirty state; run `git status` to see what changed. \
Use --snapshot to force."
);
}
}
let prev_override = std::env::var("ANODIZER_PREVIOUS_TAG")
.ok()
.filter(|s| !s.is_empty())
.or_else(|| {
std::env::var("GORELEASER_PREVIOUS_TAG")
.ok()
.filter(|s| !s.is_empty())
});
if let Some(prev_override) = prev_override {
log.verbose(&format!(
"using ANODIZER_PREVIOUS_TAG override: {}",
prev_override
));
git_info.previous_tag = Some(prev_override);
} else {
let crate_prefix = git::extract_tag_prefix(&crate_cfg.tag_template);
let prefix = crate_prefix
.as_deref()
.or_else(|| config.monorepo_tag_prefix());
git_info.previous_tag = git::find_previous_tag_with_prefix(
&tag,
config.git.as_ref(),
Some(ctx.template_vars()),
prefix,
)
.ok()
.flatten();
}
ctx.git_info = Some(git_info);
ctx.populate_git_vars();
}
Err(e) => {
if ctx.options.snapshot {
log.warn(&format!(
"could not detect git info in snapshot mode, using defaults: {e}"
));
ctx.git_info = Some(git::GitInfo {
tag: tag.clone(),
commit: "none".to_string(),
short_commit: "none".to_string(),
branch: "none".to_string(),
dirty: true,
semver: git::SemVer {
major: 0,
minor: 0,
patch: 0,
prerelease: None,
build_metadata: None,
},
commit_date: String::new(),
commit_timestamp: String::new(),
previous_tag: None,
remote_url: String::new(),
summary: "snapshot".to_string(),
tag_subject: String::new(),
tag_contents: String::new(),
tag_body: String::new(),
first_commit: None,
});
ctx.populate_git_vars();
} else {
return Err(anyhow::anyhow!("could not detect git info: {e}"));
}
}
}
} else {
ctx.populate_git_vars();
}
Ok(())
}
pub fn setup_env(
ctx: &mut Context,
config: &Config,
log: &anodizer_core::log::StageLogger,
) -> anyhow::Result<()> {
for (key, value) in std::env::vars() {
ctx.template_vars_mut().set_env(&key, &value);
}
if let Some(ref env_files_config) = config.env_files {
match env_files_config {
anodizer_core::config::EnvFilesConfig::List(files) => {
let env_vars = anodizer_core::config::load_env_files(files, log, ctx.is_strict())
.map_err(|e| anyhow::anyhow!("{}", e))?;
for (key, value) in &env_vars {
ctx.template_vars_mut().set_config_env(key, value);
}
}
anodizer_core::config::EnvFilesConfig::TokenFiles(token_config) => {
let token_vars = anodizer_core::config::load_token_files(token_config, log)
.map_err(|e| anyhow::anyhow!("{}", e))?;
for (key, value) in &token_vars {
ctx.template_vars_mut().set_config_env(key, value);
set_env_var_single_threaded(key, value);
}
}
}
} else {
let default_config = anodizer_core::config::EnvFilesTokenConfig::default();
let token_vars = anodizer_core::config::load_token_files(&default_config, log)
.map_err(|e| anyhow::anyhow!("{}", e))?;
for (key, value) in &token_vars {
ctx.template_vars_mut().set_config_env(key, value);
set_env_var_single_threaded(key, value);
}
}
if let Some(ref env_map) = config.env {
for (key, value) in env_map {
let rendered = ctx.render_template(value).unwrap_or_else(|_| value.clone());
ctx.template_vars_mut().set_config_env(key, &rendered);
set_env_var_single_threaded(key, &rendered);
}
}
if let Some(ref vars_map) = config.variables {
for (key, value) in vars_map {
let rendered = ctx.render_template(value).unwrap_or_else(|_| value.clone());
ctx.template_vars_mut().set_custom_var(key, &rendered);
}
}
let resolved_force = resolve_force_token(config);
if let Some(ref forced) = resolved_force {
let keep_github = matches!(forced, ForceTokenKind::GitHub);
let keep_gitlab = matches!(forced, ForceTokenKind::GitLab);
let keep_gitea = matches!(forced, ForceTokenKind::Gitea);
if !keep_github {
unsafe {
std::env::remove_var("GITHUB_TOKEN");
std::env::remove_var("ANODIZER_GITHUB_TOKEN");
}
}
if !keep_gitlab {
unsafe { std::env::remove_var("GITLAB_TOKEN") };
}
if !keep_gitea {
unsafe { std::env::remove_var("GITEA_TOKEN") };
}
}
if resolved_force.is_none() {
let has_github =
std::env::var("GITHUB_TOKEN").is_ok() || std::env::var("ANODIZER_GITHUB_TOKEN").is_ok();
let has_gitlab = std::env::var("GITLAB_TOKEN").is_ok();
let has_gitea = std::env::var("GITEA_TOKEN").is_ok();
let count = [has_github, has_gitlab, has_gitea]
.iter()
.filter(|&&b| b)
.count();
if count > 1 {
anyhow::bail!(
"multiple SCM tokens set simultaneously ({}). Set force_token in config \
or ANODIZER_FORCE_TOKEN env var to specify which to use.",
[
if has_github {
Some("GITHUB_TOKEN")
} else {
None
},
if has_gitlab {
Some("GITLAB_TOKEN")
} else {
None
},
if has_gitea { Some("GITEA_TOKEN") } else { None },
]
.into_iter()
.flatten()
.collect::<Vec<_>>()
.join(", ")
);
}
}
if ctx.options.token.is_none() && !ctx.is_snapshot() && !ctx.is_dry_run() {
let release_disabled = config
.crates
.first()
.and_then(|c| c.release.as_ref()?.disable.as_ref())
.is_some_and(|d| d.is_disabled(|t| ctx.render_template(t)));
let needs_token = config.crates.iter().any(|c| c.release.is_some())
&& !ctx.should_skip("release")
&& !release_disabled;
if needs_token {
let hint = match ctx.token_type {
anodizer_core::scm::ScmTokenType::GitLab => {
"no GitLab token found. Set GITLAB_TOKEN."
}
anodizer_core::scm::ScmTokenType::Gitea => "no Gitea token found. Set GITEA_TOKEN.",
anodizer_core::scm::ScmTokenType::GitHub => {
"no GitHub token found. Set GITHUB_TOKEN or ANODIZER_GITHUB_TOKEN."
}
};
anyhow::bail!("{}", hint);
}
}
Ok(())
}
pub fn write_effective_config(config: &Config, log: &StageLogger) -> Result<()> {
let dist = &config.dist;
std::fs::create_dir_all(dist)
.with_context(|| format!("failed to create dist directory: {}", dist.display()))?;
let effective_path = dist.join("config.yaml");
let yaml = serde_yaml_ng::to_string(config).context("failed to serialize effective config")?;
std::fs::write(&effective_path, &yaml)
.with_context(|| format!("failed to write {}", effective_path.display()))?;
log.verbose(&format!(
"wrote effective config to {}",
effective_path.display()
));
Ok(())
}
pub fn run_report_sizes(ctx: &mut Context, config: &Config, log: &StageLogger) {
if config.report_sizes.unwrap_or(false) {
anodizer_core::artifact::print_size_report(&mut ctx.artifacts, log);
}
}
pub fn write_metadata_and_artifacts(
ctx: &mut Context,
config: &Config,
log: &StageLogger,
) -> Result<()> {
let dist = &config.dist;
std::fs::create_dir_all(dist)
.with_context(|| format!("failed to create dist directory: {}", dist.display()))?;
let metadata_path = dist.join("metadata.json");
let goos = anodizer_core::context::map_os_to_goos(std::env::consts::OS);
let goarch = anodizer_core::context::map_arch_to_goarch(std::env::consts::ARCH);
let tag = ctx.template_vars().get("Tag").cloned().unwrap_or_default();
let previous_tag = ctx
.template_vars()
.get("PreviousTag")
.cloned()
.unwrap_or_default();
let version = ctx.version();
let commit = ctx
.template_vars()
.get("FullCommit")
.cloned()
.unwrap_or_default();
let date = ctx.template_vars().get("Date").cloned().unwrap_or_default();
let project_metadata = serde_json::json!({
"project_name": config.project_name,
"tag": tag,
"previous_tag": previous_tag,
"version": version,
"commit": commit,
"date": date,
"runtime": {
"goos": goos,
"goarch": goarch,
}
});
let json_str = serde_json::to_string_pretty(&project_metadata)
.context("failed to serialize project metadata JSON")?;
std::fs::write(&metadata_path, &json_str)
.with_context(|| format!("failed to write {}", metadata_path.display()))?;
log.status(&format!("wrote {}", metadata_path.display()));
ctx.artifacts.add(anodizer_core::artifact::Artifact {
kind: ArtifactKind::Metadata,
name: "metadata.json".to_string(),
path: metadata_path.clone(),
target: None,
crate_name: config.project_name.clone(),
metadata: Default::default(),
size: None,
});
let artifacts_path = dist.join("artifacts.json");
let artifacts_json = ctx
.artifacts
.to_artifacts_json()
.context("failed to serialize artifact list")?;
let json_str = serde_json::to_string_pretty(&artifacts_json)
.context("failed to serialize artifacts JSON")?;
std::fs::write(&artifacts_path, &json_str)
.with_context(|| format!("failed to write {}", artifacts_path.display()))?;
log.status(&format!("wrote {}", artifacts_path.display()));
if let Some(ref meta) = config.metadata
&& let Some(ref ts_tmpl) = meta.mod_timestamp
{
let rendered = ctx
.render_template(ts_tmpl)
.context("failed to render metadata.mod_timestamp template")?;
if !rendered.is_empty() {
let mtime = anodizer_core::util::parse_mod_timestamp(&rendered)
.with_context(|| format!("invalid metadata.mod_timestamp value: {:?}", rendered))?;
anodizer_core::util::set_file_mtime(&metadata_path, mtime)?;
anodizer_core::util::set_file_mtime(&artifacts_path, mtime)?;
log.status(&format!(
"set mtime on metadata.json and artifacts.json to {}",
rendered
));
}
}
Ok(())
}
pub fn infer_project_name(config: &mut Config, log: &StageLogger) {
if !config.project_name.is_empty() {
return;
}
if let Ok(cargo_toml) = std::fs::read_to_string("Cargo.toml")
&& let Ok(doc) = cargo_toml.parse::<toml_edit::DocumentMut>()
&& let Some(name) = doc
.get("package")
.and_then(|p| p.get("name"))
.and_then(|n| n.as_str())
{
config.project_name = name.to_string();
log.verbose(&format!("inferred project_name '{}' from Cargo.toml", name));
}
}
pub fn auto_detect_github(config: &mut Config, log: &StageLogger) {
let detected_github = git::detect_github_repo().ok();
for crate_cfg in &mut config.crates {
if let Some(ref mut release) = crate_cfg.release
&& release.github.is_none()
{
if let Some((ref owner, ref name)) = detected_github {
release.github = Some(GitHubConfig {
owner: owner.clone(),
name: name.clone(),
});
} else {
log.warn("could not auto-detect GitHub repo from git remote");
}
}
}
}
pub fn setup_context(ctx: &mut Context, config: &Config, log: &StageLogger) -> Result<()> {
resolve_scm_token_type(ctx, config);
ctx.populate_time_vars();
ctx.populate_runtime_vars();
setup_env(ctx, config, log)?;
resolve_git_context(ctx, config, log)?;
Ok(())
}
pub fn resolve_scm_token_type(ctx: &mut Context, config: &Config) {
let env_hint = if std::env::var("GITLAB_TOKEN").is_ok() {
Some("gitlab")
} else if std::env::var("GITEA_TOKEN").is_ok() {
Some("gitea")
} else {
None
};
let force_token = resolve_force_token(config);
ctx.token_type = scm::resolve_token_type(force_token.as_ref(), env_hint);
if ctx.options.token.is_none() {
ctx.options.token = match ctx.token_type {
ScmTokenType::GitLab => std::env::var("GITLAB_TOKEN").ok(),
ScmTokenType::Gitea => std::env::var("GITEA_TOKEN").ok(),
ScmTokenType::GitHub => std::env::var("ANODIZER_GITHUB_TOKEN")
.ok()
.or_else(|| std::env::var("GITHUB_TOKEN").ok()),
};
}
}
pub fn load_artifacts_from_dist(ctx: &mut Context, dist: &Path) -> Result<()> {
let artifacts_path = dist.join("artifacts.json");
if !artifacts_path.exists() {
anyhow::bail!(
"no artifacts.json found in {}. Run a full release or merge first.",
dist.display()
);
}
let content = std::fs::read_to_string(&artifacts_path)
.with_context(|| format!("read {}", artifacts_path.display()))?;
#[derive(serde::Deserialize)]
struct MetadataArtifact {
kind: String,
#[serde(default)]
name: Option<String>,
path: String,
target: Option<String>,
crate_name: String,
#[serde(default)]
metadata: HashMap<String, String>,
#[serde(default)]
size: Option<u64>,
}
let artifacts: Vec<MetadataArtifact> = serde_json::from_str(&content)
.with_context(|| format!("parse {}", artifacts_path.display()))?;
for a in artifacts {
let kind = ArtifactKind::parse(&a.kind)
.ok_or_else(|| anyhow::anyhow!("unknown artifact kind: {}", a.kind))?;
ctx.artifacts.add(Artifact {
kind,
name: a.name.unwrap_or_default(),
path: std::path::PathBuf::from(&a.path),
target: a.target,
crate_name: a.crate_name,
metadata: a.metadata,
size: a.size,
});
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use anodizer_core::config::{ChangelogConfig, CrateConfig, SignConfig};
use anodizer_core::context::ContextOptions;
use anodizer_core::scm::ScmTokenType;
fn make_crate(name: &str) -> CrateConfig {
CrateConfig {
name: name.to_string(),
path: ".".to_string(),
tag_template: format!("{}-v{{{{ .Version }}}}", name),
..Default::default()
}
}
#[test]
fn test_apply_workspace_overlay_replaces_crates() {
let mut config = Config {
project_name: "test".to_string(),
crates: vec![make_crate("original")],
..Default::default()
};
let ws = WorkspaceConfig {
name: "ws".to_string(),
crates: vec![make_crate("ws-crate")],
..Default::default()
};
apply_workspace_overlay(&mut config, &ws);
assert_eq!(config.crates.len(), 1);
assert_eq!(config.crates[0].name, "ws-crate");
}
#[test]
fn test_apply_workspace_overlay_merges_env() {
let mut config = Config {
project_name: "test".to_string(),
env: Some(HashMap::from([
("SHARED".to_string(), "from-top".to_string()),
("TOP_ONLY".to_string(), "top-value".to_string()),
])),
..Default::default()
};
let ws = WorkspaceConfig {
name: "ws".to_string(),
crates: vec![],
env: Some(HashMap::from([
("SHARED".to_string(), "from-ws".to_string()),
("WS_ONLY".to_string(), "ws-value".to_string()),
])),
..Default::default()
};
apply_workspace_overlay(&mut config, &ws);
let env = config.env.as_ref().unwrap();
assert_eq!(env.get("TOP_ONLY").unwrap(), "top-value");
assert_eq!(env.get("SHARED").unwrap(), "from-ws");
assert_eq!(env.get("WS_ONLY").unwrap(), "ws-value");
}
#[test]
fn test_apply_workspace_overlay_replaces_signs() {
let mut config = Config {
project_name: "test".to_string(),
signs: vec![SignConfig {
cmd: Some("gpg".to_string()),
..Default::default()
}],
..Default::default()
};
let ws = WorkspaceConfig {
name: "ws".to_string(),
crates: vec![],
signs: vec![SignConfig {
cmd: Some("cosign".to_string()),
..Default::default()
}],
..Default::default()
};
apply_workspace_overlay(&mut config, &ws);
assert_eq!(config.signs.len(), 1);
assert_eq!(config.signs[0].cmd.as_deref(), Some("cosign"));
}
#[test]
fn test_apply_workspace_overlay_replaces_changelog() {
let mut config = Config {
project_name: "test".to_string(),
changelog: Some(ChangelogConfig {
sort: Some("asc".to_string()),
..Default::default()
}),
..Default::default()
};
let ws = WorkspaceConfig {
name: "ws".to_string(),
crates: vec![],
changelog: Some(ChangelogConfig {
sort: Some("desc".to_string()),
..Default::default()
}),
..Default::default()
};
apply_workspace_overlay(&mut config, &ws);
assert_eq!(
config.changelog.as_ref().unwrap().sort.as_deref(),
Some("desc")
);
}
#[test]
fn test_apply_workspace_overlay_skips_none_fields() {
let mut config = Config {
project_name: "test".to_string(),
changelog: Some(ChangelogConfig {
sort: Some("asc".to_string()),
..Default::default()
}),
..Default::default()
};
let ws = WorkspaceConfig {
name: "ws".to_string(),
crates: vec![],
..Default::default()
};
apply_workspace_overlay(&mut config, &ws);
assert_eq!(
config.changelog.as_ref().unwrap().sort.as_deref(),
Some("asc")
);
}
#[test]
fn test_load_artifacts_from_dist_valid() {
use anodizer_core::artifact::ArtifactKind;
use anodizer_core::context::{Context, ContextOptions};
let dir = tempfile::TempDir::new().unwrap();
let artifacts_json = serde_json::json!([
{
"kind": "binary",
"name": "myapp",
"path": "dist/myapp",
"target": "x86_64-unknown-linux-gnu",
"crate_name": "myapp",
"metadata": {},
"size": 4096
},
{
"kind": "archive",
"name": "myapp.tar.gz",
"path": "dist/myapp.tar.gz",
"target": null,
"crate_name": "myapp",
"metadata": {"format": "tar.gz"}
}
]);
std::fs::write(
dir.path().join("artifacts.json"),
serde_json::to_string_pretty(&artifacts_json).unwrap(),
)
.unwrap();
let config = Config::default();
let mut ctx = Context::new(config, ContextOptions::default());
load_artifacts_from_dist(&mut ctx, dir.path()).unwrap();
let all = ctx.artifacts.all();
assert_eq!(all.len(), 2);
assert_eq!(all[0].kind, ArtifactKind::Binary);
assert_eq!(all[0].name, "myapp");
assert_eq!(
all[0].size,
Some(4096),
"size should be preserved from JSON"
);
assert_eq!(all[1].kind, ArtifactKind::Archive);
assert_eq!(all[1].name, "myapp.tar.gz");
assert_eq!(
all[1].metadata.get("format").map(|s| s.as_str()),
Some("tar.gz")
);
assert_eq!(
all[1].size, None,
"size should be None when absent from JSON"
);
}
#[test]
fn test_load_artifacts_from_dist_missing_file() {
use anodizer_core::context::{Context, ContextOptions};
let dir = tempfile::TempDir::new().unwrap();
let config = Config::default();
let mut ctx = Context::new(config, ContextOptions::default());
let result = load_artifacts_from_dist(&mut ctx, dir.path());
assert!(result.is_err());
let msg = result.unwrap_err().to_string();
assert!(
msg.contains("no artifacts.json found"),
"error should mention missing file: {msg}"
);
}
#[test]
fn test_load_artifacts_from_dist_invalid_json() {
use anodizer_core::context::{Context, ContextOptions};
let dir = tempfile::TempDir::new().unwrap();
std::fs::write(dir.path().join("artifacts.json"), "not valid json").unwrap();
let config = Config::default();
let mut ctx = Context::new(config, ContextOptions::default());
let result = load_artifacts_from_dist(&mut ctx, dir.path());
assert!(result.is_err());
}
#[test]
fn test_load_artifacts_from_dist_unknown_kind() {
use anodizer_core::context::{Context, ContextOptions};
let dir = tempfile::TempDir::new().unwrap();
let artifacts_json = serde_json::json!([
{
"kind": "unknown_kind",
"name": "thing",
"path": "dist/thing",
"target": null,
"crate_name": "myapp",
"metadata": {}
}
]);
std::fs::write(
dir.path().join("artifacts.json"),
serde_json::to_string_pretty(&artifacts_json).unwrap(),
)
.unwrap();
let config = Config::default();
let mut ctx = Context::new(config, ContextOptions::default());
let result = load_artifacts_from_dist(&mut ctx, dir.path());
assert!(result.is_err());
let msg = result.unwrap_err().to_string();
assert!(
msg.contains("unknown artifact kind"),
"error should mention unknown kind: {msg}"
);
}
#[test]
fn test_load_artifacts_from_dist_roundtrip() {
use anodizer_core::artifact::{Artifact, ArtifactKind, ArtifactRegistry};
use anodizer_core::context::{Context, ContextOptions};
let mut registry = ArtifactRegistry::new();
registry.add(Artifact {
kind: ArtifactKind::Checksum,
name: String::new(),
path: std::path::PathBuf::from("dist/checksums.txt"),
target: None,
crate_name: "myapp".to_string(),
metadata: Default::default(),
size: Some(256),
});
registry.add(Artifact {
kind: ArtifactKind::Binary,
name: String::new(),
path: std::path::PathBuf::from("dist/myapp"),
target: Some("aarch64-apple-darwin".to_string()),
crate_name: "myapp".to_string(),
metadata: Default::default(),
size: None,
});
let json_val = registry.to_artifacts_json().unwrap();
let json_str = serde_json::to_string_pretty(&json_val).unwrap();
let dir = tempfile::TempDir::new().unwrap();
std::fs::write(dir.path().join("artifacts.json"), &json_str).unwrap();
let config = Config::default();
let mut ctx = Context::new(config, ContextOptions::default());
load_artifacts_from_dist(&mut ctx, dir.path()).unwrap();
let loaded = ctx.artifacts.all();
assert_eq!(loaded.len(), 2);
assert_eq!(loaded[0].kind, ArtifactKind::Checksum);
assert_eq!(loaded[0].name, "checksums.txt");
assert_eq!(loaded[0].size, Some(256));
assert_eq!(loaded[1].kind, ArtifactKind::Binary);
assert_eq!(loaded[1].name, "myapp");
assert_eq!(loaded[1].target.as_deref(), Some("aarch64-apple-darwin"));
assert_eq!(loaded[1].size, None);
}
static ENV_MUTEX: std::sync::Mutex<()> = std::sync::Mutex::new(());
fn with_clean_token_env<F: FnOnce()>(f: F) {
let _lock = ENV_MUTEX.lock().unwrap_or_else(|e| e.into_inner());
let saved: Vec<(&str, Option<String>)> = [
"GITLAB_TOKEN",
"GITEA_TOKEN",
"ANODIZER_GITHUB_TOKEN",
"GITHUB_TOKEN",
"ANODIZER_FORCE_TOKEN",
"GORELEASER_FORCE_TOKEN",
]
.iter()
.map(|&k| (k, std::env::var(k).ok()))
.collect();
for &(k, _) in &saved {
unsafe { std::env::remove_var(k) };
}
f();
for (k, v) in saved {
match v {
Some(val) => unsafe { std::env::set_var(k, val) },
None => unsafe { std::env::remove_var(k) },
}
}
}
#[test]
fn test_resolve_scm_token_type_default_is_github() {
with_clean_token_env(|| {
let config = Config::default();
let mut ctx = Context::new(config.clone(), ContextOptions::default());
resolve_scm_token_type(&mut ctx, &config);
assert_eq!(ctx.token_type, ScmTokenType::GitHub);
assert!(ctx.options.token.is_none());
});
}
#[test]
fn test_resolve_scm_token_type_force_gitlab() {
with_clean_token_env(|| {
let config = Config {
force_token: Some(ForceTokenKind::GitLab),
..Default::default()
};
let mut ctx = Context::new(config.clone(), ContextOptions::default());
unsafe { std::env::set_var("GITLAB_TOKEN", "glpat-test123") };
resolve_scm_token_type(&mut ctx, &config);
assert_eq!(ctx.token_type, ScmTokenType::GitLab);
assert_eq!(ctx.options.token.as_deref(), Some("glpat-test123"));
});
}
#[test]
fn test_resolve_scm_token_type_force_gitea() {
with_clean_token_env(|| {
let config = Config {
force_token: Some(ForceTokenKind::Gitea),
..Default::default()
};
let mut ctx = Context::new(config.clone(), ContextOptions::default());
unsafe { std::env::set_var("GITEA_TOKEN", "gitea-tok") };
resolve_scm_token_type(&mut ctx, &config);
assert_eq!(ctx.token_type, ScmTokenType::Gitea);
assert_eq!(ctx.options.token.as_deref(), Some("gitea-tok"));
});
}
#[test]
fn test_resolve_scm_token_type_env_gitlab_detected() {
with_clean_token_env(|| {
unsafe { std::env::set_var("GITLAB_TOKEN", "glpat-env") };
let config = Config::default();
let mut ctx = Context::new(config.clone(), ContextOptions::default());
resolve_scm_token_type(&mut ctx, &config);
assert_eq!(ctx.token_type, ScmTokenType::GitLab);
assert_eq!(ctx.options.token.as_deref(), Some("glpat-env"));
});
}
#[test]
fn test_resolve_scm_token_type_env_gitea_detected() {
with_clean_token_env(|| {
unsafe { std::env::set_var("GITEA_TOKEN", "gitea-env") };
let config = Config::default();
let mut ctx = Context::new(config.clone(), ContextOptions::default());
resolve_scm_token_type(&mut ctx, &config);
assert_eq!(ctx.token_type, ScmTokenType::Gitea);
assert_eq!(ctx.options.token.as_deref(), Some("gitea-env"));
});
}
#[test]
fn test_resolve_scm_token_type_github_token_from_env() {
with_clean_token_env(|| {
unsafe { std::env::set_var("GITHUB_TOKEN", "ghp-from-env") };
let config = Config::default();
let mut ctx = Context::new(config.clone(), ContextOptions::default());
resolve_scm_token_type(&mut ctx, &config);
assert_eq!(ctx.token_type, ScmTokenType::GitHub);
assert_eq!(ctx.options.token.as_deref(), Some("ghp-from-env"));
});
}
#[test]
fn test_resolve_scm_token_type_anodizer_github_token_takes_precedence() {
with_clean_token_env(|| {
unsafe { std::env::set_var("ANODIZER_GITHUB_TOKEN", "anodizer-tok") };
unsafe { std::env::set_var("GITHUB_TOKEN", "gh-tok") };
let config = Config::default();
let mut ctx = Context::new(config.clone(), ContextOptions::default());
resolve_scm_token_type(&mut ctx, &config);
assert_eq!(ctx.token_type, ScmTokenType::GitHub);
assert_eq!(
ctx.options.token.as_deref(),
Some("anodizer-tok"),
"ANODIZER_GITHUB_TOKEN should take precedence over GITHUB_TOKEN"
);
});
}
#[test]
fn test_resolve_scm_token_type_cli_token_preserved() {
with_clean_token_env(|| {
unsafe { std::env::set_var("GITHUB_TOKEN", "from-env") };
let config = Config::default();
let opts = ContextOptions {
token: Some("from-cli".to_string()),
..Default::default()
};
let mut ctx = Context::new(config.clone(), opts);
resolve_scm_token_type(&mut ctx, &config);
assert_eq!(ctx.token_type, ScmTokenType::GitHub);
assert_eq!(
ctx.options.token.as_deref(),
Some("from-cli"),
"CLI --token flag should not be overwritten by env var"
);
});
}
#[test]
fn test_resolve_scm_token_type_force_overrides_env_detection() {
with_clean_token_env(|| {
unsafe { std::env::set_var("GITLAB_TOKEN", "glpat-ignored") };
let config = Config {
force_token: Some(ForceTokenKind::GitHub),
..Default::default()
};
let mut ctx = Context::new(config.clone(), ContextOptions::default());
resolve_scm_token_type(&mut ctx, &config);
assert_eq!(
ctx.token_type,
ScmTokenType::GitHub,
"force_token should override env-based detection"
);
assert!(
ctx.options.token.is_none(),
"no GitHub token env var set, so token should remain None"
);
});
}
#[test]
fn test_resolve_scm_token_type_gitlab_priority_over_gitea() {
with_clean_token_env(|| {
unsafe { std::env::set_var("GITLAB_TOKEN", "gl-tok") };
unsafe { std::env::set_var("GITEA_TOKEN", "gt-tok") };
let config = Config::default();
let mut ctx = Context::new(config.clone(), ContextOptions::default());
resolve_scm_token_type(&mut ctx, &config);
assert_eq!(
ctx.token_type,
ScmTokenType::GitLab,
"GITLAB_TOKEN should be checked before GITEA_TOKEN"
);
assert_eq!(ctx.options.token.as_deref(), Some("gl-tok"));
});
}
#[test]
fn test_resolve_scm_token_type_anodizer_force_token_env_gitlab() {
with_clean_token_env(|| {
unsafe { std::env::set_var("ANODIZER_FORCE_TOKEN", "gitlab") };
unsafe { std::env::set_var("GITLAB_TOKEN", "glpat-env") };
let config = Config::default();
let mut ctx = Context::new(config.clone(), ContextOptions::default());
resolve_scm_token_type(&mut ctx, &config);
assert_eq!(
ctx.token_type,
ScmTokenType::GitLab,
"ANODIZER_FORCE_TOKEN=gitlab should force GitLab"
);
assert_eq!(ctx.options.token.as_deref(), Some("glpat-env"));
});
}
#[test]
fn test_resolve_scm_token_type_anodizer_force_token_env_github() {
with_clean_token_env(|| {
unsafe { std::env::set_var("ANODIZER_FORCE_TOKEN", "github") };
unsafe { std::env::set_var("GITLAB_TOKEN", "glpat-ignored") };
unsafe { std::env::set_var("GITHUB_TOKEN", "ghp-forced") };
let config = Config::default();
let mut ctx = Context::new(config.clone(), ContextOptions::default());
resolve_scm_token_type(&mut ctx, &config);
assert_eq!(
ctx.token_type,
ScmTokenType::GitHub,
"ANODIZER_FORCE_TOKEN=github should override GITLAB_TOKEN detection"
);
assert_eq!(ctx.options.token.as_deref(), Some("ghp-forced"));
});
}
#[test]
fn test_resolve_scm_token_type_goreleaser_force_token_compat() {
with_clean_token_env(|| {
unsafe { std::env::set_var("GORELEASER_FORCE_TOKEN", "gitea") };
unsafe { std::env::set_var("GITEA_TOKEN", "gitea-compat") };
let config = Config::default();
let mut ctx = Context::new(config.clone(), ContextOptions::default());
resolve_scm_token_type(&mut ctx, &config);
assert_eq!(
ctx.token_type,
ScmTokenType::Gitea,
"GORELEASER_FORCE_TOKEN should work as compat fallback"
);
assert_eq!(ctx.options.token.as_deref(), Some("gitea-compat"));
});
}
#[test]
fn test_resolve_scm_token_type_anodizer_force_token_overrides_goreleaser() {
with_clean_token_env(|| {
unsafe { std::env::set_var("ANODIZER_FORCE_TOKEN", "github") };
unsafe { std::env::set_var("GORELEASER_FORCE_TOKEN", "gitlab") };
unsafe { std::env::set_var("GITHUB_TOKEN", "ghp-wins") };
unsafe { std::env::set_var("GITLAB_TOKEN", "glpat-loses") };
let config = Config::default();
let mut ctx = Context::new(config.clone(), ContextOptions::default());
resolve_scm_token_type(&mut ctx, &config);
assert_eq!(
ctx.token_type,
ScmTokenType::GitHub,
"ANODIZER_FORCE_TOKEN should take precedence over GORELEASER_FORCE_TOKEN"
);
assert_eq!(ctx.options.token.as_deref(), Some("ghp-wins"));
});
}
#[test]
fn test_resolve_scm_token_type_config_force_token_overrides_env() {
with_clean_token_env(|| {
unsafe { std::env::set_var("ANODIZER_FORCE_TOKEN", "gitlab") };
unsafe { std::env::set_var("GITHUB_TOKEN", "ghp-config") };
let config = Config {
force_token: Some(ForceTokenKind::GitHub),
..Default::default()
};
let mut ctx = Context::new(config.clone(), ContextOptions::default());
resolve_scm_token_type(&mut ctx, &config);
assert_eq!(
ctx.token_type,
ScmTokenType::GitHub,
"config.force_token should override ANODIZER_FORCE_TOKEN env var"
);
assert_eq!(ctx.options.token.as_deref(), Some("ghp-config"));
});
}
#[test]
fn test_resolve_scm_token_type_invalid_force_token_env_ignored() {
with_clean_token_env(|| {
unsafe { std::env::set_var("ANODIZER_FORCE_TOKEN", "invalid") };
unsafe { std::env::set_var("GITLAB_TOKEN", "glpat-detected") };
let config = Config::default();
let mut ctx = Context::new(config.clone(), ContextOptions::default());
resolve_scm_token_type(&mut ctx, &config);
assert_eq!(
ctx.token_type,
ScmTokenType::GitLab,
"invalid ANODIZER_FORCE_TOKEN should fall back to env detection"
);
assert_eq!(ctx.options.token.as_deref(), Some("glpat-detected"));
});
}
}