Skip to content

Commit 4cff2cc

Browse files
committed
Auto merge of #12085 - weihanglo:ci-check-version-bump, r=<try>
ci: check if a crate needs a version bump when source files change
2 parents 2d693e2 + 01b3f1e commit 4cff2cc

File tree

7 files changed

+217
-36
lines changed

7 files changed

+217
-36
lines changed

.github/workflows/main.yml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,18 @@ jobs:
4343
- run: rustup update stable && rustup default stable
4444
- run: cargo stale-label
4545

46+
check-version-bump:
47+
runs-on: ubuntu-latest
48+
env:
49+
BASE_SHA : ${{ github.event.pull_request.base.sha }}
50+
COMMIT_SHA: ${{ github.sha }}
51+
steps:
52+
- uses: actions/checkout@v3
53+
with:
54+
fetch-depth: 0 # make `git diff` work
55+
- run: rustup update stable && rustup default stable
56+
- run: ci/validate-version-bump.sh
57+
4658
# Ensure Cargo.lock is up-to-date
4759
lockfile:
4860
runs-on: ubuntu-latest
@@ -213,6 +225,7 @@ jobs:
213225
name: bors build finished
214226
needs:
215227
- build_std
228+
- check-version-bump
216229
- docs
217230
- lockfile
218231
- resolver
@@ -229,6 +242,7 @@ jobs:
229242
name: bors build finished
230243
needs:
231244
- build_std
245+
- check-version-bump
232246
- docs
233247
- lockfile
234248
- resolver

benches/capture/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ version = "0.1.0"
44
edition = "2021"
55
license = "MIT OR Apache-2.0"
66
description = "Tool for capturing a real-world workspace for benchmarking."
7+
publish = false
78

89
[dependencies]
910
cargo_metadata.workspace = true

ci/validate-version-bump.sh

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
#!/bin/bash
2+
# This script checks if a crate needs a version bump.
3+
#
4+
# At the time of writing, it doesn't check what kind of bump is required.
5+
# In the future, we could take SemVer compatibliity into account, like
6+
# integrating `cargo-semver-checks` of else
7+
#
8+
# Inputs:
9+
# BASE_SHA The commit SHA of the branch where the PR wants to merge into.
10+
# COMMIT_SHA The commit SHA that triggered the workflow.
11+
12+
set -euo pipefail
13+
14+
# When `BASE_SHA` is missing, we assume it is from bors merge commit,
15+
# so `HEAD~` should find the previous commit on master branch.
16+
base_sha=$(git rev-parse "${BASE_SHA:-HEAD~}")
17+
commit_sha=$(git rev-parse "${COMMIT_SHA:-HEAD}")
18+
19+
echo "Base branch is $base_sha"
20+
echo "The current is $commit_sha"
21+
22+
changed_crates=$(
23+
git diff --name-only "$base_sha" "$commit_sha" -- crates/ credential/ benches/ \
24+
| cut -d'/' -f2 \
25+
| sort -u
26+
)
27+
28+
if [ -z "$changed_crates" ]
29+
then
30+
echo "No file changed in sub crates."
31+
exit 0
32+
fi
33+
34+
output=$(
35+
echo "$changed_crates" \
36+
| xargs printf -- '--package %s\n' \
37+
| xargs cargo unpublished --check-version-bump
38+
)
39+
40+
if [ -z "$output" ]
41+
then
42+
echo "No version bump needed for sub crates."
43+
exit 0
44+
fi
45+
46+
echo "$output"
47+
exit 1

crates/cargo-test-macro/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ homepage = "https://github.com/rust-lang/cargo"
77
repository = "https://github.com/rust-lang/cargo"
88
documentation = "https://github.com/rust-lang/cargo"
99
description = "Helper proc-macro for Cargo's testsuite."
10+
publish = false
1011

1112
[lib]
1213
proc-macro = true

crates/cargo-test-support/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ name = "cargo-test-support"
33
version = "0.1.0"
44
license = "MIT OR Apache-2.0"
55
edition = "2021"
6+
publish = false
67

78
[lib]
89
doctest = false

crates/xtask-unpublished/src/xtask.rs

