218 lines
7.9 KiB
JavaScript
Executable File
218 lines
7.9 KiB
JavaScript
Executable File
#!/usr/bin/env node
|
|
/**
|
|
* ════════════════════════════════════════════════════════════════
|
|
* 启动前最小密钥校验 · check-secrets.js
|
|
* Sovereign: TCS-0002∞ · 国作登字-2026-A-00037559
|
|
* 守护: 铸渊 · ICE-GL-ZY001
|
|
* ════════════════════════════════════════════════════════════════
|
|
*
|
|
* 给非技术触发者(冰朔/Awen)用. 在 workflow 跑实际部署之前先跑一遍
|
|
* 这个脚本, 它会读 secrets-manifest.json, 对照当前注入到环境变量
|
|
* 里的 secrets, 输出『中文人话 + 是否够 + 不够还差什么』的清单.
|
|
*
|
|
* 用法:
|
|
* node scripts/preflight/check-secrets.js \
|
|
* --workflow lighthouse-cn-deploy \
|
|
* --stage bootstrap \
|
|
* --target main
|
|
*
|
|
* 退出码:
|
|
* 0 全部 required 都满足, 可以继续
|
|
* 1 有 required 缺失, 必须先补完才能继续 (workflow 应当中止)
|
|
* 2 manifest / 入参出错
|
|
*
|
|
* 安全: 只校验 secret 是否『非空』. 永远不打印 secret 的值, 只打印
|
|
* 名称和是否设置. 真值不会进 stdout / stderr.
|
|
*/
|
|
|
|
'use strict';
|
|
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
|
|
// ─── CLI 解析 ──────────────────────────────────────────────────
|
|
function parseArgs(argv) {
|
|
const out = {};
|
|
for (let i = 0; i < argv.length; i++) {
|
|
const a = argv[i];
|
|
if (a.startsWith('--')) {
|
|
const k = a.slice(2);
|
|
const v = argv[i + 1] && !argv[i + 1].startsWith('--') ? argv[++i] : 'true';
|
|
out[k] = v;
|
|
}
|
|
}
|
|
return out;
|
|
}
|
|
|
|
const args = parseArgs(process.argv.slice(2));
|
|
const WORKFLOW = args.workflow || 'lighthouse-cn-deploy';
|
|
const STAGE = args.stage || 'bootstrap';
|
|
const TARGET = args.target || 'main';
|
|
const FAIL_FAST = args['fail-fast'] !== 'false'; // 默认 fail-fast
|
|
|
|
const MANIFEST_PATH = path.resolve(__dirname, 'secrets-manifest.json');
|
|
|
|
// ─── 读 manifest ──────────────────────────────────────────────
|
|
let manifest;
|
|
try {
|
|
manifest = JSON.parse(fs.readFileSync(MANIFEST_PATH, 'utf8'));
|
|
} catch (err) {
|
|
console.error(`[预检] ❌ 读不到 manifest: ${MANIFEST_PATH}`);
|
|
console.error(` ${err.message}`);
|
|
process.exit(2);
|
|
}
|
|
|
|
const wf = manifest.workflows && manifest.workflows[WORKFLOW];
|
|
if (!wf) {
|
|
console.error(`[预检] ❌ manifest 里没有 workflow: ${WORKFLOW}`);
|
|
console.error(` 可选: ${Object.keys(manifest.workflows || {}).join(', ')}`);
|
|
process.exit(2);
|
|
}
|
|
|
|
// ─── 分类校验 ─────────────────────────────────────────────────
|
|
const env = process.env;
|
|
|
|
const buckets = {
|
|
ok: [], // 已配置, OK
|
|
missing_hard: [], // required 但缺, 必须立即补
|
|
missing_stage: [], // 当前 stage 必须补
|
|
missing_target: [],// 当前 target 必须补
|
|
missing_soft: [], // optional 缺失, 仅提示
|
|
warned: [], // 强校验失败 (比如密码太短)
|
|
};
|
|
|
|
function classify(secret) {
|
|
const v = env[secret.name];
|
|
const present = typeof v === 'string' && v.length > 0;
|
|
|
|
// 强校验
|
|
if (present && secret['强校验']) {
|
|
if (secret['强校验'] === 'length>=24' && v.length < 24) {
|
|
buckets.warned.push({
|
|
...secret,
|
|
warn: `值长度 ${v.length} < 24 (强烈建议 ≥24 位)`,
|
|
});
|
|
return;
|
|
}
|
|
}
|
|
|
|
switch (secret.level) {
|
|
case 'required':
|
|
if (present) buckets.ok.push(secret);
|
|
else buckets.missing_hard.push(secret);
|
|
break;
|
|
case 'required_on_stage':
|
|
if ((secret.stages || []).includes(STAGE)) {
|
|
if (present) buckets.ok.push(secret);
|
|
else buckets.missing_stage.push(secret);
|
|
} else {
|
|
if (present) buckets.ok.push(secret);
|
|
}
|
|
break;
|
|
case 'required_on_target':
|
|
if ((secret.targets || []).includes(TARGET)) {
|
|
if (present) buckets.ok.push(secret);
|
|
else buckets.missing_target.push(secret);
|
|
} else {
|
|
if (present) buckets.ok.push(secret);
|
|
}
|
|
break;
|
|
case 'optional':
|
|
default:
|
|
if (present) buckets.ok.push(secret);
|
|
else buckets.missing_soft.push(secret);
|
|
break;
|
|
}
|
|
}
|
|
|
|
(wf.secrets || []).forEach(classify);
|
|
|
|
// ─── 输出报告 ─────────────────────────────────────────────────
|
|
const line = '═'.repeat(64);
|
|
console.log(line);
|
|
console.log(` 启动前密钥预检 · workflow=${WORKFLOW}`);
|
|
console.log(` stage=${STAGE} target=${TARGET}`);
|
|
console.log(` manifest=${path.relative(process.cwd(), MANIFEST_PATH)}`);
|
|
console.log(line);
|
|
|
|
function printSecret(s, prefix) {
|
|
const tail = s['用途'] ? ` · ${s['用途']}` : '';
|
|
console.log(` ${prefix} ${s.name}${tail}`);
|
|
if (s['如何配置'] && (prefix.includes('❌') || prefix.includes('⚠️'))) {
|
|
console.log(` ↳ 怎么配: ${s['如何配置']}`);
|
|
}
|
|
if (s.where && prefix.includes('❌')) {
|
|
console.log(` ↳ 哪里取: ${s.where}`);
|
|
}
|
|
if (s.default && prefix.includes('⚠️') && !s['如何配置']) {
|
|
console.log(` ↳ 没配会用默认值: ${s.default}`);
|
|
}
|
|
}
|
|
|
|
console.log('');
|
|
console.log(`§ 已配置 (${buckets.ok.length})`);
|
|
buckets.ok.forEach((s) => printSecret(s, '✅'));
|
|
|
|
if (buckets.missing_hard.length) {
|
|
console.log('');
|
|
console.log(`§ 必须补 · 缺这些直接停车 (${buckets.missing_hard.length})`);
|
|
buckets.missing_hard.forEach((s) => printSecret(s, '❌ 必须'));
|
|
}
|
|
|
|
if (buckets.missing_stage.length) {
|
|
console.log('');
|
|
console.log(`§ 当前阶段(${STAGE})必须补 (${buckets.missing_stage.length})`);
|
|
buckets.missing_stage.forEach((s) => printSecret(s, '❌ 阶段必须'));
|
|
}
|
|
|
|
if (buckets.missing_target.length) {
|
|
console.log('');
|
|
console.log(`§ 当前目标(${TARGET})必须补 (${buckets.missing_target.length})`);
|
|
buckets.missing_target.forEach((s) => printSecret(s, '❌ 目标必须'));
|
|
}
|
|
|
|
if (buckets.warned.length) {
|
|
console.log('');
|
|
console.log(`§ 强校验告警 (${buckets.warned.length})`);
|
|
buckets.warned.forEach((s) => {
|
|
console.log(` ⚠️ ${s.name} · ${s.warn}`);
|
|
});
|
|
}
|
|
|
|
if (buckets.missing_soft.length) {
|
|
console.log('');
|
|
console.log(`§ 可以缓 · 不影响主流程 (${buckets.missing_soft.length})`);
|
|
buckets.missing_soft.forEach((s) => printSecret(s, '⚠️ 可缓'));
|
|
}
|
|
|
|
// ─── 结论 ─────────────────────────────────────────────────────
|
|
const hardMiss =
|
|
buckets.missing_hard.length +
|
|
buckets.missing_stage.length +
|
|
buckets.missing_target.length;
|
|
|
|
console.log('');
|
|
console.log(line);
|
|
if (hardMiss === 0) {
|
|
console.log(` [OK] 预检通过 · 已配置 ${buckets.ok.length} 个密钥, 可继续部署`);
|
|
if (buckets.warned.length) {
|
|
console.log(` (${buckets.warned.length} 个告警, 不阻塞)`);
|
|
}
|
|
if (buckets.missing_soft.length) {
|
|
console.log(` (${buckets.missing_soft.length} 个可选项缺, 走默认/降级)`);
|
|
}
|
|
console.log(line);
|
|
process.exit(0);
|
|
} else {
|
|
console.log(` [STOP] 预检不通过 · 还差 ${hardMiss} 个必须密钥`);
|
|
console.log('');
|
|
console.log(' 下一步给 Awen / 冰朔:');
|
|
console.log(' 1. 打开 GitHub 仓库 → Settings → Secrets and variables → Actions');
|
|
console.log(' 2. 把上面 ❌ 标的密钥逐个 New repository secret 填进去');
|
|
console.log(' 3. 回到 Actions → 重新 Run workflow');
|
|
console.log(' 系统不会继续往下跑 — 不会误触发, 不会改坏服务器.');
|
|
console.log(line);
|
|
if (FAIL_FAST) process.exit(1);
|
|
process.exit(0);
|
|
}
|