xtask/
semver_checks.rs

1use std::collections::HashMap;
2use std::fmt::Display;
3use std::process::{Command, Stdio};
4
5use cargo_metadata::MetadataCommand;
6
7#[derive(Debug, Clone, Copy, clap::ValueEnum)]
8enum SemverType {
9    Patch,
10    Minor,
11    Major,
12}
13
14impl Display for SemverType {
15    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
16        match self {
17            SemverType::Patch => write!(f, "patch"),
18            SemverType::Minor => write!(f, "minor"),
19            SemverType::Major => write!(f, "major"),
20        }
21    }
22}
23
24#[derive(Debug, clap::Args)]
25pub struct SemverArgs {
26    /// Type of the next release
27    #[clap(long = "type", default_value_t = SemverType::Minor)]
28    tpe: SemverType,
29    /// Baseline version to check against. If that's not
30    /// set the version is inferred from the current package version
31    #[clap(long = "baseline-version")]
32    baseline_version: Option<String>,
33}
34
35impl SemverArgs {
36    pub fn run(&self) {
37        let metadata = MetadataCommand::default().exec().unwrap();
38        let false_positives_for_diesel = HashMap::from([
39            (
40                "inherent_method_missing",
41                // this method was not supposed to be public at all
42                &["SerializedDatabase::new"] as &[_],
43            ),
44            (
45                "trait_added_supertrait",
46                // false positive as cargo semver-checks does not perform trait solving
47                // https://github.com/obi1kenobi/cargo-semver-checks/issues/1265
48                &["trait diesel::connection::Instrumentation gained Downcast"] as &[_],
49            ),
50            (
51                "trait_no_longer_dyn_compatible",
52                // That's technically true, but
53                // noone is able to meaningful use these
54                // traits as trait object as they are only marker
55                // traits, so it's "fine" to break that
56                &[
57                    "trait SqlOrd",
58                    "trait Foldable",
59                    "trait SqlType",
60                    "trait SingleValue",
61                ] as &[_],
62            ),
63        ]);
64        self.run_semver_checks_for(
65            &metadata,
66            "diesel",
67            &["sqlite", "postgres", "mysql", "extras", "with-deprecated"],
68            false_positives_for_diesel,
69        );
70        self.run_semver_checks_for(&metadata, "diesel_migrations", &[], HashMap::new());
71        self.run_semver_checks_for(
72            &metadata,
73            "diesel-dynamic-schema",
74            &["postgres", "mysql", "sqlite"],
75            HashMap::new(),
76        );
77    }
78
79    fn run_semver_checks_for(
80        &self,
81        metadata: &cargo_metadata::Metadata,
82        crate_name: &str,
83        features: &[&str],
84        allow_list: HashMap<&str, &[&str]>,
85    ) {
86        let baseline_diesel_version = if let Some(ref baseline_version) = self.baseline_version {
87            baseline_version.clone()
88        } else {
89            let mut baseline_diesel_version = metadata
90                .packages
91                .iter()
92                .find_map(|c| (c.name == crate_name).then_some(&c.version))
93                .unwrap()
94                .clone();
95            if baseline_diesel_version.major != 0 {
96                baseline_diesel_version.patch = 0;
97            }
98            baseline_diesel_version.to_string()
99        };
100        let mut command = Command::new("cargo");
101        command
102            .args([
103                "semver-checks",
104                "-p",
105                crate_name,
106                "--only-explicit-features",
107                "--baseline-version",
108                &baseline_diesel_version,
109                "--release-type",
110                &self.tpe.to_string(),
111            ])
112            .current_dir(&metadata.workspace_root);
113        for f in features {
114            command.args(["--features", f]);
115        }
116        println!("Run cargo semver-checks via `{command:?}`");
117        let res = command
118            .stderr(Stdio::piped())
119            .stdout(Stdio::piped())
120            .output()
121            .unwrap();
122        let std_out = String::from_utf8(res.stdout).expect("Valid UTF-8");
123        let std_err = String::from_utf8(res.stderr).expect("Valid UTF-8");
124        let mut failed = false;
125        let mut std_out_out = String::new();
126        // That's all here as we want to "patch" the output of cargo-semver-checks
127        // to be able to ignore specific instances of lint violations because:
128        //
129        // * There are a lot of false positives
130        // * Diesel is complex
131        // * Sometimes we want to do things that are technically breaking changes
132        for lint in std_out.split("\n---") {
133            if lint.trim().is_empty() {
134                continue;
135            }
136            let (lint, content) = lint
137                .trim()
138                .strip_prefix("failure")
139                .unwrap_or(lint)
140                .trim()
141                .split_once(':')
142                .expect("Two parts exist");
143            let ignore_list = allow_list.get(lint).copied().unwrap_or_default();
144
145            let failures = content
146                .lines()
147                .skip_while(|l| !l.trim().starts_with("Failed in:"))
148                .skip(1)
149                .filter(|l| ignore_list.iter().all(|e| !l.trim().starts_with(e)))
150                .collect::<Vec<_>>();
151            let content = content
152                .lines()
153                .take_while(|l| !l.trim().starts_with("Failed in:"));
154            if !failures.is_empty() {
155                failed = true;
156                if !std_out_out.is_empty() {
157                    std_out_out += "\n";
158                }
159                std_out_out += "--- failure ";
160                std_out_out += lint;
161                std_out_out += ":";
162                for l in content {
163                    std_out_out += l;
164                    std_out_out += "\n";
165                }
166                std_out_out += "Failed in:";
167                for failure in failures {
168                    std_out_out += "\n";
169                    std_out_out += failure;
170                }
171            }
172        }
173        let (front, back) = std_err.split_once("\n\n").unwrap_or((&std_err, ""));
174        eprintln!("{front}\n");
175        if failed {
176            eprintln!("Cargo semver check failed");
177            println!("{std_out_out}");
178            println!();
179        }
180        eprintln!("{back}");
181        if failed {
182            std::process::exit(1);
183        }
184    }
185}