Skip to content

Commit 59be995

Browse files
janechuCopilot
andcommitted
chore: automate Rust crate versioning in publish-ci
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>
1 parent 7548617 commit 59be995

4 files changed

Lines changed: 236 additions & 1 deletion

File tree

build/DESIGN.md

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
# Design — @microsoft/fast-build-tools
2+
3+
This document explains how the build utilities work internally.
4+
5+
---
6+
7+
## publish-rust.mjs
8+
9+
### Purpose
10+
11+
Bridges the gap between Beachball (which manages npm package versioning) and the Rust crate (`microsoft-fast-build`) which lives outside the npm workspace structure. The script replicates Beachball's "inspect commits → bump version → package artifact" pattern for the Rust side.
12+
13+
### Data flow
14+
15+
```
16+
git tag --list "microsoft-fast-build_v*"
17+
18+
19+
latestTag (or null if no tags exist)
20+
21+
22+
git log ${latestTag}..HEAD -- crates/microsoft-fast-build/
23+
24+
25+
commit messages (subject + body)
26+
27+
28+
determineBump(commits)
29+
30+
├─ BREAKING CHANGE / type!: → "major"
31+
├─ feat: → "minor"
32+
└─ anything else → "patch"
33+
34+
35+
bumpVersion(current, bump)
36+
37+
38+
updateCargoToml(newVersion) ← in-place version replacement
39+
40+
41+
cargo package --no-verify --allow-dirty
42+
43+
44+
copy .crate → publish_artifacts/cargo/
45+
```
46+
47+
### Design decisions
48+
49+
1. **Tag-based range** — The script finds the latest `microsoft-fast-build_v*` tag via `git tag --list --sort=-version:refname` rather than constructing a tag name from the current `Cargo.toml` version. This avoids breakage if the Cargo.toml version diverges from the most recent tag.
50+
51+
2. **Output to `publish_artifacts/cargo/`** — Packaged crate files are placed under the existing `publish_artifacts/` directory (already in `.gitignore`) in a `cargo/` subdirectory. This mirrors the npm pattern (`publish_artifacts/` for npm packages) without requiring additional `.gitignore` entries.
52+
53+
3. **Conventional commit parsing** — The regex patterns match standard conventional commit prefixes. The `BREAKING CHANGE` trailer and `type!:` syntax both trigger major bumps, matching the conventional commits specification.
54+
55+
4. **No-op on no changes** — If no commits touch the crate directory since the last tag, the script exits with code 0 and produces no side effects. This allows `publish-ci` to always invoke it safely.
56+
57+
5. **`--no-verify --allow-dirty`**`cargo package` is invoked with these flags because the working tree may contain uncommitted Beachball changes and the crate tests are validated separately in CI.
58+
59+
### Integration
60+
61+
The script is chained before `beachball publish` in the root `package.json`:
62+
63+
```json
64+
"publish-ci": "node build/publish-rust.mjs && beachball publish -y --no-publish"
65+
```
66+
67+
Running the Rust step first ensures the `Cargo.toml` version bump is captured before Beachball creates its commit and tags.

