From a813552e504adeee94e93d43af21db4a26f34943 Mon Sep 17 00:00:00 2001 From: Kara Daviduik Date: Fri, 6 Feb 2026 14:11:10 -0500 Subject: [PATCH] fix(calver): skeleton major bumps no longer cascade to hydrogen packages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The CalVer scripts treated `skeleton` identically to `@shopify/hydrogen` and `@shopify/hydrogen-react` when detecting major bumps. A skeleton-only major changeset (eg: API version update) would incorrectly force all CalVer packages to advance to the next quarter. Introduces `CALVER_SYNC_PACKAGES` to separate "which packages use CalVer" from "which packages' major bumps trigger cross-package synchronization". Only hydrogen and hydrogen-react majors trigger quarter advancement — skeleton major bumps are now treated as independent patch-level changes. Four consumption points fixed (2 directly, 2 transitively): - `detectBumpType()` gates `hasMajor` to sync-eligible packages - `hasMajorChangesets()` iterates sync packages only - `enforce-calver-ci.js` inherits fix via detectBumpType() temp file - `enforce-calver-local.js` inherits fix via hasMajorChangesets() --- .changeset/calver-bump-type.test.js | 273 +++++++++++++++++++++++++- .changeset/calver-shared.js | 67 ++++--- .changeset/detect-calver-bump-type.js | 13 +- 3 files changed, 318 insertions(+), 35 deletions(-) diff --git a/.changeset/calver-bump-type.test.js b/.changeset/calver-bump-type.test.js index dc668a2ae9..0123980a5c 100644 --- a/.changeset/calver-bump-type.test.js +++ b/.changeset/calver-bump-type.test.js @@ -14,7 +14,11 @@ const fs = require('fs'); const path = require('path'); -const {getNextVersion, parseVersion} = require('./calver-shared.js'); +const { + getNextVersion, + parseVersion, + hasMajorChangesets, +} = require('./calver-shared.js'); const { detectBumpType, writeBumpType, @@ -216,6 +220,90 @@ Test non-CalVer package console.log('\nTest 7: No changesets should return null'); result = detectBumpType(); assertEqual(result, null, 'No changesets returns null'); + + cleanupTestChangesets(); + + console.log( + '\nTest 8: skeleton major alone should return "patch" (not "major")', + ); + createTestChangeset( + 'skeleton-major', + `--- +'skeleton': major +--- + +Skeleton major bump +`, + ); + result = detectBumpType(); + assertEqual(result, 'patch', 'skeleton major returns patch'); + + cleanupTestChangesets(); + + console.log( + '\nTest 9: skeleton major + hydrogen patch should return "patch"', + ); + createTestChangeset( + 'skeleton-major', + `--- +'skeleton': major +--- + +Skeleton major bump +`, + ); + createTestChangeset( + 'hydrogen-patch', + `--- +'@shopify/hydrogen': patch +--- + +Hydrogen patch bump +`, + ); + result = detectBumpType(); + assertEqual(result, 'patch', 'skeleton major + hydrogen patch returns patch'); + + cleanupTestChangesets(); + + console.log( + '\nTest 10: skeleton major + hydrogen major should return "major"', + ); + createTestChangeset( + 'skeleton-major', + `--- +'skeleton': major +--- + +Skeleton major bump +`, + ); + createTestChangeset( + 'hydrogen-major', + `--- +'@shopify/hydrogen': major +--- + +Hydrogen major bump +`, + ); + result = detectBumpType(); + assertEqual(result, 'major', 'skeleton major + hydrogen major returns major'); + + cleanupTestChangesets(); + + console.log('\nTest 11: hydrogen-react major alone should return "major"'); + createTestChangeset( + 'react-major', + `--- +'@shopify/hydrogen-react': major +--- + +React major bump +`, + ); + result = detectBumpType(); + assertEqual(result, 'major', 'hydrogen-react major returns major'); } function testVersionCalculation() { @@ -257,7 +345,11 @@ Minor feature addition ); const detectedBumpType = detectBumpType(); - assertEqual(detectedBumpType, 'patch', 'Detected bump type is patch (not major)'); + assertEqual( + detectedBumpType, + 'patch', + 'Detected bump type is patch (not major)', + ); const currentVersion = '2025.7.2'; const newVersion = getNextVersion(currentVersion, detectedBumpType); @@ -275,6 +367,165 @@ Minor feature addition cleanupTestChangesets(); } +function testHasMajorChangesets() { + console.log('\nšŸ” Testing hasMajorChangesets()...\n'); + + cleanupTestChangesets(); + + console.log('Test 1: skeleton major alone should return false'); + createTestChangeset( + 'skeleton-major', + `--- +'skeleton': major +--- + +Skeleton major bump +`, + ); + let result = hasMajorChangesets(); + assertEqual(result, false, 'skeleton major returns false'); + + cleanupTestChangesets(); + + console.log('\nTest 2: hydrogen major alone should return true'); + createTestChangeset( + 'hydrogen-major', + `--- +'@shopify/hydrogen': major +--- + +Hydrogen major bump +`, + ); + result = hasMajorChangesets(); + assertEqual(result, true, 'hydrogen major returns true'); + + cleanupTestChangesets(); + + console.log('\nTest 3: hydrogen-react major alone should return true'); + createTestChangeset( + 'react-major', + `--- +'@shopify/hydrogen-react': major +--- + +React major bump +`, + ); + result = hasMajorChangesets(); + assertEqual(result, true, 'hydrogen-react major returns true'); + + cleanupTestChangesets(); + + console.log('\nTest 4: skeleton major + hydrogen major should return true'); + createTestChangeset( + 'skeleton-major', + `--- +'skeleton': major +--- + +Skeleton major bump +`, + ); + createTestChangeset( + 'hydrogen-major', + `--- +'@shopify/hydrogen': major +--- + +Hydrogen major bump +`, + ); + result = hasMajorChangesets(); + assertEqual(result, true, 'skeleton major + hydrogen major returns true'); + + cleanupTestChangesets(); + + console.log('\nTest 5: no changesets should return false'); + result = hasMajorChangesets(); + assertEqual(result, false, 'no changesets returns false'); +} + +function testSkeletonMajorBugScenario() { + console.log('\nšŸ› Testing PR #3451 skeleton-major bug scenario...\n'); + + console.log( + 'Scenario: skeleton:major + hydrogen:patch should NOT bump hydrogen to next quarter', + ); + + cleanupTestChangesets(); + createTestChangeset( + 'api-version', + `--- +'skeleton': major +--- + +Update to new API version +`, + ); + createTestChangeset( + 'hydrogen-fix', + `--- +'@shopify/hydrogen': patch +--- + +Fix a bug in hydrogen +`, + ); + + const detectedBumpType = detectBumpType(); + assertEqual( + detectedBumpType, + 'patch', + 'Detected bump type is patch (not major)', + ); + + const hasMajor = hasMajorChangesets(); + assertEqual(hasMajor, false, 'hasMajorChangesets() returns false'); + + const currentVersion = '2025.10.3'; + const newVersion = getNextVersion(currentVersion, detectedBumpType); + assertEqual( + newVersion, + '2025.10.4', + `hydrogen stays in quarter: ${currentVersion} → ${newVersion}`, + ); + + assert( + newVersion !== '2026.1.0', + 'hydrogen is NOT incorrectly advanced to 2026.1.0', + ); + + cleanupTestChangesets(); + + console.log( + '\nScenario 2: Single file with skeleton:major + hydrogen:patch in same frontmatter', + ); + + createTestChangeset( + 'combined-api-version', + `--- +'skeleton': major +'@shopify/hydrogen': patch +--- + +API version update with skeleton major and hydrogen patch in one changeset +`, + ); + + const combinedBumpType = detectBumpType(); + assertEqual(combinedBumpType, 'patch', 'Single-file combined returns patch'); + + const combinedHasMajor = hasMajorChangesets(); + assertEqual( + combinedHasMajor, + false, + 'Single-file combined hasMajorChangesets is false', + ); + + cleanupTestChangesets(); +} + function testWriteAndCleanup() { console.log('\nšŸ“ Testing file write and cleanup...\n'); @@ -294,9 +545,13 @@ function testWriteAndCleanup() { } function main() { - console.log('═══════════════════════════════════════════════════════════════'); + console.log( + '═══════════════════════════════════════════════════════════════', + ); console.log(' CalVer Bump Type Detection Tests'); - console.log('═══════════════════════════════════════════════════════════════'); + console.log( + '═══════════════════════════════════════════════════════════════', + ); backupExistingChangesets(); @@ -304,15 +559,21 @@ function main() { testDetectBumpType(); testVersionCalculation(); testBugScenario(); + testHasMajorChangesets(); + testSkeletonMajorBugScenario(); testWriteAndCleanup(); } finally { cleanupTestChangesets(); restoreBackedUpChangesets(); } - console.log('\n═══════════════════════════════════════════════════════════════'); + console.log( + '\n═══════════════════════════════════════════════════════════════', + ); console.log(` Results: ${testsPassed} passed, ${testsFailed} failed`); - console.log('═══════════════════════════════════════════════════════════════\n'); + console.log( + '═══════════════════════════════════════════════════════════════\n', + ); if (testsFailed > 0) { process.exit(1); diff --git a/.changeset/calver-shared.js b/.changeset/calver-shared.js index aeb3fab1d0..b0467085b6 100644 --- a/.changeset/calver-shared.js +++ b/.changeset/calver-shared.js @@ -16,6 +16,10 @@ const CALVER_PACKAGES = [ 'skeleton', ]; +// Packages whose major bumps trigger cross-package CalVer synchronization. +// skeleton is excluded because its major bumps are independent (template-only). +const CALVER_SYNC_PACKAGES = ['@shopify/hydrogen', '@shopify/hydrogen-react']; + // Parse version string into components function parseVersion(version) { const match = version.match(/^(\d{4})\.(\d+)\.(\d+)(?:\.(\d+))?/); @@ -88,20 +92,23 @@ function versionToBranchName(year, major) { // Check if there are major changesets for CalVer packages function hasMajorChangesets() { const changesetDir = path.join(process.cwd(), '.changeset'); - + try { const files = fs.readdirSync(changesetDir); - + for (const file of files) { if (!file.endsWith('.md') || file === 'README.md') continue; - + const filePath = path.join(changesetDir, file); const content = fs.readFileSync(filePath, 'utf-8'); - - // Check for major bumps in any CalVer package - for (const pkg of CALVER_PACKAGES) { + + // Only sync-eligible packages trigger cross-package major bumps + for (const pkg of CALVER_SYNC_PACKAGES) { // Check for both single and double quotes (changesets can use either) - if (content.includes(`"${pkg}": major`) || content.includes(`'${pkg}': major`)) { + if ( + content.includes(`"${pkg}": major`) || + content.includes(`'${pkg}': major`) + ) { return true; } } @@ -110,29 +117,34 @@ function hasMajorChangesets() { // If we can't read changesets, assume no major bump return false; } - + return false; } // Check if there are any changesets for CalVer packages (any bump type) function hasCalVerChangesets() { const changesetDir = path.join(process.cwd(), '.changeset'); - + try { const files = fs.readdirSync(changesetDir); - + for (const file of files) { if (!file.endsWith('.md') || file === 'README.md') continue; - + const filePath = path.join(changesetDir, file); const content = fs.readFileSync(filePath, 'utf-8'); - + // Check for any bumps (patch, minor, major) in CalVer packages for (const pkg of CALVER_PACKAGES) { // Check for both single and double quotes (changesets can use either) - if (content.includes(`"${pkg}": patch`) || content.includes(`'${pkg}': patch`) || - content.includes(`"${pkg}": minor`) || content.includes(`'${pkg}': minor`) || - content.includes(`"${pkg}": major`) || content.includes(`'${pkg}': major`)) { + if ( + content.includes(`"${pkg}": patch`) || + content.includes(`'${pkg}': patch`) || + content.includes(`"${pkg}": minor`) || + content.includes(`'${pkg}': minor`) || + content.includes(`"${pkg}": major`) || + content.includes(`'${pkg}': major`) + ) { return true; } } @@ -141,7 +153,7 @@ function hasCalVerChangesets() { // If we can't read changesets, assume no CalVer changesets return false; } - + return false; } @@ -189,7 +201,7 @@ function getAllPackagePaths() { .map((dir) => path.join(process.cwd(), 'packages', dir, 'package.json')) .concat(path.join(process.cwd(), 'templates/skeleton/package.json')) .filter((p) => fs.existsSync(p)); - + return packages; } @@ -231,7 +243,7 @@ function updateInternalDependencies(updates, dryRun = false) { // Update CHANGELOG headers after version changes function updateChangelogs(updates, dryRun = false) { const updatedChangelogs = []; - + Object.entries(updates).forEach(([pkgName, update]) => { const pkgDir = path.dirname(getPackagePath(pkgName)); const changelogPath = path.join(pkgDir, 'CHANGELOG.md'); @@ -239,12 +251,16 @@ function updateChangelogs(updates, dryRun = false) { if (fs.existsSync(changelogPath)) { let changelog = fs.readFileSync(changelogPath, 'utf-8'); // Replace the version header that changesets just created with CalVer version - const changesetVersionEscaped = (update.changesetVersion || update.changeset || update.from).replace(/\./g, '\\.'); + const changesetVersionEscaped = ( + update.changesetVersion || + update.changeset || + update.from + ).replace(/\./g, '\\.'); const newChangelog = changelog.replace( new RegExp(`^## ${changesetVersionEscaped}$`, 'm'), `## ${update.to}`, ); - + if (newChangelog !== changelog) { if (!dryRun) { fs.writeFileSync(changelogPath, newChangelog); @@ -253,14 +269,14 @@ function updateChangelogs(updates, dryRun = false) { } } }); - + return updatedChangelogs; } // CLI interface for bash scripts if (require.main === module) { - const [,, command, ...args] = process.argv; - + const [, , command, ...args] = process.argv; + try { switch (command) { case 'get-next': @@ -297,6 +313,7 @@ Commands: module.exports = { QUARTERS, CALVER_PACKAGES, + CALVER_SYNC_PACKAGES, parseVersion, getNextVersion, getBumpType, @@ -310,5 +327,5 @@ module.exports = { validateUpdates, getAllPackagePaths, updateInternalDependencies, - updateChangelogs -}; \ No newline at end of file + updateChangelogs, +}; diff --git a/.changeset/detect-calver-bump-type.js b/.changeset/detect-calver-bump-type.js index d3703b5e26..7464f2c973 100644 --- a/.changeset/detect-calver-bump-type.js +++ b/.changeset/detect-calver-bump-type.js @@ -17,7 +17,7 @@ const fs = require('fs'); const path = require('path'); -const {CALVER_PACKAGES} = require('./calver-shared.js'); +const {CALVER_PACKAGES, CALVER_SYNC_PACKAGES} = require('./calver-shared.js'); const CALVER_BUMP_FILE = path.join(__dirname, '.calver-bump-type'); @@ -48,16 +48,21 @@ function detectBumpType() { const content = fs.readFileSync(filePath, 'utf-8'); for (const pkg of CALVER_PACKAGES) { + const canTriggerMajorSync = CALVER_SYNC_PACKAGES.includes(pkg); + if ( - content.includes(`"${pkg}": major`) || - content.includes(`'${pkg}': major`) + canTriggerMajorSync && + (content.includes(`"${pkg}": major`) || + content.includes(`'${pkg}': major`)) ) { hasMajor = true; } else if ( content.includes(`"${pkg}": minor`) || content.includes(`'${pkg}': minor`) || content.includes(`"${pkg}": patch`) || - content.includes(`'${pkg}': patch`) + content.includes(`'${pkg}': patch`) || + content.includes(`"${pkg}": major`) || + content.includes(`'${pkg}': major`) ) { hasMinorOrPatch = true; }