1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157
// Copyright 2024 Google LLC
//
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
// https://www.apache.org/licenses/LICENSE-2.0> or the MIT license
// <LICENSE-MIT or https://opensource.org/licenses/MIT>, at your
// option. This file may not be copied, modified, or distributed
// except according to those terms.
//! Utilities for automatically releasing Rust code.
use crate::cmd::run_cmd;
use crate::{Package, Repo};
use anyhow::{Context, Result};
use crates_index::SparseIndex;
use std::env;
use std::process::Command;
/// Release each package in `packages`, if needed.
///
/// For each package, this will create a remote git tag (if it doesn't
/// already exist) and a crates.io release (if it doesn't already
/// exist).
///
/// Note that when releasing to crates.io, the order of `packages` may
/// be significant if the packages depend on one another.
pub fn release_packages(packages: &[Package]) -> Result<()> {
let commit_sha = get_github_sha()?;
let repo = Repo::open()?;
repo.fetch_git_tags()?;
let mut index = SparseIndex::new_cargo_default()?;
for package in packages {
auto_release_package(&repo, package, &mut index, &commit_sha)?;
}
Ok(())
}
/// Release a single package, if needed.
///
/// This publishes to crates.io if the corresponding version does not already
/// exist there, and also pushes a new git tag if one doesn't exist yet.
pub fn auto_release_package(
repo: &Repo,
package: &Package,
index: &mut SparseIndex,
commit_sha: &str,
) -> Result<()> {
let local_version = package.get_local_version()?;
println!("local version of {} is {local_version}", package.name());
// Create the crates.io release if it doesn't exist.
if does_crates_io_release_exist(package, &local_version, index)? {
println!(
"{}-{local_version} has already been published",
package.name()
);
} else {
publish_package(package)?;
}
// Create the remote git tag if it doesn't exist.
let tag = package.get_git_tag_name(&local_version);
if repo.does_git_tag_exist(&tag)? {
println!("git tag {tag} already exists");
} else {
repo.make_and_push_git_tag(&tag, commit_sha)?;
}
Ok(())
}
/// Get the commit to operate on from the `GITHUB_SHA` env var. When
/// running in Github Actions, this will be set to the SHA of the commit
/// that triggered the workflow.
///
/// See Github Actions' [Variables] documentation for details.
///
/// [Variables]: https://docs.github.com/en/actions/learn-github-actions/variables
pub fn get_github_sha() -> Result<String> {
let commit_var_name = "GITHUB_SHA";
env::var(commit_var_name).context(format!("failed to get env var {commit_var_name}"))
}
/// Returned by [`update_index`] to indicate whether a crate exists on
/// crates.io.
#[must_use]
pub struct RemoteCrateExists(pub bool);
/// Update the local crates.io cache.
///
/// Based on <https://github.com/frewsxcv/rust-crates-index/blob/HEAD/examples/sparse_http_ureq.rs>
pub fn update_index(index: &mut SparseIndex, package: &Package) -> Result<RemoteCrateExists> {
let crate_name = package.name();
println!("fetching updates for {}", package.name());
let request: ureq::Request = index.make_cache_request(crate_name).unwrap().into();
match request.call() {
Ok(response) => {
index.parse_cache_response(crate_name, response.into(), true)?;
Ok(RemoteCrateExists(true))
}
// Handle the case where the package does not yet have any
// releases.
Err(ureq::Error::Status(404, _)) => {
println!("packages {} does not exist yet", package.name());
Ok(RemoteCrateExists(false))
}
Err(err) => Err(err.into()),
}
}
/// Check if a new release of `package` should be published.
pub fn does_crates_io_release_exist(
package: &Package,
local_version: &str,
index: &mut SparseIndex,
) -> Result<bool> {
let remote_versions = get_remote_package_versions(package, index)?;
if remote_versions.contains(&local_version.to_string()) {
return Ok(true);
}
Ok(false)
}
/// Get all remote versions of `package`.
pub fn get_remote_package_versions(
package: &Package,
index: &mut SparseIndex,
) -> Result<Vec<String>> {
// The local cache may be out of date, fetch updates from the remote.
let exists = update_index(index, package)?;
// If the crate hasn't been published yet, return an empty list of versions.
if !exists.0 {
return Ok(Vec::new());
}
let cr = index.crate_from_cache(package.name())?;
Ok(cr
.versions()
.iter()
.map(|v| v.version().to_string())
.collect())
}
/// Publish `package` to crates.io.
pub fn publish_package(package: &Package) -> Result<()> {
let mut cmd = Command::new("cargo");
cmd.args(["publish", "--package", package.name()]);
run_cmd(cmd)?;
Ok(())
}