mirror of
https://github.com/davila7/claude-code-templates.git
synced 2026-10-02 03:14:35 +08:00
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:
@@ -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."
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
|
||||
Generated
+2
-2
@@ -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",
|
||||
|
||||
@@ -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
@@ -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"
|
||||
|
||||
@@ -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}.`);
|
||||
}
|
||||
@@ -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 /);
|
||||
});
|
||||
Reference in New Issue
Block a user