Skip to content

Commit 5bb59ff

Browse files
Add a bunch of validation
1 parent 7e1411a commit 5bb59ff

File tree

6 files changed

+209
-26
lines changed

6 files changed

+209
-26
lines changed
File renamed without changes.
Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
#!/usr/bin/env node
2+
/**
3+
* Validate changesets in CI
4+
*
5+
* Checks:
6+
* 1. Changesets are present (PRs require changesets)
7+
* 2. No major version bumps (breaking changes disallowed)
8+
* 3. Changeset status passes (validates format, config, dependencies)
9+
*
10+
* Usage: node .github/scripts/validate-changesets.mts
11+
*/
12+
13+
import { execSync } from 'child_process';
14+
import { readFileSync, readdirSync } from 'fs';
15+
import { join } from 'path';
16+
17+
const CHANGESETS_DIR = '.changeset';
18+
const RED = '\x1b[31m';
19+
const GREEN = '\x1b[32m';
20+
const YELLOW = '\x1b[33m';
21+
const RESET = '\x1b[0m';
22+
23+
interface ChangesetFrontmatter {
24+
[packageName: string]: 'major' | 'minor' | 'patch';
25+
}
26+
27+
function parseChangesetForMajorCheck(filePath: string): ChangesetFrontmatter | null {
28+
try {
29+
const content = readFileSync(filePath, 'utf-8');
30+
31+
// Extract frontmatter between --- markers
32+
const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
33+
if (!frontmatterMatch) {
34+
return null;
35+
}
36+
37+
const frontmatter = frontmatterMatch[1];
38+
const result: ChangesetFrontmatter = {};
39+
40+
// Parse YAML-like frontmatter
41+
// Format: "@scope/package": minor
42+
const lines = frontmatter.split('\n').filter(line => line.trim());
43+
for (const line of lines) {
44+
const match = line.match(/^["']?([^"':]+)["']?\s*:\s*(major|minor|patch)/);
45+
if (match) {
46+
const [, packageName, bumpType] = match;
47+
result[packageName.trim()] = bumpType as 'major' | 'minor' | 'patch';
48+
}
49+
}
50+
51+
return Object.keys(result).length > 0 ? result : null;
52+
} catch (error) {
53+
// Parsing errors will be caught by changeset status
54+
return null;
55+
}
56+
}
57+
58+
function checkChangesetPresence(): boolean {
59+
console.log('\n🔍 Checking for changeset presence...\n');
60+
61+
try {
62+
const statusOutput = execSync('yarn changeset status --since=origin/main 2>&1', {
63+
encoding: 'utf-8'
64+
});
65+
66+
if (statusOutput.includes('No changesets present')) {
67+
console.log(`${RED}❌ No changesets found${RESET}\n`);
68+
console.log(`${YELLOW}This PR requires a changeset to document the changes.${RESET}`);
69+
console.log(`${YELLOW}To create a changeset, run: yarn changeset${RESET}\n`);
70+
return false;
71+
}
72+
73+
console.log(`${GREEN}✅ Changesets found${RESET}`);
74+
return true;
75+
} catch (error: any) {
76+
console.log(`${RED}❌ Failed to check changeset status${RESET}\n`);
77+
console.log(error.message);
78+
return false;
79+
}
80+
}
81+
82+
function checkForMajorBumps(): boolean {
83+
console.log('\n🔍 Checking for major version bumps...\n');
84+
85+
const changesetFiles = readdirSync(CHANGESETS_DIR)
86+
.filter(file => file.endsWith('.md') && file !== 'README.md')
87+
.map(file => join(CHANGESETS_DIR, file));
88+
89+
if (changesetFiles.length === 0) {
90+
console.log(`${YELLOW}No changesets found (skipping major check)${RESET}`);
91+
return true;
92+
}
93+
94+
let hasMajor = false;
95+
const majorBumps: Array<{ file: string; packages: string[] }> = [];
96+
97+
for (const file of changesetFiles) {
98+
const frontmatter = parseChangesetForMajorCheck(file);
99+
if (!frontmatter) continue;
100+
101+
const majorPackages = Object.entries(frontmatter)
102+
.filter(([, bumpType]) => bumpType === 'major')
103+
.map(([pkg]) => pkg);
104+
105+
if (majorPackages.length > 0) {
106+
hasMajor = true;
107+
majorBumps.push({ file, packages: majorPackages });
108+
}
109+
}
110+
111+
if (hasMajor) {
112+
console.log(`${RED}❌ Major version bumps detected!${RESET}\n`);
113+
for (const { file, packages } of majorBumps) {
114+
console.log(`${RED} ${file}:${RESET}`);
115+
for (const pkg of packages) {
116+
console.log(`${RED} - ${pkg}: major${RESET}`);
117+
}
118+
}
119+
console.log(`\n${RED}Major version bumps are not allowed.${RESET}`);
120+
console.log(`${YELLOW}If you need to make a breaking change, please discuss with the team first.${RESET}\n`);
121+
return false;
122+
}
123+
124+
console.log(`${GREEN}✅ No major version bumps found${RESET}`);
125+
return true;
126+
}
127+
128+
function validateChangesetStatus(): boolean {
129+
console.log('\n🔍 Validating changesets with changeset status...\n');
130+
131+
try {
132+
// This validates:
133+
// - Changeset file format
134+
// - Package references
135+
// - Dependency graph
136+
// - Config validity
137+
const statusOutput = execSync('yarn changeset status --since=origin/main 2>&1', {
138+
encoding: 'utf-8'
139+
});
140+
141+
// Check for errors (but "No changesets present" is not an error here)
142+
if (statusOutput.toLowerCase().includes('error') && !statusOutput.includes('No changesets present')) {
143+
console.log(`${RED}❌ Changeset validation failed!${RESET}\n`);
144+
console.log(statusOutput);
145+
return false;
146+
}
147+
148+
console.log(`${GREEN}✅ Changeset validation passed${RESET}`);
149+
return true;
150+
} catch (error: any) {
151+
console.log(`${RED}❌ Changeset validation failed!${RESET}\n`);
152+
console.log(`${RED}Error:${RESET}`, error.message);
153+
if (error.stdout) console.log(error.stdout);
154+
if (error.stderr) console.log(error.stderr);
155+
return false;
156+
}
157+
}
158+
159+
function main(): void {
160+
console.log(`\n${'='.repeat(60)}`);
161+
console.log('Changesets Validation');
162+
console.log(`${'='.repeat(60)}`);
163+
164+
const results = {
165+
presence: checkChangesetPresence(),
166+
majorBumps: checkForMajorBumps(),
167+
validation: validateChangesetStatus()
168+
};
169+
170+
console.log(`\n${'='.repeat(60)}`);
171+
console.log('Validation Results:');
172+
console.log(`${'='.repeat(60)}\n`);
173+
174+
console.log(`Changeset presence: ${results.presence ? GREEN + '✅ PASS' : RED + '❌ FAIL'}${RESET}`);
175+
console.log(`Major version check: ${results.majorBumps ? GREEN + '✅ PASS' : RED + '❌ FAIL'}${RESET}`);
176+
console.log(`Changeset validation: ${results.validation ? GREEN + '✅ PASS' : RED + '❌ FAIL'}${RESET}\n`);
177+
178+
const allPassed = results.presence && results.majorBumps && results.validation;
179+
180+
if (!allPassed) {
181+
console.log(`${RED}Validation failed!${RESET}\n`);
182+
process.exit(1);
183+
}
184+
185+
console.log(`${GREEN}All validations passed! ✅${RESET}\n`);
186+
}
187+
188+
main();

