xtask/cmd/release/
publish.rs

1use std::collections::HashSet;
2
3use anyhow::Context;
4
5use super::utils::{GitReleaseArtifact, Package};
6use crate::cmd::release::utils::WorkspaceReleaseMetadata;
7use crate::utils::{self, Command, cargo_cmd, concurrently, git_workdir_clean};
8
9#[derive(Debug, Clone, clap::Parser)]
10pub struct Publish {
11    /// Packages to include in the check
12    /// by default all packages are included
13    #[arg(long = "package", short = 'p')]
14    packages: Vec<String>,
15    /// Allow the command to execute even if there are uncomitted changes in the workspace
16    #[arg(long)]
17    allow_dirty: bool,
18    /// Concurrency to run at. By default, this is the total number of cpus on the host.
19    #[arg(long, default_value_t = num_cpus::get())]
20    concurrency: usize,
21    /// Do not release anything.
22    #[arg(long)]
23    dry_run: bool,
24    /// Token to use when uploading to crates.io
25    #[arg(long)]
26    crates_io_token: Option<String>,
27}
28
29impl Publish {
30    pub fn run(self) -> anyhow::Result<()> {
31        if !self.allow_dirty {
32            git_workdir_clean()?;
33        }
34
35        // get current commit
36        let current_commit = Command::new("git")
37            .arg("rev-parse")
38            .arg("HEAD")
39            .output()
40            .context("git rev-parse HEAD")?;
41
42        if !current_commit.status.success() {
43            anyhow::bail!(
44                "failed to get current commit sha: {}",
45                String::from_utf8_lossy(&current_commit.stderr)
46            );
47        }
48
49        let current_commit = String::from_utf8_lossy(&current_commit.stdout);
50        let current_commit = current_commit.trim();
51
52        let metadata = utils::metadata().context("metadata")?;
53
54        let workspace_relese_metadata =
55            WorkspaceReleaseMetadata::from_metadadata(&metadata).context("workspace metadata")?;
56
57        let packages = {
58            let members = metadata.workspace_members.iter().collect::<HashSet<_>>();
59            metadata
60                .packages
61                .iter()
62                .filter(|p| members.contains(&p.id))
63                .filter(|p| self.packages.contains(&p.name) || self.packages.is_empty())
64                .map(|p| Package::new(&workspace_relese_metadata, p.clone()))
65                .collect::<anyhow::Result<Vec<_>>>()?
66        };
67
68        concurrently::<_, _, anyhow::Result<()>>(self.concurrency, packages.iter(), |p| p.fetch_published())?;
69
70        let mut crates_io_publish = Vec::new();
71        let mut git_releases = Vec::new();
72
73        for package in &packages {
74            if !package.slated_for_release() {
75                tracing::info!("{} is not slated for release", package.name);
76                continue;
77            }
78
79            if package.last_published_version().is_some_and(|p| p.vers == package.version) {
80                tracing::info!("{}@{} has already been released on crates.io", package.name, package.version);
81            } else if package.should_publish() {
82                tracing::info!("{}@{} has not yet been published", package.name, package.version);
83                crates_io_publish.push(&package.name);
84            }
85
86            if let Some(git) = package.git_release().context("git release")? {
87                git_releases.push((package, git));
88            }
89        }
90
91        if !crates_io_publish.is_empty() {
92            let mut release_cmd = cargo_cmd();
93
94            release_cmd
95                .env("RUSTC_BOOTSTRAP", "1")
96                .arg("-Zunstable-options")
97                .arg("-Zpackage-workspace")
98                .arg("publish")
99                .arg("--no-verify");
100
101            if self.dry_run {
102                release_cmd.arg("--dry-run");
103            }
104
105            for package in &crates_io_publish {
106                release_cmd.arg("-p").arg(package.as_ref());
107            }
108
109            if let Some(token) = &self.crates_io_token {
110                release_cmd.arg("--token").arg(token);
111            }
112
113            if !release_cmd.status().context("crates io release")?.success() {
114                anyhow::bail!("failed to publish crates");
115            }
116        }
117
118        for (package, release) in &git_releases {
119            let gh_release_view = Command::new("gh")
120                .arg("release")
121                .arg("view")
122                .arg(release.tag_name.trim())
123                .arg("--json")
124                .arg("url")
125                .output()
126                .context("gh release view")?;
127
128            if gh_release_view.status.success() {
129                tracing::info!("{} is already released", release.tag_name.trim());
130                continue;
131            }
132
133            let mut gh_release_create = Command::new("gh");
134
135            gh_release_create
136                .arg("release")
137                .arg("create")
138                .arg(release.tag_name.trim())
139                .arg("--target")
140                .arg(current_commit)
141                .arg("--title")
142                .arg(release.name.trim())
143                .arg("--notes")
144                .arg(release.body.trim());
145
146            for artifact in &release.artifacts {
147                match artifact {
148                    GitReleaseArtifact::File { path, name } => {
149                        let artifact = package.manifest_path.parent().unwrap().join(path);
150                        let name = name.as_deref().or_else(|| artifact.file_name());
151                        gh_release_create.arg(if let Some(name) = name {
152                            format!("{artifact}#{name}")
153                        } else {
154                            artifact.to_string()
155                        });
156                    }
157                }
158            }
159
160            if !self.dry_run {
161                if !gh_release_create.status().context("gh release create")?.success() {
162                    anyhow::bail!("failed to create gh release");
163                }
164            } else {
165                tracing::info!("skipping running: {gh_release_create}")
166            }
167        }
168
169        tracing::info!("released packages");
170
171        Ok(())
172    }
173}