Introduce check to re-exports CI for error types
What changed, and why it matters
This commit adds a new automated CI check (a shell script) that verifies every error type defined in a Rust submodule named `error` is also re-exported at the parent module level. It is a code-quality and API-consistency enforcement tool, not a security patch. It does not change any library code that handles Bitcoin data, cryptography, or network messages.
No security action required. Review as a normal CI/infrastructure change.
Security signals we found
No strong security signals were identified.
Evidence from the diff
The change introduces contrib/check-error-reexports.sh and wires it into .github/workflows/rust.yml. The script scans api/all-features.txt files for public structs/enums whose path contains ::error:: and asserts a matching pub struct/enum/type/use line exists at the parent path (with ::error:: removed). This is purely a lint/policy check; no runtime behavior of the crates is modified.
Changed components
.github/workflows/rust.ymlcontrib/check-error-reexports.shInspect captured patch +110 / −0
diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml
index 8bbffa1c..2337d2f0 100644
--- a/.github/workflows/rust.yml
+++ b/.github/workflows/rust.yml
@@ -236,6 +236,8 @@ jobs:
run: |
contrib/generate-primitives-re-export-test.sh && (cd ./primitives && cargo test --all-features)
contrib/generate-bitcoin-re-export-test.sh && (cd ./bitcoin && cargo test --all-features)
+ - name: "Check error type re-exports"
+ run: contrib/check-error-reexports.sh
DiffMutants:
name: Check cargo mutants in diff - stable toolchain
diff --git a/contrib/check-error-reexports.sh b/contrib/check-error-reexports.sh
new file mode 100755
index 00000000..9d0d21c1
--- /dev/null
+++ b/contrib/check-error-reexports.sh
@@ -0,0 +1,108 @@
+#!/usr/bin/env bash
+#
+# Check that every error type defined in an `error` submodule is re-exported
+# at the parent module level.
+#
+# The policy is: if a type is defined at `foo::error::BarError` then it must
+# also be accessible as `foo::BarError` (i.e. re-exported by the parent module).
+#
+# Uses the API text files to verify the policy. Types are identified by
+# `pub struct` or `pub enum` lines whose path contains `::error::` as a path
+# segment (not as part of a type name). A re-export is present when the same
+# type name appears at the parent path in any `pub struct`, `pub enum`, or
+# `pub type` line (without the `::error::` segment).
+#
+# Usage: ./contrib/check-error-reexports.sh
+
+set -euo pipefail
+
+main() {
+ check_required_commands
+
+ local has_violations=false
+
+ # Auto-discover all crates that have API files.
+ while IFS= read -r api_file; do
+ local crate
+ crate=$(basename "$(dirname "$(dirname "$api_file")")")
+
+ # Collect all types defined in ::error:: submodules.
+ # We match `pub struct` and `pub enum` lines where the path contains
+ # `::error::` as a segment, and the type name is the final segment
+ # (no further `::` after the type name).
+ local error_module_types
+ error_module_types=$(grep -oP '^(?:#\[non_exhaustive\] )?pub (?:struct|enum|use) \K[A-Za-z0-9_]+(?:::[A-Za-z0-9_]+)*::error::[A-Za-z0-9_]+Error(?=\(|;|\s|<|$)' "$api_file" \
+ | sort -u || true)
+
+ if [[ -z "$error_module_types" ]]; then
+ say "$crate: OK (no ::error:: submodule types found)"
+ continue
+ fi
+
+ local violations=""
+
+ while IFS= read -r type_path; do
+ # Derive the expected re-export path by removing the `::error` segment.
+ # e.g. foo::bar::error::BazError -> foo::bar::BazError
+ local parent_path
+ parent_path="${type_path/::error::/::}"
+
+ # Escape the path for use as a fixed string in grep.
+ local escaped
+ escaped=$(printf '%s' "$parent_path" | sed 's/[.[\*^$]/\\&/g')
+
+ # A re-export is present when pub struct, enum, or type appears at
+ # exactly this path (followed by end-of-input, whitespace, or `(`).
+ # The line may be prefixed with an attribute such as `#[non_exhaustive]`.
+ if ! grep -qP "^(?:#\[[^\]]+\] )?pub (?:struct|enum|type|use) ${escaped}(?:\(|;|\s|<|$)" "$api_file"; then
+ violations="${violations} - ${type_path} -> missing ${parent_path}"$'\n'
+ fi
+ done <<< "$error_module_types"
+
+ if [[ -n "$violations" ]]; then
+ has_violations=true
+ say_err "Crate '$crate': error types without a parent-level re-export:"
+ say_err "$violations"
+ else
+ say "$crate: OK"
+ fi
+ done < <(find . -path "*/api/all-features.txt" | sort)
+
+ if $has_violations; then
+ err "Some error types are missing a re-export in the parent module."
+ fi
+
+ say "All checks passed."
+}
+
+check_required_commands() {
+ need_cmd find
+ need_cmd grep
+ need_cmd sed
+ need_cmd sort
+}
+
+say() {
+ echo "$1"
+}
+
+say_err() {
+ echo "$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.