Skip to content

Commit eeae225

Browse files
author
Jose Storopoli
committed
ci: add cargo-semver-checks
1 parent d9567b0 commit eeae225

3 files changed

Lines changed: 243 additions & 0 deletions

File tree

.github/workflows/rust.yml

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -302,3 +302,36 @@ jobs:
302302
uses: model-checking/kani-github-action@v1.1
303303
with:
304304
args: "--only-codegen"
305+
306+
API:
307+
needs: Prepare
308+
name: API - nightly toolchain
309+
runs-on: ubuntu-latest
310+
strategy:
311+
fail-fast: false
312+
steps:
313+
- name: "Checkout repo"
314+
uses: actions/checkout@v4
315+
with:
316+
fetch-depth: 0 # we need full history for cargo semver-checks
317+
- name: "Select toolchain"
318+
uses: dtolnay/rust-toolchain@v1
319+
with:
320+
toolchain: ${{ needs.Prepare.outputs.nightly_version }}
321+
- name: "Install cargo-binstall"
322+
uses: cargo-bins/cargo-binstall@main
323+
- name: "Binstall cargo-semver-checks"
324+
run: cargo binstall cargo-semver-checks --no-confirm
325+
- name: "Run semver checker script"
326+
run: ./contrib/check-semver.sh
327+
- name: "Add PR label to breaking changes"
328+
uses: actions-ecosystem/action-add-labels@v1
329+
if: ${{ hashFiles('semver-break') != '' }}
330+
with:
331+
labels: "API break"
332+
- name: Comment PR
333+
uses: thollander/actions-comment-pull-request@v2
334+
if: ${{ hashFiles('semver-break') != '' }}
335+
with:
336+
message: |
337+
:rotating_light: API BREAKING CHANGE DETECTED

CONTRIBUTING.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,14 @@ Use of `unsafe` code is prohibited unless there is a unanimous decision among
194194
library maintainers on the exclusion from this rule. In such cases there is a
195195
requirement to test unsafe code with sanitizers including Miri.
196196