.github/workflows/changesets-version.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,4 +57,4 @@ jobs:
5757

5858
- name: Post-version hook for dependency-profiles
5959
if: steps.changesets.outputs.hasChangesets == 'true'
60-
run: node scripts/update-dependency-profiles-postbump.mts
60+
run: node .github/scripts/update-dependency-profiles-postbump.mts

.github/workflows/pr-validation.yml

Lines changed: 2 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -30,22 +30,5 @@ jobs:
3030
- name: Install dependencies
3131
run: yarn install --immutable
3232

33-
- name: Check for changesets
34-
run: |
35-
echo "🔍 Checking for changesets..."
36-
37-
# Check if changesets exist
38-
if yarn changeset status --since=origin/main 2>&1 | grep -q "No changesets present"; then
39-
echo "❌ No changesets found"
40-
echo ""
41-
echo "This PR requires a changeset to document the changes."
42-
echo ""
43-
echo "To create a changeset, run:"
44-
echo " yarn changeset"
45-
echo ""
46-
echo "The changeset bot has also left a comment on this PR with instructions."
47-
exit 1
48-
else
49-
echo "✅ Changesets found"
50-
yarn changeset status --verbose
51-
fi
33+
- name: Validate changesets
34+
run: node .github/scripts/validate-changesets.mts

