mod cliff;
mod git;
use anyhow::{Context, Result};
use clap::Parser;
use std::env;
use std::fs;
use std::path::PathBuf;
#[derive(Parser)]
struct Args {
#[arg(short = 'C', long, default_value = ".")]
repo_path: PathBuf,
#[arg(long)]
version: String,
#[arg(long)]
current_version: String,
#[arg(long, default_value = "CHANGELOG.md")]
changelog_path: String,
#[arg(long)]
server_host: String,
#[arg(long)]
project_path: String,
#[arg(long)]
branch: String,
#[arg(long, default_value = "")]
extra_files: String,
#[arg(long, default_value = "Phoenix CI")]
author_name: String,
#[arg(long, default_value = "ci@phoenix.invalid")]
author_email: String,
#[arg(long, default_value = "release-commit.env")]
env_output: PathBuf,
}
fn files_to_commit(changelog_path: &str, extra_files: &str) -> Vec<String> {
let mut files = vec!["pixi.toml".to_string(), "codemeta.json".to_string(), changelog_path.to_string()];
files.extend(extra_files.split_whitespace().map(String::from));
files
}
fn remote_push_url(token: &str, server_host: &str, project_path: &str) -> String {
format!("https://oauth2:{token}@{server_host}/{project_path}.git")
}
fn env_file_content(sha: &str, version: &str, current_version: &str) -> String {
format!("RELEASE_COMMIT_SHA={sha}\nREPLACE_VERSION={version}\nCURRENT_VERSION={current_version}\n")
}
fn main() -> Result<()> {
let args = Args::parse();
git::trust_directory(&args.repo_path)?;
cliff::prepend_changelog(&args.repo_path, &args.version, &args.changelog_path)?;
let token = env::var("GITLAB_TOKEN")
.or_else(|_| env::var("GL_TOKEN"))
.context("GITLAB_TOKEN or GL_TOKEN must be set")?;
let remote_url = remote_push_url(&token, &args.server_host, &args.project_path);
git::set_remote_url(&args.repo_path, "origin", &remote_url)?;
for file in files_to_commit(&args.changelog_path, &args.extra_files) {
if args.repo_path.join(&file).is_file() {
git::add(&args.repo_path, &file)?;
}
}
let message = format!("chore(release): {} [skip ci]", args.version);
git::commit(&args.repo_path, &args.author_name, &args.author_email, &message)?;
git::push(&args.repo_path, "origin", &format!("HEAD:{}", args.branch))?;
git::tag(&args.repo_path, &args.version)?;
git::push(&args.repo_path, "origin", &args.version)?;
let sha = git::rev_parse_head(&args.repo_path)?;
fs::write(&args.env_output, env_file_content(&sha, &args.version, &args.current_version))
.with_context(|| format!("failed to write {}", args.env_output.display()))?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn files_to_commit_includes_builtins_and_extra_files() {
let files = files_to_commit("CHANGELOG.md", "Cargo.toml Cargo.lock");
assert_eq!(files, vec!["pixi.toml", "codemeta.json", "CHANGELOG.md", "Cargo.toml", "Cargo.lock"]);
}
#[test]
fn files_to_commit_handles_no_extra_files() {
let files = files_to_commit("CHANGELOG.md", "");
assert_eq!(files, vec!["pixi.toml", "codemeta.json", "CHANGELOG.md"]);
}
#[test]
fn files_to_commit_collapses_extra_whitespace() {
let files = files_to_commit("CHANGELOG.md", " CMakeLists.txt Cargo.toml ");
assert_eq!(files, vec!["pixi.toml", "codemeta.json", "CHANGELOG.md", "CMakeLists.txt", "Cargo.toml"]);
}
#[test]
fn remote_push_url_embeds_the_token_as_oauth2_password() {
let url = remote_push_url("tok123", "gitlab.example.com", "group/project");
assert_eq!(url, "https://oauth2:tok123@gitlab.example.com/group/project.git");
}
#[test]
fn env_file_content_exposes_the_three_expected_variables() {
let content = env_file_content("abc123", "1.2.3", "1.2.2");
assert_eq!(content, "RELEASE_COMMIT_SHA=abc123\nREPLACE_VERSION=1.2.3\nCURRENT_VERSION=1.2.2\n");
}
}