Skip to content
Draft
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
117 changes: 117 additions & 0 deletions build/publish-rust.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
#!/usr/bin/env node
/**
* 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
*
Comment on lines +3 to +8
Copy link

Copilot AI Apr 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
* Version bump rules (conventional commits):
* BREAKING CHANGE / feat!: / fix!: → major
* feat: → minor
* anything else → patch
*
Comment on lines +9 to +13
Copy link

Copilot AI Apr 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
* 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");
Copy link

Copilot AI Apr 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
const outputDir = join(repoRoot, "publish_artifacts_cargo");
const outputDir = join(repoRoot, "publish_artifacts", "cargo");

Copilot uses AI. Check for mistakes.

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();
Copy link

Copilot AI Apr 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Copilot uses AI. Check for mistakes.
}

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";
Comment on lines +9 to +66
Copy link

Copilot AI Apr 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
}

function bumpVersion(version, bump) {
const { major, minor, patch } = parseVersion(version);
switch (bump) {
case "major": return `${major + 1}.0.0`;
case "minor": return `${major}.${minor + 1}.0`;
case "patch": return `${major}.${minor}.${patch + 1}`;
}
}

function updateCargoToml(newVersion) {
let content = readFileSync(cargoTomlPath, "utf-8");
content = content.replace(/^(version\s*=\s*)"[^"]+"/m, `$1"${newVersion}"`);
writeFileSync(cargoTomlPath, content);
}

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");
const crateFiles = globSync(`microsoft-fast-build-${version}.crate`, { cwd: targetPackageDir });
if (crateFiles.length === 0) {
throw new Error(`Could not find packaged .crate file in ${targetPackageDir}`);
}
for (const file of crateFiles) {
copyFileSync(join(targetPackageDir, file), join(outputDir, file));
console.log(`Packaged ${file} → ${outputDir}`);
}
}

const currentVersion = getCurrentVersion();
const commits = getCommitsSinceCrateLastTag("microsoft-fast-build", currentVersion);
const bump = determineBump(commits);

if (!bump) {
console.log("No changes to Rust crate since last release, skipping.");
process.exit(0);
}

const newVersion = bumpVersion(currentVersion, bump);
console.log(`Bumping microsoft-fast-build: ${currentVersion} → ${newVersion} (${bump})`);

updateCargoToml(newVersion);
packageCrate(newVersion);
console.log(`Rust crate microsoft-fast-build@${newVersion} packaged to ${outputDir}/`);
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
"check": "beachball check ",
"build:gh-pages": "npm run build -w @microsoft/fast-site",
"publish": "beachball publish",
"publish-ci": "beachball publish -y --no-publish",
"publish-ci": "beachball publish -y --no-publish && node build/publish-rust.mjs",
"sync": "beachball sync",
"test:diff:error": "echo \"Untracked files exist, try running npm prepare to identify the culprit.\" && exit 1",
"test:diff": "git update-index --refresh && git diff-index --quiet HEAD -- || npm run test:diff:error",
Expand Down
Loading