Skip to content
5 changes: 5 additions & 0 deletions .changeset/dummy-cli-hydrogen-test.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@shopify/cli-hydrogen": patch
---

Test changeset to validate independent patches and synced majors logic for SemVer packages
77 changes: 58 additions & 19 deletions .changeset/enforce-calver-ci.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,38 +25,53 @@ const {
} = require('./calver-shared.js');

// Read all package.json files for CalVer packages
// Uses the original version from git as baseline (before changesets corruption)
// Fetches individual git baselines for independent patch versioning
// Hydrogen baseline used for major sync across all CalVer packages
function readPackageVersions() {
const versions = {};

// Get the original version from git (before changesets ran)
// This prevents using corrupted versions like 2026.0.0 that changesets might generate
let sourceOfTruthVersion;

// Get hydrogen's git baseline (used for major bump synchronization)
let hydrogenBaselineVersion;
try {
// Try to get the version from the base branch (before changesets modified it)
const gitVersion = execSync('git show HEAD~1:packages/hydrogen/package.json 2>/dev/null || git show origin/main:packages/hydrogen/package.json', {
encoding: 'utf-8',
stdio: ['pipe', 'pipe', 'ignore']
});
const versionMatch = gitVersion.match(/"version":\s*"([^"]+)"/);
if (versionMatch) {
sourceOfTruthVersion = versionMatch[1];
console.log(`Using original version from git: ${sourceOfTruthVersion}`);
hydrogenBaselineVersion = versionMatch[1];
console.log(`Using hydrogen baseline from git: ${hydrogenBaselineVersion}`);
}
} catch (error) {
// Fallback to current version if git command fails
const hydrogenPath = getPackagePath('@shopify/hydrogen');
const hydrogenPkg = readPackage(hydrogenPath);
sourceOfTruthVersion = hydrogenPkg.version;
console.log(`Using current version as fallback: ${sourceOfTruthVersion}`);
hydrogenBaselineVersion = hydrogenPkg.version;
console.log(`Using hydrogen current version as fallback: ${hydrogenBaselineVersion}`);
}


// Get each package's individual baseline for independent patch versioning
for (const pkgName of CALVER_PACKAGES) {
const pkgPath = getPackagePath(pkgName);
const pkg = readPackage(pkgPath);

// Fetch this package's own git baseline
let packageOwnBaseline;
try {
const gitPath = pkgPath.replace(process.cwd() + '/', '');
const gitVersion = execSync(`git show HEAD~1:${gitPath} 2>/dev/null || git show origin/main:${gitPath}`, {
encoding: 'utf-8',
stdio: ['pipe', 'pipe', 'ignore']
});
const versionMatch = gitVersion.match(/"version":\s*"([^"]+)"/);
packageOwnBaseline = versionMatch ? versionMatch[1] : hydrogenBaselineVersion;
} catch (error) {
packageOwnBaseline = hydrogenBaselineVersion;
}

