1use std::{
2 collections::{hash_map::Entry, HashMap},
3 env,
4 path::{Path, PathBuf},
5 process::Command,
6};
7
8use anyhow::{Context, Result};
9use cargo_metadata::{semver::Version, DependencyKind};
10use changesets::ChangeType;
11use clap::{Parser, Subcommand};
12use indexmap::IndexSet;
13use petgraph::{prelude::DiGraphMap, Direction};
14
15#[derive(Debug, Parser)]
16struct CliArgs {
17 #[clap(long)]
18 pub dry_run: bool,
19
20 #[clap(subcommand)]
21 pub cmd: Cmd,
22}
23
24#[derive(Debug, Subcommand)]
25enum Cmd {
26 Bump,
27}
28
29fn main() -> Result<()> {
30 let CliArgs { dry_run, cmd } = CliArgs::parse();
31
32 let workspace_dir = env::var("CARGO_WORKSPACE_DIR")
33 .map(PathBuf::from)
34 .context("CARGO_WORKSPACE_DIR is not set")?;
35
36 match cmd {
37 Cmd::Bump => {
38 run_bump(&workspace_dir, dry_run)?;
39 }
40 }
41
42 Ok(())
43}
44
45fn run_bump(workspace_dir: &Path, dry_run: bool) -> Result<()> {
46 let changeset_dir = workspace_dir.join(".changeset");
47
48 let changeset = changesets::ChangeSet::from_directory(&changeset_dir)
49 .context("failed to load changeset")?;
50
51 if changeset.releases.is_empty() {
52 eprintln!("No changeset found");
53 return Ok(());
54 }
55
56 let (versions, graph) = get_data()?;
57 let mut new_versions = VersionMap::new();
58
59 let mut worker = Bump {
60 versions: &versions,
61 graph: &graph,
62 new_versions: &mut new_versions,
63 };
64
65 for (pkg_name, release) in changeset.releases {
66 let is_breaking = worker
67 .is_breaking(pkg_name.as_str(), release.change_type())
68 .with_context(|| format!("failed to check if package {pkg_name} is breaking"))?;
69
70 worker
71 .bump_crate(pkg_name.as_str(), release.change_type(), is_breaking)
72 .with_context(|| format!("failed to bump package {pkg_name}"))?;
73 }
74
75 for (pkg_name, version) in new_versions {
76 run_cargo_set_version(&pkg_name, &version, dry_run)
77 .with_context(|| format!("failed to set version for {pkg_name}"))?;
78 }
79
80 {
81 eprintln!("Removing changeset files... ");
82 if !dry_run {
83 std::fs::remove_dir_all(&changeset_dir).context("failed to remove changeset files")?;
84 }
85 }
86
87 {
88 update_changelog().with_context(|| "failed to update changelog")?;
91 }
92
93 git_commit(dry_run).context("failed to commit")?;
94 git_tag_core(dry_run).context("failed to tag core")?;
95
96 Ok(())
97}
98
99fn run_cargo_set_version(pkg_name: &str, version: &Version, dry_run: bool) -> Result<()> {
100 let mut cmd = Command::new("cargo");
101 cmd.arg("set-version")
102 .arg("-p")
103 .arg(pkg_name)
104 .arg(version.to_string());
105
106 eprintln!("Running {cmd:?}");
107
108 if dry_run {
109 return Ok(());
110 }
111
112 cmd.status().context("failed to run cargo set-version")?;
113
114 Ok(())
115}
116
117fn get_swc_core_version() -> Result<String> {
118 let md = cargo_metadata::MetadataCommand::new()
119 .no_deps()
120 .exec()
121 .expect("failed to run cargo metadata");
122
123 md.packages
124 .iter()
125 .find(|p| p.name == "swc_core")
126 .map(|p| p.version.to_string())
127 .context("failed to find swc_core")
128}
129
130fn git_commit(dry_run: bool) -> Result<()> {
131 let core_ver = get_swc_core_version()?;
132
133 let mut cmd = Command::new("git");
134 cmd.arg("commit").arg("-am").arg(format!(
135 "chore: Publish crates with `swc_core` `v{core_ver}`"
136 ));
137
138 eprintln!("Running {cmd:?}");
139
140 if dry_run {
141 return Ok(());
142 }
143
144 cmd.status().context("failed to run git commit")?;
145
146 Ok(())
147}
148
149fn git_tag_core(dry_run: bool) -> Result<()> {
150 let core_ver = get_swc_core_version()?;
151
152 let mut cmd = Command::new("git");
153 cmd.arg("tag").arg(format!("swc_core@v{core_ver}"));
154
155 eprintln!("Running {cmd:?}");
156
157 if dry_run {
158 return Ok(());
159 }
160
161 cmd.status().context("failed to run git tag")?;
162
163 Ok(())
164}
165
166struct Bump<'a> {
167 versions: &'a VersionMap,
169 graph: &'a InternedGraph,
171
172 new_versions: &'a mut VersionMap,
173}
174
175impl Bump<'_> {
176 fn is_breaking(&self, pkg_name: &str, change_type: Option<&ChangeType>) -> Result<bool> {
177 let original_version = self
178 .versions
179 .get(pkg_name)
180 .context(format!("failed to find original version for {pkg_name}"))?;
181
182 Ok(match change_type {
183 Some(ChangeType::Major) => true,
184 Some(ChangeType::Minor) => original_version.major == 0,
185 Some(ChangeType::Patch) => false,
186 Some(ChangeType::Custom(label)) => {
187 if label == "breaking" {
188 true
189 } else {
190 panic!("unknown custom change type: {label}")
191 }
192 }
193 None => false,
194 })
195 }
196
197 fn bump_crate(
198 &mut self,
199 pkg_name: &str,
200 change_type: Option<&ChangeType>,
201 is_breaking: bool,
202 ) -> Result<()> {
203 eprintln!("Bumping crate: {pkg_name}");
204
205 let original_version = self
206 .versions
207 .get(pkg_name)
208 .context(format!("failed to find original version for {pkg_name}"))?;
209
210 let mut new_version = original_version.clone();
211
212 match change_type {
213 Some(ChangeType::Patch) => {
214 new_version.patch += 1;
215 }
216 Some(ChangeType::Minor) => {
217 new_version.minor += 1;
218 new_version.patch = 0;
219 }
220 Some(ChangeType::Major) => {
221 new_version.major += 1;
222 new_version.minor = 0;
223 new_version.patch = 0;
224 }
225 Some(ChangeType::Custom(label)) => {
226 if label == "breaking" {
227 if original_version.major == 0 {
228 new_version.minor += 1;
229 new_version.patch = 0;
230 } else {
231 new_version.major += 1;
232 new_version.minor = 0;
233 new_version.patch = 0;
234 }
235 } else {
236 panic!("unknown custom change type: {label}")
237 }
238 }
239 None => {
240 if is_breaking {
241 if original_version.major == 0 {
242 new_version.minor += 1;
243 new_version.patch = 0;
244 } else {
245 new_version.major += 1;
246 new_version.minor = 0;
247 new_version.patch = 0;
248 }
249 } else {
250 new_version.patch += 1;
251 }
252 }
253 };
254
255 match self.new_versions.entry(pkg_name.to_string()) {
256 Entry::Vacant(v) => {
257 v.insert(new_version);
258 }
259 Entry::Occupied(mut o) => {
260 o.insert(new_version.max(o.get().clone()));
261 }
262 }
263
264 if is_breaking {
265 let a = self.graph.node(pkg_name);
268 for dep in self.graph.g.neighbors_directed(a, Direction::Incoming) {
269 let dep_name = &*self.graph.ix[dep];
270 eprintln!("Bumping dependant crate: {dep_name}");
271 self.bump_crate(dep_name, None, true)?;
272 }
273 }
274
275 Ok(())
276 }
277}
278
279fn update_changelog() -> Result<()> {
280 let mut cmd = Command::new("yarn");
282 cmd.arg("changelog");
283
284 eprintln!("Running {cmd:?}");
285
286 cmd.status().context("failed to run yarn changelog")?;
287
288 Ok(())
289}
290
291type VersionMap = HashMap<String, Version>;
292
293#[derive(Debug, Default)]
294struct InternedGraph {
295 ix: IndexSet<String>,
296 g: DiGraphMap<usize, ()>,
297}
298
299impl InternedGraph {
300 fn add_node(&mut self, name: String) -> usize {
301 self.ix.get_index_of(&name).unwrap_or_else(|| {
302 let ix = self.ix.len();
303 self.ix.insert_full(name);
304 ix
305 }) as _
306 }
307
308 fn node(&self, name: &str) -> usize {
309 self.ix.get_index_of(name).unwrap_or_else(|| {
310 panic!("unknown node: {name}");
311 })
312 }
313}
314
315fn get_data() -> Result<(VersionMap, InternedGraph)> {
316 let md = cargo_metadata::MetadataCommand::new()
317 .exec()
318 .expect("failed to run cargo metadata");
319
320 let workspace_packages = md
321 .workspace_packages()
322 .into_iter()
323 .filter(|p| p.publish != Some(vec![]))
324 .map(|p| p.name.clone())
325 .collect::<Vec<_>>();
326 let mut graph = InternedGraph::default();
327 let mut versions = VersionMap::new();
328
329 for pkg in md.workspace_packages() {
330 versions.insert(pkg.name.clone(), pkg.version.clone());
331 }
332
333 for pkg in md.workspace_packages() {
334 for dep in &pkg.dependencies {
335 if dep.kind != DependencyKind::Normal {
336 continue;
337 }
338
339 if workspace_packages.contains(&dep.name) {
340 let from = graph.add_node(pkg.name.clone());
341 let to = graph.add_node(dep.name.clone());
342
343 if from == to {
344 continue;
345 }
346
347 graph.g.add_edge(from, to, ());
348 }
349 }
350 }
351
352 Ok((versions, graph))
353}