197+
### API changes
198+
199+
All PRs that change the public API of `rust-bitcoin` will be checked on CI for
200+
semversioning compliance. This means that if the PR changes the public API in a
201+
way that is not backwards compatible, the PR will be flagged as a breaking change.
202+
Please check the [Rust workflow](.github/workflows/rust.yml).
203+
Under the hood we use [`cargo-semver-checks`](https://github.com/obi1kenobi/cargo-semver-checks).
204+
197205

198206
### Policy
199207

contrib/check-semver.sh

Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
#!/usr/bin/env bash
2+
#
3+
# Checks semver compatibility between the current and target branches.
4+
# Under the hood uses cargo semver-checks to check for breaking changes.
5+
# We cannot use it directly since it only supports checking against published
6+
# crates.
7+
# That's the intended use case for cargo semver-checks:
8+
# you run before publishing a new version of a crate to check semver breaks.
9+
# Here we are hacking it by first generating JSON files from cargo doc
10+
# and then using those files to check for breaking changes with
11+
# cargo semver-checks.
12+
13+
set -euo pipefail
14+
15+
# Our nightly version.
16+
NIGHTLY=$(cat nightly-version)
17+
18+
# These are the hardcoded flags that cargo semver-checks uses
19+
# under the hood to invoke rustdoc.
20+
RUSTDOCFLAGS="-Z unstable-options --document-private-items --document-hidden-items --output-format=json --cap-lints=allow"
21+
22+
# These will be set to the commit SHA from the PR's target branch
23+
# GitHub Actions CI.
24+
# NOTE: if running locally this will be set to master.
25+
if [ -n "${GITHUB_BASE_REF+x}" ]; then
26+
TARGET_COMMIT=$GITHUB_BASE_REF # running on CI
27+
else
28+
TARGET_COMMIT=$(git rev-parse master) # running locally
29+
fi
30+
31+
main() {
32+
# we need cargo nightly to generate the JSON files from cargo doc.
33+
need_nightly
34+
35+
# On current commit:
36+
# 1. bitcoin: all-features and no-default-features.
37+
generate_json_files_all_features "bitcoin" "current"
38+
generate_json_files_no_default_features "bitcoin" "current"
39+
40+
# 2. base58ck: all-features and no-default-features.
41+
generate_json_files_all_features "base58ck" "current"
42+
generate_json_files_no_default_features "base58ck" "current"
43+
44+
# 3. bitcoin_hashes: no-default-features and alloc feature.
45+
generate_json_files_no_default_features "bitcoin_hashes" "current"
46+
generate_json_files_features_alloc "bitcoin_hashes" "current"
47+
48+
# 4. bitcoin-units: no-default-features and alloc feature.
49+
generate_json_files_no_default_features "bitcoin-units" "current"
50+
generate_json_files_features_alloc "bitcoin-units" "current"
51+
52+
# 5. bitcoin-io: no-default-features and alloc feature.
53+
generate_json_files_no_default_features "bitcoin-io" "current"
54+
generate_json_files_features_alloc "bitcoin-io" "current"
55+
56+
57+
# Switch to target commit.
58+
echo "Checking out target commit at $TARGET_COMMIT"
59+
git checkout "$TARGET_COMMIT"
60+
61+
# On target commit:
62+
# 1. bitcoin: all-features and no-default-features.
63+
generate_json_files_all_features "bitcoin" "master"
64+
generate_json_files_no_default_features "bitcoin" "master"
65+
66+
# 2. base58ck: all-features and no-default-features.
67+
generate_json_files_all_features "base58ck" "master"
68+
generate_json_files_no_default_features "base58ck" "master"
69+
70+
# 3. bitcoin_hashes: no-default-features and alloc feature.
71+
generate_json_files_no_default_features "bitcoin_hashes" "master"
72+
generate_json_files_features_alloc "bitcoin_hashes" "master"
73+
74+
# 4. bitcoin-units: no-default-features and alloc feature.
75+
generate_json_files_no_default_features "bitcoin-units" "master"
76+
generate_json_files_features_alloc "bitcoin-units" "master"
77+
78+
# 5. bitcoin-io: no-default-features and alloc feature.
79+
generate_json_files_no_default_features "bitcoin-io" "master"
80+
generate_json_files_features_alloc "bitcoin-io" "master"
81+
82+
# Check for API semver breaks on all the generated JSON files above.
83+
run_cargo_semver_check "bitcoin" "all-features"
84+
run_cargo_semver_check "bitcoin" "no-default-features"
85+
run_cargo_semver_check "base58ck" "all-features"
86+
run_cargo_semver_check "base58ck" "no-default-features"
87+
run_cargo_semver_check "bitcoin_hashes" "no-default-features"
88+
run_cargo_semver_check "bitcoin_hashes" "alloc"
89+
run_cargo_semver_check "bitcoin-units" "no-default-features"
90+
run_cargo_semver_check "bitcoin-units" "alloc"
91+
run_cargo_semver_check "bitcoin-io" "no-default-features"
92+
run_cargo_semver_check "bitcoin-io" "alloc"
93+
94+
# Invoke cargo semver-checks to check for breaking changes
95+
# in all generated files.
96+
check_for_breaking_changes
97+
}
98+
99+
# Run cargo doc with the cargo semver-checks rustdoc flags.
100+
# We don't care about dependencies.
101+
run_cargo_doc() {
102+
RUSTDOCFLAGS="$RUSTDOCFLAGS" cargo +"$NIGHTLY" doc --no-deps "$@"
103+
}
104+
105+
# Run cargo semver-check
106+
run_cargo_semver_check() {
107+
local crate="$1"
108+
local variant="$2"
109+
110+
echo "Running cargo semver-checks for $crate $variant"
111+
# Hack to not fail on errors.
112+
# This is necessary since cargo semver-checks will fail if the
113+
# semver check fails.
114+
# We check that manually later.
115+
set +e
116+
cargo +"$NIGHTLY" semver-checks -v --baseline-rustdoc "$crate-master-$variant.json" --current-rustdoc "$crate-current-$variant.json" > "$crate-$variant-semver.txt" 2>&1
117+
set -e
118+
}
119+
120+
# The following function uses cargo doc to generate JSON files that
121+
# cargo semver-checks can use.
122+
# - no-default-features: generate JSON doc files with no default features.
123+
generate_json_files_no_default_features() {
124+
local crate="$1"
125+
local version="$2"
126+
127+
echo "Running cargo doc no-default-features for $crate $version"
128+
run_cargo_doc --no-default-features -p "$crate"
129+
130+
# replace _ for - in crate name.
131+
# This is necessary since some crates have - in their name
132+
# which will be converted to _ in the output file by cargo doc.
133+
mv "target/doc/${crate//-/_}.json" "$crate-$version-no-default-features.json"
134+
}
135+
# - all-features: generate JSON doc files with all features.
136+
generate_json_files_all_features() {
137+
local crate="$1"
138+
local version="$2"
139+
140+
echo "Running cargo doc all-features for $crate $version"
141+
run_cargo_doc --all-features -p "$crate"
142+
143+
# replace _ for - in crate name.
144+
# This is necessary since some crates have - in their name
145+
# which will be converted to _ in the output file by cargo doc.
146+
mv -v "target/doc/${crate//-/_}.json" "$crate-$version-all-features.json"
147+
}
148+
# - alloc: generate JSON doc files with the alloc feature.
149+
generate_json_files_features_alloc() {
150+
local crate="$1"
151+
local version="$2"
152+
153+
echo "Running cargo doc --features alloc for $crate $version"
154+
run_cargo_doc --no-default-features --features alloc -p "$crate"
155+
156+
# replace _ for - in crate name.
157+
# This is necessary since some crates have - in their name
158+
# which will be converted to _ in the output file by cargo doc.
159+
mv -v "target/doc/${crate//-/_}.json" "$crate-$version-alloc.json"
160+
}
161+
162+
# Check if there are breaking changes.
163+
# We loop through all the generated files and check if there is a FAIL
164+
# in the cargo semver-checks output.
165+
# If we detect a fail, we create an empty file semver-break.
166+
# If the following CI step finds this file, it will add:
167+
# 1. a comment on the PR.
168+
# 2. a label to the PR.
169+
check_for_breaking_changes() {
170+
for file in *semver.txt; do
171+
echo "Checking $file"
172+
if grep -q "FAIL" "$file"; then
173+
echo "You have introduced changes to the public API"
174+
echo "FAIL found in $file"
175+
# flag it as a breaking change
176+
# Handle the case where FAIL is found
177+
touch semver-break
178+
fi
179+
done
180+
if ! [ -f semver-break ]; then
181+
echo "No breaking changes found"
182+
fi
183+
}
184+
185+
# Safekeeping: check if we have a nightly compiler.
186+
need_nightly() {
187+
cargo_ver=$(cargo +"$NIGHTLY" --version)
188+
if echo "$cargo_ver" | grep -q -v nightly; then
189+
err "Need a nightly compiler; have $cargo_ver"
190+
fi
191+
}
192+
193+
err() {
194+
echo "$1" >&2
195+
exit 1
196+
}
197+
198+
#
199+
# Main script
200+
#
201+
main "$@"
202+
exit 0

0 commit comments

Comments
 (0)