build/README.md

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
# @microsoft/fast-build-tools
2+
3+
Private build utilities for the FAST monorepo. These scripts are not published — they support internal CI, formatting, and packaging workflows.
4+
5+
## Scripts
6+
7+
### `publish-rust.mjs`
8+
9+
Automates version bumping and packaging of the `microsoft-fast-build` Rust crate during CI publish runs.
10+
11+
**How it works:**
12+
13+
1. Finds the latest Beachball-generated tag matching `microsoft-fast-build_v*`
14+
2. Inspects git commits touching `crates/microsoft-fast-build/` since that tag
15+
3. Determines the version bump type using [conventional commits](https://www.conventionalcommits.org/):
16+
- `BREAKING CHANGE` / `feat!:` / `fix!:` / `refactor!:` / `chore!:`**major**
17+
- `feat:`**minor**
18+
- anything else → **patch**
19+
4. Updates the version in `crates/microsoft-fast-build/Cargo.toml`
20+
5. Runs `cargo package` and copies the `.crate` file to `publish_artifacts/cargo/`
21+
6. Exits cleanly (no-op) if no relevant commits exist since the last release
22+
23+
**Usage:**
24+
25+
The script is invoked automatically by the root `publish-ci` npm script:
26+
27+
```bash
28+
npm run publish-ci
29+
# runs: node build/publish-rust.mjs && beachball publish -y --no-publish
30+
```
31+
32+
### `biome-changed.mjs`
33+
34+
Runs Biome linting/formatting only on files with uncommitted git changes. Used by `npm run lint`, `npm run biome:check`, and related commands.
35+
36+
### `clean.mjs`
37+
38+
Deletes specified directories. Used by workspace `clean` scripts to remove `dist/` and other build output.
39+
40+
### `get-package-json.js`
41+
42+
Resolves the directory of a dependency's `package.json`. Used internally by build configuration.

build/publish-rust.mjs

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
#!/usr/bin/env node
2+
/**
3+
* Bumps the Rust crate version based on conventional commits since the last
4+
* Beachball-generated release tag, then packages the crate into publish_artifacts/cargo/.
5+
*
6+
* Beachball tags use the format: {package-name}_v{version}
7+
* e.g. microsoft-fast-build_v0.1.0
8+
*
9+
* Version bump rules (conventional commits):
10+
* BREAKING CHANGE / feat!: / fix!: / refactor!: / chore!: → major
11+
* feat: → minor
12+
* anything else → patch
13+
*
14+
* Only commits that touch crates/microsoft-fast-build/ are considered.
15+
* If no such commits exist since the last Beachball tag, the script exits without change.
16+
*/
17+
import { execSync } from "node:child_process";
18+
import { copyFileSync, globSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
19+
import { dirname, join } from "node:path";
20+
import { fileURLToPath } from "node:url";
21+
22+
const __dirname = dirname(fileURLToPath(import.meta.url));
23+
const repoRoot = join(__dirname, "..");
24+
const crateDir = join(repoRoot, "crates", "microsoft-fast-build");
25+
const cargoTomlPath = join(crateDir, "Cargo.toml");
26+
const outputDir = join(repoRoot, "publish_artifacts", "cargo");
27+
28+
function getCurrentVersion() {
29+
const content = readFileSync(cargoTomlPath, "utf-8");
30+
const match = content.match(/^version\s*=\s*"([^"]+)"/m);
31+
if (!match) throw new Error("Could not find version in Cargo.toml");
32+
return match[1];
33+
}
34+
35+
function parseVersion(version) {
36+
const [major, minor, patch] = version.split(".").map(Number);
37+
return { major, minor, patch };
38+
}
39+
40+
function getCommitsSinceLastTag(crateName) {
41+
// Beachball generates tags in the format {package-name}_v{version}.
42+
// Find the latest matching tag rather than constructing one from the
43+
// current Cargo.toml version, which may diverge from the last release.
44+
const latestTag =
45+
execSync(
46+
`git -C "${repoRoot}" tag --list "${crateName}_v*" --sort=-version:refname`,
47+
{ encoding: "utf-8" },
48+
)
49+
.trim()
50+
.split("\n")[0] || null;
51+
52+
const range = latestTag ? `${latestTag}..HEAD` : "HEAD";
53+
const log = execSync(
54+
`git -C "${repoRoot}" log ${range} --pretty=format:"%s%n%b" -- crates/microsoft-fast-build/`,
55+
{ encoding: "utf-8" },
56+
);
57+
return log.trim();
58+
}
59+
60+
function determineBump(commits) {
61+
if (!commits) return null;
62+
63+
if (
64+
/BREAKING CHANGE/m.test(commits) ||
65+
/^(feat|fix|refactor|chore)!(\([^)]*\))?:/m.test(commits)
66+
) {
67+
return "major";
68+
}
69+
if (/^feat(\([^)]*\))?:/m.test(commits)) {
70+
return "minor";
71+
}
72+
return "patch";
73+
}
74+
75+
function bumpVersion(version, bump) {
76+
const { major, minor, patch } = parseVersion(version);
77+
switch (bump) {
78+
case "major":
79+
return `${major + 1}.0.0`;
80+
case "minor":
81+
return `${major}.${minor + 1}.0`;
82+
case "patch":
83+
return `${major}.${minor}.${patch + 1}`;
84+
}
85+
}
86+
87+
function updateCargoToml(newVersion) {
88+
let content = readFileSync(cargoTomlPath, "utf-8");
89+
content = content.replace(/^(version\s*=\s*)"[^"]+"/m, `$1"${newVersion}"`);
90+
writeFileSync(cargoTomlPath, content);
91+
}
92+
93+
function packageCrate(version) {
94+
mkdirSync(outputDir, { recursive: true });
95+
execSync(
96+
`cargo package --manifest-path "${cargoTomlPath}" --no-verify --allow-dirty`,
97+
{ stdio: "inherit" },
98+
);
99+
const targetPackageDir = join(crateDir, "target", "package");
100+
const crateFiles = globSync(`microsoft-fast-build-${version}.crate`, {
101+
cwd: targetPackageDir,
102+
});
103+
if (crateFiles.length === 0) {
104+
throw new Error(`Could not find packaged .crate file in ${targetPackageDir}`);
105+
}
106+
for (const file of crateFiles) {
107+
copyFileSync(join(targetPackageDir, file), join(outputDir, file));
108+
console.log(`Packaged ${file}${outputDir}`);
109+
}
110+
}
111+
112+
const currentVersion = getCurrentVersion();
113+
const commits = getCommitsSinceLastTag("microsoft-fast-build");
114+
const bump = determineBump(commits);
115+
116+
if (!bump) {
117+
console.log("No changes to Rust crate since last release, skipping.");
118+
process.exit(0);
119+
}
120+
121+
const newVersion = bumpVersion(currentVersion, bump);
122+
console.log(`Bumping microsoft-fast-build: ${currentVersion}${newVersion} (${bump})`);
123+
124+
updateCargoToml(newVersion);
125+
packageCrate(newVersion);
126+
console.log(`Rust crate microsoft-fast-build@${newVersion} packaged to ${outputDir}/`);

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@
3434
"check": "beachball check ",
3535
"build:gh-pages": "npm run build -w @microsoft/fast-site",
3636
"publish": "beachball publish",
37-
"publish-ci": "beachball publish -y --no-publish",
37+
"publish-ci": "node build/publish-rust.mjs && beachball publish -y --no-publish",
3838
"sync": "beachball sync",
3939
"test:diff:error": "echo \"Untracked files exist, try running npm prepare to identify the culprit.\" && exit 1",
4040
"test:diff": "git update-index --refresh && git diff-index --quiet HEAD -- || npm run test:diff:error",

0 commit comments

Comments
 (0)