mirror of
https://github.com/wuzf/2fa.git
synced 2026-09-03 07:17:09 +08:00
feat(deploy): auto-detect and reuse existing KV namespace in deploy scripts
This commit is contained in:
77
scripts/deploy-config.js
Normal file
77
scripts/deploy-config.js
Normal file
@@ -0,0 +1,77 @@
|
||||
export function injectWorkerVersion(configText, version) {
|
||||
const updated = configText.replace(
|
||||
/^(\s*SW_VERSION\s*=\s*)"[^"]*"(\s*)$/m,
|
||||
`$1"${version}"$2`
|
||||
);
|
||||
|
||||
if (updated === configText) {
|
||||
throw new Error('在 wrangler.toml 中未找到 SW_VERSION 配置');
|
||||
}
|
||||
|
||||
return updated;
|
||||
}
|
||||
|
||||
export function extractWorkerName(configText) {
|
||||
const match = configText.match(/^name\s*=\s*"([^"]+)"/m);
|
||||
return match ? match[1] : null;
|
||||
}
|
||||
|
||||
export function injectKvNamespaceId(configText, id) {
|
||||
// Match the first [[kv_namespaces]] block that contains binding = "SECRETS_KV"
|
||||
// and is NOT inside an [env.*] section.
|
||||
const lines = configText.split('\n');
|
||||
let inEnvSection = false;
|
||||
let kvBlockStart = -1;
|
||||
let bindingLine = -1;
|
||||
let existingIdLine = -1;
|
||||
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const trimmed = lines[i].trim();
|
||||
|
||||
// Track whether we're inside an [env.*] section
|
||||
if (/^\[env\./.test(trimmed)) {
|
||||
inEnvSection = true;
|
||||
continue;
|
||||
}
|
||||
if (/^\[(?!env\.)/.test(trimmed) || /^\[\[(?!env\.)/.test(trimmed)) {
|
||||
inEnvSection = false;
|
||||
}
|
||||
|
||||
if (inEnvSection) continue;
|
||||
|
||||
if (trimmed === '[[kv_namespaces]]') {
|
||||
kvBlockStart = i;
|
||||
bindingLine = -1;
|
||||
existingIdLine = -1;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (kvBlockStart >= 0) {
|
||||
// End of block: next section header or empty line after content
|
||||
if (/^\[/.test(trimmed) || /^\[\[/.test(trimmed)) {
|
||||
if (bindingLine >= 0) break;
|
||||
kvBlockStart = -1;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (/^binding\s*=\s*"SECRETS_KV"/.test(trimmed)) {
|
||||
bindingLine = i;
|
||||
}
|
||||
if (/^id\s*=\s*"/.test(trimmed)) {
|
||||
existingIdLine = i;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (bindingLine < 0) {
|
||||
return configText;
|
||||
}
|
||||
|
||||
if (existingIdLine >= 0) {
|
||||
lines[existingIdLine] = `id = "${id}"`;
|
||||
} else {
|
||||
lines.splice(bindingLine + 1, 0, `id = "${id}"`);
|
||||
}
|
||||
|
||||
return lines.join('\n');
|
||||
}
|
||||
@@ -16,19 +16,20 @@
|
||||
*/
|
||||
|
||||
import { execSync } from 'child_process';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { readFileSync, writeFileSync } from 'fs';
|
||||
import { dirname, join } from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
import { extractWorkerName, injectKvNamespaceId, injectWorkerVersion } from './deploy-config.js';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
|
||||
// 解析命令行参数
|
||||
const args = process.argv.slice(2);
|
||||
const versionStrategy = args.includes('--git') ? '--git' :
|
||||
args.includes('--package') ? '--package' :
|
||||
'';
|
||||
args.includes('--package') ? '--package' :
|
||||
'';
|
||||
|
||||
// 提取环境参数
|
||||
const envIndex = args.indexOf('--env');
|
||||
const envArg = envIndex !== -1 && args[envIndex + 1] ? `--env ${args[envIndex + 1]}` : '';
|
||||
|
||||
@@ -38,89 +39,34 @@ console.log(' 2FA Manager 自动化部署');
|
||||
console.log('========================================');
|
||||
console.log('');
|
||||
|
||||
// Step 1: 生成版本号
|
||||
console.log('📦 Step 1: 生成 Service Worker 版本号...');
|
||||
try {
|
||||
const versionCmd = `node ${join(__dirname, 'generate-version.js')} ${versionStrategy} --verbose`;
|
||||
const version = execSync(versionCmd, { encoding: 'utf-8' }).trim().split('\n')[0];
|
||||
const version = generateVersion(versionStrategy);
|
||||
const wranglerPath = join(__dirname, '..', 'wrangler.toml');
|
||||
const originalConfig = readFileSync(wranglerPath, 'utf-8');
|
||||
|
||||
console.log(` ✅ 版本号: ${version}`);
|
||||
console.log('');
|
||||
|
||||
// Step 2: 临时修改 wrangler.toml
|
||||
console.log('📝 Step 2: 注入版本到配置...');
|
||||
const wranglerPath = join(__dirname, '..', 'wrangler.toml');
|
||||
|
||||
// 读取原始配置
|
||||
const fs = await import('fs');
|
||||
const originalConfig = fs.readFileSync(wranglerPath, 'utf-8');
|
||||
let modifiedConfig = injectWorkerVersion(originalConfig, version);
|
||||
|
||||
let modifiedConfig = originalConfig;
|
||||
|
||||
// 替换版本号
|
||||
modifiedConfig = modifiedConfig.replace(
|
||||
/SW_VERSION = "v1"/,
|
||||
`SW_VERSION = "${version}"`
|
||||
);
|
||||
|
||||
// 检测 KV namespace 配置,自动查找或创建
|
||||
const hasKvBinding = /\[\[kv_namespaces\]\]\r?\nbinding = "SECRETS_KV"\r?\nid = "/.test(modifiedConfig);
|
||||
if (!hasKvBinding) {
|
||||
console.log(' 🔍 检测到 KV namespace 未配置,查找已有的...');
|
||||
|
||||
let kvId = null;
|
||||
|
||||
// Step A: 先从已有的 KV namespace 中查找
|
||||
try {
|
||||
const listOutput = execSync('npx wrangler kv namespace list', { encoding: 'utf-8' });
|
||||
const namespaces = JSON.parse(listOutput);
|
||||
// 精确匹配 "SECRETS_KV"(deploy.js 创建的,用户数据在此)
|
||||
const existing = namespaces.find((ns) => ns.title === 'SECRETS_KV');
|
||||
if (existing) {
|
||||
kvId = existing.id;
|
||||
console.log(` ✅ 找到已有 KV namespace "${existing.title}": ${kvId}`);
|
||||
}
|
||||
} catch {
|
||||
console.log(' ⚠️ 查询 KV namespace 列表失败,尝试创建新的...');
|
||||
}
|
||||
|
||||
// Step B: 没找到才创建
|
||||
if (!kvId) {
|
||||
try {
|
||||
console.log(' 📦 未找到已有 KV namespace,创建新的...');
|
||||
const kvOutput = execSync('npx wrangler kv namespace create SECRETS_KV', {
|
||||
encoding: 'utf-8',
|
||||
});
|
||||
const idMatch = kvOutput.match(/id = "([a-f0-9]+)"/);
|
||||
if (idMatch) {
|
||||
kvId = idMatch[1];
|
||||
console.log(` ✅ KV namespace 已创建: ${kvId}`);
|
||||
} else {
|
||||
console.warn(' ⚠️ 无法从输出中提取 KV ID,尝试继续部署...');
|
||||
}
|
||||
} catch (kvError) {
|
||||
console.error(' ❌ 创建 KV namespace 失败');
|
||||
throw kvError;
|
||||
}
|
||||
}
|
||||
|
||||
// 注入 KV 配置到 wrangler.toml
|
||||
if (kvId) {
|
||||
const kvBlock = `[[kv_namespaces]]\nbinding = "SECRETS_KV"\nid = "${kvId}"`;
|
||||
// 替换已有的注释块,或在 [vars] 前插入
|
||||
const commentedKvPattern = /# \[\[kv_namespaces\]\]\r?\n# binding = "SECRETS_KV"\r?\n(?:#[^\n]*\r?\n)*/;
|
||||
if (commentedKvPattern.test(modifiedConfig)) {
|
||||
modifiedConfig = modifiedConfig.replace(commentedKvPattern, kvBlock + '\n');
|
||||
} else {
|
||||
modifiedConfig = modifiedConfig.replace(/(\[vars\])/, kvBlock + '\n\n$1');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fs.writeFileSync(wranglerPath, modifiedConfig, 'utf-8');
|
||||
console.log(` ✅ 已注入版本: ${version}`);
|
||||
console.log('');
|
||||
|
||||
// Step 3: 执行部署
|
||||
// Step 2.5: 自动检测并绑定已有 KV namespace,防止重复创建
|
||||
console.log('🔍 Step 2.5: 检测已有 KV namespace...');
|
||||
const existingKv = findExistingKvId(extractWorkerName(modifiedConfig));
|
||||
if (existingKv) {
|
||||
modifiedConfig = injectKvNamespaceId(modifiedConfig, existingKv.id);
|
||||
console.log(` ✅ 复用已有 KV: ${existingKv.title} (${existingKv.id})`);
|
||||
} else {
|
||||
console.log(' ℹ️ 未检测到已有 KV,将由 Wrangler 自动创建');
|
||||
}
|
||||
console.log('');
|
||||
|
||||
writeFileSync(wranglerPath, modifiedConfig, 'utf-8');
|
||||
|
||||
console.log('🚀 Step 3: 部署到 Cloudflare Workers...');
|
||||
console.log(` 命令: npx wrangler deploy ${envArg}`.trim());
|
||||
console.log('');
|
||||
@@ -128,7 +74,7 @@ try {
|
||||
try {
|
||||
execSync(`npx wrangler deploy ${envArg}`.trim(), {
|
||||
stdio: 'inherit',
|
||||
encoding: 'utf-8'
|
||||
encoding: 'utf-8',
|
||||
});
|
||||
|
||||
console.log('');
|
||||
@@ -139,7 +85,6 @@ try {
|
||||
console.log(`📦 版本: ${version}`);
|
||||
console.log(`🌐 环境: ${envArg || '生产环境 (production)'}`);
|
||||
console.log('');
|
||||
|
||||
} catch (deployError) {
|
||||
console.error('');
|
||||
console.error('❌ ========================================');
|
||||
@@ -148,13 +93,11 @@ try {
|
||||
console.error('');
|
||||
throw deployError;
|
||||
} finally {
|
||||
// Step 4: 恢复原始配置
|
||||
console.log('🔄 Step 4: 恢复配置文件...');
|
||||
fs.writeFileSync(wranglerPath, originalConfig, 'utf-8');
|
||||
writeFileSync(wranglerPath, originalConfig, 'utf-8');
|
||||
console.log(' ✅ 配置已恢复');
|
||||
console.log('');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('');
|
||||
console.error('❌ 部署流程失败:');
|
||||
@@ -162,3 +105,31 @@ try {
|
||||
console.error('');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
function generateVersion(versionStrategyArg) {
|
||||
console.log('📦 Step 1: 生成 Service Worker 版本号...');
|
||||
const versionCmd = `node ${join(__dirname, 'generate-version.js')} ${versionStrategyArg} --verbose`;
|
||||
return execSync(versionCmd, { encoding: 'utf-8' }).trim().split('\n')[0];
|
||||
}
|
||||
|
||||
function findExistingKvId(workerName) {
|
||||
try {
|
||||
const output = execSync('npx wrangler kv namespace list', {
|
||||
encoding: 'utf-8',
|
||||
stdio: ['pipe', 'pipe', 'pipe'],
|
||||
});
|
||||
const namespaces = JSON.parse(output);
|
||||
if (!namespaces.length) return null;
|
||||
|
||||
// 按优先级匹配,覆盖 wrangler auto-provision、Dashboard 创建、老版本等命名格式
|
||||
const match =
|
||||
namespaces.find(ns => ns.title === `${workerName}-SECRETS_KV`) ||
|
||||
namespaces.find(ns => ns.title.includes('SECRETS_KV')) ||
|
||||
namespaces.find(ns => ns.title === workerName) ||
|
||||
(namespaces.length === 1 ? namespaces[0] : null);
|
||||
|
||||
return match ? { id: match.id, title: match.title } : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
84
tests/scripts/deploy-config.test.js
Normal file
84
tests/scripts/deploy-config.test.js
Normal file
@@ -0,0 +1,84 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { extractWorkerName, injectKvNamespaceId, injectWorkerVersion } from '../../scripts/deploy-config.js';
|
||||
|
||||
describe('injectWorkerVersion', () => {
|
||||
it('replaces SW_VERSION without touching KV bindings', () => {
|
||||
const config = `name = "2fa"
|
||||
main = "src/worker.js"
|
||||
|
||||
[[kv_namespaces]]
|
||||
binding = "SECRETS_KV"
|
||||
|
||||
[vars]
|
||||
SW_VERSION = "v1"
|
||||
`;
|
||||
|
||||
const updated = injectWorkerVersion(config, 'v20260325-123456');
|
||||
|
||||
expect(updated).toContain('SW_VERSION = "v20260325-123456"');
|
||||
expect(updated).toContain('[[kv_namespaces]]\nbinding = "SECRETS_KV"');
|
||||
expect(updated.match(/\[\[kv_namespaces\]\]/g)).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('throws when SW_VERSION is missing', () => {
|
||||
expect(() => injectWorkerVersion('[vars]\n', 'v20260325-123456')).toThrow(
|
||||
'在 wrangler.toml 中未找到 SW_VERSION 配置'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('extractWorkerName', () => {
|
||||
it('extracts name from config', () => {
|
||||
expect(extractWorkerName('name = "2fa"\nmain = "src/worker.js"')).toBe('2fa');
|
||||
});
|
||||
|
||||
it('returns null when name is missing', () => {
|
||||
expect(extractWorkerName('main = "src/worker.js"')).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('injectKvNamespaceId', () => {
|
||||
const baseConfig = `name = "2fa"
|
||||
main = "src/worker.js"
|
||||
|
||||
[[kv_namespaces]]
|
||||
binding = "SECRETS_KV"
|
||||
|
||||
[vars]
|
||||
SW_VERSION = "v1"
|
||||
|
||||
[env.development]
|
||||
name = "2fa-dev"
|
||||
|
||||
[[env.development.kv_namespaces]]
|
||||
binding = "SECRETS_KV"
|
||||
`;
|
||||
|
||||
it('inserts id when none exists', () => {
|
||||
const result = injectKvNamespaceId(baseConfig, 'abc123');
|
||||
expect(result).toContain('binding = "SECRETS_KV"\nid = "abc123"');
|
||||
});
|
||||
|
||||
it('replaces existing id', () => {
|
||||
const configWithId = baseConfig.replace(
|
||||
'binding = "SECRETS_KV"\n\n[vars]',
|
||||
'binding = "SECRETS_KV"\nid = "old-id"\n\n[vars]'
|
||||
);
|
||||
const result = injectKvNamespaceId(configWithId, 'new-id');
|
||||
expect(result).toContain('id = "new-id"');
|
||||
expect(result).not.toContain('old-id');
|
||||
});
|
||||
|
||||
it('does not modify env.development kv_namespaces', () => {
|
||||
const result = injectKvNamespaceId(baseConfig, 'abc123');
|
||||
// The env.development block should not have id injected
|
||||
const devBlock = result.split('[[env.development.kv_namespaces]]')[1];
|
||||
expect(devBlock).not.toContain('id = "abc123"');
|
||||
});
|
||||
|
||||
it('returns config unchanged when no SECRETS_KV binding found', () => {
|
||||
const noKvConfig = 'name = "2fa"\n[vars]\nSW_VERSION = "v1"\n';
|
||||
expect(injectKvNamespaceId(noKvConfig, 'abc123')).toBe(noKvConfig);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user