diff --git a/scripts/publish_release.py b/scripts/publish_release.py index 1ef1829d..bd15d939 100644 --- a/scripts/publish_release.py +++ b/scripts/publish_release.py @@ -234,6 +234,44 @@ def version_record(self, crate: str, version: str) -> RegistryRecord: return RegistryRecord(True, checksum) +def package_archive_checksum( + crate: str, version: str, extra_arguments: Sequence[str] = () +) -> str: + command = [ + "cargo", + "package", + "--locked", + "--no-verify", + "-p", + crate, + *extra_arguments, + ] + result = subprocess.run( + command, cwd=ROOT, check=False, capture_output=True, text=True + ) + if result.returncode != 0: + raise PublishError( + f"cargo package failed for {crate}:\n{result.stderr.strip()}" + ) + archive = ROOT / "target" / "package" / f"{crate}-{version}.crate" + try: + return hashlib.sha256(archive.read_bytes()).hexdigest() + except OSError as error: + raise PublishError(f"could not hash packaged archive {archive}: {error}") from None + + +def is_unpublished_dependency_failure(output: str) -> bool: + message = output.lower() + return all( + marker in message + for marker in ( + "failed to select a version for the requirement", + "candidate versions found which didn't match", + "location searched: crates.io index", + ) + ) + + def package_checksums( manifest: ReleaseManifest, version: str, metadata: Mapping[str, Any] ) -> dict[str, str]: @@ -270,27 +308,14 @@ def package_checksums( checksums: dict[str, str] = {} for crate in manifest.ordered_crates: - command = [ - "cargo", - "package", - "--locked", - "--no-verify", - "-p", - crate, - *patch_arguments, - ] - result = subprocess.run( - command, cwd=ROOT, check=False, capture_output=True, text=True - ) - if result.returncode != 0: - raise PublishError( - f"cargo package failed for {crate}:\n{result.stderr.strip()}" - ) - archive = ROOT / "target" / "package" / f"{crate}-{version}.crate" try: - checksums[crate] = hashlib.sha256(archive.read_bytes()).hexdigest() - except OSError as error: - raise PublishError(f"could not hash packaged archive {archive}: {error}") from None + checksums[crate] = package_archive_checksum(crate, version) + except PublishError as error: + if not is_unpublished_dependency_failure(str(error)): + raise + checksums[crate] = package_archive_checksum( + crate, version, patch_arguments + ) return checksums @@ -435,7 +460,7 @@ def publish_remaining( api: Any, manifest: ReleaseManifest, version: str, - checksums: Mapping[str, str], + checksums: dict[str, str], start_index: int, *, run: Callable[[list[str]], subprocess.CompletedProcess[str]] = _run_publish, @@ -453,6 +478,7 @@ def publish_remaining( print(result.stdout.rstrip()) if result.stderr: print(result.stderr.rstrip(), file=sys.stderr) + checksums[crate] = package_archive_checksum(crate, version) if result.returncode == 0: break diff --git a/scripts/tests/test_publish_release.py b/scripts/tests/test_publish_release.py index d1c8b840..b7201f1f 100644 --- a/scripts/tests/test_publish_release.py +++ b/scripts/tests/test_publish_release.py @@ -93,6 +93,16 @@ def test_workspace_packages_resolve_unpublished_exact_dependencies_locally(self) def run(command: list[str], **kwargs: object) -> subprocess.CompletedProcess[str]: commands.append(command) self.assertEqual(kwargs["cwd"], root) + crate = command[command.index("-p") + 1] + if crate == "consumer" and "--config" not in command: + return subprocess.CompletedProcess( + command, + 1, + "", + "failed to select a version for the requirement " + "`base = \"=0.7.3\"`; candidate versions found which " + "didn't match; location searched: crates.io index", + ) return subprocess.CompletedProcess(command, 0, "", "") with ( @@ -105,14 +115,15 @@ def run(command: list[str], **kwargs: object) -> subprocess.CompletedProcess[str f'patch.crates-io.{crate}.path="{root.resolve() / "crates" / crate}"' for crate in manifest.ordered_crates } - self.assertEqual(len(commands), 2) - for command in commands: - configs = { - command[index + 1] - for index, argument in enumerate(command) - if argument == "--config" - } - self.assertEqual(configs, expected_patches) + self.assertEqual(len(commands), 3) + self.assertNotIn("--config", commands[0]) + self.assertNotIn("--config", commands[1]) + configs = { + commands[2][index + 1] + for index, argument in enumerate(commands[2]) + if argument == "--config" + } + self.assertEqual(configs, expected_patches) class RegistryTests(unittest.TestCase): @@ -200,19 +211,33 @@ def run(command: list[str]) -> subprocess.CompletedProcess[str]: api.records["a"] = publish_release.RegistryRecord(True, "aaa") return subprocess.CompletedProcess(command, 0, "published", "") - publish_release.publish_remaining( - api, - manifest, - "0.7.3", - {"a": "aaa"}, - 0, - run=run, - sleep=delays.append, - ) + refreshed: list[tuple[str, str]] = [] + + def package_archive_checksum(crate: str, version: str) -> str: + refreshed.append((crate, version)) + return "aaa" + + checksums = {"a": "staged-checksum"} + with mock.patch.object( + publish_release, + "package_archive_checksum", + side_effect=package_archive_checksum, + ): + publish_release.publish_remaining( + api, + manifest, + "0.7.3", + checksums, + 0, + run=run, + sleep=delays.append, + ) self.assertEqual(attempts, 2) self.assertEqual(delays, [5]) self.assertGreaterEqual(len(api.calls), 1) + self.assertEqual(refreshed, [("a", "0.7.3"), ("a", "0.7.3")]) + self.assertEqual(checksums, {"a": "aaa"}) def test_authentication_and_validation_failures_are_never_retried(self) -> None: for output in (