release_utils/
release.rs

1// Copyright 2024 Google LLC
2//
3// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
4// https://www.apache.org/licenses/LICENSE-2.0> or the MIT license
5// <LICENSE-MIT or https://opensource.org/licenses/MIT>, at your
6// option. This file may not be copied, modified, or distributed
7// except according to those terms.
8
9//! Utilities for automatically releasing Rust code.
10
11use crate::cmd::{run_cmd, RunCommandError};
12use crate::{
13    get_github_sha, CrateRegistry, GetCrateVersionsError, GetLocalVersionError,
14    Package, Repo, VarError,
15};
16use std::fmt::{self, Display, Formatter};
17use std::process::Command;
18
19/// Error returned by [`release_packages`].
20#[derive(Debug)]
21pub enum ReleasePackagesError {
22    /// Environment error.
23    Env(VarError),
24
25    /// A git error occurred.
26    Git(Box<dyn std::error::Error + Send + Sync + 'static>),
27
28    /// Failed to release a package.
29    Package {
30        /// Name of the package.
31        package: String,
32        /// Underlying error.
33        cause: ReleasePackageError,
34    },
35}
36
37impl Display for ReleasePackagesError {
38    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
39        match self {
40            Self::Env(_) => write!(f, "environment error"),
41            Self::Git(_) => write!(f, "git error"),
42            Self::Package { package, .. } => {
43                write!(f, "failed to release package {package}")
44            }
45        }
46    }
47}
48
49impl std::error::Error for ReleasePackagesError {
50    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
51        match self {
52            Self::Env(err) => Some(err),
53            Self::Git(err) => Some(&**err),
54            Self::Package { cause, .. } => Some(cause),
55        }
56    }
57}
58
59/// Release each package in `packages`, if needed.
60///
61/// For each package, this will create a remote git tag (if it doesn't
62/// already exist) and a crates.io release (if it doesn't already
63/// exist).
64///
65/// Note that when releasing to crates.io, the order of `packages` may
66/// be significant if the packages depend on one another.
67pub fn release_packages(
68    packages: &[Package],
69) -> Result<(), ReleasePackagesError> {
70    let commit_sha = get_github_sha().map_err(ReleasePackagesError::Env)?;
71
72    let repo =
73        Repo::open().map_err(|err| ReleasePackagesError::Git(Box::new(err)))?;
74    repo.fetch_git_tags()
75        .map_err(|err| ReleasePackagesError::Git(Box::new(err)))?;
76
77    for package in packages {
78        auto_release_package(&repo, package, &commit_sha).map_err(|err| {
79            ReleasePackagesError::Package {
80                package: package.name().to_string(),
81                cause: err,
82            }
83        })?;
84    }
85
86    Ok(())
87}
88
89/// Error returned by [`auto_release_package`].
90#[derive(Debug)]
91pub enum ReleasePackageError {
92    /// Failed to get the local version.
93    LocalVersion(GetLocalVersionError),
94
95    /// Failed to get the published versions of the crate.
96    RemoteVersions(GetCrateVersionsError),
97
98    /// Failed to publish the crate.
99    Publish(RunCommandError),
100
101    /// Failed to create or push the git tag.
102    Git(RunCommandError),
103}
104
105impl Display for ReleasePackageError {
106    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
107        match self {
108            Self::LocalVersion(_) => {
109                write!(f, "failed to get local package version")
110            }
111            Self::RemoteVersions(_) => {
112                write!(f, "failed to get the published package versions")
113            }
114            Self::Publish(_) => write!(f, "failed to publish the crate"),
115            Self::Git(_) => write!(f, "git error"),
116        }
117    }
118}
119
120impl std::error::Error for ReleasePackageError {
121    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
122        match self {
123            Self::LocalVersion(err) => Some(err),
124            Self::RemoteVersions(err) => Some(err),
125            Self::Publish(err) => Some(err),
126            Self::Git(err) => Some(err),
127        }
128    }
129}
130
131/// Release a single package, if needed.
132///
133/// This publishes to crates.io if the corresponding version does not already
134/// exist there, and also pushes a new git tag if one doesn't exist yet.
135pub fn auto_release_package(
136    repo: &Repo,
137    package: &Package,
138    commit_sha: &str,
139) -> Result<(), ReleasePackageError> {
140    let local_version = package
141        .get_local_version()
142        .map_err(ReleasePackageError::LocalVersion)?;
143    println!("local version of {} is {local_version}", package.name());
144
145    // Create the crates.io release if it doesn't exist.
146    if does_crates_io_release_exist(package, &local_version)
147        .map_err(ReleasePackageError::RemoteVersions)?
148    {
149        println!(
150            "{}-{local_version} has already been published",
151            package.name()
152        );
153    } else {
154        publish_package(package).map_err(ReleasePackageError::Publish)?;
155    }
156
157    // Create the remote git tag if it doesn't exist.
158    let tag = package.get_git_tag_name(&local_version);
159    if repo
160        .does_git_tag_exist(&tag)
161        .map_err(ReleasePackageError::Git)?
162    {
163        println!("git tag {tag} already exists");
164    } else {
165        repo.make_and_push_git_tag(&tag, commit_sha)
166            .map_err(ReleasePackageError::Git)?;
167    }
168
169    Ok(())
170}
171
172/// Check if a new release of `package` should be published.
173pub fn does_crates_io_release_exist(
174    package: &Package,
175    local_version: &str,
176) -> Result<bool, GetCrateVersionsError> {
177    let cargo = CrateRegistry::new();
178    let remote_versions = match cargo.get_crate_versions(package.name()) {
179        Ok(v) => v,
180        Err(GetCrateVersionsError::NotPublished) => Vec::new(),
181        Err(err) => return Err(err),
182    };
183
184    if remote_versions.contains(&local_version.to_string()) {
185        return Ok(true);
186    }
187
188    Ok(false)
189}
190
191/// Publish `package` to crates.io.
192pub fn publish_package(package: &Package) -> Result<(), RunCommandError> {
193    let mut cmd = Command::new("cargo");
194    cmd.args(["publish", "--package", package.name()]);
195    run_cmd(cmd)
196}