guanghulab/scripts/preflight/check-secrets.js

218 lines
7.9 KiB
JavaScript
Raw Permalink Normal View History

#!/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);
}