chore: automate Rust crate versioning in publish-ci#7373
chore: automate Rust crate versioning in publish-ci#7373
Conversation
When 'npm run publish-ci' runs, it now also: - Inspects git commits touching crates/microsoft-fast-build/ since the last 'microsoft-fast-build-vX.Y.Z' tag using conventional commit rules - Bumps the version in crates/microsoft-fast-build/Cargo.toml - Packages the crate via 'cargo package' and copies the .crate file to publish_artifacts_cargo/ If no relevant commits are found the script exits cleanly with no changes. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…e detection Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…Beachball Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
This PR extends the existing publish-ci workflow so that, in addition to Beachball’s npm packaging, the microsoft-fast-build Rust crate is version-bumped and packaged automatically during CI publishing.
Changes:
- Chain a new Rust publish step into
npm run publish-ci. - Add
build/publish-rust.mjsto compute a semver bump from conventional commits affectingcrates/microsoft-fast-build/, updateCargo.toml, and runcargo package. - Copy the resulting
.crateartifact intopublish_artifacts_cargo/.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 6 comments.
| File | Description |
|---|---|
| package.json | Updates publish-ci to run the Rust publish script after Beachball. |
| build/publish-rust.mjs | New script to detect crate changes since the last release tag, bump Cargo.toml, and package the Rust crate artifact. |
build/publish-rust.mjs
Outdated
| function getCommitsSinceCrateLastTag(crateName, currentVersion) { | ||
| // Beachball generates tags in the format {package-name}_v{version} | ||
| const tagName = `${crateName}_v${currentVersion}`; | ||
| let fromRef; | ||
| try { | ||
| execSync(`git -C "${repoRoot}" rev-parse "${tagName}"`, { stdio: "pipe" }); | ||
| fromRef = tagName; | ||
| } catch { | ||
| fromRef = null; | ||
| } | ||
|
|
||
| const range = fromRef ? `${fromRef}..HEAD` : "HEAD"; | ||
| const log = execSync( | ||
| `git -C "${repoRoot}" log ${range} --pretty=format:"%s%n%b" -- crates/microsoft-fast-build/`, | ||
| { encoding: "utf-8" } | ||
| ); | ||
| return log.trim(); |
There was a problem hiding this comment.
The script tries to find the last release tag by constructing ${crateName}_v${currentVersion} from Cargo.toml. If Cargo.toml’s version ever diverges from the most recent tag (e.g., the npm package tag is newer), rev-parse will fail and the script will fall back to scanning all history, causing incorrect bumps. Instead, resolve the latest existing tag matching the crate (e.g., via git describe --tags --match "${crateName}_v*" --abbrev=0 or git tag --list ... --sort=-version:refname | head -n1).
| * Bumps the Rust crate version based on conventional commits since the last | ||
| * Beachball-generated release tag, then packages the crate into publish_artifacts_cargo/. | ||
| * | ||
| * Beachball tags use the format: {package-name}_v{version} | ||
| * e.g. microsoft-fast-build_v0.1.0 | ||
| * |
There was a problem hiding this comment.
The PR description mentions tags like microsoft-fast-build-vX.Y.Z, but this script/documentation uses {package-name}_v{version} (underscore) and examples like microsoft-fast-build_v0.1.0. Please align the implementation/docs with the actual tag format being produced/consumed to avoid silently never finding the intended tag.
| const repoRoot = join(__dirname, ".."); | ||
| const crateDir = join(repoRoot, "crates", "microsoft-fast-build"); | ||
| const cargoTomlPath = join(crateDir, "Cargo.toml"); | ||
| const outputDir = join(repoRoot, "publish_artifacts_cargo"); |
There was a problem hiding this comment.
publish_artifacts_cargo/ is written to the repo root but isn’t currently covered by the existing ignore list (only publish_artifacts/ is ignored). This will leave new untracked files after publish-ci, which can break workflows that enforce a clean working tree. Either add publish_artifacts_cargo/ to .gitignore (preferred) or have the script write into an already-ignored directory / clean up after itself.
| const outputDir = join(repoRoot, "publish_artifacts_cargo"); | |
| const outputDir = join(repoRoot, "publish_artifacts", "cargo"); |
| * Version bump rules (conventional commits): | ||
| * BREAKING CHANGE / feat!: / fix!: → major | ||
| * feat: → minor | ||
| * anything else → patch | ||
| * | ||
| * Only commits that touch crates/microsoft-fast-build/ are considered. | ||
| * If no such commits exist since the last Beachball tag, the script exits without change. | ||
| */ | ||
| import { execSync } from "node:child_process"; | ||
| import { readFileSync, writeFileSync, mkdirSync, copyFileSync, globSync } from "node:fs"; | ||
| import { join, dirname } from "node:path"; | ||
| import { fileURLToPath } from "node:url"; | ||
|
|
||
| const __dirname = dirname(fileURLToPath(import.meta.url)); | ||
| const repoRoot = join(__dirname, ".."); | ||
| const crateDir = join(repoRoot, "crates", "microsoft-fast-build"); | ||
| const cargoTomlPath = join(crateDir, "Cargo.toml"); | ||
| const outputDir = join(repoRoot, "publish_artifacts_cargo"); | ||
|
|
||
| function getCurrentVersion() { | ||
| const content = readFileSync(cargoTomlPath, "utf-8"); | ||
| const match = content.match(/^version\s*=\s*"([^"]+)"/m); | ||
| if (!match) throw new Error("Could not find version in Cargo.toml"); | ||
| return match[1]; | ||
| } | ||
|
|
||
| function parseVersion(version) { | ||
| const [major, minor, patch] = version.split(".").map(Number); | ||
| return { major, minor, patch }; | ||
| } | ||
|
|
||
| function getCommitsSinceCrateLastTag(crateName, currentVersion) { | ||
| // Beachball generates tags in the format {package-name}_v{version} | ||
| const tagName = `${crateName}_v${currentVersion}`; | ||
| let fromRef; | ||
| try { | ||
| execSync(`git -C "${repoRoot}" rev-parse "${tagName}"`, { stdio: "pipe" }); | ||
| fromRef = tagName; | ||
| } catch { | ||
| fromRef = null; | ||
| } | ||
|
|
||
| const range = fromRef ? `${fromRef}..HEAD` : "HEAD"; | ||
| const log = execSync( | ||
| `git -C "${repoRoot}" log ${range} --pretty=format:"%s%n%b" -- crates/microsoft-fast-build/`, | ||
| { encoding: "utf-8" } | ||
| ); | ||
| return log.trim(); | ||
| } | ||
|
|
||
| function determineBump(commits) { | ||
| if (!commits) return null; | ||
|
|
||
| if (/BREAKING CHANGE/m.test(commits) || /^(feat|fix|refactor|chore)!(\([^)]*\))?:/m.test(commits)) { | ||
| return "major"; | ||
| } | ||
| if (/^feat(\([^)]*\))?:/m.test(commits)) { | ||
| return "minor"; | ||
| } | ||
| return "patch"; |
There was a problem hiding this comment.
The header comment’s major-bump rules say BREAKING CHANGE / feat!: / fix!: cause a major, but determineBump also treats refactor! and chore! as major. Update the doc comment to match the implemented behavior (or narrow the regex) so future maintainers don’t misinterpret the bump logic.
| * Version bump rules (conventional commits): | ||
| * BREAKING CHANGE / feat!: / fix!: → major | ||
| * feat: → minor | ||
| * anything else → patch | ||
| * |
There was a problem hiding this comment.
The PR description says bump logic is only feat: → minor and everything else → patch, but this script also performs a major bump when it detects BREAKING CHANGE or type!:. Either update the PR description to reflect the major-bump behavior or remove the major path if it’s not intended.
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
- Use 'git tag --list' to find latest Beachball tag matching the crate instead of constructing a tag name from Cargo.toml version, which can diverge from the actual last-released version - Add publish_artifacts_cargo/ to .gitignore so CI clean-tree checks pass Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Summary
When
npm run publish-ciruns, it now also handles themicrosoft-fast-buildRust crate automatically.Changes
build/publish-rust.mjs(new)A Node.js script that:
microsoft-fast-build_v*(e.g.microsoft-fast-build_v0.1.0) usinggit tag --list, falling back to all history if none existscrates/microsoft-fast-build/since that tagfeat:→ minorcrates/microsoft-fast-build/Cargo.tomlcargo packageand copies the.cratefile topublish_artifacts_cargo/package.jsonUpdated
publish-cito chain the Rust script after beachball:.gitignoreAdded
publish_artifacts_cargo/so the packaged artifacts don't show as untracked files and break clean-tree checks.Behavior
crates/microsoft-fast-build/publish_artifacts_cargo/This mirrors the existing pattern: beachball →
publish_artifacts(npm), new script →publish_artifacts_cargo(Rust).