Skip to content

Commit daea481

Browse files
author
CodeWhale Bot
committed
fix(release): enforce supported Cargo and retain publication checks
Use one Cargo publication dry run for all 21 crates before uploading, with an explicit Cargo 1.90+ release-tool requirement and crates.io destination. Update the runbook and test resumability without weakening full artifact verification. Validation: 5/5 offline tarball fixtures passed; real 21-crate publication dry run passed with no upload; real two-crate unpublished dependency dry run passed; publication order, bash syntax, and git diff --check passed. Runtime root has no npm test/check:web scripts. Signed-off-by: CodeWhale Bot <bot@codewhale.net>
1 parent cf62b8e commit daea481

3 files changed

Lines changed: 58 additions & 13 deletions

File tree

docs/RELEASE_RUNBOOK.md

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -99,12 +99,21 @@ clippy/test/npm-smoke gates for `fix/*`, `rebrand/*`, `work/v*`, and `main`.
9999
GitHub Actions keeps the cheap drift/fmt statuses plus macOS and Windows
100100
coverage, while CNB carries the Linux work.
101101

102-
`publish-crates.sh dry-run` first validates the maintained publication order
103-
against the locked Cargo workspace graph. It then performs a full
104-
`cargo publish --dry-run` for crates without unpublished workspace dependencies
105-
and a packaging preflight for dependent workspace crates. That avoids false
106-
negatives from crates.io not yet containing the new workspace version while
107-
still validating package contents before publish.
102+
`publish-crates.sh` requires Cargo 1.90 or newer for multi-package verification;
103+
this release-tool requirement is separate from the runtime's Rust 1.88 MSRV.
104+
Use an up-to-date stable toolchain (`rustup update stable`) for release work.
105+
106+
Both modes validate publication order against the locked workspace graph, then
107+
run one `cargo publish --dry-run --locked --registry crates-io` covering all
108+
21 release crates. Cargo resolves unpublished workspace dependencies through a
109+
temporary local registry, builds every unpacked tarball, and checks publication
110+
metadata before any upload. Dry-run mode permits source edits and stops there.
111+
Publish mode requires the approved release checkout and assets, then skips
112+
versions already on crates.io and uploads the remaining crates in dependency
113+
order. Resuming still verifies the complete source release; it never weakens
114+
the artifact gate merely because an earlier crate was already uploaded.
115+
Registry-side acceptance and credentials are still checked during real upload;
116+
a successful preflight cannot guarantee that every later upload will succeed.
108117

109118
For npm wrapper verification, build the single runtime and run the
110119
cross-platform smoke harness. This packs the npm wrapper, installs it into a

scripts/release/publish-crates.sh

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,15 @@ case "${mode}" in
1414
;;
1515
esac
1616

17+
# Multi-package publication verification was stabilized in Cargo 1.90.
18+
# Release tooling can require a newer Cargo than the runtime's Rust MSRV.
19+
cargo_version="$(cargo --version)"
20+
if [[ ! "${cargo_version}" =~ ^cargo[[:space:]]+([0-9]+)\.([0-9]+)\. ]] ||
21+
(( BASH_REMATCH[1] < 1 || (BASH_REMATCH[1] == 1 && BASH_REMATCH[2] < 90) )); then
22+
echo "Release preflight requires Cargo 1.90 or newer; found ${cargo_version}. Run rustup update stable and use that toolchain." >&2
23+
exit 1
24+
fi
25+
1726
packages=("${release_crates[@]}")
1827
crates_user_agent="CodeWhale release publish check (https://github.com/Hmbown/CodeWhale)"
1928

@@ -42,10 +51,10 @@ if [[ "${mode}" == "publish" ]]; then
4251
"${script_dir}/verify-release-assets.sh"
4352
fi
4453

45-
# Package the complete release together. Cargo resolves unpublished workspace
46-
# dependencies through a temporary local registry, then builds each unpacked
47-
# tarball. A file inventory alone cannot detect missing embedded assets.
48-
package_args=(--locked)
54+
# Verify the complete release together, including Cargo's publication checks.
55+
# Unpublished dependencies resolve through a temporary local registry, and
56+
# each unpacked tarball builds before the first real upload is attempted.
57+
package_args=(--dry-run --locked --registry crates-io)
4958
if [[ "${mode}" == "dry-run" ]]; then
5059
package_args+=(--allow-dirty)
5160
fi
@@ -54,7 +63,7 @@ for package in "${packages[@]}"; do
5463
done
5564

5665
echo "Verifying all ${#packages[@]} release package tarballs before any upload..."
57-
cargo package "${package_args[@]}"
66+
cargo publish "${package_args[@]}"
5867
if [[ "${mode}" == "dry-run" ]]; then
5968
echo "Release package verification OK; no crates uploaded."
6069
exit 0
@@ -88,7 +97,7 @@ for package in "${packages[@]}"; do
8897
if crate_version_exists "${package}" "${workspace_version}"; then
8998
echo "Skipping ${package} ${workspace_version}; already published."
9099
else
91-
cargo publish --locked -p "${package}"
100+
cargo publish --locked --registry crates-io -p "${package}"
92101
wait_for_crate_version "${package}" "${workspace_version}"
93102
fi
94103
echo "::endgroup::"

scripts/release/publish-crates.test.py

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414

1515
class PublishPreflightTests(unittest.TestCase):
1616
def setUp(self):
17+
real_cargo = shutil.which("cargo")
18+
self.assertIsNotNone(real_cargo, "Cargo 1.90+ must be installed to run release preflight tests")
1719
self.temporary = tempfile.TemporaryDirectory()
1820
self.addCleanup(self.temporary.cleanup)
1921
self.root = Path(self.temporary.name)
@@ -58,6 +60,8 @@ def setUp(self):
5860
cargo = bin_dir / "cargo"
5961
cargo.write_text(
6062
'#!/usr/bin/env bash\nset -euo pipefail\n'
63+
'if [[ "${1:-}" == --version && -n "${TEST_CARGO_VERSION:-}" ]]; then\n'
64+
' echo "$TEST_CARGO_VERSION"; exit 0\nfi\n'
6165
# Cargo publish --dry-run still contacts the registry. Keep its real
6266
# tarball build for the old-script regression check, fully offline.
6367
'if [[ "${1:-}" == publish ]]; then\n'
@@ -74,7 +78,7 @@ def setUp(self):
7478
curl.chmod(0o755)
7579
self.env = {
7680
**os.environ,
77-
"TEST_REAL_CARGO": shutil.which("cargo"),
81+
"TEST_REAL_CARGO": real_cargo,
7882
"TEST_UPLOADS": str(self.uploads),
7983
"PATH": str(bin_dir) + os.pathsep + os.environ["PATH"],
8084
"CARGO_NET_OFFLINE": "true",
@@ -99,6 +103,29 @@ def assert_missing_asset_blocks(self, mode):
99103
self.assertFalse(self.uploads.exists(), "upload reached before all packages passed")
100104
self.assertIn("payload.txt", output)
101105

106+
def test_old_cargo_fails_before_packaging_or_upload(self):
107+
for version in ("cargo 1.88.0 (fixture)", "cargo 1.89.0 (fixture)", "unknown"):
108+
with self.subTest(version=version):
109+
self.env["TEST_CARGO_VERSION"] = version
110+
output = self.run_command(
111+
["bash", str(self.scripts / "publish-crates.sh"), "publish"], success=False
112+
)
113+
self.assertIn("requires Cargo 1.90 or newer", output)
114+
self.assertFalse(self.uploads.exists())
115+
self.assertFalse((self.root / "target/package").exists())
116+
117+
def test_resume_verifies_tarballs_and_skips_existing_versions(self):
118+
manifest = self.root / "app/Cargo.toml"
119+
manifest.write_text(manifest.read_text().replace('exclude = ["src/payload.txt"]\n', ""))
120+
(self.root / "bin/curl").write_text("#!/bin/sh\nexit 0\n")
121+
output = self.run_command(
122+
["bash", str(self.scripts / "publish-crates.sh"), "publish"], success=True
123+
)
124+
self.assertIn("Skipping codewhale-preflight-base", output)
125+
self.assertIn("Skipping codewhale-preflight-app", output)
126+
self.assertTrue((self.root / "target/package/codewhale-preflight-app-0.0.0.crate").exists())
127+
self.assertFalse(self.uploads.exists())
128+
102129
def test_dry_run_builds_dependent_tarball(self):
103130
self.assert_missing_asset_blocks("dry-run")
104131

0 commit comments

Comments
 (0)