feat(build): 冻结 Render Snapshot 物化与 build job
Plan 4 Task 2:build_jobs 生命周期(queued→snapshotting→validating(空)→succeeded) + build_input_snapshots 行(三哈希/主题/config 引用/真实版本列)+ 六文件物化 (site/content/taxonomies/routes/assets/render-manifest——canonicalJson 确定性落盘)。 - render-snapshot.ts:纯物化(db 只读)——published 修订(ADR-010 冻结语义)+ 路由/媒体 ready 集合 → 六文件;bodyHtml URL 重写(剥 host 按 path 与 ready 集合 public_path/source_url 双源比对,命中根相对化、未命中保留+计数);媒体实体路径 三候选解析(快照 storageRoot→仓库存储根→incoming 兜底) - build.service.ts:幂等键 = site+published 集合哈希+主题版本(复用不重跑); repeatable read 单事务一致性读取+落盘+快照行;报告 artifacts/jobs/<id>/reports/ build.json 先于终态落行;解析期失败零 job 行(NOT_PUBLISHED/SITE_NOT_FOUND) - sf.ts + build:create;build.module.ts Nest 薄壳 - tests/integration/build-snapshot.test.ts:10 测试(状态机/六文件面/URL 重写/ 字节确定性/NOT_PUBLISHED/ADR-010 双修订/重写器纯函数四例) Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
+53
-6
@@ -1,13 +1,14 @@
|
||||
#!/usr/bin/env node
|
||||
// sf CLI(Plan 2 Task 7 + Plan 3 Task 1 + Plan 4 Task 1):commander 四命令——
|
||||
// sf CLI(Plan 2 Task 7 + Plan 3 Task 1 + Plan 4 Task 1-2):commander 五命令——
|
||||
// - wxr:probe:只读诊断(任意可读路径;Task 8 直读 cms 用此通道),出 JSON/HTML 报告不入库;
|
||||
// - source:register:调 registerSource 登记 content-addressed 快照(路径白名单校验在服务内);
|
||||
// - import:调 createImportJob 建 import_jobs 并推进生命周期(Task 6:默认编排管道——四家写入器汇流);
|
||||
// - content:publish:调 publishSiteContent 显式发布(dry-run 清单+confirm token → --confirm 执行)。
|
||||
// - content:publish:调 publishSiteContent 显式发布(dry-run 清单+confirm token → --confirm 执行);
|
||||
// - build:create:调 createBuildJob 冻结 Render Snapshot(六文件物化 + build_input_snapshots;dist 是 Task 5)。
|
||||
// 执行机制(Plan 2 评审钉死):根 devDep tsx 直吃 TS 源码(含 apps/api 的 Nest 源码与
|
||||
// packages 的装饰器/TS 源文件),@sf/* 经根/包 tsconfig paths 直连 src——无需预构建 dist。
|
||||
// 注意:CLI 只 import register.service.ts / import.service.ts / publish.service.ts
|
||||
// (零装饰器纯函数文件),不拉起 Nest 模块——反射元数据依赖由此避免;module 文件留给 standalone context。
|
||||
// 注意:CLI 只 import register.service.ts / import.service.ts / publish.service.ts /
|
||||
// build.service.ts(零装饰器纯函数文件),不拉起 Nest 模块——反射元数据依赖由此避免;module 文件留给 standalone context。
|
||||
import { createReadStream } from 'node:fs'
|
||||
import { mkdir, writeFile } from 'node:fs/promises'
|
||||
import { dirname, join, resolve } from 'node:path'
|
||||
@@ -15,6 +16,7 @@ import { fileURLToPath } from 'node:url'
|
||||
import { Command } from 'commander'
|
||||
import { renderReportHtml, runWxrAdapterReport, type WxrAdapterReport } from '@sf/adapter-wp'
|
||||
import { getDb } from '../apps/api/src/db/client.js'
|
||||
import { BuildError, createBuildJob } from '../apps/api/src/modules/build/build.service.js'
|
||||
import {
|
||||
ImportError,
|
||||
createImportJob,
|
||||
@@ -54,7 +56,7 @@ function printProbeSummary(report: WxrAdapterReport): void {
|
||||
const program = new Command()
|
||||
program
|
||||
.name('sf')
|
||||
.description('SiteForge CLI(阶段 3:来源输入登记、WXR 探测与导入 job 生命周期)')
|
||||
.description('SiteForge CLI(阶段 4:来源登记、导入、显式发布与 Render Snapshot 冻结构建)')
|
||||
.version('0.1.0')
|
||||
|
||||
program
|
||||
@@ -297,8 +299,51 @@ program
|
||||
}
|
||||
})
|
||||
|
||||
program
|
||||
.command('build:create')
|
||||
.description('冻结 Render Snapshot:物化六文件(site/content/taxonomies/routes/assets/render-manifest)+ build_input_snapshots 行;dist 产物是 Task 5')
|
||||
.requiredOption('--site <key>', '站点 key')
|
||||
.option('--key <key>', '幂等键(缺省 = site+published 集合哈希+主题版本 自动派生;重复提交同 key 复用既有 job 不重跑)')
|
||||
.option('--artifacts <dir>', '产物根目录(缺省 repoRoot/artifacts)')
|
||||
.action(async (opts: { site: string; key?: string; artifacts?: string }) => {
|
||||
// getDb 走 DATABASE_URL_TEST ?? DATABASE_URL 回退链——临时库冒烟只需 env 覆盖
|
||||
const db = getDb()
|
||||
// exactOptionalPropertyTypes:缺席键不传 undefined 值(与 source:register 同款)
|
||||
const input: { siteKey: string; idempotencyKey?: string; artifactsRoot?: string } = { siteKey: opts.site }
|
||||
if (opts.key !== undefined) input.idempotencyKey = opts.key
|
||||
if (opts.artifacts !== undefined) input.artifactsRoot = opts.artifacts
|
||||
try {
|
||||
const result = await createBuildJob(db, input)
|
||||
console.log(
|
||||
`[build:create] job=${result.jobId} status=${result.status}${result.reused ? '(复用既有 job,未重跑)' : ''}`,
|
||||
)
|
||||
console.log(` site=${result.siteKey} input: ${result.inputDir}`)
|
||||
if (!result.reused) {
|
||||
const c = result.counts!
|
||||
console.log(
|
||||
` counts: contents=${c.contents} taxonomies=${c.taxonomies} routes=${c.routes} assets=${c.assets}`,
|
||||
)
|
||||
console.log(
|
||||
` urlRewrite: rewritten=${result.urlRewrite!.rewritten} unresolved=${result.urlRewrite!.unresolved}`,
|
||||
)
|
||||
if (result.issues!.length > 0) {
|
||||
console.log(` issues (${result.issues!.length}):`)
|
||||
for (const issue of result.issues!) console.log(` - ${issue}`)
|
||||
}
|
||||
}
|
||||
console.log(
|
||||
` hashes: content=${result.sourceHashes.content.slice(0, 12)}… route=${result.sourceHashes.route.slice(0, 12)}… asset=${result.sourceHashes.asset.slice(0, 12)}…`,
|
||||
)
|
||||
console.log(` report: ${result.reportPath}`)
|
||||
console.log(` total ${result.durationMs.toFixed(1)}ms`)
|
||||
if (result.status === 'failed') process.exitCode = 1
|
||||
} finally {
|
||||
await (db as { $client: { end: () => Promise<void> } }).$client.end()
|
||||
}
|
||||
})
|
||||
|
||||
// parseAsync:action 内抛错走此 catch(commander 默认会 process.exit(1) 并打印堆栈,
|
||||
// 这里收敛为单行错误信息 + exitCode——SourceRegisterError/ImportError/PublishError 打 code,其余原样呈现)
|
||||
// 这里收敛为单行错误信息 + exitCode——SourceRegisterError/ImportError/PublishError/BuildError 打 code,其余原样呈现)
|
||||
program.parseAsync(process.argv).catch((err: unknown) => {
|
||||
if (err instanceof SourceRegisterError) {
|
||||
console.error(`[sf] ${err.code}: ${err.message}`)
|
||||
@@ -309,6 +354,8 @@ program.parseAsync(process.argv).catch((err: unknown) => {
|
||||
console.error(`[sf] ${err.code}: ${err.message}`)
|
||||
} else if (err instanceof PublishError) {
|
||||
console.error(`[sf] ${err.code}: ${err.message}`)
|
||||
} else if (err instanceof BuildError) {
|
||||
console.error(`[sf] ${err.code}: ${err.message}`)
|
||||
} else {
|
||||
console.error(`[sf] ${err instanceof Error ? err.message : String(err)}`)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user