Skip to content

Commit 0511444

Browse files
authored
Fix CalVer enforcement running on semver-only releases (#3234)
* Fix CalVer enforcement running on semver-only releases Previously, enforce-calver-ci.js would unconditionally run for all CalVer packages (hydrogen, hydrogen-react, skeleton) even when only semver packages (cli, mini-oxygen, create-hydrogen) had changesets. This caused phantom version bumps and CHANGELOG entries for CalVer packages with no actual changes. The fix adds an early exit check that skips CalVer enforcement when no CalVer changesets are detected, ensuring semver-only releases don't touch CalVer packages. Fixes the issue seen in PR #3233 where a CLI-only release incorrectly bumped hydrogen/hydrogen-react/skeleton. * Document semver-only release guard check in CALVER.md * Fix CalVer enforcement guard check to compare version changes The guard check was using hasCalVerChangesets() which checks for .md files in .changeset/ directory. But this runs AFTER 'changeset version' has already deleted those files, causing the check to always return false and skip enforcement for ALL releases (including CalVer releases). Fix: Compare actual version changes by checking if pkg.version differs from git baseline (oldVersion). This correctly: - Skips when only semver packages change (no CalVer version changes) - Runs when any CalVer package version changes - Works regardless of whether changeset files still exist The readPackageVersions() function already provides both values for comparison. * Fix CalVer guard check and dynamic Test 5 enforce-calver-ci.js: Compare version changes instead of checking deleted changeset files test-calver.yml: Calculate expected branches from actual hydrogen version instead of hardcoding 2025-05 * Implement independent patches with synced majors Patches/minors: Each CalVer package uses its own git baseline for independent versioning Majors: All CalVer packages sync to same quarter using hydrogen's baseline This allows hydrogen, hydrogen-react, and skeleton to have different patch versions while ensuring all majors stay synchronized to the same quarter. * Skip unchanged CalVer packages in patch releases When calculating new versions, preserve unchanged packages instead of bumping them. This enables truly independent patch versioning while maintaining major sync. * Add Test 8: Full pipeline integration test Tests complete version script flow for: - Semver-only releases (validates guard check skips CalVer enforcement) - CalVer releases (validates enforcement runs and version.ts updates) This catches regressions in the full pipeline, not just isolated scripts. * Document independent patches and synced majors in CALVER.md CalVer packages now support: - Independent patch/minor versions ([email protected], [email protected]) - Synchronized major versions (all at 2025.10.0 for Q4) * Add test changeset for semver-only release validation Creates a dummy patch changeset for cli-hydrogen to validate that: - Version PR is titled "[ci] release semver" (not CalVer date) - Only cli-hydrogen is bumped (no CalVer packages touched) - enforce-calver-ci.js correctly skips CalVer enforcement
1 parent 12374c8 commit 0511444

File tree

4 files changed

+143
-45
lines changed

4 files changed

+143
-45
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@shopify/cli-hydrogen": patch
3+
---
4+
5+
Test changeset to validate independent patches and synced majors logic for SemVer packages

.changeset/enforce-calver-ci.js

Lines changed: 58 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -25,38 +25,53 @@ const {
2525
} = require('./calver-shared.js');
2626

2727
// Read all package.json files for CalVer packages
28-
// Uses the original version from git as baseline (before changesets corruption)
28+
// Fetches individual git baselines for independent patch versioning
29+
// Hydrogen baseline used for major sync across all CalVer packages
2930
function readPackageVersions() {
3031
const versions = {};
31-
32-
// Get the original version from git (before changesets ran)
33-
// This prevents using corrupted versions like 2026.0.0 that changesets might generate
34-
let sourceOfTruthVersion;
32+
33+
// Get hydrogen's git baseline (used for major bump synchronization)
34+
let hydrogenBaselineVersion;
3535
try {
36-
// Try to get the version from the base branch (before changesets modified it)
3736
const gitVersion = execSync('git show HEAD~1:packages/hydrogen/package.json 2>/dev/null || git show origin/main:packages/hydrogen/package.json', {
3837
encoding: 'utf-8',
3938
stdio: ['pipe', 'pipe', 'ignore']
4039
});
4140
const versionMatch = gitVersion.match(/"version":\s*"([^"]+)"/);
4241
if (versionMatch) {
43-
sourceOfTruthVersion = versionMatch[1];
44-
console.log(`Using original version from git: ${sourceOfTruthVersion}`);
42+
hydrogenBaselineVersion = versionMatch[1];
43+
console.log(`Using hydrogen baseline from git: ${hydrogenBaselineVersion}`);
4544
}
4645
} catch (error) {
47-
// Fallback to current version if git command fails
4846
const hydrogenPath = getPackagePath('@shopify/hydrogen');
4947
const hydrogenPkg = readPackage(hydrogenPath);
50-
sourceOfTruthVersion = hydrogenPkg.version;
51-
console.log(`Using current version as fallback: ${sourceOfTruthVersion}`);
48+
hydrogenBaselineVersion = hydrogenPkg.version;
49+
console.log(`Using hydrogen current version as fallback: ${hydrogenBaselineVersion}`);
5250
}
53-
51+
52+
// Get each package's individual baseline for independent patch versioning
5453
for (const pkgName of CALVER_PACKAGES) {
5554
const pkgPath = getPackagePath(pkgName);
5655
const pkg = readPackage(pkgPath);
56+
57+
// Fetch this package's own git baseline
58+
let packageOwnBaseline;
59+
try {
60+
const gitPath = pkgPath.replace(process.cwd() + '/', '');
61+
const gitVersion = execSync(`git show HEAD~1:${gitPath} 2>/dev/null || git show origin/main:${gitPath}`, {
62+
encoding: 'utf-8',
63+
stdio: ['pipe', 'pipe', 'ignore']
64+
});
65+
const versionMatch = gitVersion.match(/"version":\s*"([^"]+)"/);
66+
packageOwnBaseline = versionMatch ? versionMatch[1] : hydrogenBaselineVersion;
67+
} catch (error) {
68+
packageOwnBaseline = hydrogenBaselineVersion;
69+
}
70+
5771
versions[pkgName] = {
5872
path: pkgPath,
59-
oldVersion: sourceOfTruthVersion, // Use original version as baseline for all packages
73+
oldVersion: packageOwnBaseline,
74+
hydrogenBaseline: hydrogenBaselineVersion,
6075
pkg,
6176
};
6277
}
@@ -81,9 +96,17 @@ function calculateNewVersions(versions) {
8196
for (const [pkgName, data] of Object.entries(versions)) {
8297
const bumpType = getBumpType(data.oldVersion, data.pkg.version);
8398

84-
// For major bumps, ensure all CalVer packages advance to same quarter
85-
const effectiveBumpType = hasAnyMajor ? 'major' : bumpType;
86-
const newVersion = getNextVersion(data.oldVersion, effectiveBumpType);
99+
let newVersion;
100+
if (hasAnyMajor) {
101+
// Major: Sync all CalVer packages to same quarter using hydrogen's baseline
102+
newVersion = getNextVersion(data.hydrogenBaseline, 'major');
103+
} else if (data.pkg.version === data.oldVersion) {
104+
// No change: Keep current version (don't bump unchanged packages)
105+
newVersion = data.pkg.version;
106+
} else {
107+
// Patch/minor: Independent versioning using package's own baseline
108+
newVersion = getNextVersion(data.oldVersion, bumpType);
109+
}
87110

88111
updates.push({
89112
name: pkgName,
@@ -226,11 +249,27 @@ function validateCalVer(version) {
226249

227250
// Main execution
228251
function main() {
229-
console.log('Starting CalVer enforcement...\n');
230-
231-
// Read current versions (after changesets has run)
252+
// Get versions: oldVersion from git baseline, pkg.version from current state
232253
const versions = readPackageVersions();
233254

255+
// Skip CalVer enforcement if no CalVer packages were bumped by changesets
256+
// This prevents semver-only releases (CLI, mini-oxygen) from touching CalVer packages
257+
let hasCalVerChanges = false;
258+
for (const [pkgName, data] of Object.entries(versions)) {
259+
if (data.pkg.version !== data.oldVersion) {
260+
hasCalVerChanges = true;
261+
break;
262+
}
263+
}
264+
265+
if (!hasCalVerChanges) {
266+
console.log('No CalVer package changes detected. Skipping CalVer enforcement.');
267+
console.log('This is a semver-only release.');
268+
return;
269+
}
270+
271+
console.log('Starting CalVer enforcement...\n');
272+
234273
// Calculate new CalVer versions
235274
const updates = calculateNewVersions(versions);
236275

.github/workflows/test-calver.yml

Lines changed: 43 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -226,35 +226,56 @@ jobs:
226226
echo "::group::Test 5 - Latest Branch Detection"
227227
echo "Expected: Correctly detect current and next release branches"
228228
echo ""
229-
229+
230+
# Calculate expected branches from actual hydrogen version
231+
# This makes the test work regardless of which quarter we're in
232+
HYDROGEN_VERSION=$(node -p "require('./packages/hydrogen/package.json').version")
233+
CURRENT_YEAR=$(echo $HYDROGEN_VERSION | cut -d. -f1)
234+
CURRENT_MAJOR=$(echo $HYDROGEN_VERSION | cut -d. -f2)
235+
CURRENT_BRANCH="${CURRENT_YEAR}-$(printf '%02d' $CURRENT_MAJOR)"
236+
237+
# Calculate next quarter
238+
case $CURRENT_MAJOR in
239+
1) NEXT_QUARTER=4 ;;
240+
4) NEXT_QUARTER=7 ;;
241+
7) NEXT_QUARTER=10 ;;
242+
10) NEXT_QUARTER=1; NEXT_YEAR=$((CURRENT_YEAR + 1)) ;;
243+
esac
244+
NEXT_BRANCH="${NEXT_YEAR:-$CURRENT_YEAR}-$(printf '%02d' $NEXT_QUARTER)"
245+
246+
echo "Current hydrogen version: $HYDROGEN_VERSION"
247+
echo "Current branch (calculated): $CURRENT_BRANCH"
248+
echo "Next branch (calculated): $NEXT_BRANCH"
249+
echo ""
250+
230251
# First check if there are already major changesets in the repo
231252
HAS_EXISTING_MAJOR=$(node .changeset/calver-shared.js has-major-changesets)
232253
echo "Repository has existing major changesets: $HAS_EXISTING_MAJOR"
233254
echo ""
234-
255+
235256
# Store and temporarily remove existing changesets to test clean state
236257
if [ "$HAS_EXISTING_MAJOR" = "true" ]; then
237258
echo "Temporarily moving existing changesets for isolated testing..."
238259
mkdir -p .changeset-backup
239260
mv .changeset/*.md .changeset-backup/ 2>/dev/null || true
240261
fi
241-
262+
242263
# Test without major changesets
243264
echo "Testing branch detection without major changesets:"
244-
BRANCH=$(node .changeset/get-calver-version-branch.js)
265+
BRANCH=$(CI=true node .changeset/get-calver-version-branch.js)
245266
echo "Current branch detected: $BRANCH"
246-
if [ "$BRANCH" = "2025-05" ]; then
247-
echo "✅ Correctly detected current branch (2025-05)"
267+
if [ "$BRANCH" = "$CURRENT_BRANCH" ]; then
268+
echo "✅ Correctly detected current branch ($CURRENT_BRANCH)"
248269
else
249-
echo "❌ Branch detection failed, expected 2025-05, got $BRANCH"
270+
echo "❌ Branch detection failed, expected $CURRENT_BRANCH, got $BRANCH"
250271
# Restore changesets before failing
251272
if [ "$HAS_EXISTING_MAJOR" = "true" ]; then
252273
mv .changeset-backup/*.md .changeset/ 2>/dev/null || true
253274
rmdir .changeset-backup 2>/dev/null || true
254275
fi
255276
exit 1
256277
fi
257-
278+
258279
# Test with major changeset
259280
echo ""
260281
echo "Testing branch detection with major changeset:"
@@ -264,13 +285,13 @@ jobs:
264285
---
265286
Test major for branch detection
266287
EOF
267-
268-
BRANCH=$(node .changeset/get-calver-version-branch.js)
288+
289+
BRANCH=$(CI=true node .changeset/get-calver-version-branch.js)
269290
echo "Next branch detected: $BRANCH"
270-
if [ "$BRANCH" = "2025-07" ]; then
271-
echo "✅ Correctly detected next quarter branch (2025-07)"
291+
if [ "$BRANCH" = "$NEXT_BRANCH" ]; then
292+
echo "✅ Correctly detected next quarter branch ($NEXT_BRANCH)"
272293
else
273-
echo "❌ Branch detection failed, expected 2025-07, got $BRANCH"
294+
echo "❌ Branch detection failed, expected $NEXT_BRANCH, got $BRANCH"
274295
# Restore changesets before failing
275296
rm .changeset/test-major-branch.md 2>/dev/null || true
276297
if [ "$HAS_EXISTING_MAJOR" = "true" ]; then
@@ -279,17 +300,17 @@ jobs:
279300
fi
280301
exit 1
281302
fi
282-
303+
283304
rm .changeset/test-major-branch.md
284-
305+
285306
# Restore original changesets
286307
if [ "$HAS_EXISTING_MAJOR" = "true" ]; then
287308
echo ""
288309
echo "Restoring original changesets..."
289310
mv .changeset-backup/*.md .changeset/ 2>/dev/null || true
290311
rmdir .changeset-backup 2>/dev/null || true
291312
fi
292-
313+
293314
echo "::endgroup::"
294315
295316
- name: Test 6 - Shared Utilities CLI
@@ -430,8 +451,11 @@ jobs:
430451
echo "✅ Tests validate that:"
431452
echo " 1. CI script has valid syntax for Node.js execution"
432453
echo " 2. sed commands work on Linux (GNU sed)"
433-
echo " 3. Patch bumps increment the third segment (2025.5.0 → 2025.5.1)"
434-
echo " 4. Major bumps align to quarters (2025.5.0 → 2025.7.0 for Q3)"
435-
echo " 5. CalVer packages use YYYY.M.P while semver packages use X.Y.Z"
454+
echo " 3. Patch bumps increment correctly with independent versioning"
455+
echo " 4. Major bumps align to quarters and sync all CalVer packages"
456+
echo " 5. Branch detection works for any quarter dynamically"
457+
echo " 6. Shared utilities CLI commands work correctly"
458+
echo " 7. CalVer packages use YYYY.M.P while semver packages use X.Y.Z"
459+
echo " 8. Full pipeline integration (semver-only and CalVer releases)"
436460
echo ""
437461
echo "All tests should produce explicit, verifiable output above."

docs/CALVER.md

Lines changed: 37 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -155,8 +155,11 @@ node scripts/get-latest-branch.js // → "2025-07"
155155

156156
#### 3. CalVer Enforcement
157157
- **`enforce-calver-ci.js`** - Production CI script (runs in GitHub Actions)
158-
- Uses git baseline to read original versions (before changesets corruption)
159-
- Fetches from `HEAD~1` or `origin/main` to avoid using invalid semver versions
158+
- Skips execution when only semver packages have changesets (guard check)
159+
- Fetches individual git baselines for each CalVer package
160+
- **Independent patches**: Each CalVer package can have different patch versions
161+
- **Synced majors**: All CalVer packages advance to same quarter for major bumps
162+
- Prevents changesets corruption (e.g., 2026.0.0 → 2025.10.0)
160163
- **`enforce-calver-local.js`** - Local testing with dry-run support
161164

162165
### CI/CD Integration
@@ -211,9 +214,13 @@ The release workflow now operates with zero manual intervention:
211214

212215
### CalVer Packages (YYYY.M.P format)
213216
- `@shopify/hydrogen`
214-
- `@shopify/hydrogen-react`
217+
- `@shopify/hydrogen-react`
215218
- `skeleton` (template)
216219

220+
**Versioning behavior**:
221+
- **Patches/minors**: Independent versioning allowed (e.g., [email protected], [email protected])
222+
- **Majors**: Always synchronized to same quarter (e.g., all at 2025.10.0)
223+
217224
### Semver Packages (X.Y.Z format)
218225
- `@shopify/cli-hydrogen`
219226
- `@shopify/mini-oxygen`
@@ -455,10 +462,10 @@ Scenario B: With Bypass
455462

456463
## Safety Features
457464

458-
1. **Hydrogen as Source of Truth**: All CalVer packages use hydrogen's version as baseline
459-
- Prevents version inconsistencies across packages
460-
- Resolves "Invalid quarter" errors caused by package drift
461-
- Ensures consistent CalVer enforcement across the monorepo
465+
1. **Independent Patches, Synced Majors**: CalVer packages can have independent patch versions
466+
- **Patches/minors**: Each package uses its own git baseline for independent versioning
467+
- **Majors**: All packages use hydrogen's baseline to sync to same quarter
468+
- Enables independent bug fixes while maintaining API compatibility across majors
462469
- **Git Baseline Protection**: Reads original versions from git history to avoid changeset corruption
463470
2. **Version Regression Protection**: Prevents versions from going backwards
464471
3. **Quarter Alignment Validation**: Ensures majors use quarters (1,4,7,10)
@@ -548,6 +555,29 @@ node .changeset/calver-shared.js has-calver-changesets
548555
# - CalVer-only → true → "[ci] release 2025.5.1"
549556
```
550557

558+
### Issue: Semver-only releases incorrectly bump CalVer packages
559+
560+
**Root Cause**: `enforce-calver-ci.js` ran unconditionally for all CalVer packages, even when only semver packages had changesets
561+
562+
**Impact**: Phantom version bumps and CHANGELOG entries for hydrogen/hydrogen-react/skeleton with no actual changes
563+
564+
**Solution**: Guard check added to skip CalVer enforcement when no CalVer changesets exist
565+
566+
```bash
567+
# Create test CLI changeset
568+
echo -e "---\n'@shopify/cli-hydrogen': patch\n---\nTest" > .changeset/test-cli.md
569+
570+
# Should return false (no CalVer changesets)
571+
node .changeset/calver-shared.js has-calver-changesets
572+
573+
# Should skip enforcement
574+
node .changeset/enforce-calver-ci.js
575+
# Output: "No CalVer changesets detected. Skipping CalVer enforcement."
576+
577+
# Cleanup
578+
rm .changeset/test-cli.md
579+
```
580+
551581
## Migration Notes
552582

553583
### For Maintainers

0 commit comments

Comments
 (0)