swc_releaser/
main.rs

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
89
90        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    /// Original versions
168    versions: &'a VersionMap,
169    /// Dependency graph
170    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            // Iterate over dependants
266
267            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    // Run `yarn changelog`
281    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}