xtask/cmd/release/
publish.rs1use 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 #[arg(long = "package", short = 'p')]
14 packages: Vec<String>,
15 #[arg(long)]
17 allow_dirty: bool,
18 #[arg(long, default_value_t = num_cpus::get())]
20 concurrency: usize,
21 #[arg(long)]
23 dry_run: bool,
24 #[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 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(¤t_commit.stderr)
46 );
47 }
48
49 let current_commit = String::from_utf8_lossy(¤t_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}