Files
EdgeSSH/scripts/deploy.ts
T
gdz66601 3fa5257d93 feat(deploy): automate Access bootstrap and preserve Worker secrets
自动配置 Access、OTP 与邮箱授权,保留 Worker 密钥并预留 GitHub IdP 扩展。
2026-09-21 23:08:50 +08:00

80 lines
4.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { spawn } from 'node:child_process';
import { appendFile, readFile, writeFile } from 'node:fs/promises';
import { fileURLToPath } from 'node:url';
import { parse, stringify } from 'smol-toml';
import { ensureDatabase } from './cloudflare-d1.ts';
import { createDeploymentConfig, readDeploymentSettings } from './deployment-config.ts';
import { CloudflareApi, resolveAccountId } from './cloudflare-api.ts';
import { ensureAccess, resolveWorkerHostname } from './cloudflare-access.ts';
import { maskSecrets, prepareEncryptionSecret, readWorkerSecretNames } from './deployment-secrets.ts';
const root = fileURLToPath(new URL('../', import.meta.url));
const generatedConfig = '.wrangler.generated.toml';
const wrangler = fileURLToPath(new URL('../node_modules/wrangler/bin/wrangler.js', import.meta.url));
async function runWrangler(args: string[], input?: string): Promise<void> {
await new Promise<void>((resolve, reject) => {
const child = spawn(process.execPath, [wrangler, ...args, '--config', generatedConfig], {
cwd: root,
env: { ...process.env, CI: 'true' },
stdio: [input === undefined ? 'ignore' : 'pipe', 'inherit', 'inherit'],
});
child.on('error', reject);
child.on('close', (code) => code === 0 ? resolve() : reject(new Error(`Wrangler ${args[0]} 失败(退出码 ${code})。`)));
if (input !== undefined) {
child.stdin!.on('error', reject);
child.stdin!.end(input);
}
});
}
async function main(): Promise<void> {
const template = parse(await readFile(new URL('../wrangler.toml', import.meta.url), 'utf8'));
const settings = readDeploymentSettings(template, process.env);
if (process.argv.includes('--validate-only')) {
console.log('部署配置校验通过。');
return;
}
const api = new CloudflareApi(settings.apiToken);
settings.accountId = await resolveAccountId(api, settings.accountId);
process.env.CLOUDFLARE_ACCOUNT_ID = settings.accountId;
const existingSecrets = await readWorkerSecretNames(api, settings);
const hostname = settings.customDomain || await resolveWorkerHostname(api, settings);
// 旧部署可能由 Zone 级 Access 管理。未提供邮箱且已有两个 Access Secret 时保留原认证,
// 不擅自新建账户级应用覆盖它;新用户仍只需输入邮箱即可走完整自动配置。
const preserveAccess = !settings.adminEmail
&& existingSecrets.has('ACCESS_TEAM_DOMAIN') && existingSecrets.has('ACCESS_AUD');
if (!preserveAccess && !settings.adminEmail) {
throw new Error('首次部署需要 ADMIN_EMAIL,请在 Run workflow 输入管理员邮箱,或保存为 Actions 变量。');
}
const accessSecrets = preserveAccess ? {} : await ensureAccess(api, settings, hostname);
console.log(preserveAccess ? '保留已有 Access 认证配置。' : 'Access 应用、身份提供程序与邮箱策略已就绪。');
const database = await ensureDatabase(settings);
const secrets = { ...accessSecrets, ...await prepareEncryptionSecret(api, settings, database, existingSecrets) };
maskSecrets(secrets);
const config = createDeploymentConfig(template, settings, database);
// 临时配置放在仓库根目录,保持 assets、main、migrations_dir 的相对路径语义。
await writeFile(new URL(`../${generatedConfig}`, import.meta.url), stringify(config), 'utf8');
console.log(`部署 Worker ${settings.workerName},使用 D1 ${database.name}(${database.uuid})。`);
await runWrangler(['d1', 'migrations', 'apply', 'DB', '--remote']);
// Secret 只通过标准输入发送,不写临时文件或命令行参数;首次部署由 Wrangler 创建草稿 Worker。
if (Object.keys(secrets).length) await runWrangler(['secret', 'bulk'], JSON.stringify(secrets));
await runWrangler(['deploy']);
// Wrangler 默认保留既有 Secret;部署后再次核对名称,避免把缺少认证的版本当作成功。
const deployedSecrets = await readWorkerSecretNames(api, settings);
for (const name of ['ENCRYPTION_KEY', 'ACCESS_TEAM_DOMAIN', 'ACCESS_AUD']) {
if (!deployedSecrets.has(name)) throw new Error(`部署后缺少 Worker Secret:${name}。`);
}
console.log(`部署完成:https://${hostname}`);
if (process.env.GITHUB_STEP_SUMMARY) {
await appendFile(process.env.GITHUB_STEP_SUMMARY,
`## EdgeSSH 部署完成\n\n入口:https://${hostname}\n\nD1 已迁移,运行时 Secret 已保存在 Cloudflare。后续部署保留原加密密钥。\n`);
}
}
main().catch((error: Error) => {
console.error(error.message);
process.exitCode = 1;
});