Add a script that generates a Rust test to check re-exports
What changed, and why it matters
This commit adds a new CI test script that checks whether all public items from one Rust crate (bitcoin_units) are correctly re-exported in another crate (bitcoin_primitives). It does not change any library code, only adds a build/verification script and a GitHub Actions job. There is no security issue here.
No security action required. This is a testing/CI infrastructure change. Review the shell script for correctness and maintainability if desired.
Security signals we found
No strong security signals were identified.
Evidence from the diff
The commit introduces contrib/generate-re-export-test.sh, which parses api/units/all-features.txt and generates primitives/tests/check-re-exports.rs containing use statements for every pub enum, pub struct, and pub mod. The generated test will fail to compile if any re-export is missing. A new GitHub Actions job ‘Re-exports’ is added to rust.yml to run the script and then cargo test in the primitives crate. No source code behavior is changed.
Changed components
.github/workflows/rust.yml.github/workflows/README.mdcontrib/generate-re-export-test.shInspect captured patch +187 / −3
diff --git a/.github/workflows/README.md b/.github/workflows/README.md
index a1d7e431..a7b54d00 100644
--- a/.github/workflows/README.md
+++ b/.github/workflows/README.md
@@ -30,6 +30,7 @@ Run from rust.yml unless stated otherwise. Unfortunately we are now exceeding th
16. `Kani`
17. `API`
18. `Policy` - enforce repository coding policy.
-19. `release` - run by `release.yml`
-20. `labeler` - run by `manage-pr.yml`
-21. `Shellcheck` - run by `shellcheck.yml`
+19. `Re-exports`
+20. `release` - run by `release.yml`
+21. `labeler` - run by `manage-pr.yml`
+22. `Shellcheck` - run by `shellcheck.yml`
diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml
index 7323aed0..7727417b 100644
--- a/.github/workflows/rust.yml
+++ b/.github/workflows/rust.yml
@@ -345,3 +345,16 @@ jobs:
uses: dtolnay/rust-toolchain@stable
- name: "Run policy script"
run: ./contrib/check-for-policy-violations.sh
+
+ Re-exports:
+ name: Check re-exports - stable toolchain
+ runs-on: ubuntu-24.04
+ strategy:
+ fail-fast: false
+ steps:
+ - name: "Checkout repo"
+ uses: actions/checkout@v4
+ - name: "Select toolchain"
+ uses: dtolnay/rust-toolchain@stable
+ - name: "Run API checker script"
+ run: contrib/generate-re-export-test.sh && cd ./primitives && cargo test --all-features
diff --git a/contrib/generate-re-export-test.sh b/contrib/generate-re-export-test.sh
new file mode 100755
index 00000000..555a8afd
--- /dev/null
+++ b/contrib/generate-re-export-test.sh
@@ -0,0 +1,170 @@
+#!/usr/bin/env bash
+#
+# Script for generating a Rust test file that verifies all bitcoin_units items
+# are re-exported in bitcoin_primitives.
+#
+# The script parses api/units/all-features.txt and generates use statements
+# that will fail to compile if any re-exports are missing.
+
+set -euo pipefail
+
+api_file="./api/units/all-features.txt"
+output_file="./primitives/tests/check-re-exports.rs"
+
+usage() {
+ cat <<EOF
+Usage:
+
+ ./generate-re-export-test.sh
+
+DESCRIPTION
+ Generates a Rust test file that verifies all public types and modules from
+ bitcoin_units are re-exported in bitcoin_primitives.
+
+ The script parses api/units/all-features.txt and creates use statements for
+ every 'pub enum', 'pub struct', and 'pub mod' declaration.
+
+ Output file: primitives/tests/check-re-exports.rs
+EOF
+}
+
+main() {
+ while [[ $# -gt 0 ]]; do
+ case $1 in
+ -h|--help)
+ usage
+ exit 0
+ ;;
+ *)
+ say_err "unknown option: $1"
+ usage
+ exit 1
+ ;;
+ esac
+ done
+
+ check_required_commands
+ check_required_files
+
+ say "Parsing $api_file and generating Rust test..."
+
+ generate_test_file
+
+ say "Generated $output_file"
+ say "Run 'cd primitives && cargo test --all-features check_all_bitcoin_units_items_are_reexported' to test"
+}
+
+generate_test_file() {
+ local temp_file;
+ temp_file=$(mktemp)
+
+ # Generate the file header
+ cat > "$temp_file" <<'EOF'
+// SPDX-License-Identifier: CC0-1.0
+
+//! Test that all public types and modules from bitcoin_units are re-exported in bitcoin_primitives.
+//!
+//! This test is automatically generated by contrib/generate-re-export-test.sh
+//! Any compilation error indicates a missing re-export.
+
+#![allow(dead_code)]
+#![allow(unused_imports)]
+// No benefit in running this test without features enabled.
+#[cfg(not(feature = "alloc"))]
+compile_error!("alloc feature needs to be enabled");
+#[cfg(not(feature = "hex"))]
+compile_error!("hex feature needs to be enabled");
+#[cfg(not(feature = "arbitrary"))]
+compile_error!("arbitrary feature needs to be enabled");
+
+#[test]
+fn check_all_bitcoin_units_items_are_reexported() {
+ // This test will fail to compile if any bitcoin_units item is not re-exported in bitcoin_primitives
+
+EOF
+
+ # Extract and convert all pub items
+ local use_statements=()
+ local seen_items=()
+
+ while IFS= read -r line; do
+ local path=""
+
+ # Extract pub enum
+ if [[ "$line" =~ pub\ enum\ (bitcoin_units::[^[:space:]]+) ]]; then
+ path="${BASH_REMATCH[1]}"
+ # Extract pub struct
+ elif [[ "$line" =~ pub\ struct\ (bitcoin_units::[^[:space:]\(]+) ]]; then
+ path="${BASH_REMATCH[1]}"
+ # Extract pub mod
+ elif [[ "$line" =~ ^pub\ mod\ (bitcoin_units::[^[:space:]]+)$ ]]; then
+ path="${BASH_REMATCH[1]}"
+ fi
+
+ if [[ -n "$path" ]]; then
+ # Remove generic type parameters (e.g., <T>)
+ path="${path%%<*}"
+
+ # Convert bitcoin_units:: to bitcoin_primitives::
+ local primitives_path="${path//bitcoin_units::/bitcoin_primitives::}"
+
+ # Skip if we've already seen this item
+ if [[ " ${seen_items[*]} " != *" $primitives_path "* ]]; then
+ seen_items+=("$primitives_path")
+ use_statements+=(" use $primitives_path as _;")
+ fi
+ fi
+ done < "$api_file"
+
+ # Sort use statements and add to file
+ printf '%s\n' "${use_statements[@]}" | sort >> "$temp_file"
+
+ # Add closing brace
+ echo "}" >> "$temp_file"
+
+ # Move temp file to final location
+ mv "$temp_file" "$output_file"
+}
+
+check_required_files() {
+ if [[ ! -f "$api_file" ]]; then
+ err "Required file not found: $api_file"
+ fi
+
+ local output_dir
+ output_dir=$(dirname "$output_file")
+ if [[ ! -d "$output_dir" ]]; then
+ err "Output directory not found: $output_dir"
+ fi
+}
+
+check_required_commands() {
+ need_cmd grep
+ need_cmd sort
+ need_cmd mktemp
+}
+
+say() {
+ echo "generate-re-export-test: $1"
+}
+
+say_err() {
+ say "$1" >&2
+}
+
+err() {
+ echo "$1" >&2
+ exit 1
+}
+
+need_cmd() {
+ if ! command -v "$1" > /dev/null 2>&1
+ then err "need '$1' (command not found)"
+ fi
+}
+
+#
+# Main script
+#
+main "$@"
+exit 0
Why this scored 15/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.