qtcloud_devops_cli/commands/
publish.rs1use std::path::Path;
2
3use crate::model::release::{FileStorage, ReleaseStatus, Storage};
4
5pub fn run(
6 version: &str,
7 repo_path: &Path,
8 yes: bool,
9) -> Result<String, Box<dyn std::error::Error>> {
10 let mut storage = FileStorage::new(repo_path);
11 let mut record = storage
12 .load(version)
13 .ok_or_else(|| format!("版本 {} 不存在,请先执行 stage", version))?;
14
15 if record.status != ReleaseStatus::Staged {
16 return Err(format!(
17 "版本 {} 不处于 Staged 状态 (当前: {:?})",
18 version, record.status
19 )
20 .into());
21 }
22
23 if !crate::commands::release::confirm_release(version, yes) {
24 return Err("已取消发布".into());
25 }
26
27 let tag_ok = crate::commands::release::create_tag(version, repo_path);
28 if !tag_ok {
29 return Err(format!("创建标签 {} 失败", version).into());
30 }
31
32 let push_ok = crate::commands::release::push_tag(version, repo_path);
33 if !push_ok {
34 crate::commands::release::rollback_tag(version, repo_path);
35 return Err(format!("推送标签 {} 失败", version).into());
36 }
37 println!("✓ 标签 {} 已创建并推送", version);
38
39 let changelog_path = repo_path.join("CHANGELOG.md");
40 let notes = crate::commands::release::extract_notes(version, &changelog_path);
41
42 if let Some(repo) = crate::commands::release::get_remote_repo(repo_path) {
43 let release_ok = crate::commands::release::create_release(
44 version,
45 notes.as_deref().unwrap_or(""),
46 &repo,
47 );
48 if !release_ok {
49 crate::commands::release::rollback_tag(version, repo_path);
50 return Err("创建 GitHub Release 失败".into());
51 }
52 println!("✓ GitHub Release {} 已创建", version);
53 println!(" https://github.com/{}/releases/tag/{}", repo, version);
54 }
55
56 record.status = ReleaseStatus::Published;
57 record.updated_at = std::time::SystemTime::now()
58 .duration_since(std::time::UNIX_EPOCH)
59 .unwrap_or_default()
60 .as_secs()
61 .to_string();
62 storage.save(&record)?;
63
64 let id = record.id.clone();
65 println!("✓ 版本 {} 已发布 (发布尝试 ID: {})", version, id);
66 Ok(id)
67}
68
69#[cfg(test)]
70mod tests {
71 use super::*;
72 use crate::model::release::{ReleaseRecord, ReleaseStatus, Storage};
73
74 fn make_staged(version: &str) -> ReleaseRecord {
75 ReleaseRecord::new_staged(version)
76 }
77
78 #[test]
79 fn test_publish_not_staged() {
80 let dir = tempfile::tempdir().unwrap();
81 std::fs::write(dir.path().join("CHANGELOG.md"), "## [1.0.0]\n\ncontent").unwrap();
82 {
83 let mut s = FileStorage::new(dir.path());
84 let mut r = make_staged("v1.0.0");
85 r.status = ReleaseStatus::Cancelled;
86 s.save(&r).unwrap();
87 }
88 assert!(run("v1.0.0", dir.path(), true).is_err());
89 }
90
91 #[test]
92 fn test_publish_not_found() {
93 let dir = tempfile::tempdir().unwrap();
94 std::fs::write(dir.path().join("CHANGELOG.md"), "## [1.0.0]\n\ncontent").unwrap();
95 let err = run("v1.0.0", dir.path(), true).unwrap_err().to_string();
96 assert!(err.contains("请先执行 stage"));
97 }
98
99 #[test]
100 fn test_publish_user_cancels() {
101 let dir = tempfile::tempdir().unwrap();
102 std::fs::write(dir.path().join("CHANGELOG.md"), "## [1.0.0]\n\ncontent").unwrap();
103 let mut s = FileStorage::new(dir.path());
104 s.save(&make_staged("v1.0.0")).unwrap();
105 assert!(run("v1.0.0", dir.path(), false).is_err());
106 }
107}