From 118e59de140bc1daef0db2cedfab8dbde7dec5b0 Mon Sep 17 00:00:00 2001 From: Zeke Foppa <196249+bfops@users.noreply.github.com> Date: Wed, 23 Apr 2025 10:08:43 -0700 Subject: [PATCH] CI - Do some basic checks that crates are publishable (#2660) Co-authored-by: Zeke Foppa --- .github/workflows/ci.yml | 24 ++++++++++++ crates/core/Cargo.toml | 2 +- crates/expr/Cargo.toml | 4 +- crates/lib/Cargo.toml | 2 +- crates/schema/Cargo.toml | 4 +- crates/snapshot/Cargo.toml | 4 +- crates/table/Cargo.toml | 4 +- tools/crate-publish-checks.py | 72 +++++++++++++++++++++++++++++++++++ 8 files changed, 106 insertions(+), 10 deletions(-) create mode 100644 tools/crate-publish-checks.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2d2000718..d4f746f84 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -219,6 +219,30 @@ jobs: - name: Run bindgen tests run: cargo test -p spacetimedb-cli + publish_checks: + name: Check that packages are publishable + runs-on: ubuntu-latest + permissions: read-all + steps: + - uses: actions/checkout@v3 + - name: Set up Python env + run: | + test -d venv || python3 -m venv venv + venv/bin/pip3 install argparse toml + - name: Run checks + run: | + FAILED=0 + # This definition of ROOTS and invocation of find-publish-list.py is copied from publish-crates.sh + ROOTS=(bindings sdk cli standalone) + for crate in $(venv/bin/python3 tools/find-publish-list.py --recursive --quiet "${ROOTS[@]}"); do + if ! venv/bin/python3 tools/crate-publish-checks.py "crates/$crate"; then + FAILED=$(( $FAILED + 1 )) + fi + done + if [ $FAILED -gt 0 ]; then + exit 1 + fi + update: name: Test spacetimedb-update flow permissions: read-all diff --git a/crates/core/Cargo.toml b/crates/core/Cargo.toml index c589aa874..0c473b72f 100644 --- a/crates/core/Cargo.toml +++ b/crates/core/Cargo.toml @@ -132,7 +132,7 @@ perfmap = [] [dev-dependencies] spacetimedb-lib = { path = "../lib", features = ["proptest"] } spacetimedb-sats = { path = "../sats", features = ["proptest"] } -spacetimedb-commitlog = { workspace = true, features = ["test"] } +spacetimedb-commitlog = { path = "../commitlog", features = ["test"] } criterion.workspace = true # Also as dev-dependencies for use in _this_ crate's tests. diff --git a/crates/expr/Cargo.toml b/crates/expr/Cargo.toml index ad7aa058f..489beb3b0 100644 --- a/crates/expr/Cargo.toml +++ b/crates/expr/Cargo.toml @@ -20,5 +20,5 @@ spacetimedb-sql-parser.workspace = true [dev-dependencies] pretty_assertions.workspace = true -spacetimedb = { workspace = true, features = ["unstable"] } -spacetimedb-lib.workspace = true +spacetimedb = { path = "../bindings", features = ["unstable"] } +spacetimedb-lib = { path = "../lib" } diff --git a/crates/lib/Cargo.toml b/crates/lib/Cargo.toml index 417af7995..1057d8fb8 100644 --- a/crates/lib/Cargo.toml +++ b/crates/lib/Cargo.toml @@ -48,7 +48,7 @@ proptest = { workspace = true, optional = true } proptest-derive = { workspace = true, optional = true } [dev-dependencies] -spacetimedb-sats = { workspace = true, features = ["test"] } +spacetimedb-sats = { path = "../sats", features = ["test"] } bytes.workspace = true serde_json.workspace = true insta.workspace = true diff --git a/crates/schema/Cargo.toml b/crates/schema/Cargo.toml index 75758c15d..3313b72c9 100644 --- a/crates/schema/Cargo.toml +++ b/crates/schema/Cargo.toml @@ -31,10 +31,10 @@ enum-as-inner.workspace = true enum-map.workspace = true [dev-dependencies] -spacetimedb-lib = { workspace = true, features = ["test"] } +spacetimedb-lib = { path = "../lib", features = ["test"] } # these are circular dependencies, but only in tests, so it's fine spacetimedb-testing = { path = "../testing" } -spacetimedb-cli.workspace = true +spacetimedb-cli = { path = "../cli" } proptest.workspace = true serial_test.workspace = true diff --git a/crates/snapshot/Cargo.toml b/crates/snapshot/Cargo.toml index 6e2f2232e..4d3cccff2 100644 --- a/crates/snapshot/Cargo.toml +++ b/crates/snapshot/Cargo.toml @@ -28,8 +28,8 @@ tokio-util = { workspace = true, features = ["io"] } zstd-framed.workspace = true [dev-dependencies] -spacetimedb-core = { workspace = true, features = ["test"] } -spacetimedb-schema.workspace = true +spacetimedb-core = { path = "../core", features = ["test"] } +spacetimedb-schema = { path = "../schema" } anyhow.workspace = true env_logger.workspace = true diff --git a/crates/table/Cargo.toml b/crates/table/Cargo.toml index b5dc71222..0e0b4ed66 100644 --- a/crates/table/Cargo.toml +++ b/crates/table/Cargo.toml @@ -50,8 +50,8 @@ proptest = { workspace = true, optional = true } proptest-derive = { workspace = true, optional = true } [dev-dependencies] -spacetimedb-schema = { workspace = true, features = ["test"] } -spacetimedb-sats = { workspace = true, features = ["proptest"] } +spacetimedb-schema = { path = "../schema", features = ["test"] } +spacetimedb-sats = { path = "../sats", features = ["proptest"] } criterion.workspace = true proptest.workspace = true proptest-derive.workspace = true diff --git a/tools/crate-publish-checks.py b/tools/crate-publish-checks.py new file mode 100644 index 000000000..9754c46a2 --- /dev/null +++ b/tools/crate-publish-checks.py @@ -0,0 +1,72 @@ +import toml +import argparse +import sys +from pathlib import Path + +def find_non_path_spacetimedb_deps(dev_deps): + non_path_spacetimedb = [] + for name, details in dev_deps.items(): + if not name.startswith("spacetimedb"): + continue + + if isinstance(details, dict): + if "path" not in details: + non_path_spacetimedb.append(name) + else: + # String dependency = version from crates.io + non_path_spacetimedb.append(name) + return non_path_spacetimedb + +def check_cargo_metadata(data): + package = data.get("package", {}) + missing_fields = [] + + # Accept either license OR license-file + if "license" not in package and "license-file" not in package: + missing_fields.append("license/license-file") + + if "description" not in package: + missing_fields.append("description") + + return missing_fields + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description="Check Cargo.toml for metadata and dev-dependencies.") + parser.add_argument("directory", help="Directory to search for Cargo.toml") + + args = parser.parse_args() + cargo_toml_path = Path(args.directory) / "Cargo.toml" + + try: + if not cargo_toml_path.exists(): + raise FileNotFoundError(f"{cargo_toml_path} not found.") + + data = toml.load(cargo_toml_path) + + # Check dev-dependencies + dev_deps = data.get("dev-dependencies", {}) + bad_deps = find_non_path_spacetimedb_deps(dev_deps) + + # Check license/license-file and description + missing_fields = check_cargo_metadata(data) + + exit_code = 0 + + if bad_deps: + print(f"❌ These dev-dependencies in {cargo_toml_path} must be converted to use `path` in order to not impede crate publishing:") + for dep in bad_deps: + print(f" - {dep}") + exit_code = 1 + + if missing_fields: + print(f"❌ Missing required fields in [package] of {cargo_toml_path}: {', '.join(missing_fields)}") + exit_code = 1 + + if exit_code == 0: + print(f"✅ {cargo_toml_path} passed all checks.") + + sys.exit(exit_code) + + except Exception as e: + print(f"⚠️ Error: {e}") + sys.exit(2)