chore(release): centralize version source and add release automation

- Replace hardcoded 1.5.0 in logger/monitoring with APP_VERSION import
- Update README version badges to 1.6.0 with test-enforced consistency
- Add scripts/release.js: npm run release:patch/minor/major runs tests,
  bumps package.json, syncs version.js and README badges, commits and tags
This commit is contained in:
wuzf
2026-07-26 22:10:20 +08:00
parent 8f4121b78c
commit 5cc1513a58
9 changed files with 218 additions and 13 deletions

View File

@@ -4,7 +4,7 @@
**[English](README_EN.md)**
![Version](https://img.shields.io/badge/version-1.5.0-blue)
![Version](https://img.shields.io/badge/version-1.6.0-blue)
![License](https://img.shields.io/badge/license-MIT-green)
![Platform](https://img.shields.io/badge/platform-Cloudflare%20Workers-orange)

View File

@@ -2,7 +2,7 @@
A two-factor authentication key management system built on Cloudflare Workers. Free to deploy, globally accelerated, with PWA offline support.
![Version](https://img.shields.io/badge/version-1.5.0-blue)
![Version](https://img.shields.io/badge/version-1.6.0-blue)
![License](https://img.shields.io/badge/license-MIT-green)
![Platform](https://img.shields.io/badge/platform-Cloudflare%20Workers-orange)

View File

@@ -22,6 +22,10 @@
"lint:fix": "eslint src/ --fix",
"format": "prettier --write \"src/**/*.js\"",
"format:check": "prettier --check \"src/**/*.js\"",
"release:patch": "node scripts/release.js patch",
"release:minor": "node scripts/release.js minor",
"release:major": "node scripts/release.js major",
"release:sync": "node scripts/release.js --sync",
"prepare": "husky"
},
"lint-staged": {

184
scripts/release.js Normal file
View File

@@ -0,0 +1,184 @@
#!/usr/bin/env node
/**
* 发版脚本 - 一条命令完成版本号同步、提交和打 tag
*
* 版本号唯一数据源是 package.json其余位置由本脚本同步
* - src/utils/version.js (APP_VERSION前端 footer 和新版本检测依赖)
* - README.md / README_EN.md (版本徽章)
*
* 使用方式:
* npm run release:patch # 1.6.0 → 1.6.1
* npm run release:minor # 1.6.0 → 1.7.0
* npm run release:major # 1.6.0 → 2.0.0
* node scripts/release.js 1.8.0 # 指定版本号
* node scripts/release.js --sync # 仅同步(修复各处版本不一致,不提交)
*
* 发版流程:
* 1. 检查 tag 未存在、版本相关文件无未提交修改(其他文件如 wrangler.toml 允许 dirty
* 2. 运行全量测试(--skip-tests 跳过)
* 3. bump package.json + package-lock.json
* 4. 同步 version.js 和 README 徽章
* 5. 运行版本一致性测试自检
* 6. git commit + git tag v{x.y.z}tag 需符合 v{x.y.z} 格式,新版本检测依赖)
* 7. 提示手动 push不自动 push
*/
import { execSync } from 'child_process';
import { readFileSync, writeFileSync } from 'fs';
import { fileURLToPath } from 'url';
import { dirname, join } from 'path';
const __dirname = dirname(fileURLToPath(import.meta.url));
const ROOT = join(__dirname, '..');
/** 版本号写入的所有位置(新增位置时在此登记,并同步更新 tests/utils/version.test.js 的一致性校验) */
const SYNC_TARGETS = [
{
file: 'src/utils/version.js',
pattern: /(APP_VERSION = ')\d+\.\d+\.\d+(')/,
replacement: (v) => `$1${v}$2`,
},
{
file: 'README.md',
pattern: /(badge\/version-)\d+\.\d+\.\d+(-blue)/,
replacement: (v) => `$1${v}$2`,
},
{
file: 'README_EN.md',
pattern: /(badge\/version-)\d+\.\d+\.\d+(-blue)/,
replacement: (v) => `$1${v}$2`,
},
];
/** 发版 commit 包含的全部文件 */
const RELEASE_FILES = ['package.json', 'package-lock.json', ...SYNC_TARGETS.map((t) => t.file)];
function run(cmd, options = {}) {
return execSync(cmd, { cwd: ROOT, encoding: 'utf-8', ...options });
}
function readPackageVersion() {
return JSON.parse(readFileSync(join(ROOT, 'package.json'), 'utf-8')).version;
}
function bumpVersion(current, type) {
const [major, minor, patch] = current.split('.').map(Number);
switch (type) {
case 'major':
return `${major + 1}.0.0`;
case 'minor':
return `${major}.${minor + 1}.0`;
case 'patch':
return `${major}.${minor}.${patch + 1}`;
default:
return type; // 显式版本号
}
}
/** 将版本号同步到 SYNC_TARGETS 中的所有文件,任一文件匹配失败则报错退出 */
function syncVersion(version) {
let failed = false;
for (const { file, pattern, replacement } of SYNC_TARGETS) {
const path = join(ROOT, file);
const content = readFileSync(path, 'utf-8');
if (!pattern.test(content)) {
console.error(`${file}: 未匹配到版本号模式 ${pattern},请检查文件内容或更新 SYNC_TARGETS`);
failed = true;
continue;
}
const updated = content.replace(pattern, replacement(version));
if (updated === content) {
console.log(`${file} 已是 ${version}`);
} else {
writeFileSync(path, updated);
console.log(`${file}${version}`);
}
}
if (failed) {
process.exit(1);
}
}
function main() {
const args = process.argv.slice(2);
const skipTests = args.includes('--skip-tests');
const positional = args.filter((a) => !a.startsWith('--'));
// --sync 模式:仅把 package.json 的版本同步到其他位置
if (args.includes('--sync')) {
const version = readPackageVersion();
console.log(`🔄 同步版本号 ${version}(数据源: package.json\n`);
syncVersion(version);
return;
}
const type = positional[0];
if (!type || (!['major', 'minor', 'patch'].includes(type) && !/^\d+\.\d+\.\d+$/.test(type))) {
console.error('用法: node scripts/release.js <major|minor|patch|x.y.z> [--skip-tests]');
console.error(' node scripts/release.js --sync');
process.exit(1);
}
const currentVersion = readPackageVersion();
const newVersion = bumpVersion(currentVersion, type);
const tag = `v${newVersion}`;
console.log(`\n🚀 发版: ${currentVersion}${newVersion}\n`);
// 1. tag 不能已存在
if (run(`git tag -l ${tag}`).trim()) {
console.error(`❌ tag ${tag} 已存在,请检查版本号`);
process.exit(1);
}
// 2. 版本相关文件必须干净,避免发版 commit 混入无关修改
// (其他文件如 wrangler.toml 允许有本地修改,仅提示)
const dirty = run('git status --porcelain')
.split('\n')
.filter(Boolean)
.map((line) => line.slice(3).trim());
const dirtyReleaseFiles = dirty.filter((f) => RELEASE_FILES.includes(f));
if (dirtyReleaseFiles.length > 0) {
console.error(`❌ 以下版本相关文件有未提交修改,请先提交或还原:\n ${dirtyReleaseFiles.join('\n ')}`);
process.exit(1);
}
const dirtyOthers = dirty.filter((f) => !RELEASE_FILES.includes(f));
if (dirtyOthers.length > 0) {
console.warn(`⚠️ 工作区存在其他未提交修改(不会包含在发版 commit 中):\n ${dirtyOthers.join('\n ')}\n`);
}
// 3. 全量测试
if (skipTests) {
console.warn('⚠️ 已跳过测试 (--skip-tests)\n');
} else {
console.log('🧪 运行全量测试...\n');
run('npx vitest run', { stdio: 'inherit' });
}
// 4. bump package.json仅替换顶层 version 字段),并同步 package-lock.json
const pkgPath = join(ROOT, 'package.json');
const pkgContent = readFileSync(pkgPath, 'utf-8');
writeFileSync(pkgPath, pkgContent.replace(/("version":\s*")\d+\.\d+\.\d+(")/, `$1${newVersion}$2`));
console.log(`✅ package.json → ${newVersion}`);
run('npm install --package-lock-only --ignore-scripts', { stdio: 'ignore' });
console.log(`✅ package-lock.json → ${newVersion}`);
// 5. 同步其余位置
syncVersion(newVersion);
// 6. 版本一致性自检tests/utils/version.test.js 校验 APP_VERSION 和 README 徽章)
console.log('\n🔍 版本一致性自检...\n');
run('npx vitest run tests/utils/version.test.js', { stdio: 'inherit' });
// 7. 提交并打 tagcommit message 符合 Conventional Commits通过 husky commit-msg 校验)
run(`git add ${RELEASE_FILES.join(' ')}`);
run(`git commit -m "chore(release): bump version to ${newVersion}"`, { stdio: 'inherit' });
run(`git tag ${tag}`);
console.log(`\n✅ 发版完成: ${tag}commit + tag 均已创建)`);
console.log('\n📤 请推送以发布(用户端新版本检测依赖远端 tag:');
console.log('\n git push && git push --tags\n');
}
main();

View File

@@ -6,6 +6,8 @@
* 支持上下文信息、用户标识、性能指标
*/
import { APP_VERSION } from './version.js';
/**
* 日志级别枚举
*/
@@ -47,7 +49,7 @@ class Logger {
this.minLevel = options.minLevel || LogLevel.INFO;
this.environment = options.environment || 'development';
this.serviceName = options.serviceName || '2fa';
this.version = options.version || '1.5.0';
this.version = options.version || APP_VERSION;
this.enableConsole = options.enableConsole !== false;
this.enableRemote = options.enableRemote || false;
this.remoteEndpoint = options.remoteEndpoint || null;
@@ -287,7 +289,7 @@ export function getLogger(env = null) {
minLevel,
environment: env?.ENVIRONMENT || 'development',
serviceName: '2fa',
version: '1.5.0',
version: APP_VERSION,
enableConsole: true,
enableRemote: env?.LOG_REMOTE_ENDPOINT ? true : false,
remoteEndpoint: env?.LOG_REMOTE_ENDPOINT || null,

View File

@@ -6,6 +6,7 @@
/* global Sentry */
import { getLogger } from './logger.js';
import { APP_VERSION } from './version.js';
/**
* 错误严重程度级别
@@ -40,7 +41,7 @@ class MonitoringConfig {
// 自定义配置
this.environment = options.environment || 'production';
this.serviceName = options.serviceName || '2fa';
this.version = options.version || '1.5.0';
this.version = options.version || APP_VERSION;
}
/**
@@ -495,14 +496,14 @@ export function getMonitoring(env = null) {
sentryDsn: env?.SENTRY_DSN || null,
sentryEnabled: !!env?.SENTRY_DSN,
sentryEnvironment: env?.ENVIRONMENT || 'production',
sentryRelease: env?.VERSION || '1.5.0',
sentryRelease: env?.VERSION || APP_VERSION,
errorSampleRate: parseFloat(env?.ERROR_SAMPLE_RATE || '1.0'),
traceSampleRate: parseFloat(env?.TRACE_SAMPLE_RATE || '0.1'),
enablePerformanceMonitoring: env?.ENABLE_PERFORMANCE_MONITORING !== 'false',
slowRequestThreshold: parseInt(env?.SLOW_REQUEST_THRESHOLD || '3000'),
environment: env?.ENVIRONMENT || 'production',
serviceName: '2fa',
version: env?.VERSION || '1.5.0',
version: env?.VERSION || APP_VERSION,
});
defaultMonitoring = new MonitoringManager(config);

View File

@@ -13,6 +13,7 @@ import {
PerformanceTimer,
createRequestLogger
} from '../../src/utils/logger.js';
import { APP_VERSION } from '../../src/utils/version.js';
// ==================== 测试辅助工具 ====================
@@ -95,7 +96,7 @@ describe('Logger System', () => {
expect(logger.minLevel).toBe(LogLevel.INFO);
expect(logger.environment).toBe('development');
expect(logger.serviceName).toBe('2fa');
expect(logger.version).toBe('1.5.0');
expect(logger.version).toBe(APP_VERSION);
expect(logger.enableConsole).toBe(true);
expect(logger.enableRemote).toBe(false);
expect(logger.remoteEndpoint).toBeNull();
@@ -107,7 +108,7 @@ describe('Logger System', () => {
minLevel: LogLevel.WARN, // 使用非0值避免 0 || default 的问题
environment: 'production',
serviceName: 'test-service',
version: '1.5.0',
version: '9.9.9',
enableConsole: false,
enableRemote: true,
remoteEndpoint: 'https://logs.example.com',
@@ -117,7 +118,7 @@ describe('Logger System', () => {
expect(logger.minLevel).toBe(LogLevel.WARN);
expect(logger.environment).toBe('production');
expect(logger.serviceName).toBe('test-service');
expect(logger.version).toBe('1.5.0');
expect(logger.version).toBe('9.9.9');
expect(logger.enableConsole).toBe(false);
expect(logger.enableRemote).toBe(true);
expect(logger.remoteEndpoint).toBe('https://logs.example.com');
@@ -141,7 +142,7 @@ describe('Logger System', () => {
expect(logEntry).toMatchObject({
level: 'INFO',
service: '2fa',
version: '1.5.0',
version: APP_VERSION,
environment: 'development',
message: 'Test message'
});

View File

@@ -14,6 +14,7 @@ import {
resetMonitoring,
monitoring
} from '../../src/utils/monitoring.js';
import { APP_VERSION } from '../../src/utils/version.js';
// ==================== Mock 设置 ====================
@@ -115,7 +116,7 @@ describe('Monitoring System', () => {
expect(config.slowRequestThreshold).toBe(3000);
expect(config.environment).toBe('production');
expect(config.serviceName).toBe('2fa');
expect(config.version).toBe('1.5.0');
expect(config.version).toBe(APP_VERSION);
});
it('应该使用自定义配置创建', () => {
@@ -1103,7 +1104,7 @@ describe('Monitoring System', () => {
expect(monitoring.config.sentryEnabled).toBe(false);
expect(monitoring.config.environment).toBe('production');
expect(monitoring.config.serviceName).toBe('2fa');
expect(monitoring.config.version).toBe('1.5.0');
expect(monitoring.config.version).toBe(APP_VERSION);
expect(monitoring.config.errorSampleRate).toBe(1.0);
expect(monitoring.config.traceSampleRate).toBe(0.1);
expect(monitoring.config.enablePerformanceMonitoring).toBe(true);

View File

@@ -1,3 +1,5 @@
import { readFileSync } from 'node:fs';
import { describe, it, expect } from 'vitest';
import { APP_VERSION, compareVersions } from '../../src/utils/version.js';
@@ -14,6 +16,16 @@ describe('version utils', () => {
});
});
// 版本号所有写入位置的一致性校验(发版用 npm run release:*,修复不一致用 npm run release:sync
describe('version consistency', () => {
it.each(['README.md', 'README_EN.md'])('%s badge should match package.json version', (file) => {
const content = readFileSync(new URL(`../../${file}`, import.meta.url), 'utf-8');
const match = content.match(/badge\/version-(\d+\.\d+\.\d+)-blue/);
expect(match, `${file} 应包含版本徽章`).not.toBeNull();
expect(match[1], `${file} 徽章版本应与 package.json 一致`).toBe(pkg.version);
});
});
describe('compareVersions', () => {
it('should return 0 for equal versions', () => {
expect(compareVersions('1.5.0', '1.5.0')).toBe(0);