fix(release): keep CLI package versions in sync (#929)

* fix(release): keep CLI package versions in sync

Align the bundled CLI manifest with the release version and make future `npm version` runs update both package manifests and lockfiles. Fail tests and package publishing when those versions drift.

* fix(release): address version workflow review

* fix(release): harden version sync validation

* fix(release): enforce synchronized version bumps
This commit is contained in:
hatemadi15
2026-09-20 12:08:39 -04:00
committed by GitHub
parent 1f3a7f5f52
commit 26e8d67697
8 changed files with 274 additions and 19 deletions
+5 -2
View File
@@ -31,6 +31,9 @@ jobs:
registry-url: 'https://npm.pkg.github.com'
scope: '@davila7'
- name: Verify package versions
run: npm run check:version-sync
- name: Configure package for GitHub Packages
working-directory: ./cli-tool
run: |
@@ -61,7 +64,7 @@ jobs:
- name: Publish to GitHub Packages
working-directory: ./cli-tool
run: npm publish
run: npm publish --ignore-scripts=false
env:
NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -77,4 +80,4 @@ jobs:
echo "npm install -g @davila7/claude-code-templates --registry=https://npm.pkg.github.com"
echo ""
echo "Note: This workflow only publishes to GitHub Packages."
echo "Main npm registry publication is handled manually."
echo "Main npm registry publication is handled manually."
+27
View File
@@ -0,0 +1,27 @@
name: Package Version Sync
on:
pull_request:
push:
branches:
- main
permissions:
contents: read
jobs:
check:
name: Check package versions
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: '18'
- name: Run version-sync checks
run: npm test
+11 -12
View File
@@ -12,8 +12,9 @@ Node.js CLI tool for managing Claude Code components (agents, commands, MCPs, ho
# Development
npm install # Install dependencies
npm test # Run tests
npm version patch|minor|major # Bump version
npm publish # Publish to npm
npm version X.Y.Z --ignore-scripts=false # Sync, commit, and tag all package versions
npm run version:set -- X.Y.Z # Sync versions without creating a commit or tag
npm publish --ignore-scripts=false # Publish and run the trusted prepublish guard
# Component catalog
python scripts/generate_components_json.py # Update docs/components.json
@@ -227,23 +228,20 @@ python scripts/generate_components_json.py
# 2. Run tests
npm test
# 3. Check current npm version and align local version
# 3. Check current npm version, then create the synchronized version commit and tag
npm view claude-code-templates version # check latest on registry
# Edit package.json version to be one patch above the registry version
npm version X.Y.Z --ignore-scripts=false # X.Y.Z = one patch above the registry version
npm run check:version-sync
# 4. Commit version bump and push
git add package.json && git commit -m "chore: Bump version to X.Y.Z"
git push origin main
# 4. Push the version commit and tag created by npm version
git push origin main --follow-tags
# 5. Publish to npm (requires granular access token with "Bypass 2FA" enabled)
npm config set //registry.npmjs.org/:_authToken=YOUR_GRANULAR_TOKEN
npm publish
npm publish --ignore-scripts=false
npm config delete //registry.npmjs.org/:_authToken # always clean up after
# 6. Tag the release
git tag vX.Y.Z && git push origin vX.Y.Z
# 7. Deploy website (dashboard on Cloudflare Pages)
# 6. Deploy website (dashboard on Cloudflare Pages)
# Automatic on push to main (GitHub Actions). Manual: from dashboard/ run `npm run deploy`
```
@@ -251,6 +249,7 @@ git tag vX.Y.Z && git push origin vX.Y.Z
- Classic npm tokens were revoked Dec 2025. Use **granular access tokens** from [npmjs.com/settings/~/tokens](https://www.npmjs.com/settings/~/tokens)
- The token must have **Read and Write** permissions for `claude-code-templates` and **"Bypass 2FA"** enabled
- Always remove the token from npm config after publishing (`npm config delete`)
- The repository disables lifecycle scripts by default for security. Pass `--ignore-scripts=false` only to the trusted release commands shown above so their version and prepublish hooks can run.
- The local `package.json` version may drift from npm if published from CI — always check `npm view claude-code-templates version` first
- Never hardcode or commit tokens
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "claude-code-templates",
"version": "1.28.13",
"version": "1.29.6",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "claude-code-templates",
"version": "1.28.13",
"version": "1.29.6",
"license": "MIT",
"dependencies": {
"@clack/prompts": "^1.5.1",
+3 -2
View File
@@ -1,6 +1,6 @@
{
"name": "claude-code-templates",
"version": "1.29.4",
"version": "1.29.6",
"description": "CLI tool to setup Claude Code configurations with framework-specific commands, automation hooks and MCP Servers for your projects",
"main": "src/index.js",
"bin": {
@@ -17,7 +17,8 @@
"start": "node bin/create-claude-config.js",
"build:ui": "cd analytics-ui && npm install && npm run build",
"dev:ui": "cd analytics-ui && npm run dev",
"prepublishOnly": "npm run build:ui && npm test",
"check:version-sync": "node ../scripts/sync-package-versions.js --check",
"prepublishOnly": "npm run check:version-sync && npm run build:ui && npm test",
"test": "jest",
"test:watch": "jest --watch",
"test:coverage": "jest --coverage",
+6 -1
View File
@@ -9,7 +9,12 @@
},
"scripts": {
"build": "echo 'Build complete'",
"test": "echo 'No tests specified'",
"test": "npm run check:version-sync && npm run test:version-sync",
"check:version-sync": "node scripts/sync-package-versions.js --check",
"test:version-sync": "node --test scripts/sync-package-versions.test.js",
"prepublishOnly": "npm run check:version-sync",
"version": "node scripts/sync-package-versions.js && git add package.json package-lock.json cli-tool/package.json cli-tool/package-lock.json",
"version:set": "node scripts/sync-package-versions.js",
"agent:security-auditor": "security-auditor 'Security audit covering codebase, dependencies, and deployment configuration'",
"deploy": "./scripts/deploy.sh",
"deploy:dashboard": "./scripts/deploy.sh"
+92
View File
@@ -0,0 +1,92 @@
'use strict';
const fs = require('fs');
const path = require('path');
const repositoryRoot = path.resolve(__dirname, '..');
const cliArguments = process.argv.slice(2);
const checkOnly = cliArguments.includes('--check');
const requestedVersions = cliArguments.filter((argument) => argument !== '--check');
function fail(message) {
console.error(message);
process.exit(1);
}
if (requestedVersions.length > 1 || (checkOnly && requestedVersions.length > 0)) {
fail('Usage: sync-package-versions.js [--check | X.Y.Z]');
}
const requestedVersion = requestedVersions[0];
const semverPattern =
/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-(?:(?:[0-9A-Za-z-]*[A-Za-z-][0-9A-Za-z-]*)|0|[1-9]\d*)(?:\.(?:(?:[0-9A-Za-z-]*[A-Za-z-][0-9A-Za-z-]*)|0|[1-9]\d*))*)?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/;
if (requestedVersion && !semverPattern.test(requestedVersion)) {
fail(`Invalid semantic version: ${requestedVersion}`);
}
function readJson(relativePath) {
const absolutePath = path.join(repositoryRoot, relativePath);
return {
absolutePath,
data: JSON.parse(fs.readFileSync(absolutePath, 'utf8')),
relativePath,
};
}
const rootPackage = readJson('package.json');
const expectedVersion = requestedVersion || rootPackage.data.version;
const versionFiles = [
rootPackage,
readJson('package-lock.json'),
readJson('cli-tool/package.json'),
readJson('cli-tool/package-lock.json'),
];
const mismatches = [];
for (const file of versionFiles) {
const fields = [['version', file.data]];
if (file.relativePath.endsWith('package-lock.json')) {
const lockRoot = file.data.packages && file.data.packages[''];
if (!lockRoot) {
fail(`${file.relativePath} does not contain packages[""]`);
}
fields.push(['packages[""].version', lockRoot]);
}
let changed = false;
for (const [field, owner] of fields) {
if (owner.version !== expectedVersion) {
mismatches.push(
`${file.relativePath} ${field}: ${owner.version} (expected ${expectedVersion})`
);
if (!checkOnly) {
owner.version = expectedVersion;
changed = true;
}
}
}
if (changed) {
fs.writeFileSync(file.absolutePath, `${JSON.stringify(file.data, null, 2)}\n`);
}
}
if (mismatches.length > 0 && checkOnly) {
console.error('Package versions are out of sync:');
for (const mismatch of mismatches) {
console.error(`- ${mismatch}`);
}
console.error(
`Run \`npm run version:set -- ${expectedVersion}\` from the repository root to synchronize them.`
);
process.exit(1);
}
if (mismatches.length > 0) {
console.log(`Synchronized package versions to ${expectedVersion}.`);
} else {
console.log(`Package versions are synchronized at ${expectedVersion}.`);
}
+128
View File
@@ -0,0 +1,128 @@
'use strict';
const assert = require('node:assert/strict');
const { spawnSync } = require('node:child_process');
const fs = require('node:fs');
const os = require('node:os');
const path = require('node:path');
const test = require('node:test');
const scriptSource = path.join(__dirname, 'sync-package-versions.js');
function writeJson(filePath, data) {
fs.mkdirSync(path.dirname(filePath), { recursive: true });
fs.writeFileSync(filePath, `${JSON.stringify(data, null, 2)}\n`);
}
function createFixture() {
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'version-sync-'));
const scriptsDirectory = path.join(root, 'scripts');
fs.mkdirSync(scriptsDirectory);
fs.copyFileSync(scriptSource, path.join(scriptsDirectory, 'sync-package-versions.js'));
writeJson(path.join(root, 'package.json'), {
name: 'fixture',
version: '1.2.3',
});
writeJson(path.join(root, 'package-lock.json'), {
name: 'fixture',
version: '1.2.3',
lockfileVersion: 3,
requires: true,
packages: { '': { name: 'fixture', version: '1.2.3' } },
});
writeJson(path.join(root, 'cli-tool/package.json'), {
name: 'fixture-cli',
version: '1.2.3',
});
writeJson(path.join(root, 'cli-tool/package-lock.json'), {
name: 'fixture-cli',
version: '1.2.3',
lockfileVersion: 3,
requires: true,
packages: { '': { name: 'fixture-cli', version: '1.2.3' } },
});
return root;
}
function runScript(root, ...arguments_) {
return spawnSync(
process.execPath,
[path.join(root, 'scripts/sync-package-versions.js'), ...arguments_],
{ cwd: root, encoding: 'utf8' }
);
}
function readJson(root, relativePath) {
return JSON.parse(fs.readFileSync(path.join(root, relativePath), 'utf8'));
}
test('check succeeds when every version is synchronized', (t) => {
const root = createFixture();
t.after(() => fs.rmSync(root, { recursive: true, force: true }));
const result = runScript(root, '--check');
assert.equal(result.status, 0);
assert.match(result.stdout, /Package versions are synchronized at 1\.2\.3\./);
});
test('check reports every mismatch without changing any file', (t) => {
const root = createFixture();
t.after(() => fs.rmSync(root, { recursive: true, force: true }));
const cliPackagePath = path.join(root, 'cli-tool/package.json');
const cliLockPath = path.join(root, 'cli-tool/package-lock.json');
const cliPackage = readJson(root, 'cli-tool/package.json');
const cliLock = readJson(root, 'cli-tool/package-lock.json');
cliPackage.version = '1.2.2';
cliLock.version = '1.2.1';
cliLock.packages[''].version = '1.2.0';
writeJson(cliPackagePath, cliPackage);
writeJson(cliLockPath, cliLock);
const packageBefore = fs.readFileSync(cliPackagePath, 'utf8');
const lockBefore = fs.readFileSync(cliLockPath, 'utf8');
const result = runScript(root, '--check');
assert.equal(result.status, 1);
assert.match(result.stderr, /cli-tool\/package\.json version: 1\.2\.2/);
assert.match(result.stderr, /cli-tool\/package-lock\.json version: 1\.2\.1/);
assert.match(result.stderr, /cli-tool\/package-lock\.json packages\[""\]\.version: 1\.2\.0/);
assert.equal(fs.readFileSync(cliPackagePath, 'utf8'), packageBefore);
assert.equal(fs.readFileSync(cliLockPath, 'utf8'), lockBefore);
});
test('an explicit version rewrites both manifests and lockfile roots', (t) => {
const root = createFixture();
t.after(() => fs.rmSync(root, { recursive: true, force: true }));
const targetVersion = '2.0.0-beta.1+build.5';
const result = runScript(root, targetVersion);
assert.equal(result.status, 0);
for (const relativePath of [
'package.json',
'package-lock.json',
'cli-tool/package.json',
'cli-tool/package-lock.json',
]) {
const data = readJson(root, relativePath);
assert.equal(data.version, targetVersion);
if (relativePath.endsWith('package-lock.json')) {
assert.equal(data.packages[''].version, targetVersion);
}
}
});
test('invalid semantic versions produce a concise user error', (t) => {
const root = createFixture();
t.after(() => fs.rmSync(root, { recursive: true, force: true }));
const result = runScript(root, '1.2.3-01');
assert.equal(result.status, 1);
assert.equal(result.stderr, 'Invalid semantic version: 1.2.3-01\n');
assert.doesNotMatch(result.stderr, /\n\s+at /);
});