qtcloud_devops_cli/commands/
stage.rs1use std::path::Path;
2
3use crate::model::release::{FileStorage, ReleaseRecord, ReleaseStatus, Storage};
4
5pub fn run(version: &str, repo_path: &Path) -> Result<String, Box<dyn std::error::Error>> {
6 if !crate::commands::release::validate_version(version) {
7 return Err(format!("版本号格式错误: {}", version).into());
8 }
9
10 let mut storage = FileStorage::new(repo_path);
11
12 if let Some(existing) = storage.load(version) {
13 match existing.status {
14 ReleaseStatus::Published => {
15 return Err(format!("版本 {} 已发布,不可重复 stage", version).into());
16 }
17 ReleaseStatus::Staged => {
18 let now = std::time::SystemTime::now()
19 .duration_since(std::time::UNIX_EPOCH)
20 .unwrap_or_default()
21 .as_secs()
22 .to_string();
23 let mut updated = existing.clone();
24 updated.updated_at = now;
25 storage.save(&updated)?;
26 return Ok(updated.id);
27 }
28 ReleaseStatus::Cancelled => {}
29 ReleaseStatus::Retired => {
30 return Err(format!("版本 {} 已退役,不可重复 stage", version).into());
31 }
32 }
33 }
34
35 let record = ReleaseRecord::new_staged(version);
36 storage.save(&record)?;
37 println!("✓ 版本 {} 已进入 Staged 状态 (发布尝试 ID: {})", version, record.id);
38 Ok(record.id)
39}
40
41#[cfg(test)]
42mod tests {
43 use super::*;
44 use crate::model::release::Storage;
45
46 fn make_record(version: &str, status: ReleaseStatus) -> ReleaseRecord {
47 let now = std::time::SystemTime::now()
48 .duration_since(std::time::UNIX_EPOCH)
49 .unwrap_or_default()
50 .as_secs()
51 .to_string();
52 ReleaseRecord {
53 id: uuid::Uuid::new_v4().to_string(),
54 version: version.to_string(),
55 status,
56 created_at: now.clone(),
57 updated_at: now,
58 }
59 }
60
61 #[test]
62 fn test_stage_new_version() {
63 let dir = tempfile::tempdir().unwrap();
64 let id = run("v1.0.0", dir.path()).unwrap();
65 assert!(!id.is_empty());
66
67 let s = FileStorage::new(dir.path());
68 let r = s.load("v1.0.0").unwrap();
69 assert_eq!(r.status, ReleaseStatus::Staged);
70 }
71
72 #[test]
73 fn test_stage_invalid_version() {
74 let dir = tempfile::tempdir().unwrap();
75 assert!(run("bad", dir.path()).is_err());
76 }
77
78 #[test]
79 fn test_stage_published_rejected() {
80 let dir = tempfile::tempdir().unwrap();
81 let mut s = FileStorage::new(dir.path());
82 s.save(&make_record("v1.0.0", ReleaseStatus::Published))
83 .unwrap();
84 let err = run("v1.0.0", dir.path()).unwrap_err().to_string();
85 assert!(err.contains("已发布"));
86 }
87
88 #[test]
89 fn test_stage_cancelled_restage() {
90 let dir = tempfile::tempdir().unwrap();
91 let old_id;
92 {
93 let mut s = FileStorage::new(dir.path());
94 let r = make_record("v1.0.0", ReleaseStatus::Cancelled);
95 old_id = r.id.clone();
96 s.save(&r).unwrap();
97 }
98 let id = run("v1.0.0", dir.path()).unwrap();
99 assert!(!id.is_empty());
100
101 let s = FileStorage::new(dir.path());
102 let loaded = s.load("v1.0.0").unwrap();
103 assert_eq!(loaded.status, ReleaseStatus::Staged);
104 assert_ne!(loaded.id, old_id);
105 }
106
107 #[test]
108 fn test_stage_retired_rejected() {
109 let dir = tempfile::tempdir().unwrap();
110 let mut s = FileStorage::new(dir.path());
111 s.save(&make_record("v1.0.0", ReleaseStatus::Retired))
112 .unwrap();
113 assert!(run("v1.0.0", dir.path()).unwrap_err().to_string().contains("退役"));
114 }
115
116 #[test]
117 fn test_stage_idempotent() {
118 let dir = tempfile::tempdir().unwrap();
119 let id1 = run("v1.0.0", dir.path()).unwrap();
120 let id2 = run("v1.0.0", dir.path()).unwrap();
121 assert_eq!(id1, id2);
122 assert_eq!(FileStorage::new(dir.path()).list().len(), 1);
123 }
124}