Skip to main content

torii_lib/platforms/
release.rs

1//! Release page management — `torii release …`.
2//!
3//! Both GitLab and GitHub expose "Release" entities: a named tag-anchored
4//! object with a description (markdown) and a list of assets (links and/or
5//! uploaded files). gitorii's CI creates these via the API; this module is
6//! the CLI surface to edit notes, fix typos, and delete bad releases without
7//! having to rebuild the entire pipeline.
8//!
9//! Asymmetries between platforms (documented inline where they exist):
10//!   - GitLab: release description is one Markdown blob plus a list of
11//!     asset *links* (URLs pointing into the Package Registry). To
12//!     "delete an asset" you delete the *package* (`torii package
13//!     delete`), the release re-renders the link list automatically.
14//!   - GitHub: release description is one Markdown blob plus uploaded
15//!     binary *assets* attached directly to the release. Each asset
16//!     has its own id and is deletable.
17
18use super::azure::AzureReleaseClient;
19use super::bitbucket::BitbucketReleaseClient;
20use super::gitea::GiteaReleaseClient;
21use super::github::GitHubReleaseClient;
22use super::gitlab::GitLabReleaseClient;
23use super::radicle::RadicleReleaseClient;
24use super::sourcehut::SourcehutReleaseClient;
25use crate::error::{Result, ToriiError};
26use serde::{Deserialize, Serialize};
27
28#[derive(Debug, Clone, Serialize, Deserialize)]
29pub struct Release {
30    /// The tag this release is anchored to (e.g. "v0.7.9"). Used as the
31    /// identifier in API paths since releases don't have separate numeric
32    /// ids in GitLab; GitHub has both an id and a tag, we use the tag for
33    /// CLI ergonomics.
34    pub tag: String,
35    pub name: String,
36    pub description: String,
37    pub created_at: String,
38    pub web_url: String,
39    /// Optional release id (GitHub uses this for API paths; GitLab uses tag).
40    pub id: Option<String>,
41}
42
43#[allow(dead_code)]
44pub trait ReleaseClient: Send {
45    fn list(&self, owner: &str, repo: &str, limit: usize) -> Result<Vec<Release>>;
46    fn get(&self, owner: &str, repo: &str, tag: &str) -> Result<Release>;
47    /// Update release metadata. `name` / `description` are both optional —
48    /// pass None to leave them unchanged.
49    fn edit(
50        &self,
51        owner: &str,
52        repo: &str,
53        tag: &str,
54        name: Option<&str>,
55        description: Option<&str>,
56    ) -> Result<()>;
57    /// Delete the release. On GitLab this only removes the release entity
58    /// (the underlying tag stays); on GitHub the release deletion API
59    /// likewise leaves the tag intact (use `torii tag delete` separately
60    /// if you want both gone).
61    fn delete(&self, owner: &str, repo: &str, tag: &str) -> Result<()>;
62}
63
64// ============================================================================
65// GitHub Releases
66// ============================================================================
67
68pub fn get_release_client(platform: &str) -> Result<Box<dyn ReleaseClient>> {
69    match platform.to_lowercase().as_str() {
70        "github"    => Ok(Box::new(GitHubReleaseClient::new()?)),
71        "gitlab"    => Ok(Box::new(GitLabReleaseClient::new()?)),
72        "gitea"     => Ok(Box::new(GiteaReleaseClient::new()?)),
73        "sourcehut" => Ok(Box::new(SourcehutReleaseClient::new()?)),
74        "radicle"   => Ok(Box::new(RadicleReleaseClient::new()?)),
75        "bitbucket" => Ok(Box::new(BitbucketReleaseClient::new()?)),
76        "azure"     => Ok(Box::new(AzureReleaseClient::new()?)),
77        other => Err(ToriiError::Unsupported(format!("Unsupported platform: {}. Supported: github, gitlab, gitea, sourcehut, radicle, bitbucket, azure", other))),
78    }
79}
80
81#[cfg(test)]
82mod tests {
83    use crate::platforms::github::release::parse_github_release;
84    use crate::platforms::gitlab::release::parse_gitlab_release;
85
86    #[test]
87    fn parse_github_release_basic() {
88        let json = serde_json::json!({
89            "id": 12345u64,
90            "tag_name": "v0.7.9",
91            "name": "Gitorii v0.7.9",
92            "body": "Release notes here",
93            "created_at": "2026-05-19T22:00:00Z",
94            "html_url": "https://github.com/paskidev/gitorii/releases/tag/v0.7.9"
95        });
96        let r = parse_github_release(&json).unwrap();
97        assert_eq!(r.tag, "v0.7.9");
98        assert_eq!(r.name, "Gitorii v0.7.9");
99        assert_eq!(r.id.as_deref(), Some("12345"));
100    }
101
102    #[test]
103    fn parse_gitlab_release_basic() {
104        let json = serde_json::json!({
105            "tag_name": "v0.7.9",
106            "name": "Gitorii v0.7.9",
107            "description": "Release notes",
108            "created_at": "2026-05-19T22:00:00Z",
109            "_links": { "self": "https://gitlab.com/paskidev/gitorii/-/releases/v0.7.9" }
110        });
111        let r = parse_gitlab_release(&json).unwrap();
112        assert_eq!(r.tag, "v0.7.9");
113        assert_eq!(r.id, None);
114        assert_eq!(
115            r.web_url,
116            "https://gitlab.com/paskidev/gitorii/-/releases/v0.7.9"
117        );
118    }
119}