mirror of
https://github.com/wuzf/2fa.git
synced 2026-09-03 07:17:09 +08:00
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:
@@ -4,7 +4,7 @@
|
||||
|
||||
**[English](README_EN.md)**
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
A two-factor authentication key management system built on Cloudflare Workers. Free to deploy, globally accelerated, with PWA offline support.
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
|
||||
@@ -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
184
scripts/release.js
Normal 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. 提交并打 tag(commit 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();
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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'
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user