qtcloud_devops_cli/release/
publish.rs1use std::path::Path;
2
3use super::util::{self, Registry};
4
5pub fn publish(
24 version: &str,
25 repo_path: &Path,
26 yes: bool,
27 registry: Option<Registry>,
28) -> Result<(), Box<dyn std::error::Error>> {
29 if !util::validate_version(version) {
30 return Err(format!("版本号格式错误: {}", version).into());
31 }
32
33 if let Err(e) = super::ensure_changelog(repo_path, version) {
35 eprintln!(
36 "⚠ CHANGELOG 生成失败: {}\n 发布将继续,但请确保 CHANGELOG.md 包含版本 {} 的记录。",
37 e, version
38 );
39 }
41
42 let changelog_path = repo_path.join("CHANGELOG.md");
43 let precheck_errors = util::precheck_version_changelog(version, &changelog_path);
44 if !precheck_errors.is_empty() {
45 return Err(precheck_errors.join("\n").into());
46 }
47
48 if !yes && !util::confirm_release(version, false) {
49 return Err("已取消发布".into());
50 }
51
52 if !util::create_tag(version, repo_path) {
53 return Err(format!("创建标签 {} 失败", version).into());
54 }
55 if !util::push_tag(version, repo_path) {
56 util::rollback_tag(version, repo_path);
57 return Err(format!("推送标签 {} 失败", version).into());
58 }
59 println!("✓ 标签 {} 已创建并推送", version);
60
61 let notes = util::extract_notes(version, &changelog_path);
62 if let Some(repo) = util::get_remote_repo(repo_path) {
63 if !util::create_release(version, notes.as_deref().unwrap_or(""), &repo) {
64 util::rollback_tag(version, repo_path);
65 return Err("创建 GitHub Release 失败".into());
66 }
67 println!("✓ GitHub Release {} 已创建", version);
68 println!(" https://github.com/{}/releases/tag/{}", repo, version);
69 }
70 if let Some(reg) = registry {
71 println!(" {:?} 由 CI 自动发布,无需本地操作", reg);
72 }
73 println!("✓ 版本 {} 已发布", version);
74 Ok(())
75}
76
77#[cfg(test)]
78mod tests {
79 use super::*;
80 use std::path::Path;
81
82 fn git_init(path: &Path) {
83 std::process::Command::new("git")
84 .args(["init", "-b", "main"])
85 .current_dir(path)
86 .output()
87 .unwrap();
88 std::process::Command::new("git")
89 .args(["config", "user.email", "test@test.com"])
90 .current_dir(path)
91 .output()
92 .unwrap();
93 std::process::Command::new("git")
94 .args(["config", "user.name", "Test"])
95 .current_dir(path)
96 .output()
97 .unwrap();
98 }
99
100 fn git_commit(path: &Path, msg: &str) {
101 std::fs::write(path.join("file"), msg).unwrap();
102 std::process::Command::new("git")
103 .args(["add", "."])
104 .current_dir(path)
105 .output()
106 .unwrap();
107 std::process::Command::new("git")
108 .args(["commit", "-m", msg])
109 .current_dir(path)
110 .output()
111 .unwrap();
112 }
113
114 #[test]
115 fn test_publish_rejects_invalid_version() {
116 assert!(publish("bad", tempfile::tempdir().unwrap().path(), true, None).is_err());
117 }
118 #[test]
119 fn test_publish_auto_generates_changelog() {
120 let d = tempfile::tempdir().unwrap();
121 git_init(d.path());
122 git_commit(d.path(), "init");
123 let result = publish("v1.0.0", d.path(), true, None);
125 assert!(result.is_ok());
126 let changelog = std::fs::read_to_string(d.path().join("CHANGELOG.md")).unwrap_or_default();
127 assert!(changelog.contains("## [1.0.0]"));
128 }
129 #[test]
130 fn test_publish_formal_with_yes() {
131 let d = tempfile::tempdir().unwrap();
132 let r = publish("v1.0.0", d.path(), true, None);
133 assert!(r.is_ok() || r.is_err());
134 }
135 #[test]
136 fn test_publish_prerelease_with_yes() {
137 let d = tempfile::tempdir().unwrap();
138 git_init(d.path());
139 git_commit(d.path(), "init");
140 std::fs::write(
141 d.path().join("CHANGELOG.md"),
142 "## [1.0.0-rc.1]\n\ncontent\n",
143 )
144 .unwrap();
145 let r = publish("v1.0.0-rc.1", d.path(), true, None);
146 assert!(r.is_ok() || r.is_err());
147 }
148}