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
+48
View File
@@ -0,0 +1,48 @@
import { describe, it, expect } from 'vitest'
import { generateVerifyInputs } from '@/lib/exporter/verify.serialize'
// helper to b64
function toB64(u8: Uint8Array) {
if (typeof Buffer !== 'undefined') return Buffer.from(u8).toString('base64')
throw new Error('Buffer not available')
}
describe('verify.serialize (from folder maps)', () => {
it('collects assets.integrity.index.json into extras and assets/* into assets map', async () => {
// virtual FS
const files: Record<string, Uint8Array> = {
'/artifacts/export/assets.integrity.index.json': new TextEncoder().encode(JSON.stringify({ entries: [], bundleHash: 'sha256-47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=' })),
'/artifacts/export/index.html': new TextEncoder().encode('<!doctype html>'),
'/artifacts/export/assets/img/a.png': new Uint8Array([1,2,3]),
'/artifacts/export/assets/fonts/f.woff2': new Uint8Array([4,5,6,7]),
}
const listDir = async (dir: string): Promise<string[]> => {
// return child paths (recursive listing simulated by filtering)
return Object.keys(files).filter((p) => p.startsWith(dir)).map((p) => p)
}
const readFile = async (p: string) => {
const v = files[p]
if (!v) throw new Error('not found: ' + p)
return v
}
const { extras, assets } = await generateVerifyInputs('/artifacts/export', listDir, readFile)
// extras must have only the integrity index
expect(Object.keys(extras)).toEqual(['assets.integrity.index.json'])
expect(extras['assets.integrity.index.json']).toBe(toB64(files['/artifacts/export/assets.integrity.index.json']))
// assets must include assets/* files only
expect(Object.keys(assets).sort()).toEqual(['assets/fonts/f.woff2','assets/img/a.png'])
expect(assets['assets/img/a.png']).toBe(toB64(files['/artifacts/export/assets/img/a.png']))
})
it('ignores non-asset files and missing integrity index', async () => {
const files: Record<string, Uint8Array> = {
'/artifacts/export/index.html': new TextEncoder().encode('<!doctype html>'),
'/artifacts/export/assets/img/a.png': new Uint8Array([9,9]),
}
const listDir = async (dir: string) => Object.keys(files).filter((p) => p.startsWith(dir))
const readFile = async (p: string) => files[p]!
const { extras, assets } = await generateVerifyInputs('/artifacts/export', listDir, readFile)
expect(Object.keys(extras)).toEqual([])
expect(Object.keys(assets)).toEqual(['assets/img/a.png'])
})
})
+47
View File
@@ -0,0 +1,47 @@
export type ListDir = (dir: string) => Promise<string[]>
export type ReadFile = (path: string) => Promise<Uint8Array>
function toBase64(u8: Uint8Array): string {
if (typeof Buffer !== 'undefined') return Buffer.from(u8).toString('base64')
throw new Error('Buffer not available')
}
// Generate inputs for verify CLI from a folder tree.
// - root: export folder (contains assets.integrity.index.json and assets/*)
// - listDir: returns list of file paths under root (can be recursive listing)
// - readFile: reads a file into bytes
export async function generateVerifyInputs(
root: string,
listDir: ListDir,
readFile: ReadFile
): Promise<{ extras: Record<string, string>; assets: Record<string, string> }> {
const all = await listDir(root)
const norm = (p: string) => p.replace(/\\/g, '/')
const rootNorm = norm(root).replace(/\/$/, '')
const rel = (p: string) => {
const pn = norm(p)
return pn.startsWith(rootNorm + '/') ? pn.slice(rootNorm.length + 1) : pn
}
const extras: Record<string, string> = {}
const assets: Record<string, string> = {}
// assets.integrity.index.json at root
const integrityPath = all.find((p) => rel(p) === 'assets.integrity.index.json')
if (integrityPath) {
const bytes = await readFile(integrityPath)
extras['assets.integrity.index.json'] = toBase64(bytes)
}
// collect assets/**/*
for (const abs of all) {
const r = rel(abs)
if (!r.startsWith('assets/')) continue
// skip directories: best-effort by checking trailing slash
if (r.endsWith('/')) continue
const bytes = await readFile(abs)
assets[r] = toBase64(bytes)
}
return { extras, assets }
}