ci: 무결성 검증 통합(권장 플로우)
Auto PR / open-pr (push) Successful in 16s
Auto Label PR / add-automerge-label (pull_request) Successful in 6s
CI / test (pull_request) Successful in 1m13s

- scripts/make-verify-json.js: 산출물 폴더 → extras/assets JSON 생성
- scripts/verify.js: JSON 입력으로 무결성 검증(종료코드 반영)
- package.json: verify 스크립트 추가
- .gitea/workflows/ci.yml: ./artifacts/export 존재 시 검증 스텝 실행
This commit is contained in:
2025-11-16 20:52:18 +09:00
parent 86155c9fd3
commit e32d59ca44
6 changed files with 270 additions and 1 deletions
+82
View File
@@ -0,0 +1,82 @@
#!/usr/bin/env node
/*
Generate verify inputs from an export folder
Usage: node scripts/make-verify-json.js --root <export_folder> --extras <out_extras.json> --assets <out_assets.json>
*/
const fs = require('fs');
const fsp = require('fs/promises');
const path = require('path');
function parseArgs(argv) {
const out = { root: '', extras: '', assets: '' };
for (let i = 2; i < argv.length; i++) {
const a = argv[i];
if (a === '--root') out.root = argv[++i];
else if (a === '--extras') out.extras = argv[++i];
else if (a === '--assets') out.assets = argv[++i];
}
return out;
}
function toB64(buf) { return Buffer.from(buf).toString('base64'); }
async function readFileB64(p) { const b = await fsp.readFile(p); return toB64(b); }
async function walk(dir) {
const out = [];
const entries = await fsp.readdir(dir, { withFileTypes: true });
for (const ent of entries) {
const abs = path.join(dir, ent.name);
if (ent.isDirectory()) {
const sub = await walk(abs);
out.push(...sub);
} else if (ent.isFile()) {
out.push(abs);
}
}
return out;
}
(async () => {
try {
const { root, extras, assets } = parseArgs(process.argv);
if (!root || !extras || !assets) {
console.log('Usage: node scripts/make-verify-json.js --root <export_folder> --extras <out_extras.json> --assets <out_assets.json>');
process.exit(2);
}
const rootAbs = path.resolve(process.cwd(), root);
const files = await walk(rootAbs);
const norm = (p) => p.split(path.sep).join('/');
const rootNorm = norm(rootAbs).replace(/\/$/, '');
const rel = (p) => {
const pn = norm(p);
return pn.startsWith(rootNorm + '/') ? pn.slice(rootNorm.length + 1) : pn;
};
const extrasMap = {};
const assetsMap = {};
// integrity index
const idx = files.find((p) => rel(p) === 'assets.integrity.index.json');
if (idx) {
extrasMap['assets.integrity.index.json'] = await readFileB64(idx);
}
// assets/**/*
for (const abs of files) {
const r = rel(abs);
if (!r.startsWith('assets/')) continue;
const b64 = await readFileB64(abs);
assetsMap[r] = b64;
}
await fsp.mkdir(path.dirname(path.resolve(process.cwd(), extras)), { recursive: true });
await fsp.mkdir(path.dirname(path.resolve(process.cwd(), assets)), { recursive: true });
await fsp.writeFile(extras, JSON.stringify(extrasMap));
await fsp.writeFile(assets, JSON.stringify(assetsMap));
console.log(`Wrote ${extras} and ${assets}`);
} catch (e) {
console.error(e && e.stack || String(e));
process.exit(1);
}
})();
+78
View File
@@ -0,0 +1,78 @@
#!/usr/bin/env node
/*
Verify bundle integrity using extras/assets JSON maps.
Usage: node scripts/verify.js --extras <extras.json> --assets <assets.json>
*/
const fs = require('fs/promises');
const crypto = require('crypto');
function parseArgs(argv) {
const out = { extras: '', assets: '' };
for (let i = 2; i < argv.length; i++) {
const a = argv[i];
if (a === '--extras') out.extras = argv[++i];
else if (a === '--assets') out.assets = argv[++i];
}
return out;
}
function fromB64(b64) { return Buffer.from(b64, 'base64'); }
function sha256b64(buf) { return crypto.createHash('sha256').update(buf).digest('base64'); }
(async () => {
try {
const { extras, assets } = parseArgs(process.argv);
if (!extras || !assets) {
console.log('Usage: node scripts/verify.js --extras <extras.json> --assets <assets.json>');
process.exit(2);
}
const extrasJson = await fs.readFile(extras, 'utf-8');
const assetsJson = await fs.readFile(assets, 'utf-8');
const extrasMap = JSON.parse(extrasJson);
const assetsMap = JSON.parse(assetsJson);
const result = { ok: true, errors: [], summary: { total: 0, missing: 0, integrityMismatches: 0, sizeMismatches: 0, bundleHashMismatch: false } };
if (!Object.prototype.hasOwnProperty.call(extrasMap, 'assets.integrity.index.json')) {
result.ok = false; result.errors.push('Missing assets.integrity.index.json in extras');
console.log(JSON.stringify(result, null, 2)); process.exit(1);
}
let parsed;
try {
parsed = JSON.parse(fromB64(extrasMap['assets.integrity.index.json']).toString('utf-8'));
} catch {
result.ok = false; result.errors.push('Invalid assets.integrity.index.json JSON');
console.log(JSON.stringify(result, null, 2)); process.exit(1);
}
const recomputedLines = [];
let missing = 0, integrityMismatches = 0, sizeMismatches = 0;
for (const e of parsed.entries) {
const b64 = assetsMap[e.path];
if (!b64) { result.errors.push(`Missing asset for path in integrity index: ${e.path}`); missing++; continue; }
const buf = fromB64(b64);
if (typeof e.size === 'number' && e.size !== buf.length) {
result.errors.push(`Size mismatch for ${e.path}: expected ${e.size}, got ${buf.length}`); sizeMismatches++; }
const integ = 'sha256-' + sha256b64(buf);
if (integ !== e.integrity) { result.errors.push(`Integrity mismatch for ${e.path}`); integrityMismatches++; }
recomputedLines.push(e.path); recomputedLines.push(integ);
}
const bundle = 'sha256-' + sha256b64(Buffer.from(recomputedLines.join('\n'), 'utf-8'));
const bundleHashMismatch = bundle !== parsed.bundleHash;
if (bundleHashMismatch) result.errors.push('BundleHash mismatch');
result.summary.total = parsed.entries.length;
result.summary.missing = missing;
result.summary.integrityMismatches = integrityMismatches;
result.summary.sizeMismatches = sizeMismatches;
result.summary.bundleHashMismatch = bundleHashMismatch;
result.ok = result.errors.length === 0;
console.log(JSON.stringify(result, null, 2));
process.exit(result.ok ? 0 : 1);
} catch (e) {
console.error(e && e.stack || String(e));
process.exit(1);
}
})();