Lines changed: 147 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,23 @@
1+
use std::collections::HashSet;
2+
use std::fmt;
3+
use std::fmt::Write;
4+
15
use cargo::core::registry::PackageRegistry;
26
use cargo::core::QueryKind;
37
use cargo::core::Registry;
48
use cargo::core::SourceId;
9+
use cargo::ops::Packages;
510
use cargo::util::command_prelude::*;
611

12+
type Record = (String, Option<String>, String, bool);
13+
714
pub fn cli() -> clap::Command {
815
clap::Command::new("xtask-unpublished")
16+
.arg(flag(
17+
"check-version-bump",
18+
"check if any version bump is needed",
19+
))
20+
.arg_package_spec_simple("Package to inspect the published status")
921
.arg(
1022
opt(
1123
"verbose",
@@ -76,14 +88,24 @@ fn config_configure(config: &mut Config, args: &ArgMatches) -> CliResult {
7688

7789
fn unpublished(args: &clap::ArgMatches, config: &mut cargo::util::Config) -> cargo::CliResult {
7890
let ws = args.workspace(config)?;
91+
92+
let members_to_inspect: HashSet<_> = {
93+
let pkgs = args.packages_from_flags()?;
94+
if let Packages::Packages(_) = pkgs {
95+
HashSet::from_iter(pkgs.get_packages(&ws)?)
96+
} else {
97+
HashSet::from_iter(ws.members())
98+
}
99+
};
100+
79101
let mut results = Vec::new();
80102
{
81103
let mut registry = PackageRegistry::new(config)?;
82104
let _lock = config.acquire_package_cache_lock()?;
83105
registry.lock_patches();
84106
let source_id = SourceId::crates_io(config)?;
85107

86-
for member in ws.members() {
108+
for member in members_to_inspect {
87109
let name = member.name();
88110
let current = member.version();
89111
if member.publish() == &Some(vec![]) {
@@ -92,11 +114,8 @@ fn unpublished(args: &clap::ArgMatches, config: &mut cargo::util::Config) -> car
92114
}
93115

94116
let version_req = format!("<={current}");
95-
let query = cargo::core::dependency::Dependency::parse(
96-
name,
97-
Some(&version_req),
98-
source_id.clone(),
99-
)?;
117+
let query =
118+
cargo::core::dependency::Dependency::parse(name, Some(&version_req), source_id)?;
100119
let possibilities = loop {
101120
// Exact to avoid returning all for path/git
102121
match registry.query_vec(&query, QueryKind::Exact) {
@@ -107,50 +126,142 @@ fn unpublished(args: &clap::ArgMatches, config: &mut cargo::util::Config) -> car
107126
}
108127
};
109128
if let Some(last) = possibilities.iter().map(|s| s.version()).max() {
110-
if last != current {
111-
results.push((
112-
name.to_string(),
113-
Some(last.to_string()),
114-
current.to_string(),
115-
));
116-
} else {
117-
log::trace!("{name} {current} is published");
118-
}
129+
let published = last == current;
130+
results.push((
131+
name.to_string(),
132+
Some(last.to_string()),
133+
current.to_string(),
134+
published,
135+
));
119136
} else {
120-
results.push((name.to_string(), None, current.to_string()));
137+
results.push((name.to_string(), None, current.to_string(), false));
121138
}
122139
}
123140
}
141+
results.sort();
124142

125-
if !results.is_empty() {
126-
results.insert(
127-
0,
128-
(
129-
"name".to_owned(),
130-
Some("published".to_owned()),
131-
"current".to_owned(),
132-
),
133-
);
134-
results.insert(
135-
1,
143+
if results.is_empty() {
144+
return Ok(());
145+
}
146+
147+
let check_version_bump = args.flag("check-version-bump");
148+
149+
if check_version_bump {
150+
output_version_bump_notice(&results);
151+
}
152+
153+
output_table(results, check_version_bump)?;
154+
155+
Ok(())
156+
}
157+
158+
/// Outputs a markdown table of publish status for each members.
159+
///
160+
/// ```text
161+
/// | name | crates.io | local | published? |
162+
/// | ---- | --------- | ----- | ---------- |
163+
/// | cargo | 0.70.1 | 0.72.0 | no |
164+
/// | cargo-credential | 0.1.0 | 0.2.0 | no |
165+
/// | cargo-credential-1password | 0.1.0 | 0.2.0 | no |
166+
/// | cargo-credential-gnome-secret | 0.1.0 | 0.2.0 | no |
167+
/// | cargo-credential-macos-keychain | 0.1.0 | 0.2.0 | no |
168+
/// | cargo-credential-wincred | 0.1.0 | 0.2.0 | no |
169+
/// | cargo-platform | 0.1.2 | 0.1.3 | no |
170+
/// | cargo-util | 0.2.3 | 0.2.4 | no |
171+
/// | crates-io | 0.36.0 | 0.36.1 | no |
172+
/// | home | 0.5.5 | 0.5.6 | no |
173+
/// ```
174+
fn output_table(results: Vec<Record>, check_version_bump: bool) -> fmt::Result {
175+
let mut results: Vec<_> = results
176+
.into_iter()
177+
.filter(|(.., published)| !check_version_bump || *published)
178+
.map(|e| {
136179
(
137-
"====".to_owned(),
138-
Some("=========".to_owned()),
139-
"=======".to_owned(),
140-
),
141-
);
180+
e.0,
181+
e.1.unwrap_or("-".to_owned()),
182+
e.2,
183+
if e.3 { "yes" } else { "no" }.to_owned(),
184+
)
185+
})
186+
.collect();
187+
188+
if results.is_empty() {
189+
return Ok(());
142190
}
143-
for (name, last, current) in results {
144-
if let Some(last) = last {
145-
println!("{name} {last} {current}");
191+
192+
let header = (
193+
"name".to_owned(),
194+
"crates.io".to_owned(),
195+
"local".to_owned(),
196+
if check_version_bump {
197+
"need version bump?"
146198
} else {
147-
println!("{name} - {current}");
199+
"published?"
148200
}
201+
.to_owned(),
202+
);
203+
let separators = (
204+
"-".repeat(header.0.len()),
205+
"-".repeat(header.1.len()),
206+
"-".repeat(header.2.len()),
207+
"-".repeat(header.3.len()),
208+
);
209+
results.insert(0, header);
210+
results.insert(1, separators);
211+
212+
let max_col_widths = results
213+
.iter()
214+
.map(|(name, last, local, bump)| (name.len(), last.len(), local.len(), bump.len()))
215+
.reduce(|(c0, c1, c2, c3), (f0, f1, f2, f3)| {
216+
(c0.max(f0), c1.max(f1), c2.max(f2), c3.max(f3))
217+
})
218+
.unwrap();
219+
220+
let print_space = |out: &mut dyn Write, n| {
221+
for _ in 0..(n + 1) {
222+
write!(out, " ")?;
223+
}
224+
fmt::Result::Ok(())
225+
};
226+
227+
let out = &mut String::new();
228+
for (name, last, local, bump) in results {
229+
write!(out, "| {name}")?;
230+
print_space(out, max_col_widths.0 - name.len())?;
231+
232+
write!(out, "| {last}")?;
233+
print_space(out, max_col_widths.1 - last.len())?;
234+
235+
write!(out, "| {local}")?;
236+
print_space(out, max_col_widths.2 - local.len())?;
237+
238+
write!(out, "| {bump}")?;
239+
print_space(out, max_col_widths.3 - bump.len())?;
240+
241+
writeln!(out, "|")?;
149242
}
150243

244+
println!("{out}");
245+
151246
Ok(())
152247
}
153248

249+
fn output_version_bump_notice(results: &[Record]) {
250+
let pkgs_need_bump = results
251+
.iter()
252+
.filter_map(|(name, .., published)| published.then_some(name.clone()))
253+
.collect::<Vec<_>>();
254+
255+
if !pkgs_need_bump.is_empty() {
256+
print!("### :warning: ");
257+
println!("Require at least a patch version bump for each of the following packages:\n");
258+
for pkg in pkgs_need_bump {
259+
println!("* {pkg}");
260+
}
261+
println!()
262+
}
263+
}
264+
154265
#[test]
155266
fn verify_cli() {
156267
cli().debug_assert();

src/cargo/util/errors.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -327,6 +327,12 @@ impl From<std::io::Error> for CliError {
327327
}
328328
}
329329

330+
impl From<std::fmt::Error> for CliError {
331+
fn from(err: std::fmt::Error) -> CliError {
332+
CliError::new(err.into(), 1)
333+
}
334+
}
335+
330336
// =============================================================================
331337
// Construction helpers
332338

0 commit comments

Comments
 (0)