CHANGESETS_SETUP.md

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ The Azure Pipelines publish workflow has been updated to use `changeset publish`
5757

5858
## Phase 3: PR Validation (Configured ✅)
5959

60-
Two layers of changeset validation are now active:
60+
Three layers of changeset validation are now active:
6161

6262
### 1. Changeset Bot (GitHub App) ✅
6363
- **Status**: Installed
@@ -68,12 +68,22 @@ Two layers of changeset validation are now active:
6868
### 2. GitHub Actions PR Validation ✅
6969
- **Workflow**: `.github/workflows/pr-validation.yml`
7070
- **What it does**: Enforces changesets in CI/CD
71-
- Fails the PR check if no changeset is present
71+
- Checks:
72+
- ✅ Changeset files exist
73+
- ✅ No major version bumps (breaking changes disallowed)
74+
- ✅ Changeset version dry-run passes (ensures no errors)
7275
- Automatically skips for version bump PRs (`changeset-release/main`)
7376

74-
**Both work together**:
77+
**Script**: [`.github/scripts/validate-changesets.mts`](.github/scripts/validate-changesets.mts)
78+
79+
**Run locally**:
80+
```bash
81+
yarn changeset:validate
82+
```
83+
84+
**All layers work together**:
7585
- Bot provides immediate visual feedback
76-
- GitHub Actions enforces the requirement
86+
- GitHub Actions enforces requirements and validates quality
7787

7888
## Phase 4: Developer Workflow Changes
7989

@@ -133,11 +143,11 @@ After version bumps, the `dependency-profiles` package needs to be updated with
133143
- Runs `yarn install --mode update-lockfile` to update yarn.lock
134144
- Commits and pushes changes (in CI only)
135145

136-
**Script location:** [`scripts/update-dependency-profiles-postbump.mts`](scripts/update-dependency-profiles-postbump.mts)
146+
**Script location:** [`.github/scripts/update-dependency-profiles-postbump.mts`](.github/scripts/update-dependency-profiles-postbump.mts)
137147

138148
To manually run the script locally (for debugging):
139149
```bash
140-
node scripts/update-dependency-profiles-postbump.mts
150+
node .github/scripts/update-dependency-profiles-postbump.mts
141151
```
142152

143153
## Phase 4: Testing the Workflow

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,10 @@
3434
"change": "beachball change",
3535
"changeset": "changeset",
3636
"changeset:version": "changeset version && yarn install --mode update-lockfile",
37+
"changeset:version:postbump": "node .github/scripts/update-dependency-profiles-postbump.mts",
3738
"changeset:publish": "changeset publish",
3839
"changeset:status": "changeset status",
40+
"changeset:validate": "node .github/scripts/validate-changesets.mts",
3941
"check-for-changed-files": "cd scripts && yarn fluentui-scripts check-changes",
4042
"checkchange": "beachball check --changehint \"Run 'yarn change' to generate a change file\"",
4143
"checkchange:changeset": "changeset status --since=origin/main",

0 commit comments

Comments
 (0)