Skip to main content

qtcloud_devops_cli/commands/
publish.rs

1use 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}