Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
577 changes: 577 additions & 0 deletions .github/workflows/release-crates.yml

Large diffs are not rendered by default.

6 changes: 3 additions & 3 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -111,9 +111,9 @@ jobs:
# --no-verify writes the packages without compiling them, in seconds, and
# runs no build script. devtools/check-crate-packages then checks the pins,
# the LICENSE and NOTICE files, the metadata and crates.io's size limits.
# Compiling each package from its tarball takes minutes. That build belongs
# to the crates.io release workflow (follow-up work), before the upload, and
# not to each PR.
# Compiling each package from its tarball takes minutes. That build runs in
# the verify job of release-crates.yml, before the upload, and not on each
# PR.
package:
runs-on: extenddb_ubuntu-2404_4-core
timeout-minutes: 10
Expand Down
30 changes: 27 additions & 3 deletions devtools/check-crate-packages
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,13 @@ changes, it also needs `--allow-dirty`. This script needs Python 3.11 or
later. It reads the members from `cargo metadata`, so cargo resolves globs,
`exclude`, inherited fields and renamed dependencies. It then fails, with
one line per problem, unless all of the following hold for each member that
crates.io receives (one without `publish = false`):
crates.io receives (one without a `publish` field, or one whose `publish`
list names crates-io):

- The member's version is the `version` of the root manifest's
`[workspace.package]` table. A release publishes every crate at the
workspace version, and `devtools/publish-crates --version` refuses a
crate of another version.
- Each dependency on another workspace crate, normal or build, requires
that crate's version exactly (`=X.Y.Z`). A caret requirement would let a
fresh install combine crates of two releases. A pin left at the previous
Expand Down Expand Up @@ -91,8 +96,19 @@ def cargo_metadata(root: Path) -> dict:


def is_published(package: dict) -> bool:
"""`publish = false` reaches cargo metadata as an empty registry list."""
return package.get("publish") != []
"""A member that crates.io receives. `publish = false` reaches cargo
metadata as an empty registry list, and a member without the field as
null. devtools/publish-crates uses the same rule."""
registries = package.get("publish")
return registries is None or "crates-io" in registries


def workspace_version(metadata: dict) -> str | None:
"""The `version` of the root manifest's [workspace.package] table, or
None when the root manifest sets none."""
manifest = tomllib.loads((Path(metadata["workspace_root"]) / "Cargo.toml").read_text())
version = manifest.get("workspace", {}).get("package", {}).get("version")
return version if isinstance(version, str) else None


def check(root: Path, packages: Path | None = None) -> list[str]:
Expand All @@ -103,11 +119,19 @@ def check(root: Path, packages: Path | None = None) -> list[str]:
if packages is None:
packages = Path(metadata["target_directory"]) / "package"
problems: list[str] = []
release = workspace_version(metadata)
if release is None:
problems.append("the root Cargo.toml sets no [workspace.package] version, so the release version is unknown")

for name, package in sorted(members.items()):
if not is_published(package):
continue
manifest = os.path.relpath(package["manifest_path"], root)
if release is not None and package["version"] != release:
problems.append(
f"{manifest}: {name} is version {package['version']}, and the workspace version is {release}. "
f"A release publishes every crate at the workspace version (`version.workspace = true`)."
)
for dependency in package["dependencies"]:
target = dependency["name"]
if target not in members:
Expand Down
24 changes: 23 additions & 1 deletion devtools/check_crate_packages_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -207,14 +207,36 @@ def test_an_excluded_directory_is_not_a_member(self) -> None:
self.write("crates/x/src/lib.rs", "")
self.assert_passes()

def test_a_member_with_its_own_version_is_packaged_and_pinned_at_it(self) -> None:
def test_a_member_with_its_own_version_fails_and_its_pins_are_checked_at_it(self) -> None:
self.write("Cargo.toml", ROOT_MANIFEST.replace(f'"crates/a", version = "={VERSION}"', '"crates/a", version = "=2.0.0"'))
self.member("a", MEMBER_A.replace("version.workspace = true", 'version = "2.0.0"'))
(self.packages / f"example-a-{VERSION}.crate").unlink()
self.package("example-a", {"Cargo.toml": packaged_manifest("example-a", version="2.0.0")}, version="2.0.0")
self.package("example-b", {"Cargo.toml": packaged_manifest("example-b", '\n[dependencies.example-a]\nversion = "=2.0.0"\n')})
code, output = self.run_check()
self.assertEqual(code, 1, output)
self.assertIn(f"error: crates/a/Cargo.toml: example-a is version 2.0.0, and the workspace version is {VERSION}.",
output)
self.assertIn("1 problem(s) found", output)

def test_a_root_manifest_without_a_workspace_version_fails(self) -> None:
self.write("Cargo.toml", ROOT_MANIFEST.replace(f'version = "{VERSION}"\nedition', "edition"))
for directory, manifest in (("a", MEMBER_A), ("b", MEMBER_B)):
self.member(directory, manifest.replace("version.workspace = true", f'version = "{VERSION}"'))
self.assert_fails_with("error: the root Cargo.toml sets no [workspace.package] version")

def test_a_member_published_only_to_another_registry_needs_no_package(self) -> None:
self.member("b", MEMBER_B.replace("edition.workspace = true", 'edition.workspace = true\npublish = ["my-registry"]')
.replace("{ workspace = true }", '{ path = "../a" }'))
(self.packages / f"example-b-{VERSION}.crate").unlink()
self.assert_passes()

def test_a_member_whose_publish_list_names_crates_io_is_checked(self) -> None:
self.member("b", MEMBER_B.replace("edition.workspace = true",
'edition.workspace = true\npublish = ["my-registry", "crates-io"]'))
(self.packages / f"example-b-{VERSION}.crate").unlink()
self.assert_fails_with(f"example-b-{VERSION}.crate: missing")

def test_cargo_target_dir_moves_the_packages(self) -> None:
moved = self.root / "elsewhere"
for crate in self.packages.iterdir():
Expand Down
Loading
Loading