versions[pkgName] = {
path: pkgPath,
oldVersion: sourceOfTruthVersion, // Use original version as baseline for all packages
oldVersion: packageOwnBaseline,
hydrogenBaseline: hydrogenBaselineVersion,
pkg,
};
}
Expand All @@ -81,9 +96,17 @@ function calculateNewVersions(versions) {
for (const [pkgName, data] of Object.entries(versions)) {
const bumpType = getBumpType(data.oldVersion, data.pkg.version);

// For major bumps, ensure all CalVer packages advance to same quarter
const effectiveBumpType = hasAnyMajor ? 'major' : bumpType;
const newVersion = getNextVersion(data.oldVersion, effectiveBumpType);
let newVersion;
if (hasAnyMajor) {
// Major: Sync all CalVer packages to same quarter using hydrogen's baseline
newVersion = getNextVersion(data.hydrogenBaseline, 'major');
} else if (data.pkg.version === data.oldVersion) {
// No change: Keep current version (don't bump unchanged packages)
newVersion = data.pkg.version;
} else {
// Patch/minor: Independent versioning using package's own baseline
newVersion = getNextVersion(data.oldVersion, bumpType);
}

updates.push({
name: pkgName,
Expand Down Expand Up @@ -226,11 +249,27 @@ function validateCalVer(version) {

// Main execution
function main() {
console.log('Starting CalVer enforcement...\n');

// Read current versions (after changesets has run)
// Get versions: oldVersion from git baseline, pkg.version from current state
const versions = readPackageVersions();

// Skip CalVer enforcement if no CalVer packages were bumped by changesets
// This prevents semver-only releases (CLI, mini-oxygen) from touching CalVer packages
let hasCalVerChanges = false;
for (const [pkgName, data] of Object.entries(versions)) {
if (data.pkg.version !== data.oldVersion) {
hasCalVerChanges = true;
break;
}
}

if (!hasCalVerChanges) {
console.log('No CalVer package changes detected. Skipping CalVer enforcement.');
console.log('This is a semver-only release.');
return;
}

console.log('Starting CalVer enforcement...\n');

// Calculate new CalVer versions
const updates = calculateNewVersions(versions);

Expand Down
62 changes: 43 additions & 19 deletions .github/workflows/test-calver.yml
Original file line number Diff line number Diff line change
Expand Up @@ -226,35 +226,56 @@ jobs:
echo "::group::Test 5 - Latest Branch Detection"
echo "Expected: Correctly detect current and next release branches"
echo ""


# Calculate expected branches from actual hydrogen version
# This makes the test work regardless of which quarter we're in
HYDROGEN_VERSION=$(node -p "require('./packages/hydrogen/package.json').version")
CURRENT_YEAR=$(echo $HYDROGEN_VERSION | cut -d. -f1)
CURRENT_MAJOR=$(echo $HYDROGEN_VERSION | cut -d. -f2)
CURRENT_BRANCH="${CURRENT_YEAR}-$(printf '%02d' $CURRENT_MAJOR)"

# Calculate next quarter
case $CURRENT_MAJOR in
1) NEXT_QUARTER=4 ;;
4) NEXT_QUARTER=7 ;;
7) NEXT_QUARTER=10 ;;
10) NEXT_QUARTER=1; NEXT_YEAR=$((CURRENT_YEAR + 1)) ;;
esac
NEXT_BRANCH="${NEXT_YEAR:-$CURRENT_YEAR}-$(printf '%02d' $NEXT_QUARTER)"

echo "Current hydrogen version: $HYDROGEN_VERSION"
echo "Current branch (calculated): $CURRENT_BRANCH"
echo "Next branch (calculated): $NEXT_BRANCH"
echo ""

# First check if there are already major changesets in the repo
HAS_EXISTING_MAJOR=$(node .changeset/calver-shared.js has-major-changesets)
echo "Repository has existing major changesets: $HAS_EXISTING_MAJOR"
echo ""

# Store and temporarily remove existing changesets to test clean state
if [ "$HAS_EXISTING_MAJOR" = "true" ]; then
echo "Temporarily moving existing changesets for isolated testing..."
mkdir -p .changeset-backup
mv .changeset/*.md .changeset-backup/ 2>/dev/null || true
fi

# Test without major changesets
echo "Testing branch detection without major changesets:"
BRANCH=$(node .changeset/get-calver-version-branch.js)
BRANCH=$(CI=true node .changeset/get-calver-version-branch.js)
echo "Current branch detected: $BRANCH"
if [ "$BRANCH" = "2025-05" ]; then
echo "✅ Correctly detected current branch (2025-05)"
if [ "$BRANCH" = "$CURRENT_BRANCH" ]; then
echo "✅ Correctly detected current branch ($CURRENT_BRANCH)"
else
echo "❌ Branch detection failed, expected 2025-05, got $BRANCH"
echo "❌ Branch detection failed, expected $CURRENT_BRANCH, got $BRANCH"
# Restore changesets before failing
if [ "$HAS_EXISTING_MAJOR" = "true" ]; then
mv .changeset-backup/*.md .changeset/ 2>/dev/null || true
rmdir .changeset-backup 2>/dev/null || true
fi
exit 1
fi

# Test with major changeset
echo ""
echo "Testing branch detection with major changeset:"
Expand All @@ -264,13 +285,13 @@ jobs:
---
Test major for branch detection
EOF
BRANCH=$(node .changeset/get-calver-version-branch.js)

BRANCH=$(CI=true node .changeset/get-calver-version-branch.js)
echo "Next branch detected: $BRANCH"
if [ "$BRANCH" = "2025-07" ]; then
echo "✅ Correctly detected next quarter branch (2025-07)"
if [ "$BRANCH" = "$NEXT_BRANCH" ]; then
echo "✅ Correctly detected next quarter branch ($NEXT_BRANCH)"
else
echo "❌ Branch detection failed, expected 2025-07, got $BRANCH"
echo "❌ Branch detection failed, expected $NEXT_BRANCH, got $BRANCH"
# Restore changesets before failing
rm .changeset/test-major-branch.md 2>/dev/null || true
if [ "$HAS_EXISTING_MAJOR" = "true" ]; then
Expand All @@ -279,17 +300,17 @@ jobs:
fi
exit 1
fi

rm .changeset/test-major-branch.md

# Restore original changesets
if [ "$HAS_EXISTING_MAJOR" = "true" ]; then
echo ""
echo "Restoring original changesets..."
mv .changeset-backup/*.md .changeset/ 2>/dev/null || true
rmdir .changeset-backup 2>/dev/null || true
fi

echo "::endgroup::"

- name: Test 6 - Shared Utilities CLI
Expand Down Expand Up @@ -430,8 +451,11 @@ jobs:
echo "✅ Tests validate that:"
echo " 1. CI script has valid syntax for Node.js execution"
echo " 2. sed commands work on Linux (GNU sed)"
echo " 3. Patch bumps increment the third segment (2025.5.0 → 2025.5.1)"
echo " 4. Major bumps align to quarters (2025.5.0 → 2025.7.0 for Q3)"
echo " 5. CalVer packages use YYYY.M.P while semver packages use X.Y.Z"
echo " 3. Patch bumps increment correctly with independent versioning"
echo " 4. Major bumps align to quarters and sync all CalVer packages"
echo " 5. Branch detection works for any quarter dynamically"
echo " 6. Shared utilities CLI commands work correctly"
echo " 7. CalVer packages use YYYY.M.P while semver packages use X.Y.Z"
echo " 8. Full pipeline integration (semver-only and CalVer releases)"
echo ""
echo "All tests should produce explicit, verifiable output above."
44 changes: 37 additions & 7 deletions docs/CALVER.md
Original file line number Diff line number Diff line change
Expand Up @@ -155,8 +155,11 @@ node scripts/get-latest-branch.js // → "2025-07"

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

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

### CalVer Packages (YYYY.M.P format)
- `@shopify/hydrogen`
- `@shopify/hydrogen-react`
- `@shopify/hydrogen-react`
- `skeleton` (template)

**Versioning behavior**:
- **Patches/minors**: Independent versioning allowed (e.g., hydrogen@2025.7.2, hydrogen-react@2025.7.0)
- **Majors**: Always synchronized to same quarter (e.g., all at 2025.10.0)

### Semver Packages (X.Y.Z format)
- `@shopify/cli-hydrogen`
- `@shopify/mini-oxygen`
Expand Down Expand Up @@ -455,10 +462,10 @@ Scenario B: With Bypass

## Safety Features

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

### Issue: Semver-only releases incorrectly bump CalVer packages

**Root Cause**: `enforce-calver-ci.js` ran unconditionally for all CalVer packages, even when only semver packages had changesets

**Impact**: Phantom version bumps and CHANGELOG entries for hydrogen/hydrogen-react/skeleton with no actual changes

**Solution**: Guard check added to skip CalVer enforcement when no CalVer changesets exist

```bash
# Create test CLI changeset
echo -e "---\n'@shopify/cli-hydrogen': patch\n---\nTest" > .changeset/test-cli.md

# Should return false (no CalVer changesets)
node .changeset/calver-shared.js has-calver-changesets

# Should skip enforcement
node .changeset/enforce-calver-ci.js
# Output: "No CalVer changesets detected. Skipping CalVer enforcement."

# Cleanup
rm .changeset/test-cli.md
```

## Migration Notes

### For Maintainers
Expand Down