chore: automate Rust crate versioning in publish-ci#7425
chore: automate Rust crate versioning in publish-ci#7425
Conversation
Add build/publish-rust.mjs to bump the microsoft-fast-build Rust crate version based on conventional commits since the last Beachball tag, then package the crate into publish_artifacts/cargo/. Address review comments from PR #7373: - Use publish_artifacts/cargo/ (covered by existing .gitignore entry) - Align tag format documentation to underscore convention - Run Rust script before beachball to capture Cargo.toml bump in commit - Use git tag --list for tag discovery instead of version construction Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Note
Copilot was unable to run its full agentic suite in this review.
Automates Rust crate version bumping + packaging during npm run publish-ci so the microsoft-fast-build crate is versioned and exported to publish_artifacts/cargo/ alongside existing Beachball npm artifacts.
Changes:
- Add
build/publish-rust.mjsto compute bump from conventional commits since last Beachball tag, updateCargo.toml, andcargo packagethe crate intopublish_artifacts/cargo/. - Update root
publish-cito run the Rust publishing step beforebeachball publish. - Document build tooling workspace behavior and design rationale in
build/README.mdandbuild/DESIGN.md.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 5 comments.
| File | Description |
|---|---|
| package.json | Chains Rust publishing automation before Beachball in publish-ci. |
| build/publish-rust.mjs | Implements tag lookup, conventional-commit bumping, Cargo.toml update, and artifact packaging. |
| build/README.md | Documents usage and behavior of build utility scripts, including publish-rust.mjs. |
| build/DESIGN.md | Captures design decisions and data flow for the Rust publishing automation. |
|
|
||
| if ( | ||
| /BREAKING CHANGE/m.test(commits) || | ||
| /^(feat|fix|refactor|chore)!(\([^)]*\))?:/m.test(commits) |
There was a problem hiding this comment.
The type! regex does not match the conventional-commits breaking syntax type(scope)!: (the ! comes after the optional scope). As written, it matches feat!(scope): but will miss common messages like feat(parser)!: ..., causing a major bump to be incorrectly downgraded. Update the pattern to match type(scope)!: (and optionally type!:) correctly.
| /^(feat|fix|refactor|chore)!(\([^)]*\))?:/m.test(commits) | |
| /^(feat|fix|refactor|chore)(\([^)]*\))?!:/m.test(commits) |
| function updateCargoToml(newVersion) { | ||
| let content = readFileSync(cargoTomlPath, "utf-8"); | ||
| content = content.replace(/^(version\s*=\s*)"[^"]+"/m, `$1"${newVersion}"`); | ||
| writeFileSync(cargoTomlPath, content); | ||
| } |
There was a problem hiding this comment.
This replacement updates the first line starting with version = ... anywhere in the file, which can be incorrect if a dependency table (or another section) appears before [package] and contains version = "...". Consider either parsing TOML and updating [package].version, or restricting the edit to the [package] section (e.g., find the [package] header and only replace version until the next header).
| function packageCrate(version) { | ||
| mkdirSync(outputDir, { recursive: true }); | ||
| execSync( | ||
| `cargo package --manifest-path "${cargoTomlPath}" --no-verify --allow-dirty`, | ||
| { stdio: "inherit" }, | ||
| ); | ||
| const targetPackageDir = join(crateDir, "target", "package"); |
There was a problem hiding this comment.
cargo package may place artifacts under the workspace/global target directory (commonly ${repoRoot}/target/package) rather than ${crateDir}/target/package when invoked within a Cargo workspace, which would make the subsequent glob fail even though packaging succeeded. A more robust approach is to resolve Cargo’s target_directory via cargo metadata (or pass an explicit --target-dir) and use that location for target/package.
| function packageCrate(version) { | |
| mkdirSync(outputDir, { recursive: true }); | |
| execSync( | |
| `cargo package --manifest-path "${cargoTomlPath}" --no-verify --allow-dirty`, | |
| { stdio: "inherit" }, | |
| ); | |
| const targetPackageDir = join(crateDir, "target", "package"); | |
| function getCargoTargetDirectory() { | |
| const metadataJson = execSync( | |
| `cargo metadata --manifest-path "${cargoTomlPath}" --format-version 1 --no-deps`, | |
| { encoding: "utf-8" }, | |
| ); | |
| const metadata = JSON.parse(metadataJson); | |
| if (!metadata.target_directory) { | |
| throw new Error("Could not determine Cargo target_directory from cargo metadata"); | |
| } | |
| return metadata.target_directory; | |
| } | |
| function packageCrate(version) { | |
| mkdirSync(outputDir, { recursive: true }); | |
| execSync( | |
| `cargo package --manifest-path "${cargoTomlPath}" --no-verify --allow-dirty`, | |
| { stdio: "inherit" }, | |
| ); | |
| const targetPackageDir = join(getCargoTargetDirectory(), "package"); |
|
|
||
| const range = latestTag ? `${latestTag}..HEAD` : "HEAD"; | ||
| const log = execSync( | ||
| `git -C "${repoRoot}" log ${range} --pretty=format:"%s%n%b" -- crates/microsoft-fast-build/`, |
There was a problem hiding this comment.
The log format concatenates commits without a guaranteed separator between entries (especially when %b is empty), which can make the bump-detection regexes brittle (anchors with ^/m depend on reliable newlines). Add an explicit delimiter/newline between commits in the pretty format (or use a NUL-delimited format) so each commit boundary is unambiguous during parsing.
| `git -C "${repoRoot}" log ${range} --pretty=format:"%s%n%b" -- crates/microsoft-fast-build/`, | |
| `git -C "${repoRoot}" log ${range} --pretty=format:"%s%n%b%n%n" -- crates/microsoft-fast-build/`, |
| execSync( | ||
| `cargo package --manifest-path "${cargoTomlPath}" --no-verify --allow-dirty`, | ||
| { stdio: "inherit" }, | ||
| ); |
There was a problem hiding this comment.
If cargo is unavailable/misconfigured in the environment, execSync will throw with a generic stack trace that’s hard to diagnose from CI logs. Catch the failure and rethrow a targeted error that mentions the exact command and common remediation (e.g., ensuring the Rust toolchain is installed and cargo is on PATH), so publish failures are actionable.
| execSync( | |
| `cargo package --manifest-path "${cargoTomlPath}" --no-verify --allow-dirty`, | |
| { stdio: "inherit" }, | |
| ); | |
| const cargoPackageCommand = `cargo package --manifest-path "${cargoTomlPath}" --no-verify --allow-dirty`; | |
| try { | |
| execSync(cargoPackageCommand, { stdio: "inherit" }); | |
| } catch (error) { | |
| throw new Error( | |
| `Failed to run '${cargoPackageCommand}'. Ensure the Rust toolchain is installed and 'cargo' is available on PATH before publishing.`, | |
| { cause: error }, | |
| ); | |
| } |
Pull Request
📖 Description
When
npm run publish-ciruns, it now also handles themicrosoft-fast-buildRust crate automatically via a newbuild/publish-rust.mjsscript.This is a revised version of #7373 that addresses all review comments.
Changes
build/publish-rust.mjs(new)A Node.js script that:
microsoft-fast-build_v*usinggit tag --list --sort=-version:refname, falling back to all history if none existscrates/microsoft-fast-build/since that tagBREAKING CHANGE/feat!:/fix!:/refactor!:/chore!:→ majorfeat:→ minorcrates/microsoft-fast-build/Cargo.tomlcargo packageand copies the.cratefile topublish_artifacts/cargo/package.jsonUpdated
publish-cito run the Rust script before beachball:build/README.mdandbuild/DESIGN.md(new)Added documentation for the build utilities workspace.
Behavior
crates/microsoft-fast-build/publish_artifacts/cargo/This mirrors the existing pattern: beachball →
publish_artifacts/(npm), new script →publish_artifacts/cargo/(Rust).Review comments addressed from #7373
refactor!:,chore!:)publish_artifacts_cargo/topublish_artifacts/cargo/(covered by existing.gitignoreentry forpublish_artifacts/){package-name}_v{version}(underscore) format throughoutCargo.tomlversion bump is captured in the commitgit tag --list --sort=-version:refnameinstead of constructing tag from Cargo.toml version🎫 Issues
Supersedes #7373
👩💻 Reviewer Notes
No
.gitignorechanges are needed becausepublish_artifacts/is already ignored andpublish_artifacts/cargo/falls under that pattern.The script runs before beachball in
publish-ciso the Cargo.toml version bump is included in the beachball commit.📑 Test Plan
npm run buildsucceeds✅ Checklist
General
$ npm run change⏭ Next Steps