Skip to content

Commit c90c74a

Browse files
committed
WIP
1 parent 8509da7 commit c90c74a

8 files changed

Lines changed: 335 additions & 4 deletions

File tree

.ado/azure-pipelines.publish.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,11 @@ extends:
7878
displayName: Enable No-Publish (npm)
7979
condition: ${{ parameters.skipNpmPublish }}
8080
81+
- script: |
82+
node scripts/check-packages-need-publishing.ts
83+
displayName: 'Validate packages need publishing'
84+
condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'), ne('${{ parameters.skipNpmPublish }}', true))
85+
8186
- script: |
8287
# Use --no-bump and --no-push because versions have already been committed by Github Actions earlier
8388
npx beachball publish --no-bump --no-push $(SkipNpmPublishArgs) --access public --token $(npmAuth) -y --verbose

.ado/templates/setup-repo.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@ steps:
77

88
- task: UseNode@1
99
inputs:
10-
version: '22.x'
11-
displayName: 'Use Node.js 22.x'
10+
version: '24'
11+
displayName: 'Use Node.js 24'
1212

1313
- script: |
1414
yarn

.github/workflows/create-version-bump-pr.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ jobs:
3737
platform: node
3838
project-root: .
3939
cache-key-prefix: version-bump
40-
node-version: '22.12'
40+
node-version: '24'
4141
cache-npm-dependencies: yarn
4242

4343
- name: Install dependencies
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
name: Validate Change Files
2+
3+
on:
4+
pull_request:
5+
paths:
6+
- 'change/**'
7+
8+
permissions:
9+
contents: read
10+
11+
jobs:
12+
validate:
13+
runs-on: ubuntu-latest
14+
15+
steps:
16+
- name: Checkout repository
17+
uses: actions/checkout@v4
18+
with:
19+
fetch-depth: 0
20+
21+
- name: Setup the toolchain
22+
uses: microsoft/react-native-test-app/.github/actions/setup-toolchain@4.4.5
23+
with:
24+
platform: node
25+
project-root: .
26+
cache-key-prefix: validate-changefiles
27+
node-version: '24'
28+
cache-npm-dependencies: yarn
29+
30+
- name: Install dependencies
31+
run: yarn install --immutable
32+
33+
- name: Check change files are valid
34+
run: npx beachball check --verbose
35+
36+
- name: Preview packages that would be published
37+
run: |
38+
echo "## 📦 Packages that would be published after this PR merges" >> $GITHUB_STEP_SUMMARY
39+
echo "" >> $GITHUB_STEP_SUMMARY
40+
41+
# Run the check script in dry-run mode and capture output
42+
# Node 24 has native TypeScript support
43+
if node scripts/check-packages-need-publishing.ts --dry-run; then
44+
echo "✅ Package validation successful" >> $GITHUB_STEP_SUMMARY
45+
else
46+
echo "❌ Package validation failed" >> $GITHUB_STEP_SUMMARY
47+
fi
48+
49+
- name: Run beachball bump (dry-run)
50+
run: |
51+
echo "🔄 Running beachball bump to preview version changes..."
52+
npx beachball bump --verbose
53+
54+
- name: Preview NPM publish (dry-run)
55+
run: |
56+
echo "## 🚀 NPM Publish Preview" >> $GITHUB_STEP_SUMMARY
57+
echo "" >> $GITHUB_STEP_SUMMARY
58+
echo "Running \`beachball publish\` in dry-run mode (--no-publish)..." >> $GITHUB_STEP_SUMMARY
59+
echo "" >> $GITHUB_STEP_SUMMARY
60+
echo '```' >> $GITHUB_STEP_SUMMARY
61+
62+
# Run beachball publish with --no-publish flag (dry-run)
63+
# This does everything except actually publish to NPM
64+
npx beachball publish --no-bump --no-push --no-publish --access public -y --verbose 2>&1 | tee /tmp/publish-preview.log
65+
66+
# Add relevant output to summary
67+
echo '```' >> $GITHUB_STEP_SUMMARY
68+
echo "" >> $GITHUB_STEP_SUMMARY
69+
echo "✅ Dry-run completed successfully" >> $GITHUB_STEP_SUMMARY

.node-version

Lines changed: 0 additions & 1 deletion
This file was deleted.

CONTRIBUTING.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,21 @@ This repo manages semantic versioning and publishing using [Beachball](https://g
236236
1. `yarn change` will take you through a command line wizard to generate change files
237237
2. Make sure to push the newly generated change file
238238

239+
#### Publishing Workflow
240+
241+
The repository uses an automated publishing workflow:
242+
243+
1. **Change Files**: Contributors create change files using `yarn change` in their PRs
244+
2. **Version Bump PR**: When change files are merged to `main`, GitHub Actions automatically creates/updates a version bump PR
245+
- This PR is updated automatically as more changes are merged
246+
- The PR shows all packages that will be published and their new versions
247+
3. **Review and Merge**: Maintainers review the version bump PR and merge when ready
248+
4. **Automatic Publishing**: After the version bump PR is merged, Azure Pipelines automatically publishes packages to NPM
249+
250+
**Branch Support**: Only the `main` branch is configured for automatic publishing. Release branches are not supported in this workflow.
251+
252+
**For Maintainers**: The version bump PR is created by GitHub Actions using a fixed branch name (`beachball/version-bump/main`). This PR will be automatically updated as new change files are merged to main. Do not manually close or recreate this PR unless necessary.
253+
239254
#### Testing changes
240255

241256
Before you create a pull request, test your changes with the FluentUI Tester on the platforms that are affected by your change. For more information on the FluentUI Tester, please follow instructions in the [FluentUI Tester readme](./apps/fluent-tester/README.md).
Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
#!/usr/bin/env node
2+
3+
/**
4+
* Check which packages need publishing to NPM
5+
*
6+
* Scans all packages in the monorepo and checks if their current versions
7+
* exist on NPM. Fails if no packages need publishing (all versions already exist).
8+
*
9+
* Exit codes:
10+
* 0 - Success, packages need publishing
11+
* 1 - Error, no packages need publishing or script failed
12+
*/
13+
14+
import { execSync, type ExecException } from 'child_process';
15+
import { readFileSync } from 'fs';
16+
import { join } from 'path';
17+
18+
interface PackageJson {
19+
name: string;
20+
version: string;
21+
private?: boolean;
22+
}
23+
24+
/**
25+
* Check if a specific package version exists on NPM
26+
*/
27+
function checkPackageOnNpm(packageName: string, version: string): boolean {
28+
try {
29+
execSync(`npm view ${packageName}@${version} version`, {
30+
stdio: 'pipe',
31+
encoding: 'utf8',
32+
});
33+
return true; // Package exists on NPM
34+
} catch (error) {
35+
// npm view exits with code 1 when package doesn't exist (404)
36+
// Check if this is a "not found" error vs a real error (network, etc)
37+
const execError = error as ExecException;
38+
const stderr = execError.stderr?.toString() || '';
39+
if (stderr.includes('404') || stderr.includes('Not Found')) {
40+
return false; // Package doesn't exist on NPM
41+
}
42+
// For other errors (network issues, npm down, etc), throw so we don't incorrectly
43+
// report that packages need publishing when we can't actually check NPM
44+
throw error;
45+
}
46+
}
47+
48+
/**
49+
* Get all workspace packages using yarn workspaces list
50+
*/
51+
function getWorkspacePackages(): string[] {
52+
try {
53+
const output = execSync('yarn workspaces list --json', {
54+
encoding: 'utf8',
55+
stdio: ['pipe', 'pipe', 'pipe'],
56+
});
57+
58+
const workspaces: string[] = [];
59+
// Each line is a JSON object
60+
for (const line of output.trim().split('\n')) {
61+
const workspace = JSON.parse(line);
62+
// Skip the root workspace (location is '.')
63+
if (workspace.location && workspace.location !== '.') {
64+
workspaces.push(join(process.cwd(), workspace.location, 'package.json'));
65+
}
66+
}
67+
68+
return workspaces;
69+
} catch (error) {
70+
console.error('❌ ERROR: Failed to get yarn workspaces');
71+
console.error((error as Error).message);
72+
process.exit(1);
73+
}
74+
}
75+
76+
/**
77+
* Main function that checks all packages in the monorepo
78+
*/
79+
function main(dryRun = false): void {
80+
console.log('🔍 Checking which packages need publishing...\n');
81+
82+
const packagesToPublish: string[] = [];
83+
const packagesAlreadyPublished: string[] = [];
84+
const packagesSkipped: string[] = [];
85+
86+
const packageJsonPaths = getWorkspacePackages();
87+
88+
for (const packageJsonPath of packageJsonPaths) {
89+
90+
let packageJson: PackageJson;
91+
try {
92+
const content = readFileSync(packageJsonPath, 'utf8');
93+
packageJson = JSON.parse(content);
94+
} catch (error) {
95+
console.error(`⚠️ Failed to read ${packageJsonPath}:`, (error as Error).message);
96+
continue;
97+
}
98+
99+
const { name, version, private: isPrivate } = packageJson;
100+
101+
if (!name || !version) {
102+
console.log(`⏭️ Skipping ${packageJsonPath}: missing name or version`);
103+
packagesSkipped.push(packageJsonPath);
104+
continue;
105+
}
106+
107+
if (isPrivate) {
108+
console.log(`⏭️ Skipping private package: ${name}@${version}`);
109+
packagesSkipped.push(`${name}@${version}`);
110+
continue;
111+
}
112+
113+
const existsOnNpm = checkPackageOnNpm(name, version);
114+
115+
if (existsOnNpm) {
116+
console.log(`✅ Already published: ${name}@${version}`);
117+
packagesAlreadyPublished.push(`${name}@${version}`);
118+
} else {
119+
console.log(`📦 Will publish: ${name}@${version}`);
120+
packagesToPublish.push(`${name}@${version}`);
121+
}
122+
}
123+
124+
// Print summary
125+
console.log('\n' + '='.repeat(60));
126+
console.log('Summary:');
127+
console.log(` Packages to publish: ${packagesToPublish.length}`);
128+
console.log(` Already on NPM: ${packagesAlreadyPublished.length}`);
129+
console.log(` Skipped: ${packagesSkipped.length}`);
130+
console.log('='.repeat(60));
131+
132+
// Print packages to publish if any
133+
if (dryRun && packagesToPublish.length > 0) {
134+
console.log('\nPackages that will be published:');
135+
packagesToPublish.forEach(pkg => console.log(` - ${pkg}`));
136+
}
137+
138+
// Fail if nothing to publish (unless dry-run)
139+
if (packagesToPublish.length === 0) {
140+
if (dryRun) {
141+
console.log('\n✅ Dry-run: No packages would be published');
142+
console.log('All package versions already exist on NPM.');
143+
process.exit(0);
144+
} else {
145+
console.log('\n❌ ERROR: No packages need publishing!');
146+
console.log('All package versions already exist on NPM.\n');
147+
console.log('This likely means:');
148+
console.log(' 1. The version bump PR was merged without actually bumping versions');
149+
console.log(' 2. Packages were already published manually');
150+
console.log(' 3. The version bump workflow didn\'t run correctly');
151+
process.exit(1);
152+
}
153+
}
154+
155+
if (dryRun) {
156+
console.log(`\n✅ Dry-run: ${packagesToPublish.length} package(s) would be published`);
157+
} else {
158+
console.log(`\n✅ Ready to publish ${packagesToPublish.length} package(s)`);
159+
}
160+
process.exit(0);
161+
}
162+
163+
// Parse CLI args
164+
const args = process.argv.slice(2);
165+
const dryRun = args.includes('--dry-run');
166+
167+
main(dryRun);

scripts/test-package-check.sh

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
#!/bin/bash
2+
3+
# Test script to compare beachball bump output with check-packages-need-publishing.ts
4+
5+
set -e
6+
7+
# Activate mise if available
8+
if [ -f "$HOME/.local/bin/mise" ]; then
9+
eval "$("$HOME/.local/bin/mise" activate bash)"
10+
fi
11+
12+
echo "=========================================="
13+
echo "Testing package check script"
14+
echo "=========================================="
15+
echo ""
16+
17+
# Step 1: Run beachball bump in dry-run mode to see what it would change
18+
echo "Step 1: Running beachball bump (dry-run)..."
19+
echo "=========================================="
20+
21+
# Create a temporary branch to test
22+
TEMP_BRANCH="test-package-check-$(date +%s)"
23+
git switch -c "$TEMP_BRANCH" 2>/dev/null || git checkout -b "$TEMP_BRANCH"
24+
25+
# Run beachball bump and capture the output
26+
echo ""
27+
echo "Running: npx beachball bump --verbose"
28+
mise exec -- npx beachball bump --verbose 2>&1 | tee /tmp/beachball-bump-output.txt
29+
30+
echo ""
31+
echo "=========================================="
32+
echo "Step 2: Extracting packages bumped by beachball..."
33+
echo "=========================================="
34+
35+
# Get list of modified package.json files
36+
BUMPED_PACKAGES=$(git diff --name-only | grep "package.json" | grep -v "^package.json$" || true)
37+
38+
if [ -z "$BUMPED_PACKAGES" ]; then
39+
echo "⚠️ No packages were bumped by beachball"
40+
echo "This might mean there are no change files or they've already been processed"
41+
else
42+
echo "Beachball bumped the following package.json files:"
43+
echo "$BUMPED_PACKAGES"
44+
echo ""
45+
46+
# Extract package names and versions
47+
echo "Package versions after bump:"
48+
for pkg in $BUMPED_PACKAGES; do
49+
if [ -f "$pkg" ]; then
50+
NAME=$(grep '"name"' "$pkg" | head -1 | sed 's/.*"name": "\(.*\)".*/\1/')
51+
VERSION=$(grep '"version"' "$pkg" | head -1 | sed 's/.*"version": "\(.*\)".*/\1/')
52+
PRIVATE=$(grep '"private"' "$pkg" | head -1 || echo "")
53+
54+
if [ -z "$PRIVATE" ]; then
55+
echo " - $NAME@$VERSION"
56+
fi
57+
fi
58+
done
59+
fi
60+
61+
echo ""
62+
echo "=========================================="
63+
echo "Step 3: Running check-packages-need-publishing.ts..."
64+
echo "=========================================="
65+
echo ""
66+
67+
mise exec -- node scripts/check-packages-need-publishing.ts --dry-run
68+
69+
echo ""
70+
echo "=========================================="
71+
echo "Comparison complete!"
72+
echo "=========================================="
73+
echo ""
74+
echo "To clean up:"
75+
echo " git checkout main"
76+
echo " git branch -D $TEMP_BRANCH"

0 commit comments

Comments